BlockCanary源码分析

来源:互联网 发布:linux查看存储空间 编辑:程序博客网 时间:2024/06/14 01:32

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的。

@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());    }}
  • 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的代码本文就不做分析了。

阅读全文
'); })();
0 0
原创粉丝点击
热门IT博客
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 蛇血欲焰txt下载 蛇血欲焰 往事悠悠 蛇血欲焰和欧阳倩10次 蛇血欲焰txt全集下载 血欲江湖天雨寒阅读 蛇血欲焰下载 国民粉丝 风月血殇 血气 动脉血气 血气不足 血气胸 血气之勇 血气唤醒 动脉血气针 抽血气 血气分析ppt 血气分析be 血气分析正常值 血气不足怎么调理 血气分析结果判断及临床意义 动脉血气分析正常值 贺银成血气分析六步法 女性血气不足怎么调理 血气不通的症状 血气不足怎么补 血气不足吃什么 血气不通怎么调理 血气不足吃什么好 血气不足的症状 血气电解质分析仪 动脉血气分析的正常值 血气分析怎么看 雷度血气分析仪 电解质血气分析仪 男人血气不足 沃芬血气分析仪 抽血气的技巧 正常血气分析值 动脉血气分析采集方法 血气分析怎么抽 血气分析结果