2章 性能平台GodEye源码分析-数据模块
2. 数据模块
2.1 Cpu
Cpu的获取有两种方式
- 通过读取
/proc/stat
文件获取,但高版本的Android已经不给权限读取 - 通过adb命令
top -n 1
,获取当前所有进程列表的cpu使用情况,再过滤当前进程
源码分析
1、启动Cpu的监控
Cpu的监控通过启动定时器,按xml配置的时间进行采集,通过CpuUsage.getCpuInfo()
获取Cpu信息
public class CpuEngine implements Engine {
private Producer<CpuInfo> mProducer;
private long mIntervalMillis;
private CompositeDisposable mCompositeDisposable;
public CpuEngine(Producer<CpuInfo> producer, long intervalMillis) {
mProducer = producer;
mIntervalMillis = intervalMillis;
mCompositeDisposable = new CompositeDisposable();
}
@Override
public void work() {
mCompositeDisposable.add(Observable.interval(mIntervalMillis, TimeUnit.MILLISECONDS)
.subscribeOn(ThreadUtil.computationScheduler())
.observeOn(ThreadUtil.computationScheduler())
.map(new Function<Long, CpuInfo>() {
@Override
public CpuInfo apply(Long aLong) throws Exception {
ThreadUtil.ensureWorkThread("CpuEngine apply");
return CpuUsage.getCpuInfo();
}
})
.filter(new Predicate<CpuInfo>() {
@Override
public boolean test(CpuInfo cpuInfo) throws Exception {
return CpuInfo.INVALID != cpuInfo;
}
}
)
.subscribe(new Consumer<CpuInfo>() {
@Override
public void accept(CpuInfo food) throws Exception {
ThreadUtil.ensureWorkThread("CpuEngine accept");
mProducer.produce(food);
}
}));
}
@Override
public void shutdown() {
mCompositeDisposable.dispose();
}
}
2、Cpu文件采集
初始化阶段获取是否拥有/proc/stat
和/proc/ + pid + /stat
文件的读取权限
public class CpuUsage {
static {
isReadFromFile = cpuFileUsability();
}
private static boolean cpuFileUsability() {
File stat = new File("/proc/stat");
if (!stat.exists() || !stat.canRead()) {
return false;
}
int pid = android.os.Process.myPid();
File statPid = new File("/proc/" + pid + "/stat");
if (!statPid.exists() || !statPid.canRead()) {
return false;
}
return true;
}
}
如果有权限则通过文件读取
-
getCpuRateOfDevice()
对应获取设备的Cpu占用率,体现在/proc/stat
文件 -
getCpuRateOfApp()
对应获取App的Cpu占用率,体现在/proc/ + pid + /stat
文件
public static CpuInfo getCpuInfo() {
if (isReadFromFile) {
if (sLastCpuSnapshot == null) {
//1、获取首次Cpu快照
sLastCpuSnapshot = parse(getCpuRateOfDevice(), getCpuRateOfApp());
return CpuInfo.INVALID;
} else {
//2、第二次开始正常采集
CpuSnapshot current = parse(getCpuRateOfDevice(), getCpuRateOfApp());
float totalTime = (current.total - sLastCpuSnapshot.total) * 1.0f;
if (totalTime <= 0) {
L.e("totalTime must greater than 0");
return CpuInfo.INVALID;
}
//3、通过差值计算出Cpu占用率情况
long idleTime = current.idle - sLastCpuSnapshot.idle;
double totalRatio = (totalTime - idleTime) / totalTime;
double appRatio = (current.app - sLastCpuSnapshot.app) / totalTime;
double userRatio = (current.user - sLastCpuSnapshot.user) / totalTime;
double systemRatio = (current.system - sLastCpuSnapshot.system) / totalTime;
double ioWaitRatio = (current.ioWait - sLastCpuSnapshot.ioWait) / totalTime;
return new CpuInfo(filterCpuRatio(totalRatio), filterCpuRatio(appRatio), filterCpuRatio(userRatio), filterCpuRatio(systemRatio), filterCpuRatio(ioWaitRatio));
}
} else {
return getCpuInfoFromShell();
}
}
private static String getCpuRateOfDevice() {
BufferedReader cpuReader = null;
try {
cpuReader = new BufferedReader(new InputStreamReader(
new FileInputStream("/proc/stat")), BUFFER_SIZE);
String cpuRate = cpuReader.readLine();
if (cpuRate == null) {
return "";
}
return cpuRate.trim();
} catch (Throwable e) {
return "";
} finally {
IoUtil.closeSilently(cpuReader);
}
}
private static String getCpuRateOfApp() {
BufferedReader pidReader = null;
try {
int pid = android.os.Process.myPid();
pidReader = new BufferedReader(new InputStreamReader(
new FileInputStream("/proc/" + pid + "/stat")), BUFFER_SIZE);
String pidCpuRate = pidReader.readLine();
if (pidCpuRate == null) {
return "";
}
return pidCpuRate;
} catch (Throwable throwable) {
return "";
} finally {
IoUtil.closeSilently(pidReader);
}
}
从proc文件中可以获取Cpu的时间片,包含多个进程Cpu使用情况。你会看到代码先获取首次Cpu的快照,通过采集两次时间片数据,获取进程CPU占用率,CPU占用率 = (Cpu使用时间2-Cpu使用时间T1)/(系统总时间2-系统总时间1)的时间片比值,相当于相对时间内,Cpu使用时间占据总时间的占比就是CPU占用率
/proc/stat
文件
$ adb shell cat /proc/stat
//CPU指标:user,nice, system, idle, iowait, irq, softirq
cpu 4412950 601238 1778465 7121326 13345 411594 242073 0 0 0
cpu0 1353604 129916 544580 4805215 12287 140100 103310 0 0 0
cpu1 1792083 131733 628762 207535 547 129677 101769 0 0 0
cpu2 734610 172925 338288 236550 278 74416 21146 0 0 0
cpu3 480023 134831 237948 239156 197 61648 11430 0 0 0
cpu4 18583 14926 10303 406774 14 815 352 0 0 0
cpu5 16366 12440 8877 408038 8 4257 252 0 0 0
cpu6 9708 2679 5428 408075 5 407 161 0 0 0
cpu7 7973 1788 4279 409983 9 274 3653 0 0 0
intr ...
ctxt 239514543
btime 1605087699
processes 58405
procs_running 1
procs_blocked 0
softirq 42587145 1026 11042966 22389 2020257 1526512 1026 8516904 12031928 0 7424137
从文件表示的字段意思,取cpu第一排的第2-8的数据作为cpu的基准
- user(4412950):用户态时间
- nice(601238):用户态时间(低优先级,nice>0)
- system(1778465):处于核心态的运行时间
- idle(7121326):除IO等待时间以外的其它等待时间
- iowait(13345):I/O等待时间
- irq(411594):硬中断时间
- softirq(242073):软中断时间
/proc/ + pid + /stat
文件
$ adb shell cat /proc/21749/stat
21749 (com.tencent.mm) S 979 979 0 0 -1 1077952832 55150 0 25 0 549 156 0 0 20 0 65 0 6468487 4870205440 43654 184467440
73709551615 1 1 0 0 0 0 4608 4097 1073775868 0 0 0 17 2 0 0 71 0 0 0 0 0 0 0 0 0 0
从文件表示的字段意思,取第13-16的数据作为pid应用的Cpu使用总时间
- 13.utime(549):该进程处于用户态的时间
- 14.stime(156):该进程处于内核态的时间
- 15.cutime(0):当前进程等待子进程的用户态的时间
- 16.cstime(0):当前进程等待子进程的内核态的时间
知道各个指标后通过代码即可读取他们各自的指标
@VisibleForTesting
static CpuSnapshot parse(String cpuRate, String pidCpuRate) {
String[] cpuInfoArray = cpuRate.split("\\s+");
if (cpuInfoArray.length < 9) {
throw new IllegalStateException("cpu info array size must great than 9");
}
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) {
throw new IllegalStateException("pid cpu info array size must great than 17");
}
long appCpuTime = Long.parseLong(pidCpuInfoList[13])
+ Long.parseLong(pidCpuInfoList[14])
+ Long.parseLong(pidCpuInfoList[15])
+ Long.parseLong(pidCpuInfoList[16]);
return new CpuSnapshot(user, system, idle, ioWait, total, appCpuTime);
}
3、Cpu命令采集
通过adb命令top -n 1
,获取当前所有进程列表的cpu使用情况
OP4679:/ $ top -n 1
[H[J[?25l[H[JTasks: 761 total, 4 running, 756 sleeping, 0 stopped, 1 zombie
Mem: 7.4G total, 7.2G used, 168M free, 295M buffers
Swap: 2.5G total, 1.1G used, 1.3G free, 3.6G cached
800%cpu 116%user 0%nice 106%sys 555%idle 0%iow 16%irq 6%sirq 0%host
[7m PID USER PR NI VIRT RES SHR S[%CPU] %MEM TIME+ ARGS
[0m
[mm24732 u0_a424 6 -14 2.5G 327M 242M R 83.8 4.2 10:43.94 com.hensen.mobile
1074 system -2 -8 515M 17M 12M S 35.4 0.2 309:54.36 surfaceflinger
[mm26215 shell 20 0 29M 3.7M 2.7M R 25.8 0.0 0:00.07 top -n 1
960 system -3 -4 175M 6.0M 4.5M S 22.5 0.0 97:14.27 android.hardware.graphics.composer@2.3-service
24996 u0_i9125 10 -10 1.6G 91M 72M S 6.4 1.1 0:36.92 com.google.android.webview:sandboxed_process0:org.chrom+
554 logd 30 10 262M 79M 2.2M S 6.4 1.0 55:27.92 logd
102 root 20 0 0 0 0 S 6.4 0.0 4:18.83 [system]
25908 shell 20 0 123M 5.2M 4.2M S 3.2 0.0 0:09.17 adbd --root_seclabel=u:r:su:s0
25522 root 0 -20 0 0 0 S 3.2 0.0 0:01.43 [kworker/u17:3]
25399 u0_a424 20 0 2.4G 223M 5.4M S 3.2 2.9 0:27.68 app_d
25354 u0_a424 20 0 1.9G 71M 71M S 3.2 0.9 0:14.26 com.duowan.mobile:Daemon
20704 root 20 0 0 0 0 S 3.2 0.0 0:10.92 [kworker/u16:17]
[mm20703 root 20 0 0 0 0 R 3.2 0.0 0:09.43 [kworker/u16:16]
[mm20584 root 20 0 0 0 0 R 3.2 0.0 0:05.29 [kworker/u16:9]
15688 u0_a424 12 -8 1.9G 104M 104M S 3.2 1.3 3:04.35 com.duowan.mobile:yyPushService
15381 u0_a424 12 -8 1.9G 103M 103M S 3.2 1.3 2:17.64 com.duowan.mobile:sync
4915 root RT 0 0 0 0 S 3.2 0.0 9:50.10 [sugov:6]
3988 system 20 0 7.1G 86M 86M S 3.2 1.1 25:45.51 com.coloros.persist.system
1807 system 18 -2 10G 260M 260M S 3.2 3.4 272:51.65 system_server
998 system 20 0 86M 2.9M 2.5M S 3.2 0.0 14:17.59 vendor.qti.hardware.display.allocator@1.0-service
973 system 20 0 349M 3.5M 2.7M S 3.2 0.0 34:24.58 android.hardware.sensors@1.0-service
918 root 20 0 0 0 0 S 3.2 0.0 5:03.29 [lpass_IPCRTR]
246 root -3 0 0 0 0 S 3.2 0.0 19:05.06 [kgsl_worker_thr]
239 root RT 0 0 0 0 S 3.2 0.0 28:49.59 [crtc_commit:102]
26213 shell 20 0 27M 2.7M 2.1M S 0.0 0.0 0:00.02 sh -
26187 root 20 0 0 0 0 S 0.0 0.0 0:00.00 [kworker/3:1]
26166 root 0 -20 0 0 0 S 0.0 0.0 0:00.06 [kworker/2:2H]
26163 root 20 0 0 0 0 S 0.0 0.0 0:00.00 [kworker/2:1]
26159 root 20 0 0 0 0 S 0.0 0.0 0:00.00 [kworker/0:2]
26151 root 0 -20 0 0 0 S 0.0 0.0 0:00.09 [kworker/0:1H]
26123 root 20 0 0 0 0 S 0.0 0.0 0:00.31 [kworker/1:1]
26121 root 0 -20 0 0 0 S 0.0 0.0 0:00.28 [kworker/u17:0]
26071 root 20 0 0 0 0 S 0.0 0.0 0:00.14 [kworker/0:0]
26046 root 0 -20 0 0 0 S 0.0 0.0 0:00.16 [kworker/1:0H]
26035 root 0 -20 0 0 0 S 0.0 0.0 0:00.08 [kworker/3:0H]
25926 shell 20 0 28M 2.7M 2.2M S 0.0 0.0 0:05.48 logcat -v long -v epoch
25878 root 20 0 0 0 0 S 0.0 0.0 0:00.00 [kworker/5:0]
25748 system 20 0 5.1G 70M 52M S 0.0 0.9 0:00.16 com.coloros.securepay
25727 u0_a424 12 -8 1.9G 110M 105M S 0.0 1.4 0:13.01 com.duowan.mobile:RemoteBackgroundProcess
25706 u0_a33 20 0 5.1G 77M 58M S 0.0 1.0 0:00.31 android.process.media
25673 root 20 0 0 0 0 S 0.0 0.0 0:00.00 [kworker/4:1]
25665 root 0 -20 0 0 0 S 0.0 0.0 0:00.19 [kworker/0:2H]
25664 root 0 -20 0 0 0 S 0.0 0.0 0:00.08 [kworker/2:0H]
25654 root 20 0 0 0 0 S 0.0 0.0 0:00.01 [kworker/1:0]
[?25h[0m[1000;1H[K[?25h[?25h[0m[1000;1H[KOP4679:/ $ 00 [kworker/2:0]
代码启动命令是Runtime.getRuntime().exec("top -n 1")
private static CpuInfo getCpuInfoFromShell() {
java.lang.Process process = null;
CpuInfo cpuInfo = new CpuInfo();
try {
process = Runtime.getRuntime().exec("top -n 1");
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
int cpuIndex = -1;
Map<String, Float> cpuDevice = null;
while ((line = reader.readLine()) != null) {
line = line.trim();
if (TextUtils.isEmpty(line)) {
continue;
}
//1、当前设备的Cpu使用占用率
Map<String, Float> tempCpuDevice = parseCpuRateOfDeviceAndTotalByShell(line);
if (tempCpuDevice != null) {
// 800%cpu 7%user 3%nice 28%sys 755%idle 0%iow 3%irq 3%sirq 0%host
cpuDevice = tempCpuDevice;
Float total = cpuDevice.get("cpu");
Float user = cpuDevice.get("user");
Float sys = cpuDevice.get("sys");
Float idle = cpuDevice.get("idle");
Float iow = cpuDevice.get("iow");
if (total != null && total > 0 && iow != null && iow >= 0) {
cpuInfo.ioWaitRatio = iow / total;
}
if (total != null && total > 0 && sys != null && sys >= 0) {
cpuInfo.sysCpuRatio = sys / total;
}
if (total != null && total > 0 && idle != null && idle >= 0) {
cpuInfo.totalUseRatio = (total - idle) / total;
}
if (total != null && total > 0 && user != null && user >= 0) {
cpuInfo.userCpuRatio = user / total;
}
continue;
}
//2、当前App的Cpu使用占用率
int tempIndex = parseCPUIndex(line);
if (tempIndex != -1) {
// PID USER PR NI VIRT RES SHR S[%CPU] %MEM TIME+ ARGS
cpuIndex = tempIndex;
continue;
}
float tempAppRatio = parseCpuRateOfAppByShell(line, cpuIndex, cpuDevice == null ? null : cpuDevice.get("cpu"));
if (tempAppRatio != -1) {
// 816 system -2 -8 2.1G 27M 22M S 6.8 0.3 38:04.22 surfaceflinger
cpuInfo.appCpuRatio = tempAppRatio;
return cpuInfo;
}
}
} catch (IOException e) {
L.e(e);
} finally {
if (process != null) {
process.destroy();
}
}
return cpuInfo;
}
- 当前设备的Cpu使用占用率
通过adb命令我们查看需要解析的数据,可以发现在中间有段800%cpu 116%user 0%nice 106%sys 555%idle 0%iow 16%irq 6%sirq 0%host
。这句就是我们当前设备的Cpu使用占用率,接下来就是正则匹配,将解析出来的数据,放到Map<String, Float> tempCpuDevice
上,再从Map中取出对应的值
- 当前App的Cpu使用占用率
首先要解析出第五句,表示当前Cpu的Index位置,具体在[7m PID USER PR NI VIRT RES SHR S[%CPU] %MEM TIME+ ARGS
,通过匹配获取CPU字样的index
@VisibleForTesting
static int parseCPUIndex(String line) {
if (line.contains("CPU")) {
String[] titles = line.split("\\s+");
for (int i = 0; i < titles.length; i++) {
if (titles[i].contains("CPU")) {
return i;
}
}
}
return -1;
}
接着要解析第七句,表示当前Cpu的Index位置的具体Cpu的值,具体在[mm24732 u0_a424 6 -14 2.5G 327M 242M R 83.8 4.2 10:43.94 com.hensen.mobile
,通过正则获取Cpu当前的占用值83.8
,当然这个值要和Cpu的总值进行计算得出Cpu占比率
@VisibleForTesting
static float parseCpuRateOfAppByShell(String line, int cpuIndex, Float cpuTotal) {
if (line.startsWith(String.valueOf(android.os.Process.myPid()))) {
if (cpuIndex == -1) {
throw new IllegalStateException("parseCpuRateOfAppByShell but cpuIndex == -1:" + line);
}
String[] param = line.split("\\s+");
if (param.length <= cpuIndex) {
throw new IllegalStateException("parseCpuRateOfAppByShell but param.length <= cpuIndex:" + line);
}
String cpu = param[cpuIndex];
if (cpu.endsWith("%")) {
cpu = cpu.substring(0, cpu.lastIndexOf("%"));
}
if (cpuTotal == null || cpuTotal <= 0) {
throw new IllegalStateException("parseCpuRateOfAppByShell but cpuTotal == null || cpuTotal <= 0:" + line);
}
try {
return Float.parseFloat(cpu) / cpuTotal;
} catch (Throwable e) {
throw new IllegalStateException("parseCpuRateOfAppByShell but " + e + ":" + line);
}
}
return -1;
}
4、总结
其实,Cpu占用率的获取要么从文件中去获取,要么从命令去获取,通过系统给的一些关键值,我们人为解析出来,再通过计算得出来Cpu的使用率,很大程度上,Cpu的值的对错跟系统给的值准确性有关
2.2 Battery
Battery的获取有以下几种方式
- 通过系统电量变化的广播进行采集(GodEye采取这种)
- 通过命令
adb shell dumpsys battery
定时采集(网上方案) - 通过
BatteryManager
定时采集(网上方案)
源码分析
1、启动Battery的监控
启动的过程其实就是注册广播的过程
public class BatteryEngine implements Engine {
......
/**
* 任意线程
* 不保证线程安全,由外部保证
*/
@Override
public void work() {
if (mBatteryChangeReceiver == null) {
mBatteryChangeReceiver = new BatteryChangeReceiver();
mBatteryChangeReceiver.setBatteryInfoProducer(mProducer);
mContext.registerReceiver(mBatteryChangeReceiver, BatteryIntentFilterHolder.BATTERY_INTENT_FILTER);
}
}
private static final class BatteryIntentFilterHolder {
private static final IntentFilter BATTERY_INTENT_FILTER = new IntentFilter();
static {
BATTERY_INTENT_FILTER.addAction(Intent.ACTION_BATTERY_CHANGED);
BATTERY_INTENT_FILTER.addAction(Intent.ACTION_BATTERY_LOW);
BATTERY_INTENT_FILTER.addAction(Intent.ACTION_BATTERY_OKAY);
}
}
}
2、采集Battery信息
Battery的采集是通过广播传递过来的值,按照Api给定的类型,获取对应的电量状态、电池剩余容量、电池最大值、连接的电源插座、电压、温度、电池类型等信息
public class BatteryChangeReceiver extends BroadcastReceiver {
private Producer<BatteryInfo> mBatteryInfoProducer;
public void setBatteryInfoProducer(Producer<BatteryInfo> batteryInfoProducer) {
mBatteryInfoProducer = batteryInfoProducer;
}
@Override
public void onReceive(Context context, final Intent batteryInfoIntent) {
ThreadUtil.computationScheduler().scheduleDirect(new Runnable() {
@Override
public void run() {
try {
BatteryInfo batteryInfo = new BatteryInfo();
batteryInfo.status = batteryInfoIntent.getIntExtra(BatteryManager.EXTRA_STATUS, BatteryManager
.BATTERY_STATUS_UNKNOWN);
batteryInfo.health = batteryInfoIntent.getIntExtra(BatteryManager.EXTRA_HEALTH, BatteryManager
.BATTERY_HEALTH_UNKNOWN);
batteryInfo.present = batteryInfoIntent.getBooleanExtra(BatteryManager.EXTRA_PRESENT, false);
batteryInfo.level = batteryInfoIntent.getIntExtra(BatteryManager.EXTRA_LEVEL, 0);
batteryInfo.scale = batteryInfoIntent.getIntExtra(BatteryManager.EXTRA_SCALE, 0);
batteryInfo.plugged = batteryInfoIntent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0);
batteryInfo.voltage = batteryInfoIntent.getIntExtra(BatteryManager.EXTRA_VOLTAGE, 0);
batteryInfo.temperature = batteryInfoIntent.getIntExtra(BatteryManager.EXTRA_TEMPERATURE, 0);
batteryInfo.technology = batteryInfoIntent.getStringExtra(BatteryManager.EXTRA_TECHNOLOGY);
mBatteryInfoProducer.produce(batteryInfo);
} catch (Throwable e) {
L.e(String.valueOf(e));
}
}
});
}
}
3、总结
电量的获取比较简单,代码量相对较少,数据的准确也是依赖于系统的准确性
2.3 Fps
Fps指的是帧率的采集,默认指的是60帧每秒,1帧需要16ms
源码分析
1、启动Fps的监控
Fps的监控通过启动定时器,按xml配置的时间进行采集,通过mFpsMonitor.start()
启动Fps的采集,通过mFpsMonitor.exportThenReset()
获取Fps信息
public class FpsEngine implements Engine {
private Producer<FpsInfo> mProducer;
private long mIntervalMillis;
private CompositeDisposable mCompositeDisposable;
private FpsMonitor mFpsMonitor;
private final int mSystemRate;
public FpsEngine(Context context, Producer<FpsInfo> producer, long intervalMillis) {
mProducer = producer;
mIntervalMillis = intervalMillis;
mSystemRate = getRefreshRate(context);
mFpsMonitor = new FpsMonitor();
mCompositeDisposable = new CompositeDisposable();
}
@Override
public void work() {
ThreadUtil.ensureMainThread("FpsEngine work");
mFpsMonitor.start();
mCompositeDisposable.add(Observable.interval(mIntervalMillis, TimeUnit.MILLISECONDS)
.observeOn(ThreadUtil.computationScheduler())
.subscribeOn(ThreadUtil.computationScheduler())
.subscribe(aLong -> {
ThreadUtil.ensureWorkThread("FpsEngine accept");
if (!AndroidDebug.isDebugging()) {// if debugging, then ignore
int fps = mFpsMonitor.exportThenReset();
mProducer.produce(new FpsInfo(fps, mSystemRate));
}
}));
}
}
2、采集Fps信息
Fps采集FpsInfo
上报2个值
- 当前正在运行的fps(计算出来的)
- 系统的fps(通过Api获取)
当前正在运行的fps(计算出来的)
当前正在运行的fps主要是通过FpsMonitor
来监控当前的值
public class FpsMonitor implements Choreographer.FrameCallback {
private Choreographer mChoreographer;
private int mCurrentFrameCount;
private long mStartFrameTimeNanos;
private long mCurrentFrameTimeNanos;
@UiThread
public void start() {
mChoreographer = ChoreographerInjecor.getChoreographerProvider().getChoreographer();
mCurrentFrameCount = 0;
mCurrentFrameTimeNanos = mStartFrameTimeNanos;
mChoreographer.postFrameCallback(this);
}
@UiThread
public void stop() {
mChoreographer.removeFrameCallback(this);
mChoreographer = null;
mCurrentFrameCount = 0;
mStartFrameTimeNanos = 0;
mCurrentFrameTimeNanos = 0;
}
// callback every 16ms
@Override
public void doFrame(long frameTimeNanos) {
mCurrentFrameCount++;
if (mStartFrameTimeNanos == 0) {
mStartFrameTimeNanos = frameTimeNanos;
}
mCurrentFrameTimeNanos = frameTimeNanos;
mChoreographer.postFrameCallback(this);
}
/**
* 导出当前平均fps(从上次导出开始)
*
* @return
*/
@UiThread
int exportThenReset() {
if (mCurrentFrameCount < 1 || mCurrentFrameTimeNanos < mStartFrameTimeNanos) {
return -1;
}
double fps = (mCurrentFrameCount - 1) * 1000000000.0 / (mCurrentFrameTimeNanos - mStartFrameTimeNanos);
mStartFrameTimeNanos = 0;
mCurrentFrameTimeNanos = 0;
mCurrentFrameCount = 0;
return (int) Math.round(fps);
}
}
通过mFpsMonitor.start()
启动Choreographer
对象的postFrameCallback
,其代码逻辑
@UiThread
public void start() {
mChoreographer = ChoreographerInjecor.getChoreographerProvider().getChoreographer();
......
mChoreographer.postFrameCallback(this);
}
// callback every 16ms
@Override
public void doFrame(long frameTimeNanos) {
......
mChoreographer.postFrameCallback(this);
}
-
mChoreographer.postFrameCallback(this)
增加FrameCallback
到系统的Choreographer
对象 - 当系统的
Vsync 信号
下发时,会回调新增的FrameCallback
的doFrame(long frameTimeNanos)
,参数frameTimeNanos
表示当前的帧的时间戳,单位是毫微秒,换算成秒公式为1秒=1000*1000*1000=1000000000毫微秒
- 在回调调用户新增的
FrameCallback
的doFrame(long frameTimeNanos)
时,重新mChoreographer.postFrameCallback(this)
,如此循环让系统一直回调当前的帧时间
当前的逻辑从mChoreographer.postFrameCallback(this)
->doFrame(long frameTimeNanos)
->mChoreographer.postFrameCallback(this)
过程中,由于我们采用了定时器(默认2s)调用exportThenReset()
计算fps,那么在这个过程中,2s时间内我们做了哪些事情,注意是在我们定时器的2s时间内
// callback every 16ms
@Override
public void doFrame(long frameTimeNanos) {
mCurrentFrameCount++;
if (mStartFrameTimeNanos == 0) {
mStartFrameTimeNanos = frameTimeNanos;
}
mCurrentFrameTimeNanos = frameTimeNanos;
mChoreographer.postFrameCallback(this);
}
- 在每帧16ms的回调中,将帧数累加
- 在每帧16ms的回调中,标记开始帧时间(只标记一次)
- 在每帧16ms的回调中,标记结束帧时间(每次都标记)
当定时器2s触发exportThenReset()
时,其计算公式如下,由于fps概念=帧每秒,相当于需要将毫微秒进行单位转换,公式为帧数/s = 帧数*1000000000.0/毫微秒
@UiThread
int exportThenReset() {
......
double fps = (mCurrentFrameCount - 1) * 1000000000.0 / (mCurrentFrameTimeNanos - mStartFrameTimeNanos);
......
return (int) Math.round(fps);
}
这里要注意的点
- 采集时长不得小于16ms,如果小于
Vsync 信号
时长,那么每次采集到都不到1帧数据,数据样本为0 - 采集时长默认2s左右,这个值因人而异,如果不卡顿的情况下fps计算出来的值都在60,那么在卡顿的时候,这里就相当于2s内的卡顿平均,达不到实效性
- 极端情况,在采集的2s时间段内,前面8帧1.28s正常不发生卡顿,后面发生了1.5s的卡顿,那么就存在
1.5-(2-1.28)=0.78s
被平均到下一帧的fps上 - 总而言之,这个值不宜过长也不宜过短
系统的fps(通过Api获取)
系统的fps获取简单,代码如下
private static int getRefreshRate(Context context) {
Display display = ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
return Math.round(display.getRefreshRate());
}
3、总结
Fps的获取方式有很多种方案,目前源码的这种方法缺乏实效性,在业务量巨大的应用下,需要实时性的fps才比较适用,但作者这种实现方式也是其中的一种方案,值得借鉴。代码看起来都挺少,初次理解可能会比较困难,但是理解之后,就比较简单了
2.4 Traffic
Traffic指的是流量的获取,主要是通过系统提供的TrafficStats
获取
源码分析
1、启动Traffic的监控
Traffic的监控通过启动定时器,按xml配置的时间进行采集,通过create()
获取Traffic信息
public class TrafficEngine {
private Producer<TrafficInfo> mProducer;
private long mIntervalMillis;
private long mSampleMillis;
private CompositeDisposable mCompositeDisposable;
public TrafficEngine(Producer<TrafficInfo> producer, long intervalMillis, long sampleMillis) {
mProducer = producer;
mIntervalMillis = intervalMillis;
mSampleMillis = sampleMillis;
mCompositeDisposable = new CompositeDisposable();
}
public void work() {
mCompositeDisposable.add(Observable.interval(mIntervalMillis, TimeUnit.MILLISECONDS)
.subscribeOn(ThreadUtil.computationScheduler())
.observeOn(ThreadUtil.computationScheduler())
.concatMap(new Function<Long, ObservableSource<TrafficInfo>>() {
@Override
public ObservableSource<TrafficInfo> apply(Long aLong) throws Exception {
ThreadUtil.ensureWorkThread("TrafficEngine apply");
return create();
}
}).subscribe(new Consumer<TrafficInfo>() {
@Override
public void accept(TrafficInfo food) throws Exception {
ThreadUtil.ensureWorkThread("TrafficEngine accept");
mProducer.produce(food);
}
}));
}
public Observable<TrafficInfo> create() {
final TrafficSnapshot start = TrafficSnapshot.snapshot();
return Observable.timer(mSampleMillis, TimeUnit.MILLISECONDS).map(new Function<Long, TrafficInfo>() {
@Override
public TrafficInfo apply(Long aLong) throws Exception {
TrafficSnapshot endTrafficSnapshot = TrafficSnapshot.snapshot();
TrafficInfo trafficInfo = new TrafficInfo();
trafficInfo.rxTotalRate = (endTrafficSnapshot.rxTotalKB - start.rxTotalKB) * 1000 / mSampleMillis;
trafficInfo.txTotalRate = (endTrafficSnapshot.txTotalKB - start.txTotalKB) * 1000 / mSampleMillis;
trafficInfo.rxUidRate = (endTrafficSnapshot.rxUidKB - start.rxUidKB) * 1000 / mSampleMillis;
trafficInfo.txUidRate = (endTrafficSnapshot.txUidKB - start.txUidKB) * 1000 / mSampleMillis;
return trafficInfo;
}
});
}
}
2、采集Traffic信息
通过create()
获取指定采集时间内mSampleMillis
,前后两次的流量值的均值,最终获取的流量单位为kb/s
public Observable<TrafficInfo> create() {
final TrafficSnapshot start = TrafficSnapshot.snapshot();
return Observable.timer(mSampleMillis, TimeUnit.MILLISECONDS).map(new Function<Long, TrafficInfo>() {
@Override
public TrafficInfo apply(Long aLong) throws Exception {
TrafficSnapshot endTrafficSnapshot = TrafficSnapshot.snapshot();
TrafficInfo trafficInfo = new TrafficInfo();
trafficInfo.rxTotalRate = (endTrafficSnapshot.rxTotalKB - start.rxTotalKB) * 1000 / mSampleMillis;
trafficInfo.txTotalRate = (endTrafficSnapshot.txTotalKB - start.txTotalKB) * 1000 / mSampleMillis;
trafficInfo.rxUidRate = (endTrafficSnapshot.rxUidKB - start.rxUidKB) * 1000 / mSampleMillis;
trafficInfo.txUidRate = (endTrafficSnapshot.txUidKB - start.txUidKB) * 1000 / mSampleMillis;
return trafficInfo;
}
});
}
流量快照信息通过TrafficStats
系统提供的Api直接获取
/**
* 流量快照
* 还有其他一些比如区分Wi-Fi和移动流量的数据没有包含进来
* 单位kb
* Created by kysonchao on 2017/5/22.
*/
@Keep
public class TrafficSnapshot implements Serializable {
//下行总字节数
public float rxTotalKB;
//上行总字节数
public float txTotalKB;
//下行应用总字节数
public float rxUidKB;
//上行应用总字节数
public float txUidKB;
@WorkerThread
public static TrafficSnapshot snapshot() {
TrafficSnapshot snapshot = new TrafficSnapshot();
snapshot.rxTotalKB = TrafficStats.getTotalRxBytes() / 1024f;
snapshot.txTotalKB = TrafficStats.getTotalTxBytes() / 1024f;
snapshot.rxUidKB = TrafficStats.getUidRxBytes(android.os.Process.myUid()) / 1024f;
snapshot.txUidKB = TrafficStats.getUidTxBytes(android.os.Process.myUid()) / 1024f;
return snapshot;
}
}
这里要注意的是
- 定时器默认是2s采集一次,而流量快照默认是1s输出一次,两者都需要时间采集
- 如果要让流量正常输出,那么就必须遵从,定时器的时长>流量快照输出的时长
3、总结
流量的采集比较简单,通过测试后,系统的api返回的数值准确性可以保证
2.5 StartUp
StartUp指的是获取系统启动首页的时间
源码分析
StartUp指的是启动速度,侵入性较大,需要在启动的Activity上增加代码,作者通过例子也告诉我们大概的计算过程
1、启动StartUp的监控
StartUp的启动并没有做任何事情,只是初始化变量,通过StartupInfo
发送出去
public class Startup extends ProduceableSubject<StartupInfo> implements Install<StartupConfig> {
private StartupConfig mConfig;
@Override
public synchronized boolean install(StartupConfig config) {
if (config == null) {
throw new IllegalArgumentException("Startup module install fail because config is null.");
}
if (mConfig != null) {
L.d("Startup already installed, ignore.");
return true;
}
mConfig = config;
L.d("Startup installed.");
return true;
}
@Override
public synchronized void uninstall() {
if (mConfig == null) {
L.d("Startup already uninstalled, ignore.");
return;
}
mConfig = null;
L.d("Startup uninstall.");
}
@Override
public synchronized boolean isInstalled() {
return mConfig != null;
}
@Override
public StartupConfig config() {
return mConfig;
}
@Override
public void produce(StartupInfo data) {
if (mConfig == null) {
L.d("Startup is not installed, produce data fail.");
return;
}
super.produce(data);
}
@Override
protected Subject<StartupInfo> createSubject() {
return BehaviorSubject.create();
}
}
StartupInfo
表示启动速度的信息,其中startupType
可以表示冷启动和热启动,而startupTime
表示启动时间
public class StartupInfo implements Serializable {
@Retention(RetentionPolicy.SOURCE)
@StringDef({StartUpType.COLD, StartUpType.HOT})
public @interface StartUpType {
public static final String COLD = "cold";
public static final String HOT = "hot";
}
public @StartUpType
String startupType;
public long startupTime;
public StartupInfo(@StartUpType String startupType, long startupTime) {
this.startupType = startupType;
this.startupTime = startupTime;
}
@Override
public String toString() {
return "StartupInfo{" +
"startupType='" + startupType + '\'' +
", startupTime=" + startupTime +
'}';
}
}
2、采集StartUp信息
在作者的例子中发现整个采集的调用链
- 在Application上打点
StartupTracer.get().onApplicationCreate();
public class SampleApp extends Application {
@Override
public void onCreate() {
super.onCreate();
StartupTracer.get().onApplicationCreate();
......
}
}
- 在闪屏SplashActivity上打点
StartupTracer.get().onSplashCreate();
public class SplashActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_splash);
StartupTracer.get().onSplashCreate();
}
}
- 在首页Main2Activity进行统计
StartupTracer.get().onHomeCreate(this);
public class Main2Activity extends AppCompatActivity implements InstallFragment.OnInstallModuleChangeListener {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main2);
......
StartupTracer.get().onHomeCreate(this);
}
}
整个调用链都通过工具类StartupTracer
,用来记录并统计启动的过程,这里的统计涉及到两个启动类型
- 热启动:从Application->Splash->Main的过程,耗时=Main-Application
- 冷启动:从Splash->Main的过程,耗时=Main-Splash
public class StartupTracer {
public interface OnStartupEndCallback {
public void onStartupEnd();
}
private long mApplicationStartTime;
private long mSplashStartTime;
private StartupTracer() {
}
private static class InstanceHolder {
private static final StartupTracer sInstance = new StartupTracer();
}
public static StartupTracer get() {
return InstanceHolder.sInstance;
}
public void onApplicationCreate() {
mApplicationStartTime = System.currentTimeMillis();
}
public void onSplashCreate() {
mSplashStartTime = System.currentTimeMillis();
}
public void onHomeCreate(Activity activity) {
if (Looper.myLooper() != Looper.getMainLooper()) {
throw new IllegalStateException("call onHomeCreate ui thread!");
}
activity.getWindow().getDecorView().post(() -> new Handler().post(() -> {
try {
GodEyeHelper.onAppStartEnd(generateStartupInfo(System.currentTimeMillis()));
} catch (UninstallException e) {
e.printStackTrace();
}
}));
}
private StartupInfo generateStartupInfo(long homeEndTime) {
if (isCodeStart(homeEndTime)) {
return new StartupInfo(StartupInfo.StartUpType.COLD, homeEndTime - mApplicationStartTime);
} else if (isHotStart(homeEndTime)) {
return new StartupInfo(StartupInfo.StartUpType.HOT, homeEndTime - mSplashStartTime);
} else {
return null;
}
}
private boolean isCodeStart(long homeEndTime) {
return mApplicationStartTime > 0 && homeEndTime > mSplashStartTime && mSplashStartTime > mApplicationStartTime;
}
private boolean isHotStart(long homeEndTime) {
return mApplicationStartTime <= 0 && mSplashStartTime > 0 && homeEndTime > mSplashStartTime;
}
}
3、总结
启动耗时的统计对代码侵入性较大,可以考虑配置的方式,监听Activity生命周期,通过指定的Activity回调作为统计耗时的起点和终点
2.6 Thread
Thread指的是获取当前App上所有正在执行的线程,且线程本身有Api可以获取当前线程的堆栈
源码分析
1、启动Thread的监控
Thread的监控通过启动定时器,按xml配置的时间进行采集,通过dump()
获取Thread信息
class ThreadEngine implements Engine {
private Producer<List<ThreadInfo>> mProducer;
private long mIntervalMillis;
private CompositeDisposable mCompositeDisposable;
private @NonNull
ThreadFilter mThreadFilter;
private @NonNull
ThreadTagger mThreadTagger;
ThreadEngine(Producer<List<ThreadInfo>> producer, ThreadConfig config) {
ThreadFilter threadFilter = new ExcludeSystemThreadFilter();
try {
threadFilter = (ThreadFilter) Class.forName(config.threadFilter).newInstance();
} catch (Throwable e) {
L.e(String.format("Thread install warning, can not find ThreadFilter class %s, use ExcludeSystemThreadFilter, error: %s", config.threadFilter, e));
}
ThreadTagger threadTagger = new DefaultThreadTagger();
try {
threadTagger = (ThreadTagger) Class.forName(config.threadTagger).newInstance();
} catch (Throwable e) {
L.e(String.format("Thread install warning, can not find ThreadTagger class %s, use DefaultThreadTagger, error: %s", config.threadTagger, e));
}
mProducer = producer;
mIntervalMillis = config.intervalMillis;
mThreadFilter = threadFilter;
mThreadTagger = threadTagger;
mCompositeDisposable = new CompositeDisposable();
}
@Override
public void work() {
mCompositeDisposable.add(Observable.interval(mIntervalMillis, TimeUnit.MILLISECONDS).map(aLong -> {
ThreadUtil.ensureWorkThread("ThreadEngine apply");
return dump(mThreadFilter, mThreadTagger);
}).subscribeOn(ThreadUtil.computationScheduler())
.observeOn(ThreadUtil.computationScheduler())
.subscribe(food -> {
ThreadUtil.ensureWorkThread("ThreadEngine accept");
mProducer.produce(food);
}));
}
}
2、采集Thread信息
获取线程信息
- 通过获取当前线程的线程组
- 获取线程组的最*的线程组
- 复制当前最*的线程组到数组中
- 复制中如果超过当前的数组容量,则扩容后继续复制
其中有两个参数
- threadFilter:通过xml配置实例化的类,表示想过滤的线程
- threadTagger:通过xml配置实例化的类,表示可以设置线程的tag,用作Web显示
/**
* dump当前所有线程
*
* @param threadFilter
* @return
*/
@VisibleForTesting
static List<ThreadInfo> dump(@NonNull ThreadFilter threadFilter, @NonNull ThreadTagger threadTagger) {
//1、通过获取当前线程的线程组
ThreadGroup rootGroup = Thread.currentThread().getThreadGroup();
if (rootGroup == null) {
return new ArrayList<>();
}
ThreadGroup parentGroup;
//2、获取线程组的最*的线程组
while ((parentGroup = rootGroup.getParent()) != null) {
rootGroup = parentGroup;
}
Thread[] threads = new Thread[rootGroup.activeCount()];
//3、复制当前最*的线程组到数组中
while (rootGroup.enumerate(threads, true) >= threads.length) {
//4、复制中如果超过当前的数组容量,则扩容后继续复制
threads = new Thread[threads.length * 2];
}
List<ThreadInfo> threadList = new ArrayList<>();
Set<Long> threadIds = new HashSet<>();
for (Thread thread : threads) {
if (thread != null && !threadIds.contains(thread.getId()) && threadFilter.filter(thread)) {
threadIds.add(thread.getId());
ThreadInfo threadInfo = new ThreadInfo(thread);
threadInfo.threadTag = threadTagger.tag(thread, threadInfo);
threadList.add(threadInfo);
}
}
return threadList;
}
在ThreadInfo
构造函数中,会获取当前的线程的堆栈信息thread.getStackTrace()
public ThreadInfo(Thread thread) {
this.id = thread.getId();
this.name = thread.getName();
this.state = String.valueOf(thread.getState());
this.deamon = thread.isDaemon();
this.priority = thread.getPriority();
this.isAlive = thread.isAlive();
this.isInterrupted = thread.isInterrupted();
this.stackTraceElements = StacktraceUtil.getStackTraceOfThread(thread);
this.parent = new ThreadGroup(thread.getThreadGroup() == null ? "" : thread.getThreadGroup().getName());
}
public static List<String> getStackTraceOfThread(Thread thread) {
StackTraceElement[] stackTraceElements = thread.getStackTrace();
return stackTraceToStringArray(Arrays.asList(stackTraceElements));
}
public static List<String> stackTraceToStringArray(List<StackTraceElement> stackTraceElements) {
List<String> stackList = new ArrayList<>();
for (StackTraceElement traceElement : stackTraceElements) {
stackList.add(String.valueOf(traceElement));
}
return stackList;
}
3、总结
线程的获取比较简单,代码量相对较少,获取的过程中有涉及到获取堆栈信息,对性能会有些消耗
本文地址:https://blog.csdn.net/qq_30379689/article/details/111870007
上一篇: 荐 CSS入门小结