假如咱們去到一家餐廳,叫了半天都沒有人過來點菜,那等不了多久就沒耐心想走了。html
對於 App 也是同樣的,若是咱們打開一個應用半天都打不開,那很快的咱們也會失去耐心。python
啓動速度是用戶對咱們應用的第一體驗,用戶只有啓動咱們的應用才能使用咱們應用中的功能。android
就算咱們應用內部設計得再精美,其餘性能優化地再好,若是打開速度很慢的話,用戶對咱們應用的第一印象仍是不好。git
你能夠追求完美,要作到應用在 1 毫秒內啓動。github
可是通常狀況下, 咱們只要作到超越競品或者遠超競品,就能在啓動速度這一個點上讓用戶滿意。瀏覽器
用戶選擇 App 的時候會考慮各類因素,而咱們 App 開發者能作的就是在爭取經過各類技術讓咱們的 App 從衆多競品中脫穎而出。性能優化
這篇文章分爲三部分。服務器
第一部分:啓動優化基礎多線程
第一部分是第 1 大節,講的是應用啓動流程的相關知識。併發
第二部分:啓動優化方法
第二部分是第 2~4 大節,講的是經常使用的優化啓動速度的工具和方法。
第三部分:優化方法改進
第三部分是第 5~7 大節,講的是常規優化啓動方法的改進型解決方案。
啓動速度對 App 的總體性能很是重要,因此谷歌官方給出了一篇啓動速度優化的文章。
在這篇文章中,把啓動分爲了三種狀態:熱啓動、暖啓動和冷啓動。
下面咱們來看下三種啓動狀態的特色。
熱啓動是三種啓動狀態中是最快的一種,由於熱啓動是從後臺切到了前臺,不須要再建立 Applicaiton,也不須要再進行渲染布局等操做。
暖啓動的啓動速度介於冷啓動和熱啓動之間,暖啓動只會重走 Activity 的生命週期,不會重走進程建立和 Application 的建立和生命週期等。
冷啓動經歷了一系列流程,耗時也是最多的,理解冷啓動總體流程的理解,能夠幫助咱們尋找以後的一個優化方向。
冷啓動也是優化的衡量標準,通常在線上進行的啓動優化都是以冷啓動速度爲指標的。
啓動速度的優化方向是 Application 和 Activity 生命週期階段,這是咱們開發者能控制的時間,其餘階段都是系統作的。
冷啓動流程能夠分爲三步:建立進程、啓動應用和繪製界面。
建立進程
建立進程階段主要作了下面三件事,這三件事都是系統作的。
啓動應用
啓動應用階段主要作了下面三件事,從這些開始,隨後的任務和咱們本身寫的代碼有必定的關係。
繪製界面
繪製界面階段主要作了下面三件事。
上一節介紹了三種啓動狀態,這一節咱們來看一下經常使用的兩種測量啓動時間的方法:命令測量和埋點測量。
命令測量指的是用 adb 命令測量啓動時間,經過下面兩步就能實現 adb 命令測量應用啓動時間
咱們在終端中輸入一條 adb 命令打開咱們要測量的應用,打開後系統會輸出應用的啓動時間。
下面就是測量啓動時間的 adb 命令。
首屏 Activity 也要加上包名,好比下面這樣的。
上面是命令執行完成後顯示的內容,在輸出中能夠看到三個值:ThisTime、TotalTime 和 WaitTime。
下面咱們來看下這三個值分別表明什麼。
ThisTime
ThisTime 表明最後一個 Activity 啓動所須要的時間,也就是最後一個 Activity 的啓動耗時。
TotalTime
TotalTime 表明全部 Activity 啓動耗時,在上面的輸出中,TotalTime 和 ThisTime 是同樣的,由於這個 Demo 沒有寫 Splash 界面。
也就是這個 App 打開了 Application 後就直接打開了 MainActivity 界面,沒有啓動其餘頁面。
WaitTime
WaitTime 是 AMS 啓動 Activity 的總耗時。
這三者之間的關係以下。
ThisTime <= TotalToime < WaitTime
埋點測量指的是咱們在應用啓動階段埋一個點,在啓動結束時再埋一個點,二者之間的差值就是 App 的啓動耗時。
經過下面三步能夠實現埋點測量。
使用埋點測量的第一步是定義一個記錄埋點工具類。
在這裏要注意的是,除了 System.currentTimeMillis() 之外,咱們還能夠用 SystemClock.currentThreadTimeMillis() 記錄時間。
經過 SystemClock 拿到的是 CPU 真正執行的時間,這個時間與下一大節要講的 Systrace 上記錄的時間點是同樣的。
使用埋點測量的第二步是記錄啓動時間。
開始記錄的位置放在 Application 的 attachBaseContext 方法中,attachBaseContext 是咱們應用能接收到的最先的一個生命週期回調方法。
計算啓動耗時的一個誤區就是在 onWindowFocusChanged 方法中計算啓動耗時。
onWindowFocusChanged 方法只是 Activity 的首幀時間,是 Activity 首次進行繪製的時間,首幀時間和界面完整展現出來還有一段時間差,不能真正表明界面已經展示出來了。
按首幀時間計算啓動耗時並不許確,咱們要的是用戶真正看到咱們界面的時間。
正確的計算啓動耗時的時機是要等真實的數據展現出來,好比在列表第一項的展現時再計算啓動耗時。
在 Adapter 中記錄啓動耗時要加一個布爾值變量進行判斷,避免 onBindViewHolder 方法被屢次調用致使沒必要要的計算。
命令測量優勢
線下使用方便
adb 命令測量啓動速度的方式在線下使用比較方便,並且這種方式還能用於測量競品。
命令測量缺點
不能帶到線上
若是一條 adb 命令帶到線上去,沒有 app 也沒有系統幫咱們執行這一條 adb 命令,咱們就拿不到這些數據,因此不能帶到線上。
不嚴謹和精確
不能精確控制啓動時間的開始和結束。
精確
手動打點的方式比較精確,由於咱們能夠精確控制開始和結束的位置。
可帶到線上
使用埋點測量進行用戶數據的採集,能夠很方便地帶到線上,把數據上報給服務器。
服務器能夠針對全部用戶上報的啓動數據,天天作一個整合,計算出一個平均值,而後對比不一樣版本的啓動速度。
經常使用的分析方法耗時的工具備 Systrace 和 Traceview,它們兩個是相互補充的關係,咱們要在不一樣的場景下使用不一樣的工具,這樣才能發揮工具的最大做用。
本節內容以下。
Traceview 能以圖形的形式展現代碼的執行時間和調用棧信息,並且 Traceview 提供的信息很是全面,由於它包含了全部線程。
Traceview 的使用能夠分爲兩步:開始跟蹤、分析結果。
下面咱們來看看這兩步的具體操做。
咱們能夠經過 Debug.startMethodTracing("輸出文件") 就能夠開始跟蹤方法,記錄一段時間內的 CPU 使用狀況。
當咱們調用了 Debug.stopMethodTracing() 中止跟蹤方法後,系統就會爲咱們生成一個文件,咱們能夠經過 Traceview 查看這個文件記錄的內容。
文件生成的位置在 Android/data/包名/files 下,下面咱們來看一個示例。
咱們在 Application 的 onCreate 方法的開頭開始追蹤方法,而後在結尾結束追蹤,在這裏只是對 BlockCanary 卡頓監測框架進行初始化。
startMethodTracing 方法真正調用的實際上是另外一個重載方法,在這個重載方法能夠傳入 bufferSize。
bufferSize 就是分析結果文件的大小,默認是 8 兆。
咱們能夠進行擴充,好比擴充爲 16 兆、32 兆等。
這個重載方法的第三個參數是標誌位,這個標誌位只有一個選項,就是 TRACE_COUNT_ALLOCS。
運行了程序後,有兩種方式能夠獲取到跟蹤結果文件。
第一種方式是經過下面的命令把文件拉到項目根目錄。
第二種方式是在 AS 右下方的文件資源管理器中定位到 /sdcard/android/data/包名/files/ 目錄下,而後本身找個地方保存。
咱們在 AS 中打開跟蹤文件 mytrace.trace 後,就能夠用 Profiler 查看跟蹤的分析結果。
在分析結果上比較重要的是 5 種信息。
代碼指定的時間範圍
這個時間範圍是咱們經過 Debug 類精確指定的
選中的時間範圍
咱們能夠拖動時間線,選擇查看一段時間內某條線程的調用堆棧
進程中存在的線程
在這裏能夠看到在指定時間範圍內進程中只有主線程和 BlockCanary 的線程,一共有 4 條線程。
調用堆棧
在上面的跟蹤信息中,我選中了 main,也就是主線程。
還把時間範圍縮小到了特定時間區域內,放大了這個時間範圍內主線程的調用堆棧信息
方法耗時
當咱們把鼠標放到某一個方法上的時候,咱們能夠看到這個方法的耗時,好比上面的 initBlockCanary 的耗時是 19 毫秒。
Systrace 結合了 Android 內核數據,分析了線程活動後會給咱們生成一個很是精確 HTML 格式的報告。
Systrace 提供的 Trace 工具類默認只能 API 18 以上的項目中才能使用,若是咱們的兼容版本低於 API 18,咱們可使用 TraceCompat。
Systrace 的使用步驟和 Traceview 差很少,分爲下面兩步。
首先在 Application 中調用 Systrace 的跟蹤方法。
而後鏈接設備,在終端中定位到 Android SDK 目錄下,好比個人 Android SDK 目錄在 /users/oushaoze/library/Android/sdk 。
這時候我打開 SDK 目錄下的 platform-tools/systrace 就能看到 systrace.py 的一個 python 文件。
Systrace 是一個 Python 腳本,輸入下面命令,運行 systrace ,開始追蹤系統信息。
這行命令附加了下面一些選項。
-t ...
-t 後面表示的是跟蹤的時間,好比上面設定的是 10 秒就結束。
-o ...
-o 後面表示把文件輸出到指定目錄下。
-a ...
-a 後面表示的是要啓動的應用包名
輸入完這行命令後,能夠看到開始跟蹤的提示。看到 Starting tracing 後能夠打開打開咱們的應用。
10 秒後,會看到 Wrote trace HTML file: ....。
上面這段輸出就是說追蹤完畢,追蹤到的信息都寫到 trace.html 文件中了,接下來咱們打開這個文件。
打開文件後咱們能夠看到上面這樣的一個視圖,在這裏有幾個須要特別關注的地方。
8 核
我運行 Systrace 的設備是 8 核的,因此這裏的 Kernel 下面是 8 個 CPU。
縮放
當咱們選中縮放後,縮放的方式是上下移動,不是左右移動。
移動
選擇移動後,咱們能夠拖動咱們往下查看其它進程的分析信息。
時間片使用狀況
時間片使用狀況指的是各個 CPU 在特定時間內的時間片使用狀況,當咱們用縮放把特定時間段內的時間片信息放大,咱們就能夠看到時間片是被哪一個線程佔用了。
運行中的進程
左側一欄除了各個內核外,還會顯示運行中的進程。
咱們往下移動,能夠看到 MyAppplication 進程的線程活動狀況。
在這個視圖上咱們主要關注三個點。
主線程
在這裏咱們主要關注主線程的運行了哪些方法
跟蹤的時間段
剛纔在代碼中設置的標籤是 AppOnCreate,在這裏就顯示了這個跟蹤時間段的標籤
耗時
咱們選中 AppOnCreate 標籤後,就能夠看到這個方法的耗時。
在 Slice 標籤下的耗時信息包括 Wall Duration 和 CPU Duration,下面是它們的區別。
Wall Duration
Wall Time 是執行這段代碼耗費的時間,不能做爲優化指標。
假如咱們的代碼要進入鎖的臨界區,若是鎖被其餘線程持有,當前線程就進入了阻塞狀態,而等待的時間是會被計算到 Wall Time 中的。
CPU Duration
CPU Duration 是 CPU 真正花在這段代碼上的時間,是咱們關心的優化指標。
在上面的例子中 Wall Duration 是 84 毫秒,CPU Duration 是 34 毫秒,也就是在這段時間內一共有 50 毫秒 CPU 是處於休息狀態的,真正執行代碼的時間只花了 34 毫秒。
Traceview 有兩個特色:可埋點、開銷大。
可埋點
Traceview 的好處之一是能夠在代碼中埋點,埋點後能夠用 CPU Profiler 進行分析。
由於咱們如今優化的是啓動階段的代碼,若是咱們打開 App 後直接經過 CPU Profiler 進行記錄的話,就要求你有單身三十年的手速,點擊開始記錄的時間要和應用的啓動時間徹底一致。
有了 Traceview,哪怕你是老年人手速也能夠記錄啓動過程涉及的調用棧信息。
開銷大
Traceview 的運行時開銷很是大,它會致使咱們程序的運行變慢。
之因此會變慢,是由於它會經過虛擬機的 Profiler 抓取咱們當前全部線程的全部調用堆棧。
由於這個問題,Traceview 也可能會帶偏咱們的優化方向。
好比咱們有一個方法,這個方法在正常狀況下的耗時不大,可是加上了 Traceview 以後可能會發現它的耗時變成了原來的十倍甚至更多。
Systrace 的兩個特色:開銷小、直觀。
開銷小
Systrace 開銷很是小,不像 Traceview,由於它只會在咱們埋點區間進行記錄。
而 Traceview 是會把全部的線程的堆棧調用狀況都記錄下來。
直觀
在 Systrace 中咱們能夠很直觀地看到 CPU 利用率的狀況。
當咱們發現 CPU 利用率低的時候,咱們能夠考慮讓更多代碼以異步的方式執行,以提升 CPU 利用率。
查看工具
Traceview 分析結果要使用 Profiler 查看。
Systrace 分析結果是在瀏覽器查看 HTML 文件。
埋點工具類
Traceview 使用的是 Debug.startMethodTracing()。
Systrace 用的是 Trace.beginSection() 和 TraceCompat.beginSection()。
經常使用的兩種優化方法有兩種,這兩種是能夠結合使用的。
第一種是閃屏頁,在視覺上讓用戶感受啓動速度快,第二種是異步初始化。
閃屏頁是優化啓動速度的一個小技巧,雖然對實際的啓動速度沒有任何幫助,可是能讓用戶感受比啓動的速度要快一些。
閃屏頁就是在 App 打開首屏 Activity 前,首先顯示一張圖片,這張圖片能夠是 Logo 頁,等 Activity 展現出來後,再把 Theme 變回來。
冷啓動的其中一步是建立一個空白 Window,閃屏頁就是利用這個空白 Window 顯示佔位圖。
經過下面四個步驟能夠實現閃屏頁。
第一步是在 drawable 目錄下建立一個 splash.xml 文件。
第二步是在 values/styles.xml 中定義一個 Splash 主題。
第三步是在清單文件中設置 Theme。
第四步是在調用 super.onCreate 方法前切換回來
咱們這一節來看一下怎麼用線程池進行異步初始化。
本節內容包括以下部分,
異步優化就是把初始化的工做分細分紅幾個子任務,而後讓子線程分別執行這些子任務,加快初始化過程。
若是你對怎麼在 Android 中實現多線程不瞭解,能夠看一下個人上一篇文章:探索 Android 多線程優化,在這篇文章中我對在 Android 使用多線程的方法作了一個簡單的介紹。
有些初始化代碼在子線程執行的時候可能會出現問題,好比要求在 onCreate 結束前執行完成。
這種狀況咱們能夠考慮使用 CountDownLatch 實現,實在不行的時候就保留這段初始化代碼在主線程中執行。
咱們可使用線程池來實現異步初始化,使用線程池須要注意的是線程池大小的設置。
線程池大小要根據不一樣的設備設置不一樣的大小,有的手機是四核的,有的是八核的,若是把線程池大小設爲固定數值的話是不合理的。
咱們能夠參考 AsyncTask 中設置的線程池大小,在 AsyncTask 中有 CPU_COUNT 和 CORE_POOL_SIZE。
CPU_COUNT
CPU_COUNT 的值是設備的 CPU 核數。
CORE_POOL_SIZE
CORE_POOL_SIZE 是線程池核心大小,這個值的最小值是 2,最大值是 Math.min(CPU_COUNT - 1, 4)。
當設備的核數爲 8 時,CORE_POOL_SIZE 的值爲 4,當設備核數爲 4 時,這個值是 3,也就是 CORE_POOL_SIZE 的最大值是 4。
在這裏咱們能夠參考 AsyncTask 的作法來設置線程池的大小,並把初始化的工做提交到線程池中。
上一節介紹了怎麼經過線程池處理初始化任務,這一節咱們看一下改進的異步初始化工具:啓動器(LaunchStarter)。
這一節的內容包括以下部分。
經過線程池處理初始化任務的方式存在三個問題。
代碼不夠優雅
假如咱們有 100 個初始化任務,那像上面這樣的代碼就要寫 100 遍,提交 100 次任務。
沒法限制在 onCreate 中完成
有的第三方庫的初始化任務須要在 Application 的 onCreate 方法中執行完成,雖然能夠用 CountDownLatch 實現等待,可是仍是有點繁瑣。
沒法實現存在依賴關係
有的初始化任務之間存在依賴關係,好比極光推送須要設備 ID,而 initDeviceId() 這個方法也是一個初始化任務。
啓動器的核心思想是充分利用多核 CPU ,自動梳理任務順序。
第一步是咱們要對代碼進行任務化,任務化是一個簡稱,好比把啓動邏輯抽象成一個任務。
第二步是根據全部任務的依賴關係排序生成一個有向無環圖,這個圖是自動生成的,也就是對全部任務進行排序。
好比咱們有個任務 A 和任務 B,任務 B 執行前須要任務 A 執行完,這樣才能拿到特定的數據,好比上面提到的 initDeviceId。
第三步是多線程根據排序後的優先級依次執行,好比咱們如今有三個任務 A、B、C。
假如任務 B 依賴於任務 A,這時候生成的有向無環圖就是 ACB,A 和 C 能夠提早執行,B 必定要排在 A 以後執行。
Head Task
Head Task 就是全部任務執行前要作的事情,在這裏初始化一些其餘任務依賴的資源,也能夠只是打個 Log。
Tail Task
Tail Task 可用於執行全部任務結束後打印一個 Log,或者是上報數據等任務。
Idle Task
Idle Task 是在程序空閒時執行的任務。
若是咱們不使用異步的方案,全部的任務都會在主線程執行。
爲了讓其餘線程分擔主線程的工做,咱們能夠把初始化的工做拆分紅一個個的子任務,採用併發的方式,使用多個線程同時執行這些子任務。
啓動器(LaunchStarter)使用了有向無環圖實現任務之間的依賴關係,具體的代碼能夠在本文最下方找到。
使用啓動器須要完成 3 個步驟。
下面咱們來看下這 3 個步驟的具體操做。
首先在項目根目錄的 build.gradle 中添加 jitpack 倉庫。
allprojects {
repositories {
// ...
maven { url 'https://jitpack.io' }
}
}
複製代碼
而後在 app 模塊的 build.gradle 中添加依賴
dependencies {
// 啓動器
implementation 'com.github.zeshaoaaa:LaunchStarter:0.0.1'
}
複製代碼
定義任務這個步驟涉及了幾個概念:MainTask、Task、needWait 和 run。
MainTask
MainTask 是須要在主線程執行的任務
Task
Task 就是在工做線程執行的任務。
needWait
InitWeexTask 中重寫了 needWait 方法,這個方法返回 true 表示 onCreate 的執行須要等待這個任務完成。
run
run() 方法中的代碼就是須要作的初始化工做
定義好了任務後,咱們就能夠開始任務了。
這裏須要注意的是,若是咱們的任務中有須要等待完成的任務,咱們能夠調用 TaskDispatcher 的 await() 方法等待這個任務完成,好比 InitWeexTask。
使用 await() 方法要注意的是這個方法要在 start() 方法調用後才能使用。
除了上面提到的等待功能之外,啓動器還支持任務之間存在依賴關係,下面咱們來看一個極光推送初始化任務的例子。
在這一節會講實現任務依賴關係的兩個步驟。
在這裏咱們定義兩個存在依賴關係的任務:GetDeviceIdTask 和 InitJPush Task。
首先定義 GetDeviceIdTask ,這個任務負責初始化設備 ID 。
而後定義InitJPushTask,這個任務負責初始化極光推送 SDK,InitJPushTask 在啓動器中是尾部任務 Tail Task。
InitJPushTask 依賴於 GetDeviceIdTask,因此須要重寫 dependsOn 方法,在 dependsOn 方法中建立一個 Class 列表,把想依賴的任務的 Class 添加到列表中並返回。
GetDeviceIdTask 和 InitJPushTask 這兩個任務都不須要等待 Application 的 onCreate 方法執行完成,因此咱們這裏不須要調用 TaskDispatcher 的 await 方法。
上面這兩個步驟就能實現經過啓動器實現任務之間的依賴關係。
在咱們應用的 Application 和 Activity 中可能存在部分優先級不高的初始化任務,咱們能夠考慮把這些任務進行延遲初始化,好比放在列表的第一項顯示出來後再進行初始化。
常規的延遲初始化方法有兩種:onPreDraw 和 postDelayed。
除了常規方法外,還有一種改進的延遲初始化方案:延遲啓動器。
本節包括以下內容。
onPreDraw
onPreDraw 指的是在列表第一項顯示後,在 onPreDraw 回調中執行初始化任務
postDelayed
經過 Handler 的 postDelayed 方法延遲執行初始化任務
延遲啓動器
這一節咱們來看下怎麼經過 OnPreDrawListener 把任務延遲到列表顯示後再執行。
下面是 onPreDraw 方式實現延遲初始化的 3 個步驟。
第一步先聲明一個 OnFeedShowCallback。
第二步是在 Adapter 中的第一條顯示的時候調用 onFeedShow() 方法。
第三步是在 Activity 中調用 setOnFeedCallback 方法。
直接在 onFeedShow 中執行初始化任務的弊端是有可能致使滑動卡頓。
若是咱們 onPreDraw 的方式延遲執行初始化任務,假如這個任務耗時是 2 秒,那就意味着在列表顯示第一條後的 2 秒內,列表是沒法滑動的,用戶體驗不好。
還有一種方式就是經過 Handler.postDelayed 方法發送一個延遲消息,好比延遲到 100 毫秒後執行。
假如在 Activity 中有 1 個 100 行的初始化方法,咱們把前 10 行代碼放在 postDelayed 中延遲 100 毫秒執行,把前 20 行代碼放在 postDelayed 中延遲 200 毫秒執行。
這種實現的確緩解了卡頓的狀況,可是這種實現存在兩個問題
不夠優雅
假如按上面的例子,能夠分出 10 個初始化任務,每個都放在 不一樣的 postDelayed 中執行,這樣寫出來的代碼不夠優雅。
依舊卡頓
假如把任務延遲 200 毫秒後執行,而 200 後用戶還在滑動列表,那仍是會發生卡頓。
除了上面說到的方式外,如今咱們來講一個更好的解決方案:延遲啓動器。
延遲啓動器利用了 IdleHandler 實現主線程空閒時才執行任務,IdleHandler 是 Android 提供的一個類,IdleHandler 會在當前消息隊列空閒時才執行任務,這樣就不會影響用戶的操做了。
假如如今 MessageQueue 中有兩條消息,在這兩條消息處理完成後,MessageQueue 會通知 IdleHandler 如今是空閒狀態,而後 IdleHandler 就會開始處理它接收到的任務。
DelayInitDispatcher 配合 onFeedShow 回調來使用效果更好。
下面是一段使用延遲啓動器 DelayInitDispatcher 執行初始化任務的示例代碼。
看完了上面提到的一些啓動優化技巧,你有沒有獲得一些啓發呢?
又或者是你有沒有本身的一些啓動優化技巧,不妨在評論區給你們說說。
可能你以爲不值一提的技巧,能解決了其餘同窗的一個大麻煩。