一种Android应用耗电定位方案

背景

通常来说,app耗电相比于其他的性能问题(Crash,Anr)等,会受到比较少的关注,耗电通常是一个app隐藏的性能问题,同时又由于手机性能不同,使用时长不同,使用习惯不同,“耗电问题”从诞生以来,都被业内誉为伪命题,因为耗电量通常不具备较为“标准化”的衡量(我们常说的耗电量 = 模块功率 × 模块耗时),但是模块功率不同手机相差较大,同时不同厂商的定制化原因,导致了耗电的更加无法被有效衡量,但是应用耗电是客观的事实,因此google官方提出了耗电监测工具Battery Historian,希望能以客观的角度衡量耗电。但是实际耗电是关系到定制化的(比如不同app有不同的使用场景,同一个app也有使用场景不同从而导致耗电不同)所以业内也有像meta公司(facabook)的Battery-metrics一样采用了自定义化的标准去衡量自己的应用。本文从官方耗电计算、自定义耗电检测两个出发,从而实现一种app耗电的定位的方案。

耗电计算

在Android系统中,android官方要求了设备制造商必须在 /frameworks/base/core/res/res/xml/power_profile.xml 中提供组件的电源配置文件,以此声明自身各个组件的功耗(文档)

功耗文件获取

通常,power_profile.xml位于/system/framework/framework-res.apk中,这是一个android设备的系统apk,我们可以通过

adb  pull  /system/framework/framework-res.apk ./

获取当前系统的framework-res apk,这步不需要root即可进行,接着我们可以通过反编译工具,apktool或者jadx都可以,对该apk进行反编译处理,我们所需要的功耗文件就在 /res/xml/power_profile.xml 中。

系统功耗计算

我们得到的功耗文件后,系统是怎么计算功耗的呢?其实就在BatteryStatsHelper中,大部分都是通过使用时长* 功耗(功耗文件对应项)得到每一个模块的耗电,而在我们系统中,每个参与电量计算的模块都继承于PowerCalculator这个基类,同时会重写calculatorApp方法进行自定义的模块耗时计算,我们可以在BatteryStatsHelper的refreshStats方法中看到参与计算的模块。

