你可能不知道的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是如何管理的,或者说这段信息怎么看?


activity任务栈.png

上图中显示了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出来的信息,会发现两个地方值得注意。
xxxx1.png

当前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事件回调前的启动流程上,忽略后面的流程。


activity启动流程图.png

上面这张图简单的展示了当我们发起startActivity到前一个actiivty执行onPause的流程,讲解之前大致说一下Activity启动过程中应用进程和AMS进程通信的过程。

Binder.png

上面这种图清晰的展示了应用进程和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 
05-21 02:42:29.164 32626-32626/com.example.ljj.test2 I/lan: SecondActivity->onCreate: 578093578
05-21 02:42:29.165 32626-32626/com.example.ljj.test2 I/lan: SecondActivity->onResume: 578093578

通过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的差别。


temp.png

原来在这,正常启动的activity都是绑定了ProcessRecord的,而INITIALIZING状态的Activity是没有进程相关信息的,也就是它不属于任何一个进程,还不具备和应用进程交互的能力。


temp2.jpeg

真正绑定的地方在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连续启动的异常问题的跟踪过程。总结如下:

  1. 如果在同一个message连续启动同一个activity或者相邻message中分别启动同一个activity,就会出现生命周期诡异的问题,当然现在已经不诡异了。从探索方法和源码的角度给出了解释。
  2. 解释了为什么singleTop在这种模式下会失效的问题
  3. 给出了解决的思路,就是破坏消息顺序。
  4. 此外,阐述了Activity栈和启动activity流程相关的知识。

因为涉及的内容太多,不是一篇文章能写的下的,加上时间关系,可能很多地方都有些省略,望见谅。文笔有限,解释有误的方法,欢迎交流指正!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容