你可能不知道的Activity启动的诡异现象探索
这篇文章主要记录一下遇到的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:
public void clickToStart(View view){
startSecondActivity();
startSecondActivity();
}
private void startSecondActivity(){
Intent intent=new Intent(MainActivity.this,SecondActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
startActivity(intent);
}
SecondActivity.java:
public class SecondActivity extends Activity {
public static final String TAG="lan";
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.i(TAG, "SecondActivity->onCreate: "+hashCode());
}
@Override
protected void onResume() {
super.onResume();
Log.i(TAG, "SecondActivity->onResume: "+hashCode());
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
Log.i(TAG, "SecondActivity->onNewIntent: "+hashCode());
}
@Override
protected void onPause() {
super.onPause();
Log.i(TAG, "SecondActivity->onPause: "+hashCode());
}
}
当我单击一下按钮,你可以猜一下生命周期的回调顺序,这里就不卖关子了,直接打印出来的是:
05-18 22:57:38.870 25377-25377/com.example.ljj.test2 I/lan: MainActivity->onPause
05-18 22:57:38.890 25377-25377/com.example.ljj.test2 I/lan: SecondActivity->onCreate: 29211210
05-18 22:57:38.892 25377-25377/com.example.ljj.test2 I/lan: SecondActivity->onResume: 29211210
是不是有点吃惊,明明启动两次,却只执行了一次onCreate,按道理如果是singleTop生效的话,起码也得是一次onCreate和一次onNewIntent吧。此时点击back键,打印生命周期会发现已经启动的SecondActivity执行了onPause,然后又新建了一个SecondActivity。
05-18 22:57:49.924 25377-25377/com.example.ljj.test2 I/lan: SecondActivity->onPause: 29211210
05-18 22:57:49.926 25377-25377/com.example.ljj.test2 I/lan: SecondActivity->onCreate: 706348141
05-18 22:57:49.928 25377-25377/com.example.ljj.test2 I/lan: SecondActivity->onResume: 706348141
为了方便研究,我们可以尝试着把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信息。
ACTIVITY MANAGER ACTIVITIES (dumpsys activity activities)
Display #0 (activities from top to bottom):
Stack #1:
Task id #1322
* TaskRecord{22c865b8 #1322 A=com.example.ljj.test2 U=0 sz=3}
userId=0 effectiveUid=u0a220 mCallingUid=u0a39 mCallingPackage=com.android.launcher3
affinity=com.example.ljj.test2
intent={act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10200000 cmp=com.example.ljj.test2/.MainActivity}
realActivity=com.example.ljj.test2/.MainActivity
autoRemoveRecents=false isPersistable=true numFullscreen=3 taskType=0 mTaskToReturnTo=1
rootWasReset=true mNeverRelinquishIdentity=true mReuseTask=false
Activities=[ActivityRecord{177c5b58 u0 com.example.ljj.test2/.MainActivity t1322}, ActivityRecord{3419ba07 u0 com.example.ljj.test2/.SecondActivity t1322}, ActivityRecord{373cd2d2 u0 com.example.ljj.test2/.SecondActivity t1322}]
askedCompatMode=false inRecents=true isAvailable=true
lastThumbnail=null lastThumbnailFile=/data/system/recent_images/1322_task_thumbnail.png
hasBeenVisible=true firstActiveTime=1526700478326 lastActiveTime=1526700478326 (inactive for 63s)
* Hist #2: ActivityRecord{373cd2d2 u0 com.example.ljj.test2/.SecondActivity t1322}
packageName=com.example.ljj.test2 processName=com.example.ljj.test2
launchedFromUid=10220 launchedFromPackage=com.example.ljj.test2 userId=0
app=ProcessRecord{2a18f91 25377:com.example.ljj.test2/u0a220}
Intent { flg=0x20000000 cmp=com.example.ljj.test2/.SecondActivity }
frontOfTask=false task=TaskRecord{22c865b8 #1322 A=com.example.ljj.test2 U=0 sz=3}
taskAffinity=com.example.ljj.test2
realActivity=com.example.ljj.test2/.SecondActivity
baseDir=/data/app/com.example.ljj.test2-2/base.apk
dataDir=/data/data/com.example.ljj.test2
stateNotNeeded=false componentSpecified=true mActivityType=0
compat={320dpi always-compat} labelRes=0x7f060021 icon=0x7f03000a theme=0x7f0800a3
config={1.0 310mcc270mnc en_US ?layoutDir sw768dp w768dp h951dp 320dpi xlrg port finger qwerty/v/v dpad/v s.6}
taskDescription: iconFilename=null label="null" color=ff3f51b5
launchFailed=false launchCount=1 lastLaunchTime=-1m3s430ms
haveState=false icicle=null
state=RESUMED stopped=false delayedResume=false finishing=false
keysPaused=false inHistory=true visible=true sleeping=false idle=true
fullscreen=true noDisplay=false immersive=false launchMode=1
frozenBeforeDestroy=false forceNewConfig=false
mActivityType=APPLICATION_ACTIVITY_TYPE
waitingVisible=false nowVisible=true lastVisibleTime=-1m2s804ms
* Hist #1: ActivityRecord{3419ba07 u0 com.example.ljj.test2/.SecondActivity t1322}
packageName=com.example.ljj.test2 processName=com.example.ljj.test2
launchedFromUid=10220 launchedFromPackage=com.example.ljj.test2 userId=0
app=null
Intent { flg=0x20000000 cmp=com.example.ljj.test2/.SecondActivity }
frontOfTask=false task=TaskRecord{22c865b8 #1322 A=com.example.ljj.test2 U=0 sz=3}
taskAffinity=com.example.ljj.test2
realActivity=com.example.ljj.test2/.SecondActivity
baseDir=/data/app/com.example.ljj.test2-2/base.apk
dataDir=/data/data/com.example.ljj.test2
stateNotNeeded=false componentSpecified=true mActivityType=0
compat=null labelRes=0x7f060021 icon=0x7f03000a theme=0x7f0800a3
config={1.0 310mcc270mnc en_US ?layoutDir sw768dp w768dp h951dp 320dpi xlrg port finger qwerty/v/v dpad/v s.6}
launchFailed=false launchCount=0 lastLaunchTime=0
haveState=true icicle=null
state=INITIALIZING stopped=false delayedResume=false finishing=false
keysPaused=false inHistory=true visible=false sleeping=false idle=false
fullscreen=true noDisplay=false immersive=false launchMode=1
frozenBeforeDestroy=false forceNewConfig=false
mActivityType=APPLICATION_ACTIVITY_TYPE
* Hist #0: ActivityRecord{177c5b58 u0 com.example.ljj.test2/.MainActivity t1322}
packageName=com.example.ljj.test2 processName=com.example.ljj.test2
launchedFromUid=10039 launchedFromPackage=com.android.launcher3 userId=0
app=ProcessRecord{2a18f91 25377:com.example.ljj.test2/u0a220}
Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10200000 cmp=com.example.ljj.test2/.MainActivity (has extras) }
frontOfTask=true task=TaskRecord{22c865b8 #1322 A=com.example.ljj.test2 U=0 sz=3}
taskAffinity=com.example.ljj.test2
realActivity=com.example.ljj.test2/.MainActivity
baseDir=/data/app/com.example.ljj.test2-2/base.apk
dataDir=/data/data/com.example.ljj.test2
stateNotNeeded=false componentSpecified=true mActivityType=0
compat={320dpi always-compat} labelRes=0x7f060021 icon=0x7f03000a theme=0x7f0800a3
config={1.0 310mcc270mnc en_US ?layoutDir sw768dp w768dp h951dp 320dpi xlrg port finger qwerty/v/v dpad/v s.6}
taskDescription: iconFilename=null label="null" color=ff3f51b5
launchFailed=false launchCount=0 lastLaunchTime=-1m4s921ms
haveState=true icicle=Bundle[mParcelledData.dataSize=272]
state=STOPPED stopped=true delayedResume=false finishing=false
keysPaused=false inHistory=true visible=false sleeping=false idle=true
fullscreen=true noDisplay=false immersive=false launchMode=0
frozenBeforeDestroy=false forceNewConfig=false
mActivityType=APPLICATION_ACTIVITY_TYPE
waitingVisible=false nowVisible=false lastVisibleTime=-1m4s305ms
Running activities (most recent first):
TaskRecord{22c865b8 #1322 A=com.example.ljj.test2 U=0 sz=3}
Run #1: ActivityRecord{373cd2d2 u0 com.example.ljj.test2/.SecondActivity t1322}
Run #0: ActivityRecord{177c5b58 u0 com.example.ljj.test2/.MainActivity t1322}
mResumedActivity: ActivityRecord{373cd2d2 u0 com.example.ljj.test2/.SecondActivity t1322}
Stack #0:
Task id #1295
* TaskRecord{33ae7ef6 #1295 A=com.android.launcher3 U=0 sz=1}
userId=0 effectiveUid=u0a39 mCallingUid=1000 mCallingPackage=android
affinity=com.android.launcher3
.......
.......
这里为什么要贴这么长的信息,因为这段信息非常重要,而且想说一下activity是如何管理的,或者说这段信息怎么看?
上图中显示了AMS中对Activity的管理结构。注意这些数据结构都是运行在系统进程中的。这里介绍下这些数据结构,让大家有个比较清晰的认识,不然在看Activity启动流程时很容易被这些栈搞晕。
ActivityRecord:Activity管理的最小单位,我们在应用进程创建一个activity,在AMS进程中就会生成一个ActivityRecord与之对应,类似于
packageName,launchedFromUid等都是ActivityRecord类的成员变量。
ActivityRecord源码链接(基于android5.0)
* Hist #0: ActivityRecord{177c5b58 u0 com.example.ljj.test2/.MainActivity t1322}
packageName=com.example.ljj.test2 processName=com.example.ljj.test2
launchedFromUid=10039 launchedFromPackage=com.android.launcher3 userId=0
app=ProcessRecord{2a18f91 25377:com.example.ljj.test2/u0a220}
Intent { act=android.intent.action.MAIN cat=
TaskRecord:是一个栈式管理结构,每一个TaskRecord都可能存在一个或多个ActivityRecord,栈顶的ActivityRecord表示当前可见的界面,这里需要注意的是 我们平时讨论的Activity任务栈实际上指的就是TaskRecord对象,而不是ActivityStack对象,包括我们如果同时启动多个应用,只是会在同一个ActivityStack中生成多个TaskRecord而已。简单理解就是TaskRecord就是我们经常说的任务栈。同理,userId,affinity等也是TaskRecord类的成员变量,可自行查看源码了解。
* TaskRecord{22c865b8 #1322 A=com.example.ljj.test2 U=0 sz=3}
userId=0 effectiveUid=u0a220 mCallingUid=u0a39 mCallingPackage=com.android.launcher3
affinity=com.example.ljj.test2
ActivityStack:也是一个栈式管理结构,每一个ActivityStack都可能存在一个或多个TaskRecord,栈顶的TaskRecord表示当前可见的任务;一般情况下,会存在两个ActivityStack,一个是应用的Stack,还有一个HomeStack。当我们启动应用或者从后台切换应用到前台时,应用Stack会被切换到栈顶,反之HomeStack会被切换到栈顶。对应于log信息为:
Stack #1:
Task id #1322
Stack #0:
Task id #1295
ActivityDisplay: 是ActivityStackSupervisor的一个内部类,它对应于一个显示设备,不考虑多屏显示的情况下,就是指的手机屏幕,所以一般情况下,维护的ActivityDisplay数组的长度为1.
ProcessRecord:每个进程会生成一个ProcessRecord对象,所有的ProcessRecord对象都保存在AMS的一个SparseArray类型的mPidsSelfLocked变量里。运行在不同TaskRecord中的ActivityRecord可能是属于同一个 ProcessRecord。ProcessRecord非常重要,假设我们创建了一个ActivityRecord,默认情况下,是没有和进程关联的,通过ProcessRecord的addPackage方法我们可以添加ActivityRecord到ProcessRecord中,还需要将ProcessRecord绑定到ActivityRecord上,这个ActivityRecord才是一个有“生命”,有“交互能力”的Activity。
通过上面的介绍,相信再看上面的信息会非常容易了。我们仔细观察dumpsys出来的信息,会发现两个地方值得注意。
Running activities (most recent first):
TaskRecord{22c865b8 #1322 A=com.example.ljj.test2 U=0 sz=3}
Run #1: ActivityRecord{373cd2d2 u0 com.example.ljj.test2/.SecondActivity t1322}
Run #0: ActivityRecord{177c5b58 u0 com.example.ljj.test2/.MainActivity t1322}
当前TaskRecord中乍一看只有两个ActivityRecord,但是不要被蒙蔽了,此时的sz=3,笔者最早看的时候时候就被蒙蔽了,所以要仔细看详细信息。
第二个异常点在Hist #1的activity的state是NITIALIZING。从字面上理解就是在初始化状态。
Hist #1: ActivityRecord{3419ba07 u0 com.example.ljj.test2/.SecondActivity t1322}
.......
.......
state=INITIALIZING stopped=false delayedResume=false finishing=false
至此,我们大致能得出初步结论了,启动两次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方法。
final int startActivityUncheckedLocked(ActivityRecord r, ActivityRecord sourceRecord,
IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor, int startFlags,
boolean doResume, Bundle options, TaskRecord inTask) {
...........
targetStack.mLastPausedActivity = null;
targetStack.startActivityLocked(r, newTask, doResume, keepCurTransition, options);
if (!launchTaskBehind) {
// Don't set focus on an activity that's going to the back.
mService.setFocusedActivityLocked(r);
}
return ActivityManager.START_SUCCESS;
}
可以看出来我们通过调用startActivity最终的返回值是由startActivityUncheckedLocked方法返回的。并且在返回前,会调用targetStack.startActivityLocked方法,最终会执行到startPausingLocked函数。
final boolean startPausingLocked(boolean userLeaving, boolean uiSleeping, boolean resuming,
boolean dontWait) {
if (mPausingActivity != null) {
Slog.wtf(TAG, "Going to pause when pause is already pending for " + mPausingActivity);
completePauseLocked(false);
}
ActivityRecord prev = mResumedActivity;
if (prev == null) {
if (!resuming) {
Slog.wtf(TAG, "Trying to pause when nothing is resumed");
mStackSupervisor.resumeTopActivitiesLocked();
}
return false;
}
if (mActivityContainer.mParentActivity == null) {
// Top level stack, not a child. Look for child stacks.
mStackSupervisor.pauseChildStacks(prev, userLeaving, uiSleeping, resuming, dontWait);
}
if (DEBUG_STATES) Slog.v(TAG, "Moving to PAUSING: " + prev);
else if (DEBUG_PAUSE) Slog.v(TAG, "Start pausing: " + prev);
mResumedActivity = null;
mPausingActivity = prev;
mLastPausedActivity = prev;
mLastNoHistoryActivity = (prev.intent.getFlags() & Intent.FLAG_ACTIVITY_NO_HISTORY) != 0
|| (prev.info.flags & ActivityInfo.FLAG_NO_HISTORY) != 0 ? prev : null;
prev.state = ActivityState.PAUSING;
prev.task.touchActiveTime();
clearLaunchTime(prev);
final ActivityRecord next = mStackSupervisor.topRunningActivityLocked();
if (mService.mHasRecents && (next == null || next.noDisplay || next.task != prev.task)) {
prev.updateThumbnail(screenshotActivities(prev), null);
}
stopFullyDrawnTraceIfNeeded();
mService.updateCpuStats();
if (prev.app != null && prev.app.thread != null) {
if (DEBUG_PAUSE) Slog.v(TAG, "Enqueueing pending pause: " + prev);
try {
EventLog.writeEvent(EventLogTags.AM_PAUSE_ACTIVITY,
prev.userId, System.identityHashCode(prev),
prev.shortComponentName);
mService.updateUsageStats(prev, false);
prev.app.thread.schedulePauseActivity(prev.appToken, prev.finishing,
userLeaving, prev.configChangeFlags, dontWait);
} catch (Exception e) {
// Ignore exception, if process died other code will cleanup.
Slog.w(TAG, "Exception thrown during pause", e);
mPausingActivity = null;
mLastPausedActivity = null;
mLastNoHistoryActivity = null;
}
} else {
mPausingActivity = null;
mLastPausedActivity = null;
mLastNoHistoryActivity = null;
}
.....
......
注意我们是在分析第一次启动Activity的情形,mResumedActivity此时肯定是我们的MainActivity,通过上面代码执行后,prev,mPausingActivity,mLastPausedActivity指向了MainActivity,并且MainActivity的状态被更新为pausing状态,然后执行prev.app.thread.schedulePauseActivity方法,去通知应用进程MainActivity需要执行pause事件了。这里我们可以看到,pause事件是通过ActivityThread中的名为H的handler来处理的。
public final void schedulePauseActivity(IBinder token, boolean finished, boolean userLeaving, int configChanges,
boolean dontReport) {
sendMessage(finished ? H.PAUSE_ACTIVITY_FINISHING : H.PAUSE_ACTIVITY, token,
(userLeaving ? 1 : 0) | (dontReport ? 2 : 0), configChanges);
}
看到这,我们在回过头来看我们的demo。你可以猜一下输出的log顺序。
public void clickToStart(View view) {
startSecondActivity();
Log.i(SecondActivity.TAG, "clickToStart:->once complete");
startSecondActivity();
Log.i(SecondActivity.TAG, "clickToStart:->twice complete");
}
@Override
protected void onPause() {
super.onPause();
Log.i(SecondActivity.TAG, "MainActivity->onPause ");
}
05-21 01:37:04.027 2084-2084/com.example.ljj.test2 I/lan: clickToStart:->once complete
05-21 01:37:04.029 2084-2084/com.example.ljj.test2 I/lan: clickToStart:->twice complete
05-21 01:37:04.030 2084-2084/com.example.ljj.test2 I/lan: MainActivity->onPause
结果和我们上面分析的是一致的,因为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函数。
final boolean startPausingLocked(boolean userLeaving, boolean uiSleeping, boolean resuming,
boolean dontWait) {
if (mPausingActivity != null) {
Slog.wtf(TAG, "Going to pause when pause is already pending for " + mPausingActivity);
completePauseLocked(false);
}
ActivityRecord prev = mResumedActivity;
if (prev == null) {
if (!resuming) {
Slog.wtf(TAG, "Trying to pause when nothing is resumed");
mStackSupervisor.resumeTopActivitiesLocked();
}
return false;
}
mResumedActivity = null;
mPausingActivity = prev;
mLastPausedActivity = prev;
mLastNoHistoryActivity = (prev.intent.getFlags() & Intent.FLAG_ACTIVITY_NO_HISTORY) != 0
|| (prev.info.flags & ActivityInfo.FLAG_NO_HISTORY) != 0 ? prev : null;
prev.state = ActivityState.PAUSING;
prev.task.touchActiveTime();
clearLaunchTime(prev);
final ActivityRecord next = mStackSupervisor.topRunningActivityLocked();
if (mService.mHasRecents && (next == null || next.noDisplay || next.task != prev.task)) {
prev.updateThumbnail(screenshotActivities(prev), null);
}
stopFullyDrawnTraceIfNeeded();
mService.updateCpuStats();
if (prev.app != null && prev.app.thread != null) {
if (DEBUG_PAUSE) Slog.v(TAG, "Enqueueing pending pause: " + prev);
try {
EventLog.writeEvent(EventLogTags.AM_PAUSE_ACTIVITY,
prev.userId, System.identityHashCode(prev),
prev.shortComponentName);
mService.updateUsageStats(prev, false);
prev.app.thread.schedulePauseActivity(prev.appToken, prev.finishing,
userLeaving, prev.configChangeFlags, dontWait);
} catch (Exception e) {
// Ignore exception, if process died other code will cleanup.
Slog.w(TAG, "Exception thrown during pause", e);
mPausingActivity = null;
mLastPausedActivity = null;
mLastNoHistoryActivity = null;
}
} else {
mPausingActivity = null;
mLastPausedActivity = null;
mLastNoHistoryActivity = null;
}
.....
......
注意,我们第一次启动时执行过一次startPausingLocked时,mResumedActivity已经被置为null,所以此时 执行ActivityRecord prev = mResumedActivity;相当于prev也为null了,这时候就会走if(prev==null)分支
了,返回了false。所以第二次启动是不会在抛出HH.PAUSE_ACTIVITY消息的。mStackSupervisor.resumeTopActivitiesLocked();会根据mResumedActivity和mPausingActivity状态进行各种判断,最后会在allPausedActivitiesComplete()方法判断中发现有activity执行pausing未完成,直接返回了false,受篇幅原因,具体的就不带着大家细看了,感兴趣的同学可以自行查阅。
if (prev == null) {
if (!resuming) {
Slog.wtf(TAG, "Trying to pause when nothing is resumed");
mStackSupervisor.resumeTopActivitiesLocked();
}
return false;
}
分析到这里,我们先来总结下在向下进行:到目前为止,我们得出的结论是:
假设我在同一个方法里面连续启动两次Activity,两个actiivty都会入栈,第一次启动的activity抛出了pause的Message,第二次没有抛出,而抛出的pause消息要等到第二次启动完成才能得到执行。当onPause执行完后,通知AMS要启动Activity了,此时AMS从栈顶取Activity,自然拿到的是第二个Activity,执行第二个Activity的onCreate事件。这样就完全解释清楚了上面现象的缘由。
四、 进一步验证
下面我们通过查看消息队列中的内容的方式来验证,查看源码发现,MessageQueue中提供了dump方法,可以获取到queue中的内容,接下来就简单了,反射拿到它,封装了个简单的MessageDump类。
public class MessageDump {
public static void dump() {
Looper looper = Looper.getMainLooper();
ClassUtils classUtils = new ClassUtils(Looper.getMainLooper().getClass().getName(), "mQueue");
LogPrinter printer = new LogPrinter(Log.ERROR, SecondActivity.TAG);
MessageQueue queue = (MessageQueue) classUtils.get(looper);
try {
Method method = queue.getClass().getDeclaredMethod("dump", Printer.class, String.class);
method.setAccessible(true);
method.invoke(queue, printer, "");
} catch (Exception e) {
// e.printStackTrace();
throw new RuntimeException(e);
}
}
}
我们加入到代码中,分别在第一次启动前,第一次执行后,第二次执行后取dump消息队列:
public void clickToStart(View view) {
Log.i(SecondActivity.TAG, "clickToStart:->before->first");
MessageDump.dump();
Log.i(SecondActivity.TAG, "--------------------------------------------");
startSecondActivity();
Log.i(SecondActivity.TAG, "clickToStart:->after->first");
MessageDump.dump();
Log.i(SecondActivity.TAG, "--------------------------------------------");
startSecondActivity();
MessageDump.dump();
Log.i(SecondActivity.TAG, "clickToStart:->after->second");
}
05-21 02:42:28.989 32626-32626/com.example.ljj.test2 I/lan: clickToStart:->before->first
05-21 02:42:28.990 32626-32626/com.example.ljj.test2 I/lan: Message 0: { when=-8ms callback=android.view.View$UnsetPressedState target=android.view.ViewRootImpl$ViewRootHandler }
05-21 02:42:28.990 32626-32626/com.example.ljj.test2 I/lan: (Total messages: 1, idling=false, quitting=false)
05-21 02:42:28.990 32626-32626/com.example.ljj.test2 I/lan: --------------------------------------------
05-21 02:42:28.993 32626-32626/com.example.ljj.test2 I/lan: clickToStart:->after->first
05-21 02:42:28.994 32626-32626/com.example.ljj.test2 I/lan: Message 0: { when=-11ms callback=android.view.View$UnsetPressedState target=android.view.ViewRootImpl$ViewRootHandler }
05-21 02:42:28.994 32626-32626/com.example.ljj.test2 I/lan: Message 1: { when=-1ms what=101 arg1=1 obj=android.os.BinderProxy@3e564eb2 target=android.app.ActivityThread$H }
05-21 02:42:28.994 32626-32626/com.example.ljj.test2 I/lan: Message 2: { when=0 what=6 arg2=1 target=android.view.ViewRootImpl$ViewRootHandler }
05-21 02:42:28.994 32626-32626/com.example.ljj.test2 I/lan: (Total messages: 3, idling=false, quitting=false)
05-21 02:42:28.994 32626-32626/com.example.ljj.test2 I/lan: --------------------------------------------
05-21 02:42:28.996 32626-32626/com.example.ljj.test2 I/lan: Message 0: { when=-13ms callback=android.view.View$UnsetPressedState target=android.view.ViewRootImpl$ViewRootHandler }
05-21 02:42:28.996 32626-32626/com.example.ljj.test2 I/lan: Message 1: { when=-3ms what=101 arg1=1 obj=android.os.BinderProxy@3e564eb2 target=android.app.ActivityThread$H }
05-21 02:42:28.996 32626-32626/com.example.ljj.test2 I/lan: Message 2: { when=-2ms what=6 arg2=1 target=android.view.ViewRootImpl$ViewRootHandler }
05-21 02:42:28.996 32626-32626/com.example.ljj.test2 I/lan: (Total messages: 3, idling=false, quitting=false)
05-21 02:42:28.996 32626-32626/com.example.ljj.test2 I/lan: clickToStart:->after->second
05-21 02:42:28.996 32626-32626/com.example.ljj.test2 I/lan: MainActivity->onPause
通过log我们清晰的看到确实在执行第一次启动后,消息队列中多了一条消息,并且在第二次启动完毕后,该消息依然存在,与我们之前的结论是相符的。
{ when=-1ms what=101 arg1=1 obj=android.os.BinderProxy@3e564eb2 target=android.app.ActivityThread$H }
既然这样,那我第二次启动时通过Handler异步起动是不是就好了呢?
public void clickToStart(View view) {
startSecondActivity();
new Handler().post(new Runnable() {
@Override
public void run() {
startSecondActivity();
}
});
}
结果我就不贴了,如果你理解了上文中讲的内容,应该可以猜到这种情形下,启动就正常了,因为我们第二次启动的消息是放在了pause消息之后了。
五、 singleTop失效的秘密
还有一个小问题,就是如果启动模式设置为singleTop的时候为什么会失效呢?
按道理来说,即使是第一次启动时被压入了栈中,没有正常启动,那intent信息总是在的啊,按道理不应该会影响启动模式才对。我们可以先看下IActivityManager的startActivity的返回值是什么?在执行startActivity时是会得到一个返回值的,这个返回值是反映Activity的启动状态的,和启动模式关系很大。
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, Bundle options) {
try {
intent.migrateExtraStreamToClipData();
intent.prepareToLeaveProcess();
int result = ActivityManagerNative.getDefault()
.startActivity(whoThread, who.getBasePackageName(), intent,
intent.resolveTypeIfNeeded(who.getContentResolver()),
token, target != null ? target.mEmbeddedID : null,
requestCode, 0, null, null, options);
checkStartActivityResult(result, intent);
} catch (RemoteException e) {
}
return null;
}
我们通过动态代理来hook掉IActivityManager,可以拿到该返回值。这个返回值类型定义在了ActivityManager.java中,和启动模式相关的是下面这几个。
public static final int START_SUCCESS = 0;//正常启动
public static final int START_TASK_TO_FRONT = 2;//singleTask
public static final int START_DELIVERED_TO_TOP = 3;//singleTop
通过hook拿到返回值会发现,连续两次启动时,返回值均为0,而加入handler异步起动后,得到的返回值是0,3。那就说明两次启动都是正常启动,但是第二次启动也是按照普通模式启动了,所以返回的是0。这下我们就可以有目的的去看源码了,只需要看到关于intent的处理部分即可。源码在ActivityStackSupervisor类的startActivityUncheckedLocked方法中。
if (r.packageName != null) {
// If the activity being launched is the same as the one currently
// at the top, then we need to check if it should only be launched
// once.
ActivityStack topStack = getFocusedStack();
ActivityRecord top = topStack.topRunningNonDelayedActivityLocked(notTop);
if (top != null && r.resultTo == null) {
if (top.realActivity.equals(r.realActivity) && top.userId == r.userId) {
if (top.app != null && top.app.thread != null) {
if ((launchFlags & Intent.FLAG_ACTIVITY_SINGLE_TOP) != 0
|| launchSingleTop || launchSingleTask) {
ActivityStack.logStartActivity(EventLogTags.AM_NEW_INTENT, top,
top.task);
// For paranoia, make sure we have correctly
// resumed the top activity.
topStack.mLastPausedActivity = null;
if (doResume) {
resumeTopActivitiesLocked();
}
ActivityOptions.abort(options);
if ((startFlags&ActivityManager.START_FLAG_ONLY_IF_NEEDED) != 0) {
// We don't need to start a new activity, and
// the client said not to do anything if that
// is the case, so this is it!
return ActivityManager.START_RETURN_INTENT_TO_CALLER;
}
top.deliverNewIntentLocked(callingUid, r.intent);
return ActivityManager.START_DELIVERED_TO_TOP;
}
}
}
}
}
我们重点来看判定条件,top是拿的栈顶的activity,此时是第一次启动放进去的SecondActiivty,说明一下,topRunningNonDelayedActivityLocked函数是拿取除了notTop指定的activity外,位于栈顶的activity。一般notTop为null。很显然第一层,第二层的if语句都是true。最里层的if语句呢?其实我一开始看的时候没有注意最里层的if,理所当然的认为不为null,导致这里找了半天都觉得说不通。
ActivityStack topStack = getFocusedStack();
ActivityRecord top = topStack.topRunningNonDelayedActivityLocked(notTop);
if (top != null && r.resultTo == null) {
if (top.realActivity.equals(r.realActivity) && top.userId == r.userId) {
if (top.app != null && top.app.thread != null) {
无奈之下,想从最早dump出来的信息里面找一下这个activity和正常启动的activity的差别。
原来在这,正常启动的activity都是绑定了ProcessRecord的,而INITIALIZING状态的Activity是没有进程相关信息的,也就是它不属于任何一个进程,还不具备和应用进程交互的能力。
真正绑定的地方在realStartActivityLocked方法中,也就是真正启动的过程中会进行绑定。现在就解释清楚了为什么singleTop在这种情境下失效了吧。
六. 实际应用场景的思考及解决办法
有人可能会说,实际场景中哪里有人会在一个函数中或者一个message中去启动两次activity?确实这种场景几乎没有,但是我们来看一下以下的情景。
public void clickToStart(View view) {
new Handler().post(new Runnable() {//消息1
@Override
public void run() {
Intent intent=new Intent();//消息3
intent.setAction(START);
sendBroadcast(intent);
}
});
new Handler().post(new Runnable() {//消息2
@Override
public void run() {
Intent intent=new Intent();//消息4
intent.setAction(START);
sendBroadcast(intent);
}
});
}
private class MyReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction().equals(START)) {
startActivity(new Intent(MainActivity.this, SecondActivity.class));
}
}
}
我们用上述代码模拟了一下可能在实际代码中出现的场景,比如我们不同的网络接口请求,返回来后需要发送一个广播。我们应该都是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即可。
public void clickToStart(View view) {
handler.sendEmptyMessage(101);
handler.sendEmptyMessage(101);
}
private Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
Intent intent3 = new Intent(MainActivity.this, SecondActivity.class);
startActivity(intent3);
handler.removeMessages(101);
}
};
这种方式也不太优雅,解决的办法肯定有很多,这里只是举个例子而已。知道了问题的缘由后,根据业务自行处理即可。
七. 总结
这篇文章主要是为了阐述一下activity连续启动的异常问题的跟踪过程。总结如下:
- 如果在同一个message连续启动同一个activity或者相邻message中分别启动同一个activity,就会出现生命周期诡异的问题,当然现在已经不诡异了。从探索方法和源码的角度给出了解释。
- 解释了为什么singleTop在这种模式下会失效的问题
- 给出了解决的思路,就是破坏消息顺序。
- 此外,阐述了Activity栈和启动activity流程相关的知识