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
  • 前言 插件化的需求主要源於對軟體架構靈活性的追求,特別是在開發大型、複雜或需要不斷更新的軟體系統時,插件化可以提高軟體系統的可擴展性、可定製性、隔離性、安全性、可維護性、模塊化、易於升級和更新以及支持第三方開發等方面的能力,從而滿足不斷變化的業務需求和技術挑戰。 一、插件化探索 在WPF中我們想要開 ...
  • 歡迎ReaLTaiizor是一個用戶友好的、以設計為中心的.NET WinForms項目控制項庫,包含廣泛的組件。您可以使用不同的主題選項對項目進行個性化設置,並自定義用戶控制項,以使您的應用程式更加專業。 項目地址:https://github.com/Taiizor/ReaLTaiizor 步驟1: ...
  • EDP是一套集組織架構,許可權框架【功能許可權,操作許可權,數據訪問許可權,WebApi許可權】,自動化日誌,動態Interface,WebApi管理等基礎功能於一體的,基於.net的企業應用開發框架。通過友好的編碼方式實現數據行、列許可權的管控。 ...
  • Channel 是乾什麼的 The System.Threading.Channels namespace provides a set of synchronization data structures for passing data between producers and consume ...
  • efcore如何優雅的實現按年分庫按月分表 介紹 本文ShardinfCore版本 本期主角: ShardingCore 一款ef-core下高性能、輕量級針對分表分庫讀寫分離的解決方案,具有零依賴、零學習成本、零業務代碼入侵適配 距離上次發文.net相關的已經有很久了,期間一直在從事java相關的 ...
  • 前言 Spacesniffer 是一個免費的文件掃描工具,通過使用樹狀圖可視化佈局,可以立即瞭解大文件夾的位置,幫助用戶處理找到這些文件夾 當前系統C盤空間 清理後系統C盤空間 下載 Spacesniffer 下載地址:https://spacesniffer.en.softonic.com/dow ...
  • EDP是一套集組織架構,許可權框架【功能許可權,操作許可權,數據訪問許可權,WebApi許可權】,自動化日誌,動態Interface,WebApi管理等基礎功能於一體的,基於.net的企業應用開發框架。通過友好的編碼方式實現數據行、列許可權的管控。 ...
  • 一、ReZero簡介 ReZero是一款.NET中間件 : 全網唯一開源界面操作就能生成API , 可以集成到任何.NET6+ API項目,無破壞性,也可讓非.NET用戶使用exe文件 免費開源:MIT最寬鬆協議 , 一直從事開源事業十年,一直堅持開源 1.1 純ReZero開發 適合.Net Co ...
  • 一:背景 1. 講故事 停了一個月沒有更新文章了,主要是忙於寫 C#內功修煉系列的PPT,現在基本上接近尾聲,可以回頭繼續更新這段時間分析dump的一些事故報告,有朋友微信上找到我,說他們的系統出現了大量的http超時,程式不響應處理了,讓我幫忙看下怎麼回事,dump也抓到了。 二:WinDbg分析 ...
  • 開始做項目管理了(本人3年java,來到這邊之後真沒想到...),天天開會溝通整理需求,他們講話的時候忙裡偷閑整理一下常用的方法,其實語言還是有共通性的,基本上看到方法名就大概能猜出來用法。出去打水的時候看到外面太陽好好,真想在外面坐著曬太陽,回來的時候好兄弟三年前送給我的鍵盤D鍵不靈了,在打"等待 ...