应用启动速度(Launch-Time)的优化


序言

应用启动是整个app工序的第一道流程。对于开发者,一般需要在应用启动过程中进行初始化工作,启动页的UI展示。而对于用户来说,启动速度的快慢则极大地影响了使用体验,并且间接地影响了用户的留存率(对于某些追求流畅体验的用户,一个启动缓慢的app会造成卡顿迟滞的印象,被直接划掉也不是不可能),下图是经过优化的沃家视频启动页.

c21d30343d6a5e2fdbce614b359a86eb.gif

工欲善其事,必先利其器。如果想要对app的启动过程进行优化,那么首先就要了解app的启动过程和常用的优化工具,下面就结合安卓官方文档,以及一些我在预研过程中阅读的优质文章,来总结下app的三种启动过程.


冷启动

冷启动代表app从运存数据完全被擦除的状态启动启动的过程,在此之前,app所属的进程还未被创建.冷启动一般发生在系统重启后或者app被系统杀死后app首次被启动,
冷启动分为以下三个步骤:

  • 加载并启动app
  • 启动后展示系统配置的空白Window
  • 创建app进程

在创建完app进程后,则会进行下面几个步骤:

  • 创建app用到的对象
  • 启动主线程(UI线程)
  • 创建app的main activity
  • 加载activity的view
  • 布局屏幕
  • 完成首帧的绘制

而一旦完成首帧的绘制后,系统会将当前展示的background-window换出,替换为main-activity的背景。从这个时间点开始,用户就可以开始使用app了.

两个重要的时间点
  • 第一个需要注意的时间点是,当我们点击launcher上的应用图标后,首先出现的是系统绘制的window的默认背景,根据app使用的不同theme.这个默认背景是白色或黑色的空白屏幕,在性能较好的机器上,这个默认背景可能会一晃而过,但在某些性能较差的机器或者机器卡顿的情况下会导致白屏停留时间过长,所以启动屏的默认背景是一个需要优化的点,后面会详细总结如何优化这个点.

  • 第二个需要注意的地方是首帧的绘制时机,实际上我们知道在包括 Application.onCreate() ,Activity.onCreate()Activity.onResume() 等生命周期回掉函数执行时,view的布局和绘制都还没有开始,在这些生命周期回调函数中,如果对一些短小的操作耗时操作做异步处理,很可能造成负优化的效果(线程切换增加耗时,实际上没有延时的效果),最好的做法应该是在首帧绘制完成的前后异步处理耗时的逻辑,具体的做法后面总结.

冷启动的总结
image.png

通过上面这张官方文档提供的流程图可以看出,冷启动经过了 Application的创建,Activity的创建,首帧的绘制 等过程,截至到 Displayed-Time这个时间点完成了上述步骤,后面的Other Stuff 步骤可以看做是开发者自定义的一些初始化操作,如果要在log中查看这个时间点,可以通过reportFullyDrawn()这个函数来上报.


热启动

应用程序的热启动要比冷启动简单,消耗也更少,热启动的常见场景就是app的前后台切换.在从后台切换到前台的过程中,如果应用程序的activities还驻留在内存中,app就不需要再重复经历对象初始化,布局加载和渲染这些步骤.
但是,如果某些内存因为内存整理(比如说onTrimMemory())而导致被清理,那么在响应热启动事件时这些被清理的对象就需要重新创建.
热启动和冷启动在屏幕表现上一致,在app完成activity的渲染之前都会一直展示空白屏幕.


温启动

温启动这个名词平时不常见到,官方文档中是这样解释的:温启动包含了冷启动的一部分操作集,同时它的消耗要比冷启动要少.温启动的常见场景如下:

  • 用户退出app后重新进入(很多app会在退出时重新启动一个新的实例做到常驻,这里只讨论部不常驻的场景)。当app退出后,进程有可能仍在运行,这时候如果重新启动app,那么activity必须要从onCreate()生命周期开始重新创建.
  • 系统干掉了驻留内存的app。这时如果重新启动app,那么进程和activity都是需要重新创建,但onCreate() 会传入 saveInstance,通过使用saveInstance可以节省耗时.总的来说温启动在耗时上介于冷启动和热启动之间.

统计应用启动时间

总结了应用启动的几种状态,接着总结如何统计应用启动的时间,因为应用启动的时间一般很短,需要有精确的统计时间来支撑优化结果.

而统计应用启动时间我们可以通过查看日志来获取相关信息:


查看logcat日志

image.png

