一、App启动分类
1.冷启动 Cold start
在启动应用前,系统还没有App的任何进程。比如设备开机后应用的第一次启动,系统杀掉应用进程 (如:系统内存吃紧引发的 kill 和 用户主动产生的 kill) 后 的再次启动等。那么自然这种方式下,应用的启动时间最长。
2.热启动 Warm start
当应用中的 Activities 被销毁,但在内存中常驻时,应用的启动方式就会变为暖启动。相比冷启动,暖启动过程减少了对象初始化、UI的布局和渲染。启动时间更短。但启动时,系统依然会展示一个空白背景,直到第一个 Activity 的内容呈现为止。
3.温启动 Lukewarm start
用户退出您的应用,但随后重新启动。该过程可能已继续运行,但应用程序必须通过调用onCreate()从头开始重新创建活动。系统从内存中驱逐您的应用程序,然后用户重新启动它。进程和Activity需要重新启动,但任务可以从保存的实例状态包传递到onCreate()中。
启动速度优化主要是针对冷启动方式。下面看下冷启动的时候会做哪些工作。
#二、冷启动
应用发生冷启动时,系统有三件任务要做:
- 加载启动App;
- App启动之后立即展示出一个空白的Window;
- 创建App的进程;
创建App进程后,会马上执行以下任务:
- 初始化应用中的对象 (比如 Application 中的工作);
- 启动主线程 (UI 线程) ;
- 创建第一个 Activity;
- 加载内容视图 (Inflating) ;
- 计算视图在屏幕上的位置排版 (Laying out);
- 进行第一次绘制 (draw)。
只有当应用完成第一次绘制,系统当前展示的空白背景才会消失,才会被 Activity 的内容视图替换掉。也就是这个时候,用户才能和我们的应用开始交互。下图展示了冷启动过程系统和应用的一个工作时间流:
三、优化思路
作为普通应用,App进程的创建等环节我们是无法主动控制的。开发人员唯一能做的就是**在Application 和 第一个 Activity 中,减少 onCreate() 方法的工作量,从而缩短冷启动的时间。**像应用中嵌入的一些第三方 SDK,都建议在 Application 中做一些初始化工作,开发人员不妨采取懒加载的形式移除这部分代码,而在真正需要用到第三方 SDK 时再进行初始化。
Google也给出了启动加速的方向:
1、利用提前展示出来的Window,快速展示出来一个界面,给用户快速反馈的体验;
2、 避免在启动时做密集沉重的初始化(Heavy app initialization);
3、 定位问题:避免I/O操作、反序列化、网络操作、布局嵌套等
四、正确测量评估启动性能的方法
1.display time
从Android KitKat版本开始,Logcat中会输出从程序启动到某个Activity显示到画面上所花费的时间。这个方法比较适合测量程序的启动时间。
2.reportFullyDrawn
我们通常来说会使用异步懒加载的方式来提升程序画面的显示速度,这通常会导致的一个问题是,程序画面已经显示,可是内容却还在加载中。为了衡量这些异步加载资源所耗费的时间,我们可以在异步加载完毕之后调用activity.reportFullyDrawn()方法来告诉系统此时的状态,以便获取整个加载的耗时。
3.Traceview
告诉我们每一个方法执行了多长时间.这个工具可以通过 Android Device Monitor 或者从代码中启动。
3.1 Android Device Monitor启动
启动应用,点击 Start Method Tracing,应用启动后再次点击,会自动打开刚才操作所记录下的.trace文件,建议使用DDMS来查看,功能更加方便全面。
3.2 代码启动
①在onCreate开始和结尾打上trace
Debug.startMethodTracing("GithubApp");
...
Debug.stopMethodTracing();
注意加读写权限
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
运行程序, 会在sdcard上生成一个"GithubApp.trace"的文件.
②通过adb pull将文件导出到本地
adb pull /sdcard/GithubApp.trace ~/temp
③打开DDMS分析trace文件
④分析trace文件
- 在下方的方法区点击"Real Time/Call", 按照方法每次调用耗时降序排.
- 耗时超过500ms都是值得注意的.
- 看左边的方法名, 可以看到耗时大户就是我们用的几大平台的初始化方法, 特别是Bugly, 还加载native的lib, 用ZipFile操作-等.
- 点击每个方法, 可以看到其父方法(调用它的)和它的所有子方法(它调用的).
- 点击方法时, 上方的该方法执行时间轴会闪动, 可以看该方法的执行线程及相对时长.
4.Systrace
在onCreate方法里面添加trace.beginSection()与trace.endSection()方法来声明需要跟踪的起止位置,系统会帮忙统计中间经历过的函数调用耗时,并输出报表。
5.adb命令计算 App 的启动时间
adb shell am start -W packageName/packageName.activity
例如:
adb shell am start -W com.media.painter/com.media.painter.PainterMainActivity
Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.media.painter/.PainterMainActivity }
Status: ok
Activity: com.media.painter/.PainterMainActivity
ThisTime: 355
TotalTime: 355
WaitTime: 365
Complete
(注意 Android 5.0 之前的手机是没有 WaitTime 这个值的)
- WaitTime 就是总的耗时,包括前一个应用 Activity pause 的时间和新应用启动的时间;
- ThisTime 表示一连串启动 Activity 的最后一个 Activity 的启动耗时;
- TotalTime 表示新应用启动的耗时,包括新进程的启动和 Activity 的启动,但不包括前一个应用 Activity pause 的耗时。
- 开发者一般只要关心 TotalTime 即可,这个时间才是自己应用真正启动的耗时。
五、优化方案
1.主题切换
通过主题设置,不显示启动时的白屏背景。有以下几种方案:
1.1 直接不显示白屏,直到程序初始化完毕直接显示第一个Activity
<style name="LaunchStyle" parent="Theme.AppCompat.Light.DarkActionBar">
......
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowNoTitle">true</item>
</style>
或者
<style name="LaunchStyle" parent="Theme.AppCompat.Light.DarkActionBar">
......
<item name="android:windowDisablePreview">true</item>
</style>
然后设置给第一个activity
<activity
android:name=".MainActivity"
android:theme="@style/LaunchStyle">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
如果将主题设置到Application,那所有的Activity的主题都会改变
然后在MainActivity中在加载布局之前,重新设置主题
@Override
protected void onCreate(Bundle savedInstanceState) {
setTheme(R.style.AppTheme);
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
这样相当于把白屏变成透明的,隐藏起来了,但是会有一种点击图标后卡住了,过了好几秒才进入App的感觉。这种方案用户体验很差。
1.2把白屏当成闪屏页用
可以通过主题中的 windowBackground 属性,自定义应用启动时的窗口背景。窗口背景显示的内容,Google推荐两种方案,一种是显示Logo,一种利用了 placeholder ,与主界面的 UI 框架保持一致,给用户产生一种应用启动非常快的视觉感受。
①显示Logo使用方式:
drawable/branded_launch_screens:
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"
android:opacity="opaque">
<!--黑色背景颜色-->
<item android:drawable="@android:color/black" />
<!-- 产品logo-->
<item>
<bitmap
android:gravity="center"
android:src="@mipmap/empty_image01" />
</item>
<!-- 右上角的图标元素 -->
<item>
<bitmap
android:gravity="top|right"
android:src="@mipmap/github" />
</item>
<!--最下面的文字-->
<item android:bottom="50dp">
<bitmap
android:gravity="bottom"
android:src="@mipmap/ic_launcher" />
</item>
</layer-list>
android:opacity=”opaque”参数是为了防止在启动的时候出现背景的闪烁。
定义style:
<style name="AppTheme.Launcher">
<item name="android:windowBackground">@drawable/branded_launch_screens</item>
</style>
或者直接使用一张图片
<style name="AppTheme.Launcher">
<item name="android:windowFullscreen">true</item>
<item name="android:windowBackground">@mipmap/app_welcome</item>
</style>
然后将这个主题设置给启动的 Activity。
②使用placeholder:
模拟了一个高度为25dp的状态栏和一个高度为56dp的标题栏。
drawable/placeholder_ui
<?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/colorPrimaryDark" />
<!--假装这里是个toolbar-->
<item
android:drawable="@color/colorPrimary"
android:top="25dp" />
<!--状态栏25+toolbar56=距离top81-->
<item
android:drawable="@android:color/white"
android:top="81dp" />
</layer-list>
定义style:
<style name="AppTheme.Launcher">
<item name="android:windowBackground">@drawable/placeholder_ui</item>
</style>
然后将这个主题设置给启动的 Activity。
③还可以适度结合 Activity 内容视图使用动画过渡效果。
2.避免Application的onCreate进行太多的工作
在Application初始化的地方做太多繁重的事情是可能导致严重启动性能问题的元凶之一。Application里面的初始化操作不结束,其他任意的程序操作都无法进行。Application的onCreate中会做大量第三方组件的初始化工作,其实很多组件是需要做区别对待的,有些可以做延迟加载,有些可以放到其他的地方做初始化操作,特别需要留意包含Disk IO操作,网络访问等严重耗时的任务,他们会严重阻塞程序的启动。
注意点:
- 项目是多进程架构,只在主进程执行Application的onCreate();
- 流程梳理,延后执行;
- 异步加载、延时加载、懒加载
示例如下,Application以及首屏Activity中我们主要做了:
如何判断第三方的库是不是能放在子线程里面:
需要初始化的第三方一般分为两种, 一种是第三方平台的SDK(推送, 分享, 反馈, 统计等) 这个可以通过看其SDK文档, 结合业务需求考虑. 例如分享, 反馈一般不是必须要应用一开启就能用的, 这类业务一般层级比较深, 有足够的理由让它们在后台异步初始化. 另外一种第三方是第三方的库, 一般来说, 建议阅读其源码, 了解其实现原理, 再决定是否放在后台初始化.
项目修改:
将友盟、Bugly、听云、GrowingIO、BlockCanary等组件放在WorkThread中初始化;
延迟地图定位、ImageLoader、自有统计等组件的初始化:地图及自有统计延迟4秒,此时应用已经打开;而ImageLoader因为调用关系不能异步以及过久延迟,初始化从Application延迟到SplashActivity;而EventBus因为再Activity中使用所以必须在Application中初始化。
3.避免首个Activity的onCreate进行太多的工作
使用延迟加载。确保在Activity的页面显示出来之后再进行加载数据,避免过早或过晚的加载导致页面空白时间过长。可采用以下 代码实现延迟加载。在Activity的onCreate方法中:
getWindow().getDecorView().post(new Runnable() {
@Override
public void run() {
myHandler.post(mLoadingRunnable);
}
});
4.MultiDex初次启动优化
4.1问题
随着代码数量的膨胀,工程本身的代码加上引用的第三方库的代码中方法数量会超过65536的限制。是由于DEX文件格式限制,一个DEX文件中method个数采用使用原生类型short来索引文件中的方法,也就是4个字节共计最多表达65536个method,field/class的个数也均有此限制。 Google为构建超过65K方法数的应用提供官方支持的方案:MultiDex。
但是在Dalvik下MultiDex有个问题:5.0以下某些低端机会出现ANR或者长时间卡顿不进入引导页,而罪魁祸首是MultiDex.install(Context context)的dexopt过程耗时过长。因此需要在初次启动时做特别处理。
而5.0以上会使用ART,在ART下MultiDex是不存在这个问题的,这主要是因为ART下采用Ahead-of-time (AOT) compilation技术,系统在APK的安装过程中会使用自带的dex2oat工具对APK中可用的DEX文件进行编译并生成一个可在本地机器上运行的文件,这样能提高应用的启动速度,只是在安装过程中进行了处理这样会影响应用的安装速度。
4.2解决思路
1、在Application.attachBaseContext(Context base)中,判断是否初次启动,以及系统版本是否小于5.0,如果是,跳到2;否则,直接执行MultiDex.install(Context context)。
2、开启一个新进程,在这个进程中执行MultiDex.install(Context context)。执行完毕,唤醒主进程,自身结束。主进程在开启新进程后,自身是挂起的,直到被唤醒。
3、唤醒的主进程继续执行初始化操作。
最后
如果你看到了这里,觉得文章写得不错就点个赞呗?转发分享和关注一下我,以后还会更新技术干货,谢谢您的支持!如果你觉得那里值得改进的,请给我留言。一定会认真查询,修正不足。谢谢。
有一句老话说的好:
“比你优秀的对手在学习,你的仇人在磨刀,你的闺蜜在减肥,隔壁老王在练腰,我们必须不断学习,否则我们将被学习者超越。”
当然一个人学习是枯燥的,还需要一个良好的学习氛围,因此我组建了一个学习交流探讨的社群,欢迎大家一起来交流探讨共同进步。还有一些收集整理的资料,感兴趣的可以来一起学习,共同进步!
针对Android开发的同行,这边给大家整理了一些资料,其中分享内容包括但不限于
【高级UI、性能优化、移动架构师、NDK、混合式开发(ReactNative+Weex)微信小程序、Flutter等全方面的Android进阶实践技术】
希望能帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习,也是可以分享给身边好友一起学习的!
转发+点赞,加入Android开发交流群(820198451)获取小编为大家收录的进阶资料和面试题库
转发+点赞,加入Android开发交流群(820198451)获取小编为大家收录的进阶资料和面试题库
Android架构师之路很漫长,一起共勉吧!