启动信息全掌握,Android 15 重磅 API:ApplicationStartInfo

前言

App 进程启动的时候,开发者很难获悉到本次启动的详细信息,比如:

  • 是冷启动的、暖启动的、还是热启动的?
  • 是被 Broadcast 拉起来的、Activity 拉起来的、还是 ContentProvider 拉起来的?

针对这些 pain-points,Android 15 引入了全新 API:ApplicationStartInfo,用以提供 App 进程启动时候的各种信息。包括:启动类型、来源、进程等等,开发者通过这些信息,可以清晰地掌握启动的情况按需处理。

API

首先,我们来查阅 ApplicationStartInfo 文档,看看所能提供的关键方法,进行一一阐述。

getIntent() & getLaunchMode()

getIntent() 可以获取启动该 App 的 Intent 信息,包括 Activity、Broadcast、Service 三种组件的情况。

getLaunchMode() 指导本 App 应当以哪种 LaunchMode 来处理该 Intent 请求。启动模式包括:

  • LAUNCH_MODE_STANDARD(0)
  • LAUNCH_MODE_SINGLE_TOP(1)
  • LAUNCH_MODE_SINGLE_INSTANCE(2)
  • LAUNCH_MODE_SINGLE_TASK(3)
  • LAUNCH_MODE_SINGLE_INSTANCE_PER_TASK(4)

上述常量来自于请求方的 intent 参数和目标 App 在 Manifest 里 launchMode 的参数。

getPackageUid() & getRealUid()

getPackageUid() 获取的是本 App 在安装时期所属的用户组 ID。

getRealUid() 获取的则是本 App 在运行时候所属的真实用户组 ID。

Uid 和 App 的级别有关,决定了 App 所能访问的资源权限。

getPid() & getProcessName()

getPid() 获取的是本 App 的进程 ID。

getProcessName() 获取的是本 App 的进程名称。

getReason()

获取该进程被启动时候的原因。包括如下定义:

  • START_REASON_ALARM(0):因为 alarm 机制启动的进程
  • START_REASON_BACKUP(1):因为执行 backup 操作启动的进程
  • START_REASON_BOOT_COMPLETE(2):因为执行系统启动广播而启动的进程
  • START_REASON_BROADCAST(3):因为执行 Broadcast 而启动的进程
  • START_REASON_CONTENT_PROVIDER(4):因为执行 ContentProvider 的访问而启动的进程
  • START_REASON_JOB(5):因为执行 JobService 而启动的进程
  • START_REASON_LAUNCHER(6):因为执行 Launcher 上点击 icon 启动的进程
  • START_REASON_LAUNCHER_RECENTS(7):因为执行 Launcher 上的历史恢复而启动的进程
  • ...
  • START_REASON_START_ACTIVITY(11):因为执行明示的 Activity 请求而启动的进程

其中省略的有因为 Service、Push 通知等场景启动的 Reason 定义。

getStartType() & getStartupState()

getStartType() 获取的是 App 进程启动的类型:

  • START_TYPE_UNSET(0):未知的启动状态
  • START_TYPE_COLD(1):进程完全冷启动,
  • START_TYPE_WARM(2):进程暖启动,SavedInstanceState 数据还存在
  • START_TYPE_HOT(3):进程热启动,比如从后台进入前台

getStartupState() 获取的是 App 进程的启动状态:

  • STARTUP_STATE_STARTED(0):表示进程处于已经启动
  • STARTUP_STATE_ERROR(1):表示进程启动失败
  • STARTUP_STATE_FIRST_FRAME_DRAWN(2):表示进程已经启动并完成了第一帧的绘制

getStartupTimestamps()

获取启动过程里花费的时间,以 ns 为单位。

wasForceStopped()

告知 App 本次启动是否是被强制停止后的启动,App 可以通过该值决定是否要重新注册 Alarm、JobService 等等。

实战

下面我们试着采集几种情况下系统返回的 ApplicationStartInfo 信息。

首先我们需要知道如何获取 ApplicationStartInfo 实例,了解 App 启动的同学可能会猜到应该归属 ActivityManager 的处理范畴。

果然,笔者在 ActivityManager 类里发现 Android 15 新增了几个 ApplicationStartInfo 相关的配套 API:

  • addStartInfoTimestamp(key, timestampNs):允许开发者针对指定的 key 添加时间戳
  • getHistoricalProcessStartReasons(maxNum):进程启动的时候获取历史的启动信息 ApplicationStartInfo list,需要指定获取的 size 上限(指定 0 的话,会输出所有记录)
  • addApplicationStartInfoCompletionListener(executor, listener):添加 ApplicationStartInfo 发生变化时候的监听器,当进程完成启动的时候会在 executor 代表的线程里回调 listener,需要留意的是 listener 不能为 null,否则会触发 IllegalArgumentException
  • removeApplicationStartInfoCompletionListener():删除上面的监听器

