Android App 冷启动优化方案

9,059 阅读10分钟

App 启动优化

启动方式

  • 冷启动

    当启动应用时,后台没有该应用的进程(常见如:进程被杀、首次启动等),这时系统会重新创建一个新的进程分配给该应用

  • 暖启动

    当启动应用时,后台已有该应用的进程(常见如:按back键、home键,应用虽然会退出,但是该应用的进程是依然会保留在后台,可进入任务列表查看),所以在已有进程的情况下,这种启动会从已有的进程中来启动应用

  • 热启动

    相比暖启动,热启动时应用做的工作更少,启动时间更短。热启动产生的场景很多,常见如:用户使用返回键退出应用,然后马上又重新启动应用

热启动和暖启动因为会从已有的进程中来启动,不会再创建和初始化Application

平时我们讨论中基本都会将暖启动和热启动合在一起统称为热启动,因为暖启动与热启动差异很小,如果不是特别留意启动流程,那么在用户体验和感官上没有直接差异,但是在framework层执行时是有一定差异的。本次优化点也是围绕冷启动和热启动来做,将暖启动与热启动统称为热启动

另外有一点,从绝对时间上来看,app安装后的首次启动将会最耗时,因为首次启动会新建数据库,sp文件,各种缓存,配置等


白屏/黑屏问题

  • 白屏或黑屏,具体是哪一个,取决于appTheme使用的是dark还是light主题

  • Android Studio 引起的白屏

    2.x时代的AS开启了instant run以后可能会导致白屏,但实际完整的apk包不会出现此问题

  • 冷启动引起的白屏/黑屏

    点击你app那一刻到系统调用Activity.onCreate()之间的时间段。在这个时间段内,WindowManager会先加载app主题样式中的windowBackground作为app的预览元素,然后再真正去加载activitylayout布局

  • 暖启动/热启动引起的白屏/黑屏

    这点在配置较好,内存空间充足的手机上不是很明显,但低端手机或者内存吃紧的情况下依旧会出现”闪屏”效果,持续时间很短,一闪而过

优化

我将冷启动优化分为可控阶段和不可控阶段

  • 不可控阶段

    点击app以后到初始化Application之间这段时间,系统接管,从Zygote进程中fork创建新进程,GC回收等等一系列操作,和我们app无关

  • 可控阶段

    初始化Application开始,如下图

    冷启动应用程序工作流示意图

    从上图可以看到,整个冷启动流程中至少有两处onCreate,分别是ApplicationActivity,整个流程都是可控的。所以,onCreate方法做的事情越多,冷启动消耗的时间越长

