BlockCanary源碼解析

本文對BlockCanary源碼進(jìn)行了分析。

《行宮》
寥落古行宮,宮花寂寞紅。
白頭宮女在,閑坐說玄宗。
—唐,元稹

原理(轉(zhuǎn)自BlockCanary

熟悉Message/Looper/Handler系列的同學(xué)們一定知道Looper.java中這么一段:

private static Looper sMainLooper;  // guarded by Looper.class
...
/**
 * Initialize the current thread as a looper, marking it as an
 * application's main looper. The main looper for your application
 * is created by the Android environment, so you should never need
 * to call this function yourself.  See also: {@link #prepare()}
 */
public static void prepareMainLooper() {
    prepare(false);
    synchronized (Looper.class) {
        if (sMainLooper != null) {
            throw new IllegalStateException("The main Looper has already been prepared.");
        }
        sMainLooper = myLooper();
    }
}
/** Returns the application's main looper, which lives in the main thread of the application.
 */
public static Looper getMainLooper() {
    synchronized (Looper.class) {
        return sMainLooper;
    }
}

即整個應(yīng)用的主線程,只有這一個looper,不管有多少handler,最后都會回到這里。

如果再細(xì)心一點會發(fā)現(xiàn)在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);
        }
        ...
    }
}

是的,就是這個Printer - mLogging,它在每個message處理的前后被調(diào)用,而如果主線程卡住了,不就是在dispatchMessage里卡住了嗎?

核心流程圖:

該組件利用了主線程的消息隊列處理機制,通過

Looper.getMainLooper().setMessageLogging(mainLooperPrinter);

并在mainLooperPrinter中判斷start和end,來獲取主線程dispatch該message的開始和結(jié)束時間,并判定該時間超過閾值(如2000毫秒)為主線程卡慢發(fā)生,并dump出各種信息,提供開發(fā)者分析性能瓶頸。

@Override
public void println(String x) {
    if (!mStartedPrinting) {
        mStartTimeMillis = System.currentTimeMillis();
        mStartThreadTimeMillis = SystemClock.currentThreadTimeMillis();
        mStartedPrinting = true;
    } else {
        final long endTime = System.currentTimeMillis();
        mStartedPrinting = false;
        if (isBlock(endTime)) {
            notifyBlockEvent(endTime);
        }
    }
}
private boolean isBlock(long endTime) {
    return endTime - mStartTimeMillis > mBlockThresholdMillis;
}

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

源碼解析

使用方法:

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

從上面的入口先來看一下BlockCanary類,可以看到只是簡單的初始化賦值等操作,start方法中給MainLooper設(shè)置了打印消息的監(jiān)聽,構(gòu)造方法中判斷如果需要顯示通知會使用mBlockCanaryCore.addBlockInterceptor方法添加阻塞事件監(jiān)聽。

BlockCanary.java

private BlockCanaryInternals mBlockCanaryCore;

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

}

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

