一、问题背景

当我们在使用 Navigation 组件用于管理导航多个 Fragment 的时候,我们经常会先拿到其 NavController,一种标准的用法如下:

首先,在 Activity 的布局中,添加 FragmentContainerView,用于展示各 Fragment,并指定 android:nameandroidx.navigation.fragment.NavHostFragment,同时指定 navGraph

<androidx.fragment.app.FragmentContainerView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/fragment_container_view"
    android:name="androidx.navigation.fragment.NavHostFragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:keepScreenOn="true"
    app:navGraph="@navigation/nav" />

随后在 Activity 中,使用 Navigation.findNavController 方法拿到对应的 NavController

val navController = Navigation.findNavController(this, R.id.fragment_container_view)

但是,如果上面的方法在 ActivityonCreate 方法中调用,会抛出以下的异常:

java.lang.IllegalStateException: Activity * does not have a NavController set on *

即使我们通过 post 方法在主线程延迟执行,在占用较高的时候,也有概率会触发以上的异常

view.post {
	val navController = Navigation.findNavController(this, R.id.fragment_container_view)
}

本文将通过探寻源码,追踪此异常抛出的原因,通过根因分析,给出解决此问题的方案。

参考文献:

二、源码分析

(一)androidx.navigation.Navigation 类

首先,我们先来看 androidx.navigation.Navigation.findNavController() 方法,其源码如下:

@JvmStatic
public fun findNavController(activity: Activity, @IdRes viewId: Int): NavController {
    val view = ActivityCompat.requireViewById<View>(activity, viewId)
    return findViewNavController(view)
        ?: throw IllegalStateException(
            "Activity $activity does not have a NavController set on $viewId"
        )
}

可以看到,首先通过 viewId 拿到具体的 View,然后再通过 View 拿到 NavController。这里如果拿到的是空,则会抛出 IllegalStateException 的异常,其异常内容正是我们遇到的 java.lang.IllegalStateException: Activity * does not have a NavController set on *,因此可以知道,是 findViewNavController(View) 方法拿到了空的。


我们继续看 androidx.navigation.Navigation.findViewNavController(view) 方法,其源码如下:

private fun findViewNavController(view: View): NavController? =
    generateSequence(view) { it.parent as? View? }
        .mapNotNull { getViewNavController(it) }
        .firstOrNull()

@Suppress("UNCHECKED_CAST")
private fun getViewNavController(view: View): NavController? {
    val tag = view.getTag(R.id.nav_controller_view_tag)
    var controller: NavController? = null
    if (tag is WeakReference<*>) {
        controller = (tag as WeakReference<NavController>).get()
    } else if (tag is NavController) {
        controller = tag
    }
    return controller
}

首先,利用 generateSequence 方法,将 View 树转换成一个序列,从 子 View 开始,逐渐向上拿到 父 View,随后对每个 View 都使用 getViewNavController 方法尝试拿到 NavController,最后返回拿到的第一个 NavController

从这个算法可以看出,这是从子 View 开始逐渐向父 ViewNavController,直到根布局为止。因此,只要 View 是位于 Fragment 中的,通过此方法都可以找到NavController

我们再看 getViewNavController(View) 方法,其通过 view.getTag(R.id.nav_controller_view_tag) 方法,拿到了 NavController。我们知道,Viewtag 是用于在 View 上携带数据的一种方式,其需要先在适当的位置进行赋值,否则拿到的就是空的。因此,我们可以知道,这是因为 ViewtagR.id.nav_controller_view_tag 还未被赋值的时候,拿到了空的 NavController

综上所说,我们需要查找 ViewtagR.id.nav_controller_view_tag 被赋值的地方。


通过全局查找 R.id.nav_controller_view_tag,其设置 View.setTag() 的源码在androidx.navigation.Navigation.setViewNavController 如下:

@JvmStatic
public fun setViewNavController(view: View, controller: NavController?) {
    view.setTag(R.id.nav_controller_view_tag, controller)
}

查找此方法的使用地方,可以知道其是在 androidx.navigation.fragment.NavHostFragment 中被调用的,我们随后去看 NavHostFragment 类。
在这里插入图片描述

(二)androidx.navigation.fragment.NavHostFragment 类

NavHostFragment 类中调用 Navigation.setViewNavController 总共有三处地方,具体源码如下:

internal val navHostController: NavHostController by lazy {
	...
}

