Android性能優(yōu)化之線(xiàn)程監(jiān)控與線(xiàn)程統(tǒng)一詳解_第1頁(yè)
Android性能優(yōu)化之線(xiàn)程監(jiān)控與線(xiàn)程統(tǒng)一詳解_第2頁(yè)
Android性能優(yōu)化之線(xiàn)程監(jiān)控與線(xiàn)程統(tǒng)一詳解_第3頁(yè)
Android性能優(yōu)化之線(xiàn)程監(jiān)控與線(xiàn)程統(tǒng)一詳解_第4頁(yè)
Android性能優(yōu)化之線(xiàn)程監(jiān)控與線(xiàn)程統(tǒng)一詳解_第5頁(yè)
已閱讀5頁(yè),還剩6頁(yè)未讀, 繼續(xù)免費(fèi)閱讀

下載本文檔

版權(quán)說(shuō)明:本文檔由用戶(hù)提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請(qǐng)進(jìn)行舉報(bào)或認(rèn)領(lǐng)

文檔簡(jiǎn)介

第Android性能優(yōu)化之線(xiàn)程監(jiān)控與線(xiàn)程統(tǒng)一詳解目錄背景常規(guī)解決方案線(xiàn)程監(jiān)控當(dāng)前線(xiàn)程統(tǒng)計(jì)線(xiàn)程信息具體化線(xiàn)程統(tǒng)一Thread創(chuàng)建注意總結(jié)

背景

在我們?nèi)粘i_(kāi)發(fā)中,多線(xiàn)程管理一直是非常頭疼的問(wèn)題之一,尤其在歷史性長(zhǎng),結(jié)構(gòu)復(fù)雜的app中,線(xiàn)程數(shù)會(huì)達(dá)到好幾百個(gè)甚至更多,然而過(guò)多的線(xiàn)程不僅僅帶來(lái)了內(nèi)存上的消耗同時(shí)也降低了cpu調(diào)度的效率,過(guò)多的cpu調(diào)度帶來(lái)的消耗的壞處甚至超過(guò)了多線(xiàn)程帶來(lái)的好處。

在我們?nèi)粘i_(kāi)發(fā)中,通常會(huì)遇到以下幾個(gè)問(wèn)題

某個(gè)場(chǎng)景會(huì)創(chuàng)造過(guò)多的線(xiàn)程,最終導(dǎo)致oom線(xiàn)程池過(guò)多問(wèn)題,比如三方庫(kù)有一套線(xiàn)程池,自己項(xiàng)目也有一套線(xiàn)程池,隨著三方/二方業(yè)務(wù)接入,導(dǎo)致了不相兼容的線(xiàn)程池?cái)?shù)越多,降低了全體線(xiàn)程池?cái)?shù)的調(diào)度效率,比如多個(gè)okhttp的調(diào)用歷史原因?qū)е?,newThread橫行,又或者是各種線(xiàn)程使用不規(guī)范,導(dǎo)致工程混亂即使是空閑時(shí)候,依舊有線(xiàn)程在不斷Waiting各種線(xiàn)程死鎖問(wèn)題

最終種種原因?qū)е?,我們的?xiàng)目在上線(xiàn)過(guò)程中,會(huì)遇到各種線(xiàn)程不明的情況,對(duì)排查問(wèn)題或者解決問(wèn)題帶來(lái)極大的考驗(yàn)。

常規(guī)解決方案

對(duì)于上述問(wèn)題的解決,許多團(tuán)隊(duì)通過(guò)codeview去限制代碼準(zhǔn)入,比如定制Thread的規(guī)范,又或者是定義項(xiàng)目統(tǒng)一的線(xiàn)程池,在項(xiàng)目中去使用。這個(gè)方案優(yōu)點(diǎn)就是可操作性強(qiáng),便于團(tuán)隊(duì)去實(shí)施,但是這比較依靠review(或者其他代碼掃描插件),對(duì)于歷史項(xiàng)目來(lái)說(shuō)比較容易出現(xiàn)疏漏,而且后期也依舊需要維護(hù),對(duì)于大型團(tuán)隊(duì)來(lái)說(shuō),需要兼顧所有人代碼,且三方庫(kù)無(wú)法處理。同時(shí)Thread的衍生物也有很多,比如Android中的HandlerThread等等,也是線(xiàn)程。

