Android源碼系列-解密BlockCanary

blockcanary是什麼?

blockcanary是國內開發者MarkZhai開發的一套性能監控組件,它對主線程操做進行了徹底透明的監控,並能輸出有效的信息,幫助開發分析、定位到問題所在,迅速優化應用android

下圖爲官方原理介紹示例圖:git

image.png

簡介

Github地址:blockcanarygithub

特色

  • 非侵入式
  • 使用簡單
  • 實時監控
  • 提供完善的堆棧及內存信息

Android渲染機制

Android系統每隔16ms發出VSYNC信號,觸發對UI進行渲染, 若是每次渲染都成功,這樣就可以達到流暢的畫面所須要的60fps,爲了可以實現60fps,這意味着程序的大多數操做都必須在16ms內完成。若是超過了16ms那麼可能就出現丟幀的狀況。算法

本文主要對blockcanary的原理進行分析,關於渲染的詳細機制及優化,推薦參考以下文章:性能優化

Android性能優化-渲染優化bash

blockcanary怎麼用?

一、gradle引入庫微信

debugImplementation 'com.github.markzhai:blockcanary-android:1.5.0'
 releaseImplementation 'com.github.markzhai:blockcanary-no-op:1.5.0'
 
複製代碼

二、自定義Application而且在onCreate中進行初始化網絡

public class ExampleApplication extends Application {

    @Override public void onCreate() {
        super.onCreate();
        BlockCanary.install(this, new BlockCanaryContext()).start();
    }
}
複製代碼

blockcanary核心執行流程是怎樣?

blockcanary的核心原理是經過自定義一個Printer,設置到主線程ActivityThread的MainLooper中。MainLooper在dispatch消息先後都會調用Printer進行打印。從而獲取先後執行的時間差值,判斷是否超過設置的閾值。若是超過,則會將記錄的棧信息及cpu信息發通知到前臺。app

關鍵類功能說明

說明
BlockCanary 外觀類,提供初始化及開始、中止監聽
BlockCanaryContext 配置上下文,可配置id、當前網絡信息、卡頓閾值、log保存路徑等
BlockCanaryInternals blockcanary核心的調度類,內部包含了monitor(設置到MainLooper的printer)、stackSampler(棧信息處理器)、cpuSampler(cpu信息處理器)、mInterceptorChain(註冊的攔截器)、以及onBlockEvent的回調及攔截器的分發
LooperMonitor 繼承了Printer接口,用於設置到MainLooper中。經過複寫println的方法來獲取MainLooper的dispatch先後的執行時間差,並控制stackSampler和cpuSampler的信息採集。
StackSampler 用於獲取線程的棧信息,將採集的棧信息存儲到一個以key爲時間戳的LinkHashMap中。經過mCurrentThread.getStackTrace()獲取當前線程的StackTraceElement
CpuSampler 用於獲取cpu信息,將採集的cpu信息存儲到一個以key爲時間戳的LinkHashMap中。經過讀取/proc/stat文件獲取cpu的信息
DisplayService 繼承了BlockInterceptor攔截器,onBlock回調會觸發發送前臺通知
DisplayActivity 用於顯示記錄的異常信息Activity

代碼執行流程

leakcanary的核心流程主要包含3個步驟。框架

一、init-初始化

二、monitor-監聽MainLooper的dispatch時間差,推送前臺通知

三、dump-採集線程棧信息及cpu信息

這裏先上一下總體的流程圖,建議結合源碼進行查看。

image

下面咱們經過上述3個步驟相關的源碼來進行分析。

一、init

根據Application中的使用,咱們首先看install方法

public static BlockCanary install(Context context, BlockCanaryContext blockCanaryContext) {
        //BlockCanaryContext.init會將保存應用的applicationContext和用戶設置的配置參數
        BlockCanaryContext.init(context, blockCanaryContext);
        //etEnabled將根據用戶的通知欄消息配置開啓
        setEnabled(context, DisplayActivity.class, BlockCanaryContext.get().displayNotification());
        return get();
    }
    
複製代碼

接着看get方法的實現以下:

//使用單例建立了一個BlockCanary對象
    public static BlockCanary get() {
        if (sInstance == null) {
            synchronized (BlockCanary.class) {
                if (sInstance == null) {
                    sInstance = new BlockCanary();
                }
            }
        }
        return sInstance;
    }