上图是查看logcat中 TAG为 ActivityManager的日志得到的启动时间信息.可以看到从冷启动状态打开知乎客户端一共耗费了 +1s627ms(知乎客户端启动页出现白屏的情况,感觉不应该出现这种情况).


adb shell am start

通过adb命令我们可以更加得到更加详细的数据.
使用
adb shell am start -W -S [packageName]/[ActivityPath]

  • -W 列出启动过程中统计到的具体数据
  • -S 强制停止当前的Activity,重新启动

使用这个adb命令,我们可以得到较为详尽的启动统计数据,仍以知乎客户端为例子.

adb shell am start -W -S com.zhihu.android/.app.ui.activity.MainActivity
Stopping: com.zhihu.android
Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.zhihu.android/.app.ui.activity.MainActivity }
Status: ok
Activity: com.zhihu.android/.app.ui.activity.MainActivity
ThisTime: 1766
TotalTime: 1766
WaitTime: 1780
Complete

具体的启动数据如上所示,这段贴出的adb命令得到的数据和上方截图中的数据来自相同的一次测试,通过对比可以发现,通过adb shell得到的ThisTime这个值与log信息中的DisplayedTime是相同的.但是我们可以看到有三个时间数据: ThisTime TotalTime WaitTime。那么其它两个时间字段分别代表什么含义呢?


ThisTime TotalTime WaitTime 各自的含义

讲解着三个时间段的含义,不得不提到我看到的一篇质量很高的博文,博主长期负责FrameWork层的性能优化工作,对于启动优化理解很深,我试着依样画葫芦在心里讲解了一遍,发现有很多很深的点我get不到,这里就先贴出博文,不再重复分析了.
http://androidperformance.com/2015/12/31/How-to-calculation-android-app-lunch-time.html

这里罗列下最后的结论(图源为上方博文):


  • 在第①个时间段内,AMS 创建 ActivityRecord 记录块和选择合理的 Task、将当前Resume 的 Activity 进行 pause
  • 在第②个时间段内,启动进程、调用无界面 Activity 的 onCreate() 等、 pause/finish 无界面的 Activity
  • 在第③个时间段内,调用有界面 Activity 的 onCreate、onResume

所以,这三个启动时间间的关系为:

  • WaitTime 就是总的耗时,包括前一个应用 Activity pause 的时间和新应用启动的时间
  • ThisTime 表示一连串启动 Activity 的最后一个 Activity 的启动耗时
  • TotalTime 表示新应用启动的耗时,包括新进程的启动和 Activity 的启动,但不包括前一个应用 Activity pause 的耗时。也就是说,开发者一般只要关心 TotalTime 即可,这个时间才是自己应用真正启动的耗时

总之,TotalTime是我们在开发过程中应该改关注的一个时间值,它基本上可以与应用的冷启动过程挂钩。


如何优化应用的启动时间

总结了统计app启动时间的方法后,就需要思考如何优化app启动时间了.我的思路是:优化app启动实际上就是优化冷启动过程,因为冷启动过程包含了启动过程中的每一步.而从上面启动概念总结和统计方法的总结也可以看出,优化应该分为两步:优化进程创建过程的耗时(在应用层面就是优化Application的生命周期函数内的耗时操作)以及优化 Activity的创建过程


优化Applicatoin.onCreate()中的耗时操作

一般在Applicatoin的创建过程中,都会做一些初始化的工作,例如:MultiDex.install(),AppManager.init()(这里是伪代码的形式,泛指各种管理类的初始化工作)。在这些初始化的操作中,难免会包含一些文件读写数据库的增删改查参数配置,等等耗时操作,优化这部分逻辑我的思路是:通过延时异步来解决.

  • 例如Manager.init() ,这种全局管理类的初始化操作有时候可以懒加载的,就是说通过单例模式结合懒加载在我们需要用到相关类的时候在进行初始化,而不是一股脑的全部放在Application.onCreate()中.
  • 通过异步的方式.具体来说就是将不是特别紧急的耗时操作放在低优先级的工作线程中来异步进行,或者在后台service中开启线程来进行.但是这样做有一个特别需要注意的地方,对于那些短耗时的操作来说,这么做其实很有可能造成负优化的效果,因为我们的最终目的是让应用的首帧画面尽快的加载出来,用户能够迅速地进入到MainActivity当中,但是不加考虑的将短耗时操作放入异步线程中,这些工作线程很有可能在MainActivity进行布局和渲染前就已经完成了自身使命,那么这样来看布局初始化前的绝对时间并没有减少,相反因为建立线程和切换线程等开销,造成了更多的时间消耗,所以这是一个特别需要注意的地方.

