0%

手写LeakCanary&&BlockCanary&&AnrWatchDog


最近在做性能优化的东西,研究了一下相关的内存监测,卡顿监测,以及ANR监测开源框架,对里面的核心原理做了总结,并手写一份简易版以便加深印象,为后续搭建线上日志监控做铺垫,在此做一个记录,线上监控框架可根据业务在此基础上做扩展。收集必要的日志信息,排查问题及时修复BUG,提升性能和稳定性,也是每个Android工程师必不可少的技能。


BlockCanary

  1. 通过采样工具类,在子线程中获取主线程的堆栈信息并保存到Map,暴露一个接口返回采样结果
  2. 通过日志监控类,实现Printer接口,判断是否卡顿(采样开始到采样结束的时间间隔是否超过阈值),如果卡顿调用接口获取返回的采样结果在子线程中日志打印
  3. App出现卡顿,会阻塞主线程的dispatchMessage,主线程Looper的loop方法中有一个Printer在每个Message处理前后被调用,所以设置主线程的MessageLogging为自定义的Printer

    采样工具类

    开启一个采样子线程,设置原子变量记录本次是否采样保证多线程同步,避免重复开始和结束,开始采样post一个采样任务到子线程,存入当前时间戳和对应的主线程堆栈信息到Map中,如果本次仍然需采样继续延迟间隔时间执行采样任务,暴露一个获取主线程堆栈信息的接口方法,返回当前堆栈信息列表
    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
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    public class StackSampler {
    public static final String SEPARATOR = "\r\n";
    public static final SimpleDateFormat TIME_FORMATTER =
    new SimpleDateFormat("MM-dd HH:mm:ss.SSS");
    private Handler mHandler;
    private Map<Long, String> mStackMap = new LinkedHashMap<>(); //保存的主线程堆栈信息
    private int mMaxCount = 100; // 最多保存100条
    private long mSampleInterval; // 采样时间间隔
    // 本次是否采样
    protected AtomicBoolean mShouldSample = new AtomicBoolean(false);

    public StackSampler(long sampleInterval) {
    mSampleInterval = sampleInterval;
    // 开启采样子线程
    HandlerThread handlerThread = new HandlerThread("block-canary-sampler");
    handlerThread.start();
    mHandler = new Handler(handlerThread.getLooper());
    }

    /**
    * 开始采样 执行堆栈
    */
    public void startDump() {
    // 避免重复开始
    if (mShouldSample.get()) {
    return;
    }
    // 设置采样标记
    mShouldSample.set(true);
    // 移除上一个采样任务,在采样间隔时间后执行采样
    mHandler.removeCallbacks(mRunnable);
    mHandler.postDelayed(mRunnable, mSampleInterval);
    }

    public void stopDump() {
    // 避免重复结束
    if (!mShouldSample.get()) {
    return;
    }
    // 设置采样标记
    mShouldSample.set(false);

    mHandler.removeCallbacks(mRunnable);
    }


    public List<String> getStacks(long startTime, long endTime) {
    ArrayList<String> result = new ArrayList<>();
    synchronized (mStackMap) {
    for (Long entryTime : mStackMap.keySet()) {
    // 记录时间大于开始时间小于结束时间就放入返回列表中
    if (startTime < entryTime && entryTime < endTime) {
    result.add(TIME_FORMATTER.format(entryTime)
    + SEPARATOR
    + SEPARATOR
    + mStackMap.get(entryTime));
    }
    }
    }
    return result;
    }

    private Runnable mRunnable = new Runnable() {
    @Override
    public void run() {
    StringBuilder sb = new StringBuilder();
    // 获得主线程堆栈信息并拼接到字符串
    StackTraceElement[] stackTrace = Looper.getMainLooper().getThread().getStackTrace();
    for (StackTraceElement s : stackTrace) {
    sb.append(s.toString()).append("\n");
    }
    synchronized (mStackMap) {
    //最多保存100条堆栈信息,到了数量上限移除
    if (mStackMap.size() == mMaxCount) {
    mStackMap.remove(mStackMap.keySet().iterator().next());
    }
    // 存入当前时间戳和对应的堆栈信息
    mStackMap.put(System.currentTimeMillis(), sb.toString());
    }
    // 如果本次要采样,设置延迟继续执行此任务
    if (mShouldSample.get()) {
    mHandler.postDelayed(mRunnable, mSampleInterval);
    }
    }
    };

    }

    卡顿监控工具类

    初始化采样工具类,开启打印日志子线程,记录采样的开始时间和结束时间,如果时间大于设定的卡顿阈值则判定为卡顿状态,调用采样工具类获取主线程堆栈信息
    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
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    // 卡顿监控工具类
    public class LogMonitor implements Printer {

    private StackSampler mStackSampler; // 采样工具类
    private boolean mPrintingStarted = false; // 开始打印标记
    private long mStartTimestamp; // 开始时间戳
    private long mBlockThresholdMillis = 3000; // 卡顿阈值
    private long mSampleInterval = 1000; // 采样频率

    private Handler mLogHandler;

    public LogMonitor() {
    // 初始化采样工具类
    mStackSampler = new StackSampler(mSampleInterval);
    // 开启打印子线程
    HandlerThread handlerThread = new HandlerThread("block-canary-io");
    handlerThread.start();
    mLogHandler = new Handler(handlerThread.getLooper());
    }

    @Override
    public void println(String x) {
    //从if到else会执行消息分发,如果执行耗时超过阈值,输出卡顿信息
    if (!mPrintingStarted) {
    //记录开始时间
    mStartTimestamp = System.currentTimeMillis();
    mPrintingStarted = true;
    mStackSampler.startDump();
    } else {
    final long endTime = System.currentTimeMillis();
    mPrintingStarted = false;
    //出现卡顿,通知卡顿事件
    if (isBlock(endTime)) {
    notifyBlockEvent(endTime);
    }
    mStackSampler.stopDump();
    }
    }

    private void notifyBlockEvent(final long endTime) {
    // 获得卡顿时主线程堆栈信息在子线程打印
    mLogHandler.post(new Runnable() {
    @Override
    public void run() {
    List<String> stacks = mStackSampler.getStacks(mStartTimestamp, endTime);
    for (String stack : stacks) {
    Log.e("block-canary", stack);
    }
    }
    });
    }


    private boolean isBlock(long endTime) {
    return endTime - mStartTimestamp > mBlockThresholdMillis;
    }
    }

    卡顿监控入口

    设置主线程的MessageLogging为自定义的Printer
    1
    2
    3
    4
    5
    6
    public class BlockCanary {
    public static void install() {
    LogMonitor logMonitor = new LogMonitor();
    Looper.getMainLooper().setMessageLogging(logMonitor);
    }
    }