启动时间

  • Logcat 自动打印

    Android 4.4(API 19)开始,Logcat自动帮我们打印出应用的启动时间。这个时间从应用启动(创建进程)开始计算,到完成视图的第一次绘制(即Activity内容对用户可见)为止

    • 冷启动 :
    04-25 14:53:09.317 869-1214/? I/ActivityManager: Displayed cn.com.dhc.danlu/.shell.activity.InitializeActivity: +4s256ms
    04-25 14:53:11.077 869-1214/? I/ActivityManager: Displayed cn.com.dhc.danlu/.shell.main.MainActivity: +559ms
    
    • 热启动:
    04-25 14:53:20.407 869-1214/? I/ActivityManager: Displayed cn.com.dhc.danlu/.shell.activity.InitializeActivity: +178ms
    04-25 14:53:22.447 869-1214/? I/ActivityManager: Displayed cn.com.dhc.danlu/.shell.main.MainActivity: +131ms
    
    • 这个 log 信息是从com.android.server.am.ActivityRecord#reportLaunchTimeLocked(long curTime)中打印出来的
    private void reportLaunchTimeLocked(final long curTime) {
       final ActivityStack stack = task.stack;
       final long thisTime = curTime - displayStartTime;
       final long totalTime = stack.mLaunchStartTime != 0
               ? (curTime - stack.mLaunchStartTime) : thisTime;
       if (ActivityManagerService.SHOW_ACTIVITY_START_TIME) {
           Trace.asyncTraceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER, "launching", 0);
           EventLog.writeEvent(EventLogTags.AM_ACTIVITY_LAUNCH_TIME,
                   userId, System.identityHashCode(this), shortComponentName,
                   thisTime, totalTime);
           StringBuilder sb = service.mStringBuilder;
           sb.setLength(0);
           sb.append("Displayed ");
           sb.append(shortComponentName);
           sb.append(": ");
           TimeUtils.formatDuration(thisTime, sb);
           if (thisTime != totalTime) {
               sb.append(" (total ");
               TimeUtils.formatDuration(totalTime, sb);
               sb.append(")");
           }
           Log.i(ActivityManagerService.TAG, sb.toString());
       }
       mStackSupervisor.reportActivityLaunchedLocked(false, this, thisTime, totalTime);
       if (totalTime > 0) {
           //service.mUsageStatsService.noteLaunchTime(realActivity, (int)totalTime);
       }
       displayStartTime = 0;
       stack.mLaunchStartTime = 0;
    }
    
    // normal time:统计的是 Activity 从启动到界面绘制完毕的时间
    // total time :统计的是 normal time + Activity 栈建立完毕的时间
    
  • 测量 Activity 启动时间

    ActivityreportFullyDrawn()

    我们可以在Activity的任意位置调用此方法已打印你想看到的、执行完某个方法的最终时间。它会在Logcat里打印从apk初始化(和前面Displayed的时间是一样的)到reportFullyDrawn()方法被调用用了多长时间

    ActivityManager: Displayed com.Android.myexample/.StartupTiming: +768ms
    

    4.4上调用reportFullyDrawn()方法会崩溃(但是log还是能正常打印),提示需要UPDATE_DEVICE_STATS权限 ,但是这个权限无法拿到

    try {
    reportFullyDrawn();
    } catch (SecurityException e) {
    }
    
  • 本地调试启动时间

    本地调试

    上述命令可以直接启动对应包名的对应activity,但要注意不是全部activity都能使用这个命令直接启动

    • 热启动 :

    热启动

    • 冷启动:

    冷启动

    • 只需要关注TotalTime即可
  • adb screenrecord 命令

    • 首先启动带bugreport选项(它可以在frames中添加时间戳-应该是L中的特性)的screenrecord命令

    录屏

    • 然后点击app图标,等待app显示,ctrl-C停止screenrecord命令,在手机存储中会生 成aunch.mp4视频文件,然后pull到电脑
    • 打开视频逐帧播放,注意视频的上方有一个frame时间戳。一直往前直到你发现app图标高亮了为止。这个时候系统已经处理了图标上的点击事件,开始启动app了,记录下这一帧的时间。继续播放帧直到你看到了app整个UI的第一帧为止。根据不同情况(是否有启动窗口,是否有启动画面等等)。事件和窗口发生的实际顺序可能会有不同。对于一个简单的app来说,你会首先见到启动窗口,然后渐变出app真实的UI。在你看到UI上的任何内容之后,再记录下第一帧,这时app完成了布局和绘制,准备开始显示出来了。同时也记录下这一帧所发生的时间
    • 现在把这两个时间相减 ((UI displayed) - (icon tapped)) 得到app从点击到绘制就绪的所有时间。虽然这个时间包含了进程启动之前的时间,但是至少它可以用于跟其他app比较

由此可见,app冷启动时间大约为4s,热启动时间大约为132ms.

优化方案(仅针对可控区范围)

  • 从启动流程分析

    减少两处onCreate()中的初始化操作,将部分初始化移动到IntentService中进行

  • 从用户体验分析

    app首页的按返回键响应修改为响应Home键,曲线救国。让用户以为app确实退出了,但是实际上是点了Home键。如此一来,下次点击app图标的时候,直接唤起,不需要进行初始化操作,主要可以避免再次走闪屏页,参考美团,微信,QQ,淘宝等(实现的效果一样,但是实现方式就不得而知了)

    • 微博:启动后点击返回键和Home键的操作一样,底部选中tab没有做自切换
    • 美团:启动后点击返回键和Home键的操作不一样,底部选中tab做了自切换
    • QQ:同微博
    • 淘宝:启动后在首页按返回键,会先回到第一个tab,然后再退出

