Android性能優(yōu)化之多線程管理優(yōu)化方案詳解

背景

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

在我們?nèi)粘i_發(fā)中募寨,通常會遇到以下幾個問題:

  • 某個場景會創(chuàng)造過多的線程族展,最終導(dǎo)致oom。
  • 線程池過多問題拔鹰,比如三方庫有一套線程池仪缸,自己項目也有一套線程池,隨著三方/二方業(yè)務(wù)接入列肢,導(dǎo)致了不相兼容的線程池數(shù)越多恰画,降低了全體線程池數(shù)的調(diào)度效率,比如多個okhttp的調(diào)用瓷马。
  • 歷史原因?qū)е滤┗梗琻ew Thread橫行,又或者是各種線程使用不規(guī)范欧聘,導(dǎo)致工程混亂片林。
  • 即使是空閑時候,依舊有線程在不斷Waiting怀骤。
  • 各種線程死鎖問題费封。

最終種種原因?qū)е拢覀兊捻椖吭谏暇€過程中蒋伦,會遇到各種線程不明的情況弓摘,對排查問題或者解決問題帶來極大的考驗。

常規(guī)解決方案

對于上述問題的解決痕届,許多團(tuán)隊通過codeview去限制代碼準(zhǔn)入衣盾,比如定制Thread的規(guī)范寺旺,又或者是定義項目統(tǒng)一的線程池,在項目中去使用势决。

這個方案優(yōu)點就是可操作性強阻塑,便于團(tuán)隊去實施,但是這比較依靠review(或者其他代碼掃描插件)果复,對于歷史項目來說比較容易出現(xiàn)疏漏陈莽,而且后期也依舊需要維護(hù),對于大型團(tuán)隊來說虽抄,需要兼顧所有人代碼走搁,且三方庫無法處理。同時Thread的衍生物也有很多迈窟,比如Android中的HandlerThread等等私植,也是線程。

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

線程監(jiān)控

當(dāng)前線程統(tǒng)計

對線程的監(jiān)控,首先我們要統(tǒng)計當(dāng)前的信息對不對窄坦,可以直接通過

Thread.getAllStackTraces()

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

Map<Thread, StackTraceElement[]>

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