public override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    check(view is ViewGroup) { "created host view $view is not a ViewGroup" }
    Navigation.setViewNavController(view, navHostController)
    // When added programmatically, we need to set the NavController on the parent - i.e.,
    // the View that has the ID matching this NavHostFragment.
    if (view.getParent() != null) {
        viewParent = view.getParent() as View
        if (viewParent!!.id == id) {
            Navigation.setViewNavController(viewParent!!, navHostController)
        }
    }
}

public override fun onDestroyView() {
    super.onDestroyView()
    viewParent?.let { it ->
        if (Navigation.findNavController(it) === navHostController) {
            Navigation.setViewNavController(it, null)
        }
    }
    viewParent = null
}

我们可以看到,在 NavHostFragmentonViewCreated 方法,先给自身设置了 NavHostController,随后给其相同 ID 的父布局设置 NavHostController。而在 onDestroyView,给已经设置了 NavHostController 的父布局置空 NavHostController

NavHostController 是通过 by lazy 延迟初始化出来的,可以知道,其实 NavHostFragment 一直都有 NavHostController

因此,我们可以从这里知道,NavHostController 被设置到父布局的时机是在 onViewCreated 的时候,其可以被正常获取的时机是在 NavHostFragmentonViewCreated()onDestroyView() 的生命周期之间。那么这个 Fragment 的生命周期需要如何与 Activity 的生命周期关联上呢?

我们先来看 FragmentContainerViewNavHostFragment 之间的关系。

(三)androidx.fragment.app.FragmentContainerView 类

我们在使用 FragmentContainerView 的时候,会设置 android:name="androidx.navigation.fragment.NavHostFragment",而对于 FragmentContainerView 的创建,与一般的 View 是有所不同的,其使用的是传递了 FragmentManager 方法的 internal constructor(context: Context, attrs: AttributeSet, fm: FragmentManager) : super(context, attrs),我们追寻相关源码做一个简短的分析:

// 我们知道 使用 FragmentContainerView 需要使用 FragmentActivity,我们先来看 androidx.fragment.app.FragmentActivity  类
---------------------------------------------------------------------------------------------
/**
 * androidx.fragment.app.FragmentActivity
 */
// 成员变量 androidx.fragment.app.FragmentController
final FragmentController mFragments = FragmentController.createController(new HostCallbacks());

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    mFragmentLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE);
    // 在 onCreate 方法中调用创建 Fragment
    mFragments.dispatchCreate();
}

@Override 
@Nullable
public View onCreateView(@Nullable View parent, @NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs) {
		// 在 onCreateView 方法中分发 Fragment 的 OnCreateView 方法
    final View v = dispatchFragmentsOnCreateView(parent, name, context, attrs);
    if (v == null) {
        return super.onCreateView(parent, name, context, attrs);
    }
    return v;
}

@Nullable
final View dispatchFragmentsOnCreateView(@Nullable View parent, @NonNull String name,
        @NonNull Context context, @NonNull AttributeSet attrs) {
    return mFragments.onCreateView(parent, name, context, attrs);
}


---------------------------------------------------------------------------------------------
/**
 * androidx.fragment.app.FragmentController
 */

// androidx.fragment.app.FragmentHostCallback, 由 FragmentActivity 实例化 FragmentController 的时候就传入,含有 androidx.fragment.app.FragmentManager 的实例
private final FragmentHostCallback<?> mHost;

@Nullable
public View onCreateView(@Nullable View parent, @NonNull String name, @NonNull Context context,
        @NonNull AttributeSet attrs) {
    // 由 FragmentHostCallback 的 FragmentManager 实例的 FragmentLayoutInflaterFactory 实例进行创建View
    return mHost.mFragmentManager.getLayoutInflaterFactory()
            .onCreateView(parent, name, context, attrs);
}

---------------------------------------------------------------------------------------------
/**
 * androidx.fragment.app.FragmentHostCallback
 */
// androidx.fragment.app.FragmentManager 实例
final FragmentManager mFragmentManager = new FragmentManagerImpl();

---------------------------------------------------------------------------------------------
/**
 * androidx.fragment.app.FragmentManager
 */
// androidx.fragment.app.FragmentLayoutInflaterFactory 的实例
private final FragmentLayoutInflaterFactory mLayoutInflaterFactory = new FragmentLayoutInflaterFactory(this);

---------------------------------------------------------------------------------------------
/**
 * androidx.fragment.app.FragmentLayoutInflaterFactory
 */
// 在构造方法中被传递赋值
final FragmentManager mFragmentManager;

