Read the ASM4 User Manual (Chinese version). The code for this article has been uploaded to Github
“ASM bytecode staking” outline
Background and questions
In Android, you might often hear someone in the middle of the pack say, Hook, APM monitoring, dynamic code modification by the compiler, etc. You know that AspectJ can weave in relevant code through sections, but you don’t know that even a small Lambada syntax cannot be implemented in the custom Plugin. Not to mention other compatibility issues, is there a relatively perfect choice to achieve full buried point? Is there a best technology option for inserting debugging or performance monitoring code into your application while keeping it running fast? Well, without turning a corner, today we will take a detailed talk about a lightweight AOP design ASM.
1.0 Key Technologies
Before we understand ASM, we must first understand the packaging process of APP. Here we recommend you to look at Deng Fanping’sUnderstanding the Android VIRTUAL Machine, the Android application packaging process is roughly divided into the following 7 stages:
-
- Aapt package resource file phase
-
- Aidl to Java file phase
-
- Java Compilers generate the. Class file phase
-
- Dex (Generate dex file) phase
-
- Apkbuilder (Generate unsigned APK) phase
-
- Jarsigner (signature) stage
-
- Zipalign phase
For details of the work done in each stage, please refer to the article On Android packaging Process. We only need to focus on the fourth stage Dex generation before the work, because at this stage we can intercept all the.class file, and then with the help of plug-ins, we can traverse all the methods of the.class file, and then find the target method according to certain conditions, and finally modify and save, we can insert the code we need. So what exactly do we mean by plug-ins? Google has provided the Transform Api since Android Gradle 1.5. Through the Transform Api, it allows a tripartite Plugin to manipulate.class files during compilation before the application is packaged to generate.dex. We need to customize the Transform, iterate through all the methods in the.class file, then modify the listener to insert buried code, and then replace the source file to weave in the code.
1.1 Gradle Transform
Back to the question of what, what is a Transform? This is how the official Google document translates it
A transform receives input as a collection TransformInput, which is composed of JarInputs and DirectoryInputs. Both provide information about the QualifiedContent.Scopes and QualifiedContent.ContentTypes associated with their particular content.The output is handled by TransformOutputProvider which allows creating new self-contained content, each associated with their own Scopes and Content Types. The content handled by TransformInput/Output is managed by the transform system, and their location is not configurable.It is best practice to write into as many outputs as Jar/Folder Inputs have been received by the transform. Combining all the inputs into a single output prevents downstream transform from processing limited scopes.
The simple understanding is: A standard API for modifying.class files to convert.class files to target bytecode files, which you can easily compare to file input streams and output streams, except that IO operates on stream objects and Transform operates on file objects. Remember the two core apis :TransformInput and TransformOutputProvider; TransformInput, which represents the input file Abstraction interface, has two important methods
Get the DirectoryInput collection and get the JarInput collection, where DirectoryInput is involved in compiling all directory structures and their source files in the directory as source code; JarInput is all jar packages and remote packages that participate in project compilation as JAR packages. The TransformOutputProvider represents an abstract interface for output files, and there is a core interface method called getContentLocation that gets output path information
The getContentLocation method has several important parameters: name,type,scopes, and format, where name represents the name of the Task corresponding to the Transform.
QualifiedContent. ContentType is representative of the Transform to deal with the type of data; There are two default enumerated arguments: CLASSES and PESOURCES.
CLASSES represent compiled bytecodes, which can be jars or directories. PESOURCES stands for processing standard Java resources, and scopes are an interesting enumerated class,
Used to specify a Transform In which there are seven enumerated objects :PROJECT,SUB_PROJECTS,PROJECT_LOCALDEPS,SUB_PROJECTS_LOCAL_OEPS,EXTERNAL_LIBRARIES,PROVIDED_ONLY, and TESTED_CO DE. PROJECT deals only with the current PROJECT,SUB_PROJECTS deals only with subprojects, and PROJECT_LOCALDEPS deals only with local dependencies of the current PROJECT, such as JAR, ARR. SUB_PROJECTS_LOCAL_OEPS only handles local dependencies for subprojects. For example, JAR, ARR, and EXTERNAL_LIBRARIES only deal with outsourced dependent libraries. PROVIDED_ONLY only deals with dependent libraries imported locally or remotely as provided. And TESTED_CODE means test code. Format is used to format content. So that completes the TransformOutputProvider. The paper come zhongjue shallow, and must know this to practice. Let’s do a simple Transform
Step 1: Create a new Project
It automatically generates a master moudle, i.e., app
Step 2: Create a new moudle named Plugin
Step 3: Empty the plugin/build.gradle file and modify it
Step 4: Delete all files in plugin.src/main
Step 5: Create a Groovy directory
Step 6: Create the Transform class
Step 7: Create a Plugin and.properties
Step 8: Execute plugin’s uploadArchives task to build plugin
Step 9: Modify the buid.gradle file in the root directory of the project to add dependencies on the plug-in
Step 10: Declare the use of the plugin in app/build.gradle
Step 11: Build the application
At this point, a simple instance of Gradle Transform is complete
1.2 BASIC ASM Knowledge
If Transform is the starter, then today’s main course should be ASM. What is ASM? That’s the official explanation.
ASM is an all purpose Java bytecode manipulation and analysis framework. It can be used to modify existing classes or to dynamically generate classes, directly in binary form. ASM provides some common bytecode transformations and analysis algorithms from which custom complex transformations and code analysis tools can be built. ASM offers similar functionality as other Java bytecode frameworks, but is focused on performance. Because it was designed and implemented to be as small and as fast as possible, it is well suited for use in dynamic systems (but can of course be used in a static way too, e.g. in compilers).
ASM is actually a fully functional Java bytecode operation and analysis framework, through ASM, we can dynamically generate classes or enhance the function of existing classes,ASM can directly generate binary. Class files, also can be loaded into the Java virtual machine class To dynamically change the behavior of existing classes,Java binaries are stored in strictly formatted.class files. These bytecodes have enough metadata information to represent all the elements of a class, including the class name, methods, attributes, and Java bytecode instructions. ASM reads this information from bytecode files and can change the behavior of classes, analyze the information of classes, and even generate new classes based on specific requirements. ASM involved five more core classes: ClassReader, ClassWriter, MethodVisitor, ClassVistor and AdiviveAdapter. ClassReader is mainly used to parse compiled.class bytecode files, while ClassWriter is mainly used to reconstruct compiled classes, such as modifying class names, attributes, and methods, and even producing new class name stanchies. MethodVisitor is primarily a method access class, with several important methods :onMethodEnter,visitEnd, and visitAnnotation. OnMethodEnter inserts bytecode when entering a method,visitEnd exits bytecode before exiting a method, and visitAnnotation can annotate bytecode here. ClassVistor is responsible for “visiting” class member information including class annotations, class constructors, class fields, class methods, static code blocks ClassVistor has a few important methods, the key to know are visit and visitMehtod;
Visit has six of the more important parameters: the version, access, name, signature, signatureName and interfaces. Version indicates the JDK version. The corresponding table is as follows:
Access represents the class modifier, which has the following meanings:
Name is the name of the class; Signature is generic information; SignatureName is the parent of the current class; Interfaces are the list of interfaces implemented by a class. In Java, a class can implement multiple different interfaces, so this parameter is an array type. Let’s look at the sample code for ClassVistor:
class MkAnalyticsClassVisitor extends ClassVisitor implements Opcodes {
private final static String SDK_API_CLASS = "com/github/microkibaco/asm_sdk/SensorsDataAutoTrackHelper"
private String[] mInterfaces
private ClassVisitor classVisitor
private HashMap<String, MkAnalyticsMethodCell> mLambdaMethodCells = new HashMap<>()
MkAnalyticsClassVisitor(final ClassVisitor classVisitor) {
super(Opcodes.ASM6, classVisitor)
this.classVisitor = classVisitor
}
private
static void visitMethodWithLoadedParams(MethodVisitor methodVisitor, int opcode, String owner, String methodName, String methodDesc, int start, int count, List<Integer> paramOpcodes) {
for (int i = start; i < start + count; i++) {
methodVisitor.visitVarInsn(paramOpcodes[i - start], i)
}
methodVisitor.visitMethodInsn(opcode, owner, methodName, methodDesc, false)}/** * visit gets all the information about.class, such as the list of interfaces implemented by the current class *@paramVersion JDK version *@paramThe accessclass modifier *@paramName The name of the class *@paramSignature The parent of the current class *@param superName
* @paramInterfaces the list of interfaces implemented by the interfaces class. In Java, a class can implement multiple different interfaces, so this parameter is an array of type */
@Override
void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
super.visit(version, access, name, signature, superName, interfaces)
mInterfaces = interfaces
}
/** * get all information about method, such as the name of the method and its parameter description *@paramThe accessclass modifier *@paramName The name of the class *@paramDesc method signature *@paramSignature Indicates the class signature@paramExceptions Message *@return MethodVisitor
*/
@Override
MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
MethodVisitor methodVisitor = super.visitMethod(access, name, desc, signature, exceptions)
return methodVisitor
}
/** * Obtain the corresponding ASM index * whose subscript is index@paramThe types method parameter type array *@paramThe index method is used to index parameters, starting at 0 *@paramIsStaticMethod Whether the method is static *@returnASM index */ to access the index bit parameter of the method
int getVisitPosition(Type[] types, int index, boolean isStaticMethod) {
if (types == null || index < 0 || index >= types.length) {
throw new Error("getVisitPosition error")}if (index == 0) {
return isStaticMethod ? 0 : 1
} else {
return getVisitPosition(types, index - 1, isStaticMethod) + types[index - 1].getSize()
}
}
}
Copy the code
In the visitMethod above, you can mirror a particular method, and when you do that, you need to call the “call” method, so the message MethodVistitor
methodVisitor = new MkAnalyticsDefaultMethodVisitor(methodVisitor, access, name, desc) {
boolean isSensorsDataTrackViewOnClickAnnotation = false
/** * Can exit bytecode before exiting method */
@Override
void visitEnd(a) {
super.visitEnd()
if (mLambdaMethodCells.containsKey(nameDesc)) {
mLambdaMethodCells.remove(nameDesc)
}
}
/** * insert bytecode */ into the method
@Override
protected void onMethodEnter(a) {
super.onMethodEnter()
}
/** * You can annotate bytecode here@paramThe name of the annotation accessed by s *@paramB Whether method *@return methodVisitor
*/
@Override
groovyjarjarasm.asm.AnnotationVisitor visitAnnotation(String s, boolean b) {
if (s == 'Lcom/sensorsdata/analytics/android/sdk/SensorsDataTrackViewOnClick; ') {
isSensorsDataTrackViewOnClickAnnotation = true
}
return super.visitAnnotation(s, b)
}
}
Copy the code
VisitMehtod method also has a few of the more important parameters: access, name, desc, signature and exceptions. Access represents a method modifier with the following meanings:
Name indicates the method name. Desc indicates the method signature. The specific symbol type is as follows:
The method signature corresponding to the method parameter list is as follows:
Signature represents generic-related information; Exceptions indicates that an exception will be thrown. If the method does not throw an exception, this parameter is null. Finally, let’s look at the AdiviveAdapter, which implements the MethodVistor interface and is responsible for “calling” method information for specific methods
class MkAnalyticsDefaultMethodVisitor extends AdviceAdapter {
MkAnalyticsDefaultMethodVisitor(MethodVisitor mv, int access, String name, String desc) {
super(Opcodes.ASM6, mv, access, name, desc)
}
/** * indicates that ASM starts scanning the method */
@Override
void visitCode(a) {
super.visitCode()
}
@Override
void visitMethodInsn(int opcode, String owner, String name, String desc) {
super.visitMethodInsn(opcode, owner, name, desc)
}
@Override
void visitAttribute(Attribute attribute) {
super.visitAttribute(attribute)
}
/** * indicates that the method output is complete */
@Override
void visitEnd(a) {
super.visitEnd()
}
@Override
void visitFieldInsn(int opcode, String owner, String name, String desc) {
super.visitFieldInsn(opcode, owner, name, desc)
}
@Override
void visitIincInsn(int var.int increment) {
super.visitIincInsn(var, increment)
}
@Override
void visitIntInsn(int i, int i1) {
super.visitIntInsn(i, i1)
}
/** * This method is the one visitEnd called before and can be called again and again. Used to determine the stack size of a class method at execution time. *@param maxStack
* @param maxLocals
*/
@Override
void visitMaxs(int maxStack, int maxLocals) {
super.visitMaxs(maxStack, maxLocals)
}
@Override
void visitVarInsn(int opcode, int var) {
super.visitVarInsn(opcode, var)}@Override
void visitJumpInsn(int opcode, Label label) {
super.visitJumpInsn(opcode, label)
}
@Override
void visitLookupSwitchInsn(Label label, int[] ints, Label[] labels) {
super.visitLookupSwitchInsn(label, ints, labels)
}
@Override
void visitMultiANewArrayInsn(String s, int i) {
super.visitMultiANewArrayInsn(s, i)
}
@Override
void visitTableSwitchInsn(int i, int i1, Label label, Label[] labels) {
super.visitTableSwitchInsn(i, i1, label, labels)
}
@Override
void visitTryCatchBlock(Label label, Label label1, Label label2, String s) {
super.visitTryCatchBlock(label, label1, label2, s)
}
@Override
void visitTypeInsn(int opcode, String s) {
super.visitTypeInsn(opcode, s)
}
@Override
void visitLocalVariable(String s, String s1, String s2, Label label, Label label1, int i) {
super.visitLocalVariable(s, s1, s2, label, label1, i)
}
@Override
void visitInsn(int opcode) {
super.visitInsn(opcode)
}
@Override
AnnotationVisitor visitAnnotation(String s, boolean b) {
return super.visitAnnotation(s, b)
}
@Override
protected void onMethodEnter(a) {
super.onMethodEnter()
}
/** * Use onMethodExit so as not to affect the application's original click event response speed *@param opcode
*/
@Override
protected void onMethodExit(int opcode) {
super.onMethodExit(opcode)
}
}
Copy the code
However, I encountered this pit in the process of using it, and the specific reason is to review whether the Plugin package is wrong
1.3 the ASM principle
Just a long talk about the use of ASM and a simple API introduction, so what is the ASM implementation process, mainly divided into three steps
- Step 1: Define a Gradle Plugin. Then register a Transform object. In the Transform method, you can traverse the directory and jar packages separately
- Step 2: Iterate through all the.class files in the current application to find.class files and related methods that meet certain criteria
- Step 3: Modify the corresponding method to dynamically insert bytecode
1.4 ASM buried point scheme
The following takes automatic collection of click events in Android Button space as an example to introduce the implementation steps of the scheme. For other control click events are available to supplement
Step 1: Create a new Project
Step 2: Create the SDK Module
Step 3: Write a buried SDK
/ * * *@authorCreated by Yang Zhengyou (small wooden box) on 2020/10/92019 *@Email: [email protected]
* @Tel: 18390833563
* @function description:
*/
@Keep
public class SensorsDataAPI {
private final String TAG = this.getClass().getSimpleName();
public static final String SDK_VERSION = "1.0.0";
private static SensorsDataAPI INSTANCE;
private static final Object LOCK = new Object();
private static Map<String, Object> mDeviceInfo;
private String mDeviceId;
@Keep
@SuppressWarnings("UnusedReturnValue")
public static SensorsDataAPI init(Application application) {
synchronized (LOCK) {
if (null == INSTANCE) {
INSTANCE = new SensorsDataAPI(application);
}
returnINSTANCE; }}@Keep
public static SensorsDataAPI getInstance(a) {
return INSTANCE;
}
private SensorsDataAPI(Application application) {
mDeviceId = SensorsDataPrivate.getAndroidID(application.getApplicationContext());
mDeviceInfo = SensorsDataPrivate.getDeviceInfo(application.getApplicationContext());
}
/** * Track event **@paramEventName String eventName *@paramProperties JSONObject Event attribute */
@Keep
public void track(@NonNull final String eventName, @Nullable JSONObject properties) {
try {
JSONObject jsonObject = new JSONObject();
jsonObject.put("event", eventName);
jsonObject.put("device_id", mDeviceId);
JSONObject sendProperties = new JSONObject(mDeviceInfo);
if(properties ! =null) {
SensorsDataPrivate.mergeJSONObject(properties, sendProperties);
}
jsonObject.put("properties", sendProperties);
jsonObject.put("time", System.currentTimeMillis());
Log.i(TAG, SensorsDataPrivate.formatJson(jsonObject.toString()));
} catch(Exception e) { e.printStackTrace(); }}}Copy the code
Step 4: Create a new AutoTrackHelper. Java utility class in the SDK Module
We added trackViewOnClick(View View), mainly ASM to insert buried point code
/** * View is clicked, automatically buried point **@param view View
*/
@Keep
public static void trackViewOnClick(View view) {
try {
JSONObject jsonObject = new JSONObject();
jsonObject.put("$element_type", SensorsDataPrivate.getElementType(view));
jsonObject.put("$element_id", SensorsDataPrivate.getViewId(view));
jsonObject.put("$element_content", SensorsDataPrivate.getElementContent(view));
Activity activity = SensorsDataPrivate.getActivityFromView(view);
if(activity ! =null) {
jsonObject.put("$activity", activity.getClass().getCanonicalName());
}
SensorsDataAPI.getInstance().track("$AppClick", jsonObject);
} catch(Exception e) { e.printStackTrace(); }}Copy the code
Step 5: Add dependencies
Step 6: Initialize the buried SDK
Step 7: Declare the custom Application
Step 8: Create an Android Lib called Plugin
Step 9: Clear the build.gradle file and make the following changes
Step 10: Create the Groovy directory
Step 11: Create the Transform directory
/ * * *@authorCreated by Yang Zhengyou (small wooden box) on 2020/10/92208 *@Email: [email protected]
* @Tel: 18390833563
* @function description:
*/
class MkAnalyticsTransform extends Transform {
private static Project project
private MkAnalyticsExtension sensorsAnalyticsExtension
MkAnalyticsTransform(Project project, MkAnalyticsExtension sensorsAnalyticsExtension) {
this.project = project
this.sensorsAnalyticsExtension = sensorsAnalyticsExtension
}
@Override
String getName(a) {
return "MkAnalytics"
}
/** * The data types to be processed, there are two enumerated types * CLASSES representing the Java class files to be processed, and RESOURCES representing the RESOURCES to be processed in Java *@return* /
@Override
Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS
}
/** * specifies the Scope of the Transform to operate on. * 1. EXTERNAL_LIBRARIES only has external libraries * 2. PROJECT only has PROJECT content * 3. PROJECT_LOCAL_DEPS Only has local dependencies of the PROJECT (local JAR) * 4 Provide only local or remote dependencies * 5. SUB_PROJECTS has only subprojects. * 6. SUB_PROJECTS_LOCAL_DEPS has only local dependencies (local JARS) for the subproject. * 7. TESTED_CODE The code tested by the current variable (including dependencies) *@return* /
@Override
Set<QualifiedContent.Scope> getScopes() {
return TransformManager.SCOPE_FULL_PROJECT
}
@Override
boolean isIncremental(a) {
return false
}
@Override
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
_transform(transformInvocation.context, transformInvocation.inputs, transformInvocation.outputProvider, transformInvocation.incremental)
}
void _transform(Context context, Collection<TransformInput> inputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException {
if(! incremental) { outputProvider.deleteAll() }/** Inputs for Transform have two types: directory, jar, and check their inputs separately
inputs.each { TransformInput input ->
/** Traverses the directory */
input.directoryInputs.each { DirectoryInput directoryInput ->
/** The current Transform output directory */
File dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
File dir = directoryInput.file
if (dir) {
HashMap<String, File> modifyMap = new HashMap<>()
/** Traverses files ending with an extension */
dir.traverse(type: FileType.FILES, nameFilter: ~/.*\.class/) {
File classFile ->
if (MkAnalyticsClassModifier.isShouldModify(classFile.name)) {
File modified = null
if (! sensorsAnalyticsExtension.disableAppClick) {
modified = MkAnalyticsClassModifier.modifyClassFile(dir, classFile, context.getTemporaryDir())
}
if(modified ! =null) {
/ * * key for package name + the name of the class, such as: / cn/sensorsdata/autotrack/android/app/MainActivity class * /
String ke = classFile.absolutePath.replace(dir.absolutePath, "")
modifyMap.put(ke, modified)
}
}
}
// Copy all.class files in the input directory to the output directory
FileUtils.copyDirectory(directoryInput.file, dest)
modifyMap.entrySet().each {
Map.Entry<String, File> en ->
File target = new File(dest.absolutePath + en.getKey())
if (target.exists()) {
target.delete()
}
// Copy the modified.class file in the HashMap to the output directory, overwriting the.class file (the original.class file).
FileUtils.copyFile(en.getValue(), target)
en.getValue().delete()
}
}
}
/ * * traversal jar * /
input.jarInputs.each { JarInput jarInput ->
String destName = jarInput.file.name
/** Intercepts the md5 value of the file path and renames the output file because it may have the same name, overwriting */
def hexName = DigestUtils.md5Hex(jarInput.file.absolutePath).substring(0.8)
/** Get the jar name */
if (destName.endsWith(".jar")) {
destName = destName.substring(0, destName.length() - 4)}/** get the output file */
File dest = outputProvider.getContentLocation(destName + "_" + hexName, jarInput.contentTypes, jarInput.scopes, Format.JAR)
def modifiedJar = null;
if(! sensorsAnalyticsExtension.disableAppClick) { modifiedJar = MkAnalyticsClassModifier.modifyJar(jarInput.file, context.getTemporaryDir(),true)}if (modifiedJar == null) {
modifiedJar = jarInput.file
}
FileUtils.copyFile(modifiedJar, dest)
}
}
}
}
Copy the code
MkAnalyticsTransform inherits the Transform. Inside the Transform, directories and jars are iterated separately. Gradle Transform: Gradle Transform: Gradle Transform: Gradle Transform
A. Traverse the directory
Directory traversal respectively in each. The class files, first by MkAnalyticsClassModifier. IsShouldModify method is simple and filter it certainly does not need. Class files. The isShouldModify method is relatively simple to implement logically
// Put the modified.class file into a HashMap object
private static HashSet<String> exclude = new HashSet<>();
static {
exclude = new HashSet<>()
// Filter. Class file 1: files in the Android. support package
exclude.add('android.support')
// Filter.class files 2: the.class files under our SDK
exclude.add('com.github.microkibaco.asm_sdk')}/** * Determine whether to modify *@paramClassName Class object *@return boolean
*/
protected static boolean isShouldModify(String className) {
Iterator<String> iterator = exclude.iterator()
while (iterator.hasNext()) {
String packageName = iterator.next()
// Improve compilation efficiency
if (className.startsWith(packageName)) {
return false}}Class file 3: r.class and its subclasses
if (className.contains('R$') | |Class file 4: r2. class and its subclasses
className.contains('R2$') ||
className.contains('R.class') ||
className.contains('R2.class') | |// Filter.class file 5: buildconfig.class
className.contains('BuildConfig.class')) {
return false
}
return true
}
Copy the code
For example, we can simply filter the following:.class file
- Files in the Android. supoort package
- Our SDK.class files
- R.classs and its subclasses
- R2.class and its subclasses (generated by ButterKnife)
- BuildConfig.class
The main reason for filtering some files is to improve compilation efficiency.
B. traversal of the jar
Step 12: Define the Plugin
Step 13: Create a new properites file
Step 14: Build the plug-in
Step 15: Add a dependency on the plug-in
Step 16: Use plug-ins in your application
Step 17: Build the application
Confirm the app/build/intermediates/transforms/MknalyticeAutoTrack/debug/whether have generated new class files have insert a new bytecode
1.5 Risk points of ASM
Unable to collect click events for android:onClick property binding
The first step: add an annotation @ SensorsDataTrckViewOnClick
Step 2: visitMethod annotates the tag
In the MethodVistor class defined earlier, there is a method called visitMethod, which is called when a method annotation declaration is scanned. Check to see if the currently scanned annotation is of our custom annotation type. If so, mark it with visitMethod. If so, bury bytecode. VisitAnnotation is implemented as follows:
Step 3: visitAnnotation Annotation special handling
In visitAnnotation method, we assess the current scan annotations (i.e., the first parameter s) whether we custom @ SensorsDataTrckViewOnClick annotation type, if it is, just make a mark, namely
Step 4: SensorsDataTrckViewHelper tacakViewOnClick insert bytecode (view)
In onMethodExit method, if isSensorsDataTrackViewOnClickAnnotation is true, then the method with @ SensorsDataTrckViewOnClick annotation. If the annotation method one and only one View type parameters, then we will insert point code, that is inserted into the code SensorsDataTrckViewHelper. TacakViewOnClick corresponding bytecode (View)
Finally in the android: onClick attribute binding method using our custom annotations on the tag, namely @ SensorsDataTrckViewOnClick
1.6 summarize
This article introduces Gradle Transform based on App packaging process, and teaches you to write a simple Transform Demo, using ASM +Gradle Transform to achieve Button full buried point components, so that you can better understand ASM Principle, of course, there are some shortcomings of the buried component, such as: AlerDialog MenuItem CheckBox SeekBar Spinner RattingBar TabHost ListView GridView and ExpendableListView are not supported. ASM advantages need not be said, the actual development can generally be used for big picture monitoring, accurate measurement of stuck time, log reporting and so on. A perfect complement to AspectJ’s shortcomings