作者:vivo 互联网大前端团队- Zhao Kaiping

本文从一例业务中遇到的问题登程,以FLAG_ACTIVITY_NEW_TASK这一flag作为切入点,带大家探索Activity启动前的一项重要的工作——栈校验。

文中列举一系列业务中可能遇到的异样情况,详细描述了应用FLAG_ACTIVITY_NEW_TASK时可能遇到的“坑”,并从源码中探究其根源。只有正当应用flag、launchMode,能力防止因为栈机制的特殊性,导致一系列与预期不符的启动问题。

一、问题及背景

利用间互相联动、互相跳转,是实现零碎整体性、体验一致性的重要伎俩,也是最简略的一种办法。

当咱们用最罕用的办法去startActivity时,竟也会遇到失败的状况。在实在业务中,就遇到了这样一例异样:用户点击某个按钮时,想要“简简单单”跳转另一个利用,却没有任何反馈。

经验丰富的你,脑海中是否涌现出了各种猜测:是不是指标Activity甚至指标App不存在?是不是指标Activty没有对外开放?是不是有权限的限度或者跳转的action/uri错了……

实在的起因被flag、launchMode、Intent等个性层层隐匿,可能超出你此时的思考。

本文将从源码登程,探索前因后果,开展讲讲在startActivity()真正筹备启动一个Activity前,须要通过哪些“磨难”,怎么有据可依地解决由栈问题导致的启动异样。

1.1 业务中遇到的问题

业务中的场景是这样的,存在A、B、C三个利用。

(1)从利用A-Activity1跳转至利用B-Activity2;

(2)利用B-Activity2持续跳转到利用C-Activity3;

(3)C内某个按钮,会再次跳转B-Activity2,但点击后没有任何反馈。如果不通过后面A到B的跳转,C间接跳到B是能够的。

1.2 问题代码

3个Activity的Androidmanifest配置如下,均可通过各自的action拉起,launchMode均为规范模式。

<!--利用A-->       <activity            android:name=".Activity1"            android:exported="true">            <intent-filter>                <action android:name="com.zkp.task.ACTION_TO_A_PAGE1" />                <category android:name="android.intent.category.DEFAULT" />            </intent-filter>        </activity> <!--利用B-->        <activity            android:name=".Activity2"            android:exported="true">            <intent-filter>                <action android:name="com.zkp.task.ACTION_TO_B_PAGE2" />                <category android:name="android.intent.category.DEFAULT" />            </intent-filter>        </activity> <!--利用C-->        <activity            android:name=".Activity3"            android:exported="true">            <intent-filter>                <action android:name="com.zkp.task.ACTION_TO_C_PAGE3" />                <category android:name="android.intent.category.DEFAULT" />            </intent-filter>        </activity>

A-1到B-2的代码,指定flag为FLAG_ACTIVITY_NEW_TASK

private void jumpTo_B_Activity2_ByAction_NewTask() {    Intent intent = new Intent();    intent.setAction("com.zkp.task.ACTION_TO_B_PAGE2");    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);    startActivity(intent);}

B-2到C-3的代码,未指定flag

private void jumpTo_C_Activity3_ByAction_NoTask() {    Intent intent = new Intent();    intent.setAction("com.zkp.task.ACTION_TO_C_PAGE3");    startActivity(intent);}

C-3到B-2的代码,与A-1到B-2的完全一致,指定flag为 FLAG_ACTIVITY_NEW_TASK

private void jumpTo_B_Activity2_ByAction_NewTask() {    Intent intent = new Intent();    intent.setAction("com.zkp.task.ACTION_TO_B_PAGE2");    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);    startActivity(intent);}

1.3 代码初步剖析

认真查看问题代码,在实现上非常简单,有两个特色:

(1)如果间接通过C-3跳B-2,没有任何问题,但A-1曾经跳过B-2后,C-3就失败了。

(2)在A-1和C-3跳到B-2时,都设置了flag为FLAG_ACTIVITY_NEW_TASK。

根据教训,咱们揣测与栈无关,尝试将跳转前栈的状态打印进去,如下图。

