国产 无码 综合区,色欲AV无码国产永久播放,无码天堂亚洲国产AV,国产日韩欧美女同一区二区

Android 性能優(yōu)化系列:啟動優(yōu)化進(jìn)階

這篇具有很好參考價(jià)值的文章主要介紹了Android 性能優(yōu)化系列:啟動優(yōu)化進(jìn)階。希望對大家有所幫助。如果存在錯(cuò)誤或未考慮完全的地方,請大家不吝賜教,您也可以點(diǎn)擊"舉報(bào)違法"按鈕提交疑問。

啟動速度優(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)

Android 性能優(yōu)化系列:啟動優(yōu)化進(jìn)階,性能優(yōu)化,性能優(yōu)化

  • 每條指令的平均時(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à)也越來越便宜。

Android 性能優(yōu)化系列:啟動優(yōu)化進(jìn)階,性能優(yōu)化,性能優(yōu)化

高速緩存是屬于 CPU 的組成部分,并且實(shí)際有幾層高速緩存也是由 CPU 決定的。以下圖高通驍龍 888 的芯片為例,它是 8 塊核組成的 CPU,從架構(gòu)圖可以看到,它的 L2 是 1M 大小,L3 是 3M 大小,并且所有核共享。

Android 性能優(yōu)化系列:啟動優(yōu)化進(jìn)階,性能優(yōu)化,性能優(yōu)化

不同層之間的讀寫速度差距是很大的,所以為了能提高場景的速度,我們需要將和核心場景相關(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):

