How to Pinpoint Android App Power Drain with Custom ASM Instrumentation
This article explains a comprehensive approach to detecting and analyzing Android app power consumption by combining official power profiling, custom metrics, and bytecode instrumentation to capture API usage, caller information, and timing for modules such as Bluetooth, CPU, location, sensors, wakelocks, and alarms.
Background
Usually app power consumption receives less attention than crashes or ANRs. Power consumption is a hidden performance issue that varies with device hardware, usage duration, and user habits, making it difficult to standardize. Google provides the Battery Historian tool for objective measurement, while manufacturers use custom metrics. This article proposes a solution that combines official power calculation with custom detection.
Power Consumption Calculation
Android requires device manufacturers to include a component power profile file at /frameworks/base/core/res/res/power_profile.xml, which declares the power usage of each component.
Obtaining the Power Profile File
The power_profile.xml file is typically packaged inside /system/framework/framework-res.apk. Use adb pull /system/framework/framework-res.apk ./ to retrieve the APK, then decompile it with tools like apktool or jadx to locate /res/xml/power_profile.xml.
System Power Consumption
After obtaining the power profile, Android calculates consumption in BatteryStatsHelper. Most modules inherit from PowerCalculator and implement calculatorApp to compute duration‑based power usage. The refreshStats method shows the list of active calculators, such as CPU, Wi‑Fi, GPS, etc.
mPowerCalculators = new ArrayList<>();
// Add calculators in order
mPowerCalculators.add(new CpuPowerCalculator(mPowerProfile));
mPowerCalculators.add(new MemoryPowerCalculator(mPowerProfile));
// ... other calculators ...Power Consumption Detection
Google’s Battery Historian provides module‑level data, but custom detection is often needed. Starting with Android P, the Android Vitals project adds backend power monitoring.
Solution 1: Current Meter Test
Use an external current meter to measure the app’s power draw. By reading power_profile.xml, you can map current to individual modules. This method is accurate but requires expensive hardware and cannot pinpoint the exact code responsible for the consumption.
Solution 2: Battery Historian
Battery Historian is an official Google tool that, after simple configuration, reports per‑module power usage. It is easy to set up and provides fairly precise data, but it cannot identify which part of the code caused the consumption.
Solution 3: Instrumentation
By inserting bytecode instrumentation into power‑related APIs, we can collect detailed usage data and the caller class, enabling precise code‑level diagnosis. Although the quantification is less exact than a current meter, it allows us to locate problematic calls.
Choosing a Solution
Based on research, we selected Solution 3 (instrumentation). For example, in a freight‑driver app, location fetching dominates power usage; instrumentation helps identify unreasonable location calls without affecting business logic.
We reference Battery‑metrics for custom detection and Android Vitals for rule definitions.
Power API Selection
Bluetooth
Scanning is the main power consumer. Use
BluetoothManager.getAdapter().bluetoothLeScanner.startScan(...)and stopScan. Record the scan mode (low power, balanced, low latency, opportunistic) to calculate power gradients.
val bluetooth = this.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
val scanSettings = ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) // continuous scan
.build()
val scanner = bluetooth.adapter.bluetoothLeScanner
scanner.startScan(null, scanSettings, callback)SCAN_MODE_LOW_POWER – low power, default when app is background.
SCAN_MODE_BALANCED – balanced frequency.
SCAN_MODE_LOW_LATENCY – high power, recommended for foreground.
SCAN_MODE_OPPORTUNISTIC – listens to other apps' scans.
CPU
Read /proc/self/stat to obtain user and kernel time (fields 14‑17). Convert ticks to seconds using Os.sysconf(OsConstants._SC_CLK_TCK).
24010 R 24007 24010 24007 34817 24010 4210688 493 0 0 0 1 0 0 0 20 0 1 0 42056617 ...Fields 14‑17 correspond to user time, kernel time, user‑wait time, kernel‑wait time.
Location
Use requestLocationUpdates for continuous tracking and requestSingleUpdate for one‑time fixes. Continuous tracking consumes more power, especially if minTime or minDistance are too small.
fun requestLocationUpdates(provider: String, minTime: Long, minDistance: Float, listener: LocationListener) { ... }Cancel with removeUpdates. Third‑party SDKs often wrap these calls.
Sensor
Register listeners with
registerListener(listener, sensor, samplingPeriodUs, maxReportLatencyUs). Larger samplingPeriodUs reduces power; maxReportLatencyUs allows batching.
public boolean registerListener(SensorEventListener listener, Sensor sensor, int samplingPeriodUs, int maxReportLatencyUs) { ... }Check if a sensor is wake‑up with isWakeUpSensor(). Use unregisterListener to stop.
Wakelock
Both PowerManager.WakeLock and WifiManager.WifiLock keep the CPU awake. Use acquire(), optionally with a timeout, and release(). Reference counting can be disabled with setReferenceCounted(false).
val mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, this.javaClass.canonicalName)
mWakeLock.setReferenceCounted(false)
mWakeLock.acquire()Timeout version:
public void acquire(long timeout) { synchronized (mToken) { acquireLocked(); mHandler.postDelayed(mReleaser, timeout); } }Alarm
Alarms can be set with setAlarmClock, setExactAndAllowWhileIdle, or setExact. Precise alarms (e.g., setExact) break low‑power idle scheduling and increase consumption.
public void setAlarmClock(AlarmClockInfo info, PendingIntent operation) { setImpl(RTC_WAKEUP, info.getTriggerTime(), WINDOW_EXACT, 0, 0, operation, null, null, null); } public void setExactAndAllowWhileIdle(@AlarmType int type, long triggerAtMillis, PendingIntent operation) { setImpl(type, triggerAtMillis, WINDOW_EXACT, 0, FLAG_ALLOW_WHILE_IDLE, operation, null, null, null); }Power Consumption Statistics
Obtain the current battery level via the sticky broadcast ACTION_BATTERY_CHANGED. Record the level and scale, then compute the delta over a time interval.
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) ?: -1Listen for charging state changes with ACTION_POWER_CONNECTED and ACTION_POWER_DISCONNECTED.
override fun onReceive(context: Context?, intent: Intent?) {
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 Instrumentation Implementation
To replace calls like wifiLock.acquire() with a custom hook, we keep the original object on the stack, push the caller class name using LDC, and invoke a static method in a hook class. The original INVOKEVIRTUAL is changed to INVOKESTATIC while preserving the stack.
Custom Hook Class (WifiWakeLock)
@Keep
object WifiWakeLockHook {
@Synchronized @JvmStatic
fun setReferenceCounted(wakeLock: WifiLock, value: Boolean) { /* record */ }
@JvmStatic
fun acquire(wifiLock: WifiLock, acquireClass: String) { /* record start time, caller */ wifiLock.acquire() }
@JvmStatic
fun release(wifiLock: WifiLock, releaseClass: String) { /* record end time, caller */ wifiLock.release() }
}ASM Hook Helper
public class HookHelper {
static public void replaceNode(MethodInsnNode node, ClassNode klass, MethodNode method, String owner) {
// Insert LDC with caller class name
LdcInsnNode ldc = new LdcInsnNode(klass.name);
method.instructions.insertBefore(node, ldc);
// Change to static call
node.setOpcode(Opcodes.INVOKESTATIC);
// Adjust descriptor to add a String parameter
int anchor = node.desc.indexOf(')');
String subDesc = node.desc.substring(anchor);
String origin = node.desc.substring(1, anchor);
node.desc = "(L" + node.owner + ";" + origin + "Ljava/lang/String;" + subDesc;
node.owner = owner;
}
}Data Layer
Collected data is stored in a map keyed by the object's hashcode to avoid memory leaks. Each entry records acquire/release counts, timestamps, reference‑counting flag, and caller class names.
class WakeLockData {
var acquireTime: Int = 0
var releaseTime: Int = 0
var heldTime: Long = 0L
var startHoldTime: Long = 0L
var isRefCounted = true
var autoReleaseByTimeOver: Long = 0L
var autoReleaseTime: Int = 0
var holdClassName: String = ""
var releaseClassName: String = ""
fun isRelease(): Boolean { /* logic */ }
override fun toString() = "WakeLockData(acquireTime=$acquireTime, releaseTime=$releaseTime, heldTime=$heldTime, startHoldTime=$startHoldTime, isRefCounted=$isRefCounted, autoReleaseByTimeOver=$autoReleaseByTimeOver, autoReleaseTime=$autoReleaseTime, holdClassName='$holdClassName', releaseClassName='$releaseClassName')"
}The data layer exposes interfaces for other components to query specific metrics, and a debug UI can display the collected information in real time.
Future Enhancements
By parsing power_profile.xml for device‑specific module power values, we can compute actual energy consumption as energy = Σ (moduleTime × modulePower), providing a low‑cost quantitative metric for business use.
Conclusion
Custom ASM instrumentation offers a non‑intrusive, flexible way to monitor Android app power usage at the API level, allowing precise identification of inefficient code while keeping overhead low.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