因为A-1跳到B-2时设置了FLAG_ACTIVITY_NEW_TASK,B-2跳到C-3时未设置,所以1在独立栈中,2、3在另一个栈中。示意如下图。

C-3跳转B-2个别有3种可能的预期,如下图:料想1,新建一个Task,在新Task中启动一个B-2;料想2,复用曾经存在的B-2;料想3,在已有Task中新建一个实例B-2。

但实际上3种预期都没有实现,所有Activity的任何申明周期都没有变动,界面始终停留在C-3。

看一下FLAG_ACTIVITY_NEW_TASK的官网正文和代码正文,如下图:

重点关注这一段:

When using this flag, if a task is already running for the activity you are now starting, then a new activity will not be started; instead, the current task will simply be brought to the front of the screen with the state it was last in.

应用此flag时,如果你正在启动的Activity曾经在一个Task中运行,那么一个新Activity不会被启动;相同,以后Task将简略地显示在界面的后面,并显示其最初的状态。

——显然,官网文档与代码正文的表述与咱们的异常现象是统一的,指标Activity2曾经在Task中存在,则不会被启动;Task间接显示在后面,并展现最初的状态。因为指标Activty3就是起源Activity3,所以页面没有任何变动。

看起来官网还是很靠谱的,但实际效果真的能始终与官网形容统一吗?咱们通过几个场景来看一下。

二、场景拓展与验证

2.1 场景拓展

在笔者根据官网形容进行调整、复现的过程中,发现了几个比拟有意思的场景。

PS:下面业务的案例中,B-2和C-3在不同利用内,又在雷同的Task内,但实际上是否是同一个利用,对后果的影响并不大。为了防止不同利用和不同Task造成浏览凌乱,同一个栈的跳转,咱们都在本利用内进行,故业务中的场景等价于上面的【场景0】

【场景0】把业务中B-2到C-3的利用间跳转改为B-2到B-3的利用内跳转
// B-2跳转B-3public static void jumpTo_B_3_ByAction_Null(Context context) {    Intent intent = new Intent();    intent.setAction("com.zkp.task.ACTION_TO_B_PAGE3");    context.startActivity(intent);}

如下图,A-1设置NEW_TASK跳转B-2,再跳转B-3,最终设置NEW_TASK想跳转B-2。尽管跳C-3改为了跳B-3,但与之前问题的体现统一,没有反馈,停留在B-3。

有的读者会指出这样的问题:如果同一个利用内应用NEW_TASK跳转,而不指定指标的taskAffinity属性,理论是无奈在新Task中启动的。请大家疏忽该问题,能够认为笔者的操作是曾经加了taskAffinity的,这对最终后果并没有影响。

【场景1】如果指标Task和起源Task不是同一个,状况是否会如官网文档所说复用已有的Task并展现最近状态?咱们改为B-3启动一个新Task的新Activity C-4,再通过C-4跳回B-2
// B-3跳转C-4public static void jumpTo_C_4_ByAction_New(Context context) {    Intent intent = new Intent("com.zkp.task.ACTION_TO_C_PAGE4");    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);    context.startActivity(intent);}// C-4跳转B-2public static void jumpTo_B_2_ByAction_New(Context context) {    Intent intent = new Intent();    intent.setAction("com.zkp.task.ACTION_TO_B_PAGE2");    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);    context.startActivity(intent);}

如下图,A-1设置NEW_TASK跳转B-2,再跳转B-3,再设置NEW_TASK跳转C-4,最终设置NEW_TASK想跳转B-2。

料想的后果是:不会跳到B-2,而是跳到它所在Task的顶层B-3。

理论的后果是:与预期统一,的确是跳到了B-3。

【场景2】把场景1稍做批改:C-4到B-2时,咱们不通过action来跳,改为通过setClassName跳转
// C-4跳转B-2public static void jumpTo_B_2_ByPath_New(Context context) {    Intent intent = new Intent();    intent.setClassName("com.zkp.b", "com.zkp.b.Activity2"); // 间接设置classname,不通过action    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);    context.startActivity(intent);}

