微医项目经过N个版本的迭代,到现在已经很庞大了。这自然就产生了各种问题,其中就包括冷启动慢,白屏时间长问题。本篇文章是对自己优化启动时间的一个总结。
优化前的项目现状
这里以微医 3.4.0 Release包(未优化前)为例,分别对欢迎页(app
第一个页面)及主页在不同手机做连续5次冷启动数据统计。
机型 | 白屏 | 欢迎页平均时间 | 主页平均时间 |
---|---|---|---|
华为CAM-TL00H 6.0 | 有 | 5.039s(无广告会多停留1.5s) | 2.1198s |
oppoA59S 5.0 | 有 | 4.237s(同上) | 1.0678s |
以上的白屏时间为欢迎页启动的时间。
1 | 连续5次冷启动(启动后再杀掉进程)时间详细统计 |
现状总结:通过以上数据可以发现,首页冷启动时间过慢,而且每次都会伴有长时间的白屏,用户体验很差,亟需我们优化。
优化前的理论分析
app
的启动方式安卓应用的启动方式分为三种:
冷启动(
cold start
)在应用启动前,系统没有该应用的任何进程信息(包括Activity,Service等),用户点击图标的启动称之为冷启动。比如说设备开机后应用的第一次启动,杀掉进程后(包括系统主动回收和用户手动kill)再次的启动。
在冷启动过程中,系统会做以下事件。
开始加载并启动应用
应用启动后,显示一个空白的启动窗口(白屏来由)
创建应用的进程信息。创建进程后,应用要做以下几个事情
初始化Application。每创建一个进程,就会初始化一次。而创建一个进程是需要消耗系统很多资源的,另外如果在Application中的初始化操作不加以区分进程的化,会重复多次的。
启动
UI
线程创建第一个Activity
加载内容视图,计算视图在屏幕上位置
绘制视图。只有当完成第一次绘制后,系统当前展示的空白背景才会消失,被加载好的内容替代
暖启动(
warm start
)当Activity被销毁,但在内存常驻时,这种启动方式成为暖启动。相比冷启动,暖启动少了对象初始化,布局加载等过程,启动时间更短。但启动时,以然会有一个空白背景,直到第一个页面内容呈现为止。
热启动(
luckwarm start
)热启动比暖启动做的事情更少了,启动时间会更短。这种场景常见为:返回键退出
app
又快速重启,或者只是按Home回到桌面,或者app
本身做了进程重启的机制。
1
2
3
4
5研究app的启动模式后,发现主要优化的就是冷启动。只要冷启动时间优化了,热启动其实也跟着优化了。 从程序的角度分析,冷启动的时间 = initProcess + Applicaton初始化时间 + 欢迎页的初始化时间
从用户的角度分析,点击启动图标,应该立即看到app页面,而不是看到长时间的白屏,并且也应该尽早地看到app的主页。
所以冷启动的时间在上面的基础上再加上欢迎页的停留时间+主页的初始化时间。
另外页面的初始化时间不只是看onCreate()、onStart()、onResume()这几个生命周期方法的时间,
因为这几个方法调用完毕后, View树还没有完全构建完毕。启动时间的统计手段
Display Time
API
19后,Android在系统Log中增加了Display的Log信息。这个时间实际上是Activity启动,到Layout全部显示的过程,但是要注意,这里并不包括数据的加载。ADB
命令1
adb shell am start -W [packageName]/[packageName.AppstartActivity]
执行后会得到三个时间:
ThisTime
:一般和TotalTime
时间一样,如果在应用启动时开了一个过度的全透明的页面(Activity)预先处理一些事,再显示出主页面(Activity),这样将比TotalTime
小。TotalTime
:应用的启动时间,包括创建进程+Application初始化+Activity初始化到界面显示。WaitTime
:一般比TotalTime
大些,包括系统影响的耗时。Hugo
执行时间打印神器,集成至项目后,只需在想要测试的地方加上注解
@DebugLog
即可。可以是Class级别,也可以是method级别。Github传送门
本文上面的统计页面的初始化时间采用的是系统日志Displayed, 在优化过程中,统计函数的方法是自己手打Log。
具体的优化点
基于上面的分析,从以下几点对app
进行优化
白屏优化
从上面的分析得知,
app
启动后系统创建需要的进程,在完成页面First Draw前,系统会立即显示一个background window。这个就是白屏的来源,如果白屏时间过长,非常影响用户得体验。 知道这个原因,解决这个问题就比较简单了。无非就是将Theme里的windowBackground
设置成我们想要让用户看到的画面就可以了。通常有以下两种办法解决:
将主题的背景图设置成透明,这样当用户点击
app
启动图标后,并不会立即进入app
,而是在桌面停留一会儿,但其实app
这个时候已经启动了,只是因为设置成了透明,用户看不到而已,强行帅锅给手机厂商。比如微信就是这样做的。这并不是最佳的解决方案。将主题的背景图设置成
app
的Logo或者广告页。这样用户在点击启动图标后,会立即看到预设的Logo。而用户误以为这是第一个页面,从而让用户体验大大地提升。这个做法也是目前主流app
所采用的。我这里优化的,也是采用此法。在res/drawable下新建一个layer-list,放置我们
app
的Logo1
2
3
4
5
6
7
8
9
10
11
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"
android:opacity="opaque">
//这里android:opacity="opaque"参数是为了防止在启动的时候出现背景的闪烁。
<item android:drawable="@color/white"/>
<item>
<bitmap
android:gravity="center"
android:src="@drawable/gh_cm_wy_logo"/>
</item>
</layer-list>新建一个启动主题,将背景图替换为Logo
1
2
3
4
5
6
7
8<style name="Theme.launch" parent="Theme.Guahao.NoActionBar">
//设置Theme不透明并且默认渲染的背景图是我们必看影的Logo
<item name="android:windowIsTranslucent">false</item>
/替换Logo
<item name="android:windowBackground">@drawable/gh_cm_launch_bg</item>
//全屏展示,免得顶部状态栏显现颜色不一致过于脱节和突兀
<item name="android:windowFullscreen">true</item>
</style>将启动主题设置给欢迎页,在欢迎页创建后,再还原之前的主题
1
2
3
4
5
6
7
8
9<activity
android:name="欢迎页"
//设置启动主题
android:theme="@style/Theme.launch">
<intent-filter>
<category android:name="android.intent.category.LAUNCHER" />
<action android:name="android.intent.action.MAIN" />
</intent-filter>
</activity>欢迎页创建后还原主题
1
2
3
4
5
6
7
public void onCreate(final Bundle savedInstanceState) {
//还原之前的主题 必须在super之前
setTheme(R.style.Theme_welcome);
SystemBarUtil.enableFullscreen(this);
super.onCreate(savedInstanceState);
}由于我们
app
debug模式默认的启动页是工厂页,做法是类似的,代码就不贴了。
Application优化
经过上面的分析,对Application的优化分为以下几个方面。
进程优化
系统创建进程需要消耗更多系统资源,而每创建一个进程就意味着Application又会初始化一遍。
翻看我们
app
Application初始化的代码,发现我们目前有5个进程。(这里可能涉及到公司隐私,就不贴了)
这里的优化点有两个:
尽可能减少欢迎页前创建其他进程。
(这里可能涉及到公司隐私,就不贴了)
Application的初始化工作区分进程
翻看代码,发现我们这里对不同进程的初始化工作已经区分,故不用优化。
统计Application启动时各类方法的耗时。区分初始化操作的优先级,对不必要的耗时操作,可以延后加载,按需加载。不能延时的耗时操作,可以异步加载,并且降低线程的优先级,避免与主线程抢资源。
1
2
3
4//在这些异步操作中,都会调用降低线程优先级的方法
private void lowerThreadPriority(){
if (Looper.getMainLooper().getThread() != Thread.currentThread()){ android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_BACKGROUND);
}统计完各类具体初始化操作的耗时后,同时要对所有的初始化操作分析优先级。这里可能涉及到公司隐私,具体就不贴了。
分析后,发现项目中网络监控、推送、百度统计、Bugly等初始化完全可以延后至欢迎页或者主页。
布局优化
布局越是精简,系统在绘制的时候时间就会越短,从而降低启动时间。
下图是在CSDN看到某文章后,作者对常用布局的性能统计。
因此,在布局优化这块,我总结了以下几个原则。
- 减少多余的层级嵌套
- 同等层级下尽量使用帧布局,线性布局
- 减少不必要的背景绘制
- 空布局或者在某些条件才展示的布局使用
ViewStub
延迟初始化 - 如果以后引入了约束布局,可以用其优化
分析我们项目,影响首页启动的布局有四个。欢迎页,主页Activity,以及主页嵌套的两个Fragment。
欢迎页
在查看了欢迎页的布局后,发现存在2个缺点:
- 外层使用相对布局,完全可以用帧布局替换
- 展示广告的
Imageview
和展示默认图片的ImageView
完全可以共用一个。所以删除多余的布局,减少了一层嵌套
主页
由于主页是Activity内嵌Fragment,Fragment嵌套Fragment。所以有三个布局可以优化。
打开手机的
GPU过渡绘制
,发现主页是一片红。查看布局,发现了以下问题:- 主页外层的Activity的布局里面存在很多无用View。因为主页Activity只负责提供一个嵌套Fragment的容器就行了。查看代码逻辑后,将布局只改成一个帧布局,其余全部删除
- 内嵌的
HomeFragment
嵌套微医Tab Fragment的布局存在许多多余的背景,这是GPU过渡绘制
飘红的原因,全部去除 - 内嵌的
HomeFragment
的布局存在无用的相对布局嵌套相对布局,底部tab也是存在大量多余嵌套。将这些全部优化 - 微医Tab Fragment的布局中也存在无用View和大量多余的布局嵌套。 全部优化
- 主页引导图改为
ViewStub
布局代码过多,这里就不贴了
欢迎页逻辑优化
查看欢迎页代码后,发现有以下几个问题:
- 原先的广告图和默认图分别用不同的
ImageView
- 在不需要广告时,为了让用户看到默认图,特意在页面停留1.5秒再去跳转,很不科学
- 在展示广告倒计时时,采用的是Handler延时策略,存在两个弊端。首先是时间不准确,因为系统消息的调度是需要时间的而且所需时间不定。其次,在页面销毁中也没有移除消息存在内存泄漏的问题。
- 页面
onCreate
方法中没有将数据的初始化延后至View绘制后。如果做数据操作需要耗时很多,将会很大程度影响UI
的绘制,从而影响启动时间 - 欢迎页继承
BaseActivity
,而BaseActivity
有很用冗余的操作,尤其是会初始化数据库,不但消耗时间,而且该操作完全可以延后到主页。 - Sp的提交使用的是commit
- 广告展示时,过于突兀,没有一个过渡效果
解决办法:
广告图和默认图统一
广告的倒计时使用
CountDownTimer
实现,注意在页面销毁时,将timer取消掉。将不影响View初始化逻辑的操作统一延后至View绘制后。
这里我想到了两种解决办法。
MessageQueue.IdleHandler
在looper里面的message暂时处理完了,会回调这个接口。还不明白点我
两次
post Runnable
系统绘制会执行两次
Performtraversal
。
去掉为了让用户看到默认图刻意停留的1.5秒,而由于初始化的延时加载,用户也一定会看到默认图,顺带就解决了这个问题
欢迎页不继承
BaseActivity
将Application中的不必要初始化操作移到延时加载中。如百度统计,
Bugly
,推送。并且对于耗时操作放到异步线程中。封装全局线程池,异步线程都由全局线程池提供,并且异步线程降低线程优先级
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36public class ThreadPoolManager {
//饿汉式 保证速度
private static final ThreadPoolManager mInstance = new ThreadPoolManager();
private int mCorePoolSize;
private int mMaximumPoolSize;
private long mKeepAliveTime;
private TimeUnit mUnit;
private ThreadPoolExecutor mExecutor;
private ThreadPoolManager() {
mCorePoolSize = Runtime.getRuntime().availableProcessors() + 1;
//用不到,但是需要赋值,否则报错
mMaximumPoolSize = mCorePoolSize;
mKeepAliveTime = 1;
mUnit = TimeUnit.MINUTES;
mExecutor = new ThreadPoolExecutor(mCorePoolSize, mMaximumPoolSize, mKeepAliveTime, mUnit,
new LinkedBlockingQueue<Runnable>(128), Executors.defaultThreadFactory());
// keepAliveTime同样作用于核心线程
mExecutor.allowCoreThreadTimeOut(true);
}
public static final ThreadPoolManager getInstance() {
return mInstance;
}
public void execute(Runnable runnable) {
if (runnable == null) return;
mExecutor.execute(runnable);
}
public void remove(Runnable runnable) {
if (runnable == null) return;
mExecutor.remove(runnable);
}
}对提交结果不关心时,尽量
sp
使用apply而不是commit在Glide中添加默认的渐变效果,避免出现突兀的现象
(具体代码,公司隐私原因就不贴了)
- 原先的广告图和默认图分别用不同的
主页逻辑优化
查看主页相关代码,发现存在以下问题
存在很多无用的全局变量
页面初始化逻辑过多,也没有放在View绘制后
很多SP提交操作使用commit
缓存
webview
逻辑已经没用了webview
的loading只在onPageStart
中展示WebView.loadUrl("url")
不会立马就回调onPageStarted
或者onProgressChanged
因为在这一时间段 ,WebView
有可能在初始化内核 , 也有可能在与服务器建立连接。主页的两次退出逻辑用
TimerTask
实现,没有处理内存泄漏问题
解决:
- 对于没有使用到的全局变量删除。可以转为局部变量的转为局部变量
- 对不影响View初始化逻辑的操作延时加载。放到
MessageQueue.IdleHandler
中,耗时操作采用异步线程。同上 sp
用applywebview
缓存操作删除- 提前显示Loading
- 两次退出逻辑更改为计算连续两次点击时间的间隔
(具体代码,公司隐私原因就不贴了)
优化后结果
机型 | 白屏 | 欢迎页平均时间 | 主页平均时间 |
---|---|---|---|
华为CAM-TL00H 6.0 | 无 | 2.5212s(提升40.49%) | 1.5858(提升25.19%) |
oppoA59S 5.0 | 无 | 2.1554s(提升49.12%) | 0.7988s(提升25.19%) |
1 | 连续5次冷启动(启动后再杀掉进程)时间详细统计 |
总结
App
的启动时间还有很多优化的空间,并且这是一个持续性的工作,需要我们在日后的工作持续改进。