主流開源框架之BlockCanary深刻了解

主流開源框架源碼深刻了解第5篇——BlockCanary源碼分析。(源碼以1.5.0版爲準)java

UI卡頓原理

問:爲何16ms沒完成繪製就會卡頓?android

咱們先來了解幾個概念:git

  1. Android系統每隔16ms就會從新繪製一次Activity,所以,咱們的應用必須在16ms內完成屏幕刷新的所有邏輯操做,每一幀只能停留16ms,不然就會出現掉幀現象(也就是用戶看到的卡頓現象)。
  2. 16ms = 1000/60hz,至關於60fps(每秒幀率)。這是由於人眼與大腦之間的協做沒法感知超過60fps的畫面更新。12fps大概相似手動快速翻書的幀率,這個速度明顯能夠感知是不夠順滑的。24fps使得人眼感知的是連續線性運動,24fps是電影膠圈一般使用的幀率,這個幀率能夠支撐大部分電影畫面須要表達的內容。可是低於30fps是沒法順暢表現絢麗的畫面內容,此時須要使用60fps來達到想要的效果。所以,若是應用沒有在16ms內完成屏幕刷新的所有邏輯操做,就會發生卡頓。
  3. Android不容許在UI線程中作耗時的操做,不然有可能發生ANR的可能,默認狀況下,在Android中Activity的最長執行時間是5秒,BroadcastReceiver的最長執行時間則是10秒,Service前臺20s、後臺200s未完成啓動。若是超過默認最大時長,則會產生ANR。

答:Android系統每隔16ms就會發出VSYNC信號,觸發對UI進行渲染,VSYNC是Vertical Synchronization(垂直同步)的縮寫,能夠簡單的把它認爲是一種定時中斷。在Android 4.1中開始引入VSYNC機制。爲何是16ms?由於Android設定的刷新率是60FPS(Frame Per Second),也就是每秒60幀的刷新率,約16ms刷新一次。這就意味着,咱們須要在16ms內完成下一次要刷新的界面的相關運算,以便界面刷新更新。舉個例子,當運算須要24ms完成時,16ms時就沒法正常刷新了,而須要等到32ms時刷新,這就是丟幀了。丟幀越多,給用戶的感受就越卡頓。github

正常流暢刷新圖示: 算法

哎呀!丟幀啦。卡頓圖示: 數組

BlockCanary原理