refreshStats 中if (mPowerCalculators == null) {mPowerCalculators = new ArrayList<>();// Power calculators are applied in the order of registrationmPowerCalculators.add(new CpuPowerCalculator(mPowerProfile));mPowerCalculators.add(new MemoryPowerCalculator(mPowerProfile));mPowerCalculators.add(new WakelockPowerCalculator(mPowerProfile));if (!mWifiOnly) {mPowerCalculators.add(new MobileRadioPowerCalculator(mPowerProfile));}mPowerCalculators.add(new WifiPowerCalculator(mPowerProfile));mPowerCalculators.add(new BluetoothPowerCalculator(mPowerProfile));mPowerCalculators.add(new SensorPowerCalculator(mContext.getSystemService(SensorManager.class)));mPowerCalculators.add(new GnssPowerCalculator(mPowerProfile));mPowerCalculators.add(new CameraPowerCalculator(mPowerProfile));mPowerCalculators.add(new FlashlightPowerCalculator(mPowerProfile));mPowerCalculators.add(new MediaPowerCalculator(mPowerProfile));mPowerCalculators.add(new PhonePowerCalculator(mPowerProfile));mPowerCalculators.add(new ScreenPowerCalculator(mPowerProfile));mPowerCalculators.add(new AmbientDisplayPowerCalculator(mPowerProfile));mPowerCalculators.add(new SystemServicePowerCalculator(mPowerProfile));mPowerCalculators.add(new IdlePowerCalculator(mPowerProfile));mPowerCalculators.add(new CustomMeasuredPowerCalculator(mPowerProfile));mPowerCalculators.add(new UserPowerCalculator());}

我们从上面可以看到cpu,wifi,gps等等都参与耗电的模块计算,同时我们的厂商可以基于此,去定制自己的耗电视图,通常可以在应用信息-电量可以看到,以我的vivo为例子

耗电检测

当然,上面的信息是原生android提供的信息,对于手机厂商来说,是可以在此基础上增加多种耗电检测手段,因此处于一个“大杂烩”的现象。在Android P 及以上版本,谷歌官方推出了 Android Vitals 项目监控后台耗电,目前还在推进过程中,高耗电Android应用提醒的标准,比如

对于应用开发者来说,目前检测自己的应用是否耗电,有以下几个方案

方案1 电流仪测试法

通过外部电流设备,测试当前应用的耗电,同时由于我们可以获取power_profile.xml 文件,因此可以通过电流仪解析各个模块的对应耗电。

该方案优点是计算准确,缺点是硬件设备投入高且无法定位出是哪个具体代码原因导致的电量消耗。

方案2 Battery Historian

Battery Historian,是谷歌官方提供给应用的耗电检测工具,只需简单的操作配置后,我们能够得到当前运行时应用各模块耗电信息。

覆盖范围包括了所有耗电模块信息(包括了cpu,wifi,gps等),该方案优点是实施较为简单,也能得到较为精确的耗电数据,也能得到相关的耗电详情,缺点是无法定位出是代码中哪部分引起的耗电。

方案3 插桩法

我们可以通过插桩的方式,在相关的耗电api中进行字节码插桩,通过调用者的频次,调用时间进行一定的收集整理,最终可以得到相关的耗电数据,同时因为对代码进行了插桩,我们也能够获取相关调用者的数据,便于之后的代码分析,该方案优点是能够精确定位出代码调用级别的问题,缺点是耗电量化相对于前两个方案来说不那么精确,同时对于插桩api的选择也要有一定的了解,对于数据的整合需要开发。

方案选择

通过对现有方案的调研,我们最终使用了方案3 插桩法,因为耗电有一方面是客观原因,比如对于货运司机app来说,定位数据的获取是伴随着整个app应用周期的,因此耗电量也肯定集中在这个部分。选择插桩法能让我们快速定位出某些不合理的耗电调用,从而达到在不影响业务前提下进行优化。

既然选择了方案3,那么我们需要明确一下插桩的api选择,由于现在行业内并没有相关的开源,较为相关的是以Battery-metrics为代表的定制化检测工具,Battery-metrics依靠插桩的方式,统计了多个部分的耗电时长与使用频率,但是虽然数据处理这部分开源了,但是对于插桩这部分却没有开源,因此对于插桩api的选择,我们参考了 Android Vitals 监控后台耗电的规则

同时补充了我们收集的常见耗电api数据进行补充,且我们最终的耗电模块一定是系统耗电模块的子集,这里选取的是bluetooth,cpu,location,sensor,wakelock来分析,同时还要一个特别的alarm模块也需要加入,因为alarm属于杂项耗电的一种,部分厂商也会对alarm进行监控(alarm过多也会提示应用频繁,降低耗电等),好了,目标明确,我们进行开发。

耗电监控实现

这里分为两个部分,一部分是耗电 api 的选择,一部分是ASM插桩实现

耗电api选择

BlueTooth

蓝牙部分中,扫描部分是主要的耗电存在,属于梯度耗电计算,功耗模块中也有wifi.scan去记录蓝牙的扫描功耗,通常我们可以通过以下方式开启扫描

val bluetooth = this.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManagerval scanSettings: ScanSettings = ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) // 设置连续扫描.build()val scanner = bluetooth.adapter.bluetoothLeScannerval callback = object : ScanCallback() {override fun onBatchScanResults(results: MutableList<ScanResult>?) {super.onBatchScanResults(results)}override fun onScanFailed(errorCode: Int) {super.onScanFailed(errorCode)}override fun onScanResult(callbackType: Int, result: ScanResult?) {super.onScanResult(callbackType, result)}}scanner.startScan(null, scanSettings, callback)

其中值得注意的是,ScanSettings中可以配置ScanMode,这里的ScanMode的设置不同对耗电也有不同的影响

  • SCAN_MODE_LOW_POWER : 低功耗模式,默认此模式,如果应用不在前台,则强制此模式
  • SCAN_MODE_BALANCED :平衡模式,一定频率下返回结果
  • SCAN_MODE_LOW_LATENCY :高功耗模式,建议应用在前台才使用此模式
  • SCAN_MODE_OPPORTUNISTIC:这种模式下, 只会监听其他APP的扫描结果回调

同时我们可以通过

scanner.stopScan(callback)

关闭本次蓝牙扫描,到这里,我们就明白了,我们主要关注的插桩api是startScan(开启扫描)stopScan(停止扫描),并记录耗电时间与当次扫描模式,以便后续按需进行优化。

cpu