Application的优化工作大概就这些,当然因为各个app复杂程度不同,业务类型不尽相同,具体的优化操作肯定会比较深入,这里我这是按照我在工作中进行的优化工作进行的自我总结,关于我的优化过程重点也不在这里。


Activity创建过程的优化

在这次对app的优化过程中,效果最为明显的操作是对启动页(SplashActivity)的优化。通过上面的基础概念的总结可以得出结论:在Activity的 onCreate()onResume() 生命周期中, app的第一个frame其实都还没有绘制出来.但因为Activity的创建过程和调用链相当长,这里先不总结Activity的创建流程.
只需要先知道Activity的布局和渲染过程其实是在 onResume执行之后才开始的.Activity在AMS置于resume状态后,Activity所属的Window会通过WindowManagerImpl,addView()将decorView放入到 ViewRoot中,然后ViewRoot发起 traversal遍历整个view树,进行布局和渲染,最终将画面绘制到屏幕上.
所以说在不影响业务流程的情况下,为了尽快将app的首帧绘制到屏幕上,我们最后将启动页的耗时操作(例如下载广告图)放在首帧绘制完成之后进行.
那么问题来了,如何尽可能捕捉到应用完成首帧绘制的时间点呢?

在我的另外一篇总结 GPU-Rendering Profile分析中分析了android是如何将view绘制到屏幕上的,基本流程就是 Measure - Layout - Draw - GPU渲染,所以可以选择hook这几个时间点,选择合适的时机插入耗时操作,使得耗时操作可以尽量延迟执行.

  • 首先列出未优化前的代码,直接在onResume方法中启动耗时操作.
    @Override
    protected void onResume() {
        super.onResume();
        getWindow().getDecorView().post(mGetSplashDataRunnable);
  • 优化方案选择在ViewTreeObserver.onGlobalLayoutListener()的回调中插入耗时操作.
       getWindow().getDecorView().getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                getWindow().getDecorView().getViewTreeObserver().removeOnGlobalLayoutListener(this);
                getWindow().getDecorView().post(mGetSplashDataRunnable);
                Log.i(Constants.TAG.TAG_TEST_LAUNCH_TIME, "onGlobalLayout");
            }
        });
onResume.jpg

onGlobalLayout.jpg

然后列出两种方案的启动耗时测试,发现使用优化方案的平均启动时间要减少80ms左右.


自定义默认窗口背景,优化用户体验

虽然通延时加载耗时任务能够在一定程度上加快app首帧的显示速度,但是缩短的80ms的启动时间相比较总的耗时(520ms左右)来说,用户体验依旧没有得到明显地提升。
前面总结到,点击桌面上的应用图标,首先为我们展示的是一个空白的默认启动背景,为了避免白屏的出现而影响体验,最有效的方法还是需要修改默认的空白背景,替换为app的默认启动页画面.
具体做法为新建一个 shape.xml,背景使用app的默认启动图

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"
    android:opacity="opaque">
    <item android:drawable="@color/white" />
    <item
        android:drawable="@drawable/default_splash_ad"
        android:gravity="fill" />
</layer-list>

然后建立一个新的主题,并将该主题设为启动页Activity的background。

    <style name="AppSplashTheme" parent="Theme.AppCompat.Light.NoActionBar">
        <item name="android:background">@drawable/shape_launch</item>
    </style>

这样就从根本上解决了启动页会产生白屏的情况,当然对于启动速度的优化仍然是必要的,否则长时间卡在启动界面,即使没有了白屏情况,用户体验也会很差.

以上。

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,567评论 25 707
  • 请保持淡定,分析代码,记住:性能很重要。 启动时间优化 毫无疑问,应用的启动速度越快越好。 本文可以帮助你优化应用...
    Mupceet阅读 11,249评论 5 19
  • 先讲点题外话 简述Activity的几种启动模式 standard标准启动模式,也是Activity的启动模式,以...
    大大大大大先生阅读 6,501评论 0 3
  • 文/无语 一些琐碎的小事总会牵动我们的神经,让我们在生活中常感疲惫。 怎样才能从繁琐中抽身?我们需要清醒地活着,遇...
    无语_c7b8阅读 276评论 2 2
  • 用一生执念等待一季芳华。 每一个季节都有一个故事。 春光灿烂,春水微漾,春花娇美 有关于春的一切 似乎都 很美,很...
    瑶YY阅读 161评论 0 1