在說原理以前,咱們先來了解幾個概念:bash

  1. 主線程ActivityThread:嚴格來講,UI主線程不是ActivityThread。ActivityThread類是Android APP進程的初始類,它的main函數是這個APP進程的入口。APP進程中UI事件的執行代碼段都是由ActivityThread提供的。也就是說,Main Thread實例是存在的,只是建立它的代碼咱們不可見。ActivityThread的main函數就是在這個Main Thread裏被執行的。這個主線程會建立一個Looper(Looper.prepare),而Looper又會關聯一個MessageQueue,主線程Looper會在應用的生命週期內不斷輪詢(Looper.loop),從MessageQueue取出Message 更新UI。網絡

    // ActivityThread類:
     public static void main(String[] args) {
         Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ActivityThreadMain");
         ...
         Looper.prepareMainLooper();
         ...
         ActivityThread thread = new ActivityThread();
         thread.attach(false, startSeq);
     
         if (sMainThreadHandler == null) {
             sMainThreadHandler = thread.getHandler();
         }
         ...
         // End of event ActivityThreadMain.
         Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
         // Looper開始輪詢
         Looper.loop();
     
         throw new RuntimeException("Main thread loop unexpectedly exited");
     }
    複製代碼
  2. Vsync信號:屏幕的刷新過程是每一行從左到右(行刷新,水平刷新,Horizontal Scanning),從上到下(屏幕刷新,垂直刷新,Vertical Scanning)。當整個屏幕刷新完畢,即一個垂直刷新週期完成,會有短暫的空白期,此時發出 VSync 信號。因此,VSync 中的 V指的是垂直刷新中的垂直-Vertical。Android系統每隔16ms發出VSYNC信號,觸發對UI進行渲染,VSync是Vertical Synchronization(垂直同步)的縮寫,是一種在PC上很早就普遍使用的技術,能夠簡單的把它認爲是一種定時中斷。而在Android 4.1(JB)中已經開始引入VSync機制,用來同步渲染,讓App的UI和SurfaceFlinger能夠按硬件產生的VSync節奏進行工做。併發

  3. 界面刷新:界面上任何一個 View 的刷新請求最終都會走到 ViewRootImpl 中的 scheduleTraversals() 裏來安排一次遍歷繪製 View 樹的任務;而且經過源碼均可以知道全部的界面刷新(包括Vsync信號觸發的),都會經過Choreographer 的 postCallback() 方法,將界面刷新這個 Runnable 任務以當前事件放進一個待執行的隊列裏,最後經過主線程的Looper的loop方法取出消息並執行。app

    // Looper類:
        public static void loop() {
            final Looper me = myLooper();
            ...
            // 獲取當前Looper的消息隊列
            final MessageQueue queue = me.mQueue;
            ...
            for (; ; ) {
                // 取出一個消息
                Message msg = queue.next(); // might block
                ...
                // "此mLogging可經過Looper.getMainLooper().setMessageLogging方法設置自定義"
                final Printer logging = me.mLogging;
                if (logging != null) {// 消息處理前
                    // "若mLogging不爲null,則此處可回調到該類的println方法"
                    logging.println(">>>>> Dispatching to " + msg.target + " " +
                            msg.callback + ": " + msg.what);
                }
        
                ...
                try {
                    // 消息處理
                    msg.target.dispatchMessage(msg);
                    dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
                } finally {
                    if (traceTag != 0) {
                        Trace.traceEnd(traceTag);
                    }
                }
                ...
        
                if (logging != null) {// 消息處理後
                    // "消息處理後,也可調用logging的println方法"
                    logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
                }
        
                ...
            }
        }
    複製代碼
  4. 卡頓發生點:從第3條中,咱們能夠看出,全部消息最終都通過dispatchMessage方法。所以界面的卡頓最終都應該是發生在Handler的dispatchMessage裏。

    // Handler類:
    public void dispatchMessage(Message msg) {
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }
    }
    複製代碼
  5. 屏幕刷新機制,具體可參考:Android 屏幕刷新機制
    Handler消息機制和View的繪製機制,具體可參考:Handler機制View繪製流程源碼分析

原理: 上面幾個概念中,其實裏面已包含卡頓監控的原理啦。咱們在界面刷新中Looper的loop方法註釋中聲明:"若mLogging不爲null,則此處可回調到該類的println方法",所以咱們能夠經過自定義的mLogging(實際爲Printer接口的子類),實現Printer接口的println方法,而後在println方法中監控是否有卡頓發生。從loop方法中,能夠看出logging.println調用是成對出現的,會在消息處理先後分別調用,所以能夠在自定義的println方法中經過標識來分辨是消息處理前/後,經過計算時間差與咱們本身設置的閥值(咱們認爲消息處理的最長時間,即卡頓的臨界值)比對,來監控咱們的程序是否發生卡頓。

官方原理介紹示例圖:

BlockCanary簡介

1. 關聯類功能說明

  1. BlockCanary:外觀類,提供初始化及開始、中止監聽
  2. BlockCanaryContext:配置上下文,可配置id、當前網絡信息、卡頓閾值、log保存路徑等。建議:經過本身實現繼承該類的子類,配置應用標識符,用戶uid,網絡類型,卡頓判斷閥值,Log保存位置等,可經過繼承該類將卡頓信息收集上傳雲端或保存本地等。
  3. BlockCanaryInternals:blockcanary核心的調度類,內部包含了monitor(設置到MainLooper的printer)、stackSampler(棧信息處理器)、cpuSampler(cpu信息處理器)、mInterceptorChain(註冊的攔截器)、以及onBlockEvent的回調及攔截器的分發。
  4. LooperMonitor:繼承了Printer接口,用於設置到MainLooper中。經過複寫println的方法來獲取MainLooper的dispatch先後的執行時間差,並控制stackSampler和cpuSampler的信息採集。
  5. StackSampler:用於獲取線程的棧信息,將採集的棧信息存儲到一個以key爲時間戳的LinkHashMap中。經過mCurrentThread.getStackTrace()獲取當前線程的StackTraceElement。
  6. CpuSampler:用於獲取cpu信息,將採集的cpu信息存儲到一個以key爲時間戳的LinkHashMap中。經過讀取/proc/stat文件獲取cpu的信息。
  7. DisplayService:繼承了BlockInterceptor攔截器,onBlock回調會觸發發送前臺通知。
  8. DisplayActivity:用於顯示記錄的異常信息的Activity。
  9. HandlerThreadFactory:傳入一個HandlerThread類Looper的異步Handler。HandlerThread本質上是一個線程類,它繼承了Thread;HandlerThread有本身的內部Looper對象,能夠進行loop循環;經過獲取HandlerThread的looper對象傳遞給Handler對象,能夠在handleMessage方法中執行異步任務;建立HandlerThread後必須先調用HandlerThread.start()方法,Thread會先調用run方法,建立Looper對象。

