It’s time for me to perform, finally to the core of our — ASM.
Town building website
Asm. Ow2. IO/developer – g…
ASM is an open source tool for manipulating bytecodes and inserting new logic into the raw bytecodes at compile time. It is often used in conjunction with Gradle Transform.
ASM contains two API usage modes — Core API and Tree API. In most scenarios, Core API is used.
The Core API represents a class in the form of event access, abstracting it into a series of events, each representing an element of the class, such as a header, a field, a method declaration, an instruction, and so on.
The Core API defines a set of possible events and the order in which they must be accessed, provides a ClassVisitor, which generates an event for each parsed element, and a ClassWriter, which generates the sequence of these events into compiled classes.
The core of ASM is to take access elements from events returned by the visitor pattern and modify them.
The general flow ASM uses in code is as follows:
- InputStream reads the Class file
- Based on the InputStream stream, create a ClassReader instance and load the bytecode file
- Create a ClassWriter instance to write the modified bytecode
- Create a ClassVisitor instance based on a ClassWriter
- Trigger the ClassReader object to parse the Class information (accept classVisitor)
- ClassWriter receives the passed data and writes it to a new file
The ASM workflow is as follows:
- ClassReader loads the bytecode file and turns on visitor mode
- Split the Class file into access events
- The Visitor method iterates over the ClassVisitor, calling back to the event
- When a secondary Visitor, such as visitMethod, is encountered in the ClassVisitor, do a deep traversal
- Loop the entire process until the end of the access
ClassVisitor
ClassVisitor provides access mechanism events for annotations, fields, and methods inside a class.
The entire access process is as follows:
Visit — visitSource — visitModule — visitNestHost — visitOuterClass — visitAnnotation — visitTypeAnnotation — VisitAttribute — visitNestMember — visitInnerClass — visitField — visitMethod — visitEnd
Visit and visitEnd must be called once, visitModule can be called at most once, and the rest may be called multiple times.
Each visitor of the ClassVisitor, which corresponds to each structure and access node in the class file, triggers a parse callback when the corresponding content is accessed. In these access methods, methods like visit, visitEnd, return void directly, VisitAnnotation, visitField, and visitMethod return the corresponding AnnotationVisitor, FieldVisitor, and MethodVisitor to drill down on more elaborate events with these auxiliary visitors.
Class/interface | instructions |
---|---|
AnnotationVisitor | Defines the sequence of events that are triggered when an annotation is parsed, and the corresponding method is called when an annotation of primitive value type, enum value type, Array value type, or annotation value type is parsed |
FieldVisitor | Defines the events that are triggered when a field is parsed, such as parsed to annotations on the field, parsed to field-related properties, and so on |
MethodVisitor | Defines the events that are triggered when a method is parsed, such as annotations, properties, code on the method, and so on |
Callbacks to various events in the ClassVisitor, along with refined events in the auxiliary Visitor, allow the user to easily modify the bytecode without the relative bytecode internal offset. The ClassVisitor manages these processes, and the user can modify the bytecode by overwriting the corresponding Visitor.
A common Visitor callback event is shown below.
The method name | instructions |
---|---|
visit | To access the header information of a class, version is the class version, access is the access modifier, name is the class name, signature is the signature of the class, possibly null, superName is the superclass name, and interfaces are the name of the interface |
visitAnnotation | When accessing the annotation information of a class, descriptor is the signature description information, and visible is whether to be visible at runtime |
visitAttribute | Access the properties of the class |
visitInnerClass | Access information about an inner class in a class that is not necessarily a member of the class being accessed (it could be an anonymous inner class in a method, a class declared in a method, and so on). Name is the name of the inner class, outerName is the name of the class where the inner class resides, and innerName is the name of the inner class |
visitOuterClass | To access an external class of that class, this method must be called only if the class has a enclosing class. Owner is the name of the class that owns the class. Name is the name of the method that contains the class. If the class is not included in the method of its enclosing class, null is returned |
visitEnd | Called to end access to class |
visitField | Returns a FieldVisitor for manipulating field-related information, access for access modifier, name for class name, signature for class signature, possibly null, descriptor for description |
visitMethod | Return a MethodVisitor to manipulate field-specific information, access for access modifier, name for method name, signature for method signature, possibly null, Descriptor for description, exceptions for exceptions |
visitModule | Access the corresponding module |
visitTypeAnnotation | Annotation to access the signature of the class |
FieldVisitor
The FieldVisitor is called as follows:
VisitAnnotation — visitTypeAnnotation — visitAttribute — visitEnd
- VisitAnnotation: An annotation that accesses the Field
- VisitTypeAnnotation: An annotation on the type that accesses the Field
- VisitEnd: This method is called when the Field access is complete
MethodVisitor
The call flow for MethodVisitor is as follows:
VisitParameter – visitAnnotationDefault visitAnnotation – visitAnnotableParameterCount – visitParameterAnnotation — visitTypeAnnotation — visitAttribute — visitCode — visitFrame — visitInsn — visitLabel — visitInsnAnnotation — VisitTryCatchBlock – visitTryCatchAnnotation visitLocalVariable – visitLocalVariableAnnotation – visitLineNumber – visitMaxs – visitEnd
MethodVisitor provides access to methods, such as onMethodEnter, onMethodExit, and so on.
- VisitCode: Start of access
- VisitMaxs: End of visit
- VisitInsn: Access the no-operand command, such as return
- VisitLdcInsn: Access the LDC directive, that is, access the constant pool index
- VisitMethodInsn: Access method instruction, that is, call a method
ClassWriter
ClassWriter is used, as described, to write bytecode back to a file. If you just modify the bytecode, you can pass the bytes to the FileOutputStream directly through the toByteArray method. If you create a new Class, Then you need to use some internal methods of ClassWriter.
ClassWriter inherits from ClassVisitor, so it also accesses nodes through various visitors.
The common method for ClassWriter is the following, which is similar to the ClassVisitor but does a different job.
Part of the way | instructions |
---|---|
ClassWriter(final int flags) | Construct a ClassWriter object with flag values of 0, 1, and 2 (0 indicates that the maximum operand stack, local variation table, and frame change need to be calculated manually. ClassWriter.COMPUTE_MAXS means that the local variable table and operand stack are automatically evaluated, but visitMaxs must be called and the method arguments are ignored. Frame changes need to be calculated manually. ClassWriter.COMPUTE_FRAMES means automatic calculation, but visitMaxs must be called, and method parameters are ignored. But ClassWriter.COMPUTE_MAXS is 10% slower than 0 and twice as slow as COMPUTE_FRAMES.) |
visit(final int version, final int access,final String name,final String signature,final String superName,final String[] interfaces) | Construct the header information of the class file, version is the specified JDK version (value is the constant defined by Opcodes), access is the class modifier (same as version), name is the class name, signature is generic, null indicates that the field is not a generic signature. SuperName is the fully qualified name of the parent class to inherit, and Interfaces are the fully qualified name of the interface to implement |
visitField(final int access,final String name,final String descriptor,final String signature,final Object value) | Construct member attributes of the class file. Name is the name of the member attribute, descriptor is the type signature of the attribute, and value is the value of the attribute. Only static fields are used |
visitMethod(final int access,final String name,final String descriptor,final String signature,final String[] exceptions) | Constructs the method signature of the class file and returns an object that has a constructor body (that is, a method modifier, method name, return value, and fully qualified parameters) |
visitAnnotation(final String descriptor, final boolean visible) | Construct an annotation object for class, descriptor is the description name of the annotation, visible is runtime |
toByteArray() | Returns the byte stream of generated bytecode and writes the byte stream back to the file to produce the adjusted class file |
Similar to visitAnnotation, visitField, and visitMethod, the following classes encapsulate more elaborate operation nodes. |
Class/interface | instructions |
---|---|
AnnotationWriter | AnnotationVisitor is implemented to create annotation-related bytecode instructions to create annotation-part bytecode |
FieldWriter | FieldVisitor is implemented to create field-related bytecode |
MethodWriter | MethodVisitor is implemented to create method-dependent bytecode |
SignatureWriter | SignatureVisitor is implemented to create generic-dependent bytecode |
AnnotationWriter | AnnotationVisitor is implemented to create annotation-related bytecode |
ClassReader
ClassReader is used to load the bytecode file, turn on Visitor mode, split the Class file into multiple access events, and call back the various Visitor.
- ClassReader(byte[] b)
- ClassReader(byte[] b, int off, int len)
- ClassReader(InputStream is)
- ClassReader(String name)
ClassReader provides multiple ways to access files and load bytecodes.
The order in which the various VisitXXX methods are called is actually the order in which they are called in the Accept method of ClassReader.
The accept(final ClassVisitor ClassVisitor, final int parsingOptions) parameter in the accept(final ClassVisitor ClassVisitor, final int parsingOptions) method represents the options used to parse the class.
- Classreader.skip_code: All methods that exclude code access, along with method parameter attributes and annotations
- Classreader.skip_frames — Skip the flags for StackMap and StackMapTable properties and skip the methodVisit.visitFrame method, which is the best choice for us developers
- Classreader.skip_debug — Used to ignore debug information, such as source file, line count, and variable information
- Classreader.expand_frames — Extends StackMapTable data to allow visitors to get information about all local variable types and the current stack position, which can significantly degrade performance, but this flag is recommended
General procedure
The above are the basic concepts of ASM, because ASM uses the visitor pattern, which is less used in the usual development, so it is difficult to understand, but only to understand these concepts clearly, in order to better use ASM.
ASM pins code, usually in a Transform, and makes changes to the specified Class file as it traverses the file.
A standard ASM usage code is shown below.
for (file in files) { var inputStream: FileInputStream? = null var outputStream: FileOutputStream? = null try {val classWriter = classWriter (ClassWriter.COMPUTE_MAXS) ClassVisitor = CustomClassVisitor(classWriter) inputStream = FileInputStream(file) // Construct ClassReader Val from inputStream ClassReader = classReader (inputStream).accept(ClassVisitor).accept(ClassVisitor) Classreader.expand_frames) // Return the modified bytecode as a byte array val bytes = classWriter.tobytearray () // rewrite the modified code back to outputStream = FileOutputStream(file.path) outputStream.write(bytes) outputStream.flush() } catch (throwable: Throwable) { throwable.printStackTrace() } finally { closeQuietly(inputStream) closeQuietly(outputStream) } }Copy the code
This is consistent with the steps we outlined at the beginning of this article, where ASM separates the process from the logic by placing the specific access logic in the CustomClassVisitor pattern.
In the CustomClassVisitor, you can take further advantage of the callbacks that ASM provides for bytecode filtering and modification.
If you don’t need to customize the ClassVisitor in depth, you can quickly use the following code in Transform.
try { val inputStream = Files.asByteSource(file).openBufferedStream() val classReader = ClassReader(inputStream) val classVisitor: ClassVisitor = object : ClassVisitor(Opcodes.ASM9) { override fun visitSource(source: String? , debug: String?) { // TODO } } classReader.accept(classVisitor, ClassReader.SKIP_CODE) } catch (e: Exception) { log("${e.message}") } finally { closeQuietly(inputStream) }Copy the code
Using ASM is relatively simple, but the difficulty lies in the transformation of bytecode, so in the next article, we will analyze various ASM usage scenarios.
I would like to recommend my website xuyisheng. Top/focusing on Android-Kotlin-flutter welcome you to visit