[
Thread[Binder:30506_2,5,main],
 Thread[FinalizerWatchdogDaemon,5,system], 
 Thread[Binder:30506_3,5,main], 
 Thread[Jit thread pool worker thread 0,5,system], 
 Thread[ReferenceQueueDaemon,5,system], 
 Thread[Profile Saver,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[Signal Catcher,10,system], 
 Thread[FinalizerDaemon,5,system], 
 Thread[HeapTaskDaemon,5,system]
 ]

我們可以看到key是一個thread對象鸭津,如果我們要設(shè)計一個自己的apm的話可以通過遍歷key拿到一個Thread對象,然后再通過該Thread對象拿到自身的信息即可肠缨,比如獲取thread的名稱逆趋。

Thread.getAllStackTraces().keys.map {    it.name}

線程信息具體化

通過上述,我們可以拿到了當(dāng)前所有的線程信息怜瞒,但是很遺憾的是父泳,其中有一些線程信息幾乎是“不可用”的般哼,比如我們用new Thread構(gòu)建出來的線程吴汪,如果不給它指定的名字的話,默認(rèn)就會出現(xiàn)類似這種情蒸眠,比如Thread-1漾橙,這種名稱的線程對我們來說幾乎是沒有任何意義的,我們暫且把它稱為“匿名線程”楞卡,解決匿名線程的手段有很多霜运,之前在學(xué)完ASM Tree api脾歇,再也不怕hook了這篇我們可以看到,我們可以用asm對調(diào)用thread進(jìn)行插樁淘捡,通過改變指令調(diào)用函數(shù)藕各,把普通的空參數(shù)Thread()方法變成帶有name的構(gòu)造方法Thread(String)進(jìn)行hook處理褥紫,把調(diào)用者名稱的信息放到前置的ldc指令来破,從而到達(dá)一個轉(zhuǎn)化的效果母谎。

737008951c21ac8fb01d378349b69475.png

asm 代碼實例如下:

method.instructions.insertBefore(
        node, new LdcInsnNode(klass.name))
                def r = node.desc.lastIndexOf(')')
                把構(gòu)造函數(shù)描述變成了帶有string name的構(gòu)造函數(shù)描述
                def desc = "${node.desc.substring(0, r)}Ljava/lang/String;${node.desc.substring(r)}"println(" * ${node.owner}.${node.name}${node.desc} => ${node.owner}.${node.name}$desc: ${klass.name}.${method.name}${method.desc}")node.desc = desc

當(dāng)然坛善,Thread還有很多構(gòu)造函數(shù)乌询,我們就不一一舉例子去適配阔馋,相關(guān)的操作也是類似的擎椰,涉及到Executors等其他創(chuàng)建線程的方式膨桥,我們也可以通過這種指令替換的方式去進(jìn)行Thread的命名操作创葡。

線程統(tǒng)一

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

在項目中蓝翰,我們有各種各樣的線程調(diào)度api,直接new Thread女嘲,Executors畜份,ThreadPoolExecutor等等,它們公共點就是都用到了Thread欣尼,最終都是靠著Thread去運行爆雹,但是想要把它們統(tǒng)一起來,我們要兼顧更上一層的api愕鼓,那么適配工作量可是不少8铺!那么我們有沒有一種黑科技菇晃,能夠簡單點就把線程統(tǒng)一到一個特定的線程池册倒,作為收口呢?(注意這里討論的是把全項目的線程統(tǒng)一磺送,包括三方庫)驻子,為了找到突破點灿意,我們先看一下最基本的Thread是怎么創(chuàng)建出來的

Thread創(chuàng)建

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

var thread = Thread{    Log.i("hello","this is my thread ${Thread.currentThread().name}")}

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

NEW java/lang/ThreadDUPINVOKEDYNAMIC run()Ljava/lang/Runnable; [ 
 // handle kind 0x6 : 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,
          // handle kind 0x6 : INVOKESTATIC  
          com/example/spider/MainActivity.onCreate$lambda-0()V,   ()V]INVOKESPECIAL java/lang/Thread.<init> (Ljava/lang/Runnable;)VASTORE 2

我們來一一說明下調(diào)用的指令:

  1. NEW 創(chuàng)建一個java/lang/Thread對象域慷,此時只是引用被創(chuàng)建鞭执,所引用的對象還沒有創(chuàng)建,并加入操作數(shù)棧頂部芒粹。
8b759f94e51dacdd69c29b00d12cb38a.jpg
  1. DUP 將操作數(shù)棧頂部的參數(shù)復(fù)制一份兄纺,并加入操作數(shù)棧。
de8af0d76ab683d3bc730c1dbed5e7d0.jpg

3.INVOKEDYNAMIC lambad用到的函數(shù)調(diào)用指令化漆,運行時綁定信息估脆,()Ljava/lang/Runnable,由于入?yún)閚ull座云,所以不消耗操作數(shù)棧的參數(shù)疙赠,返回值是Runnable,所以會在操作數(shù)棧上新加入一個Runnable對象朦拖。


2533c1a3c1e47d40fc0870237fb9c302.jpg

4.INVOKESPECIAL 構(gòu)造函數(shù)能調(diào)用到的特殊指令圃阳,即創(chuàng)建一個對象,(Ljava/lang/Runnable;)V璧帝,我們看到入?yún)⒅挥幸粋€Runnable對象捍岳,但是實際上調(diào)用INVOKESPECIAL的構(gòu)造函數(shù)隱藏了一個條件,就是需要一個被創(chuàng)建對象對應(yīng)的引用對象睬隶,這就是dup存在的原因锣夹,因為需要消耗一個Thread引用對象!這點需要注意苏潜。

25308b0434ac8f064569af20e1dc6836.jpg

5.ASTORE 2银萍,就是把操作數(shù)棧頂部的變量放到了局部變量表index為2的地方,這里為什么是2呢恤左,是由當(dāng)前運行環(huán)境決定的贴唇,靜態(tài)方法中index為0的就是參數(shù)1,而普通方法index為0的地方卻是this指針飞袋,這點是需要注意的戳气,除了index = 0 的地方有這個約定,其他index下標(biāo)其實就是函數(shù)環(huán)境的決定的授嘀。(這也側(cè)面說明物咳,存在AStore锣险,ALoad這些指令的時候蹄皱,我們很難去做通用性插樁览闰,因為這里依賴了局部變量表的具體實現(xiàn))

