Android卡顿优化 | AndroidPerformanceMonitor(BlockCanary)源码详析(真的很详细哦!)

为了另外一篇性能优化实战方案讲解博客的结构清晰和篇幅, 我们“断章取义”,把框架的源码解析部分搬到这边哈~ 项目GitHub

目录 1. 监控周期的 定义 2. dump模块 / 关于.log文件 3. 采集堆栈周期的 设定 4. 框架的 配置存储类 以及 文件系统操作封装 5. 文件写入过程(生成.log文件的源码) 6. 上传文件 7. 设计模式、技巧 8. 框架中各个主要类的功能划分

1. 【监控周期的 定义】 blockCanary打印一轮信息的周期, 是从主线程一轮阻塞的开始开始,到阻塞的结束结束,为一轮信息; 这个周期我们也可以成为BlockCanary监控周期/监控时间段

2. 【dump模块 / 关于.log文件】 这一个周期的信息,除了展现在通知处,还会展示在logcat处, 同时框架封装了dump模块 即框架会把我们这一轮信息,在手机(移动终端)内存中, 输出成一个.log文件; 【当然,前提是在终端需要给这个APP授权,允许APP读写内存 存放.log文件的目录名,我们可以在上面提到的配置类自定义

如这里定义成blockcanary

在终端生成的文件与目录便是这样:

3. 【采集堆栈周期的 设定】 我们说过配置类中,这个函数可以指定认定为卡顿的阈值时间

这里指定为500ms,使得刚刚那个2s的阻塞被确定为卡顿问题

其实还有一个函数,

用于指定在一个监控周期内,采集数据的周期!!!:

这里返回的同样是500ms

即从线程阻塞开始,每500ms采集一次数据,

给出一个阻塞问题出现的根源

而刚刚那个卡顿问题阻塞的时间是2s

那毫无疑问我们可以猜到,刚刚那个.log文件的内容里边,

2s/500ms = 4采集的堆栈信息!!

但是一个监控周期/log文件只打印一次现场的详细信息:

如果设置为250ms,那便是有2s/250ms = 8采集的堆栈信息了:

4. 【框架的 配置存储类 以及 文件系统操作封装】 框架准备了一个存储配置的类,用于存储响应的配置: 配置存储类:

getPath():拿到sd卡根目录到存储log文件夹的目录路径;!!!!!!!!

detectedBlockDirectory():返回file类型存储log文件文件夹目录(如果没有这个文件夹,就创建文件夹,再返回file类型的这个文件夹);!!!!!!!!!!

getLogFiles()

如果detectedBlockDirectory()返回的那个存储log文件文件夹目录存在的话,

就把这个目录下所有的.log文件过滤提取出来,

并存储在一个File[](即File数组)里边,最后返回这个File数组;!!!!!!!

getLogFiles()中的listFiles()是JDK中的方法,

用来返回文件夹类型的File类实例对应文件夹中(对应目录下)所有的文件,

这里用的是它的重载方法,

就是传入一个过滤器,可以过滤掉不需要的文件;!!!!!!!

BlockLogFileFilter是过滤器,用于过滤出.log文件;

###下面稍微实战一下这个文件封装:

呐我们在MainActivity的onCreate中,使用getLogFiles()

功能是刚说的获取BlockCanary生成的所有.log文件,以.log文件的形式返回,

完了我们把它打印出来:

运行之后,呐,毫无悬念,BlockCanary生成的所有.log文件都被打印出来了:

拿到了文件, 意味着我们可以在适当的时机, 将之上传到服务器处理!!!

5. 【文件写入过程(生成.log文件的源码)】

  • 一切要从框架的初始化开始说起:
  • install()做了什么, install()里边,初始化了BlockCanaryContext和Notification等的一些对象, 重要的,最后return调用了,get() 有点单例的味道哈,BlockCanary的构造方法是私有的(下图可以见得), get()正是返回一个BlockCanary实例, 当然new这一下也就调用了BlockCanary的构造方法; 哦~ BlockCanary的构造方法中, 调用了BlockCanaryInternals.getInstance(); 拿到一个BlockCanaryInternals实例,赋给类中的全局变量!

BlockCanaryInternals.getInstance();同样是使用了单例模式, 返回一个BlockCanaryInternals实例:

同样也是new时候调用了BlockCanaryInternals的构造方法:

可以看到BlockCanaryInternals的构造方法中 出现了关于配置信息存储类以及文件的写入逻辑了; LogWriter.save(blockInfo.toString());注意这里传入的是配置信息的字符串,接着是LogWriter.save(),这里的str便是刚刚的blockInfo.toString(),即配置信息;

往下还有一层save(一参对应刚刚的字符串"looper",二参为Block字符串信息【最早是来自BlockCanaryInternals中的LogWriter.save(blockInfo.toString());中的 blockInfo.toString() 】)

可以看到.log文件名的命名规则的就是定义在这里了, .log文件写入的输入流逻辑,也都在这里了;

对比一下刚刚实验的结果,也就是实际生成的.log文件文件名 可见文件名跟上面save()方法中定义好的规则是一样的,无误;

这两个在表头的字符串格式化器, 第一个是用来给.log文件命名的,.log文件名中的时间序列来自这里; 第二个是在save()函数中,用来写入文件的, 用时间来区分堆栈信息的每一次收集:

下面这个方法是用来构造zip文件实例的, 给出一个文件名,再构造一个成对应的File实例;

这个则是用来删除本框架生成的所有log文件的:

其他的很容易看懂,就不多说了;

6.【上传文件】 首先框架想得很周到哈,它已经为我们封装了一个Uploader类,源码如下:

/*   * Copyright (C) 2016 MarkZhai (http://zhaiyifan.cn).   *   * Licensed under the Apache License, Version 2.0 (the "License");   * you may not use this file except in compliance with the License.   * You may obtain a copy of the License at   *   *     http://www.apache.org/licenses/LICENSE-2.0   *   * Unless required by applicable law or agreed to in writing, software   * distributed under the License is distributed on an "AS IS" BASIS,   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.   * See the License for the specific language governing permissions and   * limitations under the License.   */  ...  final class Uploader {        private static final String TAG = "Uploader";      private static final SimpleDateFormat FORMAT =              new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.US);        private Uploader() {          throw new InstantiationError("Must not instantiate this class");      }        private static File zip() {          String timeString = Long.toString(System.currentTimeMillis());          try {              timeString = FORMAT.format(new Date());          } catch (Throwable e) {              Log.e(TAG, "zip: ", e);          }          File zippedFile = LogWriter.generateTempZip("BlockCanary-" + timeString);          BlockCanaryInternals.getContext().zip(BlockCanaryInternals.getLogFiles(), zippedFile);          LogWriter.deleteAll();          return zippedFile;      }        public static void zipAndUpload() {          HandlerThreadFactory.getWriteLogThreadHandler().post(new Runnable() {              @Override              public void run() {                  final File file = zip();                  if (file.exists()) {                      BlockCanaryInternals.getContext().upload(file);                  }              }          });      }  }

都封装成zip文件了,想得很周到很齐全吼, 点一下这个upload,又回到配置类BlockCanaryContext这儿来,

或者可以参考一下 这篇博客!!!!!!!

可以在后台开启一个线程,定时扫描并上传。

或者

可以利用一下刚刚提到的 框架的文件系统操作封装 ,

再结合 自定义网络请求逻辑,

把文件上传到服务器也是ok的!

7. 设计模式、技巧: 7.1 单例模式,不用多说, 刚刚提到BlockCanaryBlockCanaryInternals里边都用到了; 7.2 回调机制设计: 内部接口,供给回调:

定义内部接口的类,“抽象调用”回调接口方法:

接口暴露给外部,在外部实现回调:

8 .框架中各个主要类的功能划分

BlockCanary 提供给外部使用的,负责框架整体的方法调度;整体的、最顶层的调度;

BlockCanaryInternals

 封装控制 周期性采集堆栈信息打印、输入的关键逻辑;

(卡顿判定阈值采集信息周期 的配置,都在这里首先被使用)

