啟動(dòng)的兩種方式
- 冷啟動(dòng)
當(dāng)直接從桌面上直接啟動(dòng)沈跨,同時(shí)后臺(tái)沒(méi)有該進(jìn)程的緩存庆尘,這個(gè)時(shí)候系統(tǒng)就需要重新創(chuàng)建一個(gè)新的進(jìn)程并且分配各種資源宙枷。(應(yīng)用第一次啟動(dòng)或者我們按下home鍵滑動(dòng)刪除我們的應(yīng)用后再啟動(dòng))
- 熱啟動(dòng)
該app后臺(tái)有該進(jìn)程的緩存送挑,這時(shí)候啟動(dòng)的進(jìn)程就屬于熱啟動(dòng)千元。(熱啟動(dòng)不需要重新分配進(jìn)程屯远,也就是說(shuō)不會(huì)再走Application了蔓姚,直接進(jìn)入的就是app的入口Activity,這樣就速度就會(huì)快很多)
統(tǒng)計(jì)App啟動(dòng)時(shí)間
使用命令行來(lái)啟動(dòng)app慨丐,同時(shí)進(jìn)行時(shí)間測(cè)量(單位:毫秒)
adb shell am start -W PackageName/PackageName.activity ;--注意:W一定要大些
eg: adb am start -W com.gfd.eshop/com.gfd.eshop.feature.SplashActivity
下面通過(guò)命令行來(lái)啟動(dòng)App,分別統(tǒng)計(jì)一個(gè)冷啟動(dòng)和熱啟動(dòng)所花費(fèi)的時(shí)間坡脐,通過(guò)時(shí)間來(lái)對(duì)比一下
參數(shù)明說(shuō):
* ThisTime: 指當(dāng)前指定的Activity的啟動(dòng)時(shí)間
* TotalTime: 整個(gè)應(yīng)用的啟動(dòng)時(shí)間,Application+Activity的使用的時(shí)間房揭。
* WaitTime: 包括系統(tǒng)的影響時(shí)間
開發(fā)者一般只要關(guān)心 TotalTime 即可备闲,這個(gè)時(shí)間才是自己應(yīng)用真正啟動(dòng)的耗時(shí)。通過(guò)上面兩幅圖的比較捅暴,很明顯恬砂,冷啟動(dòng)要比熱啟動(dòng)耗時(shí)。
App啟動(dòng)流程
要想提高應(yīng)用的啟動(dòng)速度蓬痒,就要了解App的啟動(dòng)流程泻骤,從而知道時(shí)間到底花費(fèi)在哪,我們也是據(jù)此來(lái)優(yōu)化梧奢。
主要啟動(dòng)流程
- 1.通過(guò) Launcher 啟動(dòng)應(yīng)用時(shí)狱掂,點(diǎn)擊應(yīng)用圖標(biāo)后,
Launcher
調(diào)用startActivity
啟動(dòng)應(yīng)用亲轨。 - 2.
Launcher Activity
最終調(diào)用Instrumentation
的execStartActivity
來(lái)啟動(dòng)應(yīng)用趋惨。 - 3.
Instrumentation
調(diào)用ActivityManagerProxy
(ActivityManagerService 在應(yīng)用進(jìn)程的一個(gè)代理對(duì)象) 對(duì)象的 startActivity 方法啟動(dòng) Activity。 - 4.到目前為止所有過(guò)程都在
Launcher
進(jìn)程里面執(zhí)行惦蚊,接下來(lái)ActivityManagerProxy
對(duì)象跨進(jìn)程調(diào)用ActivityManagerService
(運(yùn)行在 system_server 進(jìn)程)的startActivity
方法啟動(dòng)應(yīng)用器虾。 - 5.
ActivityManagerService
的startActivity
方法經(jīng)過(guò)一系列調(diào)用讯嫂,最后調(diào)用zygoteSendArgsAndGetResult
通過(guò)socket
發(fā)送給zygote
進(jìn)程,zygote
進(jìn)程會(huì)孵化出新的應(yīng)用進(jìn)程兆沙。 - 6.
zygote
進(jìn)程孵化出新的應(yīng)用進(jìn)程后欧芽,會(huì)執(zhí)行ActivityThread
類的main
方法。在該方法里會(huì)先準(zhǔn)備好 Looper 和消息隊(duì)列挤悉,然后調(diào)用 attach 方法將應(yīng)用進(jìn)程綁定到ActivityManagerService
渐裸,然后進(jìn)入loop
循環(huán),不斷地讀取消息隊(duì)列里的消息装悲,并分發(fā)消息昏鹃。 - 7.
ActivityManagerService
保存應(yīng)用進(jìn)程的一個(gè)代理對(duì)象,然后ActivityManagerService
通過(guò)代理對(duì)象通知應(yīng)用進(jìn)程創(chuàng)建入口Activity
的實(shí)例诀诊,并執(zhí)行它的生命周期函數(shù)洞渤。
用戶在 Launcher 程序里點(diǎn)擊應(yīng)用圖標(biāo)時(shí),會(huì)通知 ActivityManagerService 啟動(dòng)應(yīng)用的入口 Activity属瓣, ActivityManagerService 發(fā)現(xiàn)這個(gè)應(yīng)用還未啟動(dòng)载迄,則會(huì)通知 Zygote 進(jìn)程孵化出應(yīng)用進(jìn)程,然后在這個(gè)應(yīng)用進(jìn)程里執(zhí)行 ActivityThread 的 main 方法抡蛙。應(yīng)用進(jìn)程接下來(lái)通知 ActivityManagerService 應(yīng)用進(jìn)程已啟動(dòng)护昧,ActivityManagerService 保存應(yīng)用進(jìn)程的一個(gè)代理對(duì)象,這樣 ActivityManagerService 可以通過(guò)這個(gè)代理對(duì)象控制應(yīng)用進(jìn)程粗截,然后 ActivityManagerService 通知應(yīng)用進(jìn)程創(chuàng)建入口 Activity 的實(shí)例惋耙,并執(zhí)行它的生命周期函數(shù)。
上面的啟動(dòng)流程是 Android 提供的機(jī)制熊昌,作為開發(fā)者我們需要清楚或者至少了解其中的過(guò)程和原理绽榛,但我們并不能在這過(guò)程中做什么文章,我們能做的是從上述過(guò)程中最后一步開始婿屹,即 ActivityManagerService
通過(guò)代理對(duì)象通知應(yīng)用進(jìn)程創(chuàng)建入口 Activity
的實(shí)例灭美,并執(zhí)行它的生命周期函數(shù)開始.
Application從構(gòu)造方法開始 ---> attachBaseContext() ---> onCreate()
Activity構(gòu)造方法 ---> onCreate() ---> 設(shè)置顯示界面布局,設(shè)置主題昂利、背景等等屬性
---> onStart() ---> onResume() ---> 顯示里面的view(測(cè)量届腐、布局、繪制页眯,顯示到界面上)
時(shí)間到底花在哪了梯捕?
- 1.不要在
Application
的構(gòu)造方法、attachBaseContext()
窝撵、onCreate()
里面進(jìn)行初始化耗時(shí)操作。 - 2.
MainActivity
襟铭,由于用戶只關(guān)心最后的顯示的這一幀碌奉,對(duì)我們的布局的層次要求要減少短曾,自定義控件的話測(cè)量、布局赐劣、繪制的時(shí)間要減少嫉拐。不要在onCreate
、onStart
魁兼、onResume
當(dāng)中做耗時(shí)操作婉徘。 - 3.對(duì)于
SharedPreference
的初始化。因?yàn)樗跏蓟臅r(shí)候是需要將數(shù)據(jù)全部讀取出來(lái)放到內(nèi)存當(dāng)中咐汞。所以盡可能減少sp文件數(shù)量(IO需要時(shí)間)盖呼;像這樣的初始化最好放到線程里面;大的數(shù)據(jù)緩存到數(shù)據(jù)庫(kù)里面化撕。
app啟動(dòng)的耗時(shí)主要是在:
Application
初始化 和MainActivity
的界面加載繪制時(shí)間几晤。由于MainActivity
的業(yè)務(wù)和布局復(fù)雜度非常高,甚至該界面必須要有一些初始化的數(shù)據(jù)才能顯示植阴。那么這個(gè)時(shí)候MainActivity
就可能半天都出不來(lái)蟹瘾,這就給用戶感覺(jué)app太卡了。我們要做的就是給用戶趕緊利落的體驗(yàn)掠手。點(diǎn)擊app就立馬彈出我們的界面憾朴。于是我們使用SplashActivity
一個(gè)非常簡(jiǎn)單的一個(gè)歡迎頁(yè)面上面都不干就只顯示一個(gè)圖片∨绺耄或者邏輯相對(duì)比較簡(jiǎn)單众雷。但是SplashActivity
啟動(dòng)之后,還是需要跳到MainActivity
魁衙。MainActivity
還是需要從頭開始加載布局和數(shù)據(jù)报腔。想到SplashActivity
里面可以去做一些MainActivity
的數(shù)據(jù)的預(yù)加載。然后需要通過(guò)意圖傳到MainActivity
剖淀。
可不可以再做一些更好的優(yōu)化呢纯蛾?
耗時(shí)主要在Application
和Activity
的啟動(dòng)及資源加載時(shí)間;預(yù)加載的數(shù)據(jù)花的時(shí)間纵隔。如果我們能讓這兩個(gè)時(shí)間重疊在一個(gè)時(shí)間段內(nèi)并發(fā)地做這兩個(gè)事情就可以節(jié)省時(shí)間了翻诉。
解決方案
將SplashActivity
和MainActivity
合為一個(gè)。應(yīng)用一啟動(dòng)還是加載MainActivity
捌刮,SplashActivity
可以變成一個(gè)SplashFragment
碰煌,然后放一個(gè)FrameLayout
作為根布局直接現(xiàn)實(shí)SplashFragment
,SplashFragment
里面非常之簡(jiǎn)單绅作,就是現(xiàn)實(shí)一個(gè)圖片芦圾,或者一些簡(jiǎn)單的邏輯,啟動(dòng)非扯砣希快,當(dāng)SplashFragmen
t顯示完畢后再將它remove
个少。同時(shí)在splash
的2S的友好時(shí)間內(nèi)進(jìn)行網(wǎng)絡(luò)數(shù)據(jù)緩存洪乍。這個(gè)時(shí)候我們才看到MainActivity
,就不必再去等待網(wǎng)絡(luò)數(shù)據(jù)返回了夜焦。
那么問(wèn)題又來(lái)了
將SplashView
和ContentView
加載放到一起來(lái)做了 壳澳,這可能會(huì)影響應(yīng)用的啟動(dòng)時(shí)間∶>可以使用ViewStub
延遲加載MainActivity
當(dāng)中的View
來(lái)達(dá)到減輕這個(gè)影響巷波。ViewStub
的設(shè)計(jì)就是為了防止MainActivity
的啟動(dòng)加載資源太耗時(shí)了。延遲進(jìn)行加載卸伞,不影響啟動(dòng),但是ViewStub
加載也需要時(shí)間抹镊。我們可以等到主界面(Splash頁(yè)面
)出來(lái)以后再去加載。
如何設(shè)計(jì)延遲加載
第一時(shí)間想到的就是在onCreate
里面調(diào)用handler.postDelayed()
方法瞪慧。問(wèn)題是這個(gè)延遲時(shí)間如何控制髓考?真的準(zhǔn)確嗎?假設(shè)弃酌,需要在splash
做一個(gè)動(dòng)畫氨菇,需要達(dá)到的效果:應(yīng)用已經(jīng)啟動(dòng)并加載完成,界面已經(jīng)顯示出來(lái)了妓湘,然后我們?cè)偃?dòng)動(dòng)畫查蓉。如果我們這樣:
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
。榜贴。豌研。。唬党。鹃共。。驶拱。霜浴。
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
。蓝纲。阴孟。。税迷。永丝。。
}
}, 2000);
}
這個(gè)延時(shí)任務(wù)真的是在執(zhí)行到這個(gè)代碼的時(shí)候開始計(jì)時(shí)的嗎箭养?其實(shí)不是的慕嚷,在
onCreate()
中執(zhí)行到該代碼的時(shí)候,只是先將任務(wù)加載到消息隊(duì)列中,等測(cè)量繪制完畢后才會(huì)開始執(zhí)行闯冷。
之前在使用Handler
執(zhí)行延時(shí)任務(wù)時(shí)我們都會(huì)放在onCreate()
中去執(zhí)行砂心,其實(shí)這樣的延時(shí)時(shí)間是不準(zhǔn)確的懈词。我們需要在界面加載完畢后再開始執(zhí)行蛇耀。我們可以使用onwindowfocuschange
或者 ViewTreeObserver
。
代碼實(shí)現(xiàn)
以上我們理清了應(yīng)用啟動(dòng)耗時(shí)一些原因以及各種問(wèn)題的解決方案坎弯,下面我們寫代碼實(shí)戰(zhàn)一番纺涤。
主頁(yè)面布局
使用ViewStub
來(lái)延時(shí)加載我們主頁(yè)面真正的布局,FrameLayout
用來(lái)替換SplashFragment
來(lái)顯示開始的動(dòng)畫抠忘。
<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中我們只是簡(jiǎn)單展示一個(gè)圖片撩炊,顯示一個(gè)2秒的動(dòng)畫
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>
主頁(yè)面代碼
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();//延時(shí)加載主頁(yè)面真正的布局
mHandler.postDelayed(new DelayRunnable(MainActivity.this, splashFragment) ,2000);//2秒后移除SplashFragment
}
} );
}
});
}
//延時(shí)要執(zhí)行的任務(wù)
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();
}
}
}
}
總結(jié)
一開始我們先顯示SplashFragment,等布局加載完成后我們?cè)偃ゼ虞d主頁(yè)面真正的布局崎脉,因?yàn)镾plashFragment頁(yè)面需要顯示一段時(shí)間拧咳,在這個(gè)時(shí)間段里我們?nèi)ゼ虞d了主頁(yè)面真正的布局,也就是說(shuō)在顯示SplashFragment的時(shí)候同時(shí)加載主頁(yè)面真正的布局囚灼。等SplashFragment顯示完畢后立刻展示主頁(yè)面骆膝。這樣就減少了主界面加載的時(shí)間。注意灶体,這里延時(shí)加載主要是不要影響SplashFragment布局的加載阅签。