現(xiàn)在比較流行的方案是通過(guò)字節(jié)碼插樁的方式,統(tǒng)一做線(xiàn)程監(jiān)控亦或進(jìn)行線(xiàn)程統(tǒng)一,比如監(jiān)控處理的matrix,還有優(yōu)化相關(guān)的booster等。線(xiàn)程統(tǒng)一這個(gè)依靠項(xiàng)目的情況,會(huì)有全統(tǒng)一線(xiàn)程池的情況(所以共用一個(gè)線(xiàn)程池),也有統(tǒng)一某單一業(yè)務(wù)的線(xiàn)程池的情況(比如只收口項(xiàng)目okhttp的線(xiàn)程池)下面我們圍繞這兩個(gè)主題,分別進(jìn)行探討

線(xiàn)程監(jiān)控

當(dāng)前線(xiàn)程統(tǒng)計(jì)

對(duì)線(xiàn)程的監(jiān)控,首先我們要統(tǒng)計(jì)當(dāng)前的信息對(duì)不對(duì),可以直接通過(guò)

Thread.getAllStackTraces()

獲取到當(dāng)前所有thread的信息與堆棧情況,其返回值是一個(gè)map對(duì)象,

MapThread,StackTraceElement[]

獲取結(jié)果例子如下

[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是一個(gè)thread對(duì)象,如果我們要設(shè)計(jì)一個(gè)自己的apm的話(huà)可以通過(guò)遍歷key拿到一個(gè)Thread對(duì)象,然后再通過(guò)該Thread對(duì)象拿到自身的信息即可,比如獲取thread的名稱(chēng)

Thread.getAllStackTraces().keys.map{

線(xiàn)程信息具體化

通過(guò)上述,我們可以拿到了當(dāng)前所有的線(xiàn)程信息,但是很遺憾的是,其中有一些線(xiàn)程信息幾乎是不可用的,比如我們用newThread構(gòu)建出來(lái)的線(xiàn)程,如果不給它指定的名字的話(huà),默認(rèn)就會(huì)出現(xiàn)類(lèi)似這種情,比如Thread-1,這種名稱(chēng)的線(xiàn)程對(duì)我們來(lái)說(shuō)幾乎是沒(méi)有任何意義的,我們暫且把它稱(chēng)為匿名線(xiàn)程,解決匿名線(xiàn)程的手段有很多,之前在學(xué)完ASMTreeapi,再也不怕hook了這篇我們可以看到,我們可以用asm對(duì)調(diào)用thread進(jìn)行插樁,通過(guò)改變指令調(diào)用函數(shù),把普通的空參數(shù)Thread()方法變成帶有name的構(gòu)造方法Thread(String)進(jìn)行hook處理,把調(diào)用者名稱(chēng)的信息放到前置的ldc指令,從而到達(dá)一個(gè)轉(zhuǎn)化的效果。

轉(zhuǎn)化前Thread構(gòu)造函數(shù)轉(zhuǎn)化后Thread構(gòu)造函數(shù)Thread()Thread(String)Thread(Runnable)Thread(Runnable,String)Thread(ThreadGroup,Runnable)Thread(ThreadGroup,Runnable,String)......

asm代碼實(shí)例如下

method.instructions.insertBefore(

node,

newLdcInsnNode()

defr=node.desc.lastIndexOf(')')

把構(gòu)造函數(shù)描述變成了帶有stringname的構(gòu)造函數(shù)描述

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

當(dāng)然,Thread還有很多構(gòu)造函數(shù),我們就不一一舉例子去適配,相關(guān)的操作也是類(lèi)似的,涉及到Executors等其他創(chuàng)建線(xiàn)程的方式,我們也可以通過(guò)這種指令替換的方式去進(jìn)行Thread的命名操作。這里就不再贅述,可以參考booster的做法

線(xiàn)程統(tǒng)一

線(xiàn)程的統(tǒng)一可以依靠項(xiàng)目統(tǒng)一的線(xiàn)程池,但是這個(gè)約束不到第三方,我們可以利用ASM等工具進(jìn)行線(xiàn)程的統(tǒng)一,線(xiàn)程統(tǒng)一包括全模塊統(tǒng)一跟單模塊統(tǒng)一(特定模塊),由于單模塊統(tǒng)一涉及具體業(yè)務(wù),比如對(duì)okhttpclient的調(diào)度線(xiàn)程統(tǒng)一,由于不具備通用性,需要根據(jù)模塊具體實(shí)現(xiàn)去統(tǒng)一,我們這里就不討論了,單模塊統(tǒng)一有個(gè)好處就是風(fēng)險(xiǎn)低,只影響單一模塊的線(xiàn)程調(diào)度。我們討論一下全模塊的統(tǒng)一。

在項(xiàng)目中,我們有各種各樣的線(xiàn)程調(diào)度api,直接newThread,Executors,ThreadPoolExecutor等等,它們公共點(diǎn)就是都用到了Thread,最終都是靠著Thread去運(yùn)行,但是想要把它們統(tǒng)一起來(lái),我們要兼顧更上一層的api,那么適配工作量可是不少!!那么我們有沒(méi)有一種黑科技,能夠簡(jiǎn)單點(diǎn)就把線(xiàn)程統(tǒng)一到一個(gè)特定的線(xiàn)程池,作為收口呢?(注意這里討論的是把全項(xiàng)目的線(xiàn)程統(tǒng)一,包括三方庫(kù)),為了找到突破點(diǎn),我們先看一下最基本的Thread是怎么創(chuàng)建出來(lái)的

Thread創(chuàng)建

最常用的Thread創(chuàng)建肯定是最簡(jiǎn)單的,我們舉個(gè)例子

varthread=Thread{

Log.i("hello","thisismythread${Thread.currentThread().name}")

那么這段代碼它做了什么呢?我們要從字節(jié)碼的角度去分析,才能找到突破點(diǎn)

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

我們來(lái)一一說(shuō)明下調(diào)用的指令:

NEW創(chuàng)建一個(gè)java/lang/Thread對(duì)象,此時(shí)只是引用被創(chuàng)建,所引用的對(duì)象還沒(méi)有創(chuàng)建,并加入操作數(shù)棧頂部

2.DUP將操作數(shù)棧頂部的參數(shù)復(fù)制一份,并加入操作數(shù)棧

3.INVOKEDYNAMIClambad用到的函數(shù)調(diào)用指令,運(yùn)行時(shí)綁定信息,()Ljava/lang/Runnable,由于入?yún)閚ull,所以不消耗操作數(shù)棧的參數(shù),返回值是Runnable,所以會(huì)在操作數(shù)棧上新加入一個(gè)Runnable對(duì)象

4.INVOKESPECIAL構(gòu)造函數(shù)能調(diào)用到的特殊指令,即創(chuàng)建一個(gè)對(duì)象,(Ljava/lang/Runnable;)V,我們看到入?yún)⒅挥幸粋€(gè)Runnable對(duì)象,但是實(shí)際上調(diào)用INVOKESPECIAL的構(gòu)造函數(shù)隱藏了一個(gè)條件,就是需要一個(gè)被創(chuàng)建對(duì)象對(duì)應(yīng)的引用對(duì)象,這就是dup存在的原因,因?yàn)樾枰囊粋€(gè)Thread引用對(duì)象!這點(diǎn)需要注意

5.ASTORE2,就是把操作數(shù)棧頂部的變量放到了局部變量表index為2的地方,這里為什么是2呢,是由當(dāng)前運(yùn)行環(huán)境決定的,靜態(tài)方法中index為0的就是參數(shù)1,而普通方法index為0的地方卻是this指針,這點(diǎn)是需要注意的,除了index=0的地方有這個(gè)約定,其他index下標(biāo)其實(shí)就是函數(shù)環(huán)境的決定的。(這也側(cè)面說(shuō)明,存在AStore,ALoad這些指令的時(shí)候,我們很難去做通用性插樁,因?yàn)檫@里依賴(lài)了局部變量表的具體實(shí)現(xiàn))

