BlockCanary源码分析

来源:互联网 发布:javascript静态网页 编辑:程序博客网 时间:2024/06/05 07:03

BlockCanary是一个Android平台的非侵入式的性能监控组件,应用只需要实现一个抽象类,提供一些该组件需要的上下文环境,就可以在平时使用应用的时候检测主线程上的各种卡慢问题,并通过组件提供的各种信息分析出原因并进行修复。(作者原述)

很敬佩作者能够写出这么高质量的代码,BlockCanary的代码设计简洁明了,结构清晰,从第一行代码install走下去,很快就能了解到整个框架的思路。去网上搜了下作者,发现作者12年毕业,自己很汗颜。
本文主要介绍BlockCanary的实现原理和源码设计。

项目地址

https://github.com/markzhai/AndroidPerformanceMonitor

设计思路

首先我们确定一个事实,所有的UI操作都要经过Looper和Handler的处理。如图所示。这里写图片描述
ActivityThread是个android私有类,在应用层无法直接访问(通过反射可以访问),Handler是一个应用层经常自定义的类。Looper是个公开类,而且被final修饰,所以无法被继承,这样就给了应用层分析的机会。下面是Looper.loop的代码:
这里写图片描述
从代码中可以看到,在dispatchMessage的前后,Looper都做了一次logging的判断,进行log的打印。非常幸运的是,android提供了一个公开的接口,允许各app自定义这个Printer。该接口是:
这里写图片描述
大家应该都知道主线程默认会生成一个Looper,这个Looper其实就是在ActivityThread中被创建的,在创建之后,也写了setMessageLogging的代码,只不过if(false)了。
这里写图片描述
BlockCanary的关键代码就在Looper. setMessageLogging上,通过注册自己的Printer,得到UI事件的开始处理时间和结束处理时间,若时间超过block阈值,则提取主线程的调用栈信息和cpu使用信息。

下图是作者绘制的流程图,非常明了的展示了BlockCanary的工作方式。一个额外注意的地方就是在dispatchMessage的同时,会执行一个计时器,如果超过了某个阈值,就会后台dump主线程的调用栈和cpu使用信息、内存使用信息。
这里写图片描述
接下来就开始代码分析吧。

源码分析

BlockCanary号称APP一行代码即可搞定。
这里写图片描述
就从BlockCanary.install这个方法开始吧。

BlockCanary.install

public static BlockCanary install(Context context, BlockCanaryContext blockCanaryContext) {    BlockCanaryContext.init(context, blockCanaryContext);    setEnabled(context, DisplayActivity.class, BlockCanaryContext.get().displayNotification());    return get();}

该方法包括三个步骤
1. BlockCanaryContext.init(context, blockCanaryContext);。就是把app设置的参数传递给BlockCanaryContext。
2. setEnabled(context, DisplayActivity.class, BlockCanaryContext.get().displayNotification()); DisplayActivity是用来展示block信息的界面,该方法就是根据app设置是否显示通知来设置该activity是否展示。通常在debug的时候为true,release的时候为false。
3. get()。使用懒汉的单例模式,生成BlockCanary实例.

BlockCanary()

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

BlockCanaryInternals是BlockCanary的内部实现,在这个类中创建了Looper的printer。mBlockCanaryCore是其单例对象。代码当中,还有两句addBlockInterceptor,该方法是注册两个block的拦截器,供UI或者APP额外处理block的事件。mBlockCanaryCore.addBlockInterceptor(BlockCanaryContext.get());是在APP中实现,
mBlockCanaryCore.addBlockInterceptor(new DisplayService())是BlockCanary的内部实现,用来发出block通知,调出DisplayActivity展示block信息。
BlockInterceptor的定义如下。

    void onBlock(Context context, BlockInfo blockInfo);}

BlockCanaryInternals()

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

这个方法做了四件事:
1. new StackSampler。该类是用来dump thread的stack信息。传递的参数是主线程和dump间隔。
2. new CpuSampler。该类是用来dump cpu的使用情况,传递的参数是dump间隔。
3. setMonitor。终于创建监听器了。到LooperMonitor类中,发现该类实现了Printer,并且实现了println方法。还记得文章开始提到的Looper.loop()中dispatchMessage前后的logging方法吗?没错,最终就会调用到LooperMonitor.println()中来。
4. onBlockEvent是在当block事件发生后,监听器要做的事情,包括:保存已经dump下来的主线程调用栈,cpu使用情况和内存使用情况以及监听的app的基本信息等;通知block拦截器分别处理各自的事情,也就是通知DisplayService去创建通知栏信息。
5. LogWriter.cleanObsolete(),删除过时的log。BlockCanary默认保存两天的log。
终于初始化完了。接下来就该启动监听了吧。还记得Application的那唯一一句代码吗?除了install,可千万别忘了后面还有个start呢。

Start monitor

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

这句代码可真是BlockCanary最关键的一句代码。mBlockCanaryCore.monitor就是刚才BlockCanaryInternals构造函数中setMonitor设置的。
监听器设置了,UI事件的响应可以监听了。接下来我们继续分析监听器的处理过程。需要详细分析LooperMonitor这个类。

LooperMonitor

先分析该类中最重要的一个方法:println。

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

可以看到这个方法的主要思想是:如果是dispatchMessage之前调用,则记录开始时间和startDump。mStartTimestamp表示的是当前系统时间,mStartThreadTimestamp表示的是当前线程纯运行时间。SystemClock.currentThreadTimeMillis()这个方法返回的时间并不是当前线程的已存活时间,而是当前线程处于运行状态的总时间。如果该线程执行过sleep或者wait,那么sleep和wait的时间是不记录到该方法里的。如果是dispatchMessage之后调用,则根据时间差判断是否block,如果block,则发送block事件,会调用到前面提到的onBlockEvent方法中,同时停止dump。isBlock和notifyBlockEvent的方法很简单,不在详述。

doDump

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

还记得在BlockCanaryInternals的构造函数中创建的stackSampler和cpuSampler吗?该他们上场了。这两个类都继承AbstractSampler,区别在于doSample()上。本文主要讲这个方法。先看StackSampler的。

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

代码很简单,提取当前线程的调用栈。mCurrentThread其实就是主线程,因为在new StackSampler传递参数时,代码中用的是Looper.getMainLooper().getThread(),然后以当前时间戳为key放到sStackMap中。sStackMap是HashMap吗?是,但作者将他定义是一个LinkedHashMap。LinkedHashMap和HashMap的区别是:LinkedHashMap能够记录entry的插入顺序,而HashMap无法做到,所以当进行遍历时,LinkedHashMap是按照先插入后输出的顺序进行,而HashMap的顺序是未知的。在该场景下LinkedHashMap输出的内容基本上可以保证按时间戳从小到大排列。
接下来我们再看CpuSampler的doSample方法:

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

这个方法就是读取/proc/下的两个stat内存文件。可以直接通过adb shell去查看这两个文件中的原始内容是什么。
/proc/stat的内容如下:
这里写图片描述
/proc/${pid}/stat的内容如下:
这里写图片描述
然后执行parse方法进行解析。

整个监听的过程中就分析完了,还有一些UI的代码本文就不做分析了。

原创粉丝点击