你真的瞭解BlockCanary了嗎?讓我帶你走進源碼,深入瞭解BlockCanary的實現原理與使用

這次疫情還沒有過去,但是生活依舊,還是要工作,還是要掙錢,在這裏多的話不說,“武漢加油!”閒言少敘,我們直接走入主題,我們在開發複雜項目的時候,代碼的迭代,修改等,都會出現UI卡頓,或者出現ANR的時候,造成的程序崩潰,等,我們如何定位到卡頓的位置等,所以國內開發者,給我送來一個福利,BlockCanary這個框架。

1.介紹 BlockCanary

BlockCanary 這個框架是android平臺,非侵入式的性能監控組件。使用時提供一個抽象類,傳一個上下文環境就可以使用了,使用方便.

2.UI卡頓問題

原理:

在android開發中,我們的APP的幀頻性能最優的目標就是保持在60fps上。

60fps --->16ms/幀

所以我們儘量保證每次在16ms內處理完所有的CPU與GPU計算,繪製,渲染等操作,否則會造成丟幀卡頓問題。

 

3.UI卡頓的原因分析

1.UI線程中做耗時操作

1)主線程的作用:把事件分發給合適的view或者widget

解決辦法:我們通過handler在子線程中做耗時操作

runOnUiThread方法:

View.post 方法

VIew.postDelayed方法

2.佈局layout過於複雜,沒辦法在16ms中完成渲染

3.View的過度繪製,由於過度繪製導致在同一幀重複繪製

4.view頻繁的觸發measure,layout

5.內存頻繁觸發GC過多(在同一幀內頻繁的創建臨時變量)

 

4.BlockCanary的簡單實用

1)添加開源庫的依賴:

compile 'com.github.markzhai:blockcanary-android:1.5.0'

2)在Application中註冊我們的BlockCanary

BlockCanary.install(this, new AppBlockContext()).start();

3).創建一個類AppBlockContext 繼承 BlockCanaryContext:

public class AppBlockContext extends BlockCanaryContext {
    // 實現各種上下文,包括應用標示符,用戶uid,網絡類型,卡慢判斷闕值,Log保存位置等

    /**
     * Implement in your project.
     *
     * @return Qualifier which can specify this installation, like version + flavor.
     */
    public String provideQualifier() {
        return "unknown";
    }

    /**
     * Implement in your project.
     *
     * @return user id
     */
    public String provideUid() {
        return "uid";
    }

    /**
     * Network type
     *
     * @return {@link String} like 2G, 3G, 4G, wifi, etc.
     */
    public String provideNetworkType() {
        return "unknown";
    }

    /**
     * Config monitor duration, after this time BlockCanary will stop, use
     * with {@code BlockCanary}'s isMonitorDurationEnd
     *
     * @return monitor last duration (in hour)
     */
    public int provideMonitorDuration() {
        return -1;
    }

    /**
     * Config block threshold (in millis), dispatch over this duration is regarded as a BLOCK. You may set it
     * from performance of device.
     *
     * @return threshold in mills
     */
    public int provideBlockThreshold() {
        return 1000;
    }

    /**
     * Thread stack dump interval, use when block happens, BlockCanary will dump on main thread
     * stack according to current sample cycle.
     * <p>
     * Because the implementation mechanism of Looper, real dump interval would be longer than
     * the period specified here (especially when cpu is busier).
     * </p>
     *
     * @return dump interval (in millis)
     */
    public int provideDumpInterval() {
        return provideBlockThreshold();
    }

    /**
     * Path to save log, like "/blockcanary/", will save to sdcard if can.
     *
     * @return path of log files
     */
    public String providePath() {
        return "/blockcanary/";
    }

    /**
     * If need notification to notice block.
     *
     * @return true if need, else if not need.
     */
    public boolean displayNotification() {
        return true;
    }

    /**
     * Implement in your project, bundle files into a zip file.
     *
     * @param src  files before compress
     * @param dest files compressed
     * @return true if compression is successful
     */
    public boolean zip(File[] src, File dest) {
        return false;
    }

    /**
     * Implement in your project, bundled log files.
     *
     * @param zippedFile zipped file
     */
    public void upload(File zippedFile) {
        throw new UnsupportedOperationException();
    }


