使用Navigation返回时Fragment重走生命周期?原因
随着Jetpack系列框架的市场认可度越来越高,使用Navigation框架用单个Activity+多个Fragment开发一个app又一次成为了可能,但是在使用Navigation的时候,总是会出现一些问题,比如FragmentA打开了FragmentB,然后再返回的时候,FragmentA重走了生命周期,很多时候这不是我们想要的结果,为什么会出现这样的问题又如何去解决这个问题?今天就从源码层面探究一下。
首先,我创建了一个非常简单的NavGraph,如图所示:
NavGraph
这两个Fragment的功能非常简单,SourceFragment中只有一个按钮,用来点击跳转到TargetFragment,而TargetFargment中没有任何逻辑。
然后在修改容器Activity的资源文件:
<androidx.constraintlayout.widget.ConstraintLayout
...>
<fragment
android:id="@+id/container"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="@navigation/navigation_main" />
</androidx.constraintlayout.widget.ConstraintLayout>
这样就把一个简单的Navigation跳转的Demo做好了。
然后我们看下SourceFragment从初次加载到打开TargetFragment再返回到SourceFragment这三个阶段生命周期的变化。
第一阶段,加载SourceFragment:
onAttach -> onCreateView -> onViewCreated -> onStart -> onResume
这个阶段没有问题,和使用普通Fragment的时候生命周期变化一致。
第二阶段:打开TargetFragment:
onPause -> onStop
SourceFragment在不可见的时候只是进入了onStop,并没有走onDestory。到目前为止,看起来一切正常。
第三阶段:返回到SourceFragment
onCreateView -> onViewCreated -> onStart -> onResume
我们惊奇的发现,这个阶段的生命周期和我们预想的并不一致,重新返回到SourceFragment重走了onAttach外的生命周期,重走生命周期不仅意味着要消耗额外的资源对SourceFragment进行重新渲染,也降低了用户体验,那么接下来就进入这篇文章的主题:
一、为什么会重走生命周期?
二、如何解决?
想要分析问题,首先要了解原理,先简单看一下Navigation框架大致的实现原理。
在容器Activity的布局文件中,我们使用一个<fragment>标签,并且为标签显示的指定了一个android:name属性,里面配置的是一个Fragment的全路径,官方提供的是androidx.navigation.fragment.NavHostFragment,我们都知道,Activity加载布局的时候会根据配置的全路径通过反射获取到Fragment对象,然后attach到该Activity,最终完成Fragment的加载。想要了解Navigation框架,从NavHostFragment入手再合适不过。
public class NavHostFragment extends Fragment implements NavHost {
...
}
public interface NavHost {
@NonNull
NavController getNavController();
}
NavHostFragment就是一个Fragment的子类实现了一个简单的接口,能够对外提供获取NavController的方法,该方法的返回值就是NavHostFragment的一个属性mNavController。
private NavHostController mNavController;
@NonNull
@Override
public final NavController getNavController() {
if (mNavController == null) {
throw new IllegalStateException("NavController is not available before onCreate()");
}
return mNavController;
}
mNavController属性的初始化是在onCreate生命周期中完成的。
@CallSuper
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final Context context = requireContext();
mNavController = new NavHostController(context);
mNavController.setLifecycleOwner(this);
//略...调用一些mNavController方法
onCreateNavController(mNavController); //这个方法比较重要,下面会提及
//设置导航图ID
if (mGraphId != 0) {
mNavController.setGraph(mGraphId);
} else {
//设置一个空导航
}
}
mGraphId就是在fragment标签中配置的navGraph属性是在onInflate方法中获取的:
@CallSuper
@Override
public void onInflate(@NonNull Context context, @NonNull AttributeSet attrs,@Nullable Bundle savedInstanceState){
super.onInflate(context, attrs, savedInstanceState);
final TypedArray navHost = context.obtainStyledAttributes(attrs,androidx.navigation.R.styleable.NavHost);
//通过自定义属性获取navigation导航图
final int graphId = navHost.getResourceId(androidx.navigation.R.styleable.NavHost_navGraph, 0);
if (graphId != 0) {
mGraphId = graphId;
}
...
}
其实NavHostFragment才是容器Activity加载的第一个Fragment,在mNavController.setGraph方法调用之后,会经过一些列的方法调用,最终替换为在navigation资源文件中配置的startDestination属性中的Fragment。
以上就是NavHostFragment类的主题功能,其实非常简单已读。NavController虽然看起来比较多,但它的功能还是比明确的,就是对外提供设置NavGraph、跳转方法navigate、返回事件控制以及监听Destination的变化。但真正执行视图跳转的逻辑并不是NavController执行的,而是通过mNavigatorProvider分发到了不同的Navigator中,然后执行真正的跳转逻辑:
//NavController中navigate最终的重载
private void navigate(@NonNull NavDestination node, @Nullable Bundle args,
@Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
//...
//根据跳转类型的不同,分发到不同的navigator中执行跳转逻辑
Navigator<NavDestination> navigator = mNavigatorProvider.getNavigator(node.getNavigatorName());
Bundle finalArgs = node.addInDefaultArgs(args);
//调用navigator里的navigate方法
NavDestination newDest = navigator.navigate(node, finalArgs,navOptions, navigatorExtras);
//...更新mBackStack栈
}
抽象类Navigator一共有5个子类:
@Navigator.Name("activity")
public class ActivityNavigator extends Navigator<ActivityNavigator.Destination> {
//... 控制Activity的跳转
}
@Navigator.Name("dialog")
public final class DialogFragmentNavigator extends Navigator<DialogFragmentNavigator.Destination> {
//...控制DialogFragment的跳转
}
@Navigator.Name("fragment")
public class FragmentNavigator extends Navigator<FragmentNavigator.Destination> {
//...控制Fragment的跳转
}
@Navigator.Name("navigation")
public class NavGraphNavigator extends Navigator<NavGraph> {
//...控制变更NavGraph
}
@Navigator.Name("NoOp")
public class NoOpNavigator extends Navigator<NavDestination> {
//...忽略不计...
}
NavigatorProvider类负责管理以上五种Navigator,管理的方式非常简单,就是用一个名为mNavigators的HashMap<String,Navigator>把通过addNavigator方法添加的Navigator缓存起来,其中key就是@Navigator.Name("xxx")注解里面给定的xxx,在getNavigator时从缓存中取出来给调用方。
在我们使用NavHostFragment的时候,框架会为我添加前四种Navigator,分别是在上文提到过的NavHostFragment的onCreate方法中的调用的onCreateNavController(mNavController)方法:
@CallSuper
protected void onCreateNavController(@NonNull NavController navController) {
//添加DialogFragmentNavigator
navController.getNavigatorProvider().addNavigator(
new DialogFragmentNavigator(requireContext(), getChildFragmentManager()));
//添加FragmentNavigator
navController.getNavigatorProvider().addNavigator(createFragmentNavigator());
}
和NavController的构造方法里:
public NavController(@NonNull Context context) {
mContext = context;
while (context instanceof ContextWrapper) {
if (context instanceof Activity) {
mActivity = (Activity) context;
break;
}
context = ((ContextWrapper) context).getBaseContext();
}
//这里
mNavigatorProvider.addNavigator(new NavGraphNavigator(mNavigatorProvider));
mNavigatorProvider.addNavigator(new ActivityNavigator(mContext));
}
以上就是Navigation框架的大体逻辑,总结一下就是:
NavHostFragment作为容器Activity第一个加载的Fragment,维护了一个NavController的实例,并在NavigatorProvider中添加了4种Navigator用来执行不同的视图跳转逻辑,并在onCreate方法的最后,通过NavController.setGraph方法设置了在fragment标签中配置的nvGraph的id,把NavHostFragment重定向到了navigation.xml里配置的startDestination。NavController的跳转逻辑也通过跳转类型的不过,通过内部维护的NavigatorProvider分发到了不同的Navigator进行跳转。
那现在情况就很明了了,我们在SourceFragment中调用的跳转方法:
nextButton.setOnClickListener {
findNavController().navigate(R.id.action_sourceFragment_to_targetFragment)
}
最终会经过一系列的处理分发到FragmentNavigator的navigate方法中去:
@Nullable
@Override
public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args,
@Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
//略...
final FragmentTransaction ft = mFragmentManager.beginTransaction();
ft.replace(mContainerId, frag);
ft.setPrimaryNavigationFragment(frag);
//略...
ft.setReorderingAllowed(true);
ft.commit();
//略...
}
看到这里终于恍然大悟,原来Navigation框架还是基于FragmentTransaction的封装!因为在打开新的Fragment的时候,老Fragment直接被replace掉了,那Fragment重走生命周期就是一个老生常谈的问题了。
既然知道了原因,那就开始着手解决,同原来的Fragment重绘解决方案一致,只需要把replace方法替换为hidden和add方法即可,不过有个地方需要特别注意一下,因为容器Activity第一个加载的是NavHostFragment,而这个Fragment是需要被replace掉的,其他的Fragment则不再需要。
创建copy一份FragmentNavigator类并重命名为NoReplaceFragmentNavigator,只对navigate方法进行部分修改:
@Nullable
@Override
public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args,
@Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
//ft.replace(mContainerId, frag); 修改前
if (mBackStack.size > 0) {
ft.hide(mFragmentManager.getFragments.get(mBackStack.size - 1)).add(mContainerId, frag);
}else{
ft.replace(mContainerId, frag);
}
}
然后再copy一份NavHostFragment类并重命名为NoReplaceNavHostFragment,对createFragmentNavigator进行修改,将原来的FragmentNavigator替换为NoReplaceFragmentNavigator即可。createFragmentNavigator被标注为弃用,但是文档里并没有给出替代的方法,奇怪奇怪,有知道的大佬帮忙解下惑~
最后,在容器Activity布局文件的fragment标签中android:name属性修改为NoReplaceFragmentNavigator的全路径即可。
尚未解决的问题:
通过以上的修改,确实可以避免重新返回到SourceFragment时重绘的问题,但是却带来了一个新的问题,就是使用FragmentTransaction的hidden方法并不会让当前Fragment的生命周期发生变更,也就是在执行前文提到第二阶段和第三阶段的时候,SourceFragment的生命周期是没有发生任何变化的。FragmentTransaction一直存在这么一个问题,不过倒是有一个折中的解决方案,重写Fragment的onHiddenChanged方法:
override fun onHiddenChanged(hidden: Boolean) {
super.onHiddenChanged(hidden)
if(hidden){
//当前Fragment不可见
}else{
//当前Fragment可见
}
}
可以把那些需要在生命周期里处理的逻辑放到这个方法里面,但使用Lifecycle监听Fragment生命周期变化就无能为力了...
如果有更好的方案,欢迎交流分享~
如果该文章能够帮到你,欢迎点赞评论和关注,一起交流探讨~