开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第3天,点击查看活动详情 上一篇我们分析了hook系统服务来监控信息的Feature,接下来我们看看其他Feature是怎么获取的
TrafficMonitorFeature
网络消耗统计 代码非常简单
public final class TrafficMonitorFeature extends AbsMonitorFeature {
private static final String TAG = "Matrix.battery.TrafficMonitorFeature";
@Override
protected String getTag() {
return TAG;
}
@Override
public int weight() {
return Integer.MIN_VALUE;
}
@Nullable
public TrafficMonitorFeature.RadioStatSnapshot currentRadioSnapshot(Context context) {
RadioStatUtil.RadioStat stat = RadioStatUtil.getCurrentStat(context);
if (stat == null) {
return null;
}
TrafficMonitorFeature.RadioStatSnapshot snapshot = new TrafficMonitorFeature.RadioStatSnapshot();
snapshot.wifiRxBytes = Snapshot.Entry.DigitEntry.of(stat.wifiRxBytes);
snapshot.wifiTxBytes = Snapshot.Entry.DigitEntry.of(stat.wifiTxBytes);
snapshot.mobileRxBytes = Snapshot.Entry.DigitEntry.of(stat.mobileRxBytes);
snapshot.mobileTxBytes = Snapshot.Entry.DigitEntry.of(stat.mobileTxBytes);
return snapshot;
}
调用系统API获取流量消耗数据
public static RadioStat getCurrentStat(Context context) {
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.M) {
return null;
}
if (checkIfFrequently()) {
MatrixLog.i(TAG, "over frequently just return");
return null;
}
try {
NetworkStatsManager network = (NetworkStatsManager) context.getSystemService(Context.NETWORK_STATS_SERVICE);
if (network == null) {
return null;
}
RadioStat stat = new RadioStat();
try (NetworkStats stats = network.querySummary(NetworkCapabilities.TRANSPORT_WIFI, null, 0, System.currentTimeMillis())) {
while (stats.hasNextBucket()) {
NetworkStats.Bucket bucket = new NetworkStats.Bucket();
if (stats.getNextBucket(bucket)) {
if (bucket.getUid() == android.os.Process.myUid()) {
stat.wifiRxBytes += bucket.getRxBytes();
stat.wifiTxBytes += bucket.getTxBytes();
}
}
}
}
try (NetworkStats stats = network.querySummary(NetworkCapabilities.TRANSPORT_CELLULAR, null, 0, System.currentTimeMillis())) {
while (stats.hasNextBucket()) {
NetworkStats.Bucket bucket = new NetworkStats.Bucket();
if (stats.getNextBucket(bucket)) {
if (bucket.getUid() == android.os.Process.myUid()) {
stat.mobileRxBytes += bucket.getRxBytes();
stat.mobileTxBytes += bucket.getTxBytes();
}
}
}
}
return stat;
} catch (Throwable e) {
MatrixLog.w(TAG, "querySummary fail: " + e.getMessage());
return null;
}
}
LooperTaskMonitorFeature
监控Looper的使用信息
应用进入后台后会延迟启动监控startWatching
@Override
public void onForeground(boolean isForeground) {
super.onForeground(isForeground);
if (isForeground) {
if (mDelayWatchingTask != null) {
mCore.getHandler().removeCallbacks(mDelayWatchingTask);
}
} else {
mDelayWatchingTask = new Runnable() {
@Override
public void run() {
startWatching();
}
};
mCore.getHandler().postDelayed(mDelayWatchingTask, mCore.getConfig().greyTime);
}
}
把需要监控的looper加入Map集合,默认全部,当然也支持白名单配置
void startWatching() {
synchronized (mWatchingList) {
if (mLooperTaskListener == null) {
return;
}
MatrixLog.i(TAG, "#startWatching");
if (mCore.getConfig().looperWatchList.contains("all")) {
// 1. Update watching for all handler threads
Collection<Thread> allThreads = getAllThreads();
for (Thread thread : allThreads) {
if (thread instanceof HandlerThread) {
// update watch for handler thread
Looper looper = ((HandlerThread) thread).getLooper();
if (looper != null) {
if (!mLooperMonitorTrace.containsKey(looper)) {
watchLooper((HandlerThread) thread);
}
}
} else if (Looper.getMainLooper().getThread() == thread) {
// update watch for main thread
if (!mLooperMonitorTrace.containsKey(Looper.getMainLooper())) {
watchLooper("main", Looper.getMainLooper());
}
}
}
} else {
// 2. Update watching for configured threads
Collection<Thread> allThreads = Collections.emptyList();
for (String threadToWatch : mCore.getConfig().looperWatchList) {
if (TextUtils.isEmpty(threadToWatch)) {
continue;
}
if ("main".equalsIgnoreCase(threadToWatch)) {
// update watch for main thread
Looper mainLooper = Looper.getMainLooper();
if (!mLooperMonitorTrace.containsKey(mainLooper)) {
watchLooper("main", mainLooper);
}
continue;
}
if (!mWatchingList.contains(threadToWatch)) {
if (allThreads.isEmpty()) {
// for lazy load
allThreads = getAllThreads();
}
for (Thread thread : allThreads) {
if (Looper.getMainLooper().getThread() == thread) {
continue;
}
if (thread.getName().contains(threadToWatch)) {
// update watch for configured thread
if (thread instanceof HandlerThread) {
Looper looper = ((HandlerThread) thread).getLooper();
if (looper != null) {
if (!mLooperMonitorTrace.containsKey(looper)) {
watchLooper(thread.getName(), looper);
}
}
}
}
}
}
}
}
}
}
加入Map并增加looper监听
这里的核心在于调用了系统addListener方法,不用反射我么也可以实现监控
LooperMonitor looperMonitor = LooperMonitor.of(looper);
looperMonitor.addListener(mLooperTaskListener);
void watchLooper(String name, Looper looper) {
if (TextUtils.isEmpty(name) || looper == null) {
return;
}
synchronized (mWatchingList) {
if (mLooperTaskListener != null) {
// remove if existing
mWatchingList.remove(name);
LooperMonitor remove = mLooperMonitorTrace.remove(looper);
if (remove != null) {
remove.onRelease();
}
// add looper tracing
LooperMonitor looperMonitor = LooperMonitor.of(looper);
looperMonitor.addListener(mLooperTaskListener);
mWatchingList.add(name);
mLooperMonitorTrace.put(looper, looperMonitor);
}
}
}
接下来看看mLooperTaskListener做了什么
onTurnOn中我们初始化
onDispatchStart onDispatchEnd监控消息的开始执行和结束,监控这两个点可以达到监控的目的
@Override
public void onTurnOn() {
super.onTurnOn();
mLooperTaskListener = new LooperMonitor.LooperDispatchListener() {
@Override
public boolean isValid() {
return mCore.isTurnOn();
}
@Override
public void onDispatchStart(String x) {
super.onDispatchStart(x);
if (mCore.getConfig().isAggressiveMode) {
MatrixLog.i(TAG, "[" + Thread.currentThread().getName() + "]" + x);
}
String taskName = computeTaskName(x);
if (!TextUtils.isEmpty(taskName)) {
int hashcode = computeHashcode(x);
if (hashcode > 0) {
onTaskStarted(taskName, hashcode);
}
}
}
@Override
public void onDispatchEnd(String x) {
super.onDispatchEnd(x);
if (mCore.getConfig().isAggressiveMode) {
MatrixLog.i(TAG, "[" + Thread.currentThread().getName() + "]" + x);
}
String taskName = computeTaskName(x);
if (!TextUtils.isEmpty(taskName)) {
int hashcode = computeHashcode(x);
if (hashcode > 0) {
onTaskFinished(taskName, hashcode);
}
}
}
// Samples:
// >>>>> Dispatching to Handler (android.os.Handler) {5774ba9} null: 22
// <<<<< Finished to Handler (android.os.Handler) {5774ba9} null
// >>>>> Dispatching to Handler (android.os.Handler) {5774ba9} null: 33
// <<<<< Finished to Handler (android.os.Handler) {5774ba9} null
// >>>>> Dispatching to Handler (android.os.Handler) {5774ba9} com.tencent.matrix.batterycanary.monitor.feature.MonitorFeatureLooperTest$6$1@a8ee52e: 0
// <<<<< Finished to Handler (android.os.Handler) {5774ba9} com.tencent.matrix.batterycanary.monitor.feature.MonitorFeatureLooperTest$6$1@a8ee52e
private String computeTaskName(String rawInput) {
if (TextUtils.isEmpty(rawInput)) return null;
String symbolBgn = "} ";
String symbolEnd = "@";
int indexBgn = rawInput.indexOf(symbolBgn);
int indexEnd = rawInput.lastIndexOf(symbolEnd);
if (indexBgn >= indexEnd - 1) return null;
return rawInput.substring(indexBgn + symbolBgn.length(), indexEnd);
}
private int computeHashcode(String rawInput) {
if (TextUtils.isEmpty(rawInput)) return -1;
String symbolBgn = "@";
String symbolEnd = ": ";
int indexBgn = rawInput.indexOf(symbolBgn);
int indexEnd = rawInput.contains(symbolEnd) ? rawInput.lastIndexOf(symbolEnd) : Integer.MAX_VALUE;
if (indexBgn >= indexEnd - 1) return -1;
String hexString = indexEnd == Integer.MAX_VALUE ? rawInput.substring(indexBgn + symbolBgn.length()) : rawInput.substring(indexBgn + symbolBgn.length(), indexEnd);
try {
return Integer.parseInt(hexString, 16);
} catch (NumberFormatException ignored) {
return -1;
}
}
};
}
先看看onDispatchStart 生成标识调用onTaskStarted
@WorkerThread
@Override
protected void onTaskStarted(final String key, final int hashcode) {
// Trace task jiffies 生成快照信息
final TaskJiffiesSnapshot bgn = createSnapshot(key, Process.myTid());
if (bgn != null) {
mTaskJiffiesTrace.put(hashcode, bgn);
// Update task stamp list 更新全局变量
onStatTask(Process.myTid(), key, bgn.jiffies.get());
}
}
存入抽象类的全局变量,表示这一阶段的数据信息,存满的时候会减半
protected void onStatTask(int tid, @NonNull String taskName, long currJiffies) {
synchronized (mTaskStampList) {
List<TimeBreaker.Stamp> stampList = mTaskStampList.get(tid);
if (stampList == null) {
stampList = new ArrayList<>();
stampList.add(0, mFirstTaskStamp);
mTaskStampList.put(tid, stampList);
}
stampList.add(0, new TimeBreaker.Stamp(taskName, currJiffies));
}
//过载减半
checkOverHeat();
}
再来看看onDispatchEnd 最终走到onTaskFinished
protected void onTaskFinished(final String key, final int hashcode) {
//通过每条消息的唯一code正常应该查出开始分发的消息
final TaskJiffiesSnapshot bgn = mTaskJiffiesTrace.remove(hashcode);
if (bgn != null) {
//有了开始说明才有结束,生成快照信息
TaskJiffiesSnapshot end = createSnapshot(key, Process.myTid());
if (end != null) {
end.isFinished = true;
//更新增量数据
updateDeltas(bgn, end);
}
// Update task stamp list
onStatTask(Process.myTid(), IDLE_TASK,
end == null ? bgn.jiffies.get() : end.jiffies.get());
}
}
也是在抽象类AbsTaskMonitorFeature记录了全局信息,这样Looper的统计就收集完成了
JiffiesMonitorFeature
主要用来记录线程信息
判断一个线程功耗是否出现异常需要先了解一下 Linux Jiffy 的概念,忘了的同学可以先复习一原凯的内容节课的内容: SQLiteLint耗电分析1之统计原理。我们通过线程的 Jiffiy 消耗以及线程的运行状态,就可以推断出当前线程在这段时间内有没有异常。
Delta<JiffiesSnapshot> delta = compositor.getDelta(JiffiesSnapshot.class);
if (delta != null) {
long windowMillis = delta.during; // 时间窗口
for (ThreadJiffiesEntry threadEntry : delta.dlt.threadEntries.getList()) {
String name = threadEntry.name; // 线程名
int tid = threadEntry.tid; // tid
String status = threadEntry.stat; // 线程状态
long jiffies = threadEntry.get(); // 线程在这段时间内的 Jif
...
}
}
一般来说,当线程的 Jiffy 开销约等于 6000 jiffies / 分钟的时候,就说线程在这段时间内一直在吃 CPU 资源(CPU 中断),换算下来线程的 CPU Load 约为 100%(App 的 CPU Load 为所有线程的 CPU Load 累加,具体值的区间在 [0, 100 × CPU Core Num])。当 App 在后台的时候,线程持续这种状态超过 10 分钟以上,就说明已经出现待机功耗异常;当时间超过 30min 甚至 1 hour 以上,就可以在系统电量统计排行上面看到明显的电量曲线的变化。
以上线程异常的规则定义可以根据自身 App 需要灵活定义,不过我们实践发现,如果长时间的后台线程 Jiffies 推算出来的 CPU Load 一直在 100% ,并且该线程没有明显的 Log 输入,则说明当前线程很可能出现了死循环(一般是 Bug 导致,而且陈年老代码里更容易出现);而如果有大量相关的 Log 输出,则说明该线程相关的业务功能在 App 进入待机状态后,可能漏了执行相应的退出和析构逻辑(一般在新开发而没经过测试 & 先上考验的新代码里容易出现)。
代码的核心在于mFgThreadWatchDog mBgThreadWatchDog 线程
public final class JiffiesMonitorFeature extends AbsMonitorFeature {
private static final String TAG = "Matrix.battery.JiffiesMonitorFeature";
public interface JiffiesListener {
@Deprecated
void onParseError(int pid, int tid);
void onWatchingThreads(ListEntry<? extends JiffiesSnapshot.ThreadJiffiesEntry> threadJiffiesList);
}
private final ThreadWatchDog mFgThreadWatchDog = new ThreadWatchDog();
private final ThreadWatchDog mBgThreadWatchDog = new ThreadWatchDog();
@Override
protected String getTag() {
return TAG;
}
@Override
public int weight() {
return Integer.MAX_VALUE;
}
@Override
public void onForeground(boolean isForeground) {
super.onForeground(isForeground);
if (isForeground) {
mFgThreadWatchDog.start();
mBgThreadWatchDog.stop();
} else {
mBgThreadWatchDog.start();
mFgThreadWatchDog.stop();
}
}
我们进入线程看看
class ThreadWatchDog implements Runnable {
private long duringMillis;
private final List<ProcessInfo.ThreadInfo> mWatchingThreads = new ArrayList<>();
@Override
public void run() {
// 定时监控线程信息
MatrixLog.i(TAG, "threadWatchDog start, size = " + mWatchingThreads.size()
+ ", delayMillis = " + duringMillis);
List<JiffiesSnapshot.ThreadJiffiesSnapshot> threadJiffiesList = new ArrayList<>();
synchronized (mWatchingThreads) {
for (ProcessInfo.ThreadInfo item : mWatchingThreads) {
JiffiesSnapshot.ThreadJiffiesSnapshot snapshot = JiffiesSnapshot.ThreadJiffiesSnapshot.parseThreadJiffies(item);
if (snapshot != null) {
snapshot.isNewAdded = false;
threadJiffiesList.add(snapshot);
}
}
}
if (!threadJiffiesList.isEmpty()) {
ListEntry<JiffiesSnapshot.ThreadJiffiesSnapshot> threadJiffiesListEntry = ListEntry.of(threadJiffiesList);
mCore.getConfig().callback.onWatchingThreads(threadJiffiesListEntry);
}
// next loop
if (duringMillis <= 5 * 60 * 1000L) {
mCore.getHandler().postDelayed(this, setNext(5 * 60 * 1000L));
} else if (duringMillis <= 10 * 60 * 1000L) {
mCore.getHandler().postDelayed(this, setNext(10 * 60 * 1000L));
} else {
// done
synchronized (mWatchingThreads) {
mWatchingThreads.clear();
}
}
}
void watch(int pid, int tid) {
synchronized (mWatchingThreads) {
// Distinct
for (ProcessInfo.ThreadInfo item : mWatchingThreads) {
if (item.pid == pid && item.tid == tid) {
return;
}
}
mWatchingThreads.add(ProcessInfo.ThreadInfo.of(pid, tid));
}
}
void start() {
synchronized (mWatchingThreads) {
MatrixLog.i(TAG, "ThreadWatchDog start watching, count = " + mWatchingThreads.size());
if (!mWatchingThreads.isEmpty()) {
mCore.getHandler().postDelayed(this, reset());
}
}
}
void stop() {
mCore.getHandler().removeCallbacks(this);
}
private long reset() {
duringMillis = 0L;
setNext(5 * 60 * 1000L);
return duringMillis;
}
private long setNext(long millis) {
duringMillis += millis;
return millis;
}
}
那么mWatchingThreads哪来的呢?哪些需要被监控?
看下watchBackThreadSate调用处 备注说的很清楚
// Watching thread state when thread is:
// 1. still running (status 'R')
// 2. runing time > 10min
// 3. avgJiffies > THRESHOLD
protected void checkBadThreads(final CompositeMonitors monitors) {
monitors.getDelta(JiffiesSnapshot.class, new Consumer<Delta<JiffiesSnapshot>>() {
@Override
public void accept(final Delta<JiffiesSnapshot> delta) {
monitors.getAppStats(new Consumer<AppStats>() {
@Override
public void accept(final AppStats appStats) {
final long minute = appStats.getMinute();
for (final ThreadJiffiesEntry threadJiffies : delta.dlt.threadEntries.getList()) {
if (!threadJiffies.stat.toUpperCase().contains("R")) {
continue;
}
monitors.getFeature(JiffiesMonitorFeature.class, new Consumer<JiffiesMonitorFeature>() {
@Override
public void accept(JiffiesMonitorFeature feature) {
// Watching thread state when thread is:
// 1. still running (status 'R')
// 2. runing time > 10min
// 3. avgJiffies > THRESHOLD
long avgJiffies = threadJiffies.get() / minute;
if (appStats.isForeground()) {
if (minute > 10 && avgJiffies > getMonitor().getConfig().fgThreadWatchingLimit) {
MatrixLog.i(TAG, "threadWatchDog fg set, name = " + delta.dlt.name
+ ", pid = " + delta.dlt.pid
+ ", tid = " + threadJiffies.tid);
feature.watchBackThreadSate(true, delta.dlt.pid, threadJiffies.tid);
}
} else {
if (minute > 10 && avgJiffies > getMonitor().getConfig().bgThreadWatchingLimit) {
MatrixLog.i(TAG, "threadWatchDog bg set, name = " + delta.dlt.name
+ ", pid = " + delta.dlt.pid
+ ", tid = " + threadJiffies.tid);
feature.watchBackThreadSate(false, delta.dlt.pid, threadJiffies.tid);
}
}
}
});
}
}
});
}
});
}
具体的细节大家可以深入看看
InternalMonitorFeature
当前Process.myTid() 设备状态记录
@Override
public void onTurnOn() {
super.onTurnOn();
mCore.getHandler().post(new Runnable() {
@Override
public void run() {
mWorkerTid = Process.myTid();
}
});
}
@Nullable
protected InternalSnapshot createSnapshot(int tid) {
InternalSnapshot snapshot = new InternalSnapshot();
snapshot.tid = tid;
snapshot.appStat = BatteryCanaryUtil.getAppStat(mCore.getContext(), mCore.isForeground());
snapshot.devStat = BatteryCanaryUtil.getDeviceStat(mCore.getContext());
try {
Callable<String> supplier = mCore.getConfig().onSceneSupplier;
snapshot.scene = supplier == null ? "" : supplier.call();
} catch (Exception ignored) {
snapshot.scene = "";
}
ProcStatUtil.ProcStat stat = ProcStatUtil.of(Process.myPid(), tid);
if (stat == null) {
return null;
}
snapshot.jiffies = Snapshot.Entry.DigitEntry.of(stat.getJiffies());
snapshot.name = stat.comm;
return snapshot;
}
DeviceStatMonitorFeature
上面提到,耗电异常是指某一段内发生的异常,因此耗电异常时候的 App 和 Device 状态也必须统计的是一段时间内状态,具体讲就是这端事件内状态的切换细节以及每个状态的时间除以整个时间窗口的占比。
这部分状态的统计工作主要是由 AppStatMonitor 和 DeviceStatMonitor 这两个监控模块完成的。其中 AppStatMonitor 负责监控 App 前台、后台、ForegroundService 以及浮窗 4 种状态的变化,而 DeviceStatMonitor 则负责设备充电、未充电、息屏、Doze(低电耗模式)、App Standby(应用待机模式)这 5 种状态的统计。
在启用 Device 状态监控模块的时候,可以获取设备状态的统计数据: 是否充电 是否熄屏
BatteryCanary.getMonitorFeature(DeviceStatMonitorFeature.class, feat -> {
long duringMillis = xxx; // 时间窗口
DeviceStatMonitorFeature.DevStatSnapshot stats = feat.currentDevStatSnapshot(duringMillis);
long unChargingRatio = stats.unChargingRatio.get(); // 未充电状态时间占比
long screenOff = stats.screenOff.get(); // 息屏状态时间占比
long lowEnergyRatio = stats.lowEnergyRatio.get(); // 低电耗状态时间占比
long chargingRatio = stats.chargingRatio.get(); // 充电状态时间占比
});
public class AppStats {
@IntDef(value = {
APP_STAT_FOREGROUND,
APP_STAT_FOREGROUND_SERVICE,
APP_STAT_BACKGROUND
})
@Retention(RetentionPolicy.SOURCE)
public @interface AppStatusDef {
}
@IntDef(value = {
DEV_STAT_CHARGING,
DEV_STAT_UN_CHARGING,
DEV_STAT_SCREEN_OFF,
DEV_STAT_SAVE_POWER_MODE
})
从入口开始看起,生成了DevStatListener监控类
@Override
public void configure(BatteryMonitorCore monitor) {
super.configure(monitor);
mDevStatListener = new DevStatListener();
}
监控并记录信息
@Override
public void onTurnOn() {
super.onTurnOn();
int deviceStat = BatteryCanaryUtil.getDeviceStat(mCore.getContext());
@SuppressLint("VisibleForTests") TimeBreaker.Stamp firstStamp = new TimeBreaker.Stamp(String.valueOf(deviceStat));
synchronized (TAG) {
mStampList = new ArrayList<>();
mStampList.add(0, firstStamp);
}
mDevStatListener.setListener(new Consumer<Integer>() {
@SuppressLint("VisibleForTests")
@Override
public void accept(Integer integer) {
//设备状态,记录信息
BatteryCanaryUtil.getProxy().updateDevStat(integer);
synchronized (TAG) {
if (mStampList != Collections.EMPTY_LIST) {
MatrixLog.i(BatteryEventDelegate.TAG, "onStat >> " + BatteryCanaryUtil.convertDevStat(integer));
mStampList.add(0, new TimeBreaker.Stamp(String.valueOf(integer)));
//超过容量减半
checkOverHeat();
}
}
}
});
if (!mDevStatListener.isListening()) {
mDevStatListener.startListen(mCore.getContext());
}
}
getDeviceStat获取设备是否是否充电,是否熄屏,是否是节能模式
为了方便统一管理数据,BatteryCanary 把电量问题相关的 App 状态、Device 状态、Activity 切换信息等数据都封装到 AppStats 这个类里,可以通过以下方式来获取当前时间窗口内的 AppStats:
long duringMillis = xxx; // 时间窗口
AppStats appStats = AppStats.current(duringMillis);
if (appStats.isValid) {
long minute = appStats.getMinute(); // 时间窗口(分钟)
int appStat = appStats.getAppStat(); // App 状态
long fgRatio = appStats.fgRatio.get(); // 前台时间占比
long bgRatio = appStats.bgRatio.get(); // 后台时间占比
long fgSrvRatio = appStats.fgSrvRatio.get(); // 前台服务时间占比
long floatRatio = appStats.floatRatio.get(); // 浮窗时间占比
int devStat = appStats.getDevStat(); // Device 状态
long unChargingRatio = appStats.unChargingRatio.get(); // 未充电状态时间占比
long screenOff = appStats.screenOff.get(); // 息屏状态时间占比
long lowEnergyRatio = appStats.lowEnergyRatio.get(); // 低电耗状态时间占比
long chargingRatio = appStats.chargingRatio.get(); // 充电状态时间占比
String scene = appStats.sceneTop1; // Top1 Activity
int sceneRatio = appStats.sceneTop1Ratio; // Top1 Activity 占比
}
那么如何监听的呢?进入startListen看看
public boolean startListen(Context context) {
if (!mIsListening) {
if (!BatteryEventDelegate.isInit()) {
throw new IllegalStateException("BatteryEventDelegate is not yet init!");
}
mBatterStatListener = new BatteryEventDelegate.Listener() {
@Override
public boolean onStateChanged(String event) {
switch (event) {
case Intent.ACTION_POWER_CONNECTED:
mIsCharging = true;
mListener.accept(1);
break;
case Intent.ACTION_POWER_DISCONNECTED:
mIsCharging = false;
mListener.accept(2);
break;
case Intent.ACTION_SCREEN_ON:
if (!mIsCharging) {
mListener.accept(2);
}
break;
case Intent.ACTION_SCREEN_OFF:
if (!mIsCharging) {
mListener.accept(3);
}
break;
default:
break;
}
return false;
}
@Override
public boolean onAppLowEnergy(BatteryEventDelegate.BatteryState batteryState, long backgroundMillis) {
return false;
}
};
mIsCharging = BatteryCanaryUtil.isDeviceCharging(context);
BatteryEventDelegate.getInstance().addListener(mBatterStatListener);
mIsListening = true;
}
return true;
}
接口套接口有点绕,我们耐心再往下看,进入addListener
BatteryEventDelegate.getInstance().addListener(mBatterStatListener);
很简单的一个观察者模式设计,遇到这种情况直接看mListenerList什么时候被遍历调用
public void addListener(@NonNull Listener listener) {
synchronized (mListenerList) {
if (!mListenerList.contains(listener)) {
mListenerList.add(listener);
}
}
}
遍历完成会删除,看来只分发一次,再往上走
@VisibleForTesting
void dispatchSateChangedEvent(Intent intent) {
MatrixLog.i(TAG, "onSateChanged >> " + intent.getAction());
synchronized (mListenerList) {
for (Listener item : mListenerList) {
if (item.onStateChanged(intent.getAction())) {
removeListener(item);
}
}
}
}
可以知道只在主线程处理,再往上走
private void onSateChangedEvent(final Intent intent) {
if (Looper.myLooper() == Looper.getMainLooper()) {
dispatchSateChangedEvent(intent);
} else {
mUiHandler.post(new Runnable() {
@Override
public void run() {
dispatchSateChangedEvent(intent);
}
});
}
}
现在非常清楚,其实就是借助了系统的广播实现了监控,在我们启动当前插件插件的时候就已经注册了广播
public void startListening() {
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_SCREEN_ON);
filter.addAction(Intent.ACTION_SCREEN_OFF);
filter.addAction(Intent.ACTION_POWER_CONNECTED);
filter.addAction(Intent.ACTION_POWER_DISCONNECTED);
mContext.registerReceiver(new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (action != null) {
switch (action) {
case Intent.ACTION_SCREEN_ON:
case Intent.ACTION_SCREEN_OFF:
case Intent.ACTION_POWER_CONNECTED:
case Intent.ACTION_POWER_DISCONNECTED:
onSateChangedEvent(intent);
break;
}
}
}
}, filter);
}
除了熄屏电量监控还获取了cpu情况 电池温度,细节可以看下BatteryCanaryUtil这个类,或借助系统api,或读取系统文件实现
public CpuFreqSnapshot currentCpuFreq() {
CpuFreqSnapshot snapshot = new CpuFreqSnapshot();
try {
snapshot.cpuFreqs = Snapshot.Entry.ListEntry.ofDigits(BatteryCanaryUtil.getCpuCurrentFreq());
} catch (Throwable e) {
MatrixLog.printErrStackTrace(TAG, e, "#currentCpuFreq error");
snapshot.cpuFreqs = Snapshot.Entry.ListEntry.ofDigits(new int[]{});
}
return snapshot;
}
public BatteryTmpSnapshot currentBatteryTemperature(Context context) {
BatteryTmpSnapshot snapshot = new BatteryTmpSnapshot();
snapshot.temp = Snapshot.Entry.DigitEntry.of(mCore.getCurrentBatteryTemperature(context));
return snapshot;
}
AppStatMonitorFeature
主要用于记录app前后台时间占比以及进程优先级变化情况
BatteryCanary.getMonitorFeature(AppStatMonitorFeature.class, feat -> {
long duringMillis = xxx; // 时间窗口
AppStatMonitorFeature.AppStatSnapshot stats = feat.currentAppStatSnapshot(duringMillis);
long fgRatio = stats.fgRatio.get(); // 前台时间占比
long bgRatio = stats.bgRatio.get(); // 后台时间占比
long fgSrvRatio = stats.fgSrvRatio.get(); // 前台服务时间占比
long floatRatio = stats.floatRatio.get(); // 浮窗时间占比
});
先看看前台回调
@Override
public void onForeground(boolean isForeground) {
super.onForeground(isForeground);
//更新前后台信息
int appStat = BatteryCanaryUtil.getAppStatImmediately(mCore.getContext(), isForeground);
BatteryCanaryUtil.getProxy().updateAppStat(appStat);
synchronized (TAG) {
if (mStampList != Collections.EMPTY_LIST) {
MatrixLog.i(BatteryEventDelegate.TAG, "onStat >> " + BatteryCanaryUtil.convertAppStat(appStat));
//记录到前台的状态信息
mStampList.add(0, new TimeBreaker.Stamp(String.valueOf(appStat)));
//过载减半
checkOverHeat();
}
}
MatrixLog.i(TAG, "updateAppImportance when app " + (isForeground ? "foreground" : "background"));
//获取app的当前进程的重要级别
updateAppImportance();
// if (!isForeground) {
// MatrixLog.i(TAG, "checkBackgroundAppState when app background");
// checkBackgroundAppState(0L);
// }
}
记录当前进程最重要的级别
private void updateAppImportance() {
if (mAppImportance <= mForegroundServiceImportanceLimit && mGlobalAppImportance <= mForegroundServiceImportanceLimit) {
return;
}
Runnable runnable = new Runnable() {
@SuppressWarnings("SpellCheckingInspection")
@Override
public void run() {
Context context = mCore.getContext();
String mainProc = context.getPackageName();
if (mainProc.contains(":")) {
mainProc = mainProc.substring(0, mainProc.indexOf(":"));
}
ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
if (am == null) {
return;
}
List<ActivityManager.RunningAppProcessInfo> processes = am.getRunningAppProcesses();
if (processes == null) {
return;
}
for (ActivityManager.RunningAppProcessInfo item : processes) {
if (item.processName.startsWith(mainProc)) {
if (mGlobalAppImportance > item.importance) {
MatrixLog.i(TAG, "update global importance: " + mGlobalAppImportance + " > " + item.importance
+ ", reason = " + item.importanceReasonComponent);
mGlobalAppImportance = item.importance;
}
if (item.processName.equals(context.getPackageName())) {
if (mAppImportance > item.importance) {
MatrixLog.i(TAG, "update app importance: " + mAppImportance + " > " + item.importance
+ ", reason = " + item.importanceReasonComponent);
mAppImportance = item.importance;
}
}
}
}
}
};
if (Looper.myLooper() == Looper.getMainLooper()) {
mCore.getHandler().post(runnable);
} else {
runnable.run();
}
}
这个级别是什么?我们看看源码注释
The relative importance level that the system places on this process. These constants are numbered so that "more important" values are always smaller than "less important" values. 系统对这个过程的相对重要性。这些常量被编号,因此“更重要的”值总是比“不重要的”值小。
可以根据进程信息中的 importance 来判断 app 的状态
- importance = 400 后台
- importance = 500 空进程
- importance =100 在屏幕最前端、可获取到焦点
- importance = 300 在服务中
- importance = 200 在屏幕前端、获取不到焦点
当进入后台时,我们还查是否服务的优先级更高,如果更高会监控记录
@WorkerThread
@Override
public void onBackgroundCheck(long duringMillis) {
super.onBackgroundCheck(duringMillis);
MatrixLog.i(TAG, "#onBackgroundCheck, during = " + duringMillis);
if (mGlobalAppImportance > mForegroundServiceImportanceLimit || mAppImportance > mForegroundServiceImportanceLimit) {
Context context = mCore.getContext();
ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
if (am == null) {
return;
}
List<ActivityManager.RunningServiceInfo> runningServices = am.getRunningServices(Integer.MAX_VALUE);
if (runningServices == null) {
return;
}
for (ActivityManager.RunningServiceInfo item : runningServices) {
if (!TextUtils.isEmpty(item.process) && item.process.startsWith(context.getPackageName())) {
if (item.foreground) {
MatrixLog.i(TAG, "checkForegroundService whether app importance is low, during = " + duringMillis);
// foreground service is running when app importance is low
if (mGlobalAppImportance > mForegroundServiceImportanceLimit) {
// global
MatrixLog.w(TAG, "foreground service detected with low global importance: "
+ mAppImportance + ", " + mGlobalAppImportance + ", " + item.service);
mCore.onForegroundServiceLeak(false, mAppImportance, mGlobalAppImportance, item.service, duringMillis);
}
if (mAppImportance > mForegroundServiceImportanceLimit) {
if (item.process.equals(context.getPackageName())) {
// myself
MatrixLog.w(TAG, "foreground service detected with low app importance: "
+ mAppImportance + ", " + mGlobalAppImportance + ", " + item.service);
mCore.onForegroundServiceLeak(true, mAppImportance, mGlobalAppImportance, item.service, duringMillis);
}
}
}
}
}
}
// MatrixLog.i(TAG, "checkBackgroundAppState when app background, during = " + duringMillis);
// checkBackgroundAppState(duringMillis);
}
AppStat 和 DevStat 是耗电异常判断的重要依据,例如:在 App 开启前台服务或者浮窗的情况下,我们运行 App 出现比较高的功耗,同样在设备出于充电状态的情况下我们也可以适当放宽一些待机功耗的限制;相反在 App 处于后台并且设备处于息屏状态的时候,我们应当最大程度收紧待机功耗的阈值,取消 App 的后台任务或者降低相关线程的优先级,让 App 进入 Standby 状态并且让设备能成功进入 Doze 模式。
根据我们对多份耗电异常的 Battery Historian 的排查,我们发现设备无法进入 Doze 模式是系统耗电排行中电量曲线出现明显下降的主要原因。而导致设备无法进入 Doze 模式的主要原因基本都是 App 的进程优先级一直没有下降(比如有活跃的线程一直在 Running 导致 CPU Load 负载很高)。
CpuStatFeature
CpuFreq 也是跟 CPU 功耗相关的一个主要的依据。一个 CPU 往往由几个集群(Cluster)组成,每个集群里面的 CPU Core 的规格都是一样的,比如我的手机就是由大(2 core)、中(4 core)、小(2 core)三个集群共 8 CPU Core 组成。CPU 工作时,每个 Core 会运行在不同的 CpuFreq,工作频率(Step)越高则功耗越大,相应的设备发烫也就更严重。
获取 App 当前瞬时的 CpuFreq 意义不大,我们需要通过监控 CpuFreq 的变化,来大抵推断出 App 在某个使用场景下 CPU 功耗的变化情况,如果 CpuFreq 变化太大且长时间处于比较高的 Step,则我们应该着重关注该场景下用户发热发烫的反馈情况,必要时候做出相应的降频策略。
统计记录CPU使用情况和获取不同模块的功率消耗信息,这个数据在安卓的PowerProfile文件中
public class CpuStatFeature extends AbsTaskMonitorFeature {
private static final String TAG = "Matrix.battery.CpuStatFeature";
private PowerProfile mPowerProfile;
@Override
protected String getTag() {
return TAG;
}
@Override
public int weight() {
return 0;
}
@Override
public void onTurnOn() {
super.onTurnOn();
tryInitPowerProfile();
}
@Override
public void onForeground(boolean isForeground) {
super.onForeground(isForeground);
if (!isForeground) {
if (mPowerProfile == null) {
mCore.getHandler().post(new Runnable() {
@Override
public void run() {
tryInitPowerProfile();
}
});
}
}
}
进入tryInitPowerProfile
@WorkerThread
private void tryInitPowerProfile() {
if (mPowerProfile != null) {
return;
}
synchronized (this) {
if (mPowerProfile != null) {
return;
}
try {
// Check PowerProfile compat
mPowerProfile = PowerProfile.init(mCore.getContext());
// Check KernelCpuSpeedReader compat
for (int i = 0; i < mPowerProfile.getCpuCoreNum(); i++) {
final int numSpeedSteps = mPowerProfile.getNumSpeedStepsInCpuCluster(mPowerProfile.getClusterByCpuNum(i));
//校验是否正确
new KernelCpuSpeedReader(i, numSpeedSteps).smoke();
}
// Check KernelCpuUidFreqTimeReader compat
int[] clusterSteps = new int[mPowerProfile.getNumCpuClusters()];
for (int i = 0; i < clusterSteps.length; i++) {
clusterSteps[i] = mPowerProfile.getNumSpeedStepsInCpuCluster(i);
}
//校验是否正确
new KernelCpuUidFreqTimeReader(Process.myPid(), clusterSteps).smoke();
} catch (IOException e) {
MatrixLog.w(TAG, "Init cpuStat failed: " + e.getMessage());
mPowerProfile = null;
}
}
}
这里主要获取了PowerProfile,他是是安卓的电量功率文件,拿到该文件可以知道不同模块的功率和cpu的数情况
进入看看 PowerProfile = PowerProfile.init(mCore.getContext());
public static PowerProfile init(Context context) throws IOException {
synchronized (sLock) {
try {
sInstance = new PowerProfile(context).smoke();
return sInstance;
} catch (Throwable e) {
throw new IOException(e);
}
}
}
其实就是一个xml文件,这里解析到内中,
PowerProfile(Context context) {
// Read the XML file for the given profile (normally only one per device)
synchronized (sLock) {
if (sPowerItemMap.size() == 0 && sPowerArrayMap.size() == 0) {
//解析功率数据到内存
readPowerValuesFromXml(context);
}
//解析cpu情况
initCpuClusters();
}
}
CompositeMonitors
为了方便计算统计数据 Diff,BatteryCanary 使用一个组合器(CompositeMonitors)来存放每一个电量监控模块(Monitor)的统计数据,封装了比较繁琐的数据 Diff 的计算代码,使用的时候只需要在开始监控的地方调用一下 CompositeMonitors#start() 并在结束的地方调用一下 CompositeMonitors#finish() ,即可获得这段时间内的电量统计信息,然后可以根据自身 App 的指标需要,判断出哪些电量监控模块出否出现异常。
这里需要特别强调的是,正因为耗电检测是一个过程,所以每一个过程(或者说是使用场景)对应的时间窗口是可能出现重叠的。我们可以根据徐存,给每一个使用场景创建一个不同的 CompositeMonitors 来监控(不同场景通过 Scope 来区分)。
例如,以下代码实现了 App 不同使用场景下的电量监控:
// 1. 后台电量监控
CompositeMonitors compositor = new CompositeMonitors(core, "scope_bg");
// App 进入后台
compositor.start();
// App 进入前台
compositor.finish();
// 获取线程功耗统计数据
Delta<JiffiesSnapshot> appJiffies = compositor.getDelta(JiffiesSnapshot.class);
// 获取功耗 TOP1 的线程信息
ThreadJiffiesEntry threadJiffies = appJiffies.dlt.threadEntries.getList().get(0);
// 2. 针对某 Activity 的电量监控
CompositeMonitors compositor = new CompositeMonitors(core, "scope_activity_xxx");
// 进入 Activity#onActivityStarted
compositor.start();
// 退出 Activity#onActivityFinished
compositor.finish();
// 获取电量统计数据
BatteryMonitorCore
所以模块的运作最终回归到核心类Core 通过定时间隔不断的发监控消息
private class ForegroundLoopCheckTask implements Runnable {
int lastWhat = MSG_ID_JIFFIES_START;
@Override
public void run() {
if (mForegroundModeEnabled) {
Message message = Message.obtain(mHandler);
message.what = lastWhat;
message.arg1 = MSG_ARG_FOREGROUND;
mHandler.sendMessageAtFrontOfQueue(message);
lastWhat = (lastWhat == MSG_ID_JIFFIES_END ? MSG_ID_JIFFIES_START : MSG_ID_JIFFIES_END);
mHandler.postDelayed(this, mFgLooperMillis);
}
}
}
一个周期记录开始和结束
@Override
public boolean handleMessage(Message msg) {
if (msg.what == MSG_ID_JIFFIES_START) {
notifyTraceBegin();
return true;
}
if (msg.what == MSG_ID_JIFFIES_END) {
notifyTraceEnd(msg.arg1 == MSG_ARG_FOREGROUND);
return true;
}
return false;
}
private void notifyTraceBegin() {
MatrixLog.d(TAG, "#onTraceBegin");
getConfig().callback.onTraceBegin();
}
private void notifyTraceEnd(boolean isForeground) {
MatrixLog.d(TAG, "#onTraceEnd");
getConfig().callback.onTraceEnd(isForeground);
}
回调的处理最终委托给BatteryMonitorCallback
@CallSuper
@Override
public void onTraceBegin() {
mTraceBgnMillis = SystemClock.uptimeMillis();
mCompositeMonitors.clear();
mCompositeMonitors.start();
// TODO: Remove deprecated statements
// Configure begin snapshots
mAlarmFeat = mMonitor.getMonitorFeature(AlarmMonitorFeature.class);
if (mAlarmFeat != null) {
mLastAlarmSnapshot = mAlarmFeat.currentAlarms();
}
mAppStatFeat = mMonitor.getMonitorFeature(AppStatMonitorFeature.class);
mBlueToothFeat = mMonitor.getMonitorFeature(BlueToothMonitorFeature.class);
if (mBlueToothFeat != null) {
mLastBlueToothSnapshot = mBlueToothFeat.currentSnapshot();
}
mDevStatFeat = mMonitor.getMonitorFeature(DeviceStatMonitorFeature.class);
if (mDevStatFeat != null) {
mLastCpuFreqSnapshot = mDevStatFeat.currentCpuFreq();
mLastBatteryTmpSnapshot = mDevStatFeat.currentBatteryTemperature(mMonitor.getContext());
}
mJiffiesFeat = mMonitor.getMonitorFeature(JiffiesMonitorFeature.class);
if (mJiffiesFeat != null) {
mLastJiffiesSnapshot = mJiffiesFeat.currentJiffiesSnapshot();
}
mLocationFeat = mMonitor.getMonitorFeature(LocationMonitorFeature.class);
if (mLocationFeat != null) {
mLastLocationSnapshot = mLocationFeat.currentSnapshot();
}
mTrafficFeat = mMonitor.getMonitorFeature(TrafficMonitorFeature.class);
if (mTrafficFeat != null) {
mLastTrafficSnapshot = mTrafficFeat.currentRadioSnapshot(mMonitor.getContext());
}
mWakeLockFeat = mMonitor.getMonitorFeature(WakeLockMonitorFeature.class);
if (mWakeLockFeat != null) {
mLastWakeWakeLockSnapshot = mWakeLockFeat.currentWakeLocks();
}
mWifiMonitorFeat = mMonitor.getMonitorFeature(WifiMonitorFeature.class);
if (mWifiMonitorFeat != null) {
mLastWifiSnapshot = mWifiMonitorFeat.currentSnapshot();
}
mCpuStatFeat = mMonitor.getMonitorFeature(CpuStatFeature.class);
if (mCpuStatFeat != null && mCpuStatFeat.isSupported()) {
mLastCpuStateSnapshot = mCpuStatFeat.currentCpuStateSnapshot();
}
}
@Override
public void onTraceEnd(boolean isForeground) {
mIsForeground = isForeground;
long duringMillis = SystemClock.uptimeMillis() - mTraceBgnMillis;
if (mTraceBgnMillis <= 0L || duringMillis <= 0L) {
MatrixLog.w(TAG, "skip invalid battery tracing, bgn = " + mTraceBgnMillis + ", during = " + duringMillis);
return;
}
mCompositeMonitors.finish();
onCanaryDump(mCompositeMonitors);
}
通过monior收集到统计的各种信息,在结束时dump到日志或存储或上报
protected void onCanaryDump(final CompositeMonitors monitors) {
monitors.getAppStats(new Consumer<AppStats>() {
@Override
public void accept(AppStats appStats) {
onCanaryDump(appStats);
}
});
Dumper dumper = createDumper();
Printer printer = createPrinter();
printer.writeTitle();
dumper.dump(monitors, printer);
printer.writeEnding();
printer.dump();
checkBadThreads(monitors);
onCanaryReport(monitors);
synchronized (tasks) {
tasks.clear();
}
}
这里以开启全部监控功能的待机场景为例: Logcat 会输出以下格式化的文本:
**__************************************** PowerTest *****************************************
| pid=12773 fg=fg during(min)=1 diff(jiffies)=10828 avg(jiffies/min)=10828
+ --------------------------------------------------------------------------------------------
| jiffies(13) :
| -> desc = (status)name(tid) avg/total
| -> inc_thread_num = 2
| -> cur_thread_num = 30
| -> (+/R)Benchmark(12833) 10436/10436 jiffies
| -> (~/S)RenderThread(12808) 135/135 jiffies
| -> (~/R).tencent.matrix(12773) 126/126 jiffies
| -> (+/S)JDWP Transport (12908) 38/38 jiffies
| -> (~/S)Jit thread pool(12787) 33/33 jiffies
| -> (~/S)default_matrix_(12802) 26/26 jiffies
| -> (~/S)matrix_time_upd(12800) 8/8 jiffies
| -> (~/S)Binder:12773_2(12794) 7/7 jiffies
| ......
| #overHeat
+ --------------------------------------------------------------------------------------------
| awake :
| <alarm>
| -> 104732(mls) 1(min)
| -> inc_alarm_count = 0
| -> inc_trace_count = 0
| -> inc_dupli_group = 0
| -> inc_dupli_count = 0
| <wake_lock>
| -> 104732(mls) 1(min)
| -> inc_lock_count = 0
| -> inc_time_total = 0
+ --------------------------------------------------------------------------------------------
| scanning :
| <bluetooh>
| -> 104719(mls) 1(min)
| -> inc_regs_count = 0
| -> inc_dics_count = 0
| -> inc_scan_count = 0
| <wifi>
| -> 104648(mls) 1(min)
| -> inc_scan_count = 0
| -> inc_qury_count = 0
| <location>
| -> 104646(mls) 1(min)
| -> inc_scan_count = 0
+ --------------------------------------------------------------------------------------------
| dev_stats :
| <cpu_load>
| -> 104715(mls) 1(min)
| -> usage = 103%
| -> cpu0 = [0, 0, 0, 1832, 0, 0, 3, 4675, 1194, 434, 412, 91, 37, 77, 59, 48, 45, 1564]
| -> cpu1 = [0, 0, 0, 1832, 0, 0, 3, 4675, 1194, 434, 412, 91, 37, 77, 59, 48, 45, 1564]
| -> cpu2 = [0, 0, 0, 1832, 0, 0, 3, 4675, 1194, 434, 412, 91, 37, 77, 59, 48, 45, 1565]
| -> cpu3 = [0, 0, 0, 1832, 0, 0, 3, 4675, 1194, 434, 412, 91, 37, 77, 59, 48, 45, 1565]
| -> cpu4 = [7190, 37, 31, 32, 17, 429, 405, 254, 128, 25, 19, 13, 9, 8, 4, 6, 1868]
| -> cpu5 = [7190, 37, 31, 32, 17, 429, 405, 254, 128, 25, 19, 13, 9, 8, 4, 6, 1868]
| -> cpu6 = [7190, 37, 31, 32, 17, 429, 405, 254, 128, 25, 19, 13, 9, 8, 4, 6, 1868]
| -> cpu7 = [1603, 11, 0, 0, 79, 4, 4, 11, 4, 5, 8, 2, 0, 0, 2, 0, 0, 0, 0, 8738]
| <cpu_sip>
| -> inc_cpu_sip = 21.34(mAh)
| -> cur_cpu_sip = 66720.12(mAh)
| -> inc_prc_sip = 3.58(mAh)
| -> cur_prc_sip = 4.81(mAh)
| <cpufreq>
| -> 104686(mls) 1(min)
| -> inc = [0, 0, 0, 0, -192, -192, -96, 0]
| -> cur = [1785, 1785, 1785, 1785, 2227, 2227, 2323, 2841]
| <cpufreq_sampling>
| -> 104719(mls) 1000(itv)
| -> max = 2419.0
| -> min = 2419.0
| -> avg = 2419.0
| -> cnt = 17
| <batt_temp>
| -> 104719(mls) 1(min)
| -> inc = 0
| -> cur = 273
| <batt_temp_sampling>
| -> 104720(mls) 1000(itv)
| -> max = 273.0
| -> min = 273.0
| -> avg = 273.0
| -> cnt = 17
+ --------------------------------------------------------------------------------------------
| app_stats :
| <stat_time>
| -> time = 1(min)
| -> fg = 100
| -> bg = 0
| -> fgSrv = 0
| -> devCharging = 100
| -> devScreenOff = 0
| -> sceneTop1 = Current AppScene/100
| <run_time>
| -> time = 1(min)
| -> fg = 100
| -> bg = 0
| -> fgSrv = 0