在复杂的项目环境中,由于历史代码庞大,业务复杂,包含各种第三方库,所以在出现了卡顿的时候,我们很难定位到底是哪里出现了问题,即便知道是哪一个Activity/Fragment,也仍然需要进去里面一行一行看,动辄数千行的类再加上跳来跳去调来调去的,结果就是不了了之随它去了,实在不行了再优化吧。于是一拖再拖,最后可能压根就改不动了,客户端越来越卡。
Android应用卡顿是非常普遍的现象,偶尔出现ANR。只有当APP出现ANR,我们才能得到当前堆栈信息。当应用只是卡顿或只是不太流畅的时候,我们能不能找出卡顿元凶呢?不依赖Debug和源码的情况,能不能找出卡顿的堆栈信息呢?我们需要找到一种方法来检测哪些函数可能会使应用发生ANR,在开发阶段就能找出卡顿元凶,提高应用流畅度。
BlockCanary就是来解决这个问题的。告别打点,告别Debug,哪里卡顿,一目了然,让优化代码变得有的放矢。BlockCanary 地址:https://github.com/moduth/blockcanary
在Android中所有KeyEvent和TouchEvent都是按照先后顺序放入队列中,依次执行,并且只有当前一个事件执行完毕,才能开始执行下一个事件。每个正在执行的事件都被被保存在waitQueue中,执行完毕之后从waitQueue中移除。
当用户触发一个事件的时候,首先判断waitQueue是否为空,如果为空,可以立即响应该事件。如果队列不为空,说明还有事件没有执行完毕。判断当前时间和上一个事件响应时间是否大于超时时间(一般5秒,broadCastReceiver 10秒),如果超时,则会通过ActivityManagerService以弹窗的形式通知用户。
从 ANR原理 可以知道,当一个事件处理时间超过阈值就会触发ANR。Android中所有事件(包括Activity,Service生命周期管理)都是通过Looper+MessageQueue+Handler来处理的。所有事件都被封装成Message,然后被放入MessageQueue中,Looper负责将Message交给对应的Handler去处理。
Looper是一个死循环,通过dispatchMessage分发Message到对应Handler中处理。我们可以近似认为dispatchMessage花费的时间就是每条消息处理时间。因此,我们只要记录每个Mesage dispatchMessage的执行时间,就可以得到事件花费的时间。
从Looper源码中我们发现,执行dispatchMessage前后都有一个logging打印,并且Looper提供了注册logging的方法。所有我们可以在MainThread Looper中注册一个logging,在每条消息dispatchMessage前后,都能收到一条打印记录。通过记录dispatchMessage之前的时间t1和dispatchMessage执行之后的log时间t2,totalTime = t2-t1得到该事件执行时间。
Looper.java
BlockCanary启动一个线程负责保存UI线程当前堆栈信息,将堆栈信息以及CPU信息保存分别保存在 mThreadStackEntries和mCpuInfoEntries中,每条信息都以时间撮为key保存。
BlockCanary注册了logging来获取事件开始结束时间。如果检测到事件处理时间超过阈值(默认值1s),则从mThreadStackEntries中查找T1~T2这段时间内的堆栈信息,并且从mCpuInfoEntries中查找T1~T2这段时间内的CPU及内存信息。并且将信息格式化后保存到本地文件,并且通知用户。
BlockCanary sdk在jcenter 中,目前最新的版本为1.2.0。
(1) 修改gradle
allprojects {
repositories {
mavenCentral()
jcenter()
}
}
dependencies {
// most often used way, enable notification to notify block event
compile 'com.github.moduth:blockcanary-ui:1.2.0'
// this way you only write block logs, without notification
// compile 'com.github.moduth:blockcanary-android:1.2.0'
// this way you only enable BlockCanary in debug package
// debugCompile 'com.github.moduth:blockcanary-ui:1.2.0'
// releaseCompile 'com.github.moduth:blockcanary-no-op:1.2.0'
}
(2) 注册BlockCanary
在自定义application中注册。
public class DemoApplication extends Application {
@Override
public void onCreate() {
...
// Do it on main process
BlockCanary.install(this, new AppBlockCanaryContext()).start();
}
}
通过以上两个步骤,我们就能使用BlockCanary了。
(3) 修改事件超时阈值
事件处理时间阈值默认为1000ms,我们可以通过修改BlockCanaryContext.java 的getConfigBlockThreshold方法修改事件处理超时阈值。
/**
* Config block threshold (in millis), dispatch over this duration is regarded as a BLOCK. You may set it
* from performance of device.
*
* @return threshold in mills
*/
public int getConfigBlockThreshold() {
return 1000;
}
cpuCore:手机cpu个数。
processName:应用包名。
freeMemory: 手机剩余内存,单位KB。
totalMemory: 手机内训总和,单位KB。
timecost: 该Message(事件)执行时间,单位 ms。
threadtimecost: 该Message(事件)执行线程时间(线程实际运行时间,不包含别的线程占用cpu时间),单位 ms。
cpubusy: true表示cpu负载过重,false表示cpu负载不重。cpu负载过重导致该Message(事件) 超时,错误不在本事件处理上。
cpuBusy判断:
CpuSampler.java
public CpuSampler(long sampleIntervalMillis) {
super(sampleIntervalMillis);
BUSY_TIME = (int) (mSampleIntervalMillis * 1.2f);
}
public boolean isCpuBusy(long start, long end) {
if (end - start > mSampleIntervalMillis) {
long s = start - mSampleIntervalMillis;
long e = start + mSampleIntervalMillis;
long last = 0;
synchronized (mCpuInfoEntries) {
for (Map.Entry entry :mCpuInfoEntries.entrySet()) {
long time = entry.getKey();
if (s < time && time < e) {
if (last != 0 && time - last > BUSY_TIME) {
return true;
}
last = time;
}
}
}
}
return false;
}
BUSY_TIME = (int) (mSampleIntervalMillis * 1.2f);
CpuSampler在子线程中每隔mSampleIntervalMillis读取一次cpu信息,如果在BUSY_TIME 没有读取下一条cpu信息,表示cpu忙,来不及处理本pid的任务,导致应用出现超时。