2. BlockCanary簡單使用

// Application中
    // 卡頓優化
    // 指定的卡頓閥值爲500毫秒——provideBlockThreshold()方法;可在onBlock方法處收集堆棧信息
    BlockCanary.install(this, new AppBlockCanaryContext()).start();

/**
 * BlockCanary配置的各類信息(部分)
 */
public class AppBlockCanaryContext extends BlockCanaryContext {
    // 實現各類上下文,包括應用標識符,用戶uid,網絡類型,卡頓判斷閥值,Log保存位置等

    /**
     * 指定的卡頓閥值 500毫秒
     */
    public int provideBlockThreshold() {
        return 500;
    }

    /**
     * 保存日誌的路徑
     */
    public String providePath() {
        return "/blockcanary/";
    }

    /**
     * 是否須要在通知欄通知卡頓
     */
    public boolean displayNotification() {
        return true;
    }

    /**
     * 此處可收集堆棧信息,以備上傳分析
     * Block interceptor, developer may provide their own actions.
     */
    public void onBlock(Context context, BlockInfo blockInfo) {
        Log.i("lz","blockInfo "+blockInfo.toString());
        // 獲取當前執行方法的調用棧信息
//        String trace = Log.getStackTraceString(new Throwable());
    }
複製代碼

AppBlockCanaryContext具體配置可參考:AppBlockCanaryContext.java

BlockCanary源碼

1. BlockCanary.install

public static BlockCanary install(Context context, BlockCanaryContext blockCanaryContext) {
        // 將上下文和咱們自定義的blockCanaryContext傳入
        BlockCanaryContext.init(context, blockCanaryContext);
        // 根據displayNotification()設置是否啓用或者禁用DisplayActivity組件
        setEnabled(context, DisplayActivity.class, BlockCanaryContext.get().displayNotification());
        // 返回單例BlockCanary
        return get();
    }
複製代碼

咱們能夠看到install方法中幹了3件事情,咱們分別來分析一下。

1. BlockCanaryContext.init

// BlockCanaryContext類:
    private static Context sApplicationContext;
    private static BlockCanaryContext sInstance = null;

    static void init(Context context, BlockCanaryContext blockCanaryContext) {
        sApplicationContext = context;
        // 將咱們自定義的blockCanaryContext類,保存在BlockCanaryContext類的成員變量sInstance中
        sInstance = blockCanaryContext;
    }
複製代碼

第一步,實際上就是在咱們使用BlockCanary時,將咱們自定義的AppBlockCanaryContext保存在BlockCanaryContext類的成員變量sInstance中,以供BlockCanary能夠經過sInstance,來使用咱們自已配置的各類信息(包括應用標識符,用戶uid,網絡類型,卡頓判斷閥值,Log保存位置等)。

2. setEnabled啓用或禁用組件

// BlockCanaryContext類:
    public static BlockCanaryContext get() {
        if (sInstance == null) {
            throw new RuntimeException("BlockCanaryContext null");
        } else {
            return sInstance;
        }
    }
    
 // BlockCanary類:    
    // 調用newSingleThreadExecutor初始化文件IO線程池
    private static final Executor fileIoExecutor = newSingleThreadExecutor("File-IO");

    private static void setEnabledBlocking(Context appContext,
                                           Class<?> componentClass,
                                           boolean enabled) {
        // 初始化組件對象
        ComponentName component = new ComponentName(appContext, componentClass);
        // 獲取包管理者
        PackageManager packageManager = appContext.getPackageManager();
        int newState = enabled ? COMPONENT_ENABLED_STATE_ENABLED : COMPONENT_ENABLED_STATE_DISABLED;
        // 動態不殺死應用啓用或者禁用組件,若enabled爲true則啓用,不然禁用
        packageManager.setComponentEnabledSetting(component, newState, DONT_KILL_APP);
    }

