声明:本篇文章已授权微信公众号 YYGeeker 独家发布。

前言

对于一个APP来说,启动秒开,切换顺畅的体验能给用户留下良好的第一印象,启动速度对于用户体验及提高用户留存的重要性不言而喻。那么我们首先从它开始入手,从理论结合实际来谈谈有哪些优化启动速度及性能的技巧。

一、介绍

Google 官方介绍文档:https://developer.android.com/topic/performance/vitals/launch-time 有兴趣可以自行阅读。

Google 对应用的启动定义了三个概念,分为冷启动、热启动、温启动。而启动最耗时同时也是我们主要去优化的地方,就是冷启动。在冷启动App之时,手机系统会先执行以下三个任务:

  1. 点击Launcher APP图标,响应启动App;
  2. App启动之后展示一个空白的Window;
  3. 创建App的进程。

这三个任务执行完毕之后,我们的App进程就创建成功了,然后会执行以下操作:

  1. 创建 APP 对象;
  2. 启动Main Thread;
  3. 创建MainActivity;
  4. 加载视图;
  5. 布置到屏幕;
  6. 进行首次绘制。

大致流程如下图所示:
Android 启动优化(一)_第1张图片

从系统层面来看,一个 Activity 走完 onCreate/onStart/onResume 这几个生命周期之后,只是完成了应用自身的一些配置,比如 window 的一些属性的设置/View树的建立,并没有显示。换句话来说,其实到这一步系统只是调用了 inflate 而已。后面 ViewRootImpl 还会调用两次performTraversals ,初始化 Egl 以及 measure/layout/draw 等。

因此,在Android系统里,我们定义一个应用的启动时间, 肯定不能以Activity 的回调函数作为基准,而应该以用户在手机屏幕上看到我们在 onCreate 的 setContentView 中设置的 layout 完全显示为准,也就是我们常说的应用第一帧。而一旦成了第一次绘制,系统进程就会用Main Activity替换掉之前已经展示的Background Window。

App进程的创建等环节,我们无法去主动干涉控制。那么我们可以优化启动速度的方向有哪些呢? 本篇文章将围绕它展开讨论。

二、分析

Google对启动时长定义了这三个概念:

  • ThisTime:最后一个启动的Activity的启动耗时;
  • TotalTime:表示自己应用启动的耗时,包括新进程的启动和Activity的启动;
  • WaitTime: ActivityManagerService启动App的总耗时(包括当前一个应用Activity的onPause()和自己Activity的启动)

ThisTime、TotalTime 的值在 frameworks\base\services\core\java\com\android\server\am\ActivityRecord.java 文件的 reportLaunchTimeLocked() 函数中计算得到,有兴趣可自行翻阅。

一般来说,开发者只要关心TotalTime即可,这个时间才是我们自己应用真正启动的耗时。一般来说我们可通过如下几种方法检测启动耗时:

2.1 系统log打印

在Android 4.4(API级别19)及更高版本中,系统会输出一个包含名为Displayed的值的输出行。此值表示Activity启动过程和完成在屏幕上绘制相应活动之间所经过的时间长度。

我们在Android Studio的Logcat可以查看这个输出信息,需要注意的是我们在logcat视图中,需要去除过滤器,选择 No Filters。因为系统的输出信息是在系统进程服务,而不是应用程序本身输出的启动日志,具体可参考下图:

Android 启动优化(一)_第2张图片

2.2 ADB 命令打印

通过adb启动我们的Activity或Service,控制台会输出应用的启动时间,cmd命令格式如下:

adb shell am start -W  [packagename/activity]

如执行 手机YY 启动时间统计命令:

adb shell am start -W  com.duowan.mobile/com.yy.mobile.ui.splash.SplashActivity

log 打印如下:

Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.duowan.mobile/com.yy.mobile.ui.splash.SplashActivity }Status: okActivity: com.duowan.mobile/com.yy.mobile.ui.splash.SplashActivityThisTime: 1794TotalTime: 1794WaitTime: 1831Complete

2.3 Trace 文件

通过Trace文件我们也可以分析启动时的相关信息,在ANR触发时,系统会自动生成Trace文件以供我们去分析。一般来说,正常情况下Trace 文件可以有以下几种主动生成的方式:

代码生成

