Using Java Bytecode for Android Module Dependency Analysis
The article explains how analyzing Java bytecode with tools such as Javassist during the Android Gradle transform phase yields precise, method‑level module dependency data—overcoming the limitations of Gradle trees, import scans, and IDE analysis—and provides a reliable way to map and enforce clean dependency structures in large Android codebases.
This article introduces the use of Java bytecode technology for dependency analysis in Android modules, where “bytecode” specifically refers to Java bytecode.
Background : With the rapid growth of mobile business, Android applications have become large and modularized. For example, AMap’s Android codebase exceeds one million lines and involves more than 100 modules. Without a standard dependency detection and monitoring tool, module relationships can quickly become chaotic. From a module owner’s perspective, it is crucial to know who depends on the module, which interfaces are used, and which other modules the current module depends on. From a global view, a healthy dependency structure should avoid lower‑level modules depending on higher‑level ones and prevent circular dependencies.
Common dependency analysis methods :
Gradle dependency tree – obtained via ./gradlew : :dependencies --configuration releaseCompileClasspath -q . It provides module‑level dependencies but includes unused declared libraries and lacks method‑level granularity.
Scanning import statements – yields class‑level relationships but cannot handle wildcard imports and may be inefficient when imports are unused.
IDE (Android Studio) analysis – the “Analyze → Analyze Dependencies” feature can produce method‑level dependencies and SDK usage, but it is time‑consuming, the results are not reusable, and it requires separate scans for forward and reverse dependencies.
Why use bytecode? In the Android build pipeline, Java source files and generated R.java are compiled into .class files, then transformed into .dex and finally packaged into an APK. The .class files represent Java bytecode, which can be analyzed reliably even when source code is unavailable (e.g., third‑party JAR/AAR) and when dex files lack suitable analysis tools.
How to perform bytecode‑based dependency analysis :
When to analyze? During the transform task of the Android Gradle plugin, all bytecode (including third‑party libraries) is provided as JarInput . By inspecting the file field of JarInput , the module name and its class files can be obtained.
Which tool to use? Common bytecode libraries are ASM, Javassist, and CGLib. ASM is lightweight but requires direct JVM instruction handling; CGLib wraps ASM; Javassist offers a higher‑level API at the cost of some performance. For rapid prototyping, Javassist was chosen.
Implementation steps – example:
Sample Java code to be analyzed:
1: package com.account;
2: import com.account.B;
3: public class A {
4: void methodA() {
5: B b = new B(); // instantiate B
6: b.methodB(); // call methodB
7: }
8: }Step 1 – Initialize environment and load A.class :
// Initialize ClassPool and add class path
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath('
');
// Load class A
CtClass cls = pool.get("com.account.A");
// Register expression editor
MyExprEditor editor = new MyExprEditor(cls);
cls.instrument(editor);Step 2 – Define a custom ExprEditor to capture method calls:
class MyExprEditor extends ExprEditor {
@Override
void edit(MethodCall m) {
// Class where the call occurs
String clsAName = ctCls.name;
// Method containing the call
String where = m.where().methodInfo.getName();
// Line number of the call
int line = m.lineNumber;
// Target class and method
String clsBName = m.className;
String methodBName = m.methodName;
// Store or output the dependency information
}
// Other overrides omitted
}The editor intercepts all method calls in class A . After processing, the extracted dependency data looks like:
---------------------------------------------------------------------------
| Class1 | Class2 | Expr | method1 | method2 | lineNo |
---------------------------------------------------------------------------
| com.account.A | com.account.B | NewExpr | methodA |
| 5 |
| com.account.A | com.account.B | MethodCall | methodA | methodB | 6 |
---------------------------------------------------------------------------
Explanation: line 5 of
A.methodA
invokes
B
's constructor; line 6 invokes
B.methodB
.Combining this class‑level information with the module‑to‑class mapping obtained in step 1 yields method‑level dependency data between modules. This data can be used to define custom dependency rules, generate global dependency graphs, etc.
Conclusion : The article emphasizes the importance of module dependency analysis in the development process, compares three traditional approaches (Gradle tree, import scanning, IDE analysis), and demonstrates that bytecode analysis provides the most granular and reliable solution for Android projects.
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.