    private static void executeOnFileIoThread(Runnable runnable) {
        fileIoExecutor.execute(runnable);
    }

    private static Executor newSingleThreadExecutor(String threadName) {
        return Executors.newSingleThreadExecutor(new SingleThreadFactory(threadName));
    }

    private static void setEnabled(Context context,
                                   final Class<?> componentClass,
                                   final boolean enabled) {
        final Context appContext = context.getApplicationContext();
        executeOnFileIoThread(new Runnable() {
            @Override
            public void run() {
                setEnabledBlocking(appContext, componentClass, enabled);
            }
        });
    }
複製代碼

從上述代碼中,能夠看出來setEnabled方法,經過參數:BlockCanaryContext.get().displayNotification(),來設置DisplayActivity組件(用於顯示記錄的異常信息給開發者)是否啓用。

  1. BlockCanaryContext.get()返回的實際上就是第一步中所說到的咱們自定義的AppBlockCanaryContext對象的引用變量sInstance,所以,若咱們自定義的AppBlockCanaryContext中定義了displayNotification()方法,則按照咱們本身定義的執行,若沒有定義則按照其父類,即BlockCanaryContext中的displayNotification()方法返回值執行,默認返回爲true。
  2. setEnabled方法中,經過executeOnFileIoThread方法,使用靜態常量fileIoExecutor線程池執行異步任務,根據咱們傳入的enabled(是否容許啓用組件標識),來最終啓用或者禁用對應組件。關於動態啓用或者禁用組件可參考:Android動態啓用和禁用四大組件

3. get()返回單例BlockCanary對象

public static BlockCanary get() {
        if (sInstance == null) {
            synchronized (BlockCanary.class) {
                if (sInstance == null) {
                    sInstance = new BlockCanary();
                }
            }
        }
        return sInstance;
    }
    
    private BlockCanary() {
        // 將BlockCanaryContext.get(),即sInstance(咱們自定義的AppBlockCanaryContext)
        // 設置到BlockCanary核心類BlockCanaryInternals中,用來獲取咱們自定義配置的信息
        BlockCanaryInternals.setContext(BlockCanaryContext.get());
        // 初始化BlockCanaryInternals
        mBlockCanaryCore = BlockCanaryInternals.getInstance();
        // 添加攔截器(將自定義的AppBlockCanaryContext添加到攔截器中,可回調其onBlock方法)
        mBlockCanaryCore.addBlockInterceptor(BlockCanaryContext.get());
        // 根據咱們自定義的AppBlockCanaryContext獲取是否展現通知,默認爲true
        if (!BlockCanaryContext.get().displayNotification()) {
            return;
        }
        // 若容許展現通知,則將DisplayService繼續添加到攔截器中
        mBlockCanaryCore.addBlockInterceptor(new DisplayService());

    }
複製代碼

咱們從這部分源碼中看到,BlockCanary的構造方法中完成了其核心類:BlockCanaryInternals的初始化與設置(包括sInstance傳入和添加攔截器),那麼咱們再來看一看BlockCanaryInternals的初始化都有些什麼操做:

// BlockCanaryInternals類:
    static BlockCanaryInternals getInstance() {
        if (sInstance == null) {
            synchronized (BlockCanaryInternals.class) {
                if (sInstance == null) {
                    sInstance = new BlockCanaryInternals();
                }
            }
        }
        return sInstance;
    }
    
    public BlockCanaryInternals() {
        // 初始化堆棧採樣器
        stackSampler = new StackSampler(
                Looper.getMainLooper().getThread(),
                sContext.provideDumpInterval());
        // 初始化cpu採樣器
        cpuSampler = new CpuSampler(sContext.provideDumpInterval());
        // 設置監視器,傳入LooperMonitor looper監控器
        // LooperMonitor 實際上就是咱們上面【BlockCanary原理】中講到的Printer接口的子類
        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) {
                        // 遍歷全部攔截器成員,調用每一個成員的onBlock,並將卡頓信息傳入
                        for (BlockInterceptor interceptor : mInterceptorChain) {
                            interceptor.onBlock(getContext().provideContext(), blockInfo);
                        }
                    }
                }
            }
        }, getContext().provideBlockThreshold(), getContext().stopWhenDebugging()));

        LogWriter.cleanObsolete();
    }
    
    private void setMonitor(LooperMonitor looperPrinter) {
        // setMonitor把建立的LooperMonitor賦值給BlockCanaryInternals的成員變量monitor。
        monitor = looperPrinter;
    }
