链接:
https://juejin.cn/post/6899309167450783752
1发热案例分析
Android 框架层通过一个名为 batterystats 的系统服务,电池的信息,电压,温度,充电状态等等,都是由BatteryService来提供的。电池的这些信息是BatteryService通过广播主动把数据传送给所关心的应用程序。实现了电量统计的功能,batterystats实现原理可以查阅电量统计服务 Android 提供的 dumpsys 命令用于查看系统服务的信息(实现原理可以查阅 dumpsys 介绍)将batterystats作为参数,就能输出完整的电量统计信息。
小编在某游戏直播平台之一项目组开发过一段时间发现直播页面发热问题一直饱受用户诟病,因此我准备出一篇技术文章详细介绍整个优化流程,经过功能测试发现: 如果在游戏直播中播放视频,手机很快就会发烫。针对这种现象,我马上拉取数据进行了分析,测试数据表明游戏直播耗电量竟然高达 7%,经过调研,发现 Battery Historian 这个框架还挺合适线下优化的。
2发热测试工具
2.1 开发环境
首先确保你的电脑已经安装,并配置好以下相关环境变量:
Python 2.7 环境Docker 环境go 1.8.1 环境adb 环境
然后你还得准备一台 Android 5.0 以上手机,因为 Battery Historian 是在 Android 5.0 以上运行环境上跑的,最后 找一台适合高富帅的 Mac OS X 系统,实在没有就拿乞丐版 window 操作~
2.2 Battery Historian 使用指南
接下来我们来看一下 Battery Historian 具体使用:
2.2.1 使用 Docker 监听 battery-historian 9999 端口
2.2.2 配置 go 的环境
2.2.3 通过 go 下载 Battery Historian 源码
2.2.4 运行 Battery Historian
2.2.5 手机连上我们的 USB,先唤醒 Battery Historian 然后再清空电池历史状态
2.2.6 断开 USB ,打开测试应用,疯狂测试,20 分钟后将 bugreport_xxx 版本.zip 文件导出,通过命令将该文件上传到http://localhost:9999 即可
2.2.7 查看当前进程的关键信息
图片可能不是很清楚,我这边再给大家总结一下核心参数信息
参数参数说明Sync是否跟后台同步,可以把鼠标停在某一项上面。可以看到何时 sync 同步 启动的,持续时间 Duration 多久。电池容量不会显示单一行为消耗的具体电量,这里只能显示使用电池的频率和时长,你可以看分时段的剩余电量来了解具体消耗了多少电量。wake_lock_inwake_lock 有不同的组件,这个地方记录在某一个时刻,有哪些部件开始工作,以及工作的时间。wake_lockwake_lock 该属性是记录 wake_lock 模块的工作时间。是否有停止的时候等。Android 的休眠唤醒主要基于 wake_lock 机制,只要系统中存在任一有效的 wake_lock,系统就不能进入深度休眠,但可以进行设备的浅度休眠操作。wake_lock 一般在关闭 lcd、tp 但系统仍然需要正常运行的情况下使用,比如听歌、传输很大的文件等。running界面的状态,主要判断是否处于 idle 的状态。用来判断无操作状态下电量的消耗。plugged充电状态,这一栏显示是否进行了充电,以及充电的时间范围。例如上图反映了我们在第 22s 插入了数据线,然后一直持续了数据采集结束screen屏幕是否点亮,这一点可以考虑到睡眠状态和点亮状态下电量的使用信息。top该栏显示当前时刻哪个 app 处于最上层,就是当前手机运行的 app,用来判断某个 app 对手机电量的影响,这样也能判断出该 app 的耗电量信息。该栏记录了应用在某一个时刻启动,以及运行的时间,这对我们比对不同应用对性能的影响有很大的帮助。
3发热测试过程
我们首先找出一款被骂的最狠的一款测试手机 xx 三星 xx 版本,电池容量:3000mAh,游戏直播和秀场直播以及直播回放 WiFi 环境下,打开 App,播放同一个测试直播源资源分别测试 20 分钟。
测试场景是这样的
通过 小木箱 的账号进入游戏开播竖屏页面,开启游戏直播通过 小木箱 的账号进入直播回放竖屏页面, 进行直播回放通过 小木箱 的账号进入秀场直播竖屏页面, 开启秀场直播
为了保证测试数据的准确性,要保证四点,第一点是,手机不要灭屏,因为屏幕唤醒本身就会有耗电开销,第二点是不要使用蜂窝网络,这样测试的数据不具备公正性,第三点把没有用到的传感器关掉,最后在测试过程不要充电,保持测试环境的一致性。
测试完毕后,在导航栏选中你的进程 ID
CPU User TimeDevice estimated power use测试时长
直播应用耗电量的问题: 无非就建立 socket 连接过程中.推送心跳包,会定时唤醒 CPU 这样可能会有耗电风险,然后如果自定义 View 设计不合理,进行高频刷新 UI,也会造成耗电,而且本公司产品 UI 布局设计很不合理,布局嵌套很严重,存在重复渲染问题。
如果你们应用不合适的时间点播放礼物,什么送跑车,送游艇,脸萌这种效果都可能会存在耗电问题,耗电最难的一点就是要解决底层的 wake_lock 及时释放,因为 wake_lock 可以保证 CPU 进行休眠。
4耗电优化建议
省电这一块 主要是需要控制 wakelock 的使用。控制无谓的 CPU 运行和计算,项目有一些上传下载操作进行着,可采用高效下载策略,做到批量网络请求。可以交给JobScheduler 来处理,JobScheduler 集中处理收到的任务,选择合适的时间合适的网络,再一起执行频繁定位类的 App 确实是耗电大户,可以在非必须的情况下,采用缓存数据,或者通过简化业务流程的情况下来进行优化避免在后台进行日志上报以及前台进行拉活工作如果面有一些任务的队列里面积累了大量的任务,每次都循环的执行任务太久,耗电会明显,然后如果项目存在大量轮询接口的时候,应该将其整合起来,减少CPU消耗对于网络请求或者 websocket 通信要对数据进行 gzip 压缩处理数据解析不要使用原生的 JSONObject ,应该使用 Gson jackson ptoBuffer 或者其他数据解析工具项目的直播或音频是电量消耗大户,可判断用户是否处于充电状态,在获取用户电量,在某个临界点时,降低直播码率,提高观看直播时间清单文件application标签下 keepScreeanOn 保持常量模式不够完善,导致用户长时间误操作情况下,屏幕长时间保持常亮。建议修改模式为: 用户5分钟未操作状态下,屏幕变暗且屏幕不关闭。满足屏幕处于开启状态下省电项目存在大量图片展示,图片加载时会消耗大量的流量与电量。可判断用户是否处于充电状态,在获取用户电量,在某个临界点,降低加载图片码率,提高操作时间5APM 耗电监控建设
影响App电量的因素有: 有屏幕, GPS,CPU,Radio,Video/Audio,Wifi 或蓝牙等
那么我们该如何计算耗电量呢?怎么监控多进程,前后台等不同方式监测耗电情况呢?
手机系统耗电这块我们可以根据 /system/framework/framework-res.apk 的 power_profile.xml–文件进行耗电估算,里面的 value 值代表着不同类型设备发送不同动作的耗电系数: 如下图所示:
包括: BlueTooth , Wifi, Radio ,CPU 等。
手机的系统耗电量 = CPU 毫秒数* 系数 1 流量 Bytes*系数 2 ….,当然不同类型参数是不一样的,应用也无法获取电量的具体细节~
我们可以看一下 com.android.internal.os.BatteryStatsHelper 的关键方法 processAppUsage 里面按照 不同类型分别估算了不同服务耗电情况~
privatevoidprocessAppUsage(SparseArray<UserHandle>asUsers){finalbooleanforAllUsers=(asUsers.get(UserHandle.USER_ALL)!=null);mStatsPeriod=mTypeBatteryRealtimeUs;BatterySipperosSipper=null;finalSparseArray<?extendsUid>uidStats=mStats.getUidStats();finalintNU=uidStats.size();for(intiu=0;iu<NU;iu ){finalUidu=uidStats.valueAt(iu);finalBatterySipperapp=newBatterySipper(BatterySipper.DrainType.APP,u,0);mCpuPowerCalculator.calculateApp(app,u,mRawRealtimeUs,mRawUptimeUs,mStatsType);mWakelockPowerCalculator.calculateApp(app,u,mRawRealtimeUs,mRawUptimeUs,mStatsType);mMobileRadioPowerCalculator.calculateApp(app,u,mRawRealtimeUs,mRawUptimeUs,mStatsType);mWifiPowerCalculator.calculateApp(app,u,mRawRealtimeUs,mRawUptimeUs,mStatsType);mBluetoothPowerCalculator.calculateApp(app,u,mRawRealtimeUs,mRawUptimeUs,mStatsType);mSensorPowerCalculator.calculateApp(app,u,mRawRealtimeUs,mRawUptimeUs,mStatsType);mCameraPowerCalculator.calculateApp(app,u,mRawRealtimeUs,mRawUptimeUs,mStatsType);mFlashlightPowerCalculator.calculateApp(app,u,mRawRealtimeUs,mRawUptimeUs,mStatsType);mMediaPowerCalculator.calculateApp(app,u,mRawRealtimeUs,mRawUptimeUs,mStatsType);finaldoubletotalPower=app.sumPower();if(DEBUG&&totalPower!=0){Log.d(TAG,String.format(“UID%d:totalpower=%s”,u.getUid(),makemAh(totalPower)));}//—-code——}
至此: 我们也总结出影响 App 耗电的模块有如下几种情况
APM 耗电监控的关键就是需要定义全局参数估算 Location 时间, Alarm 次数,Net 访问量,wake_lock 持有时间,CPU 耗电情况等等~
耗电监控最大的难度在于:怎样通过 Hook wake_lock 持有长和 alarm 阀值 来预判 alarm 是否在做定时的重复任务,怎样通过 Hook 代理 LOCATION_SERVICE 实现 GPS 监控 ; 怎样通过 Hook 传感器的 SENSOR_SERVICE 中的“mSensorListeners”,拿到部分信息,最后才是通过埋点方案,在申请资源的时候将堆栈信息保存起来。当我们触发某个规则上报问题的时候,可以将收集到的堆栈信息、电池是否充电、电池的健康状态,CPU 信息、应用前后台时间等辅助信息也一起带上实现。
整个耗电监控架构图如下: 利用多进程收集信息然后再传递给主进程,然后对耗电进行评估,最后再决定是否上报。
那么我们怎么去监测手机的电量变化情况呢?
最核心的是通过广播来实现,在 BatterMannager 有六个比较核心的字段:
字段含义STATUS_CHARGING表示充电状态STATUS_DISCHARGING放电中STATUS_NOT_CHARGING未充电STATUS_FULL电池满BATTERY_PLUGGED_AC表示充电类型BATTERY_PLUGGED_USB表示 USB
电池的健康状态 也有一些七个比较核心的参数,他们返回一个 code, 这里我们也可以看一下:
字段含义BATTERY_HEALTH_UNKNOWN未知BATTERY_HEALTH_GOOD良好BATTERY_HEALTH_OVERHEA过热BATTERY_HEALTH_DEAD没电BATTERY_HEALTH_OVER_VOLTAGE过电压BATTERY_HEALTH_UNSPECIFIED_FAILURE未知错误BATTERY_HEALTH_COLD过冷
基于这几点我们可以大胆预设做一套符合企业规则的耗电 APM 体系。
监控耗电本身也会带来更多耗电,那么我们该如何持续监控电池电量变化,其实我们真实环境是我们得找到低电量状态,如果手机电量过低自动关机,电池温度异常状态下监控并将数据上报,APM 管理系统可进行统计分析后,再通知手机做出电量补救措施,那么怎么看手机充电状态呢?
可以参考以下代码
IntentFilterifilter=newIntentFilter(Intent.ACTION_BATTERY_CHANGED);IntentbatteryStatusIntent=registerReceiver(null,ifilter);//如果设备正在充电,可以提取当前的充电状态和充电方式(无论是通过 USB 还是交流充电器),如下所示://Arewecharging/charged?intstatus=batteryStatusIntent.getIntExtra(BatteryManager.EXTRA_STATUS,-1);booleanisCharging=status==BatteryManager.BATTERY_STATUS_CHARGING||status==BatteryManager.BATTERY_STATUS_FULL;//Howarewecharging?intchargePlug=batteryStatusIntent.getIntExtra(BatteryManager.EXTRA_PLUGGED,-1);booleanusbCharge=chargePlug==BatteryManager.BATTERY_PLUGGED_USB;booleanacCharge=chargePlug==BatteryManager.BATTERY_PLUGGED_AC;if(isCharging){if(usbCharge){Toast.makeText(MainActivity.this,”手机正处于USB连接!”,Toast.LENGTH_SHORT).show();}elseif(acCharge){Toast.makeText(MainActivity.this,”手机通过电源充电中!”,Toast.LENGTH_SHORT).show();}}else{Toast.makeText(MainActivity.this,”手机未连接USB线!”,Toast.LENGTH_SHORT).show();}
关于 Hook 方案就不多做介绍了,java 层一般使用动态代理加静态代理方案实现,其他插桩方式如: ASM,javasisit,AspectJ 亦可,native 可以考虑使用 weishu 的 arthoook 替换 native 方法,这个可以结合我的 Github 项目 BatteryCanary 进行代码分析。
因为 在 Android P 之后,很多的 Hook 点都不支持了。所以 APM 耗电监控变得异常艰辛曲折~
6总结
https://github.com/MicroKibaco/BatteryCanary
推荐阅读:
经历这么多版本,RxJava本质上不变的是什么?Handler 10问,你顶的住吗?没见过这么详细的,自定义控件图形探索!