主流开源框架之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…

...

注:如有什么地方阐述有误,敬请指正。期待您的点赞哦!!!

相关文章
相关标签/搜索