複製代碼

BlockCanaryInternals的構造方法中,初始化了幾個變量,包括:堆棧採樣器、cpu採樣器、looper監控器,以及looper監控器的回調方法onBlockEvent。

2. BlockCanary.start()

1. 監控卡頓

public void start() {
        if (!mMonitorStarted) {
            mMonitorStarted = true;
            // 設置Looper中的mLogging,每次消息處理先後,
            // 均可回調自定義的實現Printer接口LooperMonitor類的println方法
            Looper.getMainLooper().setMessageLogging(mBlockCanaryCore.monitor);
        }
    }
複製代碼

將在BlockCanaryInternals中建立的LooperMonitor給主線程Looper的mLogging變量賦值。這樣主線程Looper就能夠消息分發先後使用LooperMonitor#println輸出日誌。此時BlockCanary已經開始監控卡頓狀況,因此咱們如今須要關注的就是LooperMonitor的println方法。

再回顧一下Looper的loop方法:

//Looper
    for (;;) {
        Message msg = queue.next();
        // 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);
        }
        ...
    }
複製代碼

Lopper的loop方法中logging如今就是BlockCanary中實現了Printer接口的LooperMonitor類。

// LooperMonitor類:
    private boolean mPrintingStarted = false;
    @Override
    public void println(String x) {
        if (mStopWhenDebugging && Debug.isDebuggerConnected()) {
            return;
        }
        if (!mPrintingStarted) {
            // 獲取消息處理前系統當前時間
            mStartTimestamp = System.currentTimeMillis();
            // 獲取當前線程運行時間
            mStartThreadTimestamp = SystemClock.currentThreadTimeMillis();
            // 將此標識置爲true,下此進入就是消息處理以後
            mPrintingStarted = true;
            // 開始獲取堆棧信息
            startDump();
        } else {
            // 獲取消息處理後系統當前時間
            final long endTime = System.currentTimeMillis();
            // 將此標識置爲true,下此進入就是下一條消息處理以前
            mPrintingStarted = false;
            // 判斷是否發生卡頓
            if (isBlock(endTime)) {
                // 發生卡頓,通知卡頓事件發生
                notifyBlockEvent(endTime);
            }
            // 中止獲取堆棧信息
            stopDump();
        }
    }
複製代碼

對於每個Message消息而言,println方法都是按順序成對出現的,所以根據mPrintingStarted是不是消息開始前的標識,來判斷此消息當前的處理先後兩種狀態。下面咱們來看一下卡頓發生的狀況:

// LooperMonitor類:
    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異步線程Looper的Handler
        HandlerThreadFactory.getWriteLogThreadHandler().post(new Runnable() {
            @Override
            public void run() {
                // 異步線程中執行onBlockEvent回調
                mBlockListener.onBlockEvent(startTime, endTime, startThreadTime, endThreadTime);
            }
        });
    }
複製代碼

經過消息執行的先後時間差 - 咱們自定義AppBlockCanaryContext中設置的卡頓閥值,來肯定是否發生卡頓,卡頓後的回調消息是在設置爲異步線程Looper的Handler中執行。

// 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,則會發送前臺的通知,代碼以下:

// 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);
        // 根據不一樣的sdk兼容全部版本的通知欄顯示
        show(context, contentTitle, contentText, pendingIntent);
    }
複製代碼

2. 卡頓信息記錄