如下图,A-1设置NEW_TASK跳转B-2,再跳转B-3,再设置NEW_TASK跳转C-4,最终设置NEW_TASK想跳转B-2。

料想的后果是:与场景0统一,会跳到B-2所在Task的已有顶层B-3。

理论的后果是:在已有的Task2中,产生了一个新的B-2实例。

仅仅是扭转了一下从新跳转B-2的形式,成果就齐全不一样了!这与官网文档中提到该flag与"singleTask" launchMode值产生的行为并不统一!

【场景3】把场景1再做批改:这次C-4不跳栈底的B-2,改为跳转B-3,且还是通过action形式。
// C-4跳转B-3public static void jumpTo_B_3_ByAction_New(Context context) {    Intent intent = new Intent();    intent.setAction("com.zkp.task.ACTION_TO_B_PAGE3");    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);    context.startActivity(intent);}

如下图,A-1设置NEW_TASK跳转B-2,再跳转B-3,再设置NEW_TASK跳转C-4,最终设置NEW_TASK想跳转B-3。

料想的后果是:与场景0统一,会跳到B-2所在Task的顶层B-3。

理论的后果是:在已有的Task2中,产生了一个新的B-3实例。

不是说好的,Activity曾经存在时,展现其所在Task的最新状态吗?明明Task2中曾经有了B-3,并没有间接展现它,而是生成了新的B-3实例。

【场景4】既然Activity没有被复用,那Task肯定会被复用吗?把场景3稍做批改,间接给B-3指定一个独自的affinity。
<activity    android:name=".Activity3"    android:exported="true"    android:taskAffinity="b3.task"><!--指定了亲和性标识-->    <intent-filter>        <action android:name="com.zkp.task.ACTION_TO_B_PAGE3" />        <category android:name="android.intent.category.DEFAULT" />    </intent-filter></activity>

如下图,A-1设置NEW_TASK跳转B-2,再跳转B-3,再设置NEW_TASK跳转C-4,最终设置NEW_TASK想跳转B-3。

——这次,连Task也不会再被复用了……Activity3在一个新的栈中被实例化了。

再回看官网的正文,就会显得十分不精确,甚至会让开发者对该局部的认知产生严重错误!略微扭转过程中的某个毫无关联的属性(如跳转指标、跳转形式……),就会产生很大差别。

在看flag相干正文时,咱们要建立一个意识:Task和Activity跳转的实际效果,是launchMode、taskAffinity、跳转形式、Activity在Task中的层级等属性综合作用的后果,不要置信“一面之词”。

回到问题自身,到底是哪些起因造就了下面的不同成果呢?只有源码最值得信赖了。

三、场景剖析与源码摸索

本文以Android 12.0源码为根底,进行探索。上述场景在不同Android版本上的体现是统一的。

3.1 源码调试注意事项

源码的调试办法,许多文章曾经有了具体的教学,本文不再赘述。此处只简略总结其中须要留神的事项

  1. 下载模拟器时,不要应用Google Play版本,该版本相似user版本,无奈抉择system_process过程进行断点。
  2. 即便是Google官网模拟器和源码,在断点时,也会有行数重大不对应的状况(比方:模拟器理论会运行到办法A,但在源码中打断点时,理论不能定位到办法A的对应行数),该问题并没有很好的解决办法,只能尽量躲避,如使模拟器版本与源码版本保持一致、多打一些断点减少要害行数被定位到的几率。

3.2 初步断点,明确启动后果

以【场景0】为例,咱们初步确认一下,为什么B-3跳转B-2会无反馈,零碎是否告知了起因。

3.2.1 明确启动后果及其起源

在Android源码的断点调试中,常见的有两类过程:利用过程和system_process过程。

在利用过程中,咱们能获取到利用启动后果的状态码result,这个result用来通知咱们启动是否胜利。波及堆栈如下图(标记1)所示:

Activity类::startActivity() → startActivityForResult() → Instrumentation类::execStartActivity(),返回值result则是ATMS(ActivityTaskManagerService)执行的后果。