524bf0eb969ec653ccb6942b05f82549.jpg

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

那么我們想想看压鉴,怎么達(dá)到我們統(tǒng)一線程池的目的《途校看到Thread的創(chuàng)建過程我們就知道油吭,Thread會依賴局部變量表(第5條),所以我們?nèi)绻苯訉hread進(jìn)行操作的話署拟,是不行的婉宰,因為局部變量表的存儲index是依靠當(dāng)前環(huán)境的!其實我們統(tǒng)一線程池推穷,想要統(tǒng)一的也不一定是要統(tǒng)一Thread心包,而是統(tǒng)一Runnable執(zhí)行的線程環(huán)境對吧!突破點就來了馒铃,我們對Runnable進(jìn)行操作蟹腾,把其原本依賴執(zhí)行的Thread變成我們自己線程池的Thread是不是就可以了!

目標(biāo)明確了区宇,但是我們也需要為此做一些特定的處理娃殖,因為這種自定義指令集的處理,用其他ASM工具也是無法生成的议谷,所以我們才具體解釋相關(guān)的指令集炉爆。最終這邊的方案就是,進(jìn)行Thread調(diào)用替換卧晓,即把new Thread這個指令叶洞,替換為我們自己的MyThread的指令進(jìn)行定制化處理。步驟如下:

  1. 替換原本的INVOKESPECIAL指令調(diào)用為我們自己的MyThread調(diào)用禀崖,這里給出MyThread實現(xiàn)衩辟。
class MyThread(private val runnable: Runnable) : Thread(runnable) {
   // 調(diào)用到自己的start
   override fun start() { 
      Log.i("hello", "MyThread")
       // runnable 在定義的統(tǒng)一線程池執(zhí)行
       ThreadHelper.runInCustomPool(runnable)
   }
}
f55fb501593d1e84d10645646458bf41.jpg
  1. 原本指令返回的是Thread,由于我們替換為了MyThread波附,那么原本跟Thread強綁定的NEW指令艺晴,DUP指令就也需要變更跟MyThread類型相關(guān)的指令,我們這里就不采用替換掸屡,采取新加的方式封寞。(替換也可以,這里選擇方便處理仅财,因為操作數(shù)只對棧頂元素生效)
daec00ddb2a48a817047b0ae4aadeac9.jpg
  1. 到了這一步狈究,還不行,因為我們原本要返回的是Thread對象盏求,現(xiàn)在變成了MyThread對象抖锥,所以我們需要一個轉(zhuǎn)化指令CHECKCAST亿眠。
c2c16b463de2af82339db642c76a13c9.jpg

我們給出具體的ASM代碼:

class MyThreadHookUtils {
    static THREAD = "java/lang/Thread"    static void transform(ClassNode klass) {
            // 我們自定義的MyThread類不需要參加轉(zhuǎn)化
            if (klass.name.equals("com/example/spider/MyThread")) {
                return
            }
            klass.methods?.forEach { methodNode ->            methodNode.instructions.each {
                    if (it.opcode == Opcodes.INVOKESPECIAL) {
                        transformInvokeSpecial((MethodInsnNode) it, klass, methodNode)
                    }
                }
            }
        }
        private static void transformInvokeSpecial(MethodInsnNode node, ClassNode klass, MethodNode method) {
            // 如果不是構(gòu)造函數(shù),就直接退出
             if (node.owner != THREAD) {
                return
            }
            println("transformInvokeSpecial")
            transformThreadInvokeSpecial(node, klass, method)
        }
        private static void transformThreadInvokeSpecial( MethodInsnNode node,            ClassNode klass,            MethodNode method    ) { 
           println("init  ===>  " + node.desc + " " + node.owner) 
           if (node.desc.equals("(Ljava/lang/Runnable;)V")) {
                int index = method.instructions.indexOf(node) 
               def dyc = method.instructions[index - 1]
                InsnList insertNodes1 = new InsnList()
                TypeInsnNode newInsnNode = new TypeInsnNode(Opcodes.NEW, "com/example/spider/MyThread")
                InsnNode dupNode = new InsnNode(Opcodes.DUP)
                insertNodes1.add(newInsnNode)
                insertNodes1.add(dupNode) 
               method.instructions.insertBefore(dyc, insertNodes1) 
               MethodInsnNode methodHookNode = new MethodInsnNode(Opcodes.INVOKESPECIAL, "com/example/spider/MyThread","<init>",                  "(Ljava/lang/Runnable;)V", false)
                TypeInsnNode typeInsnNode = new TypeInsnNode(Opcodes.CHECKCAST, "java/lang/Thread")
                InsnList insertNodes = new InsnList() 
                insertNodes.add(methodHookNode)
                insertNodes.add(typeInsnNode)
                method.instructions.insertBefore(node, insertNodes) 
                method.instructions.remove(node)
                println("hook  ===>  " + node.name + " " + node.owner + " " + method.instructions.indexOf(node))
            } 
       }
    }