API 19或者以上可以通过如下方式打印trace 文件:

Debug.startMethodTracing("test");  //开始 trace(保存文件到 "/sdcard/test.trace" )// 省略业务代码... Debug.stopMethodTracing(); //结束 trace:

使用DDMS

DDMS(Dalvik Debug Monitor Service),是AndroidSDK里面自带的工具,开发环境中对Dalvik虚拟机调试监控的一种服务,它用于对Android的应用程序以及Framework层的代码进行性能分析。具体使用方式可自行查阅资料,这里就不做过多补充了。

2.4 Profile 工具

从上面几个简单的方法我们可以知道启动的总体耗时,那么具体是哪个操作执行耗时过长呢? 我们需要借助工具根据实际情况去分析。Android Studio CPU Profiler 是Google 官方提供的检测工具,我们平常开发中可利用它来分析应用启动过程中cpu执行耗时详情信息。需要注意的是,由于是这种方式是侵入式的,实际耗时会有些许失真,收集到的时长会比真实时间要长一些,不过我们可以通过整体耗时比例及触发业务场景,识别性能瓶颈,去对症下药优化应用启动时间。

小结: 应用启动耗时的具体症结可以通过各种工具来监测,上述只是常用的一些工具,同时Android studio不同的版本 工具可能也会更新和完善,这里就不过多具体阐述工具的使用了。

三、解决方案

从上述几个方面的分析,我们可以知道应用启动耗时的一些监测方式,通过监测我们可以查找具体症结所在,然后对症下药,接下来我们再讨论一下常见优化解决方案,提供思路及方案以供参考。

3.1 主题优化

相信大家都看到过这种情况:应用启动时,有时会出现短暂黑屏或白屏的现象。如果启动比较慢的时候,白屏/黑屏过久甚至会长达几秒,这严重影响了用户的体验。那么为什么会出现黑屏或白屏?我们又应该怎么解决这个问题呢?具体缘由我们可以探索源码来仔细分析。

Activity窗口的启动窗品的创建过程

从上图我们可以看出,启动过程窗口的创建最终是交由PhoneWindowManager去管理的,那么我们下载source code 去看一下 PhoneWindowManager 对于主题设置的相关逻辑:

  • 系统版本:Nougat 7.1
  • 源码根目录: frameworks/base/services/core/java/com/android/server/policy/PhoneWindowManager.java
public class PhoneWindowManager implements WindowManagerPolicy {//省略代码··· /** {@inheritDoc} */    @Override    public View addStartingWindow(IBinder appToken, String packageName, int theme,            CompatibilityInfo compatInfo, CharSequence nonLocalizedLabel, int labelRes,            int icon, int logo, int windowFlags, Configuration overrideConfig) {        if (!SHOW_STARTING_ANIMATIONS) {            return null;        }        if (packageName == null) {            return null;        }        WindowManager wm = null;        View view = null;        try {            Context context = mContext;            if (DEBUG_STARTING_WINDOW) Slog.d(TAG, "addStartingWindow " + packageName                    + ": nonLocalizedLabel=" + nonLocalizedLabel + " theme="                    + Integer.toHexString(theme));            if (theme != context.getThemeResId() || labelRes != 0) {                try {                    context = context.createPackageContext(packageName, 0);                    context.setTheme(theme);                } catch (PackageManager.NameNotFoundException e) {                    // Ignore                }            }                        if (overrideConfig != null && overrideConfig != EMPTY) {                if (DEBUG_STARTING_WINDOW) Slog.d(TAG, "addStartingWindow: creating context based"                        + " on overrideConfig" + overrideConfig + " for starting window");                final Context overrideContext = context.createConfigurationContext(overrideConfig);                overrideContext.setTheme(theme);                final TypedArray typedArray = overrideContext.obtainStyledAttributes(                        com.android.internal.R.styleable.Window);                final int resId = typedArray.getResourceId(R.styleable.Window_windowBackground, 0);                if (resId != 0 && overrideContext.getDrawable(resId) != null) {                    // We want to use the windowBackground for the override context if it is                    // available, otherwise we use the default one to make sure a themed starting                    // window is displayed for the app.                    if (DEBUG_STARTING_WINDOW) Slog.d(TAG, "addStartingWindow: apply overrideConfig"                            + overrideConfig + " to starting window resId=" + resId);                    context = overrideContext;                }            }       //省略代码···    }}

分析探索源码过程中我们可以知道,App启动闪黑屏/白屏的原因在于PhoneWindowManager中的addStartingWindow 方法里的设置逻辑,从addStartingWindow方法中我们不难看出 系统进程在创建Application的过程中会产生一个BackgroudWindow,直到完成第一次绘制,系统进程才会用MainActivity的界面背景替换掉原来的占位BackgroudWindow。

Android 启动优化(一)_第3张图片

从点击Lunach Icon那一刻起,到系统调用Activity.onCreate()之间的这个时间段内,WindowManager会先加载app主题样式中的windowBackground做为App的预览元素,然后再真正去加载Activity的layout布局。很显然,出现启动白屏或黑屏的情况(取决于主题是Dark还是Light),是因为我们的Application或Activity启动的这个过程太耗时,从而导致系统默认的BackgroundWindow没有及时被替换。

经过上述分析,那么问题就迎刃而解了。应用启动时黑屏或白屏过久的现象,无非是因为应用启动时WindowManager会去加载app主题样式中的windowBackground 而这个背景是根据当前应用的主题背景色决定的。那么我们有两种解决办法:

