Android性能優(yōu)化之啟動優(yōu)化

應用啟動類型

  1. 冷啟動
    場景:開機后第一次啟動應用 或者 應用被殺死后再次啟動
    生命周期:Process.start->Application創(chuàng)建->attachBaseContext->onCreate->onStart->onResume->Activity生命周期
    啟動速度:在幾種啟動類型中最慢锡移,也是我們優(yōu)化啟動速度最大的攔路虎

  2. 溫啟動
    場景:應用已經啟動续徽,返回鍵退出
    生命周期:onCreate->onStart->onResume->Activity生命周期
    啟動速度:較快

  3. 熱啟動
    場景: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進行交互了。


image.png

從冷啟動的流程看拆内,我們無法干預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 中做泥从,需要梳理清楚當前啟動過程正在運行的每一個模塊,哪些是一定需要的沪摄、哪些可以砍掉躯嫉、哪些可以懶加載纱烘。但是需要注意的是,懶加載要防止集中化祈餐,否則容易出現首頁顯示后用戶無法操作的情形擂啥。總的來說帆阳,用以下四個維度分整理啟動的各個點:

  • 必要且耗時:啟動初始化哺壶,考慮用線程來初始化。
  • 必要不耗時:首頁繪制蜒谤。
  • 非必要但耗時:數據上報山宾、插件初始化。
  • 非必要不耗時:不用想鳍徽,這塊直接去掉资锰,在需要用的時再加載。

把數據整理出來后阶祭,按需實現加載邏輯绷杜,采取分步加載、異步加載濒募、延期加載策略鞭盟,如下圖所示:


image.png

一句話概述,要提高應用的啟動速度瑰剃,核心思想是在啟動過程中少做事情齿诉,越少越好。

業(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)化嘀韧,特別是防止主線程出現長時間的空轉。

image.png

特別是現在有很多啟動框架缠捌,會使用 Pipeline 機制锄贷,根據業(yè)務優(yōu)先級規(guī)定業(yè)務初始化時機。比如微信內部使用的 <u>mmkernel</u> 曼月、阿里最近開源的 <u>Alpha</u> 啟動框架谊却,它們?yōu)楦鱾€任務建立依賴關系,最終構成一個有向無環(huán)圖哑芹。對于可以并發(fā)的任務炎辨,會通過線程池最大程度提升啟動速度。如果任務的依賴關系沒有配置好绩衷,很容易出現下圖這種情況蹦魔,即主線程會一直等待 taskC 結束,空轉 2950 毫秒咳燕。
image.png

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)化

布局越復雜,測量布局繪制的時間就越長署辉。主要做到以下幾點:

  1. 布局的層級越少族铆,加載速度越快。
  2. 一個控件的屬性越少哭尝,解析越快哥攘,刪除控件中的無用屬性。
  3. 使用<ViewStub/>標簽加載一些不常用的布局刚夺,做到使用時在加載献丑。
  4. 使用<merge/>標簽減少布局的嵌套層次。
  5. 盡可能少用wrap_content侠姑,wrap_content會增加布局measure時的計算成本,已知寬高為固定值時箩做,不用wrap_content莽红。

總結


以上就是本人學習過程中對啟動優(yōu)化相關內容的總結,謝謝大家能夠閱讀到這里邦邦。

啟動優(yōu)化安吁,是一項長期的任務,任重而道遠燃辖。

開發(fā)者要未雨綢繆鬼店,在編碼過程中盡量減少給啟動帶來性能損耗的工作,主要注意以下幾個事項:

  • 盡量避免啟動時在主線程做密集繁重的工作黔龟,如:避免 I/O 操作妇智、反序列化、網絡操作氏身、鎖等待等巍棱。
  • 對模塊以及第三方庫按需加載,采取分步加載蛋欣、異步加載航徙、延期加載等策略。
  • 利用線程池管理線程陷虎,避免創(chuàng)建大量線程到踏,造成 CPU 競爭,導致主線程時間片減少尚猿。
  • 啟動過程中窝稿,盡量避免頻繁創(chuàng)建的大量對象,減少 GC 給啟動性能帶來的卡頓影響谊路。
  • 盡量避免在啟動過程中調用阻塞性的系統(tǒng)調用讹躯。
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子潮梯,更是在濱河造成了極大的恐慌骗灶,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,589評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件秉馏,死亡現場離奇詭異耙旦,居然都是意外死亡,警方通過查閱死者的電腦和手機萝究,發(fā)現死者居然都...
    沈念sama閱讀 93,615評論 3 396
  • 文/潘曉璐 我一進店門免都,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人帆竹,你說我怎么就攤上這事绕娘。” “怎么了栽连?”我有些...
    開封第一講書人閱讀 165,933評論 0 356
  • 文/不壞的土叔 我叫張陵险领,是天一觀的道長。 經常有香客問我秒紧,道長绢陌,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,976評論 1 295
  • 正文 為了忘掉前任熔恢,我火速辦了婚禮脐湾,結果婚禮上,老公的妹妹穿的比我還像新娘叙淌。我一直安慰自己秤掌,他們只是感情好,可當我...
    茶點故事閱讀 67,999評論 6 393
  • 文/花漫 我一把揭開白布凿菩。 她就那樣靜靜地躺著机杜,像睡著了一般枚碗。 火紅的嫁衣襯著肌膚如雪东揣。 梳的紋絲不亂的頭發(fā)上揩页,一...
    開封第一講書人閱讀 51,775評論 1 307
  • 那天立轧,我揣著相機與錄音卷雕,去河邊找鬼拒逮。 笑死磅甩,一個胖子當著我的面吹牛糊识,可吹牛的內容都是我干的玷氏。 我是一名探鬼主播堵未,決...
    沈念sama閱讀 40,474評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼盏触!你這毒婦竟也來了渗蟹?” 一聲冷哼從身側響起块饺,我...
    開封第一講書人閱讀 39,359評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎雌芽,沒想到半個月后授艰,有當地人在樹林里發(fā)現了一具尸體,經...
    沈念sama閱讀 45,854評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡世落,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,007評論 3 338
  • 正文 我和宋清朗相戀三年淮腾,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片屉佳。...
    茶點故事閱讀 40,146評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡谷朝,死狀恐怖,靈堂內的尸體忽然破棺而出武花,到底是詐尸還是另有隱情圆凰,我是刑警寧澤,帶...
    沈念sama閱讀 35,826評論 5 346
  • 正文 年R本政府宣布体箕,位于F島的核電站送朱,受9級特大地震影響,放射性物質發(fā)生泄漏干旁。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,484評論 3 331
  • 文/蒙蒙 一炮沐、第九天 我趴在偏房一處隱蔽的房頂上張望争群。 院中可真熱鬧,春花似錦大年、人聲如沸换薄。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,029評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽轻要。三九已至,卻和暖如春垦缅,著一層夾襖步出監(jiān)牢的瞬間冲泥,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,153評論 1 272
  • 我被黑心中介騙來泰國打工壁涎, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留凡恍,地道東北人。 一個月前我還...
    沈念sama閱讀 48,420評論 3 373
  • 正文 我出身青樓怔球,卻偏偏與公主長得像嚼酝,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子竟坛,可洞房花燭夜當晚...
    茶點故事閱讀 45,107評論 2 356

推薦閱讀更多精彩內容