看到這里,我們就能夠明白了一個(gè)Thread創(chuàng)建的字節(jié)碼是怎么樣的了

那么我們想想看,怎么達(dá)到我們統(tǒng)一線(xiàn)程池的目的。看到Thread的創(chuàng)建過(guò)程我們就知道,Thread會(huì)依賴(lài)局部變量表(第5條),所以我們?nèi)绻苯訉?duì)Thread進(jìn)行操作的話(huà),是不行的,因?yàn)榫植孔兞勘淼拇鎯?chǔ)index是依靠當(dāng)前環(huán)境的!其實(shí)我們統(tǒng)一線(xiàn)程池,想要統(tǒng)一的也不一定是要統(tǒng)一Thread,而是統(tǒng)一Runnable執(zhí)行的線(xiàn)程環(huán)境對(duì)吧!突破點(diǎn)就來(lái)了,我們對(duì)Runnable進(jìn)行操作,把其原本依賴(lài)執(zhí)行的Thread變成我們自己線(xiàn)程池的Thread是不是就可以了!

目標(biāo)明確了,但是我們也需要為此做一些特定的處理,因?yàn)檫@種自定義指令集的處理,用其他ASM工具也是無(wú)法生成的,所以我們才具體解釋相關(guān)的指令集。最終這邊的方案就是,進(jìn)行Thread調(diào)用替換,即把newThread這個(gè)指令,替換為我們自己的MyThread的指令進(jìn)行定制化處理。步驟如下