开始优化

  • 利用Google官方文档推荐的方式,我们将启动页界面的主题设置为SplashTheme。此界面是冷启动后首先加载的界面:

    mainifest

    主题内的代码如下:

    style

    这个主题相当于丢了一张图片作为背景,也就是红色背景LogoSlogan图片,无版本号

    此时我们已经“消除了”白屏/黑屏页,将冷启动的白屏/黑屏单调的纯色背景替换为我们即将展示给用户的 InitializeActivity界面的图片,从系统的Window到我们自己App跳转过程,使用了全屏属性,以达到无缝跳转

    需要说明的是,这一步做了之后,对整体启动时间并没有任何的减少,时间不变,只是说给用户的体验要友好很多,不再显示一个突兀的白屏/黑屏界面 ------将锅甩给自己appapp太卡,居然在InitializeActivity要等这么久(当然用户不知道的是:系统window界面和InitializeActivity不是同一个界面)

    当然也可以采用另一种方式,那就是将上面的主题中的backgroud设置为透明用户点击了图标开始启动的时候,界面上没有任何变化,因为此时系统启动的那个白屏/黑屏界面背景透明的 ------将锅甩给系统,太卡,点了图标居然隔了这么久才显示InitializeActivity

  • 接下来我们查看ApplicationInitializeActivityonCreate()是否有可以迁移到IntentService中的代码:

    • BaseApplication:

    BaseApplication

    ​ 可以看到,其实当中的逻辑不是很多,并且都是需要在Application中初始化完毕的,不能单独提出来进行初始化,其中只有GrowingIO可以考虑提出来

    • MainApplication:

    MainApplication

    ​ 推送初始化可以提出来

    • InitializeActivity:

    ActivityonCreate

    ​ onCreate 中乍看没有什么耗时操作,内部的几个方法也都是必须要的业务逻辑,唯一能动的就是内部针对界面停留做的延时时间,目前是2s,可以减少到1s左右。

  • 最后一步,在MainActivity中处理返回键逻辑。

    将确实是退出的逻辑替换为按Home键:

    activity返回键

    这种做法给用户一个假象:用户按返回键退出,但是实际上并没退出,app处于后台,下次点击图标时直接唤起

    针对这种操作,需要注意几个点

    • onRestart中需要判断tab状态
    • onSaveInstanceStateonRestoreInstanceState中需要保存和恢复数据,用于判断用户是点了 home还是back,这两种操作需要区分开,同时需要保存tab状态
    • 通过广播监听Home键事件
    • ......
  • 结果

    冷启动:

    优化后冷启动

    热启动:

    优化后热启动

    由上图可知,优化后的冷启动时间大约为3544ms,热启动时间大约为127ms,相比之前的4121ms以及151ms来说有一定的提升,白屏效果也被消除了。

    但是其实提升最大的点不是白屏优化,因为我们没有把ApplicationActivityonCreate的逻辑减少并提到IntentService中。

    最大的提升点是,我们让用户退出app时,造成假象,让用户以为他确实退出了app,但实际上我们是藏在后台,当用户热启动或者温启动时,我们不用再经过InitializeActivity的流程进入首页。


总结

  • 白屏/黑屏界面使用图片替换
  • onCreate中尽量避免做过多的初始化动作,如果必须,那么考虑IntentService
  • 首页中对BackHome键的动作做一些假象,使用户按Back键时以为他退出了,以减少下次启动的不必要动作(建议:非即时消息类和社交类app,这种做法慎用,因为可能有流氓之嫌。。。(逃)
  • Activity#moveTaskToBack(true)

参考资料


TODO

  • [ ] 探究:为何新建的 Hello World 工程冷启动白屏时间比线上工程短一些

代码传送门