性能优化专题一--启动优化(黑白屏、traceView耗时函数定位)
App启动流程
1、系统启动简易流程
开机–>[引导程序BootLoader->负责拉起操作系统]–>linux,init.rc–>init进程pid=1–>zygote[孵化器,创建虚拟机JVM]–>启动进程间通讯的工具SystemServer[打开binder线程池,SystemServiceManeger]–>开启各种服务ActivitymanegerService,CameraService,WindowManegaer–>启动Launch
- 打开电源,引导芯片加载BootLoader
- BootLoader拉起操作系统
- Linux找到init.rc开始初始化进程,init进程pid=1
- zygote[孵化器]创建虚拟机,启动SystemServer
- 启动Binder线程池,开启SystemServiceManager,开启各种服务ActivitymanegerService,CameraService,WindowManegaer
- 开启各种服务ActivitymanegerService启动Luncher
application开启
startActivity把信息告诉系统,然后系统打开孵化器,孵化器把信息传入创建ActivityThread main(),再然后传入Application main();onCreate();–>Activity onCreate();
我们所能改动的是Application main()–>Activity onCreate();这中间的过程
冷启动流程
冷启动是无法避免的,因为一个应用要想展示出来,系统需要为它做很多事,而这些总是需要一定的时间,开发者能做的就是将这段等待时间尽可能优雅的给用户缩短或者隐藏起来
那冷启动过程到底发生了什么导致了这些并发症呢,说到这里就要了解下应用的启动流程了:
关于应用启动及进程创建可以参考这几篇文章
从源码解析-Android中Activity启动流程包含AIDL使用案例和APP启动闪屏的缘由
解析Android中Zygote进程是如何fork一个APP进程
从Activity加载源码深入理解ActivityThrad的工作逻辑
当用户在Launcher内点击应用图标,Launcher将这个动作发送给AMS,AMS判断这个应用所在进程不存在,那就需要创建一个新的进程;然后AMS发送消息给Zygote进程,让它创建进程;Zygote进程经过fork之后,分配好应用需要的内存,就通过反射加载ActivityThread类的main方法,接下来进入应用进程了;ActivityThread的main方法会创建应用的Application,加载主题,创建应用第一个Activity,加载布局,进行绘制显示,这样你就看到第一个Activity了,应用就这样启动起来了
问题一:白屏、黑屏原因
黑白屏生成原因–>App打开过程中Application到Activity之间有一段加载的空白时间。而这段时间之内如果设置了windowsBackground颜色白色的,就为白屏,没设置的则为黑屏。
进程创建,类加载及Activity启动需要时间,如果啥都不做一直等到Activity渲染完显示,那这个空白期就很尴尬了,用户点了之后,啥都没反应,还停留在手机屏幕,出现了假死现象,用户可能会再次点击,非常影响用户体验。
于是Android就推出了一个预览窗口,这个预览窗口是AMS在启动应用的过程中判断如果我们要切换到一个新的栈,或者下一个Activity的进程不存在,那我们就需要告诉WindowManager展示一个预览窗口;给用户反馈,表明你点击有效;但是这个窗口展示什么呢?
这时候就会将你在manifest文件设置的主题中android:windowBackground 属性来设置到窗口中,你设置的是颜色,那就显示颜色;设置图片背景,那就显示图片;如果没有设置,那就默认是一个白屏或者黑屏
看到这里发现好像严格来说这不是问题,反而是个优化的结果;但是对于追求完美的程序猿来说,这种有损应用体验的情况怎么能存在呢,一定要解决
<!--白屏-->
<style name="AppTheme" parent="Base.Theme.AppCompat.Light.DarkActionBar">
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
//这一段是导致白屏的代码,点击他的parent链接到他的上一层,一直点
<style name="Platform.AppCompat.Light" parent="android:Theme.Holo.Light">
...
<item name="android:windowBackground">@color/background_material_light</item>
...
</style>
//通过这段代码可以看到默认窗口背景颜色是白色,所以呈现为白屏
<!--黑屏-->
<style name="AppTheme" parent="android:Theme">
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
<style name="Theme">
...
<item name="windowBackground">@drawable/screen_background_selector_dark</item>
...
</style>
//溯源代码可以看到窗口背景颜色为黑色,所以呈现为黑屏
三种解决方案:
方案一:开启显示背景颜色或者是设置一张splash图片:
<!--窗口背景颜色,此处不允许直接赋值如 #FFFFFF,必须在color中定义使用-->
<item name="android:windowBackground">@color/colorAccent</item>
<item name="windowBackground">@drawable/splashBg</item>
方案二:设置透明,所以没有白屏或黑屏
<!--设置背景透明-->
<item name="android:windowIsTranslucent">true</item>
方案三:禁用窗口的预览动画
<item name="android:windowBackground">@null</item>
<item name="android:windowDisablePreview">true</item>
方案二和三的缺点是:点击后界面不立即开启应用,会在桌面停留一会,完成加载后再开启
问题二:利用traceView,定位耗时函数:
在我们想要分析的函数中头部和尾部添加如下代码,traceView就会帮助我们记录下该代码块中所有函数的耗时:
File file = new File(Environment.getExternalStorageDirectory(), "app1.trace");
Log.i(TAG, "onCreate: " + file.getAbsolutePath());
//把分析结果存在一个文件
Debug.startMethodTracing(file.getAbsolutePath());
... ...
Debug.stopMethodTracing();
例如想分析onCreate()函数中的耗时函数:
@Override
public void onCreate() {
super.onCreate();
File file = new File(Environment.getExternalStorageDirectory(), "app1.trace");
Log.i(TAG, "onCreate: " + file.getAbsolutePath());
//把分析结果存在一个文件
Debug.startMethodTracing(file.getAbsolutePath());
//对全局属性赋值
mContext = getApplicationContext();
mMainThread = Thread.currentThread();
mMainThreadId = android.os.Process.myTid();
mMainLooper = getMainLooper();
mHandler = new Handler();
initNim();
initImagePicker();
initOkHttp();
Debug.stopMethodTracing();
}
将生成的trace文件pull到电脑后再拖进Android studio中即可:
adb pull /storage/emulated/0/app1.trace
如图所示,我们可以清楚的看到在onCreate中每个初始化函数的耗时时间,我么只需要依次分析下耗时较多的函数里面是否是必须要在onCreate中完成的,如果是非必须的,我们可以开启线程或者放到后面一些执行,以免影响应用启动耗时:
可以点击面板上的TopDown按钮,从大到小排序耗时:
可以非常清楚看到onCreate初始化总耗时56ms,其中的intiNIM函数就占了49ms,占比87%,因此我们后面集中精力优化下这个初始化函数,把不必要的操作移动到后面进行,以免它的初始化占用了过多的启动时间。
通过traceView工具找到耗时函数后,我们根据需要可以把这些耗时操作放到子线程中去:
@Override
public void onCreate() {
super.onCreate();
File file = new File(Environment.getExternalStorageDirectory(), "app1.trace");
Log.i(TAG, "onCreate: " + file.getAbsolutePath());
//把分析结果存在一个文件
Debug.startMethodTracing(file.getAbsolutePath());
//对全局属性赋值
mContext = getApplicationContext();
mMainThread = Thread.currentThread();
mMainThreadId = android.os.Process.myTid();
mMainLooper = getMainLooper();
mHandler = new Handler();
//因为LQRUIKit中已经对ImageLoader进行过初始化了
// initImageLoader(getApplicationContext());
new Thread(){
@Override
public void run() {
//如果要用线程来节约了这些初始化的时间
//1.里面的API不能去创建handler
//2.不能有UI操作
//3.对异步要求不高
initNim();
initImagePicker();
initOkHttp();//可以懒加载
}
}.start();
NIMClient.init(this, loginInfo(), options());
Debug.stopMethodTracing();
}
完成后,再次抓取trace文件打开后会发现,从原来的50ms降低到了14ms的初始耗时,节约了70%的耗时操作。
快捷查看APP启动时间:
方式一:在log中使用Displayed关键字即可
如下:依次打开了微信、头条、抖音、京东、淘宝,京东冷启动750ms,淘宝冷启动竟然到了3.2S
方式二:使用 adb shell am start -W 包名/包名.启动类名
adb shell am start -W com.lqr.wechat/com.lqr.wechat.activity.SplashActivity
其中,ThisTime表示启动界面前最后一个activity;TotalTime表示启动界面前所有activity的耗时;如果启动界面前仅启动一个activity那么thistime和totaltime是一样的,如果有多个activity启动total肯定比this要多;waitTime则是包含了activity启动和系统的交互时间,是最长的。
上面三个时间的具体计算类是Am.java: