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 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 execute(String command) {
return execute(new String[]{command});
}
/**
* 可执行多行命令(bat)
*
* @param commands
* @return
*/
private List execute(String[] commands) {
List results = new ArrayList();
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%
今后会做一些界面、操作优化等更新工作,敬请期待~