前言
对 CpuSampler 的几个关键方法做分析,采样原理的篇幅还不小,单独开这一篇
doSample()-CPU采样
CPU 采样时,主要获取 CPU 总的使用率,进程对 CPU 的使用率
获取这两个数据来判断,卡顿是由于进程自身使用 CPU 过高导致,还是其他程序使用 CPU 过多导致的
cpu 1234567 2345678 3456789 4567890 5678901 6789012 7890123 8901234
其中,每个数字表示从系统启动开始到当前时刻,CPU 执行各种操作的时间(以 clock tick 计算)。clock tick 在大多数机器上是 10ms。例如,第一个数字表示从系统启动开始到当前时刻,CPU 执行用户程序的时间。
在 Android 中,获取进程信息的方法有很多。例如,可以通过以下方式获取当前进程的用户 ID、线程 ID 和进程 ID
android.os.Process.myUid() # 获取当前进程的用户ID
Thread.currentThread().getId() # 获取当前线程ID
android.os.Process.myTid() # 获取当前线程ID
android.os.Process.myPid() # 获取此进程的标识符
此外,还可以通过 adb 命令来获取设备上的进程信息,如 adb shell dumpsys meminfo、adb shell procrank、adb shell + top -n 等命令
来看下采样时,具体做了哪些
@Override
protected void doSample() {
BufferedReader cpuReader = null;
BufferedReader pidReader = null;
// 读取 proc 文件,使用 BufferedReader
// 获取 Cpu 总的 Info
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();
}
// 获取进程的 Info
pidReader = new BufferedReader(new InputStreamReader(
new FileInputStream("/proc/" + mPid + "/stat")), BUFFER_SIZE);
String pidCpuRate = pidReader.readLine();
if (pidCpuRate == null) {
pidCpuRate = "";
}
// 读取,解析到Map里
parse(cpuRate, pidCpuRate);
}
- 多久采样一次?
先看下采样的调用链
- LooperMonitor.println()
- LooperMonitor.startDump()
- AbstractSampler.start()
- CpuSampler.doSample()
在 AbstractSampler.start() 调用后,以 Handler.postDelay 的方式执行 mRunnable,而关键就在这个 mRunnable 里面,它会先执行 doSample() 这个抽象方法,然后继续在内部 postDelay(mRunnable),就可以不断采样了,而间隔时间就是 delayTime
private Runnable mRunnable = new Runnable() {
@Override
public void run() {
// 执行采样
doSample();
if (mShouldSample.get()) {
// 继续调用自己
HandlerThreadFactory.getTimerThreadHandler()
.postDelayed(mRunnable, mSampleInterval);
}
}
};
parse-解析采样数据
目标是计算两个百分比,分别是总 CPU 使用率,当前进程 CPU 使用率
由于 proc 中记录的数值是从 CPU 加电到目前的累计值,所有可以使用一段时间间隔下(较短的时间间隔,BlockCanary 中默认采样间隔 300ms),两个累计值的差值,来计算 CPU 的使用率
- CPU 总使用率 =(CPU 总的执行时间 - CPU 空闲时间)/ CPU 总执行时间 * 100%
- 进程 CPU 使用率同理
parse 主要是计算后,将数据保存到内存中,当卡顿发生时,数据落盘到硬盘中,用于 DisplayActivity 中的展示
private void parse(String cpuRate, String pidCpuRate) {
//可以使用 StringTokenizer 替换 String.split,性能上更好
String[] cpuInfoArray = cpuRate.split(" ");
if (cpuInfoArray.length < 9) {
return;
}
// 将各个状态下的 CPU 时间加起来,就是总的 CPU 时间
long user = Long.parseLong(cpuInfoArray[2]);
long nice = Long.parseLong(cpuInfoArray[3]);
long system = Long.parseLong(cpuInfoArray[4]);
long idle = Long.parseLong(cpuInfoArray[5]);
long ioWait = Long.parseLong(cpuInfoArray[6]);
long total = user + nice + system + idle + ioWait
+ Long.parseLong(cpuInfoArray[7])
+ Long.parseLong(cpuInfoArray[8]);
String[] pidCpuInfoList = pidCpuRate.split(" ");
if (pidCpuInfoList.length < 17) {
return;
}
long appCpuTime = Long.parseLong(pidCpuInfoList[13])
+ Long.parseLong(pidCpuInfoList[14])
+ Long.parseLong(pidCpuInfoList[15])
+ Long.parseLong(pidCpuInfoList[16]);
if (mTotalLast != 0) {
StringBuilder stringBuilder = new StringBuilder();
long idleTime = idle - mIdleLast;
long totalTime = total - mTotalLast;
stringBuilder
.append("cpu:")
// 非空闲时间 / 总时间
.append((totalTime - idleTime) * 100L / totalTime)
.append("% ")
.append("app:")
.append((appCpuTime - mAppCpuTimeLast) * 100L / totalTime)
.append("% ")
.append("[")
.append("user:").append((user - mUserLast) * 100L / totalTime)
.append("% ")
.append("system:").append((system - mSystemLast) * 100L / totalTime)
.append("% ")
.append("ioWait:").append((ioWait - mIoWaitLast) * 100L / totalTime)
.append("% ]");
synchronized (mCpuInfoEntries) {
// 将解析结果写到 Map(内存)中
mCpuInfoEntries.put(System.currentTimeMillis(), stringBuilder.toString());
if (mCpuInfoEntries.size() > MAX_ENTRY_COUNT) {
for (Map.Entry<Long, String> entry : mCpuInfoEntries.entrySet()) {
Long key = entry.getKey();
mCpuInfoEntries.remove(key);
break;
}
}
}
}
mUserLast = user;
mSystemLast = system;
mIdleLast = idle;
mIoWaitLast = ioWait;
mTotalLast = total;
mAppCpuTimeLast = appCpuTime;
}