複製代碼

接着咱們看BlockCanary的對象的構造方法實現以下:

private BlockCanary() {
        //初始化lockCanaryInternals調度類
        BlockCanaryInternals.setContext(BlockCanaryContext.get());
        mBlockCanaryCore = BlockCanaryInternals.getInstance();
        //爲BlockCanaryInternals添加攔截器(責任鏈)BlockCanaryContext對BlockInterceptor是空實現
        mBlockCanaryCore.addBlockInterceptor(BlockCanaryContext.get());
        if (!BlockCanaryContext.get().displayNotification()) {
            return;
        }
        //DisplayService只在開啓通知欄消息的時候添加,當卡頓發生時將經過DisplayService發起通知欄消息
        mBlockCanaryCore.addBlockInterceptor(new DisplayService());

    }

複製代碼

接着咱們看BlockCanaryInternals的構造方法,實現以下:

public BlockCanaryInternals() {
        //初始化棧採集器
        stackSampler = new StackSampler(
                Looper.getMainLooper().getThread(),
                sContext.provideDumpInterval());
        //初始化cpu採集器
        cpuSampler = new CpuSampler(sContext.provideDumpInterval());

        //初始化LooperMonitor,並實現了onBlockEvent的回調,該回調會在觸發閾值後被調用
        setMonitor(new LooperMonitor(new LooperMonitor.BlockListener() {

            @Override
            public void onBlockEvent(long realTimeStart, long realTimeEnd,
                                     long threadTimeStart, long threadTimeEnd) {
                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();
    }
複製代碼

二、monitor

首先咱們先看下系統的Looper的loop()方法中對於printer的使用,以下:

for (;;) {
            Message msg = queue.next(); // might block
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            }

            // 執行dispatchMessage前,執行Printer的println方法
            final Printer logging = me.mLogging;
            if (logging != null) {
                logging.println(">>>>> Dispatching to " + msg.target + " " +
                        msg.callback + ": " + msg.what);
            }

            final long traceTag = me.mTraceTag;
            long slowDispatchThresholdMs = me.mSlowDispatchThresholdMs;
            long slowDeliveryThresholdMs = me.mSlowDeliveryThresholdMs;
            if (thresholdOverride > 0) {
                slowDispatchThresholdMs = thresholdOverride;
                slowDeliveryThresholdMs = thresholdOverride;
            }
            final boolean logSlowDelivery = (slowDeliveryThresholdMs > 0) && (msg.when > 0);
            final boolean logSlowDispatch = (slowDispatchThresholdMs > 0);

            final boolean needStartTime = logSlowDelivery || logSlowDispatch;
            final boolean needEndTime = logSlowDispatch;

            if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
                Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
            }

            final long dispatchStart = needStartTime ? SystemClock.uptimeMillis() : 0;
            final long dispatchEnd;
            try {
                msg.target.dispatchMessage(msg);
                dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
            } finally {
                if (traceTag != 0) {
                    Trace.traceEnd(traceTag);
                }
            }
            if (logSlowDelivery) {
                if (slowDeliveryDetected) {
                    if ((dispatchStart - msg.when) <= 10) {
                        Slog.w(TAG, "Drained");
                        slowDeliveryDetected = false;
                    }
                } else {
                    if (showSlowLog(slowDeliveryThresholdMs, msg.when, dispatchStart, "delivery",
                            msg)) {
                        // Once we write a slow delivery log, suppress until the queue drains.
                        slowDeliveryDetected = true;
                    }
                }
            }
            if (logSlowDispatch) {
                showSlowLog(slowDispatchThresholdMs, dispatchStart, dispatchEnd, "dispatch", msg);
            }
          // 執行dispatchMessage後,執行Printer的println方法
            if (logging != null) {
                logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
            }

            // Make sure that during the course of dispatching the
            // identity of the thread wasn't corrupted. final long newIdent = Binder.clearCallingIdentity(); if (ident != newIdent) { Log.wtf(TAG, "Thread identity changed from 0x" + Long.toHexString(ident) + " to 0x" + Long.toHexString(newIdent) + " while dispatching to " + msg.target.getClass().getName() + " " + msg.callback + " what=" + msg.what); } msg.recycleUnchecked(); } 複製代碼

當install進行初始化完成後,接着會調用start()方法,實現以下:

public void start() {
        if (!mMonitorStarted) {
            mMonitorStarted = true;
            //把mBlockCanaryCore中的monitor設置MainLooper中進行監聽
            Looper.getMainLooper().setMessageLogging(mBlockCanaryCore.monitor);
        }
    }
複製代碼

當MainLooper執行dispatch的先後會調用printer的println方法,因此這裏咱們看LooperMonitor對println方法的實現以下:

@Override
    public void println(String x) {
        //若是再debug模式,不執行監聽
        if (mStopWhenDebugging && Debug.isDebuggerConnected()) {
            return;
        }
        if (!mPrintingStarted) {//dispatchMesage前執行的println
            //記錄開始時間
            mStartTimestamp = System.currentTimeMillis();
            mStartThreadTimestamp = SystemClock.currentThreadTimeMillis();
            mPrintingStarted = true;
            //開始採集棧及cpu信息
            startDump();
        } else {//dispatchMesage後執行的println
            //獲取結束時間
            final long endTime = System.currentTimeMillis();
            mPrintingStarted = false;
            //判斷耗時是否超過閾值
            if (isBlock(endTime)) {
                notifyBlockEvent(endTime);
            }
            stopDump();
        }
    }
 //判斷是否超過閾值
 private boolean isBlock(long endTime) {
        return endTime - mStartTimestamp > mBlockThresholdMillis;
    }
//回調監聽
 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);
            }
        });
    }