這個時候磅废,任何Thread的start方法或者其他方法纳像,都會調(diào)用到我們自定義的MyThread類的方法里面,在這里做線程池統(tǒng)一的處理拯勉,就非常方便了竟趾,因為我們有Runnable對象!同時所以方法我們都可以隨意去玩了宫峦!

注意

注意的是岔帽,這種全局Thread插樁是有風(fēng)險的,在實際項目中导绷,我們會通過白名單的方式山卦,選擇性的去統(tǒng)一部分Thread,因為全局統(tǒng)一容易導(dǎo)致不可預(yù)期的問題诵次。同時還有一個非常注意的點账蓉,我們可以看到上面關(guān)于指令的代碼全部是基于index的去定位各種指令集的,NEW -> DUP ->INVOKEDYNAMIC ->INVOKESPECIAL 然而在真實項目中逾一,這個指令集順序不一定可靠铸本,因為可能會被插入其他指令或者無關(guān)指令,所以我們還有一步就是指令順序的校驗遵堵,必須是滿足NEW -> DUP ->INVOKEDYNAMIC ->INVOKESPECIAL這幾個順序的函數(shù)指令集才進(jìn)行插樁箱玷,這部分內(nèi)容比較簡單,就不列舉了陌宿,比較INSN指令的OpCode即可锡足,校驗規(guī)則按照項目實際需要。

總結(jié)

看到這里壳坪,我們對Thread應(yīng)該有了足夠的了解舶得,同時本篇也介紹了ASM相關(guān)黑科技操作在Thread類的使用!同時也是android 性能優(yōu)化系列的第一篇爽蝴,在未來我會繼續(xù)輸出更多的文章沐批,感謝大家觀看!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末蝎亚,一起剝皮案震驚了整個濱河市九孩,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌发框,老刑警劉巖躺彬,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡宪拥,警方通過查閱死者的電腦和手機(jī)仿野,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來江解,“玉大人,你說我怎么就攤上這事徙歼±绾樱” “怎么了?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵魄梯,是天一觀的道長桨螺。 經(jīng)常有香客問我,道長酿秸,這世上最難降的妖魔是什么灭翔? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮辣苏,結(jié)果婚禮上肝箱,老公的妹妹穿的比我還像新娘。我一直安慰自己稀蟋,他們只是感情好煌张,可當(dāng)我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著退客,像睡著了一般骏融。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上萌狂,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天档玻,我揣著相機(jī)與錄音,去河邊找鬼茫藏。 笑死误趴,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的务傲。 我是一名探鬼主播冤留,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼树灶!你這毒婦竟也來了纤怒?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤天通,失蹤者是張志新(化名)和其女友劉穎泊窘,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡烘豹,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年瓜贾,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片携悯。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡祭芦,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出憔鬼,到底是詐尸還是另有隱情龟劲,我是刑警寧澤,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布轴或,位于F島的核電站昌跌,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏照雁。R本人自食惡果不足惜蚕愤,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望饺蚊。 院中可真熱鬧萍诱,春花似錦、人聲如沸污呼。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽曙求。三九已至碍庵,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間悟狱,已是汗流浹背静浴。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留挤渐,地道東北人苹享。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像浴麻,于是被迫代替她去往敵國和親得问。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,722評論 2 345