实战的代码很简单:

  1. 在 Application 里通过 ActivityManager 拿到历史 ApplicationStartInfo list 并打印
  2. 并添加 ApplicationStartInfo 发生变化时候的监听
kotlin 复制代码
 class OSVApplication : Application() {
     ...
     override fun onCreate() {
         super.onCreate()
         Log.d("AppStart", "OSVApplication#onCreate()")
 ​
         val activityManager = getSystemService(ActivityManager::class.java)
         val applicationStartInfoList = activityManager.getHistoricalProcessStartReasons(3)
         val applicationStartConsumer = Consumer<ApplicationStartInfo> {
             Log.d("AppStart", "changed applicationStartInfo:${it.printApplicationStartInfo()}")
         }
 ​
         Log.d("AppStart", "Original applicationStartInfo list:\n")
         for (info in applicationStartInfoList) {
             Log.d("AppStart", "${info.printApplicationStartInfo()}")
         }
 ​
         activityManager.addApplicationStartInfoCompletionListener(
             executor,
             applicationStartConsumer
         )
     }
 }

测试的 DEMO 里只提供了 Activity 组件,我们针对该组件进行测试。

对于 Activity 画面来说,一般的启动方式有如下几种:

  • 最常见的从 Launcher 上直接启动
  • 偶尔的从 History 恢复启动
  • 别的 App 通过 Action 或包名启动(后面我们用 adb 模拟)

我们将尝试如上几种启动场景,看会输出怎样的 ApplicationStartInfo 结果。

首次通过 Launcher 启动 App

安装下测试 DEMO,并首次从 Launcher 上启动 App,看下 log。

bash 复制代码
 03-30 20:46:27.461  4499  4499 D AppStart: OSVApplication#onCreate()
 03-30 20:46:27.477  4499  4499 D AppStart: Original applicationStartInfo list:
 03-30 20:46:27.484  4499  4499 D AppStart: ApplicationStartInfo{intent:Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10000000 cmp=com.ellison.demo/.appStart.AppStartActivity } launchMode:0 packageUid:10197 realUid:10197 pid:0 processName:com.ellison.demo reason:6  startType:1 startupState:0 startupTimestamps:{0=56169205650, 3=56491075233} wasForceStopped:false}
 ​
 03-30 20:46:27.638  4499  4499 D AppStart: AppStartActivity#onCreate()
 03-30 20:46:27.961  4499  4563 D AppStart: ApplicationStartInfo{intent:Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10000000 cmp=com.ellison.demo/.appStart.AppStartActivity } launchMode:0 packageUid:10197 realUid:10197 pid:0 processName:com.ellison.demo reason:6  startType:1 startupState:2 startupTimestamps:{0=56169205650, 3=56491075233} wasForceStopped:false}

可以看到:

  1. 获取到的 ApplicationStartInfo 记录只有 1 条,符合预期
  2. intent 内容显示是从 Launcher 过来的启动请求
  3. launchMode 是 0 即 LAUNCH_MODE_STANDARD,因为咱们测试 Activity 的 launchMode 没声明,自然是默认值
  4. pid 是 0,这点有点奇怪,理论上来说应该是 App 的进程号 4499
  5. reason 是 6 即 START_REASON_LAUNCHER,表示是从 Launcher 上启动的
  6. startType 是 1 即 START_TYPE_COLD,表示进程冷启动
  7. startupState 是 0 即 STARTUP_STATE_STARTED,表示进程启动了
  8. wasForceStopped 是 false,因为是首次安装,还没启动过,自然没有被强制停止过,合理~
  9. 当目标 Activity 完成启动,在 listener 里回调了此次启动记录,所以信息都一致,只有 startupState 变化了,是 2 即 STARTUP_STATE_FIRST_FRAME_DRAWN,表示进程完成了第 1 帧的描画

kill 后从 History 启动

接着,我们手动 kill 进程之后,再通过 Launcher 上的 History 画面恢复进程看看 log:

bash 复制代码
 03-30 20:48:47.472  5218  5218 D AppStart: OSVApplication#onCreate()
 03-30 20:48:47.475  5218  5218 D AppStart: Original applicationStartInfo list:
 ​
 03-30 20:48:47.476  5218  5218 D AppStart: ApplicationStartInfo{intent:Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10100000 cmp=com.ellison.demo/.appStart.AppStartActivity } launchMode:0 packageUid:10197 realUid:10197 pid:0 processName:com.ellison.demo reason:6  startType:1 startupState:0 startupTimestamps:{0=196526136925} wasForceStopped:true}
 ​
 03-30 20:48:47.476  5218  5218 D AppStart: ApplicationStartInfo{intent:Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10000000 cmp=com.ellison.demo/.appStart.AppStartActivity } launchMode:0 packageUid:10197 realUid:10197 pid:0 processName:com.ellison.demo reason:6  startType:1 startupState:2 startupTimestamps:{0=56169205650, 3=56491075233} wasForceStopped:false}
 ...
  1. ApplicationStartInfo 信息增加到了 2 条,因为这是第 2 次启动了,可以理解
  2. 最新的 1 条里的 wasForceStopped 变成了 true,因为上次咱们手动 kill 了进程,所以系统正确地提供了这是被强制 kill 之后的首次启动
  3. intent 信息里 flg 是不同的,因为 Launcher 上对于 icon 启动和 History 恢复是不一样的 launch flags