/**
* 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;
}

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

其中BlockCanaryInternals類中monitor變量的類型是LooperMonitor類,該類實現(xiàn)了Printer接口,從原理部分我們知道如果我們使用Looper.getMainLooper().setMessageLogging()方法設(shè)置了打印日志的監(jiān)聽之后,主線程中所有的事件都會調(diào)用此方法:

LooperMonitor.java

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

當(dāng)事件開始會調(diào)用startDump方法開始采樣,獲取堆棧信息和CPU信息,事件結(jié)束會判斷是否超過指定阻塞時間值,如果超過會在HandlerThreadFactory提供的HandlerThread子線程中通過接口回調(diào)到BlockCanaryInternals類中,該接口通過LooperMonitor構(gòu)造方法傳入,然后調(diào)用stopDump方法停止采樣。

LooperMonitor.java

private BlockListener mBlockListener = null;

public interface BlockListener {
   void onBlockEvent(long realStartTime,
                     long realTimeEnd,
                     long threadTimeStart,
                     long threadTimeEnd);
}

public LooperMonitor(BlockListener blockListener, long blockThresholdMillis, boolean stopWhenDebugging) {
   if (blockListener == null) {
       throw new IllegalArgumentException("blockListener should not be null.");
   }
   mBlockListener = blockListener;
   mBlockThresholdMillis = blockThresholdMillis;
   mStopWhenDebugging = stopWhenDebugging;
}

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

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

接下來我們看下AbstractSampler抽象類,CpuSampler和StackSampler繼承自該類,該類中主要處理了start和stop方法,以及一個Runnable調(diào)用抽象方法doSample,runnable會在HandlerThreadFactory類中提供的HandleThread子線程中執(zhí)行。

AbstractSampler.java

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

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

...

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

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

public void stop() {
   if (!mShouldSample.get()) {
       return;
   }
   mShouldSample.set(false);
   HandlerThreadFactory.getTimerThreadHandler().removeCallbacks(mRunnable);
}

abstract void doSample();

StackSampler類中通過mCurrentThread.getStackTrace()獲取堆棧信息,存儲到sStackMap靜態(tài)變量中。

StackSampler.java

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

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

CpuSampler類中對CPU進(jìn)行采樣,并將CPU信息存儲到mCpuInfoEntries變量中:

CpuSampler.java

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

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

private void parse(String cpuRate, String pidCpuRate) {
   String[] cpuInfoArray = cpuRate.split(" ");
   if (cpuInfoArray.length < 9) {
       return;
   }

   long user = Long.parseLong(cpuInfoArray[2]);
   long nice = Long.parseLong(cpuInfoArray[3]);
   long system = Long.parseLong(cpuInfoArray[4]);
   long idle = Long.parseLong(cpuInfoArray[5]);
   long ioWait = Long.parseLong(cpuInfoArray[6]);
   long total = user + nice + system + idle + ioWait
           + Long.parseLong(cpuInfoArray[7])
           + Long.parseLong(cpuInfoArray[8]);

   String[] pidCpuInfoList = pidCpuRate.split(" ");
   if (pidCpuInfoList.length < 17) {
       return;
   }

   long appCpuTime = Long.parseLong(pidCpuInfoList[13])
           + Long.parseLong(pidCpuInfoList[14])
           + Long.parseLong(pidCpuInfoList[15])
           + Long.parseLong(pidCpuInfoList[16]);

   if (mTotalLast != 0) {
       StringBuilder stringBuilder = new StringBuilder();
       long idleTime = idle - mIdleLast;
       long totalTime = total - mTotalLast;

       stringBuilder
               .append("cpu:")
               .append((totalTime - idleTime) * 100L / totalTime)
               .append("% ")
               .append("app:")
               .append((appCpuTime - mAppCpuTimeLast) * 100L / totalTime)
               .append("% ")
               .append("[")
               .append("user:").append((user - mUserLast) * 100L / totalTime)
               .append("% ")
               .append("system:").append((system - mSystemLast) * 100L / totalTime)
               .append("% ")
               .append("ioWait:").append((ioWait - mIoWaitLast) * 100L / totalTime)
               .append("% ]");

       synchronized (mCpuInfoEntries) {
           mCpuInfoEntries.put(System.currentTimeMillis(), stringBuilder.toString());
           if (mCpuInfoEntries.size() > MAX_ENTRY_COUNT) {
               for (Map.Entry<Long, String> entry : mCpuInfoEntries.entrySet()) {
                   Long key = entry.getKey();
                   mCpuInfoEntries.remove(key);
                   break;
               }
           }
       }
   }
   mUserLast = user;
   mSystemLast = system;
   mIdleLast = idle;
   mIoWaitLast = ioWait;
   mTotalLast = total;

   mAppCpuTimeLast = appCpuTime;
}

接下來回到BlockCanaryInternals類看下阻塞事件發(fā)生時的處理:

BlockCanaryInternals.java

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.java

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

然后創(chuàng)建BlockInfo存儲到本地文件中,如果設(shè)置了阻塞監(jiān)聽,會逐一回調(diào)給監(jiān)聽者。如果我們設(shè)置了顯示通知,會回調(diào)給DisplayService類處理通知的顯示。

判斷CPU是否比較busy的值為采樣時間間隔的1.2倍,如果兩次采樣時間間隔大于采樣時間間隔的1.2倍,則認(rèn)為CPU當(dāng)前是busy的。

CpuSampler.java

public CpuSampler(long sampleInterval) {
   super(sampleInterval);
   BUSY_TIME = (int) (mSampleInterval * 1.2f);
}

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

至此,BlockCanary中的關(guān)鍵源碼處理流程我們就分析完了,剩下的就是blockcanary-android模塊中的通知及相關(guān)頁面的處理,感興趣的同學(xué)請自行了解。

參考

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容