你可能不知道的Activity启动的诡异现象探索

news2024/9/20 5:56:14

你可能不知道的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连续启动的异常问题的跟踪过程。总结如下:

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2118381.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

JAVA社交新潮流同城组局搭子不愁系统小程序源码

社交新潮流——同城组局,搭子不愁系统 🎉【潮流前沿,社交新风向】🎉 在这个快节奏的城市生活中,你是否常常感到周末无聊,想找点乐子却苦于没有合适的搭子?别担心,今天我要给大家安…

【最经典的79个】软件测试面试题(内含答案)备战“金九银十”

001.软件的生命周期(prdctrm) 计划阶段(planning)-〉需求分析(requirement)-〉设计阶段(design)-〉编码(coding)->测试(testing)->运行与维护(running maintrnacne) 测试用例 用例编号 测试项目 测试标题 重要级别 预置条件 输入数据 执行步骤 预期结果 0002.问&…

RPG经典设计逻辑——DD系统简单拆解

RPG经典设计逻辑——D&D系统简单拆解 作为TGA2023年度游戏,身披数十项奖项及提名的《博德之门3》,近日又获得了2024雨果奖“最佳游戏或交互式作品”的奖项。“雨果奖”之名源于“科幻杂志之父”雨果・根斯巴克,是科幻及奇幻领域最负盛名…

Mysql高阶语句(2)

Mysql视图:优化操作与安全方案 概述: 视图(View)是数据库中的一种虚拟表,虽然不包含实际数据,但可以映射真实数据,像是数据库表的镜花水月或倒影。视图会动态地保存结果集的数据,使…

springboot邮件发送的方式?如何配置服务?

Springboot邮件发送的优化策略?Springboot发信技巧? Spring Boot作为一个流行的Java框架,提供了简单而强大的方式来实现邮件发送功能。AokSend将详细介绍Spring Boot邮件发送的方式,帮助开发者快速掌握这一技能。 SpringBoot邮件…

跟我一起学Python编程(五),变量的定义和使用

今天给大家介绍一下Python变量的定义和使用,我们要使用编程语言去做一件事情,一定要使用到变量。 什么是变量? 在Python中,变量可以用来存储各种类型的数据,如数字,文本(称为字符串&#xff09…

云计算之大数据(上)

目录 一、Elasticsearch 1.1 产品组件 1.1.1 X-Pack 1.1.2 Beats数据采集中心 1.1.3 Logstash 1.1.4 Kibana 1.2 架构特性 1.2.1 性能 1.2.2 安全性 1.2.3 可用性 1.2.4 可扩展性 1.2.5 可维护性 1.2.6 国际化 1.3 综合检索分析 1.4 全观测 1.5 大数据检索加速…

遗传算法与深度学习实战(5)——遗传算法中常用遗传算子

遗传算法与深度学习实战(5)——遗传算法中常用遗传算子 0. 前言1. 常用选择算子1.1 轮盘选择 (roulette wheel selection)1.2 随机遍历抽样 (Stochastic universal sampling, SUS)1.3 排序选择 (rank-based selection)1.4 适应度缩放选择(Fit…

无需更换摄像头,无需施工改造,降低智能化升级成本的智慧工业开源了

智慧工业视觉监控平台是一款功能强大且简单易用的实时算法视频监控系统。它的愿景是最底层打通各大芯片厂商相互间的壁垒,省去繁琐重复的适配流程,实现芯片、算法、应用的全流程组合,从而大大减少企业级应用约95%的开发成本。用户只需在界面上…

图神经网络模型介绍(1)

我们将图神经网络分为基于谱域的模型和基于空域的模型,并按照发展顺序详解每个类别中的重要模型。 1.1基于谱域的图神经网络 谱域上的图卷积在图学习迈向深度学习的发展历程中起到了关键的作用。本节主要介绍三个具有代表性的谱域图神经网络:谱图卷积网…

软件测试学习笔记丨接口测试用例设计

本文转自测试人社区,原文链接:https://ceshiren.com/t/topic/32078 一、简介 接口测试在需求分析完成之后,即可设计对应的接口测试用例,然后根据用例进行接口测试。接口测试用例的设计,也需要用到黑盒测试用例的设计方…

​数据链路层——流量控制可靠传输机制 ​

https://www.cnblogs.com/nekodream/p/18048072 数据链路层的流量控制 较高的发送速度和较低的接收能力的不匹配,会造成传输出错,因此流量控制也是数据链路层的一项重要工作。 数据链路层的流量控制是点对点的,而传输层的流量控制是端到端的。 数据链路层流量控制…

UEFI——Variable的使用

一、Variable简介 Variable Services是Runtime Services的一部分,提供关于variable的一些服务,Variable被定义为键值对,由标识信息加上属性(键)和任意数据(值)组成。用在固件部内部和固件与操作…

JAVA工具----自动化API文档APIFOX

前言:想必大家在后端开发的过程过,最烦的就是写接口文档了。然而如果是对接前写,就更加烦躁,可能之前写好的接口文档全都要改,还会占用大量的时间,一般的小团队也耗不起这样的费时。那么使用Swagger和侵入式…

如何查询小红书、抖音等多平台品牌声量数据?

现在很多企业都需要掌握小红书、抖音、公众号、微博等各个社媒平台上,企业品牌声量趋势情况,行业内容占有率如何,搜索指数高低等等,借此来判断企业的品牌影响力、行业地位以及最近的活动/事件等营销效果。那么跨平台查询是比较麻烦…

一篇清楚的明白什么是场外个股期权?

今天带你了解一篇清楚的明白什么是场外个股期权?场外个股期权是一种非标准化的期权合约,在金融机构和投资者之间定制进行。它具有更大的灵活性,但也伴随着较高的信用风险和复杂性。适用于对冲风险、增强收益和设计结构性产品等多种策略。 场…

提取音频转mp3的工具哪个好?这六个新手必备

各位音乐爱好者们!是不是有时候你会听到一首歌,觉得它的中间伴奏简直太棒了,想要把它单独提取出来,却发现无从下手呢? 别担心,现在有了提取音频在线工具,这些难题都能迎刃而解。下面&#xff0…

Jmeter_循环获取请求接口的字段,并写入文件

通过JSON提取器、计数器、beanshell,循环读取邮箱接口的返回字段,筛选出flag为3的收件人,并写入csv文件。 1、调用接口,获取所有的邮件$.data.total.count; 2、beanshell后置处理total转换成页码,这里是227…

STM32 HAL freertos零基础(二)-通过STM32CubeMX配置Freertos后在程序中进行任务创建,便于任务管理与识别。

1、简介 通过STM32CubeMX配置Freertos后,建立的任务都在freertos.c文件中,不易于观察,并且每次生成新任务还需要打开STM32CubeMX,本次教程讲解一种通过STM32CubeMX配置Freertos后在程序中进行任务创建,起到类似添加传…

官宣:28家重点实验室,获5750万元资金支持!

【欧亚科睿学术】 点击关注:关注GZH【欧亚科睿学术】,第一时间了解科研最新动态! 根据省级财政专项资金管理办法有关规定,拟安排2024年省重点实验室28项,拟安排省财政资金5750万元。现予以公示: 本文来源…