應用啟動類型
冷啟動
場景:開機后第一次啟動應用 或者 應用被殺死后再次啟動
生命周期:Process.start->Application創(chuàng)建->attachBaseContext->onCreate->onStart->onResume->Activity生命周期
啟動速度:在幾種啟動類型中最慢锡移,也是我們優(yōu)化啟動速度最大的攔路虎溫啟動
場景:應用已經啟動续徽,返回鍵退出
生命周期:onCreate->onStart->onResume->Activity生命周期
啟動速度:較快熱啟動
場景:Home鍵最小化應用
生命周期:onResume->Activity生命周期
啟動速度:快
從上面的總結可以看出,在應用的啟動過程中鞍盗,冷啟動是最慢最耗時的缺菌,系統(tǒng)以及應用本身都有大量的工作需要處理焕窝,所以厌均,冷啟動對于應用的啟動速度是最具挑戰(zhàn)以及最有必要進行優(yōu)化的。
冷啟動流程
冷啟動指的是應用程序從進程在系統(tǒng)不存在葡公,到系統(tǒng)創(chuàng)建應用運行進程空間的過程罐农。冷啟動通常會發(fā)生在一下兩種情況:
- 設備啟動以來首次啟動應用程序
- 系統(tǒng)殺死應用程序之后再次啟動應用程序
在冷啟動的最開始,系統(tǒng)需要負責做三件事:
- 加載以及啟動app
- app啟動之后立刻顯示一個空白的預覽窗口
- 創(chuàng)建app進程
一旦系統(tǒng)完成創(chuàng)建app進程后催什,app進程將要接著負責完成下面的工作:
- 創(chuàng)建Application對象
- 創(chuàng)建并且啟動主線程ActivityThread
- 創(chuàng)建啟動第一個Activity
- Inflating views
- 布局屏幕
- 執(zhí)行第一次繪制
一旦app進程完完成了第一次繪制工作,系統(tǒng)進程就會用main activity替換前面顯示的預覽窗口宰睡,這個時候蒲凶,用戶就可以正式開始與app進行交互了。
從冷啟動的流程看拆内,我們無法干預app進程創(chuàng)建等系統(tǒng)操作旋圆,我們能夠干預的有:
- 預覽窗口
- Application生命周期回調
- Activity生命周期回調
優(yōu)化分析測量工具
對研發(fā)人員來說,啟動速度是我們的“門面”麸恍,它清清楚楚可以被所有人看到灵巧,我們都希望自己應用的啟動速度可以秒殺所有競爭對手搀矫。
“工欲善其事必先利其器”,我們需要先找到一款適合做啟動優(yōu)化分析的工具或者方式刻肄。
- adb shell am start -W [packageName]/[ packageName. AppstartActivity]
在統(tǒng)計 app 啟動時間時瓤球,系統(tǒng)為我們提供了 adb 命令,可以輸出啟動時間。系統(tǒng)在繪制完成后敏弃,ActivityManagerService 會回調該方法卦羡,但是能夠方便我們通過腳本多次啟動測量 TotalTime,對比版本間啟動時間差異麦到。但是統(tǒng)計時間不如 Systrace 準確绿饵。
- 代碼埋點
通過代碼埋點來準確獲取記錄每個方法的執(zhí)行時間,知道哪些地方耗時瓶颠,然后再有針對性地優(yōu)化拟赊。例如通過在 app 啟動生命周期中,關鍵位置加入時間點記錄粹淋,達到測量目的吸祟;又例如可以在 Application 的 attachBaseContext方法中記錄開始時間,然后在啟動的第一個 Activity 的 onWindowFocusChanged方法記錄結束時間廓啊。但是從用戶點擊 app Icon 到 Application 被創(chuàng)建欢搜,再到 Activity 的渲染,中間還是有很多步驟的谴轮,比如冷啟動的進程創(chuàng)建過程炒瘟,而這個時間用此版本是沒辦法統(tǒng)計了,必須得承受這點數據的不準確性第步。
Nanoscope
Nanoscope 非常真實疮装,不過暫時只支持 Nexus 6 和 x86 模擬器。Simpleperf
Simpleperf 的火焰圖并不適合做啟動流程分析粘都。TraceView
通過 TraceView 主要可以得到兩種數據:單次執(zhí)行耗時的方法 以及 執(zhí)行次數多的方法廓推。但是TraceView 性能耗損太大,不能比較正確反映真實情況翩隧。Systrace
Systrace 能夠追蹤關鍵系統(tǒng)調用的耗時情況樊展,如系統(tǒng)的 IO 操作、內核工作隊列堆生、CPU 負載专缠、Surface 渲染、GC 事件以及 Android 各個子系統(tǒng)的運行狀況等淑仆。但是不支持應用程序代碼的耗時分析涝婉。綜上所述,這幾種方式都各有各的優(yōu)點以及缺點蔗怠,我們都要掌握墩弯。
啟動優(yōu)化方法
在拿到整個啟動流程的全景圖之后吩跋,我們可以清楚地看到這段時間內系統(tǒng)、應用各個進程和線程的運行情況渔工,現在我們要開始真正開始“干活”了锌钮。具體的優(yōu)化方式,我把它們分為預覽窗口優(yōu)化涨缚、業(yè)務梳理轧粟、業(yè)務優(yōu)化、多進程優(yōu)化脓魏、線程優(yōu)化兰吟、GC 優(yōu)化和系統(tǒng)調用優(yōu)化。業(yè)務梳理茂翔、業(yè)務優(yōu)化混蔼、線程優(yōu)化、GC 優(yōu)化珊燎、系統(tǒng)調用優(yōu)化和布局優(yōu)化惭嚣。
預覽窗口優(yōu)化
當用戶點擊應用桌面圖標啟動應用的時候,利用提前展示出來的 Window悔政,快速展示出一個界面晚吞,用戶只需要很短的時間就可以看到“預覽頁”,這種完全“跟手”的感覺在高端機上體驗非常好谋国,但對于中低端機槽地,會把總的的閃屏時間變得更長。
如果點擊圖標沒有響應芦瘾,用戶主觀上會認為是手機系統(tǒng)響應比較慢捌蚊。所以比較推薦的做法是,只在 Android 6.0 或者 Android 7.0 以上才啟用“預覽窗口”方案近弟,讓手機性能好的用戶可以有更好的體驗缅糟。
要實現預覽窗口的顯示,只需要在利用 activity 的windowBackground主題屬性提供一個簡單的自定義 drawable 給啟動的 activity祷愉,如下:
Layout XML file:
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" android:opacity="opaque">
<!-- The background color, preferably the same as your normal theme -->
<item android:drawable="@android:color/white"/>
<!-- Your product logo - 144dp color version of your app icon -->
<item>
<bitmap
android:src="@drawable/product_logo_144dp"
android:gravity="center"/>
</item>
</layer-list>
Manifest file:
<activity ...
android:theme="@style/AppTheme.Launcher" />
這樣一個 activity 啟動的時候窗宦,就會先顯示一個預覽窗口,給用戶快速響應的體驗二鳄。當 activity想要恢復原來 theme迫摔,可以通過在調用super.onCreate() 和setContentView()之前調用 setTheme(R.style.AppTheme),如下:
public class MyMainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
// Make sure this is before calling super.onCreate
setTheme(R.style.Theme_MyApp);
super.onCreate(savedInstanceState);
// ...
}
}
業(yè)務梳理
不要一股腦把全部初始化工作放在 Application 中做泥从,需要梳理清楚當前啟動過程正在運行的每一個模塊,哪些是一定需要的沪摄、哪些可以砍掉躯嫉、哪些可以懶加載纱烘。但是需要注意的是,懶加載要防止集中化祈餐,否則容易出現首頁顯示后用戶無法操作的情形擂啥。總的來說帆阳,用以下四個維度分整理啟動的各個點:
- 必要且耗時:啟動初始化哺壶,考慮用線程來初始化。
- 必要不耗時:首頁繪制蜒谤。
- 非必要但耗時:數據上報山宾、插件初始化。
- 非必要不耗時:不用想鳍徽,這塊直接去掉资锰,在需要用的時再加載。
把數據整理出來后阶祭,按需實現加載邏輯绷杜,采取分步加載、異步加載濒募、延期加載策略鞭盟,如下圖所示:
一句話概述,要提高應用的啟動速度瑰剃,核心思想是在啟動過程中少做事情齿诉,越少越好。
業(yè)務優(yōu)化
通過梳理之后培他,剩下的都是啟動過程一定要用的模塊鹃两。這個時候,我們只能硬著頭皮去做進一步的優(yōu)化舀凛。優(yōu)化前期需要“抓大放小”俊扳,先看看主線程究竟慢在哪里。最理想是通過算法進行優(yōu)化猛遍,例如一個數據解密操作需要 1 秒馋记,通過算法優(yōu)化之后變成 10 毫秒。退而求其次懊烤,我們要考慮這些任務是不是可以通過異步線程預加載實現梯醒,但需要注意的是過多的線程預加載會讓我們的邏輯變得更加復雜。
業(yè)務優(yōu)化做到后面腌紧,會發(fā)現一些架構和歷史包袱會拖累我們前進的步伐茸习。比較常見的是一些事件會被各個業(yè)務模塊監(jiān)聽,大量的回調導致很多工作集中執(zhí)行壁肋,部分框架初始化“太厚”号胚,例如一些插件化框架籽慢,啟動過程各種反射、各種 Hook猫胁,整個耗時至少幾百毫秒箱亿。還有一些歷史包袱非常沉重,而且“牽一發(fā)動全身”弃秆,改動風險比較大届惋。但是我想說,如果有合適的時機菠赚,我們依然需要勇敢去償還這些“歷史債務”脑豹。
多進程優(yōu)化
Android app 是支持多進程的,在 Manifest 中只要在組件聲明中加入android:process屬性就可以讓組件在啟動時運行在不同的進程中锈至。舉個例子: 對于多進程 app 晨缴,可能擁有主進程,插件進程以及下載進程峡捡,但開發(fā)者只能在 Manifest 中聲明一個 Application 組件击碗,如果對應不同進程的組件啟動時,系統(tǒng)會創(chuàng)建三個進程们拙,創(chuàng)建三個 Application 對象稍途,同時attachBaseContext、onCreate等生命周期回調方法也會被調用三次砚婆。
但是每個進程需要初始化的內容肯定是不一樣的械拍,所以,為了防止資源的浪費装盯,我們需要在Application 中區(qū)分進程坷虑,對應進程只初始化對應的內容。
線程優(yōu)化
線程優(yōu)化分兩方面:
第一埂奈,耗時任務異步化迄损。子線程處理耗時任務,主線程做的事情越少账磺,越早進入Acitivity繪制階段芹敌,界面越早展現。例如不在主線程做如 IO 垮抗、網絡等耗時操作氏捞。但是要注意,子線程不能阻塞主線程冒版。
第二液茎,線程池管理線程,控制線程的數量。線程數量太多會相互競爭 CPU 資源豁护,導致分給主線程的時間片減少哼凯,從而導致啟動速度變慢。線程切換的數據我們可以通過卡頓優(yōu)化中學到的 sched 文件查看楚里,這里特別需要注意 nr_involuntary_switches 被動切換的次數。
proc/[pid]/sched:
nr_voluntary_switches:主動上下文切換次數猎贴,因為線程無法獲取所需資源導致上下文切換班缎,最普遍的是 IO。
nr_involuntary_switches:被動上下文切換次數她渴,線程被系統(tǒng)強制調度導致上下文切換达址,例如大量線程在搶占 CPU。
第三趁耗,避免主線程與子線程之間的鎖阻塞等待沉唠。有一次我們把主線程內的一個耗時任務放到線程中并發(fā)執(zhí)行,但是發(fā)現這樣做根本沒起作用苛败。仔細檢查后發(fā)現線程內部會持有一個鎖满葛,主線程很快就有其他任務因為這個鎖而等待。通過 Systrace 可以看到鎖等待的事件罢屈,我們需要排查這些等待是否可以優(yōu)化嘀韧,特別是防止主線程出現長時間的空轉。
特別是現在有很多啟動框架缠捌,會使用 Pipeline 機制锄贷,根據業(yè)務優(yōu)先級規(guī)定業(yè)務初始化時機。比如微信內部使用的 <u>mmkernel</u> 曼月、阿里最近開源的 <u>Alpha</u> 啟動框架谊却,它們?yōu)楦鱾€任務建立依賴關系,最終構成一個有向無環(huán)圖哑芹。對于可以并發(fā)的任務炎辨,會通過線程池最大程度提升啟動速度。如果任務的依賴關系沒有配置好绩衷,很容易出現下圖這種情況蹦魔,即主線程會一直等待 taskC 結束,空轉 2950 毫秒咳燕。
GC優(yōu)化
在啟動過程勿决,要盡量減少 GC 的次數,避免造成主線程長時間的卡頓招盲,特別是對 Dalvik 來說低缩,我們可以通過 Systrace 單獨查看整個啟動過程 GC 的時間。
啟動過程避免進行大量的字符串操作,特別是序列化跟反序列化過程咆繁。一些頻繁創(chuàng)建的對象讳推,例如網絡庫和圖片庫中的 Byte 數組、Buffer 可以復用玩般。如果一些模塊實在需要頻繁創(chuàng)建對象银觅,可以考慮移到 Native 實現。
Java 對象的逃逸也很容易引起 GC 問題坏为,我們在寫代碼的時候比較容易忽略這個點究驴。我們應該保證對象生命周期盡量的短,在棧上就進行銷毀匀伏。
系統(tǒng)調用優(yōu)化
部分系統(tǒng)的API使用是阻塞性的洒忧,文件很小可能無法感知,當文件過大够颠,或者使用頻繁時熙侍,可能造成阻塞。例如:SharedPreference.Editor 的提交操作建議使用異步的 apply履磨,而不是阻塞的 commit蛉抓。
通過 systrace 的 System Service 類型,我們可以看到啟動過程 System Server 的CPU 工作情況蹬耘。在啟動過程芝雪,我們盡量不要做系統(tǒng)調用,例如 PackageManagerService 操作综苔、Binder 調用等待惩系。
在啟動過程也不要過早地拉起應用的其他進程,System Server 和新的進程都會競爭 CPU 資源如筛。特別是系統(tǒng)內存不足的時候堡牡,當我們拉起一個新的進程,可能會成為“壓死駱駝的最后一根稻草”杨刨。它可能會觸發(fā)系統(tǒng)的 low memorykiller 機制晤柄,導致系統(tǒng)殺死和拉起(保活)大量的進程妖胀,從而影響前臺進程的 CPU芥颈。舉個例子,之前一個程序在啟動過程會拉起下載和視頻播放進程赚抡,改為按需拉起后爬坑,線上啟動時間提高了 3%,對于 1GB 以下的低端機優(yōu)化涂臣,整個啟動時間可以優(yōu)化 5%~8%盾计,效果還是非常明顯的。
布局優(yōu)化
布局越復雜,測量布局繪制的時間就越長署辉。主要做到以下幾點:
- 布局的層級越少族铆,加載速度越快。
- 一個控件的屬性越少哭尝,解析越快哥攘,刪除控件中的無用屬性。
- 使用<ViewStub/>標簽加載一些不常用的布局刚夺,做到使用時在加載献丑。
- 使用<merge/>標簽減少布局的嵌套層次。
- 盡可能少用wrap_content侠姑,wrap_content會增加布局measure時的計算成本,已知寬高為固定值時箩做,不用wrap_content莽红。
總結
以上就是本人學習過程中對啟動優(yōu)化相關內容的總結,謝謝大家能夠閱讀到這里邦邦。
啟動優(yōu)化安吁,是一項長期的任務,任重而道遠燃辖。
開發(fā)者要未雨綢繆鬼店,在編碼過程中盡量減少給啟動帶來性能損耗的工作,主要注意以下幾個事項:
- 盡量避免啟動時在主線程做密集繁重的工作黔龟,如:避免 I/O 操作妇智、反序列化、網絡操作氏身、鎖等待等巍棱。
- 對模塊以及第三方庫按需加載,采取分步加載蛋欣、異步加載航徙、延期加載等策略。
- 利用線程池管理線程陷虎,避免創(chuàng)建大量線程到踏,造成 CPU 競爭,導致主線程時間片減少尚猿。
- 啟動過程中窝稿,盡量避免頻繁創(chuàng)建的大量對象,減少 GC 給啟動性能帶來的卡頓影響谊路。
- 盡量避免在啟動過程中調用阻塞性的系統(tǒng)調用讹躯。