监控模块解析
概述
solopi的监控部分主要在工程目录src的shared下,部分对性能要求较高的监控指标采用c语言收集,利用jni技术提供调用接口。
整体框架解耦性较高,其基础性能数据监控代码在display目录下。
调用链解析
displayable接口作为基础的性能数据监控接口,被具体的性能监控实现类继承实现,具体的文件在目录display\items\下,共有6个文件,实现了对电量、cpu数据、fps数据、内存数据等的监控。
每个displayable实现类由注解DisplayItem记录属性,由DisplayItemInfo解释和使用。
displayable实现类由DisplayProvider类进行服务包装,统一对外提供运行入口和持续收集能力。
具体的实现方式是,DisplayProvider提供了一个对外的启动入口,startDisplay(name)方法,传入的参数是displayable实现类的TAG属性,该属性记录了实现类的类名称,从而可以运用反射原理,对选中的实现类的实现启动。
完整的调用链关系示例如下:
PerformanceActivity加载性能监控列表mFlootListView;
---------------------------->
mFlootListView绑定性能监控适配器PerformFloatAdapter,在适配器内的onclick()方法内,调用displayManager.updateRecordingItems方法;
---------------------------->
updateRecordingItems通过Provider.startDisplay和Provider.stopDisplay方法实现对监控服务的启停;
---------------------------->
在startDisplay方法内,传入监控实现类的tagname,通过反射动态调用监控服务。
adb提权
基本原理
由于在性能数据收集中,一些数据的采集会受限于android系统的版本(例如android 7 以上,无法直接读取/proc/stat文件)或者具体机型(例如 oppo的手机,即使是android 7也无法直接读取到/proc/stat文件)导致收集失败,因此,除了传统的机内读取文件等形式,solopi还补充了通过adb执行命令的形式来收集数据。
在solopi中,adb功能主要分为两个部分,底层的实现(密钥生成、连接建立等等)引用自开源项目 adblib,git地址:https://github.com/cgutman/AdbLib ,其使用说明和api文档很全,这里不再阐述。 还有部分建立在底层之上,是对adb命令的封装和执行,主要集中在CmdLine和CmdTools里。
其基本原理是,在设备上建立与守护进程adbd的连接,从而可以在设备上执行adb shell命令。
adb连接过程
以点击录制工具时为例,简述adb的连接过程。
- screenRecordBtn.setOnClickListener对录制按钮设置点击监听事件;
- 点击录制工具按钮后,方法PermissionUtil.grantHighPrivilegePermissionAsync(new CmdTools.GrantHighPrivPermissionCallback() {...检测是否具备adb连接条件,如果不具备,则提示“请在命令行执行 adb tcpip 5555”;
- 用户执行命令后,设备的adbd守护进程开始监听端口5555,准备建立连接;
- 再次点击录制工具按钮,重新检测后,执行 CmdTools.generateConnection(),建立adb连接。
cpu性能数据收集
cpu的性能数据收集方法在display目录下的CPUTools文件内,下面是该文件的解析。
原理概述
solopi内,cpu的主要实现原理只有一个(但是途径有两个),就是通过读取/prpo/stat和/proc/pid/stat文件来计算出所要参数。
/proc/pid/stat和/proc/stat这两个文件网上的资料很多,这里就不过多阐述了,主要讲一下具体的算法。
stat读取途径
solopi内有两种读取stat文件的途径,分别是系统内直接读取(由c实现)和adb命令读取。
原因主要是在安卓7.0以上,无法直接读取stat文件,所以这里做了系统判断,如果是7.0以上的或者是特殊机型,那么使用adb途径读取文件;如果是7.0以下的,那么直接使用c进行读取,使用c来读取的好处是更快资源消耗更低,使用adb是不得已的用法。
整体cpu使用率的计算
计算cpu总量的方法是getUsage(),
cpu总量的计算代码(已加注释)如下:
try {
currentJiffies = Long.parseLong(cpuInfos[1]) + Long.parseLong(cpuInfos[2]) + Long.parseLong(cpuInfos[3])
+ Long.parseLong(cpuInfos[4]) + Long.parseLong(cpuInfos[5]) + Long.parseLong(cpuInfos[6])
+ Long.parseLong(cpuInfos[7]);// 相加得到当前使用总量
currentIdle = Long.parseLong(cpuInfos[4]);// 当前的空闲用量
} catch (ArrayIndexOutOfBoundsException e) {
LogUtil.e(TAG, "ArrayIndexOutOfBoundsException" + e.getMessage(), e);
return -1f;
} catch (NumberFormatException e) {
LogUtil.e(TAG, "CPU行【%s】格式无法解析", load);
}
if (lastJiffies == 0 || lastIdle == 0) {
lastJiffies = currentJiffies; //currentJiffies是总使用量; lastJiffies 最后记录的总使用量
lastIdle = currentIdle; //currentIdle是空闲时间;lastIdle 最后记录的空闲时间
return -1f;
} else {
long gapJiffies = currentJiffies - lastJiffies; // gapJiffies 间隔时间段算出的间隔总量
long gapIdle = currentIdle - lastIdle; // gapIdle 间隔时间段算出的空闲总量
lastJiffies = currentJiffies; // 刷新一下最后用量
lastIdle = currentIdle;
if (gapIdle < 0 || gapJiffies < 0) {
return -1f;//数据有问题返回-1f
}
LogUtil.d(TAG, "CPU占用率:" + (gapJiffies - gapIdle) / (float) gapJiffies);
return 100 * (gapJiffies - gapIdle) / (float) gapJiffies;
}
可以看到,solopi的整体cpu占用率计算公式是: 100 * (gapJiffies - gapIdle) / (float) gapJiffies,即(总占用-空闲占用)/总占用
指定进程cpu占用率计算
指定进程的cpu占用率计算的方法是getPidsUsage(),
该方法主要使用命令“grep cpu /proc/stat && cat /proc/pid/stat”,执行后的结果存在数组内,分为两个部分,第一部分用于计算总体占用量,这个和上面的计算过程基本一致;第二部分用于计算进程的占用量,计算代码如下:
/**
* 应用CPU处理
* /proc/pid/stat 应用占用情况
* 2265 (id.XXX) S 610 609 0 0 -1 1077952832 130896 1460 185 0 683 329 3 10 14 -6 63 0 1982194 2124587008 28421 18446744073709551615 1 1 0 0 0 0 4612 0 1073798392 18446744073709551615 0 0 17 3 0 0 0 0 0 0 0 0 0 0 0 0 0
* 第14-17位之和为应用占用CPU时间之和
*/
SparseArray appResult = new SparseArray<>(pids.length + 1);
// 第一行是全局cpu数据
String[] splitLines = new String[origin.length - 1];
System.arraycopy(origin, 1, splitLines, 0, origin.length - 1);
// 处理每行获取到的数据
SparseArray newAppProcessTime = new SparseArray<>(appProcessTime.size() + 1);
for (String line: splitLines) {
String[] processInfos = line.trim().split("\\s+");
LogUtil.d(TAG, Arrays.toString(processInfos));
// 获取失败的状态
if (processInfos.length < 17) {
continue;
}
try {
int pid = Integer.parseInt(processInfos[0]);
Long pidProcessTime = Long.parseLong(processInfos[13]) + Long.parseLong(processInfos[14]) + Long.parseLong(processInfos[15]) + Long.parseLong(processInfos[16]);
Long lastProcessTime = appProcessTime.get(pid);
newAppProcessTime.put(pid, pidProcessTime);
// 如果没有上次记录,则跳过
if (lastProcessTime == null) {
continue;
}
// 计算APP进程处理时间
Long processRunning = pidProcessTime - lastProcessTime;
appResult.put(pid, 100 * (processRunning / (float) cpuRunning));
} catch (NumberFormatException e) {
LogUtil.e(TAG, "Format for string: " + line + " failed", e);
}
}
可以看到,进程单独的用量的公式是:
(processRunning / (float) cpuRunning)
内存数据收集
内存部分的数据收集主要在display目录的MemoryTools下,下面是该部分的解析。
原理概述
MemoryTools的数据收集主要依靠安卓原生api。
系统内存获取
系统内存获取主要使用的getTotalMemory(MemoryInfo)方法,入参MemoryInfo是一个内部类,其主要fieid有availMem、totalMem、threshold和lowMemory,含义如下:
- availMem:系统上的可用内存;
- totalMem:内核可访问的总内存;
- threshold:我们认为内存较低并开始查杀后台服务和其他非外部进程的阈值;
- lowMemory:如果系统认为自己当前处于低内存状态,则设置为true。
这里系统的内存获取,直接使用了availMem和totalMem字段的值。
该部分的代码如下:
获取总内存:
/**
* 获取总内存数据
* @return
*/
private Long getTotalMemory() {
if (activityManager == null) {
return 0L;
}
MemoryInfo info = new MemoryInfo();
activityManager.getMemoryInfo(info);
return info.totalMem / BYTES_PER_MEGA;
}
获取可用内存:
public static Long getAvailMemory(Context cx) {// 获取android当前可用内存大小
if (cx == null) {
return 0L;
}
ActivityManager am = (ActivityManager) cx.getSystemService(Context.ACTIVITY_SERVICE);
MemoryInfo mi = new MemoryInfo();
am.getMemoryInfo(mi);
LogUtil.i(TAG, "Available memory: " + mi.availMem);
// mi.availMem; 当前系统的可用内存
return mi.availMem / BYTES_PER_MEGA;// 将获取的内存大小规格化
}
指定进程的内存获取
指定进程的内存获取主要使用的Debug.MemoryInfo.getTotalPss()和Debug.MemoryInfo.getTotalPrivateDirty(),分别获取了应用的总pss内存和PrivateDirty内存。
该部分代码如下:
获取指定pid的内存:
if (pid != null && pid.getPid() > 0) {
Debug.MemoryInfo[] memInfos = activityManager.getProcessMemoryInfo(new int[]{pid.getPid()});
if (memInfos != null && memInfos.length > 0) {
Debug.MemoryInfo info = memInfos[0];
return String.format(Locale.CHINA, "pss:%.2fMB/privateDirty:%.2fMB", info.getTotalPss() / 1024f, info.getTotalPrivateDirty() / 1024f);
}
}
电量数据收集
内存部分的数据收集主要在display目录的BatteryInfo下,下面是该部分的解析。
原理概述
安卓5.0及以上,直接通过调用系统原生api获取电量信息;以下则读取/sys/class/power_supply/下一个包含battery的文件夹中的current_now文件;这里主要分析5.0以上的(5.0以下的设备实在太少了且越来越少了)
瞬时电流计算
瞬时电流的值主要通过调用原生api:BatteryManager.getLongProperty()获取,其入参是规定的常量,主要有以下参数:
- BATTERY_PROPERTY_CHARGE_COUNTER: 剩余电池容量,单位为微安时
- BATTERY_PROPERTY_CURRENT_NOW: 瞬时电池电流,单位为微安
- BATTERY_PROPERTY_CURRENT_AVERAGE: 平均电池电流,单位为微安
- BATTERY_PROPERTY_CAPACITY: 剩余电池容量,显示为整数百分比
- BATTERY_PROPERTY_ENERGY_COUNTER: 剩余能量,单位为纳瓦时
在solopi里,BatteryInfo.getCurrent(...)方法内直接使用BatteryManager.getLongProperty(BATTERY_PROPERTY_CURRENT_NOW)获取到瞬时电量;有趣的是,我还发现在getCurrent内,也使用了BatteryManager.getLongProperty(BATTERY_PROPERTY_CURRENT_AVERAGE)来获取平均电量,但是最终没有使用该值作为平均电量的展示,原因会在下面写到。
平均电流计算
在solopi里,平均电流的计算公式是: point / loop;
point是每次获取的瞬时电量current的累加值,loop是累加次数,因此,平均电流就是总累加值除以累加次数。
为什么不是直接调用原生api获取?因为通过BatteryManager.getLongProperty(BATTERY_PROPERTY_CURRENT_AVERAGE)获取到的值不是从你开启监控的那一刻开始计算的,而是带上了之前的用量,如果需要精准到你开始监控的点,就只有自己计算了。
通过这样的计算方式,在solopi中也可以很方便的清除电流值(也就是初始化point和loop的值)从新时刻再次计算。
网络数据收集
内存部分的数据收集主要在display目录的NetWorkTools下,下面是该部分的解析。
原理概述
主要通过读取/proc/net/xt_qtaguid/stats文件。
应用的上下行网络数据获取
网络数据的获取,主要是通过adb shell执行"/proc/net/xt_qtaguid/stats | grep uid"命令,读取到对应文件,读取出的数据每一行的格式大约是:
138 wlan0 0x0 10141 0 39063593 22051 3175000 24476 39063593 22051 0 0 0 0 3175000 24476 0 0 0 0
其中,第四列(这里solopi的注释写的是第一列,应该是误写了)代表了应用程序的uid;第6和8列为 rx_bytes(接收数据)和tx_bytes(上传数据),包含tcp,udp等所有网络流量传输。
需要说明的是,这里获取到的数据,并不是瞬时数据,而是程序自开机以来的累计值,因此这个数据还需要进行相应的计算才能得出间隔时间内上传和接收的量。
此外,通过uid获取应用的程序流量,也并非天衣无缝的,的确一般而言,一个应用只会被分配一个单独的uid,但是如果是同一个开发方旗下有一些应用需要共享数据,就可能会在menifest配置文件中使用相同的sharedUserId,这样Android系统就会在安装应用时为其分配相同的UID。
具体的计算方法是,
上下行速率
在间隔的时间内,用获取的流量差(本次获取的数据量-上次获取的数据量)/ 获取的时间差(本次获取的时刻 - 上次获取的时刻),这样就得到了间隔时间内的速率。上下行累计数据
第一次获取数据时,记一个开始的数据量,在结束时,用最后一次获取的数据量 - 开始的数据量。
该部分的代码是:
public static float[] getAppResult(int uid) {
String[] cmds;
/**
* /proc/net/xt_qtaguid/stats 记录各应用网络自开机使用情况
* 每一行数据:
* 26 wlan0 0x0 10039 0 10143 20 3061 27 10143 20 0 0 0 0 3061 27 0 0 0 0
* 第一列为UID,第6和8列为 rx_bytes(接收数据)和tx_bytes(传输数据)
*/
cmds = CmdTools.execAdbCmd("cat /proc/net/xt_qtaguid/stats | grep " + uid, 0).split("\n");
Long currentTime = System.currentTimeMillis();
Long rxTotal = 0L;
Long txTotal = 0L;
for (String cmd: cmds) {
String[] data = cmd.trim().split("\\s+");
if (data.length > 8) {
rxTotal += Long.parseLong(data[5]);
txTotal += Long.parseLong(data[7]);
}
}
LogUtil.i(TAG, "get Total Rx: " + rxTotal + " | get Total Tx: " + txTotal);
float rxSpeed = (rxTotal - lastAppRx) * KB_MILLION_SECOND / (currentTime - lastAppTime);
if (rxSpeed >= 0) {
lastAppRx = rxTotal;
} else {
rxSpeed = 0F;
}
float txSpeed = (txTotal - lastAppTx) * KB_MILLION_SECOND / (currentTime - lastAppTime);
if (txSpeed >= 0) {
lastAppTx = txTotal;
} else {
txSpeed = 0F;
}
lastAppTime = currentTime;
LogUtil.d(TAG, "加载Rx: %f, Tx: %f", rxSpeed, txSpeed);
if (startAppRx == 0 || startAppTx == 0) {
startAppRx = lastAppRx;
startAppTx = lastAppTx;
}
if (triggerReload) {
startAppRx = lastAppRx;
startAppTx = lastAppTx;
triggerReload = false;
}
return new float[]{rxSpeed, (lastAppRx - startAppRx) / 1024F, txSpeed, (lastAppTx - startAppTx) / 1024F};
}
整机的上下行网络数据获取
整机的上下行网络数据获取,这里直接使用了TrafficStats类里的方法,其实其底层也是通过读取/proc/net/xt_qtaguid/stats文件来进行的,包括计算方式和单独的应用获取网络数据基本相同,因此这里就不再多加阐述。