日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網為廣大站長提供免費收錄網站服務,提交前請做好本站友鏈:【 網站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(50元/站),

點擊這里在線咨詢客服
新站提交
  • 網站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會員:747

近日,據報道,甲骨文已經與TikTok的中國所有者字節跳動進行了初步談判,并認真考慮購買該應用在美國、加拿大、澳大利亞和新西蘭的業務。知情人士還補充稱,甲骨文正在與一群已經持有字節跳動股票的美國投資者合作,包括美國泛大西洋投資集團和紅杉資本。

/ 前言 /

當大家打開AndroidStudio的Profiler工具時,是否遇到過這種情況:

當你懂了以下的技巧,優化創建的幾百個線程不是問題

 

哇塞好幾百個線程??名字咋都是12345?怎么都在sleep或wait但就不銷毀?

其實,當一個項目規模越來越大時,隨著開發人員變更、老代碼不規范、三方sdk引入越來越多,很難避免線程數量暴漲的問題。當線程過多時,不僅有oom風險,更會帶來很多內存泄漏的隱患。但通過Profiler工具也只是知道線程數量,用Thread.getAllStackTraces()方法獲取到的也只有線程的運行時堆棧,即到run()方法就結束了,并不知道start()是被誰調用的。所以也就無法得知當前線程所對應的業務邏輯,以及它在當前時刻是否本應該被銷毀。

/ 思路 /

那如何才能得知一個線程start()方法的調用棧呢?如果我們有個BaseThread,然后所有的線程都使用或繼承于它,就好辦了,我們可以在start()方法中獲取堆棧信息:

@Synchronized
override fun start() {
    val stackElements = Throwable().stackTrace
    super.start()
}

想要做到這一點,可以使用asm編譯期間改字節碼來完成。這里簡單介紹下asm,它可以通過自定義gradle插件,在代碼被編譯成class后、打包成dex前,把項目中的所有class文件(包括你自己寫的,和三方jar包中的)遍歷一遍,遍歷期間你就可以通過asm任意修改class字節碼,以達到各種不可告人的目的。那么我們就可以通過asm來把項目中的Thread都替換掉:

new Thread() -> new BaseThread()
extends Thread -> extends BaseThread

/ 操作 /

現在有了調用棧,如何將調用棧和線程建立聯系呢?其實Thread初始化時已經產生了線程id,我們可以在start()中把線程id和對應棧信息放到一個map中。另外我們還可以復寫run()方法,當super.run()執行完后,線程即將銷毀,我們可以在此時把map中對應信息移除掉。

好了!就是這么簡單,接下來看線程池。線程池中的線程按剛剛的方法是替換不掉的,因為這些代碼在framework層,asm管不著。并且對于線程池來說,我們需要的不是它的線程在哪被啟動,而是線程正在執行的task是從哪里被添加的,這樣我們才知道是哪個業務添加了task使得線程一直在運行。

線程池一般有兩種創建方法,直接new和調用Executors.xxx,我們先看new出來的,按照上述套路,搞個BaseThreadPool,使用asm全部替換掉,然后在構造函數中獲取線程池創建堆棧,再復寫execute、submit、invokeAny等提交task的方法,在其中獲取task添加堆棧。但獲取到的堆棧如何跟線程對應上呢?我們知道提交的task都是Runnable或Callable形式,如果我們寫個PoolRunnable把它包一層,把線程池名字和task添加棧傳進去,然后在run()中調用Thread.currentThread()獲取當前線程,就把線程池名、線程池創建棧、線程id、task添加棧都關聯起來了。

class PoolRunnable constructor(
    private val any: Any,
    private val callStack: String,
    private val poolName: String? = null
) : Runnable, Callable<Any>, Comparable<Any> {

    override fun run() {
        val threadId = Thread.currentThread().id
        // 至此callStack、poolName、thread就可以全部關聯上
        // poolName和poolCreateStack可在外面事先創建關聯

        (any as Runnable).run()

        // 任務已執行結束,callStack表示task添加棧,此時應為空代表線程當前無任務在運行
        info.callStack = ""
    }

    override fun call(): Any {
        // 類似run()方法
    }

    override fun compareTo(other: Any): Int {
        // 省略代碼
    }
}