但有一点出乎意外的是,最新的 1 条的 reason 并非预期的 7 即 START_REASON_LAUNCHER_RECENTS。不知道这的偏差是 DP 阶段的 bug 还是笔者的理解有 gap。

kill 后从 adb 启动

最后,我们再手动 kill 进程,然后用如下的 adb 模拟外部的调用:

bash 复制代码
 adb shell am start -n com.ellison.demo/.appStart.AppStartActivity

再看下 log:

bash 复制代码
 03-30 20:50:52.262  5456  5456 D AppStart: OSVApplication#onCreate()
 03-30 20:50:52.264  5456  5456 D AppStart: Original applicationStartInfo list:
 ​
 03-30 20:50:52.265  5456  5456 D AppStart: ApplicationStartInfo{intent:Intent { flg=0x10000000 cmp=com.ellison.demo/.appStart.AppStartActivity } launchMode:0 packageUid:10197 realUid:10197 pid:0 processName:com.ellison.demo reason:11  startType:1 startupState:0 startupTimestamps:{0=321414404318} wasForceStopped:true}
 ​
 03-30 20:50:52.265  5456  5456 D AppStart: ApplicationStartInfo{intent:Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10100000 cmp=com.ellison.demo/.appStart.AppStartActivity } launchMode:0 packageUid:10197 realUid:10197 pid:0 processName:com.ellison.demo reason:6  startType:1 startupState:2 startupTimestamps:{0=196526136925} wasForceStopped:true}
 ​
 03-30 20:50:52.265  5456  5456 D AppStart: ApplicationStartInfo{intent:Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10000000 cmp=com.ellison.demo/.appStart.AppStartActivity } launchMode:0 packageUid:10197 realUid:10197 pid:0 processName:com.ellison.demo reason:6  startType:1 startupState:2 startupTimestamps:{0=56169205650, 3=56491075233} wasForceStopped:false}
 ...
  1. 启动信息增加到了 3 条,

  2. 最新的 1 条有如下信息:

    • intent 里正确打印了 adb 启动的命令信息
    • reason 是 11 即 START_REASON_START_ACTIVITY,成功显示这是启动 Activity 的调用
    • wasForceStopped 是 false,成功显示上次被强制 kill 了

编译注意

面向 Android 15 DP 版开发,需要按照开始使用 Android 15进行 IDE 配置。

另外,一定要升级 AGP 和 Gradle 的版本,否则会遇到如下的编译错误:

resource linking failed .../Library/Android/sdk/platforms/android-VanillaIceCream/android.jar.

AGP 建议:8.3.0,Gralde 建议:8.4.0。

结语

我们总结了新 API ApplicationStartInfo 所能提供的信息内容,并结合几种常见的 Activity 启动场景进行了实战阐述。除了个别信息与预期不符以外,大部分都是如期输出了启动信息。至于其他场景下的 App 进程启动:比如 ServiceBroadcastConentProvider 等,没有进行一一的尝试,感兴趣的同学可以自行研究。

总的来说,通过该 API 可以轻松拿到启动时候的背景信息,非常方便。而此前 App 想要获取这些信息需要做很多内部记录和判断,非常繁琐。基于这些信息,开发者可以从进程启动这种新视角来 track 和分析 App 各种使用入口的启动情况,还可以根据不同的启动信息在代码上按需处理启动逻辑。

参考资料

相关推荐
帅得不敢出门9 小时前
安卓设备adb执行AT指令控制电话卡
android·adb·sim卡·at指令·电话卡
我又来搬代码了11 小时前
【Android】使用productFlavors构建多个变体
android
德育处主任13 小时前
Mac和安卓手机互传文件(ADB)
android·macos
芦半山13 小时前
Android“引用们”的底层原理
android·java
迃-幵13 小时前
力扣:225 用队列实现栈
android·javascript·leetcode
大风起兮云飞扬丶14 小时前
Android——从相机/相册获取图片
android
Rverdoser14 小时前
Android Studio 多工程公用module引用
android·ide·android studio
aaajj14 小时前
[Android]从FLAG_SECURE禁止截屏看surface
android
@OuYang14 小时前
android10 蓝牙(二)配对源码解析
android
Liknana14 小时前
Android 网易游戏面经
android·面试