本文主要是學習了極客時間張紹文老師的 Android開發高手課 以及 谷歌官網文章 的啓動優化筆記~java
參考文章:
https://time.geekbang.org/column/article/73651 https://mp.weixin.qq.com/s/eaArt5Udc4WZ3NoH5RlEkQ https://juejin.im/post/5874bff0128fe1006b443fa0 https://developer.android.google.cn/topic/performance/vitals/launch-time
從上面的總結能夠看出,在應用的啓動過程當中,冷啓動是最慢最耗時的,系統以及應用自己都有大量的工做須要處理,因此,冷啓動對於應用的啓動速度是最具挑戰以及最有必要進行優化的。android
冷啓動指的是應用程序從進程在系統不存在,到系統建立應用運行進程空間的過程。冷啓動一般會發生在一下兩種狀況:git
在冷啓動的最開始,系統須要負責作三件事:github
一旦系統完成建立app進程後,app進程將要接着負責完成下面的工做:算法
一旦app進程完完成了第一次繪製工做,系統進程就會用main activity替換前面顯示的預覽窗口,這個時候,用戶就能夠正式開始與app進行交互了。shell
從冷啓動的流程看,咱們沒法干預app進程建立等系統操做,咱們可以干預的有:數組
對研發人員來講,啓動速度是咱們的「門面」,它清清楚楚能夠被全部人看到,咱們都但願本身應用的啓動速度能夠秒殺全部競爭對手。緩存
「工欲善其事必先利其器」,咱們須要先找到一款適合作啓動優化分析的工具或者方式。bash
在統計 app 啓動時間時,系統爲咱們提供了 adb 命令,能夠輸出啓動時間。系統在繪製完成後,ActivityManagerService 會回調該方法,可是可以方便咱們經過腳本屢次啓動測量 TotalTime,對比版本間啓動時間差別。可是統計時間不如 Systrace 準確。微信
經過代碼埋點來準確獲取記錄每一個方法的執行時間,知道哪些地方耗時,而後再有針對性地優化。例如經過在 app 啓動生命週期中,關鍵位置加入時間點記錄,達到測量目的;又例如能夠在 Application 的 attachBaseContext
方法中記錄開始時間,而後在啓動的第一個 Activity 的 onWindowFocusChanged
方法記錄結束時間。
可是從用戶點擊 app Icon 到 Application 被建立,再到 Activity 的渲染,中間仍是有不少步驟的,好比冷啓動的進程建立過程,而這個時間用此版本是沒辦法統計了,必須得承受這點數據的不許確性。
Nanoscope
Nanoscope 很是真實,不過暫時只支持 Nexus 6 和 x86 模擬器。
Simpleperf
Simpleperf 的火焰圖並不適合作啓動流程分析。
經過 TraceView 主要能夠獲得兩種數據:單次執行耗時的方法 以及 執行次數多的方法。可是TraceView 性能耗損太大,不能比較正確反映真實狀況。
Systrace 可以追蹤關鍵系統調用的耗時狀況,如系統的 IO 操做、內核工做隊列、CPU 負載、Surface 渲染、GC 事件以及 Android 各個子系統的運行情況等。可是不支持應用程序代碼的耗時分析。
綜上所述,這幾種方式都各有各的優勢以及缺點,咱們都要掌握。
可是有沒有一種比較折中比較理想的方案呢?有的。
除了可以看到例如 GC、System Server、CPU 調度等系統調用的耗時,還可以經過 Android 工程編譯的過程當中,在指定的方法先後,自動化插入插樁函數,統計方法執行時間。經過插樁,咱們能夠看到應用主線程和其餘線程的函數調用流程。它的實現原理很是簡單,就是將下面的兩個函數 經過用ASM框架修改字節碼的方式 分別插入到每一個方法的入口和出口。
class TraceMethod {
public static void i() {
Trace.beginSection();
}
public static void o() {
Trace.endSection();
}
}複製代碼
固然這裏面有很是多的細節須要考慮,好比怎麼樣下降插樁對性能的影響、哪些函數須要被排除掉。函數插樁後的效果以下:
class Test {
public void test() {
TraceMethod.i();
// 原來的工做
TraceMethod.o();
}
}複製代碼
只有準確的數據評估才能指引優化的方向,這一步是很是重要的。沒有充分評估或者評估使用了錯誤的方法,最終獲得了錯誤的方向,會致使最後發現根本達不到預期的優化效果。
在拿到整個啓動流程的全景圖以後,咱們能夠清楚地看到這段時間內系統、應用各個進程和線程的運行狀況,如今咱們要開始真正開始「幹活」了。
具體的優化方式,我把它們分爲預覽窗口優化、業務梳理、業務優化、多進程優化、線程優化、GC 優化和系統調用優化。業務梳理、業務優化、線程優化、GC 優化、系統調用優化和佈局優化。
當用戶點擊應用桌面圖標啓動應用的時候,利用提早展現出來的 Window,快速展現出一個界面,用戶只須要很短的時間就能夠看到「預覽頁」,這種徹底「跟手」的感受在高端機上體驗很是好,但對於中低端機,會把總的的閃屏時間變得更長。
若是點擊圖標沒有響應,用戶主觀上會認爲是手機系統響應比較慢。因此比較推薦的作法是,只在 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);
// ...
}
}複製代碼
不要一股腦把所有初始化工做放在 Application 中作,須要梳理清楚當前啓動過程正在運行的每個模塊,哪些是必定須要的、哪些能夠砍掉、哪些能夠懶加載。可是須要注意的是,懶加載要防止集中化,不然容易出現首頁顯示後用戶沒法操做的情形。總的來講,用如下四個維度分整理啓動的各個點:
把數據整理出來後,按需實現加載邏輯,採起分步加載、異步加載、延期加載策略,以下圖所示:
一句話概述,要提升應用的啓動速度,核心思想是在啓動過程當中少作事情,越少越好。
經過梳理以後,剩下的都是啓動過程必定要用的模塊。這個時候,咱們只能硬着頭皮去作進一步的優化。優化前期須要「抓大放小」,先看看主線程究竟慢在哪裏。最理想是經過算法進行優化,例如一個數據解密操做須要 1 秒,經過算法優化以後變成 10 毫秒。退而求其次,咱們要考慮這些任務是否是能夠經過異步線程預加載實現,但須要注意的是過多的線程預加載會讓咱們的邏輯變得更加複雜。
業務優化作到後面,會發現一些架構和歷史包袱會拖累咱們前進的步伐。比較常見的是一些事件會被各個業務模塊監聽,大量的回調致使不少工做集中執行,部分框架初始化「太厚」,例如一些插件化框架,啓動過程各類反射、各類 Hook,整個耗時至少幾百毫秒。還有一些歷史包袱很是沉重,並且「牽一髮動全身」,改動風險比較大。可是我想說,若是有合適的時機,咱們依然須要勇敢去償還這些「歷史債務」。
Android app 是支持多進程的,在 Manifest 中只要在組件聲明中加入android:process
屬性就可讓組件在啓動時運行在不一樣的進程中。舉個例子: 對於多進程 app ,可能擁有主進程,插件進程以及下載進程,但開發者只能在 Manifest 中聲明一個 Application 組件,若是對應不一樣進程的組件啓動時,系統會建立三個進程,建立三個 Application 對象,同時attachBaseContext
、onCreate
等生命週期回調方法也會被調用三次。
可是每一個進程須要初始化的內容確定是不同的,因此,爲了防止資源的浪費,咱們須要在Application 中區分進程,對應進程只初始化對應的內容。
線程優化分兩方面:
第一,耗時任務異步化。子線程處理耗時任務,主線程作的事情越少,越早進入Acitivity繪製階段,界面越早展示。例如不在主線程作如 IO 、網絡等耗時操做。可是要注意,子線程不能阻塞主線程。
第二,線程池管理線程,控制線程的數量。線程數量太多會相互競爭 CPU 資源,致使分給主線程的時間片減小,從而致使啓動速度變慢。線程切換的數據咱們能夠經過卡頓優化中學到的 sched 文件查看,這裏特別須要注意 nr_involuntary_switches 被動切換的次數。
proc/[pid]/sched:
nr_voluntary_switches:主動上下文切換次數,由於線程沒法獲取所需資源致使上下文切換,最廣泛的是 IO。
nr_involuntary_switches:被動上下文切換次數,線程被系統強制調度致使上下文切換,例如大量線程在搶佔 CPU。 複製代碼
第三,避免主線程與子線程之間的鎖阻塞等待。有一次咱們把主線程內的一個耗時任務放到線程中併發執行,可是發現這樣作根本沒起做用。仔細檢查後發現線程內部會持有一個鎖,主線程很快就有其餘任務由於這個鎖而等待。經過 Systrace 能夠看到鎖等待的事件,咱們須要排查這些等待是否能夠優化,特別是防止主線程出現長時間的空轉。
特別是如今有不少啓動框架,會使用 Pipeline 機制,根據業務優先級規定業務初始化時機。好比微信內部使用的 mmkernel 、阿里最近開源的 Alpha 啓動框架,它們爲各個任務創建依賴關係,最終構成一個有向無環圖。對於能夠併發的任務,會經過線程池最大程度提高啓動速度。若是任務的依賴關係沒有配置好,很容易出現下圖這種狀況,即主線程會一直等待 taskC 結束,空轉 2950 毫秒。
第四,設置子線程優先級。不重要任務,設置子線程優先級爲 THREAD_PRIORITY_BACKGROUND,這樣子線程最多能獲取到10%的時間片,優先保證主線程執行。
在啓動過程,要儘可能減小 GC 的次數,避免形成主線程長時間的卡頓,特別是對 Dalvik 來講,咱們能夠經過 Systrace 單獨查看整個啓動過程 GC 的時間。
啓動過程避免進行大量的字符串操做,特別是序列化跟反序列化過程。一些頻繁建立的對象,例如網絡庫和圖片庫中的 Byte 數組、Buffer 能夠複用。若是一些模塊實在須要頻繁建立對象,能夠考慮移到 Native 實現。
Java 對象的逃逸也很容易引發 GC 問題,咱們在寫代碼的時候比較容易忽略這個點。咱們應該保證對象生命週期儘可能的短,在棧上就進行銷燬。
部分系統的API使用是阻塞性的,文件很小可能沒法感知,當文件過大,或者使用頻繁時,可能形成阻塞。例如:SharedPreference.Editor 的提交操做建議使用異步的 apply,而不是阻塞的 commit。
經過 systrace 的 System Service 類型,咱們能夠看到啓動過程 System Server 的CPU 工做狀況。在啓動過程,咱們儘可能不要作系統調用,例如 PackageManagerService 操做、Binder 調用等待。
在啓動過程也不要過早地拉起應用的其餘進程,System Server 和新的進程都會競爭 CPU 資源。特別是系統內存不足的時候,當咱們拉起一個新的進程,可能會成爲「壓死駱駝的最後一根稻草」。它可能會觸發系統的 low memorykiller 機制,致使系統殺死和拉起(保活)大量的進程,從而影響前臺進程的 CPU。舉個例子,以前一個程序在啓動過程會拉起下載和視頻播放進程,改成按需拉起後,線上啓動時間提升了 3%,對於 1GB 如下的低端機優化,整個啓動時間能夠優化 5%~8%,效果仍是很是明顯的。
佈局越複雜,測量佈局繪製的時間就越長。主要作到如下幾點:
還有什麼方法能夠作進一步優化嗎?
若是咱們在啓動的過程當中須要讀一個文件 test.io 的 1KB 數據,而咱們的 buffer 不當心寫成 1byte,那麼總共要讀取 1000 次。系統是否會真的發起 1000 次磁盤 IO 呢?
事實上 1000 次讀操做只是咱們發起的次數,並非真正的磁盤 I/O 次數。你能夠參考下面 Linux 文件 I/O流程。
Linux 文件系統從磁盤讀文件的時候,會以 block 爲單位去磁盤讀取,通常 block 大小是 4KB。也就是說一次磁盤讀寫大小至少是 4KB,而後會把 4KB 數據放到頁緩存 Page Cache 中。若是下次讀取文件數據已經在頁緩存中,那就不會發生真實的磁盤 I/O,而是直接從頁緩存中讀取,大大提高了讀的速度。因此上面的例子,咱們雖然讀了 1000 次,但事實上只會發生一次磁盤 I/O,其餘的數據都會在頁緩存中獲得。
Dex 文件用的到的類和安裝包 APK 裏面各類資源文件通常都比較小,可是讀取很是頻繁。咱們能夠利用系統這個機制將它們按照讀取順序從新排列,減小真實的磁盤 I/O 次數。
在啓動優化中,數據的重排主要有兩方面:類重排 以及 資源文件重排。
類重排
類重排的實現經過 ReDex 的 Interdex 調整類在 Dex 中的排列順序。
不明白能夠看這篇文章:Redex 初探與 Interdex:Andorid 冷啓動優化
根據interdex官方介紹的原理,咱們能夠知道要實現這個優化須要解決三個問題:
redex中的方案是dump出程序啓動時的hprof文件,再從中分析出加載的類,比較麻煩。這裏咱們採用的方案是hook住ClassLoader.findClass方法,在系統加載類時日誌打印出類名,這樣分析日誌就能夠獲得啓動時加載的類序列了。
class GetClassLoader extends PathClassLoader {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 將類名 name 記錄到文件
writeToFile(name, "coldstart_classes.txt");
return super.findClass(name);
}
}複製代碼
redex的作法應該是解析出全部dex中的類,再按配置的加載類序列,從主dex開始從新生成各個dex,因此會打亂原有的dex分佈。而在手q中,分dex規則是編譯腳本中維護的,所以咱們能夠修改分包邏輯,將須要的類放到主dex。
開源就是好。Android編譯時把.class轉換成.dex是依靠dx.bat,這個工具實際執行的是sdk中的dx.jar。咱們能夠修改dx的源碼,替換這個jar包,就能夠執行自定義的dx邏輯了。簡單說下具體修改方法:
這裏須要對dex的文件格式作必定了解,再也不細說,網上有一篇很好的文章,有興趣能夠了解下 http://blog.csdn.net/jiangwei0910410003/article/details/50668549
資源文件重排
Facebook 在比較早的時候就使用「資源熱圖」來實現資源文件的重排,最近支付寶在《經過安裝包重排布優化 Android 端啓動性能》中也詳細講述了資源重排的原理和落地方法。
加載類的過程有一個 verify class 的步驟,它須要須要校驗方法的每個指令,是一個比較耗時的操做。
verify步驟能夠看這篇文章:微信 Android 熱補丁實踐演進之路
咱們能夠經過 Hook 來去掉 verify 這個步驟,這對啓動速度有幾十毫秒的優化。不過我想說,其實最大的優化場景在於首次和覆蓋安裝時。以 Dalvik 平臺爲例,一個 2MB 的 Dex 正常須要 350 毫秒,將 classVerifyMode 設爲 VERIFY_MODE_NONE 後,只須要150 毫秒,節省超過 50% 的時間。
// Dalvik Globals.h
gDvm.classVerifyMode = VERIFY_MODE_NONE;
// Art runtime.cc
verify_ = verifier::VerifyMode::kNone;複製代碼
可是 ART 平臺要複雜不少,Hook 須要兼容幾個版本。並且在安裝時大部分 Dex 已經優化好了,去掉 ART 平臺的 verify 只會對動態加載的 Dex 帶來一些好處。Atlas 中的 dalvik_hack-3.0.0.5.jar 能夠經過下面的方法去掉 verify,可是當前沒有支持 ART 平臺。
AndroidRuntime runtime = AndroidRuntime.getInstance();
runtime.init(context);
runtime.setVerificationEnabled(false);複製代碼
這個黑科技能夠大大下降首次啓動的速度,代價是對後續運行會產生輕微的影響。同時也要考慮兼容性問題,暫時不建議在 ART 平臺使用。
保活
講到黑科技,你可能第一個想到的就是保活。保活能夠減小 Application 建立跟初始化的時間,讓冷啓動變成溫啓動。不過在 Target 26 以後,保活的確變得愈來愈難。對於大廠來講,可能須要尋求廠商合做的機會。
插件化和熱修復
它們真的那麼好嗎?事實上大部分的框架在設計上都存在大量的 Hook 和私有 API 調用,帶來的缺點主要有兩個:
性能。Android Runtime 每一個版本都有不少的優化,由於插件化和熱修復用到的一些黑科技,致使底層 Runtime 的優化咱們是享受不到的。Tinker 框架在加載補丁後,應用啓動速度會下降 5%~10%。
總的來講,對於黑科技咱們須要慎重,當你足夠了解它們內部的機制之後,能夠選擇性的使用。
以上就是本人學習過程當中對啓動優化相關內容的總結,謝謝你們可以閱讀到這裏。
啓動優化,是一項長期的任務,任重而道遠。
開發者要未雨綢繆,在編碼過程當中儘可能減小給啓動帶來性能損耗的工做,主要注意如下幾個事項:
至於「啓動優化進階方法」小節中總結的優化方法要慎重選擇使用,由於這些方法或多或少會帶來一些很差影響。咱們在使用這些方法以前,要足夠了解他們的內部實現機制,作好評估工做,進行選擇性使用。
最後附上幾篇好文章幫助理解:
預覽窗口:顯示Activity的啓動窗口
Interdex的介紹: Redex 初探與 Interdex:Andorid 冷啓動優化
加載類過程 verify class 步驟的介紹: 微信 Android 熱補丁實踐演進之路
資源文件重排:支付寶 App 構建優化解析:經過安裝包重排布優化 Android 端啓動性能
插件化與熱修復:Android熱修復,沒你想的那麼難
初到掘金,人生地不熟,喜歡的朋友,點個贊鼓勵下新手唄~