一. 前言
当用户在手机桌面上点击一个从未打开过的App时(也就是冷启动),到进入第一个页面显示这段时间,默认情况下您的App会首先出现一个白色/黑色屏,过一段时间才是进入第一个Activity显示其具体布局内容。
对于一个专业的App来说,这种用户体验肯定是不能接受的,不仅会收到用户吐槽,还会造成公司品牌受损。因此提升App的启动速度是增强用户体验的重要指标。
而这个问题,相信很多朋友很早之前就遇到了,而且在网上也能搜到一大堆关于此问题的文章。但是,我看了前几页几乎所有的文章之后,让人失望的是这些文章要么是只提供解决方案,也就是说怎么弄之后问题就解决了,至于为什么,不知道,反正大家都这么干;极少数有分析原因的文章,又让人无法信服,让人怀疑甚至分析是错误的。
为了得到一个让我自己信服的答案,哥不惜拖着疲惫的身躯深夜撸码,一探究竟,接下来就给大家揭晓一下真实情况到底是什么样的。
二. 冷启动白屏的真正原因
首先,要明确一点的是,要想弄清楚冷启动白屏的真正原因,就必须知道冷启动Android经历了哪些代码流程。
我们都知道Launcher本身也是一个特殊的App,其界面就是列出所有的App Icon,当用户在手机桌面上点击一个从未打开过的App时,其实是和正常的startActivity是一样的。
通过阅读源码(具体代码细节太长,这里就省略了),我们知道主分支会经历如下过程:
1Launcher startActivity
2-> AMS查找应用进程是否创建
3-> 未创建,AMS通知从Zygote进程中fork创建出一个新的进程分配给该应用
4-> 进程创建完毕,AMS通过Binder通知应用进程
5-> ActivityThread 调用 performLaunchActivity
6-> Application构造函数及attachBaseContext(),onCreate()
7-> new Activity(), 并为此Activity创建一个new PhoneWindow(this)
8-> Activity onCreate()
9-> Activity setContentView()
10-> new DecorView() & addView(contentRoot, contentParent)
11-> onFinishInflate(): 此步骤只是inflate 所有的DecorView上的布局views,并不可见
12-> Activity onStart()
13-> Activity onResume()
14-> window.addView(mDecorView)
15-> View onAttachedToWindow() onMeasure() onSizeChanged() onLayout() onDraw()
16-> 至此步为止才把DecorView加给Window,应用首页才可见,onWindowFocusChanged(true)
17-> Activity onPause()
18-> View onWindowFocusChanged(false )
19-> Activity onStop()
20-> Activity onDestroy()
21-> View onDetackedFromWindow()
从以上流程,我们至少可以知道如下几个重要的信息:
1)Application onCreate()与Activity,Window的生成时间,以及Activity和View生命周期的严格执行顺序。
2)为什么只有当Activity在执行onResume()生命周期之后用户才能真正看到布局内容。
3)Activity, Window,DecorView及其之上的contentRoot, parentRoot, ToolBar之间的关系。
图片来源于网络
那么问题来了:按道理来讲,在点击了App Icon之后一直到onResume才能看到App的第一屏页面才对,而且根据上面的流程,我们知道PhoneWindow()是在Application onCreate()之后才生成,那么这个白屏究竟从何而来呢?
理论和实践矛盾,必然是理论出了问题,肯定是哪里有漏洞。就在我百思不得其解的情况下,搜索各方资料以及源码,终于发现了线索:
// android.view.WindowManager.LayoutParams
/**
* Window type: special application window that is displayed while the
* application is starting. Not for use by applications themselves;
* this is used by the system to display something until the
* application can show its own windows.
* In multiuser systems shows on all users' windows.
*/
public static final int TYPE_APPLICATION_STARTING = 3;
原来Android系统早已为我们考虑到了一个问题:
就是当冷启动一个APP时,创建进程需要一定时间,再创建完成前,界面不会作出反应。此时会给用户造成一种没有点击到APP的错觉,影响体验。
为了改善用户体验,Starting Window出现了,它会在创建进程这个期间显示,让用户感觉到APP启动了,而Starting Window就是白屏/黑屏的原因,它是黑屏还是白屏,默认取决于第一个启动的Activity的Theme,如果该Activity没设置Theme,默认使用Application的Theme ,这就有了Starting Window的概念,也可以称之为Preview Window。
那Starting Window具体是什么时候出现的呢?
其实在AMS请求创建应用进程之前,就已经通过WMS请求创建了,代码是在Activity状态管理者ActivityStack开始执行显示启动窗口的流程:
//ActivityStack
final void startActivityLocked(ActivityRecord r, boolean newTask, boolean keepCurTransition,
ActivityOptions options) {
if (!isHomeStack() || numActivities() > 0) {//HOME_STACK表示Launcher桌面所在的Stack
// 1.首先当前启动栈不在Launcher的桌面栈里,并且当前系统已经有激活过Activity
// We want to show the starting preview window if we are
// switching to a new task, or the next activity's process is
// not currently running.
boolean doShow = true;
if (newTask) {
// 2.要将该Activity组件放在一个新的任务栈中启动
// Even though this activity is starting fresh, we still need
// to reset it to make sure we apply affinities to move any
// existing activities from other tasks in to it.
if ((r.intent.getFlags() & Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED) != 0) {
resetTaskIfNeededLocked(r, r);
doShow = topRunningNonDelayedActivityLocked(null) == r;
}
} else if (options != null && options.getAnimationType()
== ActivityOptions.ANIM_SCENE_TRANSITION) {
doShow = false;
}
if (r.mLaunchTaskBehind) {
//3. 热启动,不需要启动窗口
// Don't do a starting window for mLaunchTaskBehind. More importantly make sure we
// tell WindowManager that r is visible even though it is at the back of the stack.
mWindowManager.setAppVisibility(r.appToken, true);
ensureActivitiesVisibleLocked(null, 0, !PRESERVE_WINDOWS);
} else if (SHOW_APP_STARTING_PREVIEW && doShow) {
//4. 显示启动窗口
r.showStartingWindow(prev, showStartingIcon);
}
} else {
// 当前启动的是桌面Launcher (开机启动)
// If this is the first activity, don't do any fancy animations,
// because there is nothing for it to animate on top of.
}
}
图片来源于网络
Starting Window就是一个用于在应用程序进程创建并初始化成功前显示的临时窗口,拥有的Window Type是TYPE_APPLICATION_STARTING。
在程序初始化完成前显示这个窗口,以告知用户系统已经知道了他要打开这个应用并做出了响应,当程序初始化完成后显示用户UI并移除这个窗口。
至此为止,冷启动白屏的原因真相大白!
一切都解释通了,从启动开始,系统创建了Starting Window,其主题和LAUNCHER Activity/Application theme一致,如果没有设置就使用系统默认的主题,即系统会将屏幕填充主题默认的背景色,亮系主题填充白色,暗系主题填充黑色,就出现了Activity启动之前的黑/白屏现象。
三. 白屏视觉优化
知道了原因,那么解决办法也很简单。既然Starting Window的主题依赖于LAUNCHER Activity或者Application theme,我们完全可以设置一个SplashActivity闪屏页,并为其设置主题如下:
配置入口Activity的theme属性为上面定义的style:
自定义的splash_bg可以放任意设置的图片,如此一来,Preview Window在冷启动时就能立马看到splash_bg,直到跳转到启动页时同样也显示的是splash_bg,这样就能做到视觉上的无缝过渡,不仅让App看起来有及时的点击反馈,而且更加漂亮。
至此,白屏问题得到完美解决。
最后,这里再提一下网上说的几种错误的说法:
1)因为Application.onCreate()执行比较耗时,才出现了白屏现象。
很明显Application.onCreate()执行耗时只会延长白屏时间,并非白屏出现的根本原因。
2)将主题背景变成透明的,就不会有黑/白屏的现象。
这种方法将使得在LAUNCHER Activity加载出来之前,用户会透过Window看到桌面,就又让用户回到了点击App无及时反应的境地。
3)禁用Starting Window
和2)一样,系统好不容易提供了一个预览窗口,你居然给禁了?肯定不行。
好了,到现在为止,我们知道,闪屏效果会从点击App Icon开始,一直持续到应用的LAUNCHER Activity才会移除,即使我们对白屏做了视觉优化,那也仅仅是在响应效果上做了反馈优化,让用户感觉App有了及时的反应和漂亮的UI,客观上并没有改变启动的等待时长。
因此,我们还必须从本质上要减少执行的时间。从之前的流程分析,我们可以控制的有三大步:Application onCreate()执行的时间,SplashActivity页面的停留时间,用户进入HomeActivity页面后进入onResume()的时间。
一. Application onCreate()速度优化
而随着App开发的功能越来越多,需要加入的第三方库也越来越多,而这些第三方库大多要求在Application的onCreate()里初始化,而我们知道onCreate默认是在UI线程里运行的,如果初始化的东西非常多的话,势必会造成进入第一个Activity的时间推迟。
一边是要用到的功能需要初始化,一边又会延长onCreate()的执行时间,那我们该如何平衡二者之间的关系呢?
我们就需要对SDK的初始化做一下具体的分类了,分为如下四类:
1)一定要在主线程中初始化,且入口Activity可能立即会用到,或者第三方SDK强制要求。
这种没办法,必须放在Application的onCreate()中执行。
2)一定要在主线程中初始化,但是入口Activity不会用到,即可以延迟初始化。
这种就可以在Application的onCreate()中去掉了。取而代之的是,可以用懒加载的方式进行初始化,即只有在第一次使用的时候才去初始化。
3)可以在子线程中初始化,但是入口Activity不会用到,即可以延迟初始化。
可以放在Application的onCreate()中,但是需要放在子线程中执行。由于这种情况可能比较多,所以最好是放入线程池中。
4)可以在子线程中初始化,且入口Activity可能立即会用到。
这种情况下可以依然放在Application的onCreate()的子线程中执行,但是需要注意的是做好线程间的通信,即子线程初始化完毕后,必须能够通知到HomeActivity的使用处。
总结成表格,如下:
二. SplashActivity速度优化
以上的分类和初始化策略,同样适用于SplashActivity,目的都是尽量缩短进入HomeActivity的时间。
另外,对于SplashActivity经常会有广告图片的加载,这种情况下可以采用静默下载,每次加载上次下载下来的图片和数据的缓存的策略,从而避免了首次实时下载耗时的情况。
还有一些特殊的业务场景,比如SplashActivity有大量网络请求(有可能还插入本地DB),此时可以和后台沟通,把多个网络接口整合为一个,如此多次网络I/O和磁盘I/O就被缩减为了一次。
三. HomeActivity加速显示
在提速了之前2个步骤之后,就来到了HomeActivity,此时仍然还不能看到布局,所以我们需要尽量减少onCreate()的时间,尽快进入onResume()使用户能看到页面。
在这一步上,我们可以做的:
1)尽量减少布局的复杂度,详情请参考《android性能优化(一)之UI渲染优化》。
2)当有多个Fragment在不同底Tab或者ViewPager时,可以采用懒加载的方式使用户在使用到某个Fragment时再去加载,避免同时加载这些Fragment。
四. GC抑制提速
以上三个步骤都是基于流程的常规提速手段。最后再提一种三界之外的黑科技,就是支付宝团队采用的GC抑制思想。
所谓GC抑制就是,在App启动的过程中,通过修改内存中的 Dalvik 库文件 libdvm.so 影响 Dalvik 的行为,从而阻止Davlik在此过程中进行垃圾回收的思想。因为在运行过程中,由于Java的GC机制会阻塞 Java 程序的执行,占用 CPU 资源,占用额外内存。
其实,平时你只要稍微留意,就能发现LogCat中有关于GC的log打印出来。主要有以下3种:
GC_EXPLICIT:Dalivk 给开发人员提供的主动触发GC的API,读者可以参看Google Maps的设计来体会这个API的用法。
GC_FOR _ALLOCK:是分配对象失败时触发的GC,这个GC会将应用所有的Java线程暂停运行,直到GC结束。
GC_CONCURRENT:是Java虚拟机根据堆的当前状态触发的GC,这个GC在Dalvik单独GC线程里运行,在部分时间里不影响应用Java线程的运行。
通过简单统计这些GC消耗的时间,我们能够得出GC严重影响应用启动时间的结论:
通过GC抑制的思想,阻止App在启动过程中Dalvik做任何的GC操作,任其内存增长,在触发OOM前停止GC抑制行为,采用空间换时间的方式进一步使App启动速度达到极致!
进入公众号,回复“程序员“可以领取一份计算机技术电子书福利合集
欢迎转发,关注公众号 肖晖
每天几分钟,掌握一个硬核面试知识点