複製代碼

當發現時間差超過閾值後,會回調onBlockEvent。具體的實如今BlockCanaryInternals的構造方法中,以下:

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

            @Override
            public void onBlockEvent(long realTimeStart, long realTimeEnd,
                                     long threadTimeStart, long threadTimeEnd) {
                //根據開始及結束時間,從棧的map當中獲取記錄信息
                ArrayList<String> threadStackEntries = stackSampler
                        .getThreadStackEntries(realTimeStart, realTimeEnd);
                if (!threadStackEntries.isEmpty()) {
                    //構建 BlockInfo對象,設置相關的信息
                    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()));
複製代碼

最後咱們看攔截器的實現DisplayService,會發送前臺的通知,代碼以下:

@Override
    public void onBlock(Context context, BlockInfo blockInfo) {
        Intent intent = new Intent(context, DisplayActivity.class);
        intent.putExtra("show_latest", blockInfo.timeStart);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
        PendingIntent pendingIntent = PendingIntent.getActivity(context, 1, intent, FLAG_UPDATE_CURRENT);
        String contentTitle = context.getString(R.string.block_canary_class_has_blocked, blockInfo.timeStart);
        String contentText = context.getString(R.string.block_canary_notification_message);
        show(context, contentTitle, contentText, pendingIntent);
    }
複製代碼

三、dump

從上面的流程咱們能夠知道,當dispatchMessage前的println觸發時,會執行dump的start方法,當dispatchMessage後的println觸發時,會執行dump的stop方法。

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

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

    private void stopDump() {
        if (null != BlockCanaryInternals.getInstance().stackSampler) {
            BlockCanaryInternals.getInstance().stackSampler.stop();
        }

        if (null != BlockCanaryInternals.getInstance().cpuSampler) {
            BlockCanaryInternals.getInstance().cpuSampler.stop();
        }
    }
複製代碼

下面咱們分Stacksampler和CpuSampler進行介紹。

一、Stacksampler