// LooperMonitor類:
    private void startDump() {
        if (null != BlockCanaryInternals.getInstance().stackSampler) {
            // 開始記錄堆棧信息
            BlockCanaryInternals.getInstance().stackSampler.start();
        }

        if (null != BlockCanaryInternals.getInstance().cpuSampler) {
            // 開始記錄cpu信息
            BlockCanaryInternals.getInstance().cpuSampler.start();
        }
    }

    private void stopDump() {
        if (null != BlockCanaryInternals.getInstance().stackSampler) {
            // 中止記錄堆棧信息
            BlockCanaryInternals.getInstance().stackSampler.stop();
        }

        if (null != BlockCanaryInternals.getInstance().cpuSampler) {
            // 中止記錄cpu信息
            BlockCanaryInternals.getInstance().cpuSampler.stop();
        }
    }
    
    public void start() {
        // mShouldSample其實是AtomicBoolean原子布爾值。
        if (mShouldSample.get()) {
            return;
        }
        // 原子布爾值,可以保證在高併發的狀況下只有一個線程可以訪問這個屬性值。
        // 原子布爾值具體詳情,參考:https://www.jianshu.com/p/8a44d4a819bc
        mShouldSample.set(true);
        // 移除上一次任務
        HandlerThreadFactory.getTimerThreadHandler().removeCallbacks(mRunnable);
        // 延遲 卡頓閥值*0.8 的時間執行相應信息的收集
        HandlerThreadFactory.getTimerThreadHandler().postDelayed(mRunnable,
                BlockCanaryInternals.getInstance().getSampleDelay());
    }

    public void stop() {
        if (!mShouldSample.get()) {
            return;
        }
        mShouldSample.set(false);
        // 移除任務
        HandlerThreadFactory.getTimerThreadHandler().removeCallbacks(mRunnable);
    }
    
    private Runnable mRunnable = new Runnable() {
        @Override
        public void run() {
            // 調用doSample方法,執行相應操做
            doSample();
            // 若此原子布爾值爲true,即此時爲開始記錄堆棧信息
            if (mShouldSample.get()) {
                // 延遲 卡頓閥值 時間執行任務
                HandlerThreadFactory.getTimerThreadHandler()
                        .postDelayed(mRunnable, mSampleInterval);
            }
        }
    };

// BlockCanaryInternals類:
    long getSampleDelay() {
        // 卡頓閥值的0.8
        return (long) (BlockCanaryInternals.getContext().provideBlockThreshold() * 0.8f);
    }
複製代碼

卡頓信息的記錄,其實是經過CpuSampler和StackSampler二者相同父類AbstractSampler類,提供的方法start和stop記錄,而start方法中經過HandlerThreadFactory獲取異步的TimerThreadHandler發送延時消息,最後分別調用CpuSampler類和StackSampler類中,繼承自AbstractSampler抽象方法doSample()完成的卡頓信息的記錄。下面分別看一下CpuSampler類和StackSampler類的doSample()方法的實現。

  1. StackSampler類的doSample()方法

    private static final LinkedHashMap<Long, String> sStackMap = new LinkedHashMap<>();
    @Override
    protected void doSample() {
        StringBuilder stringBuilder = new StringBuilder();
        // mCurrentThread.getStackTrace():返回一個表示該線程堆棧轉儲的堆棧跟蹤元素數組。
        // 經過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());
            }
            // 以當前系統時間爲key,存儲此處的堆棧信息
            sStackMap.put(System.currentTimeMillis(), stringBuilder.toString());
        }
    }
    複製代碼
  2. CpuSampler類的doSample()方法

    // 主要經過獲取/proc/stat文件 去獲取cpu的信息
    @Override
    protected void doSample() {
        BufferedReader cpuReader = null;
        BufferedReader pidReader = null;
    
        try {
            // 經過bufferReader讀取 /proc 下的cpu文件
            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();
            }
            // 經過bufferReader讀取 /proc 下的內存文件
            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;
    }
    複製代碼

    Android平臺CPU的一些常識:

    1. Android是基於Linux系統的,Android平臺關於CPU的計算是跟Linux是徹底同樣的。
    2. 在Linux中CPU活動信息是保存在/proc/stat文件中,該文件中的全部值都是從系統啓動開始累計到當前時刻。
    3. /proc/stat文件內容:
      > cat /proc/stat
      1. cpu  2255 34 2290 22625563 6290 127 456
      2. cpu0 1132 34 1441 11311718 3675 127 438
      3. cpu1 1123 0 849 11313845 2614 0 18
      4. intr 114930548 113199788 3 0 5 263 0 4 [... lots more numbers ...]
      5. ctxt 1990473
      6. btime 1062191376
      7. processes 2915
      8. procs_running 1
      9. procs_blocked 0
      複製代碼
      這些數字指明瞭CPU執行不一樣的任務所消耗的時間(從系統啓動開始累計到當前時刻)。時間單位是USER_HZ或jiffies(一般是百分之一秒)。
    4. 解析3中第一行各數值的含義
      參數	        解析 (如下數值都是從系統啓動累計到當前時刻)
      user (38082)	處於用戶態的運行時間,不包含 nice值爲負進程
      nice (627)	nice值爲負的進程所佔用的CPU時間
      system (27594)	處於核心態的運行時間
      idle (893908)	除IO等待時間之外的其它等待時間iowait (12256) 從系統啓動開始累計到當前時刻,IO等待時間
      irq (581)	硬中斷時間
      irq (581)	軟中斷時間
      stealstolen(0)	一個其餘的操做系統運行在虛擬環境下所花費的時間
      guest(0)	這是在Linux內核控制下爲客戶操做系統運行虛擬CPU所花費的時間
      複製代碼
      總結:總的CPU時間totalCpuTime = user + nice + system + idle + iowait + irq + softirq + stealstolen + guest
    5. /proc/pid/stat文件:包含了某一進程全部的活動的信息,該文件中的全部值都是從系統啓動開始累計到當前時刻
      cat /proc/6873/stat 
      6873 (a.out) R 6723 6873 6723 34819 6873 8388608 77 0 0 0 41958 31 0 0 25 0 3 0 5882654 1409024 56 4294967295 134512640 134513720 3215579040 0 2097798 0 0 0 0 0 0 0 17 0 0 0
      複製代碼
      計算CPU使用率有用相關參數:
      參數	    解析
      pid=6873	進程號
      utime=1587	該任務在用戶態運行的時間,單位爲jiffies
      stime=41958	該任務在覈心態運行的時間,單位爲jiffies
      cutime=0	全部已死線程在用戶態運行的時間,單位爲jiffies
      cstime=0	全部已死在覈心態運行的時間,單位爲jiffies
      複製代碼
      結論:進程的總CPU時間processCpuTime = utime + stime + cutime + cstime,該值包括其全部線程的CPU時間。

