BlockCanary源码分析

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

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

项目地址

https://github.com/markzhai/AndroidPerformanceMonitor

设计思路

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

下图是作者绘制的流程图,非常明了的展示了BlockCanary的工作方式。一个额外注意的地方就是在dispatchMessage的同时,会执行一个计时器,如果超过了某个阈值,就会后
接下来就开始代码分析吧。

源码分析

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

 
 
  • 6

该方法包括三个步骤
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());
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

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

 
 
  • 1
  • 2
  • 3

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
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37

这个方法做了四件事:
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);
    }
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

这句代码可真是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();
    }
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

可以看到这个方法的主要思想是:如果是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();
    }
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

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

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

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

代码很简单,提取当前线程的调用栈。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);
        }
    }
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39

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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值