背景

一般来说,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 中供给组件的电源配置文件,以此声明自身各个组件的功耗(文档)

一种Android应用耗电定位方案

功耗文件获取

一般,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 registration
    mPowerCalculators.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供给的信息,关于手机厂商来说,是可以在此根底上添加多种耗电检测手法,因而处于一个“大杂烩”的现象。在Android P 及以上版别,谷歌官方推出了 Android Vitals 项目监控后台耗电,现在还在推动过程中,高耗电Android运用提示的标准,比方

一种Android应用耗电定位方案

关于运用开发者来说,现在检测自己的运用是否耗电,有以下几个计划

计划1 电流仪测试法

经过外部电流设备,测试当时运用的耗电,一起由于咱们可以获取power_profile.xml 文件,因而可以经过电流仪解析各个模块的对应耗电。

一种Android应用耗电定位方案

该计划长处是核算准确,缺陷是硬件设备投入高且无法定位出是哪个详细代码原因导致的电量耗费。

计划2 Battery Historian

Battery Historian,是谷歌官方供给给运用的耗电检测东西,只需简略的操作配置后,咱们可以得到当时运转时运用各模块耗电信息。

一种Android应用耗电定位方案

掩盖规模包含了一切耗电模块信息(包含了cpu,wifi,gps等),该计划长处是实施较为简略,也能得到较为准确的耗电数据,也能得到相关的耗电概况,缺陷是无法定位出是代码中哪部分引起的耗电。

计划3 插桩法

咱们可以经过插桩的办法,在相关的耗电api中进行字节码插桩,经过调用者的频次,调用时刻进行必定的收集整理,终究可以得到相关的耗电数据,一起由于对代码进行了插桩,咱们也可以获取相关调用者的数据,便于之后的代码剖析,该计划长处是可以准确定位出代码调用等级的问题,缺陷是耗电量化相关于前两个计划来说不那么准确,一起关于插桩api的挑选也要有必定的了解,关于数据的整合需求开发。

计划挑选

经过对现有计划的调研,咱们终究运用了计划3 插桩法,由于耗电有一方面是客观原因,比方关于货运司机app来说,定位数据的获取是伴随着整个app运用周期的,因而耗电量也肯定会集在这个部分。挑选插桩法能让咱们快速定位出某些不合理的耗电调用,然后到达在不影响事务前提下进行优化。

已然挑选了计划3,那么咱们需求明确一下插桩的api挑选,由于现在行业内并没有相关的开源,较为相关的是以Battery-metrics为代表的定制化检测东西,Battery-metrics依托插桩的办法,核算了多个部分的耗电时长与运用频率,可是尽管数据处理这部分开源了,可是关于插桩这部分却没有开源,因而关于插桩api的挑选,咱们参阅了 Android Vitals 监控后台耗电的规矩

一种Android应用耗电定位方案

一起弥补了咱们收集的常见耗电api数据进行弥补,且咱们终究的耗电模块必定是体系耗电模块的子集,这儿选取的是bluetooth,cpu,location,sensor,wakelock来剖析,一起还要一个特别的alarm模块也需求参加,由于alarm归于杂项耗电的一种,部分厂商也会对alarm进行监控(alarm过多也会提示运用频频,降低耗电等),好了,方针明确,咱们进行开发。

耗电监控完成

这儿分为两个部分,一部分是耗电 api 的挑选,一部分是ASM插桩完成

耗电api挑选

BlueTooth

蓝牙部分中,扫描部分是首要的耗电存在,归于梯度耗电核算,功耗模块中也有wifi.scan去记载蓝牙的扫描功耗,一般咱们可以经过以下办法敞开扫描

val bluetooth = this.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
val scanSettings: ScanSettings = ScanSettings.Builder()
    .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) // 设置连续扫描
    .build()
val scanner = bluetooth.adapter.bluetoothLeScanner
val 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应用耗电定位方案

该图引自剖析 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) ?: -1
val 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()
   L4
    LINENUMBER 92 L4

    ALOAD 2 

    ICONST_1

    LDC "mylock"

    INVOKEVIRTUAL android/net/wifi/WifiManager.createWifiLock (ILjava/lang/String;)Landroid/net/wifi/WifiManager$WifiLock;

    ASTORE 4

    ALOAD 4

    LDC "wifiManager.createWifiLock(WifiManager.WIFI_MODE_FULL, "mylock")"

    INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullExpressionValue (Ljava/lang/Object;Ljava/lang/String;)V

    ALOAD 4

   L5
    LINENUMBER 91 L5

    ASTORE 3 将目标wifilock存入了index为3的局部变量表

   L6
    LINENUMBER 93 L6

    ALOAD 3  在局部变量表取出index为3的目标 wifilock

    INVOKEVIRTUAL 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)