cpu的使用时长我们可以通过读取/proc/self/stat文件获取,得到的数据

24010 (cat) R 24007 24010 24007 34817 24010 4210688 493 0 0 0 1 0 0 0 20 0 1 0 42056617 2184900608 898 18446744073709551615 392793329664 392793777696 549292849424 0 0 0 0 0 1073775864 0 0 0 17 1 0 0 0 0 0 392793800160 392793810520 393204342784 549292851860 549292851880 549292851880 549292855272 0

上面的数据我们需要第14项-17项,分别是用户态运行时间,内核态运行时间,用户态下等待子进程运行的时间(子进程运行时间),内核态下等待子进程运行的时间(子进程运行时间),我们可以在linux manual上看到各个项的含义。

值得注意的是,我们得到的时间是以clock ticks(cpu时钟节拍)计算的,所以我们需要获取cpu运行了多少秒的话,那么就需要cpu每秒的节拍,这个可以通过

Os.sysconf(OsConstants._SC_CLK_TCK)

获取,通过两者相除,我们就能得到程序cpu在用户态以及内核台运行的时间。

定位

定位也是一个耗电巨头,常见的定位是gps,当然获取不到gps定位时也会切换成net网络定位,还有wifi辅助定位等等,我们一般通过requestLocationUpdates发起一次持续定位,requestSingleUpdate发起一次单次定位。虽然requestSingleUpdate会让定位provider(比如gps)保持一定的活跃时间,但是单次定位的消耗远远小于requestLocationUpdates持续定位,我们来关注一下requestLocationUpdates,它有很多重载的函数,

fun requestLocationUpdates(provider: String,minTime: Long,minDistance: Float,listener: LocationListener) 

我们以此函数为例子

  • provider指当前定位由谁提供,常见有gps,network
  • minTime表示经过当前时间后,会重新发起一次定位
  • minDistance表示超过当前距离后,也会重新发起一次定位
  • listener就是当前定位信息的回调

持续定位存在耗电主要有以下方面:1.定位时间长,比如只有requestLocationUpdates,而没有removeUpdates,导致定位在全局范围使用 2.minTime配置时间过短,导致定位频繁 3.minDistance距离过短,也会导致定位频繁。

取消定位可以通过removeUpdates去取消,当然官方推荐是需要时就开启requestLocationUpdates,不需要就要通过removeUpdates及时关闭,达到一个性能最优的状态,当然实际开发中,我们会遇到其他三方的sdk,比如百度定位,高德定位等,因为是第三方,一般都会内部封装了requestLocationUpdates的调用。

因此,我们需要进行插桩的api就是requestLocationUpdates与removeUpdates啦,两次调用的时间间隔就是定位的耗时。

sensor

sensor 传感器也是按照梯度计算的,主要是通过时的samplingPeriodUs,与maxReportLatencyUs区分不同的梯度

public boolean registerListener(SensorEventListener listener, Sensor sensor,int samplingPeriodUs, int maxReportLatencyUs) {int delay = getDelay(samplingPeriodUs);return registerListenerImpl(listener, sensor, delay, null, maxReportLatencyUs, 0);}
  • samplingPeriodUs两次传感器事件的最小间隔(ms)可以理解为采样率,就算我们指定了这个参数,实际调度也是按照系统决定,samplingPeriodUs越大耗电越少
  • maxReportLatencyUs 允许被延迟调度的最大时间,默认为0,即希望立即调度,但是实际上也是由系统决定调度。

同样的,我们也可以通过unregisterListener取消当前的sensor监听,还是跟定位一样,官方建议我们按需使用。

通过对sensor的理解,我们会发现sensor一般是由厂商定制化决定的调度时间,我们设定的参数会有影响,不过实际也是按照系统调度,传感器事件产生时,会放入一个队列(队列大小可由厂商定制)中,当系统处于低功耗时,非wakeup的sensor就算队列满了,也不会退出低功耗休眠模式。相反,如果属于wakeup的sensor,系统就会退出休眠模式在队列满之前处理事件,进一步加大耗电,因为会使得AP(Application Processor AP是ARM架构的处理器)处于非休眠状态,该状态下ap能耗至少在50mA。我们判断sensor是否wakeup可通过

public boolean isWakeUpSensor() {return (mFlags & SENSOR_FLAG_WAKE_UP_SENSOR) != 0;}

因此对于sensor我们主要插桩的api是registerListener与unregisterListener,统计sensor的耗时与wakeup属性。

wakelock

wakelock是一种锁的机制,只要有应用持有这个锁,CPU就无法进入休眠状态(AP处理会处于非休眠状态),会一直处于工作状态。因此就算是屏幕处于熄屏状态,我们的系统也无法进行休眠,不仅如此,一些部分厂商也会通过wakelock持有市场,会弹窗提示应用耗电过多,因为无法进入低功耗状态,所以往往会放大其他模块的耗电量,即使用户什么也没做。因此如果cpu异常的app,可以排查wakelock的使用(往往是因为这个导致非使用app耗电,比如把应用放一晚上,第二天没电的情况,可着重排查wakelock使用)。

wakelock包括PowerManager.WakeLock与WifiManager.WifiLock,两者都提供了acquire方法获取一个唤醒锁

val mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK or PowerManager.ACQUIRE_CAUSES_WAKEUP,this.javaClass.canonicalName)mWakeLock.setReferenceCounted(false)mWakeLock.acquire()

其中这个lock默认是引用计数的。怎么理解呢?就是调用acquire方法与调用release方法次数一致时,才会真正把这个锁给释放掉,否则会一直持有该lock,因此,它是一个隐藏很深的耗电刺客,需要时刻注意。当然我们也可以取消引用计数机制,可通过setReferenceCounted(false)设置,此时调用一次release即可释放掉该lock。

值得一提的是,acquire也提供了timeout释放的策略

public void acquire(long timeout) {synchronized (mToken) {acquireLocked();mHandler.postDelayed(mReleaser, timeout);}}
private final Runnable mReleaser = () -> release(RELEASE_FLAG_TIMEOUT);

本质也是通过handler进行的postDelayed然后时间到了调用release方法释放。

因此我们插桩的api是acquire,setReferenceCounted以及release函数,获取wakelock的基础信息以及持有时长。

alarm

alarm严格来说并不在我们上述的耗电计算中,属于杂项耗电,但是alarm通常会被乱用,同时部分精确的闹钟(比如setAlarmClock方法)会在low-power idle mode下,也会被触发,导致低功耗模式下也进一步耗电。同时精确闹钟会脱离了系统以耗电最优的周期去触发alarm,因此耗电效率不高但是较为“守时”。(普通的alarm会被系统安排在一定的周期进行)

该图引自分析 Android 耗电原理后,飞书是这样做耗电治理的

因为set方法会被系统调度,所以我们本次不在此讨论,我们分析精确闹钟的api即可,分别是

low-power idle 能执行public void setAlarmClock(AlarmClockInfo info, PendingIntent operation) {setImpl(RTC_WAKEUP, info.getTriggerTime(), WINDOW_EXACT, 0, 0, operation,null, null, (Handler) null, null, info);}
low-power idle 能执行public void setExactAndAllowWhileIdle(@AlarmType int type, long triggerAtMillis,PendingIntent operation) {setImpl(type, triggerAtMillis, WINDOW_EXACT, 0, FLAG_ALLOW_WHILE_IDLE, operation,null, null, (Handler) null, null, null);}
low-power idle 不执行,但是精确闹钟会一定程度阻碍了系统采取耗电最优的方式进行触发public void setExact(@AlarmType int type, long triggerAtMillis, PendingIntent operation) {setImpl(type, triggerAtMillis, WINDOW_EXACT, 0, 0, operation, null, null, (Handler) null,null, null);}

因为alarm的特性,很多应用都采取alarm进行任务的调度,但是更加好的做法是,如果是应用内的定时任务,官方更加推荐直接采用Handler去实现,同时如果是后台任务,更好的做法也是采用Worker Manager去实现。但是因为历史原因,alarm其实还是被滥用的风险还是很高的,因此我们还是要对setExact,setExactAndAllowWhileIdle,setAlarmClock去进行插桩监控

耗电统计

到最后,我们如何获取耗电百分比呢?其实我们可以直接通过广播去获取当前的电量level,单位时间后再次获取,就是我们这段时间的耗电了,可以通过

val intent = registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))val level = intent?.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) ?: -1val scale = intent?.getIntExtra(BatteryManager.EXTRA_SCALE, -1) ?: -1

因为电量变化是一个粘性广播,我们可以直接从intent的返回获取到当前的电量数值,同时也可以通过注册一个广播接听当前是否处于充电状态

override fun onReceive(context: Context?, intent: Intent?) {synchronized(this) {when (intent?.action) {Intent.ACTION_POWER_CONNECTED -> {receive.invoke(SystemClock.elapsedRealtime(), Intent.ACTION_POWER_CONNECTED)}Intent.ACTION_POWER_DISCONNECTED -> {receive.invoke(SystemClock.elapsedRealtime(), Intent.ACTION_POWER_DISCONNECTED)}}}}

ASM插桩实现

通过耗电api的选择这一部分的介绍,我们能够得到了具体要进行字节码插桩的api,其实他们插桩的思路都大体一致,我们以wifiLock举例子。

我们首先要明确我们需要的统计信息是什么:

  1. 函数调用时的参数:我们知道耗电会有梯度计算,不同模式下耗电影响也不同,所以我们需要发出api调用时的参数,才能做归类统计
  2. 调用时的调用者:只是知道耗电处是不够的,还要知道是谁发起的调用,方便我们后续排查问题。

因此,我们需要调用函数的时候,不仅要能够保证获取函数原本的参数调用,同时也要添加调用者参数。我们来看一下原本的wifiLock的使用以及对应编译后的字节码:

val wifiLock: WifiManager.WifiLock =wifiManager.createWifiLock(WifiManager.WIFI_MODE_FULL, "mylock")wifiLock.acquire()
   L4LINENUMBER 92 L4ALOAD 2 ICONST_1LDC "mylock"INVOKEVIRTUAL android/net/wifi/WifiManager.createWifiLock (ILjava/lang/String;)Landroid/net/wifi/WifiManager$WifiLock;ASTORE 4ALOAD 4LDC "wifiManager.createWifiLock(WifiManager.WIFI_MODE_FULL, "mylock")"INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullExpressionValue (Ljava/lang/Object;Ljava/lang/String;)VALOAD 4L5LINENUMBER 91 L5ASTORE 3 将对象wifilock存入了index为3的局部变量表L6LINENUMBER 93 L6ALOAD 3  在局部变量表取出index为3的对象 wifilockINVOKEVIRTUAL android/net/wifi/WifiManager$WifiLock.acquire ()V

可以看到,字节码调用上本来就存在着环境相关的指令,比如ALoad,虽然跟我们的acquire方法调用无关,但是我们不能破坏指令的结构,因此我们在不破坏操作数栈的情况下,可以采用同类替换的方式,即把属于WifiLock的acquire方法转化为我们自定义的acquire方法,转化后实际调用如下:

val wifiLock: WifiManager.WifiLock =wifiManager.createWifiLock(WifiManager.WIFI_MODE_FULL, "mylock")// 替换后wifiLock.acquire() ===> WifiWakeLockHook.acquire(wifiLock)

那么我们转化的时候,就应该考虑的是:acquire被调用时,操作数栈其实隐含了一个wifilock对象,所以才能采用INCVOKEVIRTUAL的指令调用,如果想要调用变成我们自定义的hook类的话,我们也需要把wifilock对象当作参数列表的第一个参数传入,同时为了保持操作数栈的结果,可以把INCVOKEVIRTUAL指令改为INVOKESTATIC指令,同时我们也需要记录当前的调用者类名,我们就需要通过一个LDC指令把类名信息放入操作数栈中,通过INVOKESTATIC指令调用一个全新的方法,原理讲解完毕,我们一步步开始实现:

自定义Hook类,即WifiWakeLock 调用方法的替代实现

@Keepobject WifiWakeLockHook {@Synchronized@JvmStaticfun setReferenceCounted(wakeLock: WifiLock, value: Boolean) {if (wifiWakeLockRecordMap[wakeLock.hashCode()] == null) {wifiWakeLockRecordMap[wakeLock.hashCode()] = WakeLockData()}with(wifiWakeLockRecordMap[wakeLock.hashCode()]!!) {isRefCounted = value}wakeLock.setReferenceCounted(value)}@JvmStaticfun acquire(wifiLock: WifiLock, acquireClass: String) {wifiLock.acquire()if (wifiWakeLockRecordMap[wifiLock.hashCode()] == null) {wifiWakeLockRecordMap[wifiLock.hashCode()] = WakeLockData()}with(wifiWakeLockRecordMap[wifiLock.hashCode()]!!) {acquireTime++if (startHoldTime == 0L) {startHoldTime = SystemClock.uptimeMillis()}holdClassName = acquireClass}}@JvmStaticfun release(wifiLock: WifiLock, releaseClass: String) {wifiLock.release()if (wifiWakeLockRecordMap[wifiLock.hashCode()] == null) {throw NoRecordException()}with(wifiWakeLockRecordMap[wifiLock.hashCode()]!!) {heldTime = SystemClock.uptimeMillis() - startHoldTimereleaseTime++releaseClassName = releaseClass}}}