(注意这里的onBlockEvent() 回调方法

 封装文件操作模块(创建文件、创建文件目录、获取相关路径等等 这些

从SD卡根目录到存储.log文件目录 这个级别的处理,往下的目录下文件单位级别的处理,交给LogWriter)等核心逻辑;

 调用了LogWriter.save()进行log文件存储等;

 创建CpuSamplerStackSample实例,用于协助完成周期性采集

LogWriter 封装了文件流写入、处理等逻辑;

LooperMonitor协助完成周期性采集

【主要是阻塞任务始末的各种调度,即面向卡顿阈值

当然,调度的内容也包括对周期性采集启闭调度!!!!】;

如上,

&println()有点像闹钟的角色,

它在主线程的任务分发dispatchMessage前后分别被调用一次

它在采集周期开始的时候,就记录下开始时间

在阻塞任务完成之后,会再次被调用,记录下结束时间

&isBlock():借助println()中记录的关于主线程任务分发开始时间结束时间

来判断阻塞的时间是不是大于我们设定的或者默认卡顿判定时间

如果是,调用notifyBlockEvent(),间接调用到回调方法 onBlockEvent()

这个方法上面说了,在BlockCanaryInternals 的构造器中被具体实现了,

可以调用LogWriter 最终输出.log文件;

&startDump()stopDump()

我们可以看到在println()中还有startDump()stopDump()这两个方法,

分别也是在主线程任务分发开始结束时,随着println()被调用而被调用;

startDump()stopDump()的内容正是控制两个Sample类的启闭

CpuSamplerStackSample

同样负责协助完成周期性采集

CpuSampler的逻辑主要是面向CPU信息的处理,而

StackSample的逻辑主要是对堆栈信息的收集;

他们都继承自AbstractSample

首先在上面的源码我们可以看到,

BlockCanaryInternals的构造器中,

就分别创建了一个CpuSampler(参数正为采集堆栈信息周期属性)和一个StackSample实例(参数为采集堆栈信息周期属性):

!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

这个参数一路上走,经过CpuSampler的构造器,

最终是在CpuSampler的父类AbstractSampler中被使用!!!!!

!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

我们可以看到在AbstractSampler中,

AbstractSampler构造器接收了采集堆栈信息周期属性,

同时准备了一个Runnable任务单元,

任务run()中做了两件事,

第一件事是调用抽象方法doSample()

第二件事是基于这个采集堆栈周期属性这个Runnable单元

创建一个循环定时任务!!!!!!!!!!!!!!!!!!!!!!!!!

即,

这个Runnable单元start()之后,

将会每隔一个采集周期,就执行一次run()和其中的doSample()

进行堆栈信息CPU信息的周期性采集工作;

!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

这便是BlockCanary能够周期采集堆栈信息的根源!!!!!!!!

!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

那接下来我们可以展开三个点,

解决这个三个疑问点,脉络就理得差不多了:

【1. 由哪个Handler来处理这个Runnable

我们知道,

Android中的多线程任务单元可以由一个Handler去post或者postDelayed一个Runnable来开启;

而这里处理这个RunnableHandler正是

HandlerThreadFactory.getTimerThreadHandler()

HandlerThreadFactory是框架的提供的一个内部线程类,

源码解析如下,使用了工厂模式吼:

如此便可以获得,绑定了工作线程(子线程)的LooperHandler

有了这个Handler就可以处理刚刚说的Runnable任务单元了;

【2. HandlerRunnable任务单元启闭是在哪个地方?】

当然是在AbstractSampler提供的start()stop()里边了;

CpuSamplerStackSample都会继承自AbstractSampler,自然也就继承了这start()stop()

最后上面讲过了,

LooperMonitorprintln()中,

startDump()stopDump()会被调用,

而在startDump()stopDump()中,

CpuSamplerStackSample实例的start()stop()也会被调用了,

!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

从而控制了周期采集信息工作线程(子线程)任务单元【上述的Runnable实例】的启闭

!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

【3. doSample()的实现】

CpuSamplerStackSample都继承自AbstractSample

使用的都是从AbstractSample继承过来的Runnable实例

前面说过这个Runnable单元start()之后,

将会每隔一个采集周期,就执行一次run()和其中的doSample()

进行堆栈信息CPU信息的周期性采集工作;

是这样的,

然后CpuSamplerStackSample通过对父类抽象方法doSample()做了不同的实现,

使得各自循环处理的任务内容不同罢了;

CpuSampler的面向CPU信息的处理,

StackSample则对堆栈信息的收集;】

BlockCanaryContext 框架配置类的超类,提供给外部进行集成和配置信息: