Android 系统源码初步阅读之 activity 的 startActivity(intent) 与 非 activity 的 startActivity(...) 的不同

Posted Nicholas_hzf

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android 系统源码初步阅读之 activity 的 startActivity(intent) 与 非 activity 的 startActivity(...) 的不同相关的知识,希望对你有一定的参考价值。

基于 Android 11

android 源码太过庞杂,每次深入流程看完,隔天就忘了,还是得从细微处出发,由浅入深才行!

由于本文是为了分析两种子类实现 startActivity(…) 的不同,有一些细节就省略不表,专注于不同的地方

一、前置知识

  1. 日常开发的 MainActivity 的继承关系如下:
    MainActivity
    ->
    AppCompatActivity() 用于希望在较旧的 Android 设备上使用某些较新平台功能的 Activity 父类
    ->
    FragmentActivity 用于基于支持 Fragment 的 Activity 父类
    ->
    ComponentActivity 用于支持高级组件组合的 Activity 父类
    ->
    androidx.core.app.ComponentActivity 用于支持高级组件组合的 Activity 父类
    ->
    Activity Activity 基类
    ->
    ContextThemeWrapper Context 包装器,允许修改或替换被包装的上下文的主题
    ->
    ContextWrapper Context 的代理实现,它只是将其所有调用委托给另一个上下文
    ->
    Context 上下文基类,抽象类,无继承和实现接口
  2. 由继承关系可以看出,Context 抽象基类有一类比较重要的子类,就是 Activity 相关的子类,我们将其他的归为另外一类,如 ServiceContextImpl等,ContextImpl 是 Context 上下文API 的通用实现,它为活动和其他应用程序组件提供基本上下文对象。
  3. 本文重点关注的方法:
  • 两个方法功能类似,一个方法多了个可为空的 Bundle 参数。
  • 这两个方法都是 abstract 的,其实现是由 Context 的具体子类实现的。
  • 从方法二的注释就可以得到本文的最终结论
    如果此方法是从非 Activity 的上下文环境中调用的,那么 intent 中必须包括 FLAG_ACTIVITY_NEW_TASK 启动标识。这是因为,如果不是从现有的活动启动,则不存在相对应的 Task 任务栈,没有任务栈就没办法存放所启动的活动,因此需要为这个新启动的活动开辟出属于它自己的一个任务栈用于存放它
/**
 * Same as {@link #startActivity(Intent, Bundle)} with no options specified.
 * ...
 */
public abstract void startActivity(@RequiresPermission Intent intent);
/**
 * Launch a new activity.
 * ...
 * <p>Note that if this method is being called from outside of an
 * {@link android.app.Activity} Context, then the Intent must include
 * the {@link Intent#FLAG_ACTIVITY_NEW_TASK} launch flag.  This is because,
 * without being started from an existing Activity, there is no existing
 * task in which to place the new activity and thus it needs to be placed
 * in its own separate task.
 * ...
 */
public abstract void startActivity(@RequiresPermission Intent intent, @Nullable Bundle options);

二、activity 的 startActivity(…) 方法

首先从 startActivity(Intent intent) 开始进入,一路调用都比较正常,没有什么太过值得注意的地方,最后调用了 mInstrumentation.execStartActivity(…) 方法来启动目标 activity

@Override
public void startActivity(Intent intent) {
    this.startActivity(intent, null);
}
@Override
public void startActivity(Intent intent, @Nullable Bundle options) {
    ...
    if (options != null) {
        startActivityForResult(intent, -1, options);
    } else {
        // 由于 options 为空,代码走这里
        startActivityForResult(intent, -1);
    }
}
public void startActivityForResult(@RequiresPermission Intent intent, int requestCode) {
    startActivityForResult(intent, requestCode, null);
}
public void startActivityForResult(@RequiresPermission Intent intent, int requestCode, @Nullable Bundle options) {
	// mParent 变量用于判断该 activity 是否被镶嵌在另外一个 activity 中
	if (mParent == null) {
	    // 普遍情况
		options = transferSpringboardActivityOptions(options);
		Instrumentation.ActivityResult ar = mInstrumentation.execStartActivity(this, mMainThread.getApplicationThread(), mToken, this, intent, requestCode, options);
        ...
    } else {
        // 镶嵌的情况
        ...
    }
}

