Design and Implementation of a High‑Performance Code Coverage Collection Solution for Android Apps
The paper presents a high‑performance Android code‑coverage solution that uses standard reflection to read the ClassLoader’s ClassTable, achieving over five‑times faster collection than existing tools while remaining stable, compatible, multi‑process capable, and enabling incremental, cloud‑based reporting for reducing app size.
Code coverage (Code coverage) is a metric in software testing that reflects the proportion and extent of code exercised by tests. In addition to test‑time coverage, runtime coverage in production is valuable because obsolete or unused code can increase package size and introduce stability risks.
The goal of this solution is to collect per‑class usage frequency on the cloud, upload the data, and provide query and reporting capabilities. The collected data helps decide which code to remove, how to allocate resources, and ultimately reduces the app package size while improving user experience.
A comparison table (not reproduced here) shows that the self‑developed scheme outperforms common approaches such as Jacoco, Hook PathClassLoader, and Hack‑based ClassTable access in performance, stability, compatibility, and impact on app size.
Principle : To know which classes are loaded at runtime, the solution queries the ClassLoader’s internal ClassTable field via standard reflection APIs, avoiding direct calls to findLoadedClass that would trigger class loading on Android’s PathClassLoader. By copying the ClassTable pointer, the method obtains class‑load status without hacks and with minimal overhead.
Key advantages :
Collection speed >5× faster than conventional schemes.
Uses only standard APIs to access ClassTable, ensuring excellent compatibility and stability.
Only one reflective call, no “black‑tech” hacks.
No impact on class loading or app runtime.
Fully supports multi‑process and plugin collection.
The approach is limited to Android N and above because the ClassTable field was introduced in that version.
Collection workflow : The process runs serially on the device. The app is split into host (main & child processes) and plugin parts. Each process provides an interface to query class‑load status. The main process orchestrates the collection, first gathering host classes, then invoking child‑process interfaces, and finally aggregating plugin data. Only classes not previously loaded are queried, reducing the amount of work over successive runs.
Version management : Since class names are obfuscated per build, coverage data is managed per app version. Each version clears previous data, and only newly loaded classes are reported, decreasing query volume over time.
Class name acquisition can be done in two ways:
Extract directly from the APK/Dex files via reflection on BaseDexClassLoader . public static List<String> getClassesFromClassLoader(BaseDexClassLoader classLoader) throws ClassNotFoundException, IllegalAccessException { // class name data resides in BaseDexClassLoader.pathList.dexElements.dexFile Field pathListF = ReflectUtils.getField("pathList", BaseDexClassLoader.class); pathListF.setAccessible(true); Object pathList = pathListF.get(classLoader); Field dexElementsF = ReflectUtils.getField("dexElements", Class.forName("dalvik.system.DexPathList")); dexElementsF.setAccessible(true); Object[] array = (Object[]) dexElementsF.get(pathList); Field dexFileF = ReflectUtils.getField("dexFile", Class.forName("dalvik.system.DexPathList$Element")); dexFileF.setAccessible(true); ArrayList<String> classes = new ArrayList<>(256); for (int i = 0; i < array.length; i++) { DexFile dexFile = (DexFile) dexFileF.get(array[i]); Enumeration<String> enumeration = dexFile.entries(); while (enumeration.hasMoreElements()) { classes.add(enumeration.nextElement()); } } return classes; }
Download class‑name data from a cloud service built during the CI process and cache it on the device.
For child‑process collection, an AIDL‑based remote interface is used. The main process queries the child process once, receiving a file that contains both loaded and unloaded class lists, thus minimizing cross‑process overhead.
Plugin collection follows a similar pattern: each plugin’s ClassLoader is inspected, data is written to per‑plugin files, and finally merged on the host side.
De‑mapping : Since the app is obfuscated, mapping files generated at build time are stored on the server. When coverage data is displayed, the server uses the app version to retrieve the appropriate mapping and restore original class names.
Data storage and incremental calculation : A simple database table with a primary‑key column class stores collected class names. Before each collection the row count is recorded; after collection the new row count minus the old count yields the incremental set, which is uploaded.
Performance and stability : Tests on apps with >50 000 classes show an average collection time of ~0.5 s per run, memory increase of ~500 KB, and no noticeable CPU impact. The solution has been validated on multiple versions of the Gaode Map app without crashes or ANRs.
Additional notes include bypassing Android P’s black‑/gray‑list restrictions on the ClassTable field using meta‑reflection (e.g., FreeReflection) and scheduling the collection on a background thread after the app has been idle in the background.
Amap Tech
Official Amap technology account showcasing all of Amap's technical innovations.
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.