这篇文章主要记录一下遇到的android启动过程中的一个有意思的探索过程,可能文章会比较长,相信我只要读下去一定会有所收获。这里说明一下,这篇文章肯定会涉及到activity的启动流程,很多文章都已经介绍了,这里不会带着大家看具体函数,因为太枯燥了,包括我本人也看不下去,力图简单明了。
一、问题的出现
最早这个问题是测试发现的一个怪异的现象。在登录失效的情况下,在我们的应用的个人页面进行手动刷新,会有很多接口在请求回来后,发登录失效的广播,而我们的广播处理比较简单,去启动一个设置为singleTop启动模式的LoginActivity。注意这时可能有多个广播出现。为了方便说明,这里假定只有两个,调试发现,调起LoginActivity后,点击back键,必须点两次才能回退成功,第一次貌似没反应,因为我们的应用是建立在插件框架的基础上的,第一次看到这个现象的时候,怀疑是插件框架导致singleTop失效,当把Activity的生命周期打印出来后,发现了一个诡异的现象:调用了两次startActivity,发现LoginActivity的onCreate方法只执行了一次,但是当我点击back键后,LoginActivity的onCreate又执行了一次。 什么,点击back键新建了一个Activity,你相信吗? 上面的问题可以简化成以下情景来表述。新建两个Activity:MainActivity和SecondActivity。代码非常简单,MainActivity只放置个Button,点击一次,启动两次Activity,SecondActivity就更简单了,只打印生命周期即可,代码如下: MainActivity.java:
1 | public void clickToStart(View view){ |
SecondActivity.java:
1 | public class SecondActivity extends Activity { |
当我单击一下按钮,你可以猜一下生命周期的回调顺序,这里就不卖关子了,直接打印出来的是:
1 | 05-18 22:57:38.870 25377-25377/com.example.ljj.test2 I/lan: MainActivity->onPause |
是不是有点吃惊,明明启动两次,却只执行了一次onCreate,按道理如果是singleTop生效的话,起码也得是一次onCreate和一次onNewIntent吧。此时点击back键,打印生命周期会发现已经启动的SecondActivity执行了onPause,然后又新建了一个SecondActivity。
1 | 05-18 22:57:49.924 25377-25377/com.example.ljj.test2 I/lan: SecondActivity->onPause: 29211210 |
为了方便研究,我们可以尝试着把singleTop启动模式去掉(不要怀疑手动设置Flag和manifest下写死的区别,singleTop模式下是等效的),执行同样的操作,你会发现standard启动模式下,现象也是一样的。 相信现象我已经阐述的很清楚了,现在总结一下,大概有两个问题: 第一,为什么标准启动模式下启动两次activity,只执行了一次onCreate方法,另一次是在onPause后才执行。 第二,为什么singleTop模式在这种情景下会失效。
二、针对问题的思考与初步探索
首先,我们把第二个问题先放一放,根源应该是在第一个问题上,先看一下针对打印出来的log的思考。SecondActivity确实是启动了两次,那到底先启动的是第一个,还是点击一次回退键后启动的是第一个?这里我们假设一下: 假设一:最先启动的是我们第一次执行startActivity启动的那个目标Activity,那么说明第二次执行的startActivity的目标Activity被暂存了,但是暂存在了哪里呢?好像AMS没有这样的功能 假设二:最先启动的是我们第二次执行startActivity启动的那个目标,可能因为启动太快,导致第一次启动的Activity被压到栈里,但不是栈顶,当SecondActivity执行到onPause时,它才有机会重见天日。 怎么验证呢,这时候不要忘了adb shell dumpsys的功能。当我们点击click按钮后,执行adb shell dumpsys activity activities >log.txt来查看系统中所有的activity信息。
1 | ACTIVITY MANAGER ACTIVITIES (dumpsys activity activities) |
这里为什么要贴这么长的信息,因为这段信息非常重要,而且想说一下activity是如何管理的,或者说这段信息怎么看? 上图中显示了AMS中对Activity的管理结构。注意这些数据结构都是运行在系统进程中的。这里介绍下这些数据结构,让大家有个比较清晰的认识,不然在看Activity启动流程时很容易被这些栈搞晕。 ActivityRecord:Activity管理的最小单位,我们在应用进程创建一个activity,在AMS进程中就会生成一个ActivityRecord与之对应,类似于 packageName,launchedFromUid等都是ActivityRecord类的成员变量。 ActivityRecord源码链接(基于android5.0)
1 | * Hist #0: ActivityRecord{177c5b58 u0 com.example.ljj.test2/.MainActivity t1322} |
TaskRecord:是一个栈式管理结构,每一个TaskRecord都可能存在一个或多个ActivityRecord,栈顶的ActivityRecord表示当前可见的界面,这里需要注意的是 我们平时讨论的Activity任务栈实际上指的就是TaskRecord对象,而不是ActivityStack对象,包括我们如果同时启动多个应用,只是会在同一个ActivityStack中生成多个TaskRecord而已。简单理解就是TaskRecord就是我们经常说的任务栈。同理,userId,affinity等也是TaskRecord类的成员变量,可自行查看源码了解。
1 | * TaskRecord{22c865b8 #1322 A=com.example.ljj.test2 U=0 sz=3} |
ActivityStack:也是一个栈式管理结构,每一个ActivityStack都可能存在一个或多个TaskRecord,栈顶的TaskRecord表示当前可见的任务;一般情况下,会存在两个ActivityStack,一个是应用的Stack,还有一个HomeStack。当我们启动应用或者从后台切换应用到前台时,应用Stack会被切换到栈顶,反之HomeStack会被切换到栈顶。对应于log信息为:
1 | Stack #1: |
ActivityDisplay: 是ActivityStackSupervisor的一个内部类,它对应于一个显示设备,不考虑多屏显示的情况下,就是指的手机屏幕,所以一般情况下,维护的ActivityDisplay数组的长度为1. ProcessRecord:每个进程会生成一个ProcessRecord对象,所有的ProcessRecord对象都保存在AMS的一个SparseArray类型的mPidsSelfLocked变量里。运行在不同TaskRecord中的ActivityRecord可能是属于同一个 ProcessRecord。ProcessRecord非常重要,假设我们创建了一个ActivityRecord,默认情况下,是没有和进程关联的,通过ProcessRecord的addPackage方法我们可以添加ActivityRecord到ProcessRecord中,还需要将ProcessRecord绑定到ActivityRecord上,这个ActivityRecord才是一个有“生命”,有“交互能力”的Activity。
通过上面的介绍,相信再看上面的信息会非常容易了。我们仔细观察dumpsys出来的信息,会发现两个地方值得注意。
1 | Running activities (most recent first): |
当前TaskRecord中乍一看只有两个ActivityRecord,但是不要被蒙蔽了,此时的sz=3,笔者最早看的时候时候就被蒙蔽了,所以要仔细看详细信息。 第二个异常点在Hist #1的activity的state是NITIALIZING。从字面上理解就是在初始化状态。
1 | Hist #1: ActivityRecord{3419ba07 u0 com.example.ljj.test2/.SecondActivity t1322} |
至此,我们大致能得出初步结论了,启动两次Activity,确实有两个Activity被放入了TaskRecord中了。栈顶的Activity能正常启动,被栈顶压住的Activity为INITIALIZING状态。那么问题又来了,INITIALIZING状态是个什么鬼?从源码角度如何解释这种生命周期的回调顺序?
三、源码维度探索
接下来会从源码的角度进行分析,里面会涉及到一些Activity启动流程相关的东西,不清楚的同学可以找其他文章学习下,直接看也没关系,因为不会涉及到很多代码细节。我们都知道activity的启动是和当前resume状态Activity的onPause事件相关,所以我们集中精力在onPause事件回调前的启动流程上,忽略后面的流程。 上面这张图简单的展示了当我们发起startActivity到前一个actiivty执行onPause的流程,讲解之前大致说一下Activity启动过程中应用进程和AMS进程通信的过程。
上面这种图清晰的展示了应用进程和AMS交互的方式。 大家可以简单的这样理解,两个进程通信就是通过两个顶层的Binder接口实现的: IApplicationThread:是系统进程请求应用进程的接口。Binder的服务端是运行在应用进程的,具体来讲就是ApplicationThread。AMS需要应用进程做的事情都是通过IApplicationThread接口中定义的方法来实现的。比如说schedulePauseActivity(告诉应用进程可以执行Activity的onPause了),scheduleLauchActivity(告诉应用进程现在可以执行启动Activity的操作了)等等。 IActivityManager:是应用进程请求系统进程的接口。Binder的服务端是运行在系统进程的,具体来讲就是ActivityManagerService。比如启动Activity的操作,实际上是向系统发出的申请,就是通过该接口的startActivity方法执行的。 了解到这里就足够啦,我们回过头来看我们的问题。我们先来看Activity启动图上面标红的部分,需要注意的是我们在应用进程发起的startActivity是带int返回值的,换句话说,就是返回值回来之前,主线程都是被挂起的,简单理解为是startActivity这个函数在获取返回值之前都是同步的。 Activity的启动源码确实非常复杂,上面标红的很多函数代码都有几百上千行。这里我可能有的地方直接给大家部分结论,感兴趣的同学自行去翻看源码。 我们先来看我们在onClick事件中第一次启动Activiy的过程。我们可以视作为这次启动是一次正常的启动。 在ActivityStackSuperVisor类可以看作是所有ActivityStack的管理者。从命名中也可以看出来,比如将哪个栈放到栈顶,管理当前前台的任务栈等都是ActivityStackSuperVisor完成的。在ActivityStackSuperVisor的startActivityLocked方法中会创建ActivityRecord对象,作为待启动Activity在系统进程的描述。那么是在哪个方法中将ActivityRecord放入栈中的呢?答案就是在ActivityStack的startActivityLocked方法,在该方法中会调用addActivityToTop方法将ActivityRecord放入到对应的TaskRecord的栈顶。 也就是说我们第一次启动Activity是肯定放入了栈中的。并且会顺利的执行到ActivityStack的startPausingLocked方法。
1 | final int startActivityUncheckedLocked(ActivityRecord r, ActivityRecord sourceRecord, |
可以看出来我们通过调用startActivity最终的返回值是由startActivityUncheckedLocked方法返回的。并且在返回前,会调用targetStack.startActivityLocked方法,最终会执行到startPausingLocked函数。
1 | final boolean startPausingLocked(boolean userLeaving, boolean uiSleeping, boolean resuming, |
注意我们是在分析第一次启动Activity的情形,mResumedActivity此时肯定是我们的MainActivity,通过上面代码执行后,prev,mPausingActivity,mLastPausedActivity指向了MainActivity,并且MainActivity的状态被更新为pausing状态,然后执行prev.app.thread.schedulePauseActivity方法,去通知应用进程MainActivity需要执行pause事件了。这里我们可以看到,pause事件是通过ActivityThread中的名为H的handler来处理的。
1 | public final void schedulePauseActivity(IBinder token, boolean finished, boolean userLeaving, int configChanges, |
看到这,我们在回过头来看我们的demo。你可以猜一下输出的log顺序。
1 | public void clickToStart(View view) { |
1 | 05-21 01:37:04.027 2084-2084/com.example.ljj.test2 I/lan: clickToStart:->once complete |
结果和我们上面分析的是一致的,因为onClick内的内容是在同一个Message中的。当我们第一次执行startActivity时,会抛出一个H.PAUSE_ACTIVITY的Message到主线程的消息队列里,但是这个消息的执行肯定是要等到onClick事件完全执行完才能处理的,这是消息队列的特点,必须等上一个消息执行完,才会轮到下一个消息的执行。 这里还差一点没有解释到位,就是在这种情境下,执行第二次startActivity时,在系统进程中走的流程和第一次启动一样吗?这里好像缺少了一些解释。上面我们分析到第一次startActivity,在startPausingLocked函数中,执行了Binder的schedulePauseActivity方法,该方法内发送了一个名为H.PAUSE_ACTIVITY的消息,按照正常逻辑,当应用内处理完H.PAUSE_ACTIVITY消息后,会向AMS发送通知,AMS接收到pause的通知后,会重新执行resumeTopActivityLocked函数,进而执行startSpecificActivityLocked函数来完成Activity真正的启动。但是在本文的情景中,这个H.PAUSE_ACTIVITY在被处理前,我们又执行了一次startActivity的操作,那么第二次启动会不会也执行到startPausingLocked时,再一次抛出去一个H.PAUSE_ACTIVITY呢,答案是不会的,分析如下:前面创建ActivityRecord和加入栈中操作都是一样的,同样会执行到startPausingLocked函数。
1 | final boolean startPausingLocked(boolean userLeaving, boolean uiSleeping, boolean resuming, |
注意,我们第一次启动时执行过一次startPausingLocked时,mResumedActivity已经被置为null,所以此时 执行ActivityRecord prev = mResumedActivity;相当于prev也为null了,这时候就会走if(prev==null)分支 了,返回了false。所以第二次启动是不会在抛出HH.PAUSE_ACTIVITY消息的。mStackSupervisor.resumeTopActivitiesLocked();会根据mResumedActivity和mPausingActivity状态进行各种判断,最后会在allPausedActivitiesComplete()方法判断中发现有activity执行pausing未完成,直接返回了false,受篇幅原因,具体的就不带着大家细看了,感兴趣的同学可以自行查阅。
1 | if (prev == null) { |
分析到这里,我们先来总结下在向下进行:到目前为止,我们得出的结论是: 假设我在同一个方法里面连续启动两次Activity,两个actiivty都会入栈,第一次启动的activity抛出了pause的Message,第二次没有抛出,而抛出的pause消息要等到第二次启动完成才能得到执行。当onPause执行完后,通知AMS要启动Activity了,此时AMS从栈顶取Activity,自然拿到的是第二个Activity,执行第二个Activity的onCreate事件。这样就完全解释清楚了上面现象的缘由。
四、 进一步验证
下面我们通过查看消息队列中的内容的方式来验证,查看源码发现,MessageQueue中提供了dump方法,可以获取到queue中的内容,接下来就简单了,反射拿到它,封装了个简单的MessageDump类。
1 | public class MessageDump { |
我们加入到代码中,分别在第一次启动前,第一次执行后,第二次执行后取dump消息队列:
1 | public void clickToStart(View view) { |
1 | 05-21 02:42:28.989 32626-32626/com.example.ljj.test2 I/lan: clickToStart:->before->first |
通过log我们清晰的看到确实在执行第一次启动后,消息队列中多了一条消息,并且在第二次启动完毕后,该消息依然存在,与我们之前的结论是相符的。
1 | { when=-1ms what=101 arg1=1 obj=android.os.BinderProxy@3e564eb2 target=android.app.ActivityThread$H } |
既然这样,那我第二次启动时通过Handler异步起动是不是就好了呢?
1 | public void clickToStart(View view) { |
结果我就不贴了,如果你理解了上文中讲的内容,应该可以猜到这种情形下,启动就正常了,因为我们第二次启动的消息是放在了pause消息之后了。
五、 singleTop失效的秘密
还有一个小问题,就是如果启动模式设置为singleTop的时候为什么会失效呢? 按道理来说,即使是第一次启动时被压入了栈中,没有正常启动,那intent信息总是在的啊,按道理不应该会影响启动模式才对。我们可以先看下IActivityManager的startActivity的返回值是什么?在执行startActivity时是会得到一个返回值的,这个返回值是反映Activity的启动状态的,和启动模式关系很大。
1 | public ActivityResult execStartActivity( |
我们通过动态代理来hook掉IActivityManager,可以拿到该返回值。这个返回值类型定义在了ActivityManager.java中,和启动模式相关的是下面这几个。
1 | public static final int START_SUCCESS = 0;//正常启动 |
通过hook拿到返回值会发现,连续两次启动时,返回值均为0,而加入handler异步起动后,得到的返回值是0,3。那就说明两次启动都是正常启动,但是第二次启动也是按照普通模式启动了,所以返回的是0。这下我们就可以有目的的去看源码了,只需要看到关于intent的处理部分即可。源码在ActivityStackSupervisor类的startActivityUncheckedLocked方法中。
1 | if (r.packageName != null) { |
我们重点来看判定条件,top是拿的栈顶的activity,此时是第一次启动放进去的SecondActiivty,说明一下,topRunningNonDelayedActivityLocked函数是拿取除了notTop指定的activity外,位于栈顶的activity。一般notTop为null。很显然第一层,第二层的if语句都是true。最里层的if语句呢?其实我一开始看的时候没有注意最里层的if,理所当然的认为不为null,导致这里找了半天都觉得说不通。
1 | ActivityStack topStack = getFocusedStack(); |
无奈之下,想从最早dump出来的信息里面找一下这个activity和正常启动的activity的差别。
原来在这,正常启动的activity都是绑定了ProcessRecord的,而INITIALIZING状态的Activity是没有进程相关信息的,也就是它不属于任何一个进程,还不具备和应用进程交互的能力。 真正绑定的地方在realStartActivityLocked方法中,也就是真正启动的过程中会进行绑定。现在就解释清楚了为什么singleTop在这种情境下失效了吧。
六. 实际应用场景的思考及解决办法
有人可能会说,实际场景中哪里有人会在一个函数中或者一个message中去启动两次activity?确实这种场景几乎没有,但是我们来看一下以下的情景。
1 | public void clickToStart(View view) { |
1 | private class MyReceiver extends BroadcastReceiver { |
我们用上述代码模拟了一下可能在实际代码中出现的场景,比如我们不同的网络接口请求,返回来后需要发送一个广播。我们应该都是post一个消息到主线程处理的,对应于消息1和消息2,如果恰好消息1和消息2被放入消息队列的时间比较接近,或者说是相邻。此时就会出现本文出现的现象。广播肯定也是进程间通信后,发送消息到主线程执行的,对应于消息3,消息4,所以当clickToStart执行完后,消息队列从头到尾:消息1->消息2,当消息1(发送广播)执行完后,变成:消息2->消息3;当消息2(发送广播)执行完后,变成:消息3->消息4。消息3和消息4挨着,两个消息的任务都是startActivity,那么很明显,这就是咱们的demo啊,肯定会有问题。不信可以自行尝试。怎么解决呢,有人说在onReceive里异步启动啊,其实是解决不了问题的,全部异步启动和不加异步是一样的,消息在队列里的顺序是不会改变的。 这种问题吧,首先要看下需不需要解决,说的直白点,也不能算是个bug,至于解决的方法,要和具体业务而定。具体解决的办法可以封装一个handler,该handler的任务保证唯一,或者指定what类型,在startActivity的函数中,remove同类型的信息,也就是上文中消息3->消息4,保证消息3->消息4的消息类型一致,然后在执行消息3后,remove掉消息4即可。
1 | public void clickToStart(View view) { |
这种方式也不太优雅,解决的办法肯定有很多,这里只是举个例子而已。知道了问题的缘由后,根据业务自行处理即可。
七. 总结
这篇文章主要是为了阐述一下activity连续启动的异常问题的跟踪过程。总结如下:
- 如果在同一个message连续启动同一个activity或者相邻message中分别启动同一个activity,就会出现生命周期诡异的问题,当然现在已经不诡异了。从探索方法和源码的角度给出了解释。
- 解释了为什么singleTop在这种模式下会失效的问题
- 给出了解决的思路,就是破坏消息顺序。
- 此外,阐述了Activity栈和启动activity流程相关的知识