监控原理

系统中Looper的关键代码如下。我们知道,在一个应用进程中,只有一个MainLooper,而在loop()方法中可以看出,Printer logging = me.mLogging,它在每个message处理前后都会被调用。如果主线程卡主了,说明msg.target.dispatchMessage(msg)内部执行太耗时了。根据这个原理,可以做一个UI卡顿监控的方案。
我们可以获取以下信息:

  • 基础信息:系统版本、机型、进程名、应用版本号、磁盘空间、用户信息。
  • 好使信息:卡顿开始时间和结束时间。
  • CPU信息:CPU的信息、本进程CPU使用率等。
  • 堆栈信息:发生卡顿的时间段内的堆栈数据。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
for (;;) {
Message msg = queue.next(); // might block
// 省略部分代码...
// This must be in a local variable, in case a UI event sets the logger
final Printer logging = me.mLogging;
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
}
// 省略部分代码...
try {
msg.target.dispatchMessage(msg);
end = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
} finally {
if (traceTag != 0) {
Trace.traceEnd(traceTag);
}
}
// 省略部分代码...
if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}
}

代码实现

所以,我们只要实现自己的Printer,并且调用Looper.getMainLooper().setMessagePrinter(mPrinter)就可以实现自己的监控逻辑了。

卡顿监控

LogPrinter关键代码如下:

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
public class LogPrinter implements Printer, UiPerfMonitorConfig {
private final String TAG = "LogPrinter";
private LogPrinterListener mLogPrinter = null;
private long startTime;

public LogPrinter(LogPrinterListener logPrinter) {
mLogPrinter = logPrinter;
}

@Override
public void println(String s) {
//表示式dispatchMessage之前调用
if (startTime <= 0) {
startTime = System.currentTimeMillis();
mLogPrinter.onStartLog();
} else {
long diffTime = System.currentTimeMillis() - startTime;
excuTime(s, diffTime);
startTime = 0;
}
}

private void excuTime(String logInfo, long diffTime) {
int level = 0;
if (diffTime > TIME_WARNING_LEVEL_2) {
level = UI_PERF_LEVEL_2;
} else if (diffTime > TIME_WARNING_LEVEL_1) {
level = UI_PERF_LEVEL_1;
}
mLogPrinter.onEndLog(logInfo, level);
}
}

UiPerfMonitorConfig定义了相关常量:

1
2
3
4
5
6
7
8
9
10
public interface UiPerfMonitorConfig{
public final int UI_PERF_LEVEL_1 = 1;
public final int UI_PERF_LEVEL_2 = 2;

public final long TIME_WARNING_LEVEL_1 = 100;
public final long TIME_WARNING_LEVEL_2 = 300;

public final String LOG_PATH = Enviroment.getExternalStorageDirectory().getPath()
+ "androidtech/uiperf";
}

开启/停止监控

1
2
3
4
//开启
Looper.getMainLooper().setMessagePrinter(mPrinter);
//停止
Looper.getMainLooper().setMessagePrinter(null);

获取相关数据

我们需要获取发生卡顿之前的数据,和发生卡顿之后进行对比。这就需要一个特定的线程来定时的抓取需要的信息,推荐使用HandlerThread来是实现。如果需要实现采集相关数据可以在这个框架上面进行相应的扩展即可。

HandlerThread实际上就是一个Thread,只不过有自己的Handler,可以在子线程处理消息,并不会造成UI线程的堵塞。

下面是一个定时采样的基类(BaseSampler):

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 abstract class BaseSampler {
private final String TAG = "BaseSampler";
private Handler mControlHandler = null;
private int intervalTime = 500; //采样间隔
private AtomicBoolean mIsSampling = new AtomicBoolean(false);

private Runnable mRunnable = new Runnable() {
@Override
public void run() {
doSample();
if (mIsSampling.get()) {
getControlHandler().postDelayed(mRunnable, intervalTime);
}
}
};

public BaseSampler() {
GLog.d(TAG, "Init BaseSampler!");
}

/**
* 开始采样
*/
public void start() {
if (!mIsSampling.get()) {
GLog.d(TAG, "start sampler!");
getControlHandler().removeCallbacks(mRunnable);
getControlHandler().post(mRunnable);
mIsSampling.set(true);
}
}

/**
* 开始采样
*/
public void stop() {
if (mIsSampling.get()) {
GLog.d(TAG, "stop sampler!");
getControlHandler().removeCallbacks(mRunnable);
mIsSampling.set(false);
}
}

private Handler getControlHandler() {
if (mControlHandler == null) {
HandlerThread mHT = new HandlerThread("SamplerThread");
mHT.start();
mControlHandler = new Handler(mHT.getLooper());
}
return mControlHandler;
}

/**
* 采样抽象方法
*/
abstract void doSample();
}

存储与上报