三、非 activity 的 startActivity(…) 方法

在 ContextImpl 类中,startActivity(…) 的调用流程比较短,单参启动双参方法,最后也是调用 mInstrumentation.execStartActivity(…) 方法来启动目标 activity。值得注意的是,在启动目标 activity 之前,方法中做了一个条件判断,不符合条件的话会抛出 AndroidRuntimeException 异常,具体的条件以及设置原因可以通过阅读代码得知,也可以直接看 google 给的注释:通常不允许从没有 FLAG_ACTIVITY_NEW_TASK 启动标志的活动外部(即在非 activity 上下文环境中)调用 startActivity(…) 方法,除非调用者指定了目标启动活动所属的 Task id。在版本 N 和 O-MR1 之间存在一个 bug,这使得它能够在没有指定 FLAG_ACTIVITY_NEW_TASK 的情况下也可以正常启动目标活动。这里做版本判断是为了保持向后的兼容性

@Override
public void startActivity(Intent intent) {
    ...
    startActivity(intent, null);
}
@Override
public void startActivity(Intent intent, Bundle options) {
	...
    // Calling start activity from outside an activity without FLAG_ACTIVITY_NEW_TASK is
    // generally not allowed, except if the caller specifies the task id the activity should
    // be launched in. A bug was existed between N and O-MR1 which allowed this to work. We
    // maintain this for backwards compatibility.
	final int targetSdkVersion = getApplicationInfo().targetSdkVersion;
    if ((intent.getFlags() & Intent.FLAG_ACTIVITY_NEW_TASK) == 0
            && (targetSdkVersion < Build.VERSION_CODES.N
                    || targetSdkVersion >= Build.VERSION_CODES.P)
            && (options == null
                    || ActivityOptions.fromBundle(options).getLaunchTaskId() == -1)) {
        throw new AndroidRuntimeException(
                "Calling startActivity() from outside of an Activity "
                        + " context requires the FLAG_ACTIVITY_NEW_TASK flag."
                        + " Is this really what you want?");
    }
    mMainThread.getInstrumentation().execStartActivity(getOuterContext(), mMainThread.getApplicationThread(), null, (Activity) null, intent, -1, options);
}

四、最终结论

  1. 写好注释真的很重要!!!
  2. activity 上下文环境中,启动目标活动的标识位没有特殊要求,依据实际的开发场景而定,而在非 activity 上下文环境中,需要指定 FLAG_ACTIVITY_NEW_TASK 标识位。 因为在非上下文环境中开启一个新的 activity,没有默认的任务栈来存放被启动的目标活动,所以需要指定 FLAG_ACTIVITY_NEW_TASK 标识位来生成一个用于存放目标活动的任务栈。
  3. 结论二在版本 N 和 O-MR1 之间不成立,可以不指定 FLAG_ACTIVITY_NEW_TASK 标识位,但是这个是个 BUG,还是指定的好。

源码学习,路漫漫其修远兮,保持本心,好好学习!如有错误,烦请指正!感谢阅读!

以上是关于Android 系统源码初步阅读之 activity 的 startActivity(intent) 与 非 activity 的 startActivity(...) 的不同的主要内容,如果未能解决你的问题,请参考以下文章

android XML动画初步解析(activity界面之间跳转demo)

使用Android Studio调试系统应用之TvSettings:移植

Android系统源码怎么看?Android开发源码精编解析助你高效阅读源码

Android中初步自定义view

Android性能优化之内存泄漏

如何阅读Android系统源码-收藏必备