如上图(标记2)标注,ATMS类::startActivity()办法,返回了result=3。

在system_process过程中,咱们看一下这个result=3是怎么被赋值的。略去具体断点步骤,理论堆栈如下图(标注1)所示:

ATMS类::startActivity() → startActivityAsUser() → ActivityStarter类::execute() → executeRequest() → startActivityUnchecked() → startActivityInner() → recycleTask(),在recycleTask()中返回了后果。

如上图(标注2)所示,result在mMovedToFront=false时被赋值,即result=START_DELIVERED_TO_TOP=3,而START_SUCCESS=0才代表创立胜利。

看一下源码中对START_DELIVERED_TO_TOP的阐明,如下图:

Result for IActivityManaqer.startActivity: activity wasn't really started, but the given Intent was given to the existing top activity.

(IActivityManaqer.startActivityActivity的后果:Activity并未真正启动,但给定的Intent已提供给现有的顶层Activity。)

“Activity并未真正启动”——是的,因为能够复用

“给定的Intent已提供给现有的顶层Activity”——理论没有,顶层Activity3并没有收到任何回调,onNewIntent()未执行,甚至尝试通过Intent::putExtra()传入新的参数,Activity3也没有收到。官网文档又带给了咱们一个疑难点?咱们把这个问题记录下来,在前面剖析。

满足什么条件,才会造成START_DELIVERED_TO_TOP的后果呢?笔者的思路是,通过与失常启动流程比照,找出差别点。

3.3 过程断点,摸索启动流程

一般来说,在定位问题时,咱们习惯通过后果反推起因,但反推的过程只能关注到与问题强关联的代码分支,并不能能使咱们很好地理解全貌。

所以,本节内容咱们通过程序浏览的办法,正向介绍startActivity过程中与上述【场景01234】强相干的逻辑。再次简述一下:

  1. 【场景0】同一个Task内,从顶部B-3跳转B-2——停留在B-3
  2. 【场景1】从另一个Task内的C-4,跳转B-2——跳转到B-3
  3. 【场景2】把场景1中,C-4跳转B-2的形式改为setClassName()——创立新B-2实例
  4. 【场景3】把场景1中,C-4跳转B-2改为跳转B-3——创立新B-3实例
  5. 【场景4】给场景3中的B-3,指定taskAffinity——创立新Task和新B-3实例

3.3.1 流程源码概览

源码中,整个启动流程很长,波及的办法和逻辑也很多,为了便于大家理清办法调用程序,不便后续内容的浏览,笔者将本文波及到的要害类及办法调用关系整顿如下。

后续浏览中如果不分明调用关系,能够返回这里查看:

// ActivityStarter.java     ActivityStarter::execute() {        executeRequest(intent) {            startActivityUnchecked() {                startActivityInner();        }    }    ActivityStarter::startActivityInner() {        setInitialState();        computeLaunchingTaskFlags();        Task targetTask = getReusableTask(){            findTask();        }        ActivityRecord targetTaskTop = targetTask.getTopNonFinishingActivity();        if (targetTaskTop != null) {            startResult = recycleTask() {                setTargetRootTaskIfNeeded();                complyActivityFlags();                if (mAddingToTask) {                    return START_SUCCESS; //【场景2】【场景3】从recycleTask()返回                }                resumeFocusedTasksTopActivities()                return mMovedToFront ? START_TASK_TO_FRONT : START_DELIVERED_TO_TOP;//【场景1】【场景0】从recycleTask()返回            }        } else {            mAddingToTask = true;        }        if (startResult != START_SUCCESS) {            return startResult;//【场景1】【场景0】从startActivityInner()返回        }        deliverToCurrentTopIfNeeded();        resumeFocusedTasksTopActivities();        return startResult;    }

3.3.2 要害流程剖析

(1)初始化

startActivityInner()是最次要的办法,如下列几张图所示,该办法会率先调用setInitialState(),初始化各类全局变量,并调用reset(),重置ActivityStarter中各种状态。

在此过程中,咱们记下两个要害变量mMovedToFront和mAddingToTask,它们均在此被重置为false。

