Cold start in a 4.5 MB FOSS Android app: measure it with adb, then fix what's actually slow
Description and use
The workflow demonstrates that measuring cold start accurately, inspecting the artifact, and targeting real runtime work yields measurable improvements. The author’s fixes reduced the 8.4 MB dex size, eliminated unnecessary IPC, and added AOT compilation, all contributing to a faster cold start.
Problem
Cold start times are hard to improve without measurement; the author demonstrates how to measure and identify real bottlenecks in a small Android app.
Outcome
The workflow demonstrates that measuring cold start accurately, inspecting the artifact, and targeting real runtime work yields measurable improvements. The author’s fixes reduced the 8.4 MB dex size, eliminated unnecessary IPC, and added AOT compilation, all contributing to a faster cold start.
Workflow
- Force a cold start with adb:
adb shell am force-stop dev.weavrik.wifiwarn && adb shell am start -W -n dev.weavrik.wifiwarn/.MainActivityand record the timing output. - Run the command ~10 times, discard the first run, and take the median of the remaining runs.
- Unzip the APK and inspect classes.dex size with
unzip -l wifiwarn-v1.0.apk | grep -E "classes|\.dex|resources\.arsc"to confirm the artifact weight. - Examine the app’s onCreate and onResume code to identify synchronous work on the main thread.
- Apply fixes in impact order: (a) enable R8 and resource shrinking in build.gradle, (b) add a Baseline Profile via androidx.profileinstaller, (c) cache or remove duplicate IPC calls and guard migration logic, (d) avoid adding an Application subclass.
- Re‑measure after each change to verify median improvement, reverting any change that does not improve the median.
Evidence
Force a cold start: adb shell am force-stop dev.weavrik.wifiwarn adb shell am start -W -n dev.weavrik.wifiwarn/.MainActivity
unzip -l wifiwarn-v1.0.apk | grep -E "classes|\.dex|resources\.arsc" 8418928 classes.dex 982700 resources.arsc
Config.migrateLegacy() reads SharedPreferences and, on the legacy path, parses and rewrites a JSON blob — on the main thread, before the first frame.
getEnabledAccessibilityServiceList() and Settings.canDrawOverlays() are binder calls into system_server, and canDrawOverlays() is invoked twice per resume.
(a) Turn on R8. The biggest single lever for a FOSS app that vendors Material.
(b) Add a Baseline Profile. Without one, ART starts cold code in the interpreter and JITs it while your first screen draws.
(c) Get IPC off the critical path. Cache canDrawOverlays() once in onCreate, delete the duplicate call inside refreshSwitches()...
(d) Don't add an Application subclass casually. wifiwarn has none — good.
- Record date: 2026-11-02T05:14:28.969Z