這里有一點要說明,有些task可能同時繼承于Runnable、Callable、甚至Comparable,如果只包裝成Runnable,當調用其他接口方法時會crash,所以這里要把已知的可能會繼承的接口都實現。(當然如果后面發現沒覆蓋全,可以把新的接口繼續加進來。至于用戶自定義Runnable實現很多接口的情況不用擔心。因為替換為PoolRunnable后都是系統代碼在運行,主要看系統代碼是否會調用call()、compareTo()等方法就好,上層隨意調用被包裝的runnable中各種自定義方法是沒問題的)。

但是對于建立“線程-線程池”關系來說,這樣要等到run()方法被執行時才能建立關系,感覺有點晚。這里可以更進一步,替換掉線程池中threadFactory,同樣是自己包一層,在newThread()方法中即可及時拿到線程池創建的線程,這樣就可以先和線程池建立關聯,然后run()時再和堆棧建立關聯。

好了!new方式創建的線程池搞定,接下來看看Executors.xxx怎么玩,我們可不可以自己寫個ProxyExecutors,使用asm把所有Executors.xxx都替換成ProxyExecutors.xxx,然后把其中new ThreadPool都換成new BaseThreadPool呢?

object ProxyExecutors {

    @JvmStatic
    fun newFixedThreadPool(nThreads: Int): ExecutorService {
        return BaseThreadPoolExecutor(
            nThreads, nThreads,
            0L, TimeUnit.MILLISECONDS,
            LinkedBlockingQueue()
        )
    }

    @JvmStatic
    fun newCachedThreadPool(): ExecutorService {
        return BaseThreadPoolExecutor(
            0, Int.MAX_VALUE,
            60L, TimeUnit.SECONDS,
            SynchronousQueue()
        )
    }

    @JvmStatic
    fun newScheduledThreadPool(
        corePoolSize: Int,
        threadFactory: ThreadFactory?
    ): ScheduledExecutorService {
        return BaseScheduledThreadPoolExecutor(corePoolSize, threadFactory)
    }
}

對于某些線程池確實沒啥問題,但當你繼續寫下去時,會出問題,比如:

 @JvmStatic
    fun newSingleThreadScheduledExecutor(): ScheduledExecutorService {
        return BaseDelegatedScheduledExecutorService(
            ScheduledThreadPoolExecutor(1)
        )
    }

DelegatedScheduledExecutorService是Executors的私有內部類,沒辦法方便的寫出BaseDelegatedScheduledExecutorService。所以繼續觀察下,發現所有創建線程池方法都返回的都是ExecutorService或ScheduledExecutorService,既然這么統一,并且這兩個都是接口,那我們就使用動態代理吧!通過代理,就可以拿到接口的各種方法以及方法參數,然后為所欲為。以ExecutorService接口為例:

object ProxyExecutors {
    @JvmStatic
    fun newFixedThreadPool(nThreads: Int): ExecutorService {
        return proxy(Executors.newFixedThreadPool(nThreads))
    }
}
private fun proxy(executorService: ExecutorService): ExecutorService {
        if (executorService is ThreadPoolExecutor) {
            // 這里和BaseThreadPoolExecutor一樣,設置ThreadFactory為了盡早獲取線程信息和線程池建立聯系,而不用等到run時
            executorService.threadFactory = BaseThreadFactory(
                executorService.threadFactory,
                toObjectString(executorService)
            )
        }
        val handler = ProxyExecutorService(executorService)
        return Proxy.newProxyInstance(
            executorService.JAVAClass.classLoader,
            AbstractExecutorService::class.java.interfaces,
            handler
        ) as ExecutorService
    }
// 這里使用java是因為kotlin在調用java變長參數方法時有坑
public class ProxyExecutorService implements InvocationHandler {
    private ExecutorService executor;
    private String poolName = null;