start()的執行流程以下:

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

        HandlerThreadFactory.getTimerThreadHandler().removeCallbacks(mRunnable);
        //經過一個HandlerThread延時執行了mRunnable
        HandlerThreadFactory.getTimerThreadHandler().postDelayed(mRunnable,
                BlockCanaryInternals.getInstance().getSampleDelay());
    }
   //mRunnable在基類AbstractSampler中定義
  private Runnable mRunnable = new Runnable() {
        @Override
        public void run() {
            //抽象方法
            doSample();
            //繼續執行採集
            if (mShouldSample.get()) {
                HandlerThreadFactory.getTimerThreadHandler()
                        .postDelayed(mRunnable, mSampleInterval);
            }
        }
    };
 //Stacksampler的doSample()實現
  @Override
    protected void doSample() {
        StringBuilder stringBuilder = new StringBuilder();
        //經過mCurrentThread.getStackTrace()獲取StackTraceElement,加入到StringBuilder
        for (StackTraceElement stackTraceElement : mCurrentThread.getStackTrace()) {
            stringBuilder
                    .append(stackTraceElement.toString())
                    .append(BlockInfo.SEPARATOR);
        }

        synchronized (sStackMap) {
        //Lru算法,控制LinkHashMap的長度
            if (sStackMap.size() == mMaxEntryCount && mMaxEntryCount > 0) {
                sStackMap.remove(sStackMap.keySet().iterator().next());
            }
            //加入到map中
            sStackMap.put(System.currentTimeMillis(), stringBuilder.toString());
        }
    }
複製代碼

stop()的執行流程以下:

public void stop() {
        if (!mShouldSample.get()) {
            return;
        }
        //設置控制變量
        mShouldSample.set(false);
        //取消handler消息
        HandlerThreadFactory.getTimerThreadHandler().removeCallbacks(mRunnable);
    }
複製代碼

二、CpuSampler

其餘執行流程均與StackSampler一致,這裏主要分析doSample的實現,以下:

//主要經過獲取/proc/stat文件 去獲取cpu的信息
  protected void doSample() {
        BufferedReader cpuReader = null;
        BufferedReader pidReader = null;

        try {
            cpuReader = new BufferedReader(new InputStreamReader(
                    new FileInputStream("/proc/stat")), BUFFER_SIZE);
            String cpuRate = cpuReader.readLine();
            if (cpuRate == null) {
                cpuRate = "";
            }

            if (mPid == 0) {
                mPid = android.os.Process.myPid();
            }
            pidReader = new BufferedReader(new InputStreamReader(
                    new FileInputStream("/proc/" + mPid + "/stat")), BUFFER_SIZE);
            String pidCpuRate = pidReader.readLine();
            if (pidCpuRate == null) {
                pidCpuRate = "";
            }

            parse(cpuRate, pidCpuRate);
        } catch (Throwable throwable) {
            Log.e(TAG, "doSample: ", throwable);
        } finally {
            try {
                if (cpuReader != null) {
                    cpuReader.close();
                }
                if (pidReader != null) {
                    pidReader.close();
                }
            } catch (IOException exception) {
                Log.e(TAG, "doSample: ", exception);
            }
        }
    }
複製代碼

blockcanary是如何進行卡頓的斷定?

blockcanary的核心原理是經過自定義一個Printer,設置到主線程ActivityThread的MainLooper中。MainLooper在dispatch消息先後都會調用Printer進行打印。從而獲取先後執行的時間差值,判斷是否超過設置的閾值。若是超過,則斷定爲卡頓。

leakcanary是如何獲取線程的堆棧信息?

經過mCurrentThread.getStackTrace()方法,遍歷獲取StackTraceElement,轉化爲一個StringBuilder的value,並存儲到一個key爲時間戳的LinkHashMap中。

leakcanary是如何獲取cpu的信息?

經過讀取/proc/stat文件,獲取全部CPU活動的信息來計算CPU使用率。解析出信息後,轉化爲一個StringBuilder的value,並存儲到一個key爲時間戳的LinkHashMap中。

總結

思考

blockcanary充分的利用了Loop的機制,在MainLooper的loop方法中執行dispatchMessage先後都會執行printer的println進行輸出,而且提供了方法設置printer。經過分析先後打印的時差與閾值進行比對,從而斷定是否卡頓。

參考資料

Android性能優化-渲染優化

Android UI卡頓監測框架BlockCanary原理分析

推薦

Android源碼系列-解密OkHttp

Android源碼系列-解密Retrofit

Android源碼系列-解密Glide

Android源碼系列-解密EventBus

Android源碼系列-解密RxJava

Android源碼系列-解密LeakCanary

Android源碼系列-解密BlockCanary

關於

歡迎關注個人我的公衆號

微信搜索:一碼一浮生,或者搜索公衆號ID:life2code

image

相關文章
相關標籤/搜索