How to Build a Zero‑Intrusion Android Data Collection SDK with Bytecode Instrumentation
This article explores the challenges of traditional Android APM integration and presents a comprehensive, non‑intrusive bytecode instrumentation approach—using Gradle plugins, AGP APIs, and ASM—to automatically collect user behavior, network, performance, crash, and WebView data without modifying application source code.
In mobile application development, real‑time monitoring of performance (APM) and user experience is essential, but traditional solutions require developers to manually add and initialize SDKs and insert tracking code at each business logic point, leading to high coupling, heavy workload, maintenance difficulty, and high integration cost.
Intrusive – monitoring code tightly couples with business code, increasing complexity.
Large effort – manual instrumentation for large apps is time‑consuming and error‑prone.
Hard to maintain – frequent business changes may break or require updates to tracking code.
High entry cost – new projects or team members must learn the instrumentation conventions.
To address these pain points, a non‑intrusive instrumentation scheme injects monitoring probes automatically during the build process, requiring no source code changes.
Core Challenges & Concerns
Fragmentation of the Android ecosystem – rapid AGP version changes (e.g., Transform API to Instrumentation API) demand dynamic adaptation.
Compatibility with third‑party plugins – overlapping bytecode modifications can cause build errors or runtime conflicts.
Robustness & independence – injected probes must not crash the app even if the main SDK is not initialized.
Non‑Intrusive Instrumentation Solution
The most common technique in Android is bytecode instrumentation. It leverages Android Gradle Plugin (AGP) APIs (Transform or Instrumentation) to scan and modify .class files before they are compiled into .dex files.
public class MyApmPlugin implements Plugin<Project> {
@Override
public void apply(Project project) {
boolean hasAsmFactory = classExists("com.android.build.api.instrumentation.AsmClassVisitorFactory");
boolean hasTransform = classExists("com.android.build.api.transform.Transform");
if (hasAsmFactory) {
// AGP 7+ – use Instrumentation API
new Agp7PlusImpl(project).init();
} else if (hasTransform) {
// Legacy – use Transform API
new LegacyTransformImpl(project).init();
} else {
project.getLogger().warn("No supported AGP API found. Plugin disabled.");
}
}
}AGP Version Dynamic Adaptation
The plugin detects the AGP version at runtime and selects the appropriate API:
if (hasAsmFactory) {
// AGP 7+ – Instrumentation API (recommended)
} else if (hasTransform) {
// Legacy – Transform API
}Compatibility & Conflict Avoidance
Blacklist : skip system packages, common APM/patch frameworks, and the plugin’s own SDK.
Whitelist (optional): only process app‑specific packages.
Idempotent instrumentation : tag or instance‑of checks to avoid duplicate injection.
Annotation control : support @NoTrack / @TrackIgnore to let developers exclude classes or methods.
Fallback on failure : if a class fails to instrument, log and keep the original bytecode.
public class ClassInstrumentChecker {
private static final List<String> BLACKLISTED_PREFIXES = Arrays.asList(
"java/", "javax/", "kotlin/", "kotlinx/", "android/", "androidx/",
"com/my/apm/sdk/", "com/networkbench/", "com/sensorsdata/", "com/tencent/qapmsdk/",
"com/tencent/tinker/", "com/taobao/sophix/"
);
public static boolean shouldInstrument(String className) {
if (className.contains("/R$") || className.endsWith("/R") || className.endsWith("/BuildConfig")) {
return false;
}
for (String prefix : BLACKLISTED_PREFIXES) {
if (className.startsWith(prefix)) {
return false;
}
}
return true;
}
}Safety Instrumentation
Do not replace native logic – probes are inserted before or after the original method body (e.g., using super.visitMethodInsn() to keep the original network call).
No extra third‑party dependencies – injected bytecode only calls static methods in the SDK such as TrackInstrument.trackViewOnClick(...).
Probe isolation – each injected method first checks whether the main SDK is initialized; if not, it returns silently, preventing crashes.
public class TrackInstrument {
public static void trackViewOnClick(View view) {
try {
if (!MyApmAgent.isInitialized() || view == null) {
return;
}
String viewId = getViewId(view);
MyApmAgent.get().logUserAction("click", viewId);
} catch (Throwable ignored) {
// isolate exceptions
}
}
}Kotlin & Jetpack Compose Considerations
Inline functions are inlined at compile time, so instrumentation should target the underlying non‑inline methods.
Compose click handling via Modifier.clickable requires instrumentation at the Compose runtime or wrapper level.
Lambda bytecode may be generated as anonymous classes (desugaring) or invokedynamic; the visitor must handle both signatures.
public void visitInvokeDynamicInsn(String name, String descriptor, Handle bsm, Object... bsmArgs) {
super.visitInvokeDynamicInsn(name, descriptor, bsm, bsmArgs);
try {
String samMethodDesc = ((Type) bsmArgs[0]).getDescriptor();
if ("(Landroid/view/View;)V".equals(samMethodDesc)) {
Handle implMethodHandle = (Handle) bsmArgs[1];
String lambdaBodySignature = implMethodHandle.getName() + implMethodHandle.getDesc();
MyHookConfig.LAMBDA_METHODS_TO_HOOK.put(lambdaBodySignature,
MyHookConfig.HOOK_METHODS.get("onClick(Landroid/view/View;)V"));
}
} catch (Exception e) {
// ignore
}
}Instrumentation Practice Workflow
Traverse all .class files (both project classes and library jars).
Use ASM ClassReader → custom ClassVisitor to filter classes via the blacklist.
For each target class, a custom AdviceAdapter inserts probe code at method entry (e.g., onClick).
After modification, ClassWriter writes the new bytecode, replacing the original files before packaging.
public byte[] processClass(InputStream classInputStream) throws IOException {
ClassReader classReader = new ClassReader(classInputStream);
ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS);
classReader.accept(new MyClassAdapter(classWriter), ClassReader.EXPAND_FRAMES);
return classWriter.toByteArray();
}Summary
The article demonstrates how a Gradle plugin combined with AGP APIs and ASM bytecode instrumentation can provide a zero‑intrusion, high‑performance monitoring SDK for Android. By handling AGP version fragmentation, third‑party plugin conflicts, and ensuring safe probe execution, developers can automatically collect user behavior, network requests, performance metrics, crashes, and WebView events without any manual code changes.
For a production‑ready implementation, refer to Alibaba Cloud RUM’s Android SDK documentation.
Related links:
Alibaba Cloud RUM Android Integration Guide
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.
Alibaba Cloud Observability
Driving continuous progress in observability technology!
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.
