Android app性能优化之启动优化

一、应用启动的内部机制

(参考资料源于:https://developer.android.com/)

冷启动

冷启动是指应用从头开始启动:系统进程在冷启动后才创建应用进程(当启动应用时,后台没有该应用的进程)。发生冷启动的情况包括应用自设备启动后或系统终止应用后首次启动。这种启动给最大限度地减少启动时间带来了最大的挑战,因为系统和应用要做的工作比在另外两种启动状态中更多。
在冷启动开始时,系统有三个任务,它们是:

  1. 加载并启动应用。
  2. 在启动后立即显示应用的空白启动窗口。
  3. 创建应用进程。

系统一创建应用进程,应用进程就负责后续阶段:

  1. 创建应用对象。
  2. 启动主线程。
  3. 创建MainActivity。
  4. 扩充视图。
  5. 布局屏幕。
  6. 执行初始绘制。 一旦应用进程完成第一次绘制,系统进程就会换掉当前显示的后台窗口,替换为主 Activity。此时,用户可以开始使用应用。
    image

图片显示系统进程和应用进程之间如何交接工作。
在创建应用和创建 Activity 的过程中可能会出现性能问题:

应用创建
当应用启动时,空白启动窗口将保留在屏幕上,直到系统首次完成应用绘制。完成后,系统进程会换掉应用的启动窗口,允许用户开始与应用互动。 如果在自己的应用中使 Application.onCreate() 过载,系统将在应用对象上调用 onCreate() 方法。之后,应用生成主线程(也称为界面线程),并用其执行创建主 Activity 的任务。
从此时开始,系统级和应用级进程根据应用生命周期阶段继续运行。

Activity 创建
在应用进程创建 Activity 后,Activity 将执行以下操作:
初始化值。
调用构造函数。
根据 Activity 的当前生命周期状态,相应地调用回调方法,如 Activity.onCreate()。 通常,onCreate() 方法对加载时间的影响最大,因为它执行工作的开销最高:加载视图,以及初始化 运行Activity 所需的对象。

热启动

应用的热启动比冷启动简单得多,开销也更低。在热启动中,系统的所有工作就是将Activity 带到前台。 当启动应用时,后台已有该应用的进程(例如:按了home键,应用虽然会退出,但是该应用的进程是依然会保留在后台,可以进入任务列表查看。)所以在已有进程的情况下,这种启动会从已有的进程中来启动应用,这个方式叫热启动。只要应用的所有 Activity 仍驻留在内存中,应用就不必重复执行对象初始化、布局加载和呈现。
但是,如果一些内存为响应内存整理事件(如 onTrimMemory())而被完全清除,则需要为了响应热启动事件而重新创建相应的对象。

温启动

温启动包含了在冷启动期间发生的部分操作;同时,它的开销要比热启动高。有许多潜在状态可视为温启动。例如:
用户在退出应用后又重新启动应用。进程可能已继续运行,但应用必须通过调用 onCreate() 从头开始重新创建 Activity。 系统将您的应用从内存中逐出,然后用户又重新启动它。进程和 Activity 需要重启,但传递到 onCreate() 的已保存的实例 state bundle 对于完成此任务有一定帮助。
一般情况下我们做启动优化主要是针对冷启动做的。
首先我们可以先通过一些方式去获得启动的时间,可以通过Adb命令、手动打点、AOP等方式。

二、Activity启动流程

App的启动流程

  1. 通过 Launcher 启动应用时,点击应用图标后,Launcher 调用 startActivity 启动应用。
  2. Launcher Activity 最终调用 Instrumentation 的 execStartActivity 来启动应用。
  3. Instrumentation 调用 ActivityManagerProxy (ActivityManagerService 在应用进程的一个代理对象) 对象的 startActivity 方法启动 Activity。
  4. 到目前为止所有过程都在 Launcher 进程里面执行,接下来 ActivityManagerProxy 对象跨进程调用 ActivityManagerService (运行在 system_server 进程)的 startActivity 方法启动应用。
  5. ActivityManagerService 的 startActivity 方法经过一系列调用,最后调用 zygoteSendArgsAndGetResult 通过 socket 发送给 zygote 进程,zygote 进程会孵化出新的应用进程。
  6. zygote 进程孵化出新的应用进程后,会执行 ActivityThread 类的 main 方法。在该方法里会先准备好 Looper 和消息队列,然后调用 attach 方法将应用进程绑定到 ActivityManagerService,然后进入 loop 循环,不断地读取消息队列里的消息,并分发消息。
  7. ActivityManagerService 保存应用进程的一个代理对象,然后 ActivityManagerService 通过代理对象通知应用进程创建入口 Activity 的实例,并执行它的生命周期函数。

总结过程就是:用户在 Launcher 程序里点击应用图标时,会通知 ActivityManagerService 启动应用的入口 Activity, ActivityManagerService 发现这个应用还未启动,则会通知 Zygote 进程孵化出应用进程,然后在这个应用进程里执行 ActivityThread 的 main 方法。应用进程接下来通知 ActivityManagerService 应用进程已启动,ActivityManagerService 保存应用进程的一个代理对象,这样 ActivityManagerService 可以通过这个代理对象控制应用进程,然后 ActivityManagerService 通知应用进程创建入口 Activity 的实例,并执行它的生命周期函数。

生命周期函数执行流程

MainActivity的启动流程:

-> Application 构造函数
-> Application.attachBaseContext()
-> Application.onCreate()
-> Activity 构造函数
-> Activity.setTheme()
-> Activity.onCreate()
-> Activity.onStart
-> Activity.onResume
-> Activity.onAttachedToWindow
-> Activity.onWindowFocusChanged</pre>

总结

启动耗时:

从调用startActivitity到Activity可被操作为止,代表启动成功。所谓的可被操作,是指可接受各种输入事件,比如手势、键盘输入之类的,换个角度来说,也可以看成是主线程处于空闲状态,能执行后续进入的各种Message。

以A活动启动B活动为例,步骤如下:

  1. 活动A调用startActivity,到活动A成功pause为止。
  2. 活动B成功初始化,到执行完resume为止。
  3. 活动B像WSM注册窗口,到第一帧绘制完成为止。

三、系统耗时统计

  1. 使用adb命令

    adb shell am -W 包名/ activity绝对路径


    image-20201216161010942.png
  2. 手动打点
    起始时间点
    起始时间点比较容易记录:如果记录冷启动启动时间一般可以在 Application.attachBaseContext() 开始的位置记录起始时间点,因为在这之前 Context 还没有初始化,一般也干不了什么事情,当然这个是要视具体情况来定,其实只要保证在 App 的具体业务逻辑开始执行之前记录起始时间点即可。如果记录热启动启动时间点可以在 Activity.onRestart() 中记录起始时间点。
    结束时间点
    和上面一段类似,启动耗时的结束时间点要选在App显示出第一屏界面的时候,但是在什么时候App会显示出第一屏界面呢?通常上来说,Activity的onResume方法执行完之后,Activity就可以实现与用户交互了,但是实际上,一个Activity执行完onCreate、onStart、onResume之后,只是完成了应用自身的一些配置,比如Activity主题的设置,window属性的设置,view树的建立,但是其实后面还需要各个View执行measure、layout、draw等等。所以在OnResume中记录结束时间点的Log并不准确。

Activity.onWindowFocusChanged的注释:

*Called when the current {@link Window} of the activity gains or loses
* focus.  This is the best indicator of whether this activity is visible
* to the user.  The default implementation clears the key tracking
* state, so should always be called.
...

通过注释我们可以看出,这个函数是判断activity是否可见的最佳位置,所以我们可以在Activity.onWindowFocusChanged记录应用启动的结束时间点,不过需要注意的是该函数,在Activity焦点发生变化的时候就会触发,所以要做好判断,去掉不需要的情况。

四、 关于Adb命令得到的启动耗时

一般使用adb shell am start -W 包名/活动名(注意是绝对路径)命令得到的时间会有三个,分别是:

  • ThisTime:最后一个启动的Activity的启动耗时;

  • TotalTime:自己的所有Activity的启动耗时;

  • WaitTime: WaitTime 返回从 startActivity 到应用第一帧完全显示这段时间. 就是总的耗时,包括前一个Activity pause 的时间和当前Activity启动的时间。 “adb shell am start -W”的实现是在android / platform / frameworks / base / cmds / am / src / com / android / commands / am / Am.java文件中。其实就是跨Binder调用ActivityManagerService.startActivityAndWait()接口,这个接口返回的结果包含上面列出的两种时间:ThisTime、TotalTime时间。

WaitTime=endTime-startTime

startTime记录的是刚准备调用startActivityAndWait()的时间点,endTime记录的是startActivityAndWait()函数调用返回的时间点,也就是说,WaitTime=startActivityAndWait()调用耗时。 ThisTime、TotalTime的计算在frameworks\base\services\core\java\com\android\server\am\ActivityRecord.java文件的reportLaunchTimeLocked()函数中。

private void reportLaunchTimeLocked(final long curTime) {
 final ActivityStack stack = task.stack;
 if (stack == null) {
 return;
 }
 final long thisTime = curTime - displayStartTime;
 final long totalTime = stack.mLaunchStartTime != 0
 ? (curTime - stack.mLaunchStartTime) : thisTime;
 ...
 }

上面这段代码中,有curTime、displayStartTime、mLaunchStartTime这三个时间变量。 首先: curTime表示该函数调用的时间点. displayStartTime表示一连串启动Activity中的最后一个Activity的启动时间点。 mLaunchStartTime表示一连串启动Activity中第一个Activity的启动时间点。
正常情况下点击桌面图标只启动一个有界面的Activity,此时displayStartTime与mLaunchStartTime便指向同一时间点,此时ThisTime=TotalTime。另一种情况是点击桌面图标应用会先启动一个无界面的Activity做逻辑处理,接着又启动一个有界面的Activity,在这种启动一连串Activity的情况下(知乎的启动就是属于这种情况),displayStartTime便指向最后一个Activity的开始启动时间点,mLaunchStartTime指向第一个无界面Activity的开始启动时间点,此时ThisTime!=TotalTime。这两种情况如下图:

image

在上面的图中,用①②③分别标注了三个时间段,在这三个时间段内分别干了什么事呢? 在第①个时间段内,AMS创建ActivityRecord记录块和选择合理的Task、将当前Resume的Activity进行pause; 在第②个时间段内,启动进程、调用无界面Activity的onCreate()等、pause/finish无界面的Activity; 在第③个时间段内,调用有界面Activity的onCreate、onResume; 看到这里应该清楚 ThisTime、TotalTime、WaitTime三个时间的关系了吧。WaitTime就是总的耗时,包括前一个应用Activity pause的时间和新应用启动的时间;ThisTime表示一连串启动Activity的最后一个Activity的启动耗时;TotalTime表示新应用启动的耗时,包括新进程的启动和Activity的启动,但不包括前一个应用Activity pause的耗时。也就是说,开发者一般只要关心TotalTime即可,这个时间才是自己应用真正启动的耗时。 最后说下系统是根据什么来判断应用启动结束的:我们知道应用启动包括进程启动、走Activity生命周期onCreate/onResume等。在第一次onResume时添加窗口到WMS中,然后measure/layout/draw,窗口绘制完成后通知WMS,WMS在合适的时机控制界面开始显示(夹杂了界面切换动画逻辑)。记住是窗口界面显示出来后,WMS才调用reportLaunchTimeLocked()通知AMS Activity启动完成。

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

推荐阅读更多精彩内容