在拿到数据后,需要上报到后台,在后台通过大量数据样本来定位发生卡顿最多的地方,但是如果每次发生卡顿就上报(实时上报),这无疑增加了网络开销。所以科研先把日志信息先缓存到本地,在合适的时间,压缩后上传。
下面LogWriteThread是写到本地的代码:

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
public class LogWriteThread implements UiPerfMonitorConfig {
private String TAG = "LogWriteThread";
private Handler mWriteHandler = null;
private final Object FILE_LOCK = new Object();
private SimpleDateFormat FILE_NAME_FORMATTER = new SimpleDateFormat("YYYY-MM-dd");
private SimpleDateFormat TIME_FORMATTER = new SimpleDateFormat("YYYY-MM-dd HH:mm:ss");

public void saveLog(final String logInfo) {
getWriteHandler().post(new Runnable() {
@Override
public void run() {
synchronized (FILE_LOCK) {
saveLog2Local(logInfo);
}
}
});
}

private void saveLog2Local(String logInfo) {
long time = System.currentTimeMillis();
File logFile = new File(LOG_PATH + "/" + FILE_NAME_FORMATTER.format(time) + ".txt");
StringBuilder sb = new StringBuilder("/**************************************/\r\n");
sb.append(TIME_FORMATTER.format(time))
.append("\r\n/**************************************/\r\n")
.append(logInfo)
.append("\r\n");

if (logFile.exists()) {
writeLog4SampleFile(logFile.getPath(), sb.toString());
} else {
BufferedWriter writer = null;
try {
OutputStreamWriter out = new OutputStreamWriter(
new FileOutputStream(logFile.getPath(), true), "UTF-8");
writer = new BufferedWriter(out);
writer.write(sb.toString());
writer.flush();
writer.close();
writer = null;
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (writer != null) {
writer.close();
writer = null;
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

public void send2Server() {
getWriteHandler().post(new Runnable() {
@Override
public void run() {
// TODO 上传到服务器
}
});
}

/***
* 向文件中追加内容
* @param path
* @param info
*/
private void writeLog4SampleFile(String path, String info) {
RandomAccessFile randomFile = null;
try {
// 打开一个随机访问的文件流,按读写的方式
randomFile = new RandomAccessFile(path, "rw");
// 获取文件长度,字节数
long length = randomFile.length();
// 将文件指针移动到文件尾
randomFile.seek(length);
randomFile.writeBytes(info);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (randomFile != null) {
randomFile.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}

private Handler getWriteHandler() {
if (mWriteHandler == null) {
HandlerThread mHT = new HandlerThread("LogWriteThread");
mHT.start();
mWriteHandler = new Handler(mHT.getLooper());
}
return mWriteHandler;
}
}

上传到服务器的部分根据因每个后台差异没有实现,可根据自己的需求实现。

监控管理类

到目前为止,我们实现了卡顿监控各个子步骤,单需要一个管理类来管理这些功能。

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
public class UiMonitorManager implements UiPerfMonitorConfig, LogPrinterListener {
private final String TAG = "UiMonitorManager";
private static UiMonitorManager mInstance = null;
private LogPrinter mLogPrinter;
private LogWriteThread mLogWriteThread;
private int monitorState = UI_PERF_MONITOR_STOP;
private CPUInfoSampler mCpuInfoSampler;

public synchronized static UiMonitorManager getInstance() {
if (mInstance == null) {
mInstance = new UiMonitorManager();
}
return mInstance;
}

public UiMonitorManager() {
mLogPrinter = new LogPrinter(this);
mCpuInfoSampler = new CPUInfoSampler();
mLogWriteThread = new LogWriteThread();
initLogPath();
}

/**
* 初始化存储路径
*/
private void initLogPath() {
File path = new File(LOG_PATH);
if (!path.exists()) {
boolean mkdir = path.mkdir();
GLog.d(TAG, "mkdir: " + mkdir + ":" + LOG_PATH);
}
}

private void startMonitor() {
Looper.getMainLooper().setMessageLogging(mLogPrinter);
monitorState = UI_PERF_MONITOR_START;
}

private void stopMonitor() {
Looper.getMainLooper().setMessageLogging(null);
mCpuInfoSampler.stop();
monitorState = UI_PERF_MONITOR_STOP;
}

public boolean isMonitoring() {
return monitorState == UI_PERF_MONITOR_START;
}

@Override
public void onStartLog() {
mCpuInfoSampler.start();
}

@Override
public void onEndLog(String logInfo, int level) {
switch (level) {
case UI_PERF_LEVEL_1:
// mCpuInfoSampler 获取到相应的数据进行合并 先存储到本地
mLogWriteThread.saveLog(logInfo);
break;
case UI_PERF_LEVEL_2:

break;
}
}
}

该监控器的使用流程如下:

  • 初始化单例,在其构造方法中初始化了LogPrinter、CPU采样器(还可以有其他采样器)和日志管理器LogWriteThread。
  • 通过startMonitor方法开始监听卡顿情况,监听过程通过onEndLoop回调,其中Level的级别可以自定义。
  • 上传到服务器。