欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Android性能優(yōu)化之線程監(jiān)控與線程統(tǒng)一詳解

 更新時(shí)間:2022年09月17日 11:25:56   作者:Pika  
這篇文章主要為大家介紹了Android性能優(yōu)化之線程監(jiān)控與線程統(tǒng)一詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪

背景

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

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

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

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

常規(guī)解決方案

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

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

線程監(jiān)控

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

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

Thread.getAllStackTraces()

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

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

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

線程信息具體化

通過上述,我們可以拿到了當(dāng)前所有的線程信息,但是很遺憾的是,其中有一些線程信息幾乎是“不可用”的,比如我們用new Thread構(gòu)建出來的線程,如果不給它指定的名字的話,默認(rèn)就會(huì)出現(xiàn)類似這種情,比如Thread-1,這種名稱的線程對(duì)我們來說幾乎是沒有任何意義的,我們暫且把它稱為“匿名線程”,解決匿名線程的手段有很多,之前在學(xué)完ASM Tree api,再也不怕hook了這篇我們可以看到,我們可以用asm對(duì)調(diào)用thread進(jìn)行插樁,通過改變指令調(diào)用函數(shù),把普通的空參數(shù)Thread()方法變成帶有name的構(gòu)造方法Thread(String)進(jìn)行hook處理,把調(diào)用者名稱的信息放到前置的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,
        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的命名操作。這里就不再贅述,可以參考booster 的做法

線程統(tǒng)一

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

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

Thread創(chuàng)建

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

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

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

    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

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

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

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

3.INVOKEDYNAMIC lambad用到的函數(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.ASTORE 2,就是把操作數(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è)面說明,存在AStore,ALoad這些指令的時(shí)候,我們很難去做通用性插樁,因?yàn)檫@里依賴了局部變量表的具體實(shí)現(xiàn))

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

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

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

  • 替換原本的INVOKESPECIAL指令調(diào)用為我們自己的MyThread調(diào)用,這里給出MyThread實(shí)現(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)
   }
}

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

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

我們給出具體的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))
        }
    }
}

這個(gè)時(shí)候,任何Thread的start方法或者其他方法,都會(huì)調(diào)用到我們自定義的MyThread類的方法里面,在這里做線程池統(tǒng)一的處理,就非常方便了,因?yàn)槲覀冇蠷unnable對(duì)象!同時(shí)所以方法我們都可以隨意去玩了!

注意

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

總結(jié)

看到這里,我們對(duì)Thread應(yīng)該有了足夠的了解,同時(shí)本篇也介紹了ASM相關(guān)黑科技操作在Thread類的使用!更多關(guān)于Android線程監(jiān)控線程統(tǒng)一的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • Android屬性動(dòng)畫特點(diǎn)詳解

    Android屬性動(dòng)畫特點(diǎn)詳解

    這篇文章主要為大家詳細(xì)介紹了Android屬性動(dòng)畫特點(diǎn),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2018-11-11
  • android Palette調(diào)色板使用詳解

    android Palette調(diào)色板使用詳解

    本篇文章主要介紹了android Palette調(diào)色板使用詳解,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧
    2017-10-10
  • Android自定義View實(shí)現(xiàn)葉子飄動(dòng)旋轉(zhuǎn)效果(四)

    Android自定義View實(shí)現(xiàn)葉子飄動(dòng)旋轉(zhuǎn)效果(四)

    這篇文章主要為大家詳細(xì)介紹了Android自定義View實(shí)現(xiàn)葉子飄動(dòng)旋轉(zhuǎn)效果,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2017-03-03
  • 微信多圖上傳解決android多圖上傳失敗問題

    微信多圖上傳解決android多圖上傳失敗問題

    這篇文章主要介紹了微信多圖上傳解決android多圖上傳失敗問題,需要的朋友可以參考下
    2017-04-04
  • Android自定義ListView實(shí)現(xiàn)下拉刷新

    Android自定義ListView實(shí)現(xiàn)下拉刷新

    這篇文章主要為大家詳細(xì)介紹了Android自定義ListView實(shí)現(xiàn)下拉刷新的相關(guān)資料,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2016-07-07
  • Android 實(shí)現(xiàn)密碼輸入框動(dòng)態(tài)明文/密文切換顯示效果

    Android 實(shí)現(xiàn)密碼輸入框動(dòng)態(tài)明文/密文切換顯示效果

    在項(xiàng)目中遇到需要提供給用戶一個(gè)密碼輸入框明文/密文切換顯示的需求,今天小編借腳本之家平臺(tái)給大家分享下Android 實(shí)現(xiàn)密碼輸入框動(dòng)態(tài)明文/密文切換顯示效果,需要的朋友參考下
    2017-01-01
  • SimpleCommand框架ImageLoader API詳解(三)

    SimpleCommand框架ImageLoader API詳解(三)

    這篇文章主要為大家詳細(xì)介紹了SimpleCommand框架ImageLoader API,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2017-10-10
  • android webp編解碼詳解

    android webp編解碼詳解

    本文主要講解android webp編解碼,在Android開發(fā)過程中會(huì)遇到圖片的上傳和下載,這提供WebP編解碼縮小圖片,便與大家提高用戶體驗(yàn),有興趣的小伙伴可以參考下
    2016-08-08
  • Android復(fù)制assets文件到SD卡

    Android復(fù)制assets文件到SD卡

    這篇文章主要為大家詳細(xì)介紹了Android復(fù)制assets文件到SD卡的方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2018-12-12
  • Android banner詳解用法案例

    Android banner詳解用法案例

    android-banner實(shí)現(xiàn)了一般banner循環(huán)輪播的效果,一頁只顯示一張圖片,也可以一頁顯示一張圖和相鄰兩個(gè)圖片的一部分,此項(xiàng)目僅僅是banner展示圖片,沒有多余的諸如指示器、頁面切換動(dòng)畫等效果代碼,詳見效果圖和案例代碼
    2021-11-11

最新評(píng)論