    ProxyExecutorService(ExecutorService executor) {
        this.executor = executor;
        poolName = TrackerUtils.toObjectString(executor);
        // 初始化時獲取線程池信息
        String createStack = TrackerUtils.getStackString(false);
        // 省略部分代碼
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 因方法數眾多,并且被代理的各類中方法也不一致
        // 所以被調用方法中只要含有Runnable、Callable類型的參數,都替換成PoolRunnable代理
        if (args != null) {
            String callStack = TrackerUtils.getStackString(true);
            for (int i = 0; i < args.length; i++) {
                Object arg = args[i];
                if ((arg instanceof Runnable || arg instanceof Callable) && !(arg instanceof PoolRunnable)) {
                    // execute submit 等情況
                    PoolRunnable any = new PoolRunnable(arg, callStack, poolName);
                    // 替換方法參數
                    args[i] = any;
                } else if (arg instanceof Collection && !((Collection) arg).isEmpty()) {
                    // invokeAny invokeAll 等情況
                    Iterator iter = ((Collection) arg).iterator();
                    ArrayList<PoolRunnable> taskList = new ArrayList<>();
                    boolean allOk = iter.hasNext();
                    while (iter.hasNext()) {
                        Object it = iter.next();
                        if (it instanceof Runnable || it instanceof Callable) {
                            if (it instanceof PoolRunnable) {
                                taskList.add((PoolRunnable) it);
                            } else {
                                taskList.add(new PoolRunnable(it, callStack, poolName));
                            }
                        } else {
                            allOk = false;
                            break;
                        }
                    }
                    if (allOk) {
                        // 替換方法參數
                        args[i] = taskList;
                    }
                }
            }
        }

        if (method.getName().equals("shutdown") || method.getName().equals("shutdownNow")) {
            ThreadInfoManager.getINSTANCE().shutDownPool(poolName);
        }
        return method.invoke(executor, args);
    }
}

在invoke方法中,我們把所有參數為Runnable或Callable的都替換成自己的PoolRunnable,后面和new創建線程池的套路一樣,在PoolRunnable的run()或call()方法中進行線程與堆棧的關聯。完美!

但是考慮一個問題,如果有人不幸寫出這樣的代碼,會發生什么呢?

val pool = Executors.newFixedThreadPool(3) as ThreadPoolExecutor

對了,會crash,因為我們動態代理后代理對象變成了ExecutorService,沒辦法向下轉型成ThreadPoolExecutor,這也是動態代理的一個缺點,只能代理接口,如果一個類A除了實現接口還有很多自己的方法,動態代理對這些方法是無能為力的,代理后對象只是接口的實例,無法轉成類A。這個問題可以使用cglib/javassist庫解決,這里就不展開了。為了保險起見,我們可以把能用Base替換的都替換掉,只有類似newSingleThreadScheduledExecutor()這種不能用Base替換的才用動態代理:

object ProxyExecutors {