同时我们把需要记录的数据放在一个map中,为了不产生内存泄漏,我们可以直接存入对象的hashcode作为key,同时value为我们自定义的需要采集的数据。

class WakeLockData() {// acquire 方法调用次数var acquireTime: Int = 0// 释放次数var releaseTime: Int = 0// 最终持有唤醒的时间 = 最后release - startHoldTimevar heldTime: Long = 0L// 开始唤醒的时间var startHoldTime: Long = 0L// 是否采用了引用计数var isRefCounted = true// 针对调用acquire(long timeout)却不调用release 的场景var autoReleaseByTimeOver: Long = 0L// 自动release 次数var autoReleaseTime: Int = 0var holdClassName :String = ""var releaseClassName:String = ""// WakeLock 是否已经被释放fun isRelease(): Boolean {if (!isRefCounted) {if (releaseTime > 0) {return true}} else {if (acquireTime == releaseTime) {return true}// 如果acquire的次数 == releaseTime && 超时删除acquire已超时if ((acquireTime - autoReleaseTime) == releaseTime && SystemClock.uptimeMillis() - autoReleaseByTimeOver > 0) {return true}}return false}override fun toString(): String {return "WakeLockData(acquireTime=$acquireTime, releaseTime=$releaseTime, heldTime=$heldTime, startHoldTime=$startHoldTime, isRefCounted=$isRefCounted, autoReleaseByTimeOver=$autoReleaseByTimeOver, autoReleaseTime=$autoReleaseTime, holdClassName='$holdClassName', releaseClassName='$releaseClassName')"}}

ASM Hook

进行hook之前,我们需要找到我们想要hook的函数特征,我们想要hook的函数是acquirerelease的,即如何唯一识别一个函数,有三大法宝:

  • 函数名:MethodInsnNode中的name,本例子分别是 acquire 与 release
  • 函数签名:MethodInsnNode中的desc,本例子函数签名都是()V
  • 函数调用者:MethodInsnNode的owner,本例子android/net/wifi/WifiManager$WifiLock,WifiLock是WifiManager的内部类

通过这一步,我们能够找到了我们想要的函数,这样就不会因为错误的hook导致其他函数的改变,接着我们根据上述思想,在这个方法的指令集中进行我们的“小操作”

按照流程图,wifilock对象我们不需要改变,接着我们希望在调用函数最后加上调用者名称,这个加的位置是在所以应调函数的背后,比如 acquire() 函数,我们加上调用者名称后就变成这样了acquire(String 调用者名称) ,上面我们能够了解,我们可以通过MethodInsnNode的owner属性获取,接着我们通过LDC指令即可把这个字符串打入操作数栈,最后INVOKESTATIC调用自定义类的hook方法即可,最后别忘了修改函数签名(desc) ,因为我们要调用的函数指令已经变成了自定义类的函数指令

由于我们需要hook的api都是INVOKEVIRYTUAL指令,所以我们可以采用上述的思想,形成一个工具类

ASM tree apipublic class HookHelper {static public void replaceNode(MethodInsnNode node, ClassNode klass, MethodNode method, String owner) {LdcInsnNode ldc = new LdcInsnNode(klass.name);method.instructions.insertBefore(node, ldc);node.setOpcode(Opcodes.INVOKESTATIC);int anchorIndex = node.desc.indexOf(")");String subDesc = node.desc.substring(anchorIndex);String origin = node.desc.substring(1, anchorIndex);node.desc = "(L" + node.owner + ";" + origin + "Ljava/lang/String;" + subDesc;node.owner = owner;System.out.println("replaceNode result is " + node.desc);}}
  • node 当前方法的某一条指令
  • klass 当前调用class
  • method 当前方法
  • owner 为我们需要变更后的hookclass的类名

数据层

通过以上步骤,我们能够拿到所需的所有数据了,这里再做一个统一的管理

同时各个数据的暴露方式可通过接口的方式提供给调用层,当数据更新时,调用者只需关心自己感兴趣的部分即可

当然,我们默认也可以有一个debug环境下可使用的调试页面,方便及时查看自己想要的数据。

后续补充方案

我们详细介绍了耗电定位的做法,把复杂的耗电量计算转换为单位时间内,耗电时长计算与实际耗电 api 模块调用次数去计算, 当然这并不是终点,通过插桩我们可以得到各个模块的使用时间与使用次数,在后续计划中,我们可以通过对power_profile的解析,拿到不同手机的的模块数据,通过耗电量 = (各个模块调用时间 * 各个单位模块功耗)就可以低成本去量化耗电量这个指标,提供给更多的业务使用。

该方案优点如下:

优点
基于aop方案字节码插桩asm实现,对代码无侵入
自动初始化
可根据调用粒度(比如调用次数)按需dump调用
可记录调用者class,方便后续排查问题,精准定位“bad sdk”

总结

通过上面,我们能够了解到自定义耗电检测方案的原理与实现,当然具体需要采集的数据以及比对我们可自定义处理。

本文转自 [https://juejin.cn/post/7181644426257629243],如有侵权,请联系删除。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/57343.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

如何降低android应用程序的耗电量

转自&#xff1a;http://www.apkbus.com/forum.php?modviewthread&tid5459&extrapage%3D3 如果手机&#xff08;移动设备&#xff09;没电了&#xff0c;你的程序还能运行吗&#xff1f; 哈哈&#xff0c;这是地球人都知道的问题&#xff0c;那么如何才能降低androi…

IOS耗电量测试(一)耗电量数据获取

转载&#xff1a;https://blog.csdn.net/redcard0/article/details/89030124 随着游戏越来越重度&#xff0c;游戏耗电太高造成游戏发烫的投诉量已经仅次于帧率&#xff0c;高于针对内存崩溃的投诉。优化的前提是耗电量数据可以度量&#xff0c;本文主要阐述耗电量数据如何获取…

APP专项测试之耗电量测试

一、耗电量测试分析 相对于PC端来说&#xff0c;移动设备的电池电量是非常有限的&#xff0c;保持持久的续航能力尤为重要。Android的很多特性都比较耗电&#xff08;如屏幕、GPS、sensor传感器、唤醒机制、CPU、连网等的使用&#xff09;&#xff0c;我们必须要慎重检查APP的…

如何测试Android APP的耗电量?

现在可以使用google提供的battery-historian来测试&#xff0c;适用条件&#xff1a;5.0及以上手机。 battery-historian链接&#xff1a;google/battery-historian android吧 所以的android都自带的功能 设置--->电池/电源管理/ MQC在兼容性测试、功能测试、稳定性测试中都…

app耗电量测试

目录 目录 1. 引言 2. 测试方法 2.1. 直接观察 2.2. 使用adb命令进行统计 3. 典型的耗电场景 3.1. 定位 3.2. 网络传输 3.3. 音视频播放 4. app电量分析工具 4.1. Batterystats 4.2. Battery Historian 5. 环境安装 5.1. adb命令 5.2. 安装go 5.3. 安装git 5.4…

盘点COVID-19新冠药物和疫苗研发进展

COVID-19是由严重急性呼吸系统综合症冠状病毒2&#xff08;SARS-CoV-2&#xff09;引起的一种传染病&#xff0c;这是一种单股正链RNAβ冠状病毒&#xff0c;它是Beta-CoV谱系B&#xff08; Sarbecovirus亚属&#xff09;。 COVID-19代表着全球健康威胁&#xff0c;并且是可能引…

药物临床试验数据递交FDA的规定

信息来源&#xff1a; https://www.fda.gov/industry/fda-data-standards-advisory-board/study-data-standards-resources STUDY DATA TECHNICAL CONFORMANCE GUIDE v4.9 (March 2022) (研究数据技术一致性指南) 仅提取该文档中的部分内容加以翻译&#xff0c;以下中文都是…

姜敬哲/孙燕妮/原丽红合作开发可用于病毒快速分类的工具PhaGCN2

南海水产研究所姜敬哲团队、香港城市大学孙燕妮团队、广东药科大学原丽红合作开发的可用于病毒快速分类生信工具 使用PhaGCN2对病毒基因组片段分类 Virus classification for viral genomic fragments using PhaGCN2 文章链接&#xff1a;https://www.researchsquare.com/artic…

多组学在药物机制解析和诊断标志物开发中的应用

链接&#xff1a;多组学在药物机制解析和诊断标志物开发中的应用_哔哩哔哩_bilibili 药物研发流程和多组学前沿技术 药物研发流程遇到的挑战 流程&#xff1a;新药的发现——临床前研究——临床研究 挑战&#xff1a; 诊断是否清晰、机制是否明确、靶点是否可靠、药物是否有…

药物研发早期预测细胞毒性的解决方案

药物从设计到上市的整个研究阶段&#xff0c;毒性通常是导致研发失败的最主要原因。即使上市以后还有许多药物由于出现研发阶段未能发现的毒性而被撤市或严格限制使用。因此&#xff0c;如何及时、准确、快速地评价药物毒性便成为药物开发中的一个重要问题。 在体外快速的、高…

针对新冠病毒,盛普始终坚持多靶标协同的防治病毒药物研发

全球范围内的新冠确诊人数仍旧在增加,“特效治疗药”依然没有找到。凛冬已至,更适于病毒传播的寒冷高湿天气已经笼罩了半个地球。在一片阴霾的疫情当中,“希望之光”开始浮现,新冠疫苗和单一靶标的小分子药物开始被世界各国政府应急准入市场。这些疫苗和小分子药物依靠海量…

3CLpro蛋白酶抑制剂,如何靠实力进入新冠病毒诊疗方案

3月15日&#xff0c;国家卫健委发布《新型冠状病毒肺炎诊疗方案&#xff08;试行第九版&#xff09;》&#xff0c;在抗病毒治疗中&#xff0c;抗新冠病毒药物PF-07321332/利托那韦片&#xff08;Paxlovid&#xff09;被写入诊疗方案。Paxlovid是由两种抗病毒药物组成&#xff…

关于CV算法岗就业相关问题,精华回答分享

粉丝提问&#xff1a; 你好&#xff0c;看星球上做前端&#xff0c;后端&#xff0c;java 的人比较多&#xff0c;好像没有看到有多少人做算法&#xff0c;我现在已经毕业了&#xff0c;是一名 cv 算法工程师&#xff0c;但是我现在很苦恼&#xff0c;感觉自己代码能力很弱&am…

chatgpt赋能python:Python自动打开软件:实现轻松便捷的操作

Python自动打开软件&#xff1a;实现轻松便捷的操作 在许多工作场所&#xff0c;我们可能需要反复打开某个软件&#xff0c;费时费力。那么有没有一种方式能够轻松地自动打开特定软件呢&#xff1f;此时Python便是一个好帮手。PythonとWINDOWS关联性较强&#xff0c;可以方便地…

Golang/Python 调用 openAI 的API 详解

学习目标&#xff1a; OpenAI API介绍 学习如何通过 Golang 使用 OpenAI 的 API OpenAI 的常用的参数及其说明 了解OpenAI API 中令牌&#xff08;tokens) OpenAI API 提供了几个不同的终端点&#xff08;endpoints&#xff09;和模式&#xff08;modes&#xff09; 复杂和…

我改了一行公共代码后,同事说要建个没我的小群

点击上方 前端Q&#xff0c;关注公众号 回复加群&#xff0c;加入前端Q技术交流群 嗨&#xff0c;大家好&#xff01;这里是道长王jj~ &#x1f3a9;&#x1f9d9;‍♂️ 今天我想和大家分享一个惨痛的教训&#xff0c;就是当我一意孤行地删掉一个看起来没用的配置文件时&#…

chatgpt赋能python:免费的Python教程:从入门到精通

免费的Python教程&#xff1a;从入门到精通 Python是如今最热门的编程语言之一&#xff0c;在众多编程语言中占据了重要的地位。Python有着简单易学的语法&#xff0c;充足的库支持&#xff0c;高效的执行速度和海量的社区支持&#xff0c;这些使得Python成为最适合初学者的编…

chatgpt赋能python:Python报错Module:从入门到精通

Python报错Module&#xff1a;从入门到精通 在Python编程中&#xff0c;我们经常会遇到各种各样的错误。其中&#xff0c;Module错误是最常见的一种。在这篇文章中&#xff0c;我们将深入探讨Python报错Module的各种原因和解决方法&#xff0c;并希望能够帮助那些遇到这种问题…