0%

Android Actionbar Up vs 返回键Back

前言

在开发小App的时候,用到了Android中Support Actionbar并且为了方便用户返回上层Activity,因此开启了Up键。在本人的认知中,原先认为Up键与手势返回键或者系统导航栏都能够达到相同的返回上一级Activity的效果,即两者的内部函数调用逻辑和达到的效果是统一的。但在真实的开发过程中,也证明了本人的理解是错误的,正确的理解应为:ActionBar的Up键会直接重新创建与之相关联的Parent Activity,而系统返回键则会重新唤醒返回栈中的Activity,而非重新创建。

之所以会对这两者产生较大的理解错误,原因在于两个Activity相互切换的过程中,所使用的Live Data出现了重复执行的情况。假设我们现在有两个Activity A和B,现在有以下切换流程:A->B->A,那么,Activity A中的Live Data应该只会有两种情况进行更新逻辑代码的执行:

  • Activity A第一次创建时

  • Activity B产生了数据对Activity A中的Live Data数据进行了更新

但是,在本人的实验中发现即使Activity B中没有执行任何操作,使用Up键返回Activity A同样会触发A中的Live Data更新逻辑代码。因此,这证明了Up键的返回逻辑与常规的系统返回键不同,本文也以此对两者的区别进行阐述与分析。

生命周期的不同

由于在发现问题的初期,本人并不确定问题的原因,因此,我们先从两个Activity的生命周期出发进行问题的排查,通过对Up键和Back键进行返回操作,记录两者返回Activity A时,A的生命周期函数调用,就可以确定问题。

首先是Back键,在执行A->B->A这两次操作的时候,生命周期的调用过程为:onCreate()->onStart()->onResume()->onPause()->onStop()->从B返回A->onRestart()->onStart()->onResume()

其次是Up键,生命周期的调用过程为:onCreate()->onStart()->onResume()->onPause()->onStop()->从B返回A->onDestroy()->onCreate()->onStart()->onResume()

可以看出,Up键调用时会让原先存在于返回栈中的Activity A销毁,而后重新创建。而返回键则是直接调用返回栈中存在的Activity A,并非销毁原有的A重新创建。

系统返回键Back的调用逻辑