    @JvmStatic
    fun newFixedThreadPool(nThreads: Int): ExecutorService {
        // 這里使用BaseThreadPoolExecutor主要是為了避免上層代碼把ExecutorService轉型成ThreadPoolExecutor的問題,如果使用proxy方法動態代理,上層這么做會crash
        return BaseThreadPoolExecutor(
            nThreads, nThreads,
            0L, TimeUnit.MILLISECONDS,
            LinkedBlockingQueue()
        )
        // return proxy(Executors.newFixedThreadPool(nThreads))
    }

ok,線程池搞定了。但除了線程和線程池,Java和Android中還有很多封裝了線程線程池的類。比如HandlerThread,Timer,ASyncTask等,我們先處理這幾個常用類吧。

HandlerThread本質上就是Thread,采用和Thread類似的處理方式就好。

Timer內部有個私有的TimerThread成員,本質也是個Thread,看源碼可知Timer在構造方法中便啟動了這個Thread,所以調用棧也就是Timer被初始化的棧。我們可以新建BaseTimer,使用asm將代碼中的Timer替換掉,然后構造方法中獲取調用棧,再通過反射拿到內部Thread成員,獲取其id等信息,與調用棧關聯即可。

ASyncTask這個相對來說難辦一些,這里面主要有兩個Executor:THREAD_POOL_EXECUTOR和SERIAL_EXECUTOR,其中主要干活的是THREAD_POOL_EXECUTOR,而SERIAL_EXECUTOR并不是真正意義上的線程池,只是實現了Executor接口而已。SERIAL_EXECUTOR的execute()方法在向一個隊列中添加任務,然后依次取出送入THREAD_POOL_EXECUTOR中執行,所以我們需要THREAD_POOL_EXECUTOR的線程池信息,關聯SERIAL_EXECUTOR的execute()任務添加棧信息。

而外界又可以直接用THREAD_POOL_EXECUTOR添加任務,這種情況下就又需要THREAD_POOL_EXECUTOR的任務添加棧信息了,所以這里需要一些特殊處理。總體思路仍然是動態代理SERIAL_EXECUTOR和THREAD_POOL_EXECUTOR,然后把代理設置回ASyncTask,時機為App啟動時。由于ASyncTask中這兩個線程池對象是final的,所以需要通過反射修改modifiers去掉final位。這在5.0及以上系統沒什么問題,但4.x的源碼中modifiers是通過native獲取的:

/**
     * Returns the modifiers for this field. The {@link Modifier} class should
     * be used to decode the result.
     *
     * @return the modifiers for this field
     * @see Modifier
     */
    public int getModifiers() {
        return getFieldModifiers(declaringClass, slot);
    }

    private native int getFieldModifiers(Class<?> declaringClass, int slot);

對此暫未找到修改方式,所以如果要追蹤ASyncTask,請使用Android 5.0及以上手機。

至此,線程/線程池溯源基本完成了,后續會繼續完善IntentService、ForkJoinPool和Java/Android源碼中封裝的其他不太常用的線程/線程池封裝類。

初版效果長這樣:

當你懂了以下的技巧,優化創建的幾百個線程不是問題

 


當你懂了以下的技巧,優化創建的幾百個線程不是問題

 

可以看到界面中對用戶代碼做了高亮,幫助用戶在迷亂的調用棧中一眼看到問題根源。其原理大致是在asm掃描class過程中,根據獲取到的項目信息來記錄哪些是用戶寫的代碼(不能直接根據是否為jar包來判斷,如果項目存在多個module,可能這些module最后也是以jar包形式被asm掃描),然后把相關class包名記下來,過濾掉包含有“已有短包名”的“較長包名”,把這些包名list通過asm寫入到指定Java類的指定ArrayList成員中。在獲取到調用棧后,可以和這些包名list對比,進行高亮。

/ 總結 /

最后,此文作為拋磚引玉,提供一種線程溯源的思路。其實可以看出asm還是很強大的,能做什么就取決于大家的想象力了。后面會逐漸完善新功能,比如記錄線程運行時間,根據線程各維度狀態篩選/排序等,統計線程所屬包名或jar文件名,看看那些三方庫在為非作歹(特別是某些廣告sdk,簡直。。),甚至對線程/線程池運行狀態可疑的給出警告和優化建議等等。

最最后,作為asm初學者,很多用法還比較初級,如果有更優雅的實現方式,歡迎瘋狂pull requests;另外,該項目雖然目前在咕咚這個比較龐大復雜的app上可正常運行,但難免還有考慮不周全的地方,畢竟替換字節碼是比較危險的操作。

以下是我整理的資料

Android核心知識點筆記github:https://github.com/AndroidCot/Android

分享到:
標簽:線程
用戶無頭像

網友整理

注冊時間:

網站:5 個   小程序:0 個  文章:12 篇

  • 51998

    網站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會員

趕快注冊賬號,推廣您的網站吧!
最新入駐小程序

數獨大挑戰2018-06-03

數獨一種數學游戲,玩家需要根據9

答題星2018-06-03

您可以通過答題星輕松地創建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學四六

運動步數有氧達人2018-06-03

記錄運動步數,積累氧氣值。還可偷

每日養生app2018-06-03

每日養生,天天健康

體育訓練成績評定2018-06-03

通用課目體育訓練成績評定