App耗电问题随着业务迭代,不知不觉中成了痛点。相信不少团队在现实中都遇到过预装功耗问题,或者 OEM厂商对高耗电应用的通知提醒,这些问题无疑会削弱对竞品的竞争力。本文站在解决问题角度,以可监控,可预警,可分析的姿势提出监控及优化方案。
导读
监控要考虑监控什么数据,我们是结合实际需要还有参照绿盟的标准定义方式去定义了需要收集的数据并且落地,所以在这里 1.介绍了绿盟的功耗参考标准。2.就是我们收集了什么数据。3. 这些数据怎么收集。4.最后就是实现的过程中需要注意的坑另外还有些建议。
标准
绿盟的功耗标准V2.0,由一些主要企业联合起草。https://www.androidga.com/statics/html/20180718160029504.html
建议读一下,里边对于后台Cpu时间, Alarm 设定的个数等等都有规定,这些规定当然不是说全部需要遵守也肯定不会全部遵守,因为你的应用有很多不得不的地方,但是也是很好的量化标准参考,示例:
后台App对CPU的使用行为描述:
标准描述 | 测量应用在后台时对处理器的占用及 WakeLock 的设置情况 |
---|---|
判定标准 | 1. 平均每小时占用处理器累计时间不超过90s,处理器平均占用率不超过2.5% 2. 禁止应用设置WakeLock |
后台App对Alarm的使用行为描述:
判定标准 | 1.平均每小时通过Alarm掉起次数不超过20次(后台视频,后台收发消息时除外) |
---|
在线监控“姿势”
Android耗电被分解为一个个指标数据,所以耗电监控对我们来说也是指标监控,怎么有效的拿到数据是我们要考虑的重点,参考系统的BatteryStatsSevice 他们也是拿到Cpu Time,Wakelock,Wifi,Radio Scan... 等数据然后分别乘以耗电元数据最后累加形成耗电数据,测量好的耗电元数据保存在frameworks/base/core/res/res/xml/power_profile.xm 里边,现在我们大致看下里边的大头是什么,做到心里有数。
以小米MIX2为例,看下power_profile 这个文件它位于 /system/framework/framework-res.apk ,不需要Root 你可以Pull下来 用apktool d 解开,片段如下:
<snippet>
<item name="wifi.active">182.79</item>
<item name="wifi.scan">40.69</item>
<item name="dsp.audio">18.46</item>
<item name="dsp.video">54.17</item>
<item name="camera.flashlight">160</item>
<item name="camera.avg">586</item>
<item name="gps.on">59.91</item>
<item name="radio.active">134.84</item>
</snippet>
可以看出wifi.active 或者 radio.active是相当耗电的,在后台频繁的上传和下载行为很容易被判定为高耗电,因为后台Cpu利用率未必高,反而网络成大头。这是站在直接耗电 毫安时这个角度去观察,但是事实上OEM 厂商非常介意Wakelcok 或者 Alarm, 它影响的不是直接Cpu 耗电而是它们让手机不休眠,使得系统一直工作,使得非Wakeup 的Alarm一直有机会占用Cpu,这明显破坏了系统设计语义。所以我们既要参考Android的指标也要考虑OEM厂商的关注。
按照这个思路,我们也会监控一些重要的指标数据,长期监控它们的数据从而形成标准,这样我们可以在版本灰度阶段就能拿到耗电反馈,现在还要确定一个前提,按照之前提到的标准前提-后台App耗电,这个前提很重要,只有到了后台所有的监控才有意义,在前台,用户行为的严重不确定性导致数据的无规律波动,所以失去了前边提到的“可预警,可分析”设计本意,那么以下所提指标均在后台这个大前提下。
数据
在实践中我们发现,Cpu,WakeLock, Alarm, 网络(包含Wifi和射频),Location,在快手App后台耗电中占据较高的比重。其他如闪光灯,蓝牙,摄像头,在应用各进程的后台期间不会显著活动,我们暂时不太关注它们。我们根据实际需要集中在以下数据:
1.network traffic : 后台30分钟内接收的和发送的字节数.
2.wakeup alarms: 后台30分钟内用户实际有效的设定唤醒类型的alarm的个数,也即type == 0 || type == 2 设定的Alarm 且Triggertime 不是很久。
3.wakelock time: 后台30分钟内持有wakelock的时间(这个按照绿盟的标准除非特殊需要入后台音乐 后台导航 后台下载等均不需要),还有监控是否设定和释放能够匹配。
4.cpu_active_time : 拿到每个进程在后台30分钟内能统计到的cpu时间,这个数据可以说明进程休眠情况如果过高,说明一直在干活,就要分析是否合理。
5.process_cpu_usage: 拿一些sample 类似top命令可以分析平均的后台Cpu 使用率,使用率明显高的情况可以直接说明有问题了。
- location request:统计在后台使用了多久位置信息,这个可以结合业务看是否合理。
方案实施细节
-
前后台: 可以在主进程中通过LifecycleObserver获知.
-
network traffic : 这个可以读虚拟文件/proc/net/xt_qtaguid/stats
idx iface acct_tag_hex uid_tag_int cnt_set rx_bytes rx_packets tx_bytes tx_packets rx_tcp_bytes rx_tcp_packets rx_udp_bytes rx_udp_packets rx_other_bytes rx_other_packets tx_tcp_bytes tx_tcp_packets tx_udp_bytes tx_udp_packets tx_other_bytes tx_other_packets
118 lo 0x0 10194 0 15801472 89907 19058908 113734 15784830 89750 0 0 16642 157 19058830 113733 78 1 0 0
我们主要关心 cnt_set,rx_bytes,tx_bytes,其中cnt_set 这个标记前后台,事实上我只关心后台 1:前台 0:后台, rx_bytes 收到的字节数,tx_bytes 发送的字节数。
- wakeup alarms: 在每个进程进行Binder Hook,Hook掉AlarmManager ,然后把你的统计工作挂到 set 和 setRepeating 上,这里边的细节是你要查看IAlarmaManager.aidl,确认好接口在什么版本上有,比如setRepeating 只在KK之前有。参考一下代码 ,hook技术很成熟,网上有很多资料比如http://weishu.me/2016/02/16/understand-plugin-framework-binder-hook/
例如在android中我们在AlarmManager的set()之前,也就是beforeCall里边从调用参数里边取得worksource,alarm type,triggertime,以及pendingIntnet 信息上报。
public boolean beforeCall(Object who, Method method, Object... args) {
int pendingIntentIndex = ArrayUtils.indexOfObject(args, PendingIntent.class, 0);
PendingIntent pendingIntent = (PendingIntent) args[pendingIntentIndex];
int index = ArrayUtils.indexOfFirst(args, WorkSource.class);
WorkSource workSource = null;
if (index >= 0) {
workSource = (WorkSource) args[index];
KSLog.d("worksource %s ", workSource.toString());
}
// 版本间接口变动比较大 但是第一个int 必然是type 第一个long 必然是triggerAtTime
int typeIndex = ArrayUtils.indexOfFirst(args, Integer.class);
int triggerIndex = ArrayUtils.indexOfFirst(args, Long.class);
int type = (int) args[typeIndex];
long triggerElaspe = (long) args[triggerIndex];
AwakeCallRouter.getInstance().onAlarmSet(new Throwable(), type, triggerElaspe, 0,
0, 0, pendingIntent, workSource, null);
return true;
}
在PendingIntent 中可以反射调用getIntent()拿到的intent ,从而拿到action等相信会对你分析有帮助。
- wakelock time 也是采用Binder Hook的方式处理, hook PowerManagerService 中的acquireWakeLock, acquireWakeLockWithUid,releaseWakeLock 等方法,接口定义如下:
void acquireWakeLock(IBinder lock, int flags, String tag, String packageName, in WorkSource ws);
void acquireWakeLockWithUid(IBinder lock, int flags, String tag, String packageName, int uidtoblame);
void releaseWakeLock(IBinder lock, int flags);
我们可以拿到特定的lock的acquire和release时间,如果申请和释放不匹配或者申请时间和释放时间的间隔过长,我们可以拿到callstack去分析业务看是否正常。
- location request同样采用Binder Hook,hook LocationManagerService 中的requestLocationUpdates 和 removeUpdates接口定义如下:
void requestLocationUpdates(in LocationRequest request, in ILocationListener listener, in PendingIntent intent, String packageName);
void removeUpdates(in ILocationListener listener, in PendingIntent intent, String packageName);
这样我们可以监控后台位置信息的使用,可以监控是否及时的取消位置请求,还有可以通过PendingIntent做业务定位。
- cpu_active_time:这个可以在各个进程读 /proc/self/stat 去处理,虽然简单,但处理起来挺麻烦,因为涉及到前后台。读到内容如下,可以从中找出进程.smile.gifmaker(21337 11677)分别是用户态时间utime和内核态时间stime,我们的cpu_active_time就是 utime + stime 单位是jiffie(jiffie:内核表示时间的最小单位)。
5367 (.smile.gifmaker) S 753 752 0 0 -1 1077952832 477279 82786 329 4 21337 11677 46 199 19 -1 158 0 8702413 2151841792 72583 18446744073709551615 1 1 0 0 0 0 4612 4096 34040 18446744073709551615 0 0 17 3 0 0 0 0 0 0 0 0 0 0 0 0 0
- process_cpu_usage:原理就是模仿top,在比较短的时间里边 算出进程cpu时间差除以总的cpu的时间差,如下伪代码示意,里边涉及到jiffie来自/proc/pid/stat 还有 cpu 总的jiffie数据/proc/stat ,读/proc/stat 在8.0之后存在权限问题,因为我们是监控所以不需要全部到达,8.0之下占据了绝对样本数,对于8.0 root 的手机依旧不少,8.0部分我们可以采用判断 是否root 过 ,如果root过,通过su 去读也是一个办法。
long mOldCpuJiffies,mOldProcJiffies;
double calcCpuUsage(int pid) {
long procJiffies = getProcessCpuJiffies(); // read /proc/{pid}/stat
long cpuJiffies = getAllCpuJiffies(); // read /proc/stat
double usage = Math.div(((procJiffies - mOldProcJiffies) * 100.00),
(cpuJiffies - mOldCpuJiffies));
if (usage < 0) usage = 0;
if (usage > 100) usage = 100;
mOldCpuJiffies = cpuJiffies;
mOldProcJiffies = procJiffies;
return usage;
}
优化思路
根据收集到的数据优化功耗,大致两个思路,一是业务优化,二是技术优化,业务优化是安全的,这个根据自己的业务特点去优化就不多说,关于技术优化有些Aggressive的做法:
1.对于后台Wakelock 如果不是必须,建议进入后台就释放,如果不好查或者第三方Sdk中的行为,可以利用Hook的方式去统一释放。
2.对于Alarm用的时候要慎重,业务上能复用的尽量复用; 高版本可以用Job Scheduler ,另外我们可以在Hook层做Alarm透明对齐的操作(调整Trigger时间使尽量多的Alarm一起Trigger从而减少触发次数),减少Alarm 数量及Trigger次数还会减少App Wakelock持有次数。见AlarmManagerService代码:
/**
* Deliver an alarm and set up the post-delivery handling appropriately
*/
public void deliverLocked(Alarm alarm, long nowELAPSED, boolean allowWhileIdle) {
......
// The alarm is now in flight; now arrange wakelock and stats tracking
if (mBroadcastRefCount == 0) {
setWakelockWorkSource(alarm.operation, alarm.workSource,
alarm.type, alarm.statsTag, (alarm.operation == null) ? alarm.uid : -1,
true);
mWakeLock.acquire();
mHandler.obtainMessage(AlarmHandler.REPORT_ALARMS_ACTIVE, 1).sendToTarget();
}
final InFlight inflight = new InFlight(AlarmManagerService.this,
alarm.operation, alarm.listener, alarm.workSource, alarm.uid,
alarm.packageName, alarm.type, alarm.statsTag, nowELAPSED);
mInFlight.add(inflight);
mBroadcastRefCount++;
.......
}
就是在deliver alarm 的时候,如果mBroadcastRefCount == 0 那么就是没有别的Alarm在in flight 那么就要拿一个Wakelock 虽然这个Wakelock是System Server设的但是会算到我们应用身上因为WorkSource是我们的应用,所以减少Alarm的使用或者Trigger次数也可以降低Wakelock的持有次数。
最后对于不需要的第三方Sdk中的Alarm可以Hook掉。
Tips
1.事实上无论是功耗监控或者卡顿监控我们都要去除插件环境的影响例如 droidplugin 或者 virtual app 等,这个地方我们要注意,比如可以直接屏蔽掉。
2.方案的设计天然是"CS"的,需要各个进程把信息汇报给上报进程。
3.监控的特点不同于业务Feature,它不是要全部用户Accessible,所以对于某些重要信息我们利用Root用户拿到,关键是量够不够。
4.Hook的是aidl 定义的IXXInterface接口,它并不稳定所以要逐个版本去确定,可以去AndroidXRef上去查。
5.如果做的比较精细,对于直接fork出来的进程或者shell进程是不走java代码的,具体说是不走<? extends Application>如果是想要统计这些进程的CpuTime等信息你需要勾住 fork() 系统调用把进程pid 报告到Java层操作。这里边涉及到Native Hook 有兴趣可以讨论。
P.S. 顾冬,快手Android工程师,目前主要Focus在性能平台搭建,欢迎大家一起探讨内存,卡顿,流量,电量等性能问题及监控预警方法,如果对插件化架构有兴趣也欢迎指教探讨,wx: dgucpsc 也可内推哈
网友评论