前言
線程優(yōu)化一直是啟動優(yōu)化中的一個必不可少的項目隔躲。作為一個 Android 程序員摩梧,你肯定希望應(yīng)用啟動的時候,火力全開蹭越,線程池拉滿障本,每一個 CPU 核心滿載而行。
可你把線程池拉滿的時候,啟動時長就一定會降低嗎驾霜?
結(jié)果顯然是否定的案训,之前我在進行啟動優(yōu)化的時候,就遇到了類似的問題粪糙。我引入了有向無環(huán)圖類似的啟動庫后强霎,又將線程池的數(shù)量設(shè)置為:
CPU核心數(shù) * 2 + 1
看似沒什么問題,后續(xù)啟動時長居然還增長了一點點蓉冈。
為什么會出現(xiàn)這樣的問題城舞?我們今天就好好聊聊。
一寞酿、做個實驗
先做個實驗家夺,在應(yīng)用啟動過程中,主要做了兩步:
- 主線程循環(huán) 10w 次伐弹,做一些簡單的計算
- 線程池做一些異步任務(wù)拉馋,讀取文件,然后將讀取到的數(shù)據(jù)寫入數(shù)據(jù)庫惨好,這個異步任務(wù)提交了 1000 次
核心線程數(shù) = 2 * CPU 核心數(shù) + 1
煌茴,變量最大線程數(shù):
- 實驗一:最大線程數(shù) =
2 * CPU 核心數(shù) + 1
- 實驗二:最大線程數(shù) = Int.MAX_VALUE
在模擬器上,實驗二平均啟動時長 6505 ms日川,實驗一平均啟動市場 5521 ms蔓腐,從這點看,線程開太多對主線程是有影響的龄句。
二回论、基礎(chǔ)知識
在啟動流程中基礎(chǔ)知識必不可少,從上往下講就是線程撒璧、線程池透葛、內(nèi)核 和 CPU笨使,這些知識都是老生常談了卿樱。
1. 線程
線程是操作系統(tǒng)進行運算調(diào)度的最小單位,可以理解為它就是系統(tǒng)執(zhí)行的任務(wù)硫椰。
作為任務(wù)繁调,它會有各種狀態(tài):
- NEW(新建):新創(chuàng)建的線程,還沒有啟動
- RUNNABLE(可運行):可以運行的線程
- BLOCKED(阻塞):阻塞狀態(tài)的線程
- WAITTING(等待):等待狀態(tài)
- TIME_WAITTING(計時等待)
- TERMINATED(終止)
各種狀態(tài)可以進行如下轉(zhuǎn)換:
處于可運行狀態(tài)的線程不一定處于運行中靶草,如果 CPU 核心數(shù) < 線程數(shù)量蹄胰,在某個時間點,處于運行中的線程數(shù)量最多也只能等于 CPU 核心數(shù)奕翔。
除此以外裕寨,只有處于可運行狀態(tài)的線程才有機會獲取 CPU 的青睞,從而分到時間片,得以執(zhí)行宾袜。
2. 線程池
線程池的知識都很熟悉了捻艳,簡單了解一下。
2.1 核心線程
簡單來說庆猫,我們想了解的部分就是線程池的核心線程和非核心線程:
- 核心線程:核心線程會一直存在
- 非核心線程:當(dāng)非核心線程閑置超過指定的時間认轨,就會被銷毀
通過配置合適的核心線程數(shù)和非核心線程數(shù)可以幫助我們管理好線程,可以帶來以下好處:
- 降低資源消耗:重復(fù)利用線程月培,降低資源消耗
- 提供響應(yīng)速度:任務(wù)一來就執(zhí)行
- 管理好線程資源:避免無節(jié)制的使用線程嘁字,引發(fā)性能問題
除此以外,在配置核心線程數(shù)和非核心線程數(shù)的時候杉畜,還需要根據(jù)業(yè)務(wù)場景纪蜒,將 CPU 密集型和 I/O 密集型任務(wù)考慮進去。
2.2 任務(wù)劃分
我們經(jīng)常將任務(wù)分為 I/O密集型 和 CPU密集型 任務(wù)此叠,那么這兩種有什么區(qū)別呢霍掺?
I/O 密集型任務(wù)指的是該任務(wù)的大部分時間用來提交 I/O 請求或者等待 I/O 請求。這類任務(wù)常常運行很短暫的一會兒拌蜘,然后進入阻塞狀態(tài)杆烁,等待更多的 I/O 請求。常見的如數(shù)據(jù)庫操作简卧、網(wǎng)絡(luò)操作兔魂、鍵盤事件、屏幕操作等举娩。
CPU 密集型任務(wù)指的是任務(wù)的大部分代碼用來執(zhí)行代碼析校。該類任務(wù)常常會一直運行并占用著 CPU,直到時間片用完铜涉。常見的如數(shù)據(jù)計算智玻、無限循環(huán)等。
那線程數(shù)如何設(shè)置芙代?我們下面再去講吊奢。
3. 內(nèi)核
哪個線程先運行?什么時間運行纹烹?運行多久页滚?這些都是調(diào)度程序說了算!
3.1 調(diào)度程序
調(diào)度程序是一個內(nèi)核子系統(tǒng)铺呵,它是多任務(wù)操作系統(tǒng)的基礎(chǔ)裹驰。多任務(wù)操作系統(tǒng)就是能夠同時并發(fā)地交互執(zhí)行多個進程的操作系統(tǒng)。
即使是單核處理器片挂,它也可以并發(fā)的處理多個任務(wù)幻林,只不過在一個時間點贞盯,只有一個正在執(zhí)行的任務(wù)。
就好比安卓開發(fā)小王沪饺,身背幾個需求邻悬,被產(chǎn)品要求同一天上線,雖然也能夠完成随闽,但他在某個時間點父丰,只能寫一個需求,如果想一個時間點同時進行兩個需求掘宪,那得加人蛾扇,也就是我們通常說的雙核處理器,這就具備了并行的能力魏滚。
3.2 搶占式和非搶占式
多任務(wù)操作系統(tǒng)可以分為兩種類型:非搶占式多任務(wù)和搶占式多任務(wù)镀首。
Android 使用的是搶占式多任務(wù),在這種模式下鼠次,每個任務(wù)都會被分配到一定的時間用來執(zhí)行更哄,一旦時間片用完,就會自動切換到下一個任務(wù)腥寇,分配的時間我們稱之為時間片成翩。
還拿小王來舉例,小王身背三個需求赦役,每天的計劃中麻敌,上午需求 A,下午需求 B掂摔,晚上需求C术羔。到了下午,即使需求 A 沒做完乙漓,也要去做需求 B级历,這樣可以保證了每個需求每天都會有進度。
從啟動的角度來說叭披,我們肯定不希望主線程和子線程分得同樣的時間片寥殖,這可能會讓我們的應(yīng)用看著很慢。
為了給主線程分得更長的時間片趋观,每個進程都有一個 nice 值扛禽,它會影響時間片的分配锋边,但我們改不了這個皱坛,我們能夠處理的就是給線程設(shè)置優(yōu)先級,Android 中線程的優(yōu)先級從 -19 到 19豆巨,值越低代表優(yōu)先級越高剩辟,分得的時間片也就越長。
3.3 線程多了會怎么分配
上面的這些東西看似和我們應(yīng)用層開發(fā)沒關(guān)系,實則不然贩猎。
比如線程數(shù)量多了以后熊户,我們先拿小王舉例:
原先小王手里有 5 個需求,每個 2 天工時吭服,做完一個再做下一個嚷堡,10天能搞定。
現(xiàn)經(jīng)理要求他同時開發(fā) 5 個需求艇棕,保證 5 個需求每天都有進度蝌戒,那可就麻煩了,先不算 10 天開發(fā)時間沼琉,還得加上如下時間:
- 每天切其他四個項目時間成本
- 思考時間:每次切到下一個項目北苟,都會想上次開發(fā)到哪,上次的思路是什么
加上這些亂七八槽的打瘪,原來 10 天能搞定的東西友鼻,現(xiàn)在得變成 12 天。
線程多了闺骚,也會有這樣的問題彩扔,每次切換時間片都是成本。另外僻爽,線程的閑置率會上升借杰,像這樣運行 14ms 要等 185 ms:
還拿小王來看,原先五個需求进泼,桉順序做蔗衡,每個需求的生命周期就 2 天,但是并行開發(fā)后乳绕,每個需求的生命周期都拉長了绞惦,到了 12 天左右。對于啟動的主線程來講可不是好事洋措!
理想的情況應(yīng)該是量力而行济蝉,當(dāng)小王開發(fā)一個需求遇到問題需要等產(chǎn)品回復(fù)而停滯,在等待的這段時間內(nèi)菠发,開發(fā)另外一個需求王滤,知道產(chǎn)品回復(fù)完,再找一個合適的時間切回來滓鸠,這樣雁乡,反而會提升效率,將工作時間縮短到 9 天糜俗。
4. CPU
在2022年發(fā)布的 Android 低端機上踱稍,也都標(biāo)配了 8 核心的 CPU曲饱,核心數(shù)越多,就意味著并行能力越強珠月。
注意扩淀,這里用的是并行,而不是并發(fā)啤挎。
一個核心驻谆,就代表著團隊只有一個開發(fā),8 核代表著團隊有八個開發(fā)庆聘,意味著一個時間點最高可以有8個需求同時進行旺韭。
二、線程數(shù)如何設(shè)置
上面說了那么多掏觉,大家最想知道的就是線程數(shù)如何設(shè)置区端。
一般而言,核心線程數(shù)和最大線程數(shù)都設(shè)置為 CPU核心數(shù) * 2 +1 澳腹,阻塞隊列使用 LinkedBlockingDeque
织盼。
1. 任務(wù)因素
但這個數(shù)字肯定不是絕對的,我們需要考慮到 CPU 密集型任務(wù) 和 IO 密集型任務(wù)的區(qū)別酱塔。
如果我們使用子線程都是處理網(wǎng)絡(luò)沥邻、數(shù)據(jù)庫、讀文件等操作羊娃,這個數(shù)字就可以設(shè)置大一點唐全;如果子線程僅執(zhí)行一些耗時的計算代碼,這個數(shù)字就可以設(shè)置小一點蕊玷。
2. 任務(wù)閑置
即使我們自己設(shè)置的線程池沒什么問題邮利,但程序一啟動,任務(wù)執(zhí)行時候的線程閑置率一看就知道還有問題垃帅,比如這張圖:
為什么會出現(xiàn)這種閑置率太高的情況延届,原因可能如下:
- 過多使用
New Thread
或者不節(jié)制的使用線程池 - 很多第三方 SDK 都使用自身的線程池或者線程
查看閑置率有兩種,分別是使用Android Studio中的Profiler和Shell命令贸诚。
推薦大家使用 Profiler方庭,好處可太多了:
- 可以查看線程總數(shù)
- 可以查看CPU的負(fù)載情況
- 可以查看每個任務(wù)的閑置率
- ...
直接使用 Profiler 中的 System TraceView 只能查看系統(tǒng)級別的方法,如果是我們想查看的方法酱固,需要這么處理:
public void test{
Trace.beginSection("名稱");
//... 代碼省略
Trace.endSection();
}
對每個方法做上述過程確實太麻煩械念,所以都是配合函數(shù)插樁使用。
另外一個就是使用 Shell 命令运悲,我們可以在 Android Studio 中 Logcat 窗口看到應(yīng)用的進程 Id龄减,進入 adb shell 后,就可以通過輸入命令 cat /proc/{進程ID}/schedstat
查看:
emulator64_x86_64_arm64:/ $ cat /proc/7775/schedstat
5511910111 2055599424 6712
// 參數(shù)一 CPU運行時間
// 參數(shù)二 該進程等待時間
// 參數(shù)三 主動切換和被動切換的次數(shù)
這些數(shù)據(jù)只能夠我們查看大概的情況扇苞。
總結(jié)
關(guān)于線程我們能做的并不多欺殿,盡量去收斂線程:
- 禁止使用 New Thread 方式去創(chuàng)建線程
- 統(tǒng)一應(yīng)用內(nèi)線程池寄纵,并制定合適的核心線程和最大線程數(shù)量
- 編寫公司庫的時候鳖敷,如需使用線程池脖苏,提供設(shè)置線程池的接口
- 可以設(shè)置自身線程池的第三方庫,優(yōu)先設(shè)置應(yīng)用內(nèi)線程池定踱,比如 OkHttp
- Hook 第三方庫使用
New Thread
棍潘,改為應(yīng)用內(nèi)線程池 - 能懶加載盡量懶加載第三方庫,避免過早的競爭系統(tǒng)資源
主要就這些崖媚,如有不對的地方亦歉,評論區(qū)見~