其中,mMovedToFront代表当Task可复用时,是否须要将指标Task挪动到前台;mAddingToTask代表是否要将Activity退出到Task中。

(2)计算确认启动时的flag

该步骤会通过computeLaunchingTaskFlags()办法,依据launchMode、起源Activity的属性等进行初步计算,确认LaunchFlags。

此处重点解决起源Activity为空的各类场景,与咱们上文中的几种场景无关,故不再开展解说。

(3)获取能够复用的Task

该步骤通过调用getReusableTask()实现,用来查找有没有能够复用的Task。

先说论断:场景0123中,都能获取到能够复用的Task,而场景4中,未获取到可复用的Task。

为什么场景4不能够复用?咱们看一下getReusableTask()的要害实现。

上图(标注1)中,putIntoExistingTask代表是否能放入曾经存在的Task。当flag含有NEW_TASK且不含MULTIPLE_TASK时,或指定了singleInstance或singleTask的launchMode等条件,且没有指定Task或要求返回后果 时,场景01234均满足了条件。

而后,上图(标注2)通过findTask()查找能够复用的Task,并将过程中找到的栈顶Activity赋值给intentActivity。最终,上图(标注3)将intentActivity对应的Task作为后果。

findTask()是怎么查找哪个Task能够复用呢?

次要是确认两种后果mIdealRecord——“现实的ActivityRecord”  和 mCandidateRecord——"候选的ActivityRecord",作为intentActivity,并取intentActivity对应的Task作为复用Task。

什么ActivityRecord才是现实或候选的ActivityRecord呢?在mTmpFindTaskResult.process()中确认。

程序会将以后零碎中所有的Task进行遍历,在每个Task中,进行如上图所示的工作——将Task的底部Activity realActivity与指标Activity cls进行比照。

场景012中,咱们想跳转Activity2,即cls是Activity2,与Task底部的realActivity2雷同,则将该Task顶部的Activity3 r作为“现实的Activity”;

场景3中,咱们想跳转Activity3,即cls是Activity3,与Task底部的realActivity2不同,则进一步判断task底部Activity2与指标Activity3的栈亲和行,具备雷同亲和性,则将Task的顶部Activity3作为“候选Activity”;

场景4中,所有条件都不满足,最终没能找到可复用的Task。在执行完getReusableTask()后将mAddingToTask赋值为true

由此,咱们就能解释【场景4】中,新建了Task的景象。

(4)确定是否须要将指标Task挪动到前台

如果存在可复用的Task,场景0123会执行recycleTask(),该办法中会相继进行几个操作:setTargetRootTaskIfNeeded()、complyActivityFlags()。

首先,程序会执行setTargetRootTaskIfNeeded(),用来确定是否须要将指标Task挪动到前台,应用mMovedToFront作为标识。

在【场景123】中,起源Task和指标Task是不同的,differentTopTask为true,再通过一系列Task属性比照,可能得出mMovedToFront为true;

而场景0中,起源Task和指标Task雷同,differentTopTask为false,mMovedToFront放弃初始的false。

由此,咱们就能解释【场景0】中,Task不会产生切换的景象。

(5)通过比照flag、Intent、Component等确认是否要将Activity退出到Task中

还是在【场景0123】中,recycleTask()会继续执行complyActivityFlags(),用来确认是否要将Activity退出到Task中,应用mAddingToTask作为标识。

该办法会对FLAG_ACTIVITY_NEW_TASK、FLAG_ACTIVITY_CLEAR_TASK、FLAG_ACTIVITY_CLEAR_TOP等诸多flag、Intent信息进行一系列判断。

上图(标注1)中,会先判断后续是否须要重置Task,resetTask,判断条件则是FLAG_ACTIVITY_RESET_TASK_IF_NEEDED,显然,场景0123的resetTask都为false。继续执行。

接着,会有多种条件判断按程序执行。

在【场景3】中,指标Component(mActivityComponent)是B-3,指标Task的realActivity则是B-2,两者不雷同,进入了resetTask相干的判断(标注2)。

