BlockCanary CPU 采样

139 阅读3分钟

458EC93D-80A0-4abe-A1C2-DBC2B6820D1B.png

前言

对 CpuSampler 的几个关键方法做分析,采样原理的篇幅还不小,单独开这一篇

doSample()-CPU采样

CPU 采样时,主要获取 CPU 总的使用率,进程对 CPU 的使用率

获取这两个数据来判断,卡顿是由于进程自身使用 CPU 过高导致,还是其他程序使用 CPU 过多导致的

在 Android 中,可以通过读取 /proc/stat 文件来获取系统的 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);
}
  • 多久采样一次?

先看下采样的调用链

  1. LooperMonitor.println()
  2. LooperMonitor.startDump()
  3. AbstractSampler.start()
  4. 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 的使用率

  1. CPU 总使用率 =(CPU 总的执行时间 - CPU 空闲时间)/ CPU 总执行时间 * 100%
  2. 进程 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;
}