Android 性能優(yōu)化系列:啟動優(yōu)化進(jìn)階,性能優(yōu)化,性能優(yōu)化

  • 運(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 線程池

Android 性能優(yōu)化系列:啟動優(yōu)化進(jìn)階,性能優(yōu)化,性能優(yōu)化

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 線程池:

Android 性能優(yōu)化系列:啟動優(yōu)化進(jìn)階,性能優(yōu)化,性能優(yōu)化

不過 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

Android 性能優(yōu)化系列:啟動優(yōu)化進(jìn)階,性能優(yōu)化,性能優(yōu)化

上面數(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(&currentTms);
    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í),會先從寄存器讀,寄存器沒有再從高速緩存讀,最后才從主存讀取,讀取到指令后,也會先從主存加載到高速緩存,再從高速緩存加載到寄存器。

Android 性能優(yōu)化系列:啟動優(yōu)化進(jìn)階,性能優(yōu)化,性能優(yōu)化

高速緩存從主存讀取的數(shù)據(jù)量大小是有限的,這個(gè)大小為 cache line 個(gè)字節(jié),在 Linux 也被稱為頁,一頁的大小和 CPU 型號有關(guān),主流是 64 個(gè)字節(jié)大小。

Android 性能優(yōu)化系列:啟動優(yōu)化進(jìn)階,性能優(yōu)化,性能優(yōu)化

高速緩存讀取數(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 性能優(yōu)化系列:啟動優(yōu)化進(jìn)階,性能優(yōu)化,性能優(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

Android 性能優(yōu)化系列:啟動優(yōu)化進(jìn)階,性能優(yōu)化,性能優(yōu)化

假設(shè)我們要查看的進(jìn)程是 com.example.demo,pid 是 31494。

2、查看應(yīng)用的所有線程信息

adb shell
cd /proc/31494/task // 查看 com.example.demo 進(jìn)程的所有線程信息

Android 性能優(yōu)化系列:啟動優(yōu)化進(jìn)階,性能優(yōu)化,性能優(yōu)化

3、接著查看目錄線程的 stat 節(jié)點(diǎn),就能具體查看線程的詳細(xì)信息了,比如 tid、name 等

Android 性能優(yōu)化系列:啟動優(yōu)化進(jìn)階,性能優(yōu)化,性能優(yōu)化

所以我們要找到渲染線程的 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)行綁核操作。

Android 性能優(yōu)化系列:啟動優(yōu)化進(jìn)階,性能優(yōu)化,性能優(yōu)化

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:

Android 性能優(yōu)化系列:啟動優(yōu)化進(jìn)階,性能優(yōu)化,性能優(yōu)化

我們進(jìn)入其中一個(gè) cpu 目錄的 cpu{x}/cpufreq/ 目錄的 /cpuinfo_max_freq 可以查看該 cpu 的時(shí)鐘周期頻率,這里我們進(jìn)入 cpu0 查看它的時(shí)鐘周期頻率:

Android 性能優(yōu)化系列:啟動優(yōu)化進(jìn)階,性能優(yōu)化,性能優(yōu)化

這里也將該設(shè)備所有的 CPU 時(shí)鐘周期頻率列出來:

Android 性能優(yōu)化系列:啟動優(yōu)化進(jìn)階,性能優(yōu)化,性能優(yōu)化

可以看到這臺設(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ù)將線程綁定到大核

下面以渲染線程綁定 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)!

本文來自互聯(lián)網(wǎng)用戶投稿,該文觀點(diǎn)僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務(wù),不擁有所有權(quán),不承擔(dān)相關(guān)法律責(zé)任。如若轉(zhuǎn)載,請注明出處: 如若內(nèi)容造成侵權(quán)/違法違規(guī)/事實(shí)不符,請點(diǎn)擊違法舉報(bào)進(jìn)行投訴反饋,一經(jīng)查實(shí),立即刪除!

領(lǐng)支付寶紅包贊助服務(wù)器費(fèi)用

相關(guān)文章

  • Android 性能優(yōu)化系列:崩潰原因及捕獲

    在日常開發(fā)中崩潰是我們遇到的很常見的情況,可能是 NullPointerException、IllegalArgumentException 等等,當(dāng)應(yīng)用程序拋出這些我們未捕獲的異常時(shí),緊跟著的是應(yīng)用的崩潰,進(jìn)程被殺死并退出。 或許你到現(xiàn)在都一直認(rèn)為是因?yàn)閽伋隽水惓?,所以才會?dǎo)致的進(jìn)程被殺死并退出,認(rèn)為

    2024年02月11日
    瀏覽(19)
  • Android性能優(yōu)化系列-騰訊matrix-IO監(jiān)控-IOCanaryPlugin源碼分析

    作者:秋去無痕 matrix 對io的監(jiān)控包括四個(gè)方面 監(jiān)控在主線程執(zhí)行 IO 操作的問題 監(jiān)控緩沖區(qū)過小的問題 監(jiān)控重復(fù)讀同一文件 監(jiān)控內(nèi)存泄漏問題 IOCanaryPlugin,內(nèi)部由IOCanaryCore完成真正的操作。 根據(jù)配置進(jìn)行hook的安裝 取消hook 底層hook安裝包函幾個(gè)步驟,加載so,設(shè)置hook內(nèi)容,

    2024年02月09日
    瀏覽(25)
  • 前端性能優(yōu)化進(jìn)階版

    1、使用 Web Workers 和 Service Workers 來提高并行性和離線緩存。 使用 Web Workers 和 Service Workers:可以使用 Web Workers 將計(jì)算密集型任務(wù)放到其他線程中執(zhí)行,以避免卡頓和阻塞 UI 線程。Service Workers 可以用于緩存網(wǎng)頁資源以提高加載速度和離線訪問能力。 2、使用 HTTP/2 來減少請求

    2023年04月27日
    瀏覽(20)
  • 安卓進(jìn)階(一)App性能優(yōu)化

    安卓進(jìn)階(一)App性能優(yōu)化

    性能優(yōu)化的目的是為了讓應(yīng)用程序App 更快、更穩(wěn)定 更省。具體介紹如下: 更快:應(yīng)用程序 運(yùn)行得更加流暢、不卡頓,能快速響應(yīng)用戶操作 更穩(wěn)定:應(yīng)用程序 能 穩(wěn)定運(yùn)行 解決用戶需求,在用戶使用過程中不出現(xiàn)應(yīng)用程序崩潰(Crash) 和 無響應(yīng)(ANR)的問題 更?。汗?jié)省耗費(fèi)

    2024年02月07日
    瀏覽(27)
  • MySQL進(jìn)階之性能優(yōu)化與調(diào)優(yōu)技巧

    MySQL進(jìn)階之性能優(yōu)化與調(diào)優(yōu)技巧

    1.1.2 介紹 多表查詢:查詢時(shí)從多張表中獲取所需數(shù)據(jù) 單表查詢的SQL語句:select 字段列表 from 表名; 要執(zhí)行多表查詢,只需要使用逗號分隔多張表即可,如: select 字段列表 from 表1, 表2; 查詢用戶表和部門表中的數(shù)據(jù): 此時(shí),我們看到查詢結(jié)果中包含了大量的結(jié)果集,總共85條

    2024年02月05日
    瀏覽(21)
  • 前端進(jìn)階版本 ,性能優(yōu)化—-防抖、節(jié)流、重繪與回流

    前端進(jìn)階版本 ,性能優(yōu)化—-防抖、節(jié)流、重繪與回流

    目錄 【防抖】 【節(jié)流】 重繪(repaint) 回流(reflow):又叫重排(layout) 工作中要如何避免大量使用重繪與回流? 常見的會導(dǎo)致回流的元素 【防抖】 任務(wù)頻繁觸發(fā)的情況下,只有任務(wù)觸發(fā)的間隔超過指定間隔的時(shí)候,任務(wù)才會執(zhí)行。 【節(jié)流】 指定時(shí)間間隔內(nèi)只會執(zhí)行一次任務(wù)。

    2024年02月14日
    瀏覽(22)
  • 【運(yùn)維知識進(jìn)階篇】集群架構(gòu)-Nginx性能優(yōu)化

    Nginx花了好多篇文章介紹了,今天談?wù)勊膬?yōu)化。我們從優(yōu)化考慮的方面,壓力測試工具ab,具體的優(yōu)化點(diǎn)三個(gè)方面去介紹,話不多說,直接開始! 目錄 優(yōu)化考慮方面 壓力測試工具 性能優(yōu)化 一、影響性能的指標(biāo) 二、系統(tǒng)性能優(yōu)化 1、更改文件句柄 2、Time_wait狀態(tài)重用 三、代

    2024年02月06日
    瀏覽(25)
  • 「NodeJs進(jìn)階」超全面的 Node.js 性能優(yōu)化相關(guān)知識梳理

    「NodeJs進(jìn)階」超全面的 Node.js 性能優(yōu)化相關(guān)知識梳理

    相信對于前端同學(xué)而言,我們?nèi)ラ_發(fā)一個(gè)自己的簡單后端程序可以借助很多的nodeJs的框架去進(jìn)行快速搭建,但是從前端面向后端之后,我們會在很多方面會稍顯的有些陌生,比如「性能分析」,「性能測試」,「內(nèi)存管理」,「內(nèi)存查看」,「使用C++插件」,「子進(jìn)程」,「

    2024年02月01日
    瀏覽(25)
  • Android復(fù)雜UI的性能優(yōu)化實(shí)踐 - PTQBookPageView 性能優(yōu)化記錄

    Android復(fù)雜UI的性能優(yōu)化實(shí)踐 - PTQBookPageView 性能優(yōu)化記錄

    作者:彭泰強(qiáng) 要做性能優(yōu)化,首先得知道性能怎么度量、怎么表示。因?yàn)樾阅苁且粋€(gè)很抽象的詞,我們必須把它量化、可視化。那么,因?yàn)槭荱I組件優(yōu)化,我首先選用了 GPU呈現(xiàn)模式分析 這一工具。 在手機(jī)上的開發(fā)者模式里可以開啟 GPU呈現(xiàn)(渲染)模式分析 這一工具,有的

    2024年02月14日
    瀏覽(28)
  • Android性能優(yōu)化—ViewPagers + Fragment緩存優(yōu)化

    Android性能優(yōu)化—ViewPagers + Fragment緩存優(yōu)化

    大家看標(biāo)題,可能會有點(diǎn)兒懵,什么是ViewPagers,因?yàn)樵诤芫弥?,我們使用的都是ViewPager,但是現(xiàn)在更多的是在用ViewPager2,因此用ViewPagers(ViewPager、ViewPager2)來代替兩者,主要介紹兩者的區(qū)別。 ViewPagers嵌套Fragment架構(gòu),在我們常用的App中隨處可見,抖音的首頁、各大電商

    2024年02月01日
    瀏覽(20)

覺得文章有用就打賞一下文章作者

支付寶掃一掃打賞

博客贊助

微信掃一掃打賞

請作者喝杯咖啡吧~博客贊助

支付寶掃一掃領(lǐng)取紅包,優(yōu)惠每天領(lǐng)

二維碼1

領(lǐng)取紅包

二維碼2

領(lǐng)紅包