一种Android应用耗电定位方案

那么咱们转化的时分,就应该考虑的是:acquire被调用时,操作数栈其实隐含了一个wifilock目标,所以才能选用INCVOKEVIRTUAL的指令调用,假如想要调用变成咱们自定义的hook类的话,咱们也需求把wifilock目标当作参数列表的第一个参数传入,一起为了坚持操作数栈的成果,可以把INCVOKEVIRTUAL指令改为INVOKESTATIC指令,一起咱们也需求记载当时的调用者类名,咱们就需求经过一个LDC指令把类名信息放入操作数栈中,经过INVOKESTATIC指令调用一个全新的办法,原理讲解完毕,咱们一步步开端完成:

自定义Hook类,即WifiWakeLock 调用办法的代替完成

@Keep
object WifiWakeLockHook {
    @Synchronized
    @JvmStatic
    fun setReferenceCounted(wakeLock: WifiLock, value: Boolean) {
        if (wifiWakeLockRecordMap[wakeLock.hashCode()] == null) {
            wifiWakeLockRecordMap[wakeLock.hashCode()] = WakeLockData()
        }
        with(wifiWakeLockRecordMap[wakeLock.hashCode()]!!) {
            isRefCounted = value
        }
        wakeLock.setReferenceCounted(value)
    }
    @JvmStatic
    fun 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
        }
    }
    @JvmStatic
    fun release(wifiLock: WifiLock, releaseClass: String) {
        wifiLock.release()
        if (wifiWakeLockRecordMap[wifiLock.hashCode()] == null) {
            throw NoRecordException()
        }
        with(wifiWakeLockRecordMap[wifiLock.hashCode()]!!) {
            heldTime = SystemClock.uptimeMillis() - startHoldTime
            releaseTime++
            releaseClassName = releaseClass
        }
    }
}

一起咱们把需求记载的数据放在一个map中,为了不发生内存走漏,咱们可以直接存入目标的hashcode作为key,一起value为咱们自定义的需求采集的数据。

class WakeLockData() {
    // acquire 办法调用次数
    var acquireTime: Int = 0
    // 开释次数
    var releaseTime: Int = 0
    // 终究持有唤醒的时刻 = 最终release - startHoldTime
    var heldTime: Long = 0L
    // 开端唤醒的时刻
    var startHoldTime: Long = 0L
    // 是否选用了引证计数
    var isRefCounted = true
    // 针对调用acquire(long timeout)却不调用release 的场景
    var autoReleaseByTimeOver: Long = 0L
    // 自动release 次数
    var autoReleaseTime: Int = 0
    var 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导致其他函数的改变,接着咱们依据上述思维,在这个办法的指令会集进行咱们的“小操作”

一种Android应用耗电定位方案

依照流程图,wifilock目标咱们不需求改变,接着咱们期望在调用函数最终加上调用者称号,这个加的位置是在所以应调函数的背面,比方 acquire() 函数,咱们加上调用者称号后就变成这样了acquire(String 调用者称号) ,上面咱们可以了解,咱们可以经过MethodInsnNode的owner特点获取,接着咱们经过LDC指令即可把这个字符串打入操作数栈,最终INVOKESTATIC调用自定义类的hook办法即可,最终别忘了修正函数签名(desc) ,由于咱们要调用的函数指令现已变成了自定义类的函数指令

由于咱们需求hook的api都是INVOKEVIRYTUAL指令,所以咱们可以选用上述的思维,构成一个东西类


ASM tree api
public 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的类名

数据层

经过以上步骤,咱们可以拿到所需的一切数据了,这儿再做一个一致的办理

一种Android应用耗电定位方案

一起各个数据的露出办法可经过接口的办法供给给调用层,当数据更新时,调用者只需关心自己感兴趣的部分即可

一种Android应用耗电定位方案

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

一种Android应用耗电定位方案

后续弥补计划

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

该计划长处如下:

长处
根据aop计划字节码插桩asm完成,对代码无侵入
自动初始化
可依据调用粒度(比方调用次数)按需dump调用
可记载调用者class,便利后续排查问题,精准定位“bad sdk”

总结

经过上面,咱们可以了解到自定义耗电检测计划的原理与完成,当然详细需求采集的数据以及比对咱们可自定义处理。