    /**
     * Packages that developer concern, by default it uses process name,
     * put high priority one in pre-order.
     *
     * @return null if simply concern only package with process name.
     */
    public List<String> concernPackages() {
        return null;
    }

    /**
     * Filter stack without any in concern package, used with @{code concernPackages}.
     *
     * @return true if filter, false it not.
     */
    public boolean filterNonConcernStack() {
        return false;
    }

    /**
     * Provide white list, entry in white list will not be shown in ui list.
     *
     * @return return null if you don't need white-list filter.
     */
    public List<String> provideWhiteList() {
        LinkedList<String> whiteList = new LinkedList<>();
        whiteList.add("org.chromium");
        return whiteList;
    }

    /**
     * Whether to delete files whose stack is in white list, used with white-list.
     *
     * @return true if delete, false it not.
     */
    public boolean deleteFilesInWhiteList() {
        return true;
    }

    /**
     * Block interceptor, developer may provide their own actions.
     */
    public void onBlock(Context context, BlockInfo blockInfo) {

    }
}

5.BlockCanary的原理源碼實現

        在ActivityThread中有一個main方法,在main方法中會創建一個Looper,在Looper當中會關聯一個MessageQueue消息隊列,主線程創建好MainLooper之後,他就會在應用的生命週期內不斷的輪訓,通過Looper.loop方法。然後獲取到我們消息隊列當中的message,最後通知我們的主線程去更新UI.

2)實現的核心原理

         通過Hander.postMessage發送一個消息給主線程(sMainLooper.loop),主線程會通過輪訓器Looper不斷的輪訓MessageQueue中的消息隊列,通過queue.next方法獲取消息隊列中的消息,然後我們計算出調用dispatchMessage方法的前後時間值(T1,T2),通過T2減去T1的時間差來判斷是否超過我們之前設定好的閾值,如果超過了我們設定的閾值,我們就dump出我們收集的信息,來定位我們UI卡頓的原因。

如果我們在調用dispatchmessage這個方法的時候,超過我們設定的閾值的0.8倍的時候,也會Dump出我們需要的信息。

 

3)我們通過源碼進行分析

 BlockCanary.install(this, new AppBlockContext()).start();

首先我們看看他的入口,install這個方法,我們點開:

 /**
     * Install {@link BlockCanary}
     *
     * @param context            Application context
     * @param blockCanaryContext BlockCanary context
     * @return {@link BlockCanary}
     */
    public static BlockCanary install(Context context, BlockCanaryContext blockCanaryContext) {
        BlockCanaryContext.init(context, blockCanaryContext);
        setEnabled(context, DisplayActivity.class, BlockCanaryContext.get().displayNotification());
        return get();
    }

這裏調用三行代碼.我們接着點init方法:

static void init(Context context, BlockCanaryContext blockCanaryContext) {
        sApplicationContext = context;
        sInstance = blockCanaryContext;
    }

這個init方法就做了一個賦值的操作,將我們傳遞過來的context進行賦值。

 

我們返回到install方法中。看setEnabled方法:

更據用戶的通知欄消息,來開啓或者關閉展示我們BlockCanary這個消息界面.

我們看這個方法的第三個參數,dispalyNotification這個方法就是決定開啓或者關閉:

  /**
     * If need notification to notice block.
     *
     * @return true if need, else if not need.
     */
    public boolean displayNotification() {
        return true;
    }

這裏默認返回的true。如果是debug方法是true,relese返回false

我們在看看get方法的實現:

 /**
     * Get {@link BlockCanary} singleton.
     *
     * @return {@link BlockCanary} instance
     */
    public static BlockCanary get() {
        if (sInstance == null) {
            synchronized (BlockCanary.class) {
                if (sInstance == null) {
                    sInstance = new BlockCanary();
                }
            }
        }
        return sInstance;
    }

他其實就是一個單例模式,我們看看這個BlockCanary是如何實現的那?

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

    }

BlockCanaryInternals.setContext,做了一個賦值操作,

mBlockCanaryCode這個類型的變量就是BlockCanaryInternals,我們看一下getInstance方法:

/**
     * Get BlockCanaryInternals singleton
     *
     * @return BlockCanaryInternals instance
     */
    static BlockCanaryInternals getInstance() {
        if (sInstance == null) {
            synchronized (BlockCanaryInternals.class) {
                if (sInstance == null) {
                    sInstance = new BlockCanaryInternals();
                }
            }
        }
        return sInstance;
    }