3. 卡頓日誌記錄

卡頓發生時,會回調LooperMonitor的onBlockEvent方法,而此方法中,會將卡頓信息寫入本地日誌文件,日誌的路徑在自定義的AppBlockCanaryContext中定義。

// BlockCanaryInternals類構造方法中:
    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類:
    public static String save(String str) {
        String path;
        synchronized (SAVE_DELETE_LOCK) {
            path = save("looper", str);
        }
        return path;
    }
    
    private static String save(String logFileName, String str) {
        String path = "";
        BufferedWriter writer = null;
        try {
            // 根據開發者本身配置的日誌存儲路徑,生成文件
            File file = BlockCanaryInternals.detectedBlockDirectory();
            long time = System.currentTimeMillis();
            path = file.getAbsolutePath() + "/"
                    + logFileName + "-"
                    + FILE_NAME_FORMATTER.format(time) + ".log";
            // 寫入卡頓信息
            OutputStreamWriter out =
                    new OutputStreamWriter(new FileOutputStream(path, true), "UTF-8");

            writer = new BufferedWriter(out);

            writer.write(BlockInfo.SEPARATOR);
            writer.write("**********************");
            writer.write(BlockInfo.SEPARATOR);
            writer.write(TIME_FORMATTER.format(time) + "(write log time)");
            writer.write(BlockInfo.SEPARATOR);
            writer.write(BlockInfo.SEPARATOR);
            writer.write(str);
            writer.write(BlockInfo.SEPARATOR);

            writer.flush();
            writer.close();
            writer = null;

        } catch (Throwable t) {
            Log.e(TAG, "save: ", t);
        } finally {
            try {
                if (writer != null) {
                    writer.close();
                }
            } catch (Exception e) {
                Log.e(TAG, "save: ", e);
            }
        }
        return path;
    }
複製代碼

BlockCanary卡頓參數解讀

  1. cpuCore:手機cpu個數。
  2. processName:應用包名。
  3. freeMemory: 手機剩餘內存,單位KB。
  4. totalMemory: 手機內訓總和,單位KB。
  5. timecost: 該Message(事件)執行時間,單位 ms。
  6. threadtimecost: 該Message(事件)執行線程時間(線程實際運行時間,不包含別的線程佔用cpu時間),單位 ms。
  7. cpubusy: true表示cpu負載太重,false表示cpu負載不重。cpu負載太重致使該Message(事件) 超時,錯誤不在本事件處理上。

至此,BlockCanary的總體已分析完成,收工咯。

參考連接

www.jianshu.com/p/0d00cb85f…

www.jianshu.com/p/5602ca132…

www.jianshu.com/p/e58992439…

...

注:如有什麼地方闡述有誤,敬請指正。期待您的點贊哦!!!

相關文章
相關標籤/搜索