@Nullable
@Override
public View onCreateView(@Nullable final View parent, @NonNull String name,
        @NonNull Context context, @NonNull AttributeSet attrs) {
   	// 如果是 FragmentContainerView 的对象,则进行特殊处理,使用传递 FragmentManager 的构造方法
    if (FragmentContainerView.class.getName().equals(name)) {
        return new FragmentContainerView(context, attrs, mFragmentManager);
    }
    ...
}

可以看到,沿着这条路下来最后由 androidx.fragment.app.FragmentLayoutInflaterFactoryonCreateViewFragmentContainerView 进行特殊处理,使用传递 FragmentManager 的构造方法。我们来看这个构造方法:

internal constructor(
    context: Context,
    attrs: AttributeSet,
    fm: FragmentManager
) : super(context, attrs) {
    var name = attrs.classAttribute
    var tag: String? = null
    // 拿到 android:name 定义的 Fragment
    context.withStyledAttributes(attrs, R.styleable.FragmentContainerView) {
        if (name == null) {
            name = getString(R.styleable.FragmentContainerView_android_name)
        }
        tag = getString(R.styleable.FragmentContainerView_android_tag)
    }
    val id = id
    val existingFragment: Fragment? = fm.findFragmentById(id)
    // If there is a name and there is no existing fragment,
    // we should add an inflated Fragment to the view.
    if (name != null && existingFragment == null) {
        if (id == View.NO_ID) {
            val tagMessage = if (tag != null) " with tag $tag" else ""
            throw IllegalStateException(
                "FragmentContainerView must have an android:id to add Fragment $name$tagMessage"
            )
        }
        // 通过反射实例化 Fragment 对象
        val containerFragment: Fragment =
            fm.fragmentFactory.instantiate(context.classLoader, name)
        // 将自身的 ID 赋值给 Fragment 对象
        containerFragment.mFragmentId = id
        containerFragment.mContainerId = id
        containerFragment.mTag = tag
        containerFragment.mFragmentManager = fm
        containerFragment.mHost = fm.host
        containerFragment.onInflate(context, attrs, null)
        // 开始 Fragment 的事务,将 Fragment 对象 添加到 FragmentManager
        fm.beginTransaction()
            .setReorderingAllowed(true)
            .add(this, containerFragment, tag)
            .commitNowAllowingStateLoss()
    }
    // 已将 Fragment 准备好,添加到 Activity 中
    fm.onContainerAvailable(this)
}

从以上源码可以知道,在布局中使用 android:name="androidx.navigation.fragment.NavHostFragment" 定义的 NavHostFragment,将会被反射实例化出来,并将自身的 ID 赋值给 NavHostFragment,这样 FragmentContainerViewNavHostFragment 就有相同的 ID,在 NavHostFragment.onViewCreated() 即可正常给 FragmentContainerView 设置 NavController 了。

当源码追到这里的时候,已经不太好往下追踪了,那么我们可以通过在 setViewNavController 打断点的方式,来看一下 androidx.navigation.Navigation.setViewNavController 的调用链。
在这里插入图片描述
在这里插入图片描述

可以看到,从 ActivityonStart 生命周期开始,将事件下发到 FragmentControllerFragmentMananger,由 FragmentStateManager 状态机管理,移动到指定的生命周期,最后调用到 createView 方法,从而使 NavHostFragment 进行设置 NavControllerTag 上。

由此可知,在 ActivityonCreate 的时候还未设置 Tag,从而导致拿不到 NavHostController,抛出异常。

三、解决方案

通过以上源码分析可以知道, NavHostController 是在 ActivityonStart 方法中被设置到 FragmentContainerViewTag 中之后,才能通过 Navigation.findNavController() 才能拿到 NavController。因此解决此问题有多种方式。

(一)在 Activity 的 onStart 方法之后获取 NavController

第一种解决方法是在 ActivityonStart 方法之后获取 NavHostController,即

override fun onStart() {
    super.onStart()

    val navController = Navigation.findNavController(this, R.id.fragment_container_view)
}

(二)直接通过 NavHostFragment 拿到 NavController

由前文分析可以知道, NavHostController 是通过 by lazy 延迟初始化出来的,可以知道,其实 NavHostFragment 一直都有 NavHostController,即 NavControllerNavHostController 继承自 NavController),那么我们可以直接通过 NavHostFragment 拿到 NavController

我们知道,通过 FragmentManager.findFragmentById() 方法可以拿到第一个指定 IDFragment,所以在 FragmentContainerView 初始化的时候,即在 ActivityonCreate 方法 到 onStart 之间,即可拿到 NavHostFragment,代码如下:

val navController = (supportFragmentManager.findFragmentById(R.id.fragment_container_view) as? NavHostFragment)?.navController
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