当用户按下系统返回键或者订制UI的返回手势时,当前Activity会调用ComponentActivity类中的onBackPressed()方法,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* Called when the activity has detected the user's press of the back
* key. The {@link #getOnBackPressedDispatcher() OnBackPressedDispatcher} will be given a
* chance to handle the back button before the default behavior of
* {@link android.app.Activity#onBackPressed()} is invoked.
*
* @see #getOnBackPressedDispatcher()
*/
@Override
@MainThread
public void onBackPressed() {
mOnBackPressedDispatcher.onBackPressed();
}

从注释中能够清晰的看出这一函数的作用,提供给开发者一个处理默认返回动作逻辑的机会,也就是在返回前,开发者可以在这一函数中进行相关的代码逻辑编写,但需要注意,这一操作是在主线程中执行的。

这里由于本人没有特殊的需求,所以就没有重载这一函数,那么按照对应的逻辑,用户的返回动作会由mOnBackPressedDispatcher调度器来完成返回动作的分发,在调度器所在的类OnBackPressedDispatcher可以看到如下关于调度器的解释:

1
2
3
4
5
6
/**
* Dispatcher that can be used to register {@link OnBackPressedCallback} instances for handling
* the {@link ComponentActivity#onBackPressed()} callback via composition.
*/

public final class OnBackPressedDispatcher { }

调度器主要用于处理注册了返回回调函数组件的实例,并提供给这些组件进行回调函数的分发。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* Trigger a call to the currently added {@link OnBackPressedCallback callbacks} in reverse
* order in which they were added. Only if the most recently added callback is not
* {@link OnBackPressedCallback#isEnabled() enabled}
* will any previously added callback be called.
* <p>
* It is strongly recommended to call {@link #hasEnabledCallbacks()} prior to calling
* this method to determine if there are any enabled callbacks that will be triggered
* by this method as calling this method.
*/
@MainThread
public void onBackPressed() {
Iterator<OnBackPressedCallback> iterator =
mOnBackPressedCallbacks.descendingIterator();
while (iterator.hasNext()) {
OnBackPressedCallback callback = iterator.next();
if (callback.isEnabled()) {
callback.handleOnBackPressed();
return;
}
}
// 当没有订阅事件时,则会直接执行当前Activity中默认的返回逻辑
if (mFallbackOnBackPressed != null) {
mFallbackOnBackPressed.run();
}
}

在调度器执行onBackPressed函数时,它会对已经注册过回调事件的实例进行迭代查找,当找到组件能够处理回调时间时,则会将返回实践交由当前回调事件所在的组件来继续处理返回操作。

之所以这样做,在调度器开头的注释中也有给出对应的使用场景,即在Fragment中,如果需要对用户返回动作进行处理,则可以添加回调事件从而达到这一目的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* public class FormEntryFragment extends Fragment {
* {@literal @}Override
* public void onAttach({@literal @}NonNull Context context) {
* super.onAttach(context);
* OnBackPressedCallback callback = new OnBackPressedCallback(
* true // default to enabled
* ) {
* {@literal @}Override
* public void handleOnBackPressed() {
* showAreYouSureDialog();
* }
* };
* requireActivity().getOnBackPressedDispatcher().addCallback(
* this, // LifecycleOwner
* callback);
* }
* }
*/

上述的内容主要阐述了当用户按下系统返回键后,我们需要进行返回前逻辑操作时的主要处理方式。而当我们没有这样的需求时,Activity又是如何进行返回操作的呢?相信看到调度器中的mFallbackOnBackPressed变量的读者一定会有疑惑,这一个Runnable对象是用于创建一个新的返回键分发器所设计的,那么又是谁持有了这个分发器对象呢?

首先,我们可以确定一下mFallbackOnBackPressed的生成函数:

1
2
3
4
5
6
7
8
9
10
/**
* Create a new OnBackPressedDispatcher that dispatches System back button pressed events
* to one or more {@link OnBackPressedCallback} instances.
*
* @param fallbackOnBackPressed The Runnable that should be triggered if
* {@link #onBackPressed()} is called when {@link #hasEnabledCallbacks()} returns false.
*/
public OnBackPressedDispatcher(@Nullable Runnable fallbackOnBackPressed) {
mFallbackOnBackPressed = fallbackOnBackPressed;
}

可以看到mFallbackOnBackPressed是作为调度器的构造函数传入进来的,所以我们可以肯定传入这一对象的一定是创建了调度器的组件,那么,一般情况下都是当前页面对应的Activity。通过ComponentActivity的源码可以看到Activity生成调度器的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ComponentActivity extends androidx.core.app.ComponentActivity implements
LifecycleOwner,
ViewModelStoreOwner,
SavedStateRegistryOwner,
OnBackPressedDispatcherOwner {

private final OnBackPressedDispatcher mOnBackPressedDispatcher =
new OnBackPressedDispatcher(new Runnable() {
@Override
public void run() {
// 传入了父类Activity的处理逻辑给了调度器
ComponentActivity.super.onBackPressed();
}
});
}

当我们理解了整个返回事件的处理思路后,最后我们来完善对于Activity默认处理返回事件逻辑的理解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* Called when the activity has detected the user's press of the back
* key. The default implementation simply finishes the current activity,
* but you can override this to do whatever you want.
*/
public void onBackPressed() {
if (mActionBar != null && mActionBar.collapseActionView()) {
return;
}

FragmentManager fragmentManager = mFragments.getFragmentManager();

if (!fragmentManager.isStateSaved() && fragmentManager.popBackStackImmediate()) {
return;
}
if (!isTaskRoot()) {
// If the activity is not the root of the task, allow finish to proceed normally.
finishAfterTransition();
return;
}
try {
// Inform activity task manager that the activity received a back press
// while at the root of the task. This call allows ActivityTaskManager
// to intercept or defer finishing.
ActivityTaskManager.getService().onBackPressedOnTaskRoot(mToken,
new RequestFinishCallback(new WeakReference<>(this)));
} catch (RemoteException e) {
finishAfterTransition();
}
}

Activity类中我们可以找到Activity关闭的逻辑,如上代码所示,系统的返回函数考虑了Fragment和普通Activity两种情况,并且,对当前页是否为App首页进行了区分,常规的非首页Activity会使用finishAfterTransition()进行Activity的finish()操作,首页则会通知Activity的任务管理器进行当前App的拦截或关闭操作。

由此,这一个连环的返回事件调用终于被我们所理清,如果调度器中有组件注册了回调事件来处理返回动作所对应的逻辑,那么,返回动作将会由这一回调事件所对应的函数来进行消费;如果没有注册的组件进行返回事件的处理,系统会自动将返回事件交给当前Activity来进行处理。

上述便是整个系统返回键触发后的整个系统操作流程。接下来,我们将对Up键的逻辑进行整理,以便于我们对二者的特点进行掌握。

ActionBar中的Up键调用逻辑

与系统返回键的触发逻辑不同,当用户点击了ActionBar中的Up键时,系统会默认调用supportNavigateUpTo()方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* Navigate from sourceActivity to the activity specified by upIntent, finishing sourceActivity
* in the process. upIntent will have the flag {@link android.content.Intent#FLAG_ACTIVITY_CLEAR_TOP} set
* by this method, along with any others required for proper up navigation as outlined
* in the Android Design Guide.
*
* <p>This method should be used when performing up navigation from within the same task
* as the destination. If up navigation should cross tasks in some cases, see
* {@link #supportShouldUpRecreateTask(android.content.Intent)}.</p>
*
* @param upIntent An intent representing the target destination for up navigation
*/
public void supportNavigateUpTo(@NonNull Intent upIntent) {
NavUtils.navigateUpTo(this, upIntent);
}

通过注释的描述解开了我自己的疑问,Up键的设计与系统返回键存在着本质上的不同,Up键的跳转并非上一个Activity,它类似于站点导航的作用,我们可以通过自行定义导航关系来达到跳跃返回的功能,但也需要注意遵循对应的设计原则。

在源码中,可以清晰的看出使用Up键返回会携带者Intent对象往NavUtils走。在注释中,也解释了这个Intent的作用——Up键所指向的目的地(目标Activity)。

在进入到NavUtils.navigateUpTo()方法后,可以看到Up键会启动目标Activity,并且将当前Activity之上的所有Activity都清空,使目标Activity存在于顶部。例如,当前的返回栈中有ABCD这四个Activity对象,当Up键执行创建B实例后,会将CD弹栈,并且重新创建B对象,这也就是FLAG_ACTIVITY_CLEAR_TOP的作用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* Navigate from sourceActivity to the activity specified by upIntent, finishing sourceActivity
* in the process. upIntent will have the flag {@link Intent#FLAG_ACTIVITY_CLEAR_TOP} set
* by this method, along with any others required for proper up navigation as outlined
* in the Android Design Guide.
*
* <p>This method should be used when performing up navigation from within the same task
* as the destination. If up navigation should cross tasks in some cases, see
* {@link #shouldUpRecreateTask(Activity, Intent)}.</p>
*
* @param sourceActivity The current activity from which the user is attempting to navigate up
* @param upIntent An intent representing the target destination for up navigation
*/
public static void navigateUpTo(@NonNull Activity sourceActivity, @NonNull Intent upIntent) {
if (Build.VERSION.SDK_INT >= 16) {
sourceActivity.navigateUpTo(upIntent);
} else {
upIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
sourceActivity.startActivity(upIntent);
sourceActivity.finish();
}
}

相比于Back键,Up键的调用逻辑清晰且简单。这也解释了为何使用Up键时会存在Live Data重新执行更新逻辑的原因。

总结

系统返回键的功能更为强大,同时,其处理逻辑也更为复杂。而Up键则能够更快速的构建App中的路由导航功能,帮助用户快速从众多不重要的Activity返回主要的功能页面,减少用户的操作。二者重要的区别在于:

  • 系统返回键Back在默认情况下返回上一个Activity不会重新创建对象,而Up键则会默认创建新对象,并且会清空目标Activity上的所有Activity对象。

  • 系统返回键的功能更强大,对于Fragment处理返回情况的处理更有帮助,而Up键更多的作用在于锚定App中重要的功能页面,并完成页面间的导航关系,可以理解为前端页面中的面包屑组件。