BlockCanary原理解析

来源:https://www.cnblogs.com/tangZH/Undeclared/17142437.html
-Advertisement-
Play Games

一、背景 為瞭解決應卡頓,分析耗時。 二、原理 Looper中的loop方法: public static void loop() { ... for (;;) { ... // This must be in a local variable, in case a UI event sets th ...


一、背景

為瞭解決應卡頓,分析耗時。

二、原理

Looper中的loop方法:

public static void loop() {
    ...

    for (;;) {
        ...

        // This must be in a local variable, in case a UI event sets the logger
        Printer logging = me.mLogging;
        if (logging != null) {
            logging.println(">>>>> Dispatching to " + msg.target + " " +
                    msg.callback + ": " + msg.what);
        }

        msg.target.dispatchMessage(msg);

        if (logging != null) {
            logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
        }

        ...
    }
}

可以看到在執行消息的時候,如果有設置logging,那麼它會在消息開始與結束的時候列印出相關信息。如果主線程卡住了,就是在dispatchMessage這裡卡住,所以我們可以通過計算這兩條log的時間差來判斷消息的執行時間。

我們可以通過這個方法來設置Printer。

Looper.getMainLooper().setMessageLogging(mainLooperPrinter);

三、源碼解析

application中調用初始化:
BlockCanary.install(this, AppBlockCanaryContext()).start()

最終會執行到:

    private BlockCanary() {
        BlockCanaryInternals.setContext(BlockCanaryContext.get());
        mBlockCanaryCore = BlockCanaryInternals.getInstance();
        mBlockCanaryCore.addBlockInterceptor(BlockCanaryContext.get());
        if (!BlockCanaryContext.get().displayNotification()) {
            return;
        }
        mBlockCanaryCore.addBlockInterceptor(new DisplayService());

    }

核心就是mBlockCanaryCore = BlockCanaryInternals.getInstance();它會對BlockCanaryInternals進行初始化。

    public BlockCanaryInternals() {

        stackSampler = new StackSampler(
                Looper.getMainLooper().getThread(),
                sContext.provideDumpInterval());

        cpuSampler = new CpuSampler(sContext.provideDumpInterval());

        setMonitor(new LooperMonitor(new LooperMonitor.BlockListener() {

            @Override
            public void onBlockEvent(long realTimeStart, long realTimeEnd,
                                     long threadTimeStart, long threadTimeEnd) {
                // Get recent thread-stack entries and cpu usage
                ArrayList<String> threadStackEntries = stackSampler
                        .getThreadStackEntries(realTimeStart, realTimeEnd);
                if (!threadStackEntries.isEmpty()) {
                    BlockInfo blockInfo = BlockInfo.newInstance()
                            .setMainThreadTimeCost(realTimeStart, realTimeEnd, threadTimeStart, threadTimeEnd)
                            .setCpuBusyFlag(cpuSampler.isCpuBusy(realTimeStart, realTimeEnd))
                            .setRecentCpuRate(cpuSampler.getCpuRateInfo())
                            .setThreadStackEntries(threadStackEntries)
                            .flushString();
                    LogWriter.save(blockInfo.toString());

                    if (mInterceptorChain.size() != 0) {
                        for (BlockInterceptor interceptor : mInterceptorChain) {
                            interceptor.onBlock(getContext().provideContext(), blockInfo);
                        }
                    }
                }
            }
        }, getContext().provideBlockThreshold(), getContext().stopWhenDebugging()));

        LogWriter.cleanObsolete();
    }

  • stackSampler:記錄棧相關信息
  • cpuSampler:記錄CPU相關信息
  • LooperMonitor:繼承Printer
    private void setMonitor(LooperMonitor looperPrinter) {
        monitor = looperPrinter;
    }

當我們調用BlockCanary的start方法的時候,便將其設給了Looper的printer,然後我們便可以在LooperMonitor的print方法裡面去記錄列印的log的時間。

    public void start() {
        if (!mMonitorStarted) {
            mMonitorStarted = true;
            Looper.getMainLooper().setMessageLogging(mBlockCanaryCore.monitor);
        }
    }

核心代碼:

    @Override
    public void println(String x) {
        if (mStopWhenDebugging && Debug.isDebuggerConnected()) {
            return;
        }
        if (!mPrintingStarted) {
            mStartTimestamp = System.currentTimeMillis();
            mStartThreadTimestamp = SystemClock.currentThreadTimeMillis();
            mPrintingStarted = true;
            startDump();
        } else {
            final long endTime = System.currentTimeMillis();
            mPrintingStarted = false;
            if (isBlock(endTime)) {
                notifyBlockEvent(endTime);
            }
            stopDump();
        }
    }

在開始執行消息的時候去記錄相關信息,結束消息的時候停止記錄相關信息,並且判斷消息執行的時間是否超過了我們設置的閾值,超過了的話便執行notifyBlockEvent(endTime);取出記錄的相關消息提示用戶。

說到此處,想到是不是可以用mainLooperPrinter來做更多事情呢?既然主線程都在這裡,那隻要parse出app包名的第一行,每次列印出來,是不是就不需要打點也能記錄出用戶操作路徑? 再者,比如想做onClick到頁面創建後的耗時統計,是不是也能用這個原理呢? 之後可以試試看這個思路(目前存在問題是獲取線程堆棧是定時3秒取一次的,很可能一些比較快的方法操作一下子完成了沒法在stacktrace裡面反映出來)。

我們看一下怎麼記錄棧以及cpu的消息的。

    private void startDump() {
        if (null != BlockCanaryInternals.getInstance().stackSampler) {
            BlockCanaryInternals.getInstance().stackSampler.start();
        }

        if (null != BlockCanaryInternals.getInstance().cpuSampler) {
            BlockCanaryInternals.getInstance().cpuSampler.start();
        }
    }

StackSampler與CpuSampler都繼承與AbstractSampler:
AbstractSampler裡面的start方法:

    public void start() {
        if (mShouldSample.get()) {
            return;
        }
        mShouldSample.set(true);

        HandlerThreadFactory.getTimerThreadHandler().removeCallbacks(mRunnable);
        HandlerThreadFactory.getTimerThreadHandler().postDelayed(mRunnable,
                BlockCanaryInternals.getInstance().getSampleDelay());
    }

    private Runnable mRunnable = new Runnable() {
        @Override
        public void run() {
            doSample();

            if (mShouldSample.get()) {
                HandlerThreadFactory.getTimerThreadHandler()
                        .postDelayed(mRunnable, mSampleInterval);
            }
        }
    };


    long getSampleDelay() {
        return (long) (BlockCanaryInternals.getContext().provideBlockThreshold() * 0.8f);
    }

它其實是開了一個子線程每隔一定的時間就去記錄。

四、流程圖

image

五、總結

BlockCanary作為一個Android組件,目前還有局限性,因為其在一個完整的監控系統中只是一個生產者,還需要對應的消費者去分析日誌,比如歸類排序,以便看出哪些卡慢更有修複價值,需要優先處理;又比如需要過濾機型,有些奇葩機型的問題造成的卡慢,到底要不要去修複是要斟酌的。扯遠一點的話,像是埋點除了統計外,完全還能用來做鏈路監控,比如一個完整的流程是A -> B -> D -> E, 但是某個時間節點突然A -> B -> D後沒有到達E,這時候監控平臺就可以發出預警,讓開發人員及時定位。很多監控方案都需要C/S兩端的配合。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 庫函數和系統調用 庫函數調用 系統調用 在所有的ANSI C編譯器中,C庫函數都是相同的 各個操作系統的系統調用是不同的,這導致程式不可移植 它調用庫函數中的一段程式(或函數) 它調用系統內核的服務 與用戶程式相聯繫 在內核地址空間執行 它的運行時間屬於“用戶時間” 運行時間屬於“系統時間” 屬於過 ...
  • Nginx採用虛擬目錄的方式代理IIS站點 起因 背景 由於IIS出現了某種不可知的問題,H5APP的部署從IIS改為Nginx。 H5APP的Nginx的部署比較簡單,直接修改官方的實例即可 但是之前H5站點中有一個虛擬目錄用於客戶單點登錄認證,所以需要在Nginx中添加對應的虛擬目錄,但是單點認 ...
  • 使用STM32CubeMX軟體配置STM32F407開發板上串口USART1進行DMA傳輸數據,然後實現與實驗STM32CubeMX教程9 USART/UART 非同步通信相同的目標 ...
  • 文件系統結構 unix的文件系統相關知識 unix將可用的磁碟空間劃分為兩種主要類型的區域:inode區域和數據區域。 unix為每個文件分配一個inode,其中保存文件的關鍵元數據,如文件的stat屬性和指向文件數據塊的指針。 數據區域中的空間會被分成大小相同的數據塊(就像記憶體管理中的分頁)。數據 ...
  • 1月9日,計世資訊(CCW Research)發佈《2022-2023年中國信創資料庫行業市場研究報告》(以下簡稱“報告”),從產品技術能力和市場及戰略能力兩個維度對我國主要資料庫產品服務商進行競爭力分析。其中,中國電信天翼雲憑藉其產品豐富的管理功能、靈活的部署架構,位列雲資料庫產品領域領導者象限。 ...
  • 作者:俊達 引言 MySQL是MySQL安裝包預設的客戶端,該客戶端程式通常位於二進位安裝包的bin目錄中,或者通過rpm安裝包安裝mysql-community-client,是資料庫管理系統的重要組成部分。MySQL客戶端不僅僅是一個簡單的軟體工具,更是連接用戶與資料庫之間的橋梁,對於有效地使用 ...
  • 作者:櫰木 環境準備 本次使用到的二進位軟體包目錄為:系統初始化前提是操作系統已完成安裝、各個主機之間網路互通,系統常用命令已安裝,本預設這些前提條件已具備,不在闡述。 1 主機環境初始化 安裝centos系統完成後需要對主機進行初始化配置和驗證工作,在所有主機上(hd1.dtstack.com-h ...
  • 摘要 隨著任務數量、任務類型需求不斷增長,對我們的數據開發平臺提出了更高的要求。本文主要分享我們將調度引擎升級到 Apache DolphinScheduler 的實踐經驗,以及對數據開發平臺的一些思考。 1. 背景 首先介紹下我們的大數據平臺架構: 數據計算層承接了全公司的數據開發需求,負責運行各 ...
一周排行
    -Advertisement-
    Play Games
  • 最近做項目過程中,使用到了海康相機,官方只提供了C/C++的SDK,沒有搜尋到一個合適的封裝了的C#庫,故自己動手,簡單的封裝了一下,方便大家也方便自己使用和二次開發 ...
  • 前言 MediatR 是 .NET 下的一個實現消息傳遞的庫,輕量級、簡潔高效,用於實現進程內的消息傳遞機制。它基於中介者設計模式,支持請求/響應、命令、查詢、通知和事件等多種消息傳遞模式。通過泛型支持,MediatR 可以智能地調度不同類型的消息,非常適合用於領域事件處理。 在本文中,將通過一個簡 ...
  • 前言 今天給大家推薦一個超實用的開源項目《.NET 7 + Vue 許可權管理系統 小白快速上手》,DncZeus的願景就是做一個.NET 領域小白也能上手的簡易、通用的後臺許可權管理模板系統基礎框架。 不管你是技術小白還是技術大佬或者是不懂前端Vue 的新手,這個項目可以快速上手讓我們從0到1,搭建自 ...
  • 第1章:WPF概述 本章目標 瞭解Windows圖形演化 瞭解WPF高級API 瞭解解析度無關性概念 瞭解WPF體繫結構 瞭解WPF 4.5 WPF概述 ​ 歡迎使用 Windows Presentation Foundation (WPF) 桌面指南,這是一個與解析度無關的 UI 框架,使用基於矢 ...
  • 在日常開發中,並不是所有的功能都是用戶可見的,還在一些背後默默支持的程式,這些程式通常以服務的形式出現,統稱為輔助角色服務。今天以一個簡單的小例子,簡述基於.NET開發輔助角色服務的相關內容,僅供學習分享使用,如有不足之處,還請指正。 ...
  • 第3章:佈局 本章目標 理解佈局的原則 理解佈局的過程 理解佈局的容器 掌握各類佈局容器的運用 理解 WPF 中的佈局 WPF 佈局原則 ​ WPF 視窗只能包含單個元素。為在WPF 視窗中放置多個元素並創建更貼近實用的用戶男面,需要在視窗上放置一個容器,然後在這個容器中添加其他元素。造成這一限制的 ...
  • 前言 在平時項目開發中,定時任務調度是一項重要的功能,廣泛應用於後臺作業、計劃任務和自動化腳本等模塊。 FreeScheduler 是一款輕量級且功能強大的定時任務調度庫,它支持臨時的延時任務和重覆迴圈任務(可持久化),能夠按秒、每天/每周/每月固定時間或自定義間隔執行(CRON 表達式)。 此外 ...
  • 目錄Blazor 組件基礎路由導航參數組件參數路由參數生命周期事件狀態更改組件事件 Blazor 組件 基礎 新建一個項目命名為 MyComponents ,項目模板的交互類型選 Auto ,其它保持預設選項: 客戶端組件 (Auto/WebAssembly): 最終解決方案裡面會有兩個項目:伺服器 ...
  • 先看一下效果吧: isChecked = false 的時候的效果 isChecked = true 的時候的效果 然後我們來實現一下這個效果吧 第一步:創建一個空的wpf項目; 第二步:在項目裡面添加一個checkbox <Grid> <CheckBox HorizontalAlignment=" ...
  • 在編寫上位機軟體時,需要經常處理命令拼接與其他設備進行通信,通常對不同的命令封裝成不同的方法,擴展稍許麻煩。 本次擬以特性方式實現,以兼顧維護性與擴展性。 思想: 一種命令對應一個類,其類中的各個屬性對應各個命令段,通過特性的方式,實現其在這包數據命令中的位置、大端或小端及其轉換為對應的目標類型; ...