啟動速度優(yōu)化的本質(zhì)因素
應(yīng)用的速度優(yōu)化是我們使用最頻繁,也是應(yīng)用最重要的優(yōu)化之一,它包括啟動速度優(yōu)化、頁面打開速度優(yōu)化、功能或業(yè)務(wù)執(zhí)行速度優(yōu)化等等,能夠直接提升應(yīng)用的用戶體驗(yàn)。
大部分人談到速度優(yōu)化,只能想到一些零碎的優(yōu)化點(diǎn),比如使用多線程、預(yù)加載等等,沒有一個(gè)較為體系的優(yōu)化方案。
那么我們要怎么體系化的學(xué)習(xí)啟動優(yōu)化呢?能從哪些方面入手?
從底層來看,CPU、緩存、任務(wù)調(diào)度是決定應(yīng)用速度最本質(zhì)的因素,CPU 和緩存都屬于硬件層,任務(wù)調(diào)度機(jī)制則屬于操作系統(tǒng)層。速度優(yōu)化我們將圍繞這三個(gè)因素詳細(xì)說明優(yōu)化方案。
CPU 層面進(jìn)行速度優(yōu)化
所有的程序最終會被編譯成機(jī)器碼指令,然后交給 CPU 執(zhí)行,CPU 以流水線的形式一條條執(zhí)行程序的機(jī)器碼指令。當(dāng)我們想要提升某些場景(如啟動、打開頁面、滑動)等的速度時(shí),本質(zhì)上就是降低 CPU 執(zhí)行完這些場景指令的時(shí)間,這個(gè)時(shí)間簡稱為 CPU 時(shí)間。程序所消耗 CPU 時(shí)間的計(jì)算公式:CPU 時(shí)間 = 程序的指令數(shù) x 時(shí)鐘周期時(shí)間 x 每條指令的平均時(shí)鐘周期數(shù)。
-
程序的指令數(shù):就是程序編譯成機(jī)器碼指令后的指令數(shù)量
-
時(shí)鐘周期時(shí)間:每一次時(shí)鐘周期內(nèi),CPU 僅完成一次執(zhí)行,所以時(shí)鐘周期時(shí)間越短,CPU 執(zhí)行得越快。對時(shí)鐘周期時(shí)間這個(gè)概念可能不熟悉,但是它的倒數(shù)也就是 CPU 時(shí)鐘周期頻率肯定聽說過,1ns 的時(shí)鐘周期時(shí)間就是 1GHz 的時(shí)鐘周期頻率。這個(gè)指標(biāo)也是衡量 CPU 性能最重要的一個(gè)指標(biāo)
- 每條指令的平均時(shí)間周期:是指令執(zhí)行完畢所消耗的平均時(shí)間周期,指令不同所需的機(jī)器周期數(shù)也不同
從 CPU 來看,當(dāng)我們想要提升程序的速度時(shí),優(yōu)化這三項(xiàng)因子中的任何一項(xiàng)都可以達(dá)到目的。那基于這三項(xiàng)因子有哪些通用方案可以借鑒呢?
減少程序的指令數(shù)
通過減少程序的指令數(shù)來提升速度是我們最常用也是優(yōu)化方案最多的方式。下面這些方案都是通過減少指令數(shù)來提升速度的。
-
利用手機(jī)多核:從程序角度講其實(shí)就是多線程并發(fā),將要提速的場景的程序指令交給多個(gè) CPU 同時(shí)執(zhí)行,對于單個(gè) CPU 來說,需要執(zhí)行的指令數(shù)就變少了,CPU 時(shí)間自然就降低了
-
更簡潔的代碼邏輯和更優(yōu)的算法:同樣的功能用更簡潔或更優(yōu)的代碼來實(shí)現(xiàn),指令數(shù)也會減少,指令數(shù)少了程序的速度自然也就快了。可以用抓 trace 或者在函數(shù)前后統(tǒng)計(jì)耗時(shí)方式去分析,將耗時(shí)的方法用更優(yōu)的方式實(shí)現(xiàn)
-
減少 CPU 閑置:通過在 CPU 閑置的時(shí)候,執(zhí)行預(yù)創(chuàng)建 View、預(yù)準(zhǔn)備數(shù)據(jù)等預(yù)加載邏輯,在我們需要加速場景的指令數(shù)量由于預(yù)加載之行了一部分而變少了,自然也就快了
降低時(shí)鐘周期時(shí)間
想要降低手機(jī)的時(shí)鐘周期,一般只能通過升級 CPU 做到,每次新出一款 CPU,相比上一代不僅在時(shí)鐘周期時(shí)間上有優(yōu)化,每個(gè)周期內(nèi)可執(zhí)行的指令也都會有優(yōu)化,時(shí)鐘頻率周期越大處理速度越快。
雖然我們沒法降低設(shè)備的時(shí)鐘周期,但是應(yīng)該避免設(shè)備提高時(shí)鐘周期時(shí)間,也就是降頻現(xiàn)象,當(dāng)手機(jī)發(fā)熱發(fā)燙時(shí),CPU 往往都會通過降頻來減少設(shè)備的發(fā)熱現(xiàn)象,具體的方式就是通過合理的線程使用或者代碼邏輯優(yōu)化,來減少程序長時(shí)間超負(fù)荷的使用 CPU。
降低每條指令的平均時(shí)間周期
在降低每條指令的平均時(shí)間周期上,我們能做的其實(shí)也不多,因?yàn)樗?CPU 性能有很大的關(guān)系;除了 CPU 性能,以下幾個(gè)方面也會影響到指令的時(shí)間周期:
-
編程語言:Java 翻譯成機(jī)器碼后有更多的間接調(diào)用,所以比 C++ 代碼編譯成的機(jī)器碼指令的平均時(shí)間周期更長
-
編譯程序:一個(gè)好的編譯程序可以通過優(yōu)化指令來降低指令的平均時(shí)間周期
-
降低 IO 等待:嚴(yán)格來說,IO 等待的時(shí)間并不能算到指令執(zhí)行的耗時(shí)中,因?yàn)?CPU 在等待 IO 時(shí)會休眠或者去執(zhí)行其他任務(wù),但等待 IO 會使執(zhí)行完指令的時(shí)間變長
緩存層面進(jìn)行速度優(yōu)化
程序的指令并不是直接就能被 CPU 執(zhí)行的,CPU 在讀取指令時(shí)會經(jīng)過寄存器、高速緩存、主存,逐級將指令進(jìn)行緩存,層級越靠上執(zhí)行速度越快;當(dāng)然一個(gè)程序也不可能全是 CPU 計(jì)算邏輯,必然也會涉及到比如磁盤 IO 的操作或等待。所以緩存也是決定應(yīng)用速度的關(guān)鍵因素之一。
緩存對程序速度的影響主要體現(xiàn)在兩方面:
-
緩存的讀寫速度
-
緩存的命中率
緩存的讀寫速度
手機(jī)或電腦的存儲設(shè)備都被組織成了一個(gè)存儲器層次結(jié)構(gòu),在這個(gè)層次結(jié)構(gòu)中,從上至下設(shè)備的訪問速度越來越慢,但容量也越來越大,并且每字節(jié)的造價(jià)也越來越便宜。
高速緩存是屬于 CPU 的組成部分,并且實(shí)際有幾層高速緩存也是由 CPU 決定的。以下圖高通驍龍 888 的芯片為例,它是 8 塊核組成的 CPU,從架構(gòu)圖可以看到,它的 L2 是 1M 大小,L3 是 3M 大小,并且所有核共享。
不同層之間的讀寫速度差距是很大的,所以為了能提高場景的速度,我們需要將和核心場景相關(guān)的資源(代碼、數(shù)據(jù)等)盡量存儲在靠上層的存儲器中?;谶@一原理,便能衍生出非常多的優(yōu)化方案,比如 OkHttp、Fresco 等框架都會想盡辦法將數(shù)據(jù)緩存在內(nèi)存中,其次是磁盤,以此來提高速度。
緩存的命中率
在講解緩存讀取速度時(shí)有提及,緩存層級越靠上雖然訪問越快,但是容量越少,我們只能將有限的數(shù)據(jù)存放在緩存中,在這樣的制約下,提升緩存命中率往往是一件非常難的事情。
從系統(tǒng)層面,一個(gè)好的編譯器可以提升寄存器的命中率,一個(gè)好的操作系統(tǒng)可以提升高速緩存的命中率;從應(yīng)用層面,好的優(yōu)化方案可以提升主存和硬盤的命中率,比如 LruCache;應(yīng)用可以提升高速緩存的命中率,比如使用 redex 對應(yīng)用的 dex 中 class 文件重排來提升高速緩存讀取類文件時(shí)的命中率。
想要提高緩存命中率,一般都是利用局部性原理(局部性原理即數(shù)據(jù)被訪問,不久之后該數(shù)據(jù)或附近的存儲單元可能再次被訪問,將可能被再次訪問的數(shù)據(jù)提前加載)分析大概率事件等多種原理來提高緩存命中率。
任務(wù)調(diào)度層面進(jìn)行速度優(yōu)化
為了能同時(shí)運(yùn)行多個(gè)程序,所以誕生了虛擬內(nèi)存技術(shù),但只有虛擬內(nèi)存技術(shù)是不夠的,還需要任務(wù)調(diào)度機(jī)制,我們的程序才能獲得 CPU 的資源正常跑起來,所以 任務(wù)調(diào)度也是影響程序速度的本質(zhì)因素之一。
在 Linux 系統(tǒng)中,任務(wù)調(diào)度的維度是進(jìn)程,Java 線程也屬于輕量級的進(jìn)程,所以線程也是遵循 Linux 系統(tǒng)的任務(wù)調(diào)度規(guī)則。Linux 系統(tǒng)將進(jìn)程分為了實(shí)時(shí)進(jìn)程和普通進(jìn)程這兩類。
為了熟悉任務(wù)調(diào)度機(jī)制,我們需要熟悉調(diào)度機(jī)制的原理,以及進(jìn)程的生命周期。
通過實(shí)時(shí)進(jìn)程和普通進(jìn)程了解任務(wù)調(diào)度機(jī)制原理
1、實(shí)時(shí)進(jìn)程
Linux 系統(tǒng)對實(shí)時(shí)進(jìn)程的調(diào)度策略有兩種:先進(jìn)先出(SCHED_FIFO)和循環(huán)(SCHED_RR),Android 只使用了 SCHED_FIFO 策略調(diào)度實(shí)時(shí)進(jìn)程,我們主要介紹這個(gè)。
如果某個(gè)進(jìn)程占有 CPU 時(shí)間片,此時(shí)沒有更高優(yōu)先級的實(shí)時(shí)進(jìn)程搶占 CPU 或該進(jìn)程主動讓出,那么該進(jìn)程就始終保持使用 CPU 狀態(tài)。這種策略會提高進(jìn)程運(yùn)行的持續(xù)時(shí)間,減少被打斷或切換的次數(shù),所以響應(yīng)更及時(shí)。Android 中 Audio、SurfaceFlinger、Zygote 等系統(tǒng)核心進(jìn)程都是實(shí)時(shí)進(jìn)程。
2、普通進(jìn)程
Linux 系統(tǒng)對普通進(jìn)程采用了一種完全公平調(diào)度算法來實(shí)現(xiàn)對進(jìn)程的切換調(diào)度,算法細(xì)節(jié)我們不需要了解。
完全公平調(diào)度算法簡單理解就是,調(diào)度器根據(jù)進(jìn)程的運(yùn)行時(shí)間來進(jìn)行任務(wù)調(diào)度,進(jìn)程運(yùn)行時(shí)間并不是真實(shí)的物理運(yùn)行時(shí)間,而是通過 nice 值作為權(quán)值系數(shù)計(jì)算的虛擬時(shí)間。在同樣的物理時(shí)間內(nèi),nice 值越低進(jìn)程的運(yùn)行時(shí)間越少,運(yùn)行時(shí)間少更容易被調(diào)度器所選擇。
所以修改進(jìn)程優(yōu)先級 nice 值,nice 值越低代表優(yōu)先級越大,實(shí)際上就是 nice 值越低越容易被調(diào)度器選擇。
實(shí)時(shí)進(jìn)程和普通進(jìn)程的任務(wù)調(diào)度機(jī)制的結(jié)合就是系統(tǒng)任務(wù)調(diào)度機(jī)制的原理。
進(jìn)程生命周期
了解了進(jìn)程的調(diào)度原理,再來了解下進(jìn)程的生命周期。
線程實(shí)際上就是輕量級的進(jìn)程,有以下幾種狀態(tài):
-
運(yùn)行:該進(jìn)程此刻正在運(yùn)行
-
等待:進(jìn)程能夠運(yùn)行,但 CPU 分配給另一個(gè)進(jìn)程,調(diào)度器可以在下一次任務(wù)切換時(shí)選擇該進(jìn)程
-
睡眠:進(jìn)程正在睡眠無法運(yùn)行,因?yàn)樗诘却粋€(gè)外部事件,調(diào)度器無法在下一次任務(wù)切換時(shí)選擇該進(jìn)程
-
終止:進(jìn)程終止
遵循以上的調(diào)度原理和規(guī)則,任務(wù)調(diào)度層面優(yōu)化應(yīng)用場景的速度實(shí)際上就是線程的優(yōu)化。主要可以從以下兩方面優(yōu)化:
-
提高線程優(yōu)先級:關(guān)鍵線程選擇提高線程優(yōu)先級,還可以將關(guān)鍵線程綁定 CPU 大核的方式提高線程的執(zhí)行效率
-
減少線程創(chuàng)建或者狀態(tài)切換的耗時(shí):簡單理解就是合理使用線程池,根據(jù)場景合理設(shè)置線程池的常駐線程數(shù)量、線程存活時(shí)間等參數(shù)減少線程頻繁創(chuàng)建和切換
CPU 優(yōu)化
合理使用線程池提升 CPU 利用率
在 Java 中創(chuàng)建子線程是使用 Thread,但是我們一般都不會直接使用它們,而是通過 線程池 的方式創(chuàng)建線程統(tǒng)一管理。Java 同樣提供了Executors 線程池管理工具幫助我們管理線程,但一般我們不會直接使用 Executors 提供的線程池,而是會自定義線程池,更好的管理線程。
合理使用線程應(yīng)該符合這幾個(gè)條件:
-
線程不能太多也不能太少:線程太多會浪費(fèi) CPU 資源用于任務(wù)調(diào)度上,并且會減少核心線程在單位時(shí)間內(nèi)所能消耗的 CPU 資源;線程太少則發(fā)揮不出 CPU 的性能,浪費(fèi)了 CPU 資源
-
減少線程創(chuàng)建及狀態(tài)切換導(dǎo)致的 CPU 損耗:線程頻繁的創(chuàng)建、銷毀、狀態(tài)切換(如休眠切換到運(yùn)行狀態(tài)或運(yùn)行狀態(tài)切換到休眠),都是對 CPU 資源的損耗
如何在使用線程的時(shí)候符合上面講的兩個(gè)條件呢?可以盡量做到兩點(diǎn):
-
收斂線程:排查野線程和各個(gè)業(yè)務(wù)的自定義線程池
-
使用線程池:線程都從線程池創(chuàng)建,并且正確使用線程池
線程池類型
要正確使用線程池,針對不同的場景主要可以分為 CPU 密集型場景和 IO 密集型場景即 CPU 線程池和 IO 線程池。
-
CPU 線程池:用來處理 CPU 類型任務(wù),如計(jì)算、邏輯操作、UI 渲染等
-
IO 線程池:用來處理 IO 類型任務(wù),如拉取網(wǎng)絡(luò)數(shù)據(jù)、往磁盤讀寫數(shù)據(jù)等
線程池配置主要考慮四個(gè)參數(shù):
-
corePoolSize:核心線程數(shù)量,或者叫常駐線程數(shù)量
-
maximumPoolSize:最大線程數(shù)量
-
keepAliveTime:非核心線程存活時(shí)間
-
workQueue:任務(wù)隊(duì)列
還有 threadFactory 線程工廠和 RejectedExecutionHandler 異常處理兜底參數(shù)。
不同的場景使用不同的線程池配置。那么具體該怎么配置上面四個(gè)參數(shù)呢?
CPU 線程池配置
1、corePoolSize
一般設(shè)置為 CPU 的核心數(shù),理想情況下等于核心數(shù)的線程數(shù)量性能是最高的,因?yàn)榧饶艹浞职l(fā)揮 CPU 的性能,還減少了頻繁調(diào)度導(dǎo)致的 CPU 損耗。不過實(shí)際運(yùn)行過程中無法達(dá)到理想情況,所以將 核心線程數(shù)設(shè)置為 CPU 核心數(shù)可能不是最優(yōu)的,但絕對是最穩(wěn)妥且相對較優(yōu)的方案。
2、maximumPoolSize
對于 CPU 線程池來說,最大線程數(shù)就是核心線程數(shù),因?yàn)?CPU 的最大利用率就是每個(gè)核都滿載,想要達(dá)到滿載只需要核心數(shù)個(gè)并發(fā)線程就行了,能夠完全發(fā)揮出 CPU 資源,再多的線程只會增加 CPU 調(diào)度的損耗。
3、keepAliveTime
因?yàn)樽畲缶€程數(shù)就是核心線程數(shù),keepAliveTime 是設(shè)置的非核心線程的存活時(shí)間可以是 0。
4、workQueue
CPU 線程池中統(tǒng)一使用 LinkBlockingQueue,這是一個(gè)可以設(shè)置容量并支持并發(fā)的隊(duì)列。當(dāng)線程較多核心線程數(shù)處理不來時(shí),任務(wù)會到隊(duì)列中等待,所以隊(duì)列不能太小避免新來的任務(wù)進(jìn)入到錯(cuò)誤兜底的處理邏輯中。我們可以將隊(duì)列設(shè)置成無限大,但如果想要追求更好的程序穩(wěn)定性則不建議這樣做了。
如果需要對創(chuàng)建的線程重命名或?qū)€程棧大小做限制,可以在 threadFactory 線程工廠自定義處理。
假設(shè)隊(duì)列設(shè)置有限數(shù)量例如 64 個(gè),如果線程池使用出現(xiàn)異常時(shí)導(dǎo)致進(jìn)入異常兜底 RejectedExecutionHandler,可以在這里設(shè)置監(jiān)控及時(shí)發(fā)現(xiàn)異常。
以上配置邏輯實(shí)際上就是 Executors 工具類提供的 newFixedThreadPool 線程池,其實(shí)它就是 CPU 線程池:
IO 線程池配置
IO 任務(wù)消耗 CPU 資源是非常少的,當(dāng)我們處理 IO 數(shù)據(jù)時(shí)會交給 DMA(直接存儲器訪問)芯片去做,此時(shí)調(diào)度器就會把 CPU 資源切換給其他的線程去使用。因?yàn)?IO 任務(wù)對 CPU 資源消耗少,所以每來一個(gè) IO 任務(wù)就直接啟動一個(gè)線程去執(zhí)行它就行了,不需要放入隊(duì)列中,即使此時(shí)執(zhí)行了非常多的 IO 任務(wù),也都是 DMA 芯片在處理,和 CPU 無關(guān)。了解了這一特性,我們再來看看 IO 線程池如何配置。
1、corePoolSize
核心線程數(shù)量沒有具體規(guī)定,要根據(jù) app 的類型對應(yīng)場景配置數(shù)量。比如 IO 任務(wù)比較多的新聞咨詢類應(yīng)用或者大型應(yīng)用,可以設(shè)置多一些比如十幾個(gè),太少了會因?yàn)?IO 線程創(chuàng)建和銷毀頻繁產(chǎn)生損耗。如果應(yīng)用 IO 任務(wù)較少,可以直接設(shè)置為 0 個(gè)。
2、maximumPoolSize
最大線程數(shù)量可以多設(shè)置一些,確保每個(gè) IO 任務(wù)都能有線程執(zhí)行,畢竟 IO 任務(wù)對 CPU 消耗不高。一般來說,中小型應(yīng)用設(shè)置 60 個(gè)就足夠,大型應(yīng)用可以設(shè)置 100 個(gè)以上,但不建議設(shè)置特別大,防止程序出現(xiàn)異常創(chuàng)建大量 IO 線程,線程的創(chuàng)建和銷毀是消耗 CPU 資源的。
3、keepAliveTime
非核心線程閑置存活時(shí)間一般設(shè)置 60s,這個(gè)時(shí)間即能讓閑置線程復(fù)用效率較高,也能保證不會頻繁銷毀線程后又重新創(chuàng)建消耗系統(tǒng)資源。
4、workQueue
因?yàn)?IO 任務(wù)的特性是來一個(gè)任務(wù)處理一個(gè),所以 對于 IO 線程池而言是不需要等待隊(duì)列的,可以傳入 SynchronousQueue,它是一個(gè)容量為 0 的隊(duì)列。
以上配置邏輯實(shí)際上就是 Executors 工具類提供的 newCacheThreadPool 線程池,其實(shí)它就是 IO 線程池:
不過 newCacheThreadPool 設(shè)置的最大線程數(shù)是無限大,這里建議還是自己創(chuàng)建 IO 線程池,并且在設(shè)置 IO 線程池的線程優(yōu)先級時(shí),需要比 CPU 線程池的線程優(yōu)先級高一些,因?yàn)?IO 線程的任務(wù)不怎么消耗 CPU 資源,優(yōu)先級高一些可以避免得不到調(diào)度的情況出現(xiàn)。
確認(rèn)任務(wù)屬于哪種類型
那么怎么確認(rèn)任務(wù)是要放在 CPU 線程池執(zhí)行還是 IO 線程池執(zhí)行呢?
我們可以通過插樁(AspectJ、ASM、Javaassit 等)將 Runnable 的 run() 執(zhí)行時(shí)間以及對應(yīng)的線程池、線程名稱打印出來,如果任務(wù)耗時(shí)較久,還是在 CPU 線程池執(zhí)行的,那就要考慮該任務(wù)是否需要放在 IO 線程池去執(zhí)行了。
減少 CPU 閑置提升利用率
除了游戲類食品類應(yīng)用,很少有應(yīng)用會持續(xù)以較高的狀態(tài)消耗 CPU,大部分情況下 CPU 都可能處于閑置狀態(tài)。我們可以把核心場景運(yùn)行時(shí)需要執(zhí)行的任務(wù)或者數(shù)據(jù)放在閑置時(shí)提前預(yù)加載,能充分利用 CPU 閑置時(shí)刻又不會搶占核心場景 CPU 資源,減少其他場景 CPU 執(zhí)行的指令數(shù),提升速度。
我們可以啟動一個(gè)定時(shí)任務(wù)定時(shí)比如每 5s 檢測 CPU 是否已經(jīng)閑置,如果已經(jīng)閑置就通知各個(gè)業(yè)務(wù)預(yù)加載執(zhí)行任務(wù)。那么怎么知道 CPU 處于閑置?
檢測 CPU 閑置有兩種方案:
-
讀取 proc 文件節(jié)點(diǎn)下的 CPU 數(shù)據(jù)判斷 CPU 是否閑置
-
times 函數(shù)判斷 CPU 是否閑置
考慮性能及準(zhǔn)確率,在這里推薦使用 native 的 times 函數(shù)判斷 CPU 是否閑置。不過兩種方式都會講解下。
讀取 proc 文件判斷 CPU 閑置
在 Linux 系統(tǒng)上,設(shè)備和應(yīng)用的大部分信息和數(shù)據(jù)都會記錄在 proc 目錄下的某個(gè)文件中,CPU 數(shù)據(jù)同樣可以在 proc 目錄下的文件獲取。主要會涉及兩個(gè)文件:/proc/stat
和 /proc/pid/stat
,字段意義具體可以查看 Linux proc 文檔,CPU 使用率計(jì)算。
讀取 /proc/stat 獲取 CPU 總運(yùn)行時(shí)間
首先看下 /proc/stat
節(jié)點(diǎn)文件:
$ adb shell cat /proc/stat
cpu 17742 2886 22371 1720255 577 0 295 0 0 0
cpu0 4786 582 6057 428486 101 0 254 0 0 0
cpu1 3375 466 5224 432181 183 0 10 0 0 0
cpu2 4447 916 5970 429892 166 0 15 0 0 0
cpu3 5133 920 5119 429695 125 0 15 0 0 0
intr 4076330 0 0 0 952094 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 23 2979 12779 1 19744 1 18 1 0 119018 0 0 0 0 0 0 0 0 0 0 0 0 0 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 541
ctxt 9551253
btime 1688788408
processes 17971
procs_running 1
procs_blocked 0
softirq 1297436 43240 320646 10 4133 0 0 202342 344748 0 382317
上面的字段我們只需要關(guān)注標(biāo)有 cpu 的即可,后面的數(shù)字是,從系統(tǒng)啟動到當(dāng)前時(shí)刻,不同維度累計(jì)下的 CPU 時(shí)間:
1.CPU 名稱 | 2.user | 2.nice | 4.system | 5.idle | 6.iowait | 7.irq | 8.soft_irq | 9. steal、guest、guest_nice |
---|---|---|---|---|---|---|---|---|
用戶態(tài)累積消耗的 CPU 時(shí)間 | nice 值為負(fù)的進(jìn)程所累積消耗的 CPU 時(shí)間 | 內(nèi)核態(tài)累積消耗的 CPU 時(shí)間 | 除 IO 等待時(shí)間以外的其他等待時(shí)間 | 累積 IO 等待時(shí)間(這個(gè)時(shí)間是不準(zhǔn)確的,因?yàn)樾枰?IO 等待時(shí),調(diào)度器會將 CPU 切換給其他任務(wù)使用 | 累計(jì)的硬中斷時(shí)間 | 累計(jì)的軟中斷時(shí)間 | 保留字段 | |
cpu(所有核累加) | 17742 | 2886 | 22371 | 1720255 | 577 | 0 | 295 | 0 0 0 |
cpu0(第一個(gè)核) | … | … | … | … | … | … | … | … |
… | … | … | … | … | … | … | … | … |
cpu3(第四個(gè)核) | … | … | … | … | … | … | … | … |
有了上面的數(shù)據(jù)后,我們只需要 將表格中第一行 CPU 數(shù)據(jù)中的 2 到 8 列數(shù)據(jù)累加起來,就是 CPU 的總運(yùn)行時(shí)間了,CPU 總運(yùn)行時(shí)間 = user + system + nice + idle + iowait + irq + soft_irq。
讀取 /proc/pid/stat 獲取進(jìn)程 CPU 消耗時(shí)間
獲取了 CPU 總運(yùn)行時(shí)間,接下來獲取應(yīng)用的 CPU 消耗時(shí)間。
首先是獲取應(yīng)用的 pid:
adb shell ps -l // 假設(shè)應(yīng)用 com.example.demo 的 pid 查詢結(jié)果為 22419
查看 /proc/pid/stat
:
adb shell cat /proc/22419/stat
上面數(shù)據(jù)也是比較多,我們只需要關(guān)注從左到右算第 14 項(xiàng) utime 加上 第 15 項(xiàng) stime,就是這個(gè)進(jìn)程的 CPU 消耗時(shí)間,進(jìn)程 CPU 消耗時(shí)間 = utime + stime。
通過 CPU 總運(yùn)行時(shí)間和進(jìn)程 CPU 消耗時(shí)間計(jì)算進(jìn)程 CPU 使用率
有了 CPU 的總運(yùn)行時(shí)間和進(jìn)程的 CPU 消耗時(shí)間,就可以計(jì)算 CPU 使用率了。
CPU 使用率的計(jì)算限定在一定時(shí)間范圍內(nèi),比如在 5-60s 之間(時(shí)間不宜太長)如果 CPU 使用率低,就說明應(yīng)用沒怎么用 CPU 處于閑置。
如果我們預(yù)加載任務(wù)比較多,時(shí)間可以縮短一些,比如 5s 檢測一次,這 5s 內(nèi)如果 CPU 是閑置的,就執(zhí)行預(yù)加載任務(wù)。但需要注意預(yù)加載任務(wù)需要打散,也就是每個(gè)閑置周期不能執(zhí)行太多的預(yù)加載任務(wù),避免所有預(yù)加載任務(wù)都一次執(zhí)行而導(dǎo)致 CPU 過載。
計(jì)算進(jìn)程 CPU 占用率經(jīng)過以下三個(gè)步驟:
-
讀取
/proc/stat
節(jié)點(diǎn)獲取 CPU 總運(yùn)行時(shí)間 -
讀取
/proc/pid/stat
文件獲取進(jìn)程的 CPU 消耗時(shí)間 -
計(jì)算 CPU 占用率 = 應(yīng)用的 CPU 消耗時(shí)間 / CPU 總運(yùn)行時(shí)間。
需要注意的是,CPU 占用率還要依據(jù) CPU 核數(shù)設(shè)定閥值,因?yàn)榧僭O(shè) 8 核 CPU 極限 CPU 占用率可以接近 800%,4 核 CPU 極限 CPU 占用率可以接近 400%。CPU 占用率低于 30% 閥值就可以認(rèn)為已經(jīng)閑置,性能差的設(shè)備閥值可以設(shè)置低一點(diǎn),但都要基于 CPU 核數(shù)為基礎(chǔ)設(shè)置。
Timer().also {
it.schedule(object : TimerTask() {
override fun run() {
val cpuUsage = getCpuUsage()
Log.v("@@@", "cpuUsage = $cpuUsage")
}
}, 0, 5000)
}
private val cpuCount = Runtime.getRuntime().availableProcessors()
private var procStatFile: RandomAccessFile? = null
private var appStatFile: RandomAccessFile? = null
private var lastTotalCpuTime: Double = 0.0
private var lastAppCpuTime: Double = 0.0
private fun getCpuUsage(): Double {
val totalCpuTime = getTotalCpuTime()
val appCpuTime = getAppCpuTime()
Log.v("@@@", "totalCpuTime = $totalCpuTime, appCpuTime = $appCpuTime, cpuCount = $cpuCount")
// CPU 使用率百分比
return (100 * (appCpuTime / totalCpuTime)) * cpuCount
}
private fun getTotalCpuTime(): Double {
if (procStatFile == null) {
procStatFile = RandomAccessFile("/proc/stat", "r")
} else {
procStatFile!!.seek(0)
}
val procStat = procStatFile!!.readLine()
val procStats = procStat.split(" ")
var curCpuTime = 0.0
val curIdleTime = procStats[5].toDouble()
// 2-8 項(xiàng)數(shù)據(jù)累加就是 CPU 總運(yùn)行時(shí)間
// CPU 總運(yùn)行時(shí)間 = user + system + nice + idle + iowait + irq + soft_irq
for (i in 2..8) {
curCpuTime += procStats[i].toDouble()
}
if (lastTotalCpuTime == 0.0) {
lastTotalCpuTime = curCpuTime
return 0.0
}
val total = curCpuTime - lastTotalCpuTime
lastTotalCpuTime = curCpuTime
return total
}
private fun getAppCpuTime(): Double {
if (appStatFile == null) {
appStatFile = RandomAccessFile("/proc/${android.os.Process.myPid()}/stat", "r")
} else {
appStatFile!!.seek(0)
}
val appStat = appStatFile!!.readLine()
val appStats = appStat.split(" ")
// 14-17 項(xiàng)數(shù)據(jù)累加就是該進(jìn)程 CPU 消耗時(shí)間
// 14-15 項(xiàng)數(shù)據(jù)累加就是該進(jìn)程單一線程 CPU 消耗時(shí)間
// 進(jìn)程 CPU 消耗時(shí)間 = utime + stime + cutime + cstime
// 單一線程 CPU 消耗時(shí)間 = utime + stime
var appCpuTime = 0.0
for (i in 14..17) {
appCpuTime += appStats[i].toDouble()
}
if (lastAppCpuTime == 0.0) {
lastAppCpuTime = appCpuTime
return 0.0
}
val result = appCpuTime - lastAppCpuTime
lastAppCpuTime = appCpuTime
return result
}
計(jì)算結(jié)果可以通過 adb shell top 查看對應(yīng) CPU 使用率是否正確。
計(jì)算的 cpuUsage 如果小于我們設(shè)置的閾值,就可以通知任務(wù)隊(duì)列或者各個(gè)業(yè)務(wù)執(zhí)行預(yù)加載任務(wù)了。
但該方案有一定缺陷并不通用,主要原因有以下兩個(gè):
-
間隔通過文件讀寫的方式對性能有一定損耗
-
在 Android API 26(Android 8.0)開始不再支持第三方應(yīng)用讀取
/proc/stat
,否則會拋出異常
基于存在以上問題,接下來會講解第二種推薦的方案判斷 CPU 是否閑置:times 函數(shù)。
times 函數(shù)判斷 CPU 是否閑置
times 函數(shù)可以直接返回用戶的 CPU 時(shí)間和系統(tǒng)時(shí)間,因?yàn)槭窍到y(tǒng)函數(shù)會直接從內(nèi)核拿數(shù)據(jù),所以不需要解析文件性能較高。
先看下 times 函數(shù)提供了什么字段:
times.h
struct tms {
__kernel_clock_t tms_utime; // 用戶 cpu 時(shí)間
__kernel_clock_t tms_stime; // 系統(tǒng) cpu 時(shí)間
__kernel_clock_t tms_cutime; // 已終止子進(jìn)程的用戶 cpu 時(shí)間
__kernel_clock_t tms_cstime; // 已終止子進(jìn)程的用戶系統(tǒng)時(shí)間
};
#include <jni.h>
#include <string>
#include <sys/times.h>
extern "C"
JNIEXPORT jfloat JNICALL
Java_com_example_native_1demo_MainActivity_getCpuTime(JNIEnv *env,
jobject thiz) {
struct tms currentTms;
times(¤tTms);
return currentTms.tms_utime + currentTms.tms_stime;
}
可以看到 times 函數(shù)只能讀取到應(yīng)用消耗的 CPU 時(shí)間,沒法獲取到總的 CPU 時(shí)間,那么該怎么計(jì)算 CPU 使用率判斷 CPU 是否處于閑置呢?實(shí)際上我們還可以通過應(yīng)用的 CPU 速率來判斷 CPU 是否已經(jīng)閑置,CPU 速率 = 單位時(shí)間內(nèi)進(jìn)程消耗的 CPU 時(shí)間 / 單位時(shí)間。
MainActivity.java
private lateinit var timer: Timer
private var beforeCpuTime = 0f
private val random = Random()
private var num: Int = 0
private val handler = object : Handler(Looper.getMainLooper()) {
// 模擬 cpu 計(jì)算消耗
override fun handleMessage(msg: Message) {
for (i in 0 until 100_0000) {
num += i
}
sendEmptyMessageDelayed(0, random.nextInt(5000).toLong())
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
handler.sendEmptyMessageDelayed(0, random.nextInt(5000).toLong())
timer = Timer()
timer.schedule(object: TimerTask() {
override fun run() {
val cpuTime = getCpuTime()
// 計(jì)算單位時(shí)間內(nèi)的 CPU 速率
val cpuSpeed = (cpuTime - beforeCpuTime) / 5f
Log.v("@@@", "cpuSpeed = $cpuSpeed")
if (cpuSpeed <= 0.1) {
Log.v("@@@", "cpu is idle")
}
beforeCpuTime = cpuTime
}
}, 0, 5 * 1000)
}
override fun onDestroy() {
super.onDestroy()
timer.cancel()
handler.removeCallbacksAndMessages(null)
}
private external fun getCpuTime(): Float
companion object {
// Used to load the 'native_demo' library on application startup.
init {
System.loadLibrary("native_demo")
}
}
當(dāng)應(yīng)用處于閑置狀態(tài)時(shí),CPU 速率一定在 0.1 以下,我們可以根據(jù)應(yīng)用的特性,設(shè)置一個(gè)閥值判斷 CPU 是否處于閑置狀態(tài)。當(dāng) CPU 閑置時(shí)我們可以預(yù)執(zhí)行的事情很多,比如預(yù)創(chuàng)建頁面的 View、預(yù)拉取數(shù)據(jù)、預(yù)創(chuàng)建次級頁面的關(guān)鍵對象等等。
減少 CPU 等待
從底層分析 CPU 實(shí)際上是沒有等待這種狀態(tài),CPU 要么是運(yùn)行,要么是閑置,所謂的 CPU 等待其實(shí)是某個(gè)線程或進(jìn)程拿到 CPU 時(shí)間片在當(dāng)前指令段停下來,長時(shí)間無法接著執(zhí)行后面代碼指令的情況,也就是代碼可能出現(xiàn)空循環(huán)導(dǎo)致 CPU 空轉(zhuǎn)、CPU 被切走去執(zhí)行其他線程。
兩種情況經(jīng)常導(dǎo)致 CPU 等待,一是等待鎖,二是等待 IO。
鎖優(yōu)化
在 Java 并發(fā)編程時(shí)通常用 synchronized 加鎖保證并發(fā)任務(wù)的準(zhǔn)確,但某個(gè)線程拿到鎖,其他線程可能就得等待鎖。
那么為什么鎖 synchronized 會影響 CPU?請求 synchronized 流程如下:
-
判斷鎖是否被其他線程持有,如果有則通過多次循環(huán)判斷鎖是否釋放,這個(gè)過程會導(dǎo)致 CPU 空轉(zhuǎn)
-
多次 CPU 空轉(zhuǎn)還是無法獲得,請求鎖的線程會陷入休眠加入等待隊(duì)列,待鎖釋放后被喚醒
無論是 CPU 空轉(zhuǎn)還是休眠都會導(dǎo)致當(dāng)前線程無法獲得 CPU 資源,如果是核心線程比如主線程或渲染線程,就會導(dǎo)致體驗(yàn)速度變慢。
所以我們要合理的使用鎖,可以遵循以下原則:
-
無鎖比有鎖好:這里指的是該加鎖的地方才加,可以不需要的就不加鎖;除了不加鎖,還有線程本地存儲、偏向鎖等方案,都屬于無鎖優(yōu)化
-
合理處理鎖的粒度、數(shù)量:相比整個(gè)方法都是 synchronized,更推薦在存在線程安全的代碼合理用 synchronized 代碼塊針對處理,細(xì)化鎖的粒度提高性能;當(dāng)然在某些場景應(yīng)該粗化鎖的粒度,比如 StringBuffer.append() 時(shí),虛擬機(jī)會將每個(gè) append() 內(nèi)部的鎖粗化共用一把鎖
IO 任務(wù)分離
在前面已經(jīng)分析過 IO 讀寫實(shí)際上是 DMA 在執(zhí)行和 CPU 無關(guān),此時(shí)就會出現(xiàn)兩種情況:
-
其他線程要執(zhí)行 CPU 任務(wù),任務(wù)調(diào)度器會將 CPU 切換給其他線程
-
沒有 CPU 相關(guān)任務(wù),CPU 一直等待直到 DMA 讀寫數(shù)據(jù)完成,再接著執(zhí)行后面的代碼邏輯
以上兩種情況對于線程來說,執(zhí)行完所有指令的時(shí)間變長了,也就是指令所消耗的平均時(shí)鐘周期時(shí)間變長了。如果線程是主線程或者渲染線程,會導(dǎo)致體驗(yàn)速度變慢。
為了減少等待 IO 導(dǎo)致的 CPU 使用率下降,我們 可以將 IO 任務(wù)分離即將 IO 任務(wù)從主線程或主流程分離出來,單獨(dú)用 IO 線程池處理;對于主線程必須要先拿到 IO 任務(wù)結(jié)果才執(zhí)行后面邏輯的場景,可以默認(rèn)用靜態(tài)數(shù)據(jù)展示,等待 IO 任務(wù)拿到數(shù)據(jù)后再更新界面,就能做到主線程縮短了等待 IO 的時(shí)間了。
IO 任務(wù)分離并不是說不等待 IO,而是要求將主流程的任務(wù)拆分得足夠細(xì),先執(zhí)行一些不需要等待 IO 的處理,IO 完成后再刷新處理。
緩存優(yōu)化:局部性原理與 dex 類文件重排序
緩存對提升速度來說至關(guān)重要,但緩存始終受著容量的制約,所以 我們做緩存時(shí),始終要考慮在有限的容量內(nèi)需要緩存哪些數(shù)據(jù),以及如何提升緩存的命中率。當(dāng)命中率較低時(shí),業(yè)務(wù)的速度就會變慢,此時(shí)我們就需要想辦法提升命中率了。
空間換時(shí)間:局部性原理
程序運(yùn)行是 CPU 不斷讀取程序指令并執(zhí)行的過程:CPU 在讀取指令時(shí),會先從寄存器讀,寄存器沒有再從高速緩存讀,最后才從主存讀取,讀取到指令后,也會先從主存加載到高速緩存,再從高速緩存加載到寄存器。
高速緩存從主存讀取的數(shù)據(jù)量大小是有限的,這個(gè)大小為 cache line 個(gè)字節(jié),在 Linux 也被稱為頁,一頁的大小和 CPU 型號有關(guān),主流是 64 個(gè)字節(jié)大小。
高速緩存讀取數(shù)據(jù)時(shí)會一次讀滿一頁,即使 CPU 需要的數(shù)據(jù)只有 4 個(gè)字節(jié),高速緩存也會讀滿 64 個(gè)字節(jié)的數(shù)據(jù),以此降低高速緩存讀主存的次數(shù),提升高速緩存的命中率,讓 CPU 更快執(zhí)行指令。該操作就涉及到了局部性原理。
局部性是一個(gè)很重要的概念,計(jì)算機(jī)硬件中用到了大量的局部性原理來提升命中率。局部性通常有兩種不同的形式:時(shí)間局部性和空間局部性。
時(shí)間局部性表示被使用過一次的數(shù)據(jù)很可能在后面還會再被多次使用。
空間局部性表示如果一個(gè)數(shù)據(jù)被使用了一次,那么接下里這個(gè)數(shù)據(jù)附近的數(shù)據(jù)也很很可能被使用。
高速緩存讀取數(shù)據(jù)就是按照空間局部性來讀的,也就是讀取當(dāng)前需要被使用的數(shù)據(jù),以及在內(nèi)存上緊挨著的數(shù)據(jù),總共湊齊一頁大小的數(shù)據(jù)后再加載進(jìn)高速緩存中。
了解了高速緩存讀取數(shù)據(jù)的原理后,我們就能利用這個(gè)規(guī)則來優(yōu)化程序的速度了。在程序執(zhí)行過程中,假設(shè)需要讀取一個(gè)對象的數(shù)據(jù),高速緩存不僅僅讀取這一個(gè)對象的數(shù)據(jù),還會讀取這個(gè)對象后面緊挨的一些對象,直到數(shù)據(jù)量達(dá)到一頁(一般是 64 個(gè)字節(jié))。如果這個(gè)對象在內(nèi)存上緊挨著的對象就是接下來馬上被用到的,高速緩存就不需要多次讀取數(shù)據(jù)了,CPU 也減少了等待數(shù)據(jù)讀取的時(shí)間,能更快的執(zhí)行程序指令,程序運(yùn)行得更快。
Redex:dex 類文件重排序提升緩存命中率
根據(jù)上面講述的局部性原理,也可以應(yīng)用到 dex 文件的讀取。
當(dāng)我們的項(xiàng)目被編譯成 apk 包,所有的 class 文件會整合后放在一個(gè)個(gè) dex 文件中。這個(gè)時(shí)候,dex 文件中 class 文件的順序并不是按照程序執(zhí)行順序存放的,因?yàn)槲覀円膊恢?class 文件的執(zhí)行順序。
如果我們能提前將程序運(yùn)行一遍,把 dex 中的 class 對象的使用順序收集下來,再按照這個(gè)順序重新調(diào)整 dex 文件中類文件的順序,把互相引用的類盡量放在同個(gè) dex,增加類的 pre-verify,將所有啟動相關(guān)的類文件,都放在主 dex 文件中,啟動當(dāng)然就會更快了。
上面的流程實(shí)現(xiàn)起來還是很復(fù)雜的,幸運(yùn)的是這一套流程也有成熟的開源框架可以直接使用,F(xiàn)acebook 提供了 Redex,其中的 InterDexPass 方案就是使用的局部性原理達(dá)到啟動優(yōu)化的目的。
官方文檔已經(jīng)將 Redex 的環(huán)境配置和優(yōu)化項(xiàng)配置講解得很詳細(xì),這里不再贅述。
需要注意的是,如果項(xiàng)目中有使用到熱修復(fù)等方案將會與 InterDexPass 有沖突會導(dǎo)致優(yōu)化失效,想要二者兼得需要選擇其他補(bǔ)丁方案。
任務(wù)調(diào)度優(yōu)化:線程 + CPU 提升任務(wù)調(diào)度優(yōu)先級
任務(wù)調(diào)度優(yōu)化主要有兩個(gè)方向:
-
提高任務(wù)的優(yōu)先級
-
減少任務(wù)調(diào)度的耗時(shí)
在前面已經(jīng)講過減少任務(wù)調(diào)度耗時(shí)的處理方式,所以這里主要講下如何提高任務(wù)優(yōu)先級。
提高任務(wù)優(yōu)先級有兩種方案:
-
提升核心線程的優(yōu)先級
-
核心線程綁定 CPU 大核
提升核心線程優(yōu)先級
上面我們有講解到,在 Linux 中的進(jìn)程分為實(shí)時(shí)進(jìn)程和普通進(jìn)程兩類。實(shí)時(shí)進(jìn)程一般通過 RTPRI(RealTimePriority)值來描述優(yōu)先級,取值范圍是 0 到 99;普通進(jìn)程一般使用 nice 值描述進(jìn)程優(yōu)先級,取值范圍是 -20 到 19。為了架構(gòu)設(shè)計(jì)統(tǒng)一,Linux 系統(tǒng)會將 nice 值對齊成 prio 值。因?yàn)榫€程在 Linux 系統(tǒng)層面其實(shí)也是一個(gè)輕量級的進(jìn)程,所以以上優(yōu)先級的處理在線程也適用。
在 Android 中只有部分底層核心進(jìn)程才是實(shí)時(shí)進(jìn)程,如 SurfaceFlinger、Audio 等進(jìn)程,大部分的進(jìn)程都是普通進(jìn)程。應(yīng)用中的所有線程都屬于普通進(jìn)程的級別,所以我們可以通過修改 nice 值調(diào)整線程優(yōu)先級。
調(diào)整線程優(yōu)先級的方式
調(diào)整線程優(yōu)先級 nice 值有兩個(gè) API:
-
Process.setThreadPriority(int priority) / Process.setThreadPriority(int pid, int priority)
-
Thread.setPriority(int priority)
第一種方式是 Android 系統(tǒng)提供的 API,如果不傳 pid 默認(rèn)就是當(dāng)前線程,priority 可以傳 -20 到 19 之間的任何一個(gè)值,不過還是建議直接使用 Android 提供的 priority 值定義的常量,代碼可讀性更強(qiáng)。
系統(tǒng)常量 | nice 值 | 使用場景 |
---|---|---|
Process.THREAD_PRIORITY_DEFAULT | 0 | 默認(rèn)優(yōu)先級 |
Process.THREAD_PRIORITY_LOWEST | 19 | 最低優(yōu)先級 |
Process.THREAD_PRIORITY_BACKGROUND | 10 | 后臺線程建議優(yōu)先級 |
Process.THREAD_PRIORITY_LESS_FAVORABLE | 1 | 比默認(rèn)略低 |
Process.THREAD_PRIORITY_MORE_FAVORABLE | -1 | 比默認(rèn)略高 |
Process.THREAD_PRIORITY_FOREGROUND | -2 | 前臺線程優(yōu)先級 |
Process.THREAD_PRIORITY_DISPLAY | -4 | 顯示線程建議優(yōu)先級 |
Process.THREAD_PRIORITY_URGENT_DISPLAY | -8 | 顯示線程的最高優(yōu)先級 |
Process.THREAD_PRIORITY_AUDIO | -16 | 音頻線程建議優(yōu)先級 |
Process.THREAD_PRIORITY_URGENT_AUDIO | -19 | 音頻線程最高優(yōu)先級 |
我們的主線程 nice 值默認(rèn)為 0,渲染線程默認(rèn) nice 值為 -4,音頻線程建議是最高級別優(yōu)先級,因?yàn)橐纛l線程優(yōu)先級太低,就會出現(xiàn)音頻播放卡頓的情況。
第二種方式是 Java 提供的 API,Java 有自己對線程優(yōu)先級的定義和規(guī)則,但最后都會轉(zhuǎn)為對應(yīng)的 nice 值。
常量值 | nice 值 | Android 對應(yīng) nice 值 | |
---|---|---|---|
Thread.MAX_PRIORITY | 10 | -8 | Process.THREAD_PRIORITY_URGENT_DISPLAY |
Thread.MIN_PRIORITY | 0 | 19 | Process.THREAD_PRIORITY_LOWEST |
Thread.NORM_PRIORITY | 5 | 0 | Process.THREAD_PRIORITY_DEFAULT |
第二種方式能設(shè)置的優(yōu)先級較少,不太靈活,并且因?yàn)橄到y(tǒng)的一個(gè)時(shí)許問題 bug,在設(shè)置子線程優(yōu)先級時(shí),可能因?yàn)樽泳€程沒創(chuàng)建成功而設(shè)置成了主線程的,會導(dǎo)致優(yōu)先級設(shè)置異常,所以建議使用 Process.setThreadPriority() 來設(shè)置線程的優(yōu)先級,避免使用 Thread.setPriority()。
調(diào)整主線程和渲染線程(RenderThread)優(yōu)先級
在 Android 我們可以調(diào)整主線程和渲染線程,因?yàn)檫@兩個(gè)線程對任何應(yīng)用來說都非常重要。從 Android 5 開始,主線程只負(fù)責(zé)布局文件的 measure 和 layout,渲染工作放到了渲染線程,兩個(gè)線程配合工作界面才能在應(yīng)用正常顯示出來。所以通過提升這兩個(gè)線程優(yōu)先級,便能讓這兩個(gè)線程獲得更多的 CPU 時(shí)間,頁面顯示速度自然也就更快了。
調(diào)整主線程優(yōu)先級
主線程的優(yōu)先級調(diào)整很簡單,直接在 Application 的 attachBaseContext() 調(diào)用 Process.setThreadPriority(-19),將主線程設(shè)置為最高級別優(yōu)先級即可。
調(diào)整渲染線程優(yōu)先級
渲染線程又該怎么調(diào)整呢?API 需要提供 pid,如果我們能找到渲染線程的 id 就可以調(diào)整了。
應(yīng)用中線程的信息記錄在 /proc/pid/task
中,我們可以通過遍歷這個(gè)目錄下的文件查找渲染線程:
1、先查看設(shè)備所有的進(jìn)程,找到應(yīng)用的 pid
adb shell ps // 先查看設(shè)備所有的進(jìn)程,通過包名找到應(yīng)用的 pid
假設(shè)我們要查看的進(jìn)程是 com.example.demo,pid 是 31494。
2、查看應(yīng)用的所有線程信息
adb shell
cd /proc/31494/task // 查看 com.example.demo 進(jìn)程的所有線程信息
3、接著查看目錄線程的 stat 節(jié)點(diǎn),就能具體查看線程的詳細(xì)信息了,比如 tid、name 等
所以我們要找到渲染線程的 pid,只需要遍歷 /proc/pid/task
目錄下的所有目錄,查看 pid/stat
文件,如果有名稱為 (RenderThread),第一個(gè)信息就是渲染線程 pid 了。拿到 pid 也就能調(diào)整線程優(yōu)先級:
private fun getRenderThreadTid(): Int {
val appAllThreadMsgDir = File("/proc/${android.os.Process.myPid()}/task/")
if (!appAllThreadMsgDir.isDirectory) return -1
val files = appAllThreadMsgDir.listFiles() ?: arrayOf()
var result = -1
files.forEach { file ->
val br = BufferedReader(FileReader("${file.path}/stat"), 100)
val cpuRate = br.use {
return@use br.readLine()
}
if (!cpuRate.isNullOrEmpty()) {
val param = cpuRate.split(" ")
if (param.size >= 2) {
val threadName = param[1]
if (threadName == "(RenderThread)") {
result = param[0].toInt()
return@forEach
}
}
}
}
return result
}
val pid = getRenderThreadTid()
if (pid != -1) {
Process.setThreadPriority(pid, -19)
}
當(dāng)然,我們要提高的優(yōu)先級線程并非只有這兩個(gè),我們可以根據(jù)業(yè)務(wù)需要,來提高核心線程的優(yōu)先級,同時(shí)降低其他非核心線程的優(yōu)先級,該操作可以在線程池中通過線程工廠來統(tǒng)一調(diào)整。提高核心線程優(yōu)先級,降低非核心線程優(yōu)先級,兩者配合使用,才能更高效地提升應(yīng)用的速度。
核心線程綁定 CPU 大核
核心線程綁定 CPU 大核的方案雖然和任務(wù)調(diào)度關(guān)系不大,但也屬于一種提升線程優(yōu)先級的方案,其實(shí)就是將核心線程運(yùn)行在性能更好的 CPU 上以提高運(yùn)行速度,比如將主線程和渲染線程綁定在大核,提高頁面顯示速度。
線程綁核并不是很復(fù)雜的事情,因?yàn)?Linux 系統(tǒng)有提供相應(yīng)的 API 接口,系統(tǒng)提供了 pthread_setaffinity_np 和 sched_setaffinity 兩個(gè)函數(shù),都能實(shí)現(xiàn)線程綁核。不過 Android 系統(tǒng)限制了 pthread_setaffinity_np 函數(shù)的使用,所以只能通過 sched_setaffinity 函數(shù)進(jìn)行綁核操作。
sched_setaffinity 函數(shù)需要傳入三個(gè)參數(shù):
-
pid:線程 pid,如果 pid 的值為 0 則表示指定的是主線程
-
cpusetsize:mask 所指定的數(shù)的長度
-
mask:需要綁定的 cpu 序列的掩碼
#include <jni.h>
#include <sched.h>
extern "C"
JNIEXPORT jint JNICALL
Java_com_example_demo_MainActivity_bindMaxFreqCore(JNIEnv *env, jobject thiz,
jint max_freq_cpu_index, jint pid) {
cpu_set_t mask;
// 將 mask 置空
CPU_ZERO(&mask);
// 將需要綁定的 CPU 核設(shè)置給 mask,核為序列 0、1、2、3...
CPU_SET(max_freq_cpu_index, &mask);
// 線程綁核
return sched_setaffinity(pid, sizeof(mask), &mask);
}
線程綁定大核我們需要兩個(gè)參數(shù):CPU 大核的序列位置、綁定大核的線程 pid。
獲取 CPU 大核的序列位置,其實(shí)就是要知道哪個(gè) CPU 的時(shí)鐘頻率是最高的。那么怎么知道是哪個(gè) CPU 核的時(shí)鐘頻率是最高的?
獲取大核序列位置
可以通過 /sys/devices/system/cpu/
目錄下的文件查看當(dāng)前設(shè)備有幾個(gè) CPU,可以看到如下設(shè)備是有 8 個(gè) CPU:
我們進(jìn)入其中一個(gè) cpu 目錄的 cpu{x}/cpufreq/
目錄的 /cpuinfo_max_freq
可以查看該 cpu 的時(shí)鐘周期頻率,這里我們進(jìn)入 cpu0 查看它的時(shí)鐘周期頻率:
這里也將該設(shè)備所有的 CPU 時(shí)鐘周期頻率列出來:
可以看到這臺設(shè)備的 cpu6 和 cpu7 就是我們要找的大核。
所以獲取 CPU 時(shí)鐘頻率最高的核序列操作如下:
-
訪問
/sys/devices/system/cpu/
目錄遍歷文件,記錄設(shè)備的 CPU 數(shù)量 -
根據(jù) CPU 數(shù)量遍歷訪問
/sys/devices/system/cpu/cpu{x}/cpufreq/cpuinfo_max_freq
獲取最大的 CPU 時(shí)鐘周期頻率,同時(shí)也能知道大核的 CPU 序列位置
// 獲取 CPU 數(shù)量
private fun getNumberOfCpuCores(): Int {
val files = File("/sys/devices/system/cpu/").listFiles()
var size = 0
files?.forEach { file ->
val path = file.name
if (path.startsWith("cpu")) {
val chars = path.toCharArray()
for (i in 3 until path.length) {
if (chars[i] in '0'..'9') {
size++
}
}
}
}
return size
}
// 獲取 CPU 大核序列位置
private fun getMaxFreqCPUIndex(): Int {
val cores = getNumberOfCpuCores()
if (cores == 0) return -1
var maxFreq = -1
var maxFreqCPUIndex = 0
for (i in 0 until cores) {
val filename = "/sys/devices/system/cpu/cpu$i/cpufreq/cpuinfo_max_freq"
val cpuInfoMaxFreqFile = File(filename)
if (!cpuInfoMaxFreqFile.exists()) continue
val buffer = ByteArray(128)
FileInputStream(cpuInfoMaxFreqFile).use { stream ->
stream.read(buffer)
var endIndex = 0
while (buffer[endIndex].toInt().toChar() in '0'..'9') endIndex++
val freqBound = String(buffer, 0, endIndex).toInt()
if (freqBound > maxFreq) {
maxFreq = freqBound
maxFreqCPUIndex = i
}
}
}
return maxFreqCPUIndex
}
線程綁定大核步驟
線程綁定大核操作步驟如下:
-
獲取時(shí)鐘頻率最高(即性能最好)的 CPU 核序列
-
獲取需要綁定的線程 pid
-
調(diào)用 shced_setaffinity 函數(shù)將線程綁定到大核文章來源:http://www.zghlxwxcb.cn/news/detail-547234.html
下面以渲染線程綁定 CPU 大核為例:文章來源地址http://www.zghlxwxcb.cn/news/detail-547234.html
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 獲取渲染線程 pid
val renderThreadTid = getRenderThreadTid()
Log.v("@@@", "renderThreadTid = $renderThreadTid")
// 獲取 CPU 大核序列位置
val maxFreqCPUIndex = getMaxFreqCPUIndex()
Log.v("@@@", "maxFreqCPUIndex = $maxFreqCPUIndex")
// 線程綁定大核,返回 0 表示成功,否則失敗
val result = bindMaxFreqCore(maxFreqCPUIndex, renderThreadTid)
Log.v("@@@", "result = $result")
}
private fun getNumberOfCpuCores(): Int {
val files = File("/sys/devices/system/cpu/").listFiles()
var size = 0
files?.forEach { file ->
val path = file.name
if (path.startsWith("cpu")) {
val chars = path.toCharArray()
for (i in 3 until path.length) {
if (chars[i] in '0'..'9') {
size++
}
}
}
}
return size
}
private fun getMaxFreqCPUIndex(): Int {
val cores = getNumberOfCpuCores()
if (cores == 0) return -1
var maxFreq = -1
var maxFreqCPUIndex = 0
for (i in 0 until cores) {
val filename = "/sys/devices/system/cpu/cpu$i/cpufreq/cpuinfo_max_freq"
val cpuInfoMaxFreqFile = File(filename)
if (!cpuInfoMaxFreqFile.exists()) continue
val buffer = ByteArray(128)
FileInputStream(cpuInfoMaxFreqFile).use { stream ->
stream.read(buffer)
var endIndex = 0
while (buffer[endIndex].toInt().toChar() in '0'..'9') endIndex++
val freqBound = String(buffer, 0, endIndex).toInt()
if (freqBound > maxFreq) {
maxFreq = freqBound
maxFreqCPUIndex = i
}
}
}
return maxFreqCPUIndex
}
private fun getRenderThreadTid(): Int {
val appAllThreadMsgDir = File("/proc/${android.os.Process.myPid()}/task/")
if (!appAllThreadMsgDir.isDirectory) return -1
val files = appAllThreadMsgDir.listFiles() ?: arrayOf()
var result = -1
files.forEach { file ->
val br = BufferedReader(FileReader("${file.path}/stat"), 100)
val cpuRate = br.use {
return@use br.readLine()
}
if (!cpuRate.isNullOrEmpty()) {
val param = cpuRate.split(" ")
if (param.size >= 2) {
val threadName = param[1]
if (threadName == "(RenderThread)") {
result = param[0].toInt()
return@forEach
}
}
}
}
return result
}
private external fun bindMaxFreqCore(maxFreqCpuIndex: Int, pid: Int): Int
companion object {
// Used to load the 'demo' library on application startup.
init {
System.loadLibrary("demo")
}
}
}
native-lib.cpp
#include <jni.h>
#include <sched.h>
extern "C"
JNIEXPORT jint JNICALL
Java_com_example_demo_MainActivity_bindMaxFreqCore(JNIEnv *env, jobject thiz,
jint max_freq_cpu_index, jint pid) {
cpu_set_t mask;
// 將 mask 置空
CPU_ZERO(&mask);
// 將需要綁定的 CPU 核設(shè)置給 mask,核為序列 0、1、2、3...
CPU_SET(max_freq_cpu_index, &mask);
return sched_setaffinity(pid, sizeof(mask), &mask);
}
到了這里,關(guān)于Android 性能優(yōu)化系列:啟動優(yōu)化進(jìn)階的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!