之前resetTask曾经是false,故【场景3】的mAddingToTask脱离原始值,被置为true。

在【场景012】中,绝对比的两个Activity都是B-2(标注3),能够进入下一级判断——isSameIntentFilter()。

这一步判断的内容就很显著了,指标Activity2的已有Intent 与 新的Intent做比照。很显然,场景2中因为改为了setClassName跳转,Intent天然不一样了。

故【场景2】的mAddingToTask脱离原始值,被置为true。

总结看一下:

【场景123】的mMovedToFront最先被置为true,而【场景0】经验重重考验,放弃初始值为false。

——这意味着当有可复用Task时,【场景0】不须要把Task切换到前列;【场景123】须要切换到指标Task。

【场景234】的mAddingToTask别离在不同阶段被置为true,而【场景01】,始终保持初始值false。

——这意味着,【场景234】须要将Activity退出到Task中,而【场景01】不再须要。

(6)理论启动Activity或间接返回后果

被启动的各个Activity会通过resumeFocusedTasksTopActivities()等一系列操作,开始真正的启动与生命周期的调用。

咱们对于上述各个场景的摸索曾经失去答案,后续流程便不再关注。

四、问题修复及遗留问题解答

4.1 问题修复

既然后面总结了这么多必要条件,咱们只须要毁坏其中的某些条件,就能够修复业务中遇到的问题了,简略列举几个的计划。

  • 计划一:批改flag。B-3跳转B-2时,减少FLAG_ACTIVITY_CLEAR_TASK或FLAG_ACTIVITY_CLEAR_TOP,或者间接不设置flag。教训证可行。
  • 计划二:批改intent属性,即【场景2】。A-1通过action形式隐式跳转B-2,则B-3能够通过setClassName形式,或批改action内属性的形式跳转B-2。教训证可行。
  • 计划三:提前移除B-2。B-2跳转B-3时,finish掉B-2。须要留神的是,finish()要在startActivity()之前执行,以防止遗留的ActivityRecord和Intent信息对后续跳转的影响。尤其是当你把B-2作为本人利用的deeplink散发Activity时,更值得警觉。

4.2 遗留问题

还记得咱们在文章开始的某个纳闷吗,为什么没有回调onNewIntent()?

onNewIntent() 会通过deliverNewIntent()触发,而deliverNewIntent()仅通过以下两个办法调用。

complyActivityFlags()就是上文3.3.1.5中咱们着重探讨的办法,能够发现complyActivityFlags()中所有可能调用deliverNewIntent()的条件均被完满避开了。

而deliverToCurrentTopIfNeeded()办法则如下图所示。

mLaunchFlags和mLaunchMode,无奈满足条件,导致dontStart为false,无缘deliverNewIntent()。

至此,onNewIntent()的问题失去解答。

五、结语

通过一系列场景假如,咱们发现了许多出其不意的景象:

  1. 文档提到FLAG_ACTIVITY_NEW_TASK等价于singleTask,与事实并不齐全如此,只有与其余flag搭配能力达到类似的成果。这一flag的正文十分全面,甚至会引发误会,繁多因素无奈决定整体体现。
  2. 官网文档提到
  3. START_DELIVERED_TO_TOP会将新的Intent传递给顶层Activity,但事实上,并不是每一种START_DELIVERED_TO_TOP都会把新的Intent从新散发。
  4. 同一个栈底Activity,前后两次都通过action或都通过setClassName跳转到时,第二次跳转居然会失败,而两次用不同形式跳转时,则会胜利。
  5. 单纯应用FLAG_ACTIVITY_NEW_TASK时,跳栈底Activity和跳同栈内其余Activity的成果天壤之别。

业务中遇到的问题,归根结底就是对Android栈机制不够理解造成的。

在面对栈相干的编码时,开发者务必要想分明,承当新开利用栈的Activty在利用全局承当怎么的使命,要对Task历史、flag属性、launchMode属性、Intent内容等全面评估,审慎参考官网文档,能力防止栈陷阱,达成现实牢靠的成果。