一個單例模式完成了BlockCanaryInternals的實例化。

我們接着看addBlockInterceptor這行代碼:

這是一個攔截器,傳入一個上下文

主要代碼,判斷是否開啓,展開這個攔截器,通知我們的DisplayActivity。

 

接下來我們看一看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();
    }

我們看一下這個構造方法,和重要的三個變量:

1)stackSampleer

參數一:傳入我們的主線程

參數二:Dump的間隔時間

2) cpuSampler

他會dump出我們cup的一些情況

3)LooperMonitor

這是一個非常重要的東西,如何打印上下時間(T1,T2),就是通過它控制的,然後通過onBlockEvent回調監聽並打印數據。

4)cleanObsolete這個方法就是刪除我們打印的日誌

 

上邊講解的是BlockCanary的install初始化的,接下來我們講解start()的打印是如何打印的:

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

最關鍵的代碼:就是Looper.getMainLooper這行代碼:調取主線程的setMessageLogging方法,來打點我們時間。

我們接下來看看代碼Monitor是如何實現的:

我們點擊LooperMonitor繼續查看:

這個類實現了Printer這個接口。

這個類中重要的方法是:

 @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();
        }
    }

這個方法就是用來打點時間的,

首先看代碼實現:

首先判斷dispacthMessage這個方法之前調用的,如果是就會記錄開始時間,調用這個startDump這個方法,來打印出我們的堆棧信息。

 

接下來我們看看這個startDump這個方法的實現:

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

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

這個方法主要就是通過BlockCanaryInternals中的stackSampler和cpuSampler分別打印出重要信息。

 

我們繼續深入,看看他們的start方法實現:

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

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

這裏沒啥說的,主要看看postDelayed這個方法的第一個參數,mRunnalbe:

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

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

我們在看看doSample是什麼?

  abstract void doSample();

這是一個抽象方法,這也就是意味着stackSampler和cpuSampler是有不同實現的,

 

我們接着看看這個抽象方法的StackSampler的實現:

  @Override
    protected void doSample() {
        StringBuilder stringBuilder = new StringBuilder();

        for (StackTraceElement stackTraceElement : mCurrentThread.getStackTrace()) {
            stringBuilder
                    .append(stackTraceElement.toString())
                    .append(BlockInfo.SEPARATOR);
        }

        synchronized (sStackMap) {
            if (sStackMap.size() == mMaxEntryCount && mMaxEntryCount > 0) {
                sStackMap.remove(sStackMap.keySet().iterator().next());
            }
            sStackMap.put(System.currentTimeMillis(), stringBuilder.toString());
        }
    }

到這裏就是真正要打印的數據了:

我們看最後一行代碼:執行了打印,第一個參數是以我們的當前時間戳爲例,並放到HashMap當中,我們看看是什麼HashMap?

private static final LinkedHashMap<Long, String> sStackMap = new LinkedHashMap<>();

他是一個linkHashMap: 爲什麼要用這個HashMap,因爲這個LinkHashMap能夠記錄插入的順序。

所以這裏是按着先後順序插入的,

 

我們回到這Printer這個接口的println方法:

 @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();
        }
    }

我們看看這個isBlock這個方法:

  private boolean isBlock(long endTime) {
        return endTime - mStartTimestamp > mBlockThresholdMillis;
    }

到這裏我們就明白了,這裏是不是就是我們BlockCanary的核心原理,T2減去T1的時間,並判斷是否打印並返回true,就會執行notifyBlockEvent,我們看看這個實現:

 private void notifyBlockEvent(final long endTime) {
        final long startTime = mStartTimestamp;
        final long startThreadTime = mStartThreadTimestamp;
        final long endThreadTime = SystemClock.currentThreadTimeMillis();
        HandlerThreadFactory.getWriteLogThreadHandler().post(new Runnable() {
            @Override
            public void run() {
                mBlockListener.onBlockEvent(startTime, endTime, startThreadTime, endThreadTime);
            }
        });
    }

我們看看這個方法中的LooperMonitor,this這個代碼,onBlockEvent是不是就是我們前面的監聽時間回調?

到這裏是不是就把我們要打印的數據通過這個回調方法返回去了.所以到這裏我們的分析就全部完成了。如果對您有幫助,麻煩關注我一下,我會給你繼續帶來乾貨。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章