替換原本的INVOKESPECIAL指令調(diào)用為我們自己的MyThread調(diào)用,這里給出MyThread實(shí)現(xiàn)

classMyThread(privatevalrunnable:Runnable):Thread(runnable){

//調(diào)用到自己的start

overridefunstart(){

Log.i("hello","MyThread")

//runnable在定義的統(tǒng)一線(xiàn)程池執(zhí)行

ThreadHelper.runInCustomPool(runnable)

原本指令返回的是Thread,由于我們替換為了MyThread,那么原本跟Thread強(qiáng)綁定的NEW指令,DUP指令就也需要變更跟MyThread類(lèi)型相關(guān)的指令,我們這里就不采用替換,采取新加的方式(替換也可以,這里選擇方便處理,因?yàn)椴僮鲾?shù)只對(duì)棧頂元素生效)

3.到了這一步,還不行,因?yàn)槲覀冊(cè)疽祷氐氖荰hread對(duì)象,現(xiàn)在變成了MyThread對(duì)象,所以我們需要一個(gè)轉(zhuǎn)化指令CHECKCAST

我們給出具體的ASM代碼

classMyThreadHookUtils{

staticTHREAD="java/lang/Thread"

staticvoidtransform(ClassNodeklass){

//我們自定義的MyThread類(lèi)不需要參加轉(zhuǎn)化

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){

//如果不是構(gòu)造函數(shù),就直接退出

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. 本站所有資源如無(wú)特殊說(shuō)明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請(qǐng)下載最新的WinRAR軟件解壓。
  • 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請(qǐng)聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶(hù)所有。
  • 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁(yè)內(nèi)容里面會(huì)有圖紙預(yù)覽,若沒(méi)有圖紙預(yù)覽就沒(méi)有圖紙。
  • 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
  • 5. 人人文庫(kù)網(wǎng)僅提供信息存儲(chǔ)空間,僅對(duì)用戶(hù)上傳內(nèi)容的表現(xiàn)方式做保護(hù)處理,對(duì)用戶(hù)上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對(duì)任何下載內(nèi)容負(fù)責(zé)。
  • 6. 下載文件中如有侵權(quán)或不適當(dāng)內(nèi)容,請(qǐng)與我們聯(lián)系,我們立即糾正。
  • 7. 本站不保證下載資源的準(zhǔn)確性、安全性和完整性, 同時(shí)也不承擔(dān)用戶(hù)因使用這些下載資源對(duì)自己和他人造成任何形式的傷害或損失。

評(píng)論

0/150

提交評(píng)論