作者: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-3
public 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-4
public 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-2
public 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-2
public 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-3
public 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内容等全面评估,谨慎参阅官方文档,才干避免栈陷阱,达到抱负可靠的作用。