




版權說明:本文檔由用戶提供并上傳,收益歸屬內容提供方,若內容存在侵權,請進行舉報或認領
文檔簡介
第Android性能優化之線程監控與線程統一詳解目錄背景常規解決方案線程監控當前線程統計線程信息具體化線程統一Thread創建注意總結
背景
在我們日常開發中,多線程管理一直是非常頭疼的問題之一,尤其在歷史性長,結構復雜的app中,線程數會達到好幾百個甚至更多,然而過多的線程不僅僅帶來了內存上的消耗同時也降低了cpu調度的效率,過多的cpu調度帶來的消耗的壞處甚至超過了多線程帶來的好處。
在我們日常開發中,通常會遇到以下幾個問題
某個場景會創造過多的線程,最終導致oom線程池過多問題,比如三方庫有一套線程池,自己項目也有一套線程池,隨著三方/二方業務接入,導致了不相兼容的線程池數越多,降低了全體線程池數的調度效率,比如多個okhttp的調用歷史原因導致,newThread橫行,又或者是各種線程使用不規范,導致工程混亂即使是空閑時候,依舊有線程在不斷Waiting各種線程死鎖問題
最終種種原因導致,我們的項目在上線過程中,會遇到各種線程不明的情況,對排查問題或者解決問題帶來極大的考驗。
常規解決方案
對于上述問題的解決,許多團隊通過codeview去限制代碼準入,比如定制Thread的規范,又或者是定義項目統一的線程池,在項目中去使用。這個方案優點就是可操作性強,便于團隊去實施,但是這比較依靠review(或者其他代碼掃描插件),對于歷史項目來說比較容易出現疏漏,而且后期也依舊需要維護,對于大型團隊來說,需要兼顧所有人代碼,且三方庫無法處理。同時Thread的衍生物也有很多,比如Android中的HandlerThread等等,也是線程。
現在比較流行的方案是通過字節碼插樁的方式,統一做線程監控亦或進行線程統一,比如監控處理的matrix,還有優化相關的booster等。線程統一這個依靠項目的情況,會有全統一線程池的情況(所以共用一個線程池),也有統一某單一業務的線程池的情況(比如只收口項目okhttp的線程池)下面我們圍繞這兩個主題,分別進行探討
線程監控
當前線程統計
對線程的監控,首先我們要統計當前的信息對不對,可以直接通過
Thread.getAllStackTraces()
獲取到當前所有thread的信息與堆棧情況,其返回值是一個map對象,
MapThread,StackTraceElement[]
獲取結果例子如下
[Thread[Binder:30506_2,5,main],Thread[FinalizerWatchdogDaemon,5,system],Thread[Binder:30506_3,5,main],Thread[Jitthreadpoolworkerthread0,5,system],Thread[ReferenceQueueDaemon,5,system],Thread[ProfileSaver,5,system],Thread[main,5,main],Thread[Binder:30506_1,5,main],Thread[RenderThread,7,main],Thread[pika_thread,5,main],Thread[vivo.PerfThread,5,main],Thread[SignalCatcher,10,system],Thread[FinalizerDaemon,5,system],Thread[HeapTaskDaemon,5,system]]
我們可以看到key是一個thread對象,如果我們要設計一個自己的apm的話可以通過遍歷key拿到一個Thread對象,然后再通過該Thread對象拿到自身的信息即可,比如獲取thread的名稱
Thread.getAllStackTraces().keys.map{
線程信息具體化
通過上述,我們可以拿到了當前所有的線程信息,但是很遺憾的是,其中有一些線程信息幾乎是不可用的,比如我們用newThread構建出來的線程,如果不給它指定的名字的話,默認就會出現類似這種情,比如Thread-1,這種名稱的線程對我們來說幾乎是沒有任何意義的,我們暫且把它稱為匿名線程,解決匿名線程的手段有很多,之前在學完ASMTreeapi,再也不怕hook了這篇我們可以看到,我們可以用asm對調用thread進行插樁,通過改變指令調用函數,把普通的空參數Thread()方法變成帶有name的構造方法Thread(String)進行hook處理,把調用者名稱的信息放到前置的ldc指令,從而到達一個轉化的效果。
轉化前Thread構造函數轉化后Thread構造函數Thread()Thread(String)Thread(Runnable)Thread(Runnable,String)Thread(ThreadGroup,Runnable)Thread(ThreadGroup,Runnable,String)......
asm代碼實例如下
method.instructions.insertBefore(
node,
newLdcInsnNode()
defr=node.desc.lastIndexOf(')')
把構造函數描述變成了帶有stringname的構造函數描述
defdesc=
"${node.desc.substring(0,r)}Ljava/lang/String;${node.desc.substring(r)}"
println("*${node.owner}.${}${node.desc}=${node.owner}.${}$desc:${}.${}${method.desc}")
node.desc=desc
當然,Thread還有很多構造函數,我們就不一一舉例子去適配,相關的操作也是類似的,涉及到Executors等其他創建線程的方式,我們也可以通過這種指令替換的方式去進行Thread的命名操作。這里就不再贅述,可以參考booster的做法
線程統一
線程的統一可以依靠項目統一的線程池,但是這個約束不到第三方,我們可以利用ASM等工具進行線程的統一,線程統一包括全模塊統一跟單模塊統一(特定模塊),由于單模塊統一涉及具體業務,比如對okhttpclient的調度線程統一,由于不具備通用性,需要根據模塊具體實現去統一,我們這里就不討論了,單模塊統一有個好處就是風險低,只影響單一模塊的線程調度。我們討論一下全模塊的統一。
在項目中,我們有各種各樣的線程調度api,直接newThread,Executors,ThreadPoolExecutor等等,它們公共點就是都用到了Thread,最終都是靠著Thread去運行,但是想要把它們統一起來,我們要兼顧更上一層的api,那么適配工作量可是不少!!那么我們有沒有一種黑科技,能夠簡單點就把線程統一到一個特定的線程池,作為收口呢?(注意這里討論的是把全項目的線程統一,包括三方庫),為了找到突破點,我們先看一下最基本的Thread是怎么創建出來的
Thread創建
最常用的Thread創建肯定是最簡單的,我們舉個例子
varthread=Thread{
Log.i("hello","thisismythread${Thread.currentThread().name}")
那么這段代碼它做了什么呢?我們要從字節碼的角度去分析,才能找到突破點
NEWjava/lang/Thread
INVOKEDYNAMICrun()Ljava/lang/Runnable;[
//handlekind0x6:INVOKESTATIC
java/lang/invoke/LambdaMetafactory.metafactory(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
//arguments:
()V,
//handlekind0x6:INVOKESTATIC
com/example/spider/MainActivity.onCreate$lambda-0()V,
INVOKESPECIALjava/lang/Thread.init(Ljava/lang/Runnable;)V
ASTORE2
我們來一一說明下調用的指令:
NEW創建一個java/lang/Thread對象,此時只是引用被創建,所引用的對象還沒有創建,并加入操作數棧頂部
2.DUP將操作數棧頂部的參數復制一份,并加入操作數棧
3.INVOKEDYNAMIClambad用到的函數調用指令,運行時綁定信息,()Ljava/lang/Runnable,由于入參為null,所以不消耗操作數棧的參數,返回值是Runnable,所以會在操作數棧上新加入一個Runnable對象
4.INVOKESPECIAL構造函數能調用到的特殊指令,即創建一個對象,(Ljava/lang/Runnable;)V,我們看到入參只有一個Runnable對象,但是實際上調用INVOKESPECIAL的構造函數隱藏了一個條件,就是需要一個被創建對象對應的引用對象,這就是dup存在的原因,因為需要消耗一個Thread引用對象!這點需要注意
5.ASTORE2,就是把操作數棧頂部的變量放到了局部變量表index為2的地方,這里為什么是2呢,是由當前運行環境決定的,靜態方法中index為0的就是參數1,而普通方法index為0的地方卻是this指針,這點是需要注意的,除了index=0的地方有這個約定,其他index下標其實就是函數環境的決定的。(這也側面說明,存在AStore,ALoad這些指令的時候,我們很難去做通用性插樁,因為這里依賴了局部變量表的具體實現)
看到這里,我們就能夠明白了一個Thread創建的字節碼是怎么樣的了
那么我們想想看,怎么達到我們統一線程池的目的。看到Thread的創建過程我們就知道,Thread會依賴局部變量表(第5條),所以我們如果直接對Thread進行操作的話,是不行的,因為局部變量表的存儲index是依靠當前環境的!其實我們統一線程池,想要統一的也不一定是要統一Thread,而是統一Runnable執行的線程環境對吧!突破點就來了,我們對Runnable進行操作,把其原本依賴執行的Thread變成我們自己線程池的Thread是不是就可以了!
目標明確了,但是我們也需要為此做一些特定的處理,因為這種自定義指令集的處理,用其他ASM工具也是無法生成的,所以我們才具體解釋相關的指令集。最終這邊的方案就是,進行Thread調用替換,即把newThread這個指令,替換為我們自己的MyThread的指令進行定制化處理。步驟如下
替換原本的INVOKESPECIAL指令調用為我們自己的MyThread調用,這里給出MyThread實現
classMyThread(privatevalrunnable:Runnable):Thread(runnable){
//調用到自己的start
overridefunstart(){
Log.i("hello","MyThread")
//runnable在定義的統一線程池執行
ThreadHelper.runInCustomPool(runnable)
原本指令返回的是Thread,由于我們替換為了MyThread,那么原本跟Thread強綁定的NEW指令,DUP指令就也需要變更跟MyThread類型相關的指令,我們這里就不采用替換,采取新加的方式(替換也可以,這里選擇方便處理,因為操作數只對棧頂元素生效)
3.到了這一步,還不行,因為我們原本要返回的是Thread對象,現在變成了MyThread對象,所以我們需要一個轉化指令CHECKCAST
我們給出具體的ASM代碼
classMyThreadHookUtils{
staticTHREAD="java/lang/Thread"
staticvoidtransform(ClassNodeklass){
//我們自定義的MyThread類不需要參加轉化
if(.equals("com/example/spider/MyThread")){
return
klass.methods.forEach{methodNode-
methodNode.instructions.each{
if(it.opcode==Opcodes.INVOKESPECIAL){
transformInvokeSpecial((MethodInsnNode)it,klass,methodNode)
privatestaticvoidtransformInvokeSpecial(MethodInsnNodenode,ClassNodeklass,MethodNodemethod){
//如果不是構造函數,就直接退出
if(node.owner!=THREAD){
return
println("transformInvokeSpecial")
transformThreadInvokeSpecial(node,klass,method)
privatestaticvoidtransformThreadInvokeSpecial(
MethodInsnNodenode,
ClassNodeklass,
MethodNodemethod
println("init==="+node.desc+""+node.owner)
if(node.desc.equals("(Ljava/lang/Runnable;)V")){
intindex=method.instructions.indexOf(node)
defdyc=method.instructions[index-1]
InsnListinsertNodes1=newInsnList()
TypeInsnNodenewInsnNode=newTypeInsnNode(Opcodes.NEW,"com/example/spider/MyThread")
InsnNodedupNode=newInsnNode(Opcodes.DUP)
insertNodes1.add(newInsnNode)
insertNodes1.add(dupNode)
method.instructions.insertBefore(dyc,insertNodes1)
MethodInsnNodemethodHookNode=newMethodInsnNode(Opcodes.INVOKESPECIAL,
"com/example/spider/MyThread",
"init",
"(Ljava/lang/Runnable;)V",
false)
TypeInsnNodetypeInsnNode=newTypeInsnNode(Opcodes.CHECKCAST,"java/lang/Thread")
InsnListinsertNodes=newInsnList()
insertNodes.add(methodHookNode)
溫馨提示
- 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
- 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯系上傳者。文件的所有權益歸上傳用戶所有。
- 3. 本站RAR壓縮包中若帶圖紙,網頁內容里面會有圖紙預覽,若沒有圖紙預覽就沒有圖紙。
- 4. 未經權益所有人同意不得將文件中的內容挪作商業或盈利用途。
- 5. 人人文庫網僅提供信息存儲空間,僅對用戶上傳內容的表現方式做保護處理,對用戶上傳分享的文檔內容本身不做任何修改或編輯,并不能對任何下載內容負責。
- 6. 下載文件中如有侵權或不適當內容,請與我們聯系,我們立即糾正。
- 7. 本站不保證下載資源的準確性、安全性和完整性, 同時也不承擔用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。
最新文檔
- 元宵節線上活動策劃與實施
- 廣東省廣州市花都區2021-2022學年七年級下學期期末歷史試題(含答案)
- 《探索市場營銷策略》課件
- 精細化管理月報優化與實踐
- 初級護師考試溝通技巧練習與試題及答案
- 通知 課件內容審核中國成
- 工程變更管理試題及答案
- 八下語文燈籠課件
- 大班語言:眼睛生病了
- 《決策者常見的失誤》課件
- 2025年北京市朝陽區高三二模-政治+答案
- 溫州市普通高中2025屆高三第三次適應性考試物理試題及答案
- 《光纖激光切割技術》課件
- 10.信息光子技術發展與應用研究報告(2024年)
- 2025年下半年商務部外貿發展事務局第二次招聘8人易考易錯模擬試題(共500題)試卷后附參考答案
- 2024年山西杏花村汾酒集團有限責任公司招聘筆試真題
- 《行政法與行政訴訟法》課件各章節內容-第一章 行政法概述
- 浙江2025年浙江省地質院本級及所屬部分事業單位招聘筆試歷年參考題庫附帶答案詳解
- 2025年廣東廣州中物儲國際貨運代理有限公司招聘筆試參考題庫含答案解析
- 海外安保面試題及答案
- 危重患者的早期康復
評論
0/150
提交評論