  1. 把样式替换成我们应用启动页的背景,启动时windowBackground的样式和启动页Activity视觉效果保持一致。
  2. APP主题背景设置成透明样式。

透明主题参考代码:

<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">"android:windowFullscreen">true "android:windowIsTranslucent">true style>

图片背景主题参考代码:

<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar"> "android:windowBackground">@drawable/launch"android:windowFullscreen">true "android:windowDrawsSystemBarBackgrounds">falsestyle>

可根据实际情况酌情使用,需要注意的是这种修改主题背景色的办法只能优化视觉效果,对于启动耗时没有什么本质性帮助。

注:启动窗口源码部分可以参考老罗的那篇博客,链接在本文末尾。

3.2 Application初始化减负

随着移动端的发展,诸如Push、LBS、Share、HotFix等功能可以说是应用必备功能,而这也催生了大量第三方SDK的横空出世,为广大开发者带来了福音,帮助开发者节省了很多开发时间和精力。但也由于这样,随着时间推移,越来越多的第三方SDK 被引入进我们的项目中,而这些第三方SDK 大多都在Application创建时被集中初始化,导致Application的启动耗时过久。应用初次安装启动时,严重的情况下,甚至可能出现启动时间长达十几秒甚至几十秒的现象,甚至出现启动ANR。

Google官方对它的定义 —— Avoid Heavy App Initialization,主要涉及到以下几点:

  • 第三方SDK初始化(如Push、定位、统计、插件化、HotFix等)
  • IO操作(SP读写、文件拷贝、下载等)
  • 跨进程通信问题
  • 业务优化

减少启动流程的Activity数量。

根据应用实际业务情况,可考虑将闪屏页/广告页等改成 Fragment,一般来说大约可减少启动耗时100ms左右。但需要注意业务改动成本及生命周期问题。

缓存资源数据

对于一些更新频率比较低的配置信息,或者资源等,我们可以采用缓存的方式避免每次启动都去下载,从而节省启动时间和CPU资源。

梳理业务流程

针对业务相关的代码逻辑,我们主要从下述几个方面去摸索优化之路:

  1. 将业务流程进行梳理,合理分配及延时加载。
  2. 学会合理与产品经理沟通,理性衡量需求。
  3. 合理分配线程资源,充分利用cpu时间片。
  4. 避免同步锁等待浪费线程资源。

小结: 相对而言,业务相关的代码可优化空间通常是比较大的,但复杂程度也比较高。需要我们有耐心地对应用针对性抽丝剥茧,整理业务。

3.3 保活

保活可以降低应用冷启动概率,让APP变成温启动,这样可以大大减少Application 创建及初始化耗时。对于QQ、微信、淘宝等大厂应用来说,一般都可以寻求与手机厂商合作,通过应用白名单,或是定制优化应用启动时间。对于中小应用而言,则更多地是通过各种黑科技实现保活机制,不过这种方式也造成了Android生态圈的各种问题,并且Android 8.0以后Google也提高了限制,保活机制开始变得越来越难。在低版本系统可根据自身应用实际情况做一些保活手段。一些比较简单的常见手法是提高进程优先级,或利用守护进程;以及拦截系统返回键双击回退的处理逻辑等。

3.4 GC 优化

众所周知,我们的Android应用是运行在Java虚拟机之上的,而Java虚拟机进行垃圾回收的时候,要做一件很形象的事,叫做stop-the-world。也就是说,为了回收那些不再使用的对象,虚拟机必须要停止所有的线程来进行必要的回收工作。虽然这一点在ART得到了很大的改善,但GC是有代价的,它对App运行时的性能始终会有影响(诸如内存抖动等问题)。
优化建议:

  1. 合理使用 软引用 和 弱引用。
  2. 合理使用数据结构,节省内存开销。
  3. 尽量少用finalize函数。
  4. 尽量早点释放无用对象的引用。
  5. 合理创建对象。
  6. 复用网络库或者图片库缓存。
  7. 频繁创建的一些对象可以考虑迁移到Native实现。

小结: 总体而言,我们需要尽量避免频繁的GC,减少它触发的次数。以免造成主线程长时间卡顿。

3.5 Dex 优化

对于Android 开发者来说, 想必我们对Dex 都不会太陌生,它承载了类以及APK里面的各种资源文件,APP在安装以及启动过程中都会读取dex文件。虽然Dex 里面包含的文件一般都比较小,但它们的读取频率非常高。因此,我们可以想办法去优化dex的排序以及分包、类加载等逻辑来提高系统的I/O效率,提高启动速度。

可以利用 ReDex 来优化我们的APK结构及体积,它是一款由Facebook 开源的工具,通过对字节码进行优化以减小Android Apk 的大小,同时可提高 App 启动速度。ReDex GitHub地址:https://github.com/facebook/redex

ReDex优化主要有以下几方面:

  1. Inline (内联)
  2. SynthPass (合成器)
  3. Interdex (重排编,dex分包优化)
  4. 删除无用代码
  5. 类代替(只有一个实现类的接口或父类)
  6. 字符串缩减(混淆及metadata的优化等)

当然,接入ReDex有一定成本和风险,我们也可以根据实际参考facebook的思路如Interdex等,自研一套优化方案(没错,腾讯和微信团队都是这么干的)。此外,应用加固、热修复、插件化等方案对于启动速度也有比较大的影响。例如Tinker的热修复方案,大概会让启动速度增加6%-10%左右(粗略统计)。因此我们难免需要作出一些取舍,根据实际业务去做权衡利弊。

总结

本文主要讲述了APP启动耗时的定义、检测以及分析、常规解决方案等内容,通过学习我们了解了启动优化以及耗时检测的常见方法和套路。实际上,有条件的话,我们也可以对上报统计系统增加一个启动耗时的功能,针对线上用户进行监测及收集并有针对性地去优化启动速度,同时也可以根据实际业务情况对低中高端机型做差异性优化。总的来说,我们做优化工作需要考虑到的方面比较多,有时需要从细节去突破。另外,做优化不能单单看KPI指标,需要从本质上出发,为用户真实的使用体验角度去考虑问题。

参考资料:

《Android窗口管理服务WindowManagerService显示Activity组件的启动窗口(Starting Window)的过程分析》 - 罗升阳

《 Optimizing Android bytecode with ReDex 》 - Facebook

《Redex初探与Interdex:Andorid冷启动优化》 - 【腾讯Bugly干货分享】

更多相关文章

  1. 在Ubuntu上下载、编译、运行Android系统
  2. Android中应用调用系统权限
  3. android系统构架
  4. Android权限系统
  5. 深入浅出 - Android系统移植与平台开发(四)- 搭建Linux编译环境

随机推荐

  1. Android笔试总结
  2. Analyzing Android(安卓)Malware
  3. Android(安卓)Gradle manifestPlaceholde
  4. android top 10 library1
  5. 如何在activity中控制屏幕点亮与关闭
  6. Android中http请求(C# MVC接收)
  7. Android如何使用WebView访问https的网站
  8. android零碎要点---android开发者的福音,5
  9. Android禁止横竖屏和解决切换屏幕时重启A
  10. android 每天定时提醒功能实现