声明:本篇文章已授权微信公众号 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. 进行首次绘制。

大致流程如下图所示:

从系统层面来看,一个 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。因为系统的输出信息是在系统进程服务,而不是应用程序本身输出的启动日志,具体可参考下图:

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: ok
Activity: com.duowan.mobile/com.yy.mobile.ui.splash.SplashActivity
ThisTime: 1794
TotalTime: 1794
WaitTime: 1831
Complete

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 主题优化

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

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

  • 系统版本:Nougat 7.1
  • 源码根目录: frameworks/base/services/core/java/com/android/server/policy/PhoneWindowManager.java
public class PhoneWindowManager implements WindowManagerPolicy {//省略代码···/** {@inheritDoc} */@Overridepublic 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。

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

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

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

透明主题参考代码:

<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<item name="android:windowFullscreen">true</item>
<item name="android:windowIsTranslucent">true</item>
</style>

图片背景主题参考代码:

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

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

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

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干货分享】

Android 启动优化(一)相关推荐

  1. android布局优化方案,Android启动优化-布局优化

    Android启动优化-布局优化 安卓应用开发发展到今天,已经成为一个非常成熟的技术方向,从目前的情况看,安卓开发还是一个热火朝天的发展,但高级人才却相对较少,如今移动互联网的开发者也逐渐开始注重插入 ...

  2. Android 启动优化(五)- AnchorTask 1.0.0 版本正式发布了

    今天,更新一下 Android 启动优化有向无环图系列的最后一篇文章.最近一段时间,暂时不会更新这方面的文章了.系列文章汇总如下: Android 启动优化(一) - 有向无环图 Android 启动 ...

  3. 深入探索Android 启动优化(七) - JetPack App Startup 使用及源码浅析

    本文首发我的微信公众号:徐公,想成为一名优秀的 Android 开发者,需要一份完备的 知识体系,在这里,让我们一起成长,变得更好~. 前言 前一阵子,写了几篇 Android 启动优化的文章,主要是 ...

  4. 启动优化·基础论·浅析 Android 启动优化

    " [小木箱成长营]启动优化系列文章(排期中): 启动优化 · 工具论 · 启动优化常见的六种工具 启动优化 · 方法论 · 这样做启动优化时长降低 70% 启动优化 · 实战论 · 手把手 ...

  5. Android 启动优化(一) - 有向无环图

    前言 说到 Android 启动优化,大家第一时间可能会想到异步加载.将耗时任务放到子线程加载,等到所有加载任务加载完成之后,再进入首页. 多线程异步加载方案确实是 ok 的.但如果遇到前后依赖的关系 ...

  6. Android启动优化实战(有效降低APP启动时间)

    1.概述 手机点击一个APP,用户希望应用能够及时响应并快速加载.启动时间过长的应用不能满足这个期望,并且可能会令用户失望.这种糟糕的体验可能会导致用户在 Play 商店针对您的应用给出很低的评分,甚 ...

  7. android 启动优化方案,Android 项目优化(五):应用启动优化

    介绍了前面的优化的方案后,这里我们在针对应用的启动优化做一下讲解和说明. 一.App启动概述 一个应用App的启动速度能够影响用户的首次体验,启动速度较慢(感官上)的应用可能导致用户再次开启App的意 ...

  8. Android启动优化方案调研

    /   今日科技快讯   / 7月21日,国家互联网信息办公室依据<网络安全法><数据安全法><个人信息保护法><行政处罚法>等法律法规,对滴滴全球股份 ...

  9. Android 启动优化总结

    前言 性能优化包括很多方面,比如:启动优化.布局优化.内存优化.卡顿优化.网络优化.数据库优化.内存泄漏优化.包体积优化等等. 冷启动.温启动.热启动 首先了解下启动的这三个概念,也是面试常被问到的: ...

最新文章

  1. 决策树算法(五)——处理一些特殊的分类
  2. python TypeError: not all arguments converted during string formatting 解决
  3. Swift 4 无限滚动轮播图(UICollectionView实现)
  4. linux误删视频恢复吗,linux 误删文件恢复
  5. 中文乱码解决方案(Qt4.8.3 + Qt Creator)
  6. 线程池的原理和连接池的原理
  7. Ajax 生成流文件下载 以及复选框的实现
  8. [轉]C# 中的委托和事件
  9. linux下查看内存频率,内核函数,cpu频率
  10. java将图书信息写入原有文件里_Java保存图书信息
  11. vue实现简单的日历
  12. 二叉搜索树的模拟及其实现(c++)
  13. oracle取差集效率如何,Oracle Minus 取差集
  14. Linux进程虚拟内存大 性能,Linux进程分析(一) 虚拟内存和物理内存
  15. pythoneducoder苹果梨子煮水的功效_梨子煮水真的有润肺止咳化痰的功效吗?
  16. QQ留言代码,网页QQ留言
  17. 前端上传图片添加水印
  18. 【unix】unix环境高级编程
  19. 脚本:通过ssh、scp和expect批量复制文件到其它设备,已解决传输文件不完整的问题
  20. picgo免费搭建个人图床

热门文章

  1. Obsidian 国内插件安装指北
  2. 30种寿司的做法--可恶
  3. GitHub 近两万 Star,可一键生成前后端代码,这个开源项目有点强
  4. 你自嗨型的年度计划能嗨到年底吗?
  5. Redis安全漏洞影响及加固方法
  6. 开启win7无线网卡服务器,Win7怎么设置开启或者禁用无线网卡
  7. 163邮箱移动办公软件平台,移动办公云邮箱哪个好?
  8. asp.net Excel导入和导出
  9. postman post请求服务器没响应,Postman POST请求
  10. Jmeter之HTTP请求详解