Android开发APP过程中,对于某些功耗较大的功能需要实时监测CPU占用(CPU作为手机功耗的核心模块,几乎占了性能消耗的大数,因此监控了CPU基本也就了解了当前手机的运行状况)。

目前市面上的一些监控CPU的程序有的是针对某些机型的CPU(比如高通针对骁龙芯片的Trepen,MTK针对联发科芯片的Mali),有的只能监控整体CPU,而无法针对某个应用的占用进行监控(比如PerfMon);

而Android studio自带的Android Profiler工具在更新到某个版本之后就只支持API 21之后的机型了,比较旧的机型无法使用;更为严重的是,在本人实测多款机型中发现,开启Android Profiler会开启一个debug监控进程,这个监控进程对于某些机型(小米居多)的真实CPU占用是有影响的,一开启的CPU占用会迅速升高,甚至会出现可见性的卡顿情况;

 

针对这种难题,本人在查询多方资料,选择不影响实际性能的方式,实现了一个针对全机型适用的CPU监测工具,github地址:https://github.com/duguju/AndroidCPUCollector

 

核心类是CPUCollector:

/*
 * Copyright (C) 2019 jjoeyang. All Rights Reserved.
 */
package com.yzz.cpucollector;

import android.util.Log;

import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;

/**
 * CPU占用统计类
 * 单例,进入APP时通过setPkgName()设置应用包名,调用getCPURate()即可输出CPU统计信息,退出时调用release()
 *
 * Created by jjoeyang on 19/5/6
 */
public class CPUCollector {
    public static final String TAG = CPUCollector.class.getSimpleName();
    private String mPkgName = null;
    private String mLastCPU = "";
    private Thread mCpuThread;
    private boolean enableCPU = true;
    private boolean isRunning = false;
    private boolean doCollect = false;
    private static final String COMMAND_SH = "sh";
    private static final String COMMAND_LINE_END = "\n";
    private static final String COMMAND_EXIT = "exit\n";

    private int maxFrameCount = 5; // 统计的总次数,可修改
    private int resultFrameTimes = 0; // 统计次数
    private double resultAVGValue;
    private String mAvgCPUValue;

    private static CPUCollector mInstance = null;

    private CPUCollector() {
        if (mCpuThread == null) {
            mCpuThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    while (enableCPU) {
                        if (doCollect && !isRunning) {
                            doCollect = false;
                            isRunning = true;
                            mLastCPU = getCPUFromTopCMD();
                            isRunning = false;
                        } else {
                            try {
                                Thread.sleep(10);
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                    }
                }
            });
            mCpuThread.setName("CpuCollectorThread");
            mCpuThread.start();
        }
    }

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

    public void setPkgName(String pkgName) {
        mPkgName = pkgName;
    }

    public String getCPURate() {
        if (!isRunning && mPkgName != null) {
            doCollect = true;
        }
        return mLastCPU;
    }

    public String getAvgCPU() {
        return mAvgCPUValue;
    }

    public void release() {
        mPkgName = null;
        mAvgCPUValue = null;
        enableCPU = false;
        mCpuThread = null;
    }

    private String getCPUFromTopCMD() {
        String cpu = "";
        List<String> result = execute("top -n 1 -s cpu | grep " + mPkgName);
        if (result != null && result.size() > 0) {
            String r = result.get(0);
            if (r != null && r.contains("%")) {
                int end = r.indexOf("%");
                int start = -1;
                for (int i = end; i >= 0; i--) {
                    if (Character.isWhitespace(r.charAt(i))) {
                        start = i;
                        break;
                    }
                }
                if (start >= 0) {
                    cpu = r.substring(start, end);
                    calculateAVGValue(Double.parseDouble(cpu));
                }
            }
        }
        return cpu;
    }

    /**
     * 执行单条命令
     *
     * @param command
     * @return
     */
    private List<String> execute(String command) {
        return execute(new String[]{command});
    }

    /**
     * 可执行多行命令(bat)
     *
     * @param commands
     * @return
     */
    private List<String> execute(String[] commands) {
        List<String> results = new ArrayList<String>();
        int status = -1;
        if (commands == null || commands.length == 0) {
            return null;
        }
        Process process = null;
        BufferedReader successReader = null;
        BufferedReader errorReader = null;
        StringBuilder errorMsg = null;
        DataOutputStream dos = null;
        try {
            process = Runtime.getRuntime().exec(COMMAND_SH);
            dos = new DataOutputStream(process.getOutputStream());
            for (String command : commands) {
                if (command == null) {
                    continue;
                }
                dos.write(command.getBytes());
                dos.writeBytes(COMMAND_LINE_END);
                dos.flush();
            }
            dos.writeBytes(COMMAND_EXIT);
            dos.flush();

            status = process.waitFor();

            errorMsg = new StringBuilder();
            successReader = new BufferedReader(new InputStreamReader(process.getInputStream()));
            errorReader = new BufferedReader(new InputStreamReader(process.getErrorStream()));
            String lineStr;
            while ((lineStr = successReader.readLine()) != null) {
                results.add(lineStr);
            }
            while ((lineStr = errorReader.readLine()) != null) {
                errorMsg.append(lineStr);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (dos != null) {
                    dos.close();
                }
                if (successReader != null) {
                    successReader.close();
                }
                if (errorReader != null) {
                    errorReader.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }

            if (process != null) {
                process.destroy();
            }
        }
        Log.d(TAG, (String.format(Locale.CHINA, "execute command end, errorMsg:%s, and status %d: ",
                errorMsg, status)));
        return results;
    }

    private void calculateAVGValue(double resultTime) {
        if (resultFrameTimes >= maxFrameCount) {
            if (resultFrameTimes == maxFrameCount) {
                resultFrameTimes++;
            }
            mAvgCPUValue = String.format("%.2f", resultAVGValue);
            resultFrameTimes = 0;
            resultAVGValue = 0;
            return;
        }
        resultFrameTimes++;
        double allResultTime = (resultFrameTimes - 1) * resultAVGValue;
        resultAVGValue = (allResultTime + resultTime) / resultFrameTimes;
    }
}

核心原理就是通过代码方式,固定间隔调用adb监控CPU的命令(adb shell top -m 100 -n 1 -s cpu),从而输出当前应用的CPU占用情况;支持获取当前占用并进行N帧的平均统计。具体API调用及使用方式可参考github中的Demo

第一版是log输出的CPU信息,Demo中过滤以下log:

05-13 12:03:48.914 12240-12240/com.yzz.cpucollector E/duguju-cpu: 当前CPU占用: 4%  平均:5.2%

今后会做一些界面、操作优化等更新工作,敬请期待~