LeakCanary

Java中WeakReference和ReferenceQueue联合使用是监控某个对象是否被gc回收的手段,LeakCanary正是利用这个原理实现的。

WeakReference和ReferenceQueue联合使用

创建一个对象,包装到弱引用对象中并关联引用队列,把对象置空,强制GC,取出引用队列的弱引用对象是否与关联时的弱引用对象相同

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
public static void main(String[] args) {
// 创建一个引用队列
ReferenceQueue referenceQueue = new ReferenceQueue();
// 创建一个对象
Object obj = new Object();

//把obj放入弱引用对象,并和一个引用队列关联
//当obj被gc回收后,weakReference会被添加到与之关联的referenceQueue
WeakReference weakReference = new WeakReference(obj,referenceQueue);

//把obj置空,让它没有强引用
obj = null;
Runtime.getRuntime().gc(); //强制gc

try{
Thread.sleep(1000);
}catch (Exception e){}

Reference findRef = null;
do{
findRef = referenceQueue.poll();
//如果能找到上面的weakReference对象,说明obj被gc回收了
System.out.println("findRef = " +findRef + "是否等于上面的weakReference = " + (findRef == weakReference));
}while(findRef !=null);// 把所有referenceQueue的weakReference对象找出来
}

封装包含key和name的弱引用类

继承WeakReference弱引用类,方便根据key删除对象

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
40
41
42
public class KeyWeakReference<T> extends WeakReference<T> {

private String key;
private String name;

public KeyWeakReference(T referent, String key, String name) {
super(referent);
this.key = key;
this.name = name;
}

public KeyWeakReference(T referent, ReferenceQueue<? super T> q, String key, String name) {
super(referent, q);
this.key = key;
this.name = name;
}

public String getKey() {
return key;
}

public void setKey(String key) {
this.key = key;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

@Override
public String toString() {
final StringBuffer sb = new StringBuffer("KeyWeakReference{");
sb.append("key='").append(key).append('\'');
sb.append(", name='").append(name).append('\'');
sb.append('}');
return sb.toString();
}
}

监控工具类

创建一个观察对象Map,怀疑对象Map和引用队列,先清理一遍被GC的对象,遍历引用队列的所有弱引用对象,清理观察对象Map和怀疑对象Map中对应的对象,被监控的对象生成UUID作为key,把对象放入弱引用并与引用队列关联,放入到观察对象Map中,5秒后判断该对象是否还存在观察对象Map中,还存在则说明没有被GC,将该对象移动到怀疑对象Map

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
public class Watcher {
// 观察对象Map
private HashMap<String, KeyWeakReference> watchedReferences = new HashMap<>();
// 怀疑对象Map
private HashMap<String, KeyWeakReference> retainedReferences = new HashMap<>();

//当被监视的对象被gc回收后,对象的弱引用就会被加入到引用队列
private ReferenceQueue queue = new ReferenceQueue();

public Watcher() {
}

/**
* 取出引用队列的所有弱引用对象,清理观察对象Map和怀疑对象Map中对应的对象
*/
private void removeWeaklyReachableReferences() {
KeyWeakReference findRef = null;
do {
// 取出引用队列的弱引用对象
findRef = (KeyWeakReference) queue.poll();
// 不为空说明对象被gc回收了,把对应的弱引用对象从观察对象Map,怀疑对象Map移除
if (findRef != null) {
// 根据key把它从观察对象Map移除
Reference removedRef = watchedReferences.remove(findRef.getKey());
// 如果removedRef为空,有可能被放入到怀疑对象Map了
// 尝试从怀疑对象Map中移除
if (removedRef == null) {
retainedReferences.remove(findRef.getKey());
}
}
} while (findRef != null);// 把referenceQueue的所有弱引用取出来
}

/**
* 根据key把对应的弱引用对象从观察对象Map移动到怀疑对象Map
*
* @param key
*/
private synchronized void moveToRetained(String key) {
System.out.println("加入到怀疑列表...");
// 加入怀疑对象Map前,做一次清理
removeWeaklyReachableReferences();
// 根据key从观察对象Map中去找弱引用对象
KeyWeakReference retainedRef = watchedReferences.remove(key);
// 发现还没有被删除,说明没有被回收
if (retainedRef != null) {
//从观察对象Map中移除,加入到怀疑对象Map
retainedReferences.put(key, retainedRef);
}
}


public void watch(Object watchedReference, String referenceName) {
//1. 先清理下观察对象Map和怀疑对象Map
removeWeaklyReachableReferences();
//2. 被监视的对象生成唯一的uuid作为key
final String key = UUID.randomUUID().toString();
//3. 被监视的对象放入weakReference,并和一个引用队列关联
KeyWeakReference reference = new KeyWeakReference(watchedReference, queue, key, "");
//4. 加入到观察对象Map
watchedReferences.put(key, reference);

//5. 延迟5秒后检查是否还在观察对象Map,如果还在,则加入到怀疑对象Map
Executor executor = Executors.newSingleThreadExecutor();
executor.execute(() -> {
Utils.sleep(5000);
moveToRetained(key);
});

}

// 获取泄漏对象Map
public HashMap<String, KeyWeakReference> getRetainedReferences() {
retainedReferences.forEach((key, keyWeakReference) -> {
System.out.println("key: " + key + " , obj: " + keyWeakReference.get() + " , keyWeakReference: " + keyWeakReference);
}
);
return retainedReferences;
}
}

监控泄漏对象

创建对象,调用监控工具类的监控方法,查看怀疑对象Map

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static void main(String[] args) {
// 初始化监控工具类
Watcher watcher = new Watcher();
// 创建对象并开始对象监控
Object obj = new Object();
watcher.watch(obj,"");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 释放对象
obj = null;
// 强制GC
Runtime.getRuntime().gc();
sleep(100);
System.runFinalization();

System.out.println("查看是否在怀疑对象Map:" + watcher.getRetainedReferences().size());
}

ANRWatchDog

FileObserver

监控Android系统的anr日志目录/data/anr/,利用FileObserver监控目录下的文件操作,间接监控ANR问题

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
public class ANRFileObserver extends FileObserver {

public ANRFileObserver(String path) {
super(path);
}

public ANRFileObserver(String path, int mask) {
super(path, mask);
}

@Override
public void onEvent(int event, @Nullable String path) {
switch (event)
{
case FileObserver.ACCESS://文件被访问
Log.i("Zero", "ACCESS: " + path);
break;
case FileObserver.ATTRIB://文件属性被修改,如 chmod、chown、touch 等
Log.i("Zero", "ATTRIB: " + path);
break;
case FileObserver.CLOSE_NOWRITE://不可写文件被 close
Log.i("Zero", "CLOSE_NOWRITE: " + path);
break;
case FileObserver.CLOSE_WRITE://可写文件被 close
Log.i("Zero", "CLOSE_WRITE: " + path);
break;
case FileObserver.CREATE://创建新文件
Log.i("Zero", "CREATE: " + path);
break;
case FileObserver.DELETE:// 文件被删除,如 rm
Log.i("Zero", "DELETE: " + path);
break;
case FileObserver.DELETE_SELF:// 自删除,即一个可执行文件在执行时删除自己
Log.i("Zero", "DELETE_SELF: " + path);
break;
case FileObserver.MODIFY://文件被修改
Log.i("Zero", "MODIFY: " + path);
break;
case FileObserver.MOVE_SELF://自移动,即一个可执行文件在执行时移动自己
Log.i("Zero", "MOVE_SELF: " + path);
break;
case FileObserver.MOVED_FROM://文件被移走,如 mv
Log.i("Zero", "MOVED_FROM: " + path);
break;
case FileObserver.MOVED_TO://文件被移来,如 mv、cp
Log.i("Zero", "MOVED_TO: " + path);
break;
case FileObserver.OPEN://文件被 open
Log.i("Zero", "OPEN: " + path);
break;
default:
//CLOSE : 文件被关闭,等同于(IN_CLOSE_WRITE | IN_CLOSE_NOWRITE)
//ALL_EVENTS : 包括上面的所有事件
Log.i("Zero", "DEFAULT(" + event + "): " + path);
break;
}
}
}

线程

开启ANR监控后台线程,检查是否ANR,通过检查标志位和时间差判断规定时间内是否执行完成,如果没执行完成则可能发生ANR卡住了,那么打印主线程堆栈信息并调用回调接口

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
public class ANRWatchDog extends Thread {

private static final String TAG = "ANR";
private int timeout = 5000; // 超时阈值
private boolean ignoreDebugger = true; // 开启开关

static ANRWatchDog sWatchdog;

private Handler mainHandler = new Handler(Looper.getMainLooper());

private class ANRChecker implements Runnable {

private boolean mCompleted; // 是否完成
private long mStartTime; // 开始时间
private long executeTime = SystemClock.uptimeMillis(); // 执行时间

@Override
public void run() {
synchronized (ANRWatchDog.this) {
mCompleted = true;
executeTime = SystemClock.uptimeMillis();
}
}

void schedule() {
// 设置是否完成标记
mCompleted = false;
// 记录开始时间
mStartTime = SystemClock.uptimeMillis();
// 结束任务,在主线程中重置完成标记,记录结束时间
mainHandler.postAtFrontOfQueue(this);
}

boolean isBlocked() {
return !mCompleted || executeTime - mStartTime >= timeout;
}
}

// anr监听接口
public interface ANRListener {
void onAnrHappened(String stackTraceInfo);
}

private ANRChecker anrChecker = new ANRChecker();

private ANRListener anrListener;


public void addANRListener(ANRListener listener){
this.anrListener = listener;
}

public static ANRWatchDog getInstance(){
if(sWatchdog == null){
sWatchdog = new ANRWatchDog();
}
return sWatchdog;
}

private ANRWatchDog(){
super("ANR-WatchDog-Thread");
}

@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
@Override
public void run() {
// 设置为后台线程
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
while(true){
// 没有被打断停止
while (!isInterrupted()) {
synchronized (this) {
// 检查是否anr
anrChecker.schedule();
long waitTime = timeout;
long start = SystemClock.uptimeMillis();
// 防止假唤醒
while (waitTime > 0) {
try {
// 等待超时时长后检查标志位
wait(waitTime);
} catch (InterruptedException e) {
Log.w(TAG, e.toString());
}
// 发现没有到阈值继续休眠
waitTime = timeout - (SystemClock.uptimeMillis() - start);
}
// 如果没有阻塞跳过此次循环
if (!anrChecker.isBlocked()) {
continue;
}
}
// 如果开关关闭并且在调试跳过此次循环
if (!ignoreDebugger && Debug.isDebuggerConnected()) {
continue;
}
// 打印堆栈信息并回调
String stackTraceInfo = getStackTraceInfo();
if (anrListener != null) {
anrListener.onAnrHappened(stackTraceInfo);
}
}
anrListener = null;
}
}

// 获取主线程堆栈信息返回字符串
private String getStackTraceInfo() {
StringBuilder stringBuilder = new StringBuilder();
for (StackTraceElement stackTraceElement : Looper.getMainLooper().getThread().getStackTrace()) {
stringBuilder
.append(stackTraceElement.toString())
.append("\r\n");
}
return stringBuilder.toString();
}
}

ANRWatchDog.getInstance().addANRListener(new ANRWatchDog.ANRListener() {
@Override
public void onAnrHappened(String stackTraceInfo) {
Log.i(TAG, "发生了ANR: "+ stackTraceInfo);
}
});
ANRWatchDog.getInstance().start();