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

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

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,711评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,932评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,770评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,799评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,697评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,069评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,535评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,200评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,353评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,290评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,331评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,020评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,610评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,694评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,927评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,330评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,904评论 2 341

推荐阅读更多精彩内容