<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
在我們日常開發中,多執行緒管理一直是非常頭疼的問題之一,尤其在歷史性長,結構複雜的app中,執行緒數會達到好幾百個甚至更多,然而過多的執行緒不僅僅帶來了記憶體上的消耗同時也降低了cpu排程的效率,過多的cpu排程帶來的消耗的壞處甚至超過了多執行緒帶來的好處。
在我們日常開發中,通常會遇到以下幾個問題
最終種種原因導致,我們的專案在上線過程中,會遇到各種執行緒不明的情況,對排查問題或者解決問題帶來極大的考驗。
對於上述問題的解決,許多團隊通過codeview去限制程式碼准入,比如客製化Thread的規範,又或者是定義專案統一的執行緒池,在專案中去使用。這個方案優點就是可操作性強,便於團隊去實施,但是這比較依靠review(或者其他程式碼掃描外掛),對於歷史專案來說比較容易出現疏漏,而且後期也依舊需要維護,對於大型團隊來說,需要兼顧所有人程式碼,且三方庫無法處理。同時Thread的衍生物也有很多,比如Android中的HandlerThread等等,也是執行緒。
現在比較流行的方案是通過位元組碼插樁的方式,統一做執行緒監控亦或進行執行緒統一,比如監控處理的matrix,還有優化相關的booster等。執行緒統一這個依靠專案的情況,會有全統一執行緒池的情況(所以共用一個執行緒池),也有統一某單一業務的執行緒池的情況(比如只收口專案okhttp的執行緒池)下面我們圍繞這兩個主題,分別進行探討
對執行緒的監控,首先我們要統計當前的資訊對不對,可以直接通過
Thread.getAllStackTraces()
獲取到當前所有thread的資訊與堆疊情況,其返回值是一個map物件,
Map<Thread, StackTraceElement[]>
獲取結果例子如下
[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物件,如果我們要設計一個自己的apm的話可以通過遍歷key拿到一個Thread物件,然後再通過該Thread物件拿到自身的資訊即可,比如獲取thread的名稱
Thread.getAllStackTraces().keys.map { it.name }
通過上述,我們可以拿到了當前所有的執行緒資訊,但是很遺憾的是,其中有一些執行緒資訊幾乎是“不可用”的,比如我們用new Thread構建出來的執行緒,如果不給它指定的名字的話,預設就會出現類似這種情,比如Thread-1,這種名稱的執行緒對我們來說幾乎是沒有任何意義的,我們暫且把它稱為“匿名執行緒”,解決匿名執行緒的手段有很多,之前在學完ASM Tree api,再也不怕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, new LdcInsnNode(klass.name) ) def r = node.desc.lastIndexOf(')') 把建構函式描述變成了帶有string name的建構函式描述 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
當然,Thread還有很多建構函式,我們就不一一舉例子去適配,相關的操作也是類似的,涉及到Executors等其他建立執行緒的方式,我們也可以通過這種指令替換的方式去進行Thread的命名操作。這裡就不再贅述,可以參考booster 的做法
執行緒的統一可以依靠專案統一的執行緒池,但是這個約束不到第三方,我們可以利用ASM等工具進行執行緒的統一,執行緒統一包括全模組統一跟單模組統一(特定模組),由於單模組統一涉及具體業務,比如對okhttpclient的排程執行緒統一,由於不具備通用性,需要根據模組具體實現去統一,我們這裡就不討論了,單模組統一有個好處就是風險低,隻影響單一模組的執行緒排程。我們討論一下全模組的統一。
在專案中,我們有各種各樣的執行緒排程api,直接new Thread,Executors,ThreadPoolExecutor等等,它們公共點就是都用到了Thread,最終都是靠著Thread去執行,但是想要把它們統一起來,我們要兼顧更上一層的api,那麼適配工作量可是不少!!那麼我們有沒有一種黑科技,能夠簡單點就把執行緒統一到一個特定的執行緒池,作為收口呢?(注意這裡討論的是把全專案的執行緒統一,包括三方庫),為了找到突破點,我們先看一下最基本的Thread是怎麼建立出來的
最常用的Thread建立肯定是最簡單的,我們舉個例子
var thread = Thread{ Log.i("hello","this is my thread ${Thread.currentThread().name}") }
那麼這段程式碼它做了什麼呢?我們要從位元組碼的角度去分析,才能找到突破點
NEW java/lang/Thread DUP INVOKEDYNAMIC 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;)V ASTORE 2
我們來一一說明下呼叫的指令:
2. DUP 將運算元棧頂部的引數複製一份,並加入運算元棧
3.INVOKEDYNAMIC lambad用到的函數呼叫指令,執行時繫結資訊,()Ljava/lang/Runnable,由於入參為null,所以不消耗運算元棧的引數,返回值是Runnable,所以會在運算元棧上新加入一個Runnable物件
4.INVOKESPECIAL 建構函式能呼叫到的特殊指令,即建立一個物件,(Ljava/lang/Runnable;)V,我們看到入參只有一個Runnable物件,但是實際上呼叫INVOKESPECIAL的建構函式隱藏了一個條件,就是需要一個被建立物件對應的參照物件,這就是dup存在的原因,因為需要消耗一個Thread參照物件!這點需要注意
5.ASTORE 2,就是把運算元棧頂部的變數放到了區域性變數表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呼叫替換,即把new Thread這個指令,替換為我們自己的MyThread的指令進行客製化化處理。步驟如下
class MyThread(private val runnable: Runnable) : Thread(runnable) { // 呼叫到自己的start override fun start() { Log.i("hello", "MyThread") // runnable 在定義的統一執行緒池執行 ThreadHelper.runInCustomPool(runnable) } }
3.到了這一步,還不行,因為我們原本要返回的是Thread物件,現在變成了MyThread物件,所以我們需要一個轉化指令CHECKCAST
我們給出具體的ASM程式碼
class MyThreadHookUtils { static THREAD = "java/lang/Thread" static void transform(ClassNode klass) { // 我們自定義的MyThread類不需要參加轉化 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) { // 如果不是建構函式,就直接退出 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方法或者其他方法,都會呼叫到我們自定義的MyThread類的方法裡面,在這裡做執行緒池統一的處理,就非常方便了,因為我們有Runnable物件!同時所以方法我們都可以隨意去玩了!
注意的是,這種全域性Thread插樁是有風險的,在實際專案中,我們會通過白名單的方式,選擇性的去統一部分Thread,因為全域性統一容易導致不可預期的問題。同時還有一個非常注意的點,我們可以看到上面關於指令的程式碼全部是基於index的去定位各種指令集的,NEW -> DUP ->INVOKEDYNAMIC ->INVOKESPECIAL 然而在真實專案中,這個指令集順序不一定可靠,因為可能會被插入其他指令或者無關指令,所以我們還有一步就是指令順序的校驗,必須是滿足NEW -> DUP ->INVOKEDYNAMIC ->INVOKESPECIAL這幾個順序的函數指令集才進行插樁,這部分內容比較簡單,就不列舉了,比較INSN指令的OpCode即可,校驗規則按照專案實際需要。
看到這裡,我們對Thread應該有了足夠的瞭解,同時本篇也介紹了ASM相關黑科技操作在Thread類的使用!更多關於Android執行緒監控執行緒統一的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45