MVVM

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

2021-06-29  本文已影响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生命周期变化就无能为力了...

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

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

上一篇下一篇

猜你喜欢

热点阅读