美文网首页MVVM
使用Navigation返回时Fragment重走生命周期?原因

使用Navigation返回时Fragment重走生命周期?原因

作者: 王远道呀 | 来源:发表于2021-06-29 19:04 被阅读0次

    随着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生命周期变化就无能为力了...

    如果有更好的方案,欢迎交流分享~

    如果该文章能够帮到你,欢迎点赞评论和关注,一起交流探讨~

    相关文章

      网友评论

        本文标题:使用Navigation返回时Fragment重走生命周期?原因

        本文链接:https://www.haomeiwen.com/subject/brpkultx.html