BlockCanary 源碼分析

一、概述

在 Android 開發過程中,經常會遇到 UI 卡頓的問題,那怎麼去監測 UI 的卡頓呢?今天我們就來分析一款監測 UI 卡頓的框架:BlockCanary。題外話:滴滴的 DoraemonKit 開發輔助框架提供的卡頓監測原理與 BlockCanary 一致。

BlockCanary 是一個輕量的,非侵入式的性能監控組件,目前採集了 UI 卡頓相關的 線程堆棧信息CPU 使用信息,用於分析定位問題。

參考:

  1. BlockCanary :Github 傳送門 https://github.com/markzhai/AndroidPerformanceMonitor
  2. DoraemonKit:Github 傳送門 https://github.com/didi/DoraemonKit
  3. 作者:BlockCanary — 輕鬆找出Android App界面卡頓元兇

二、實現原理

在Android中,應用的卡頓主要是因爲主線程執行了耗時操作導致的。

原理:

在 Looper 中有一個 Looper.loop() 方法,一般耗時方法發生在 msg.target.dispatchMessage(msg) 中,所以我們可以利用 兩次日誌打印的時間差是否大於約定的時間值 來判斷是否有耗時操作。

Looper.loop()

// Looper.class
public static void loop() {
 	// ...省略代碼...
    for (;;) {
		// ...省略代碼...
		// 1.處理消息事件前打印日誌
        final Printer logging = me.mLogging;
        if (logging != null) {
            logging.println(">>>>> Dispatching to " + msg.target + " " + msg.callback + ": " + msg.what);
        }
        // ...省略代碼...
        
        // 2.處理消息事件
        msg.target.dispatchMessage(msg);
        
        // ...省略代碼...
        // 3.處理消息事件之後打印日誌
        if (logging != null) {
            logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
        }
        // ...省略代碼...
    }
}

三、源碼分析

1. 初始化流程

使用方法:

public class App extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        // 初始化 BlockCanary,傳入的 BlockCanaryContext 是一些配置參數信息。
        BlockCanary.install(this, new BlockCanaryContext()).start();
    }
}

下面分 install() 和 start() 兩部分來分析一下。


BlockCanary.install()

// BlockCanary.class
public static BlockCanary install(Context context, BlockCanaryContext blockCanaryContext) {
	// 下面兩步主要是進行一些賦值操作,如果有卡頓會展示在 DisplayActivity 這個頁面。
    BlockCanaryContext.init(context, blockCanaryContext);
    setEnabled(context, DisplayActivity.class, BlockCanaryContext.get().displayNotification());
    
    // 重點:這裏會創建一個BlockCanary實例對象。
    return get();
}

public static BlockCanary get() {
    if (sInstance == null) {
        synchronized (BlockCanary.class) {
            if (sInstance == null) {
                sInstance = new BlockCanary();
            }
        }
    }
    return sInstance;
}

private BlockCanary() {
	// 將Application中初始化時配置的 BlockCanaryContext 設置到 BlockCanaryInternals 中。
    BlockCanaryInternals.setContext(BlockCanaryContext.get());
    // 這裏會初始化 BlockCanaryInternals 實例,真正給 Looper 設置 Printer 的邏輯就這這裏
    mBlockCanaryCore = BlockCanaryInternals.getInstance();
    
    // 下面這兩個就是設置兩個回調函數。
    mBlockCanaryCore.addBlockInterceptor(BlockCanaryContext.get());
    if (!BlockCanaryContext.get().displayNotification()) {
        return;
    }
    mBlockCanaryCore.addBlockInterceptor(new DisplayService());
}

小結:

  1. BlockCanary 只是一個門面類,真正處理邏輯的是 BlockCanaryInternals 。

BlockCanaryInternals

static BlockCanaryInternals getInstance() {
    if (sInstance == null) {
        synchronized (BlockCanaryInternals.class) {
            if (sInstance == null) {
                sInstance = new BlockCanaryInternals();
            }
        }
    }
    return sInstance;
}

public BlockCanaryInternals() {
	// 1.設置了主線程的Looper,所以採集的是主線程的堆棧信息。
    stackSampler = new StackSampler(Looper.getMainLooper().getThread(), sContext.provideDumpInterval());
    // 2.採集CPU的使用信息。
    cpuSampler = new CpuSampler(sContext.provideDumpInterval());
	// 3.創建一個Printer對象。
    setMonitor(new LooperMonitor(new LooperMonitor.BlockListener() {

        @Override
        public void onBlockEvent(long realTimeStart, long realTimeEnd,
                                 long threadTimeStart, long threadTimeEnd) {
            // 4.如果主線程出現了卡頓,就會觸發 onBlockEvent 回調方法,然後進行線程堆棧信息和cpu信息的採集。
            // 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();
                // 5.寫入文件中保存
                LogWriter.save(blockInfo.toString());
				// 6.然後觸發外部的回調,因此外部就可以做一些自定義的操作。
                if (mInterceptorChain.size() != 0) {
                    for (BlockInterceptor interceptor : mInterceptorChain) {
                        interceptor.onBlock(getContext().provideContext(), blockInfo);
                    }
                }
            }
        }
    }, getContext().provideBlockThreshold(), getContext().stopWhenDebugging()));

    LogWriter.cleanObsolete();
}

private void setMonitor(LooperMonitor looperPrinter) {
    monitor = looperPrinter;
}

小結:

  1. 構建線程堆棧信息的採集類 StackSampler
  2. 構建 CPU 使用信息的採集類 CpuSampler
  3. 構建 Looper 內日誌打印的類 LooperMonitor
  4. 如果主線程發生了卡頓,就會觸發 LooperMonitor.onBlockEvent() 方法。
  5. LooperMonitor.onBlockEvent() 中獲取指定時間範圍內的堆棧信息和CPU信息,並寫入日誌文件。
  6. 發生卡頓時,觸發外部的回調,將採集到的數據提供給外部進行自定義的操作。

注意:

這裏雖然創建了 Looper 的日誌類 LooperMonitor,但是並不會觸發裏面日誌打印的操作,因爲還沒有將 LooperMonitor 賦值給主線程的 Looper。

那什麼時候進行的賦值操作呢?在 BlockCanary.start()方法中進行了賦值。


BlockCanary.start()

public void start() {
    if (!mMonitorStarted) {
        mMonitorStarted = true;
        // 將自定義的 Printer 賦值給主線程的 Looper。
        Looper.getMainLooper().setMessageLogging(mBlockCanaryCore.monitor);
    }
}

小結:

  1. mBlockCanaryCore.monitor 是在 BlockCanaryInternals 構造函數時初始化的。
  2. 在調用 BlockCanary.start() 時,將我們自定義的 Printer 賦值給主線程的 Looper。

下面我們來看一下 LooperMonitor 的具體實現。

LooperMonitor

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

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() {
        	// 這個回調方法在 BlockCanaryInternals 構造函數中。
            mBlockListener.onBlockEvent(startTime, endTime, startThreadTime, endThreadTime);
        }
    });
}

// 開啓線程堆棧和CPU的信息採集
private void startDump() {
    if (null != BlockCanaryInternals.getInstance().stackSampler) {
        BlockCanaryInternals.getInstance().stackSampler.start();
    }

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

// 關閉線程堆棧和CPU的信息採集
private void stopDump() {
    if (null != BlockCanaryInternals.getInstance().stackSampler) {
        BlockCanaryInternals.getInstance().stackSampler.stop();
    }

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

小結:

  1. LooperMonitor.println() 方法是在Looper.loop() 會被觸發,且消費一個事件會觸發兩個打印操作(事件消費前觸發一次,事件消費後觸發一次)。
  2. 通過 mPrintingStarted 標記來區分消費事件前和消費事件後兩次的日誌打印。
  3. 判斷前後兩次日誌打印時間差是否大於給定的值來判斷主線程是否卡頓。
  4. 事件消費前的日誌打印操作會開啓線程堆棧和CPU的信息採集。
  5. 事件消費後的日誌打印操作會關閉線程堆棧和CPU的信息採集。

初始化的操作到這裏就已經結束了,下面分析一下堆棧信息的採集和CPU信息的採集。


2. 堆棧信息採集過程

在分析之前我們先來分析一下 StackSampler 的父類 AbstractSampler。

abstract class AbstractSampler {
    private static final int DEFAULT_SAMPLE_INTERVAL = 300;

	// 通過 CAS 類來判斷,避免重複執行採集操作。
    protected AtomicBoolean mShouldSample = new AtomicBoolean(false);
    protected long mSampleInterval;

    private Runnable mRunnable = new Runnable() {
        @Override
        public void run() {
        	// 由子類在 doSample 中執行數據採集工作。
            doSample();

			// 只要 Runnable 沒有關閉,就會每間隔 mSampleInterval 時間來執行採集任務。
            if (mShouldSample.get()) {
                HandlerThreadFactory.getTimerThreadHandler()
                        .postDelayed(mRunnable, mSampleInterval);
            }
        }
    };

    public AbstractSampler(long sampleInterval) {
        if (0 == sampleInterval) {
            sampleInterval = DEFAULT_SAMPLE_INTERVAL;
        }
        mSampleInterval = sampleInterval;
    }

    public void start() {
    	// 通過 CAS 類來確認當前是否有數據採集操作。
        if (mShouldSample.get()) {
            return;
        }
        mShouldSample.set(true);
		// 線程一直處理開始狀態,所以採集任務其實就是往線程中添加 Runnable 任務。
        HandlerThreadFactory.getTimerThreadHandler().removeCallbacks(mRunnable);
        HandlerThreadFactory.getTimerThreadHandler().postDelayed(mRunnable,
                BlockCanaryInternals.getInstance().getSampleDelay());
    }

    public void stop() {
    	// 通過 CAS 類來確認當前是否有數據採集操作。
        if (!mShouldSample.get()) {
            return;
        }
        mShouldSample.set(false);
        HandlerThreadFactory.getTimerThreadHandler().removeCallbacks(mRunnable);
    }

    abstract void doSample();
}

小結:

  1. 執行任務採集功能是在子線程中執行的。
  2. 每間隔 mSampleInterval 時間,就會執行採集任務。

StackSampler

// LruCache算法,默認保留100條堆棧信息。
private static final LinkedHashMap<Long, String> sStackMap = new LinkedHashMap<>();

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

	// 1.通過 Thread.getStackTrace() 方法獲取當前線程的堆棧信息
    for (StackTraceElement stackTraceElement : mCurrentThread.getStackTrace()) {
        stringBuilder
                .append(stackTraceElement.toString())
                .append(BlockInfo.SEPARATOR);
    }

    synchronized (sStackMap) {
    	// 2.採集的堆棧信息大於指定條數就移除最早的數據。
        if (sStackMap.size() == mMaxEntryCount && mMaxEntryCount > 0) {
            sStackMap.remove(sStackMap.keySet().iterator().next());
        }
        // 3.將當前採集時間作爲Key進行存儲,便於後面根據線程阻塞時間來進行堆棧數據的篩選。
        sStackMap.put(System.currentTimeMillis(), stringBuilder.toString());
    }
}

public ArrayList<String> getThreadStackEntries(long startTime, long endTime) {
    ArrayList<String> result = new ArrayList<>();
    synchronized (sStackMap) {
        for (Long entryTime : sStackMap.keySet()) {
        	// 採集主線程卡頓時間內的堆棧信息。
            if (startTime < entryTime && entryTime < endTime) {
                result.add(BlockInfo.TIME_FORMATTER.format(entryTime)
                        + BlockInfo.SEPARATOR
                        + BlockInfo.SEPARATOR
                        + sStackMap.get(entryTime));
            }
        }
    }
    return result;
}

小結:

數據採集過程:

  1. 通過 Thread.getStackTrace() 方法獲取當前線程的堆棧信息。
  2. 採集的堆棧信息大於指定條數就移除最早的數據。
  3. 將當前採集時間作爲Key進行存儲,便於後面根據線程阻塞時間來進行堆棧數據的篩選。

數據過濾過程:

  1. 通過給定的線程阻塞的開始 / 結束時間來篩選採集到的堆棧信息。

3. CPU信息採集過程

// LruCache算法,默認保留10條堆棧信息。
private final LinkedHashMap<Long, String> mCpuInfoEntries = new LinkedHashMap<>();

protected void doSample() {
    BufferedReader cpuReader = null;
    BufferedReader pidReader = null;

    try {
    	// 1.讀取 /proc/stat 和 /proc/" + mPid + "/stat 文件裏的數據信息。
        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 = "";
        }
		// 2.對這兩個文件的數據進行解析。
        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);
        }
    }
}

// 總共只緩存10條數據,所以就全部獲取了,與堆棧信息根據時間區間來獲取的方式不一樣。
public String getCpuRateInfo() {
    StringBuilder sb = new StringBuilder();
    synchronized (mCpuInfoEntries) {
        for (Map.Entry<Long, String> entry : mCpuInfoEntries.entrySet()) {
            long time = entry.getKey();
            sb.append(BlockInfo.TIME_FORMATTER.format(time))
                    .append(' ')
                    .append(entry.getValue())
                    .append(BlockInfo.SEPARATOR);
        }
    }
    return sb.toString();
}


public boolean isCpuBusy(long start, long end) {
    if (end - start > mSampleInterval) {
        long s = start - mSampleInterval;
        long e = start + mSampleInterval;
        long last = 0;
        synchronized (mCpuInfoEntries) {
            for (Map.Entry<Long, String> entry : mCpuInfoEntries.entrySet()) {
                long time = entry.getKey();
                if (s < time && time < e) {
                    if (last != 0 && time - last > BUSY_TIME) {
                        return true;
                    }
                    last = time;
                }
            }
        }
    }
    return false;
}

小結:

CPU 信息的採集?

讀取並解析 /proc/stat/proc/ + mPid + /stat 文件裏的數據信息。

CPU 信息的獲取?

CPU 信息默認只緩存10條,所以在獲取的時候,不做時間區間的過濾。

如何判斷給定時間間隔內 CPU 是否處於 Busy 狀態?

在給定是時間區間內,前後各擴展一個 mSampleInterval 時間區間,判斷這個時間區間內的 CPU 使用時間與最近一次使用時間的間隔是否大於 BUSY_TIME,如果大於 BUSY_TIME,說明 CPU 處於 Busy 狀態。


四、小結

流程圖如下:
在這裏插入圖片描述


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