提高应用的启动速度
启动的两种方式
- 冷启动
当直接从桌面上直接启动,同时后台没有该进程的缓存,这个时候系统就需要重新创建一个新的进程并且分配各种资源。(应用第一次启动或者我们按下home键滑动删除我们的应用后再启动)
- 热启动
该app后台有该进程的缓存,这时候启动的进程就属于热启动。(热启动不需要重新分配进程,也就是说不会再走Application了,直接进入的就是app的入口Activity,这样就速度就会快很多)
统计App启动时间
使用命令行来启动app,同时进行时间测量(单位:毫秒)
adb shell am start -W PackageName/PackageName.activity ;--注意:W一定要大些
eg: adb am start -W com.gfd.eshop/com.gfd.eshop.feature.SplashActivity
下面通过命令行来启动App,分别统计一个冷启动和热启动所花费的时间,通过时间来对比一下
参数明说:
* ThisTime: 指当前指定的Activity的启动时间
* TotalTime: 整个应用的启动时间,Application+Activity的使用的时间。
* WaitTime: 包括系统的影响时间
开发者一般只要关心 TotalTime 即可,这个时间才是自己应用真正启动的耗时。通过上面两幅图的比较,很明显,冷启动要比热启动耗时。
App启动流程
要想提高应用的启动速度,就要了解App的启动流程,从而知道时间到底花费在哪,我们也是据此来优化。
主要启动流程
- 1.通过 Launcher 启动应用时,点击应用图标后,
Launcher
调用startActivity
启动应用。 - 2.
Launcher Activity
最终调用Instrumentation
的execStartActivity
来启动应用。 - 3.
Instrumentation
调用ActivityManagerProxy
(ActivityManagerService 在应用进程的一个代理对象) 对象的 startActivity 方法启动 Activity。 - 4.到目前为止所有过程都在
Launcher
进程里面执行,接下来ActivityManagerProxy
对象跨进程调用ActivityManagerService
(运行在 system_server 进程)的startActivity
方法启动应用。 - 5.
ActivityManagerService
的startActivity
方法经过一系列调用,最后调用zygoteSendArgsAndGetResult
通过socket
发送给zygote
进程,zygote
进程会孵化出新的应用进程。 - 6.
zygote
进程孵化出新的应用进程后,会执行ActivityThread
类的main
方法。在该方法里会先准备好 Looper 和消息队列,然后调用 attach 方法将应用进程绑定到ActivityManagerService
,然后进入loop
循环,不断地读取消息队列里的消息,并分发消息。 - 7.
ActivityManagerService
保存应用进程的一个代理对象,然后ActivityManagerService
通过代理对象通知应用进程创建入口Activity
的实例,并执行它的生命周期函数。
用户在 Launcher 程序里点击应用图标时,会通知 ActivityManagerService 启动应用的入口 Activity, ActivityManagerService 发现这个应用还未启动,则会通知 Zygote 进程孵化出应用进程,然后在这个应用进程里执行 ActivityThread 的 main 方法。应用进程接下来通知 ActivityManagerService 应用进程已启动,ActivityManagerService 保存应用进程的一个代理对象,这样 ActivityManagerService 可以通过这个代理对象控制应用进程,然后 ActivityManagerService 通知应用进程创建入口 Activity 的实例,并执行它的生命周期函数。
上面的启动流程是 Android 提供的机制,作为开发者我们需要清楚或者至少了解其中的过程和原理,但我们并不能在这过程中做什么文章,我们能做的是从上述过程中最后一步开始,即 ActivityManagerService
通过代理对象通知应用进程创建入口 Activity
的实例,并执行它的生命周期函数开始.
Application从构造方法开始 ---> attachBaseContext() ---> onCreate()
Activity构造方法 ---> onCreate() ---> 设置显示界面布局,设置主题、背景等等属性
---> onStart() ---> onResume() ---> 显示里面的view(测量、布局、绘制,显示到界面上)
时间到底花在哪了?
- 1.不要在
Application
的构造方法、attachBaseContext()
、onCreate()
里面进行初始化耗时操作。 - 2.
MainActivity
,由于用户只关心最后的显示的这一帧,对我们的布局的层次要求要减少,自定义控件的话测量、布局、绘制的时间要减少。不要在onCreate
、onStart
、onResume
当中做耗时操作。 - 3.对于
SharedPreference
的初始化。因为他初始化的时候是需要将数据全部读取出来放到内存当中。所以尽可能减少sp文件数量(IO需要时间);像这样的初始化最好放到线程里面;大的数据缓存到数据库里面。
app启动的耗时主要是在:
Application
初始化 和MainActivity
的界面加载绘制时间。由于MainActivity
的业务和布局复杂度非常高,甚至该界面必须要有一些初始化的数据才能显示。那么这个时候MainActivity
就可能半天都出不来,这就给用户感觉app太卡了。我们要做的就是给用户赶紧利落的体验。点击app就立马弹出我们的界面。于是我们使用SplashActivity
一个非常简单的一个欢迎页面上面都不干就只显示一个图片。或者逻辑相对比较简单。但是SplashActivity
启动之后,还是需要跳到MainActivity
。MainActivity
还是需要从头开始加载布局和数据。想到SplashActivity
里面可以去做一些MainActivity
的数据的预加载。然后需要通过意图传到MainActivity
。
可不可以再做一些更好的优化呢?
耗时主要在Application
和Activity
的启动及资源加载时间;预加载的数据花的时间。如果我们能让这两个时间重叠在一个时间段内并发地做这两个事情就可以节省时间了。
解决方案
将SplashActivity
和MainActivity
合为一个。应用一启动还是加载MainActivity
,SplashActivity
可以变成一个SplashFragment
,然后放一个FrameLayout
作为根布局直接现实SplashFragment
,SplashFragment
里面非常之简单,就是现实一个图片,或者一些简单的逻辑,启动非常快,当SplashFragmen
t显示完毕后再将它remove
。同时在splash
的2S的友好时间内进行网络数据缓存。这个时候我们才看到MainActivity
,就不必再去等待网络数据返回了。
那么问题又来了
将SplashView
和ContentView
加载放到一起来做了 ,这可能会影响应用的启动时间。可以使用ViewStub
延迟加载MainActivity
当中的View
来达到减轻这个影响。ViewStub
的设计就是为了防止MainActivity
的启动加载资源太耗时了。延迟进行加载,不影响启动,但是ViewStub
加载也需要时间。我们可以等到主界面(Splash页面
)出来以后再去加载。
如何设计延迟加载
第一时间想到的就是在onCreate
里面调用handler.postDelayed()
方法。问题是这个延迟时间如何控制?真的准确吗?假设,需要在splash
做一个动画,需要达到的效果:应用已经启动并加载完成,界面已经显示出来了,然后我们再去启动动画。如果我们这样:
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
。。。。。。。。。
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
。。。。。。。
}
}, 2000);
}
这个延时任务真的是在执行到这个代码的时候开始计时的吗?其实不是的,在
onCreate()
中执行到该代码的时候,只是先将任务加载到消息队列中,等测量绘制完毕后才会开始执行。
之前在使用Handler
执行延时任务时我们都会放在onCreate()
中去执行,其实这样的延时时间是不准确的。我们需要在界面加载完毕后再开始执行。我们可以使用onwindowfocuschange
或者 ViewTreeObserver
。
代码实现
以上我们理清了应用启动耗时一些原因以及各种问题的解决方案,下面我们写代码实战一番。
主页面布局
使用ViewStub
来延时加载我们主页面真正的布局,FrameLayout
用来替换SplashFragment
来显示开始的动画。
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.applicationstartoptimizedemo.MainActivity" >
<ViewStub
android:id="@+id/content_viewstub"
android:layout="@layout/activity_main_viewstub"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<FrameLayout
android:id="@+id/frame"
android:layout_width="match_parent"
android:layout_height="match_parent" >
</FrameLayout>
</RelativeLayout>
SplashFragment代码
SplashFragment中我们只是简单展示一个图片,显示一个2秒的动画
public class SplashFragment extends Fragment {
@Override
@Nullable
public View onCreateView(LayoutInflater inflater,@Nullable ViewGroup container,@Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_splash, container,false);
}
}
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.applicationstartoptimizedemo.MainActivity" >
<FrameLayout
android:id="@+id/frame"
android:background="@drawable/splash"
android:layout_width="match_parent"
android:layout_height="match_parent" >
</FrameLayout>
</RelativeLayout>
主页面代码
public class MainActivity extends FragmentActivity {
private Handler mHandler = new Handler();
private SplashFragment splashFragment;
private ViewStub viewStub;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//先显示SplashFragment
splashFragment = new SplashFragment();
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
transaction.replace(R.id.frame, splashFragment);
transaction.commit();
viewStub = (ViewStub)findViewById(R.id.content_viewstub);
getWindow().getDecorView().post(new Runnable() {
//布局加载完毕
@Override
public void run() {
mHandler.post(new Runnable() {
@Override
public void run() {
viewStub.inflate();//延时加载主页面真正的布局
mHandler.postDelayed(new DelayRunnable(MainActivity.this, splashFragment) ,2000);//2秒后移除SplashFragment
}
} );
}
});
}
//延时要执行的任务
static class DelayRunnable implements Runnable{
private WeakReference<Context> contextRef;
private WeakReference<SplashFragment> fragmentRef;
public DelayRunnable(Context context, SplashFragment f) {
contextRef = new WeakReference<Context>(context);
fragmentRef = new WeakReference<SplashFragment>(f);
}
@Override
public void run() {
if(contextRef!=null){
SplashFragment splashFragment = fragmentRef.get();
if(splashFragment==null){
return;
}
//移除SplashFragment
FragmentActivity activity = (FragmentActivity) contextRef.get();
FragmentTransaction transaction = activity.getSupportFragmentManager().beginTransaction();
transaction.remove(splashFragment);
transaction.commit();
}
}
}
}
总结
一开始我们先显示SplashFragment,等布局加载完成后我们再去加载主页面真正的布局,因为SplashFragment页面需要显示一段时间,在这个时间段里我们去加载了主页面真正的布局,也就是说在显示SplashFragment的时候同时加载主页面真正的布局。等SplashFragment显示完毕后立刻展示主页面。这样就减少了主界面加载的时间。注意,这里延时加载主要是不要影响SplashFragment布局的加载。