preface
- Last article: A quick start to Gradle learning
In the previous Gralde primer, we explained how to customize Gralde plugins. This article defines a simple Gradle plugin using Asm and Transfrom. This Gradle plugin can count the time of a method and when the time of a method exceeds a threshold, The Log is printed on the console, and then we can locate the position of the time consuming method through the Log to help us find out the time consuming method. A very simple function, the principle is also very simple, which requires the use of Asm knowledge and Transfrom knowledge, so this article will first introduce the Asm and Transfrom knowledge. Finally, I will show you how to implement Gradle plugin using Asm and Transform. If you are familiar with Asm and Transfrom, you can skip these two sections.
The source location is at the end of the text
Running effect
Since this is a local plugin, apply directly to app/build.gradle and (optionally) configure it with the time extension:
apply plugin: com.example.plugin.TimeCostPlugin
// The function time threshold is 200ms, and only the functions in the application are inserted (excluding third-party libraries)
time{
threshold = 200
appPackage = 'com.example.plugindemo'
}
Copy the code
Then define several time consuming functions specifically:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
try {
method1();
method2();
method3();
} catch(InterruptedException e) { e.printStackTrace(); }}private static void method1(a) throws InterruptedException {
Thread.sleep(500);
}
public void method2(a) throws InterruptedException {
Thread.sleep(300);
}
void method3(a) throws InterruptedException {
Thread.sleep(1000); }}Copy the code
Finally, compile and run it, and it will print the time function information in the console:
Clicking on the method line number takes you directly to the time function.
Asm
Official address: ASM
Official tutorials: ASM4- Guide (English edition), ASM4- Guide (Chinese edition)
Asm is a generic Java bytecode manipulation and analysis framework, it provides some simple and easy to use bytecode manipulation method, can be directly modify an existing class or dynamically generated in the form of binary class, simply speaking, Asm is a bytecode manipulation framework, through the Asm, we could generate a class, or modify an existing class, Asm has the advantages of small size, good performance, and high efficiency compared with other Bytecode operation frameworks such as Javasist, AspectJ, etc. However, it has the disadvantage of high learning cost. However, IntelliJ plug-in Asm Bytecode Outline can automatically generate Asm code for us. So for those of you who want to get started with Asm, it’s fairly straightforward. We just need to briefly learn what the Asm API means, and before that, hopefully you have a good understanding of the BASICS of the JVM: type descriptors, method descriptors, and Class file structures.
There are two types of APIS in Asm, one is the Tree API based on the tree model, and the other is the visitor API based on the visitor pattern. Visitor API is the core and basic API of Asm, so for beginners, we need to know the use of visitor API. There are three main classes in the Visitor API for reading, accessing, and generating class bytecode:
-
ClassVisitor: It’s used to access calSS bytecode, and it has a lot of visitXX methods in it, and every visitXX Method that you call means that you’re accessing some structure of the class file, such as Method, Field, Annotation, etc. We usually extend that ClassVisitor, using the proxy pattern, Delegating each visitXX method call of the extended ClassVisitor to another ClassVisitor allows us to add our own logic before and after the delegation to transform and modify the class bytecode of that class.
-
ClassReader: It reads the class bytecode given as a byte array. It has an Accept method that receives a ClassVisitor instance. Inside the Accept method, the visitXX method of the ClassVisitor is called to access the class file that has been read;
-
ClassWriter: It inherits from ClassVisitor and generates class bytecode in binary form. It has a toByteArray method that converts the generated binary form of class bytecode back into byte array form.
ClassVisitor, ClassReader, and ClassWriter are usually used in combination. Here are some practical examples to quickly understand. First, we need to introduce Asm in build.gradle, as follows:
dependencies { // The core API provides the Visitor API implementation 'org. Ow2. Asm: asm: 7.0' // Optional, some predefined class converters are provided based on the core API implementation 'org. Ow2. Asm: asm - Commons: 7.0' // Optional, provides some utility classes based on the core API implementation 'org. Ow2. Asm: asm - util: 7.0' } Copy the code
1, read, access a class
Before reading the class, let’s first introduce the visitXX method in ClassVisitor. The main structure of ClassVisitor is as follows:
public abstract class ClassVisitor {
// The ASM version is defined in the Opcodes interface. The lowest version is ASM4 and the latest is ASM7
protected final int api;
// The delegated ClassVisitor can be passed empty
protected ClassVisitor cv;
public ClassVisitor(final int api) {
this(api, null);
}
public ClassVisitor(final int api, final ClassVisitor cv) {
/ /...
this.api = api;
this.cv = cv;
}
// to start accessing the class
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
if(cv ! =null) { cv.visit(version, access, name, signature, superName, interfaces); }}// to access the source file name of the class (if any)
public void visitSource(String source, String debug) {
if(cv ! =null) { cv.visitSource(source, debug); }}// Represents access to the class's external class (if any)
public void visitOuterClass(String owner, String name, String desc) {
if(cv ! =null) { cv.visitOuterClass(owner, name, desc); }}// An annotation that indicates access to the class (if any)
public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
if(cv ! =null) {
return cv.visitAnnotation(desc, visible);
}
return null;
}
// represents access to the inner class of this class (if any)
public void visitInnerClass(String name, String outerName,
String innerName, int access) {
if(cv ! =null) { cv.visitInnerClass(name, outerName, innerName, access); }}// indicates access to the fields of this class (if any)
public FieldVisitor visitField(int access, String name, String desc,
String signature, Object value) {
if(cv ! =null) {
return cv.visitField(access, name, desc, signature, value);
}
return null;
}
// represents the method (if any) to access the class
public MethodVisitor visitMethod(int access, String name, String desc,
String signature, String[] exceptions) {
if(cv ! =null) {
return cv.visitMethod(access, name, desc, signature, exceptions);
}
return null;
}
// End access to this class
public void visitEnd(a) {
if(cv ! =null) { cv.visitEnd(); }}/ /... Some other visitXX methods are omitted
}
Copy the code
As you can see, all the visitXX methods of the ClassVisitor delegate logic to the visitorXX methods of the other ClassVisitor. We know that when a class is loaded into the JVM, its class structure is roughly as follows:
So comparing the class file structure with the methods in the ClassVisitor, we can see that the visitXX methods in the ClassVisitor, except for the visitEnd method, correspond to some structure in the class file, such as fields, methods, properties, etc. The parameters of each visitXX method represent information about fields, methods, properties, and so on. For example: Access is a modifier, signature is a generic, desc is a descriptor, and name is a name or fully-qualified name. We also notice that some visitXX methods return an instance of the XXVisitor class, which in turn has a similar visitXX method. This means that the external can continue to call the visitXX method of the returned XXVisitor instance, thus continuing to access the substructures in the corresponding structure, as explained later.
After we know what the methods in ClassVisitor do, we define a class to read and print information from that class using ClassReader and ClassVisitor. First, we define a class named OuterClass, as follows:
@Deprecated
public class OuterClass{
private int mData = 1;
public OuterClass(int data){
this.mData = data;
}
public int getData(a){
return mData;
}
class InnerClass{}}Copy the code
The OuterClass class has annotations, fields, methods, inner classes, and then a custom class named PrintClassVisitor that extends from ClassVisitor, as follows:
public class PrintClassVisitor extends ClassVisitor implements Opcodes {
public ClassPrinter(a) {
super(ASM7);
}
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
System.out.println(name + " extends " + superName + "{");
}
@Override
public void visitSource(String source, String debug) {
System.out.println(" source name = " + source);
}
@Override
public void visitOuterClass(String owner, String name, String descriptor) {
System.out.println(" outer class = " + name);
}
@Override
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
System.out.println(" annotation = " + descriptor);
return null;
}
@Override
public void visitInnerClass(String name, String outerName, String innerName, int access) {
System.out.println(" inner class = " + name);
}
@Override
public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
System.out.println(" field = " + name);
return null;
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
System.out.println(" method = " + name);
return null;
}
@Override
public void visitEnd(a) {
System.out.println("}"); }}Copy the code
There are many constants defined in the Opcodes interface. ASM7 is from the Opcodes. Each visitXX method prints out the class information, and then uses ClassReader to read the OuterClass bytecode. Pass in the ClassVisitor instance in the Accept method to complete access to the OuterClass as follows:
public static void main(String[] args) throws IOException {
// Create ClassVisitor instance
ClassPrinter printClassVisitor = new ClassPrinter();
ClassReader reads the OuterClass bytecode as an array of bytes
ClassReader classReader = new ClassReader(OuterClass.class.getName());
// Pass the ClassVisitor instance in the Accept of the ClassReader to enable access. The second argument indicates the mode of access
classReader.accept(printClassVisitor, 0); } Run output: com/example/plugindemo/OuterClass extends java/lang/Object{ source name = OuterClass.java annotation = Ljava/lang/Deprecated; innerclass = com/example/plugindemo/OuterClass$InnerClass
field = mData
method = <init>
method = getData
}
Copy the code
Classreaders are constructed to accept not only the fully qualified name of a class, but also the input stream of a class file, ultimately reading the class bytecode into memory as an array of bytes. The Accept method of ClassReader uses the memory offset to parse the byte array of the class bytecode read in the construct, parsing the structure information of the class bytecode from the byte array. The visitorXX method of the incoming ClassVisitor instance is then called to access the parsed structural information, and as you can see from the run output, there is a sequence of calls to the visitorXX method of the ClassVisitor in the Accept method, beginning with the visit method, End with the visitEnd method, intercalated with calls to the other visitXX methods, in the following general order:
visit
[visitSource]
[visitOuterClass]
[visitAnnotation]
[visitInnerClass | visitField | visitMethod]
visitEnd
// Where [] is optional and | is level
Copy the code
2, generate a class
Now that we know that ClassReader can be used to read a class, ClassVisitor can be used to access a class, and ClassWirter can be used to generate a class out of thin air, let’s generate an interface named Person that has the following structure:
public interface Person {
String NAME = "rain9155";
int getAge(a);
}
Copy the code
The code for generating the Person interface using ClassWriter is as follows:
import static org.objectweb.asm.Opcodes.*;
public class Main {
public static void main(String[] args){
// Create a ClassWriter and construct the behavior pattern of the class that is passed to modify
ClassWriter classWriter = new ClassWriter(0);
// Generate the class header
classWriter.visit(V1_7, ACC_PUBLIC + ACC_ABSTRACT + ACC_INTERFACE, "com/example/plugindemo/Person".null."java/lang/Object".null);
// Generate the file name
classWriter.visitSource("Person.java".null);
// Generate field NAME with value RAIN9155
FieldVisitor fileVisitor = classWriter.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "NAME"."Ljava/lang/String;".null."rain9155");
fileVisitor.visitEnd();
// Generate a method named getAge that returns int
MethodVisitor methodVisitor = classWriter.visitMethod(ACC_PUBLIC + ACC_ABSTRACT, "getAge"."()I".null.null);
methodVisitor.visitEnd();
// Class generation is complete
classWriter.visitEnd();
// The generated class can be returned as a byte array via the toByteArray method
byte[] bytes = classWriter.toByteArray();
}
Copy the code
ClassWirter inherits from ClassVisitor and extends the visitorXX method of ClassVisitor to give it the ability to generate class bytecode, Finally, the array of bytes returned by the toByteArray method can be dynamically loaded as a Class object by the ClassLoader. Since I’m generating an interface here, the getAge method has no method body. So the visitMethod method returns a MethodVisitor that simply calls visitEnd to generate the getAge method header. If you need to generate the internal logic of the getAge method, for example:
int getAge(a){
return 1;
}
Copy the code
So before calling the visitEnd method of the MethodVisitor, you need to call the other visitXX method of the MethodVisitor to generate the internal logic of the method. The visitXX method of the MethodVisitor is the bytecode instruction in the simulated JVM. For example, the push, the exit, the FieldVisitor returned by the visitField method and the AnnotationVisitor returned by the visitAnnotation method have the same meaning as the MethodVisitor.
You can see how tedious it is to generate a simple interface using ClassWirter. If it were a class and the methods in the class had method bodies, the code would be even more complicated. Fortunately, we can do this with the ASM Bytecode Outline plug-in. First install the plugin in your AS or IntelliJ IDE, then right-click on the class you want to view -> Show Bytecode Outline – it will Show the Bytecode and ASMified code in the side window. Click on the ASMified TAB to display the Asm code for this class. For example, here is the Asm code generated by the Person interface plugin:
As you can see, the Person interface is generated using ClassWriter.
3. Transform a class
ClassReader can be used to read a class, ClassVisitor can be used to access a class, and ClassWirter can be used to generate a class, so when we put these three together, we can read class bytecode through ClassReader, To transform a class, convert the class bytecode read by an extended ClassVisitor. After the transformation, regenerate the class using a ClassWirter. Let’s remove the OuterClass annotation. First, customize a ClassVisitor as follows:
public class RemoveAnnotationClassVisitor extends ClassVisitor implements Opcodes {
public RemoveAnnotationClassVisitor(ClassVisitor classVisitor) {
super(ASM7, classVisitor);
}
@Override
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
/ / returns null
return null; }}Copy the code
Here I’m just overriding the visitAnnotation method of the ClassVisitor, which returns null in the visitAnnotation method, so that the caller can’t use the returned AnnotationVisitor to generate annotations for the class, Then use the RemoveAnnotationClassVisitor, as follows:
public static void main(String[] args) throws IOException {
// Read the bytecode from OuterClass to ClassReader
ClassReader classReader = new ClassReader(OuterClass.class.getName());
// Define the ClassWriter used to generate the class
ClassWriter classWriter = new ClassWriter(0);
/ / hand into the ClassWriter RemoveAnnotationClassVisitor structure
RemoveAnnotationClassVisitor removeAnnotationClassVisitor = new RemoveAnnotationClassVisitor(classWriter);
/ / in the accept method of ClassReader incoming RemoveAnnotationClassVisitor instance, open access
classReader.accept(removeAnnotationClassVisitor, 0);
// Finally, use ClassWriter's toByteArray method to return an array of bytes for the transformed OuterClass class
byte[] bytes = classWriter.toByteArray();
}
Copy the code
This code simply combines the previous knowledge of reading, accessing, and generating a class. The construction of a ClassVisitor can be passed into a ClassVisitor, thus acting as a proxy for the incoming ClassVisitor, and the ClassWriter is inherited from the ClassVisitor. So the ClassWriter RemoveAnnotationClassVisitor agent, RemoveAnnotationClassVisitor handed OuterClass conversion and then to the ClassWriter, Finally we can use ClassWriter’s toByteArray method to return an array of bytes for the transformed OuterClass class.
The above code has a simple one ClassVisitor that does the transformation. If we extend it, we can also define RemoveMethodClassVisitor, AddFieldClassVisitor, and many other classVisitors with different functions. Then string all classVisitors into a chain of transformations, thinking of classReaders as the head, Classwriters as the tail, and a series of Classvisitors in the middle, The class bytecode read by the ClassReader passes through a series of ClassVisitor transformations to the ClassWriter, which eventually generates a new class by the ClassWriter. The process is shown below:
If you want to learn more about Asm, please refer to the official tutorial given at the beginning. Let’s learn about Transform.
Transform
Liverpoolfc.tv: Transform
Transform is part of the Android Gradle API. It can get all the.class files in the Android project before it is compiled into the.dex file. Then we can process all the.class files in the Transform. So Transform provides the ability for us to get the bytecode of an Android project. The red mark in the figure is the point of use for Transform:
The image above is part of the Android packaging process, and the Android packaging process is given to the Android Gradle Plugin, so if we want to customize the Transform, we have to inject it into the Android Gradle Plugin to make it work. The execution unit of the plugin is Task, but the Transform is not a Task. How is the Transform executed? The Android Gradle Plugin creates a TransformTask for each Transform, which then executes the corresponding Transform.
To introduce The Transform, first we need to introduce the Transform in build.gradle as follows:
dependencies {
// Reference the Android Gradle API, which includes the Transform API
implementation 'com. Android. Tools. Build: gradle: 4.0.0'
}
Copy the code
Since the Transform API is part of the Android Gradle API, we can simply introduce the Android Gradle API and customize a transform named MyTransform as follows:
public class MyTransform extends Transform {
@Override
public String getName(a) {
// The name used to generate the TransformTask
return "MyTransform";
}
@Override
public Set<QualifiedContent.ContentType> getInputTypes() {
// Input type
return TransformManager.CONTENT_CLASS;
}
@Override
public Set<? super QualifiedContent.Scope> getScopes() {
// The scope of the input
return TransformManager.SCOPE_FULL_PROJECT;
}
@Override
public boolean isIncremental(a) {
// Whether to enable incremental compilation
return false;
}
@Override
public void transform(TransformInvocation transformInvocation){
// This is where the class file is processed}}Copy the code
Transform is an abstract class, so it forces us to implement several methods and override the Transform method. Here’s what these methods mean:
1, getName method
Front said android gradle plugin will for each Transform to create a corresponding TransformTask, and create TransformTask the name of the general format for transformXX1WithXX2ForXX3, The value of XX2 is the return value of getName and the value of XX3 is the Build Variants of the current Build environment, such as Debug, Release etc. Build Variants for Debug, inputType for Class files, then the Transform the corresponding Task called transformClassesWithMyTransformForDebug.
2, the getInputTypes and getScopes methods
Both the getInputTypes and getScopes methods return a Set of elements of the ContentType interface and Scope enumeration, respectively. In the case of Transform, ContentType represents the type of the Transform input. The Scope represents the Scope of the Transform input. The Transform filters the Transform input from the ContentType and Scope dimensions. An input will only be consumed by Transform if it satisfies both the ContentType set returned by the getInputTypes method and the Scope set returned by the getScopes method.
In the Transform, there are two main types of input, CLASSES and RESOURCES, to implement an enumeration of the ContentType interface DefaultContentType. The meanings of each enumeration are as follows:
DefaultContentType | meaning |
---|---|
CLASSES | Represents a.class file in a JAR or folder |
RESOURCES | Represents a standard Java source file |
Similarly, in the Transform, the input Scope is also represented by the enumeration Scope, including PROJECT, SUB_PROJECTS, EXTERNAL_LIBRARIES, TESTED_CODE, and PROVIDED_ONLY. The meanings of each enumeration are as follows:
Scope | meaning |
---|---|
PROJECT | Only the current project is processed |
SUB_PROJECTS | Only subprojects of the current project are processed |
EXTERNAL_LIBRARIES | Only external dependencies for the current project are dealt with |
TESTED_CODE | Only the test code for the current project build environment is handled |
PROVIDED_ONLY | Only handles the provided-only dependent libraries for the current project |
ContentType and Scope can be combined separately, and the Set form is returned. The TransformManager class defines some common combinations that we can use directly. For example, MyTransform’s ContentType is CONTENT_CLASS. Scope is SCOPE_FULL_PROJECT, defined as follows:
public class TransformManager extends FilterableStreamCollection {
public static final Set<ContentType> CONTENT_CLASS = ImmutableSet.of(CLASSES);
public static final Set<ScopeType> SCOPE_FULL_PROJECT = ImmutableSet.of(Scope.PROJECT, Scope.SUB_PROJECTS, Scope.EXTERNAL_LIBRARIES);
/ /... There are many other combinations
}
Copy the code
You can see CONTENT_CLASS consists of CLASSES, SCOPE_FULL_PROJECT consists of PROJECT, SUB_PROJECTS, and EXTERNAL_LIBRARIES. So MyTransform will only handle.class file input from the current project (including subprojects) and external dependent libraries.
3. IsIncremental
The isIncremental method returns true to indicate whether the Transform supports incremental compilation. In fact, only tasks have incremental compilation. The Transform is eventually executed by the TransformTask. So Transform relies on Task to compile incrementally. Gradle Task incrementally compiles by detecting its inputs and outputs: When the changed file is detected, Gradle determines that the compile is incremental. Task internally increments output based on the changed file. If Gradle detects that the input has not changed since the last input, Gradle determines that the current up-to-data compilation can be skipped. If the output is deleted, Gradle determines that the compile is full and triggers the full output of the Task, i.e. output for all input files.
When the Transform is determined to be incrementally compiled, the Transform method can process each input file according to its Status, which is also an enumeration. The meanings of each enumeration are as follows:
Status | meaning |
---|---|
NOTCHANGED | This file has not changed since the last build |
ADDED | This file is a new file |
CHANGED | This file has changed (has been modified) since the last build |
REMOVED | The file has been deleted |
Enabling incremental compilation can greatly speed up Gradle builds.
Note: If your isIncremental method returns true, then the Transform method of your custom Transform must provide support for incremental compilation by processing the input file based on Status, otherwise incremental compilation will not take effect. You will see how support for incremental compilation is provided in a later plug-in implementation.
4, transform method
The transform method is where the input is processed in the transform, and the TransformTask executes the transform method that executes the transform. The transform method takes TransfromInvocation, It contains the input and output information for the current Transform. You can use the getInputs method of TransfromInvocation to get the input for the Transform, Use the getOutputProvider method of TransformInvocation to generate the output of the Transform, You can also determine whether the transform was incrementally compiled by using the return value of the isIncremental method of TransfromInvocation.
The getInputs method of TransfromInvocation returns a collection of elements of type TransformInput, where TransformInput can take two types of input, as follows:
public interface TransformInput {
// The getJarInputs method returns the JarInput collection
Collection<JarInput> getJarInputs(a);
// The getDirectoryInputs method returns a collection of DirectoryInputs
Collection<DirectoryInput> getDirectoryInputs(a);
}
Copy the code
The two types of input are abstracted as JarInput and DirectoryInput. JarInput represents input as. JarInput has a getStatus method to get the Status of the Jar file. And DirectoryInputgetChangedFiles method to get a Map < File, Status > set, so can traverse the Map collections, and then according to the corresponding Status to increment the File to the File.
TransfromInvocation’s getOutputProvider method returns a TransformOutputProvider that can be used to create the output position of the Transform, as follows:
public interface TransformOutputProvider {
// Delete all output
void deleteAll(a) throws IOException;
// Create an output position based on the name, ContentType, Scope, and Format given by the parameters
File getContentLocation(
@NonNull String name,
@NonNull Set<QualifiedContent.ContentType> types,
@NonNull Set<? super QualifiedContent.Scope> scopes,
@NonNull Format format);
}
Copy the code
Calling the getContentLocation method creates an output location and returns the File instance represented by that location, or if it exists, Through getContentLocation method to create the output of the general is located at/app/build/intermediates/transforms/build variants/transform name/directory, Where build variants are the current build environment such as Debug, release, etc., transform name is the return value of getName, For example in the debug build MyTransform output location is under the/app/build/intermediates/transforms/debug/MyTransform/directory, the directory is to Transform the output jar files or folders, The name is 0, 1, 2… Incrementing the name form, and calling the deleteAll method removes all files from the output location created by the getContentLocation method.
So if incremental compilation is not supported, the transform method would normally say:
public void transform(TransformInvocation transformInvocation) throws IOException {
// All inputs are fetched through the getInputs method of TransformInvocation, which is a collection. TransformInput represents one input
Collection<TransformInput> transformInputs = transformInvocation.getInputs();
GetOutputProvider (TransformInvocation) getOutputProvider (TransformOutputProvider); TransformOutputProvider (TransformOutputProvider) creates the Transform output
TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();
// Iterates through all the inputs, each containing a set of files for the two input types: JAR and directory
for(TransformInput transformInput : transformInputs){
Collection<JarInput> jarInputs = transformInput.getJarInputs();
// Iterate to process the JAR file
for(JarInput jarInput : jarInputs){
File dest = outputProvider.getContentLocation(
jarInput.getName(),
jarInput.getContentTypes(),
jarInput.getScopes(),
Format.JAR
);
// Here we simply copy the JAR file to the output location
FileUtils.copyFile(jarInput.getFile(), dest);
}
Collection<DirectoryInput> directoryInputs = transformInput.getDirectoryInputs();
// Traversal, processing folders
for(DirectoryInput directoryInput : directoryInputs){
File dest = outputProvider.getContentLocation(
directoryInput.getName(),
directoryInput.getContentTypes(),
directoryInput.getScopes(),
Format.DIRECTORY
);
// Here we simply recursively copy all the files in the folder to the output locationFileUtils.copyDirectory(directoryInput.getFile(), dest); }}}Copy the code
Take the input, walk through all the JarInput and DirectoryInput in the input, and then simply redirect the corresponding input to the output location. In the process, we can also get the class files in jar files and folders, modify the class files and redirect to the output. This is the goal of modifying the bytecode during compilation, which is at the heart of later plug-in implementations.
The output of each Transform is used as the input to the next Transform, which is executed sequentially, as follows:
Now that you have a general understanding of both Asm and Transform, you can start implementing the function time detection plug-in.
A plugin
Checking the time of a function is as simple as adding time detection logic at the beginning and end of each method. For example:
protected void onCreate(Bundle savedInstanceState) {
long startTime = System.currentTimeMillis();//start
super.onCreate(savedInstanceState);
long endTime = System.currentTimeMillis();//end
long costTime = endTime - startTime;
if(costTime > 100){
StackTraceElement thisMethodStack = (new Exception()).getStackTrace()[0];// Get the StackTraceElement for the current method
Log.e("TimeCost", String.format(
"===> %s.%s(%s:%s) method takes %d ms",
thisMethodStack.getClassName(), // Fully qualified name of the class
thisMethodStack.getMethodName(),/ / the method name
thisMethodStack.getFileName(), // Class file name
thisMethodStack.getLineNumber(),/ / line number
costTime // The method is time consuming)); }}Copy the code
It is impossible to manually add this code to the beginning and end of every method in the application. There are too many methods in the application, so we need the Gradle plugin to repeat the process for us. During the compilation of the project, we will use the Transform to retrieve the bytecode of every class in the project. If you don’t know how to customize a Gradle plugin, go back to the previous article. I put the Gradle plugin implementation code in the buildSrc directory. The directory structure of the whole project is as follows:
The code for the Plugin and Transform implementations is placed under com.example.plugin, and the code for the Asm implementation is placed under com.example.asm.
1. Customize Plugin
The corresponding code of custom Plugin is as follows:
public class TimeCostPlugin implements Plugin<Project> {
// When the running time of the function is greater than the threshold, it is considered as a time consuming function, unit: ms
public static long sThreshold = 100L;
// Print only the time functions in the package when the package has a value
public static String sPackage = "";
@Override
public void apply(Project project) {
try {
// Register an extension named time with the project instance
Time time = project.getExtensions().create("time", Time.class);
// Get the assignment in the time extension after the project is built
project.afterEvaluate(project1 -> {
if(time.getThreshold() >= 0){
sThreshold = time.getThreshold();
}
if(time.getAppPackage().length() > 0){ sPackage = time.getAppPackage(); }});// Get the android Gradle plugin extension instance named Android from the project instance
AppExtension appExtension = (AppExtension) project.getExtensions().getByName("android");
// Register our custom Transform in the Android Gradle Plugin by calling the appExtension instance registerTransform
appExtension.registerTransform(new TimeCostTransform());
}catch(UnknownDomainObjectException e){ e.printStackTrace(); }}/** * extend the corresponding bean class */
static class Time{
private long mThreshold = -1;
private String mPackage = "";
public Time(a){}
public long getThreshold(a) {
return mThreshold;
}
public void setThreshold(long threshold) {
this.mThreshold = threshold;
}
public String getAppPackage(a) {
return mPackage;
}
public void setAppPackage(String p) {
this.mPackage = p; }}}Copy the code
The TimeCostPlugin does two things:
1. Define an extension named time, the extension corresponding to the bean class is time, with this extension we can configure our plug-in in build.gradle, here I define the function time threshold and print functions through package filtering. Then we can use this in app/build.gradle:
apply plugin: com.example.plugin.TimeCostPlugin
// The function time threshold is 200ms, and only the functions in the application are inserted (excluding third-party libraries)
time{
threshold = 200
filter = 'com.example.plugindemo'
}
Copy the code
The assignment to the extended attribute is not available until after the project is built, so register the afterEvaluate callback in the project to get the assignment to the time extended attribute.
2. Inject our custom Transform into the Android Gradle Plugin. The Android Gradle Plugin is named android Gradle extension and the corresponding bean class is AppExtension class. AppExtension has a List of elements of type Transform. We call the registerTransform method to put TimeCostTransform into this collection. This Transform set will be used in the Android Gradle Plugin, which also registers the afterEvaluate callback of the project, In the callback it generates TransformTask for each Transform.
2, custom Transform
The corresponding code of the custom Transform is as follows:
public class TimeCostTransform extends Transform {
private static final String TAG = TimeCostTransform.class.getSimpleName();/ / the name of the class
@Override
public String getName(a) {
return TAG;
}
@Override
public Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS;
}
@Override
public Set<? super QualifiedContent.Scope> getScopes() {
return TransformManager.SCOPE_FULL_PROJECT;
}
@Override
public boolean isIncremental(a) {
return true;
}
@Override
public void transform(TransformInvocation transformInvocation) throws IOException {
System.out.println("transform(), ---------------------------start------------------------------");
Collection<TransformInput> transformInputs = transformInvocation.getInputs();
TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();
// check whether the Transform task isIncremental by using the isIncremental method of TransformInvocation. If the isIncremental method of the Transform returns false, The isIncremental method of TransformInvocation always returns false
boolean isIncremental = transformInvocation.isIncremental();
System.out.println("transform(), isIncremental = " + isIncremental);
// If not, delete all previously generated output and start again
if(! isIncremental){ outputProvider.deleteAll(); }// Iterates through all the inputs, each containing a set of files for the two input types: JAR and directory
for(TransformInput transformInput : transformInputs){
Collection<JarInput> jarInputs = transformInput.getJarInputs();
// Iterate over all jar file inputs
for(JarInput jarInput : jarInputs){
// Determine whether the Transform task is incremental
if(isIncremental){
// Incrementally process the Jar file
handleJarIncremental(jarInput, outputProvider);
}else {
// Handle Jar files non-incrementally
handleJar(jarInput, outputProvider);
}
}
Collection<DirectoryInput> directoryInputs = transformInput.getDirectoryInputs();
// Iterate through all the input directory files
for(DirectoryInput directoryInput : directoryInputs){
// Determine whether the Transform task is incremental
if(isIncremental){
// Incrementally process directory files
handleDirectoryIncremental(directoryInput, outputProvider);
}else {
// Non-incrementally process directory files
handleDirectory(directoryInput, outputProvider);
}
}
}
System.out.println("transform(), ---------------------------end------------------------------");
}
/ /...
}
Copy the code
According to the previous explanation of Transform, the meaning of each method in TimeCostTransform should be better understood. The most important one is the Transform method. Since I returned true in the isIncremental method indicating that TimeCostTransform supports incremental compilation, I need to do both full processing and incremental processing in the transform method depending on whether it isIncremental compilation or not. As the processing of JAR files and directory files are the same, the following is the processing of JAR files as an example, for the processing of directory files can be viewed at the end of the source link:
1. HandleJar method, which processes the jar file input in full, producing a new output:
private void handleJar(JarInput jarInput, TransformOutputProvider outputProvider) throws IOException {
// Get the input JAR file
File srcJar = jarInput.getFile();
// Construct the output location from the input using the getContentLocation method of the TransformOutputProvider
File destJar = outputProvider.getContentLocation(
jarInput.getName(),
jarInput.getContentTypes(),
jarInput.getScopes(),
Format.JAR
);
// Iterate through the contents of the srcJar and copy the contents of the srcJar to the destJar one by one
// If it is found that the content entry is a class file, it is modified by ASM and copied to the destJar
foreachJarWithTransform(srcJar, destJar);
}
Copy the code
The handleJar method determines the input and output and then calls the foreachJarWithTransform method as follows:
private void foreachJarWithTransform(File srcJar, File destJar) throws IOException {
try(
JarFile srcJarFile = new JarFile(srcJar);
JarOutputStream destJarFileOs = new JarOutputStream(new FileOutputStream(destJar))
){
Enumeration<JarEntry> enumeration = srcJarFile.entries();
// Iterate through each item in the srcJar
while (enumeration.hasMoreElements()){
JarEntry entry = enumeration.nextElement();
try(
// Get the input stream for each entry
InputStream entryIs = srcJarFile.getInputStream(entry)
){
destJarFileOs.putNextEntry(new JarEntry(entry.getName()));
if(entry.getName().endsWith(".class")) {// If it is a class file
// Use ASM to modify the source class file
ClassReader classReader = new ClassReader(entryIs);
ClassWriter classWriter = new ClassWriter(0);
TimeCostClassVisitor timeCostClassVisitor = new TimeCostClassVisitor(classWriter);
classReader.accept(timeCostClassVisitor, ClassReader.EXPAND_FRAMES);
// Then copy the modified class file to the destJar
destJarFileOs.write(classWriter.toByteArray());
}else {// If it is not a class file
// Copy it intact to the destJardestJarFileOs.write(IOUtils.toByteArray(entryIs)); } destJarFileOs.closeEntry(); }}}}Copy the code
Since the input is a JAR file, and a JAR file is essentially a ZIP file, the foreachJarWithTransform is like decompressing the JAR file, and then iterating through all the files in the decompressed JAR file to determine whether the file is a.class file. If the file is a.class file, it will be printed by ASM. If it is not, it will be copied to the output as it is. The logic is very simple.
2. The handleJarIncremental method increments the JAR file input and may produce new output:
private void handleJarIncremental(JarInput jarInput, TransformOutputProvider outputProvider) throws IOException {
// Get the status of the input file
Status status = jarInput.getStatus();
// Perform different operations based on the Status of the file
switch (status){
case ADDED:
case CHANGED:
handleJar(jarInput, outputProvider);
break;
case REMOVED:
// Delete all output
outputProvider.deleteAll();
break;
case NOTCHANGED:
//do nothing
break;
default:}}Copy the code
Incremental processing in the handleJarIncremental method is done by using the ADDED and CHANGED jar files based on their Status. REMOVED means that the input has been REMOVED, so the corresponding output is REMOVED. NOTCHANGED means that the input has NOTCHANGED, so it is skipped.
3. Asm processes class files
Transform (” transform “, “file”, “file”, “file”, “file”, “file”, “file”);
if(entry.getName().endsWith(".class")) {// If it is a class file
// Use ASM to modify the source class file
ClassReader classReader = new ClassReader(entryIs);
ClassWriter classWriter = new ClassWriter(0);
TimeCostClassVisitor timeCostClassVisitor = new TimeCostClassVisitor(classWriter);
classReader.accept(timeCostClassVisitor, ClassReader.EXPAND_FRAMES);
// Then copy the modified class file to the destJar
destJarFileOs.write(classWriter.toByteArray());
}
Copy the code
As described earlier in ASM, this is the step to use ASM to transform a class. First, use ClassReader to read the class file. Then call ClassReader’s Accept method to open access to the class visitor using TimeCostClassVisitor. The converted class byte stream is eventually obtained through the toByteArray method of ClassWriter, so the logic for modifying the class file is in the TimeCostClassVisitor, as follows:
public class TimeCostClassVisitor extends ClassVisitor implements Opcodes {
private String mPackage;/ / package name
private String mCurClassName;// The fully qualified name of the class being accessed
private boolean isExcludeOtherPackage;// Whether to exclude classes that are not part of package
public TimeCostClassVisitor(ClassVisitor classVisitor) {
super(ASM7, classVisitor);
mPackage = TimeCostPlugin.sPackage;
if(mPackage.length() > 0){
mPackage = mPackage.replace("."."/");
}
isExcludeOtherPackage = mPackage.length() > 0;
}
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
super.visit(version, access, name, signature, superName, interfaces);
mCurClassName = name;
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions);
if(isExcludeOtherPackage){
// If the class corresponding to the method is in the package
if(mCurClassName.startsWith(mPackage) && !"<init>".equals(name)){
return newTimeCostMethodVisitor(methodVisitor, access, descriptor); }}else {
if(!"<init>".equals(name)){
return newTimeCostMethodVisitor(methodVisitor, access, descriptor); }}returnmethodVisitor; }}Copy the code
TimeCostClassVisitor inherits from ClassVisitor. Since we only need to modify the methods in the class file, we only override the visit and visitMethod methods of the ClassVisitor. The visit method obtains the fully qualified name of the currently accessed class, which is combined with the visitMethod and the package name obtained by the TimeCostPlugin extension to determine whether the methods of this class need to be filtered out. If this class is not a class in the package, So instead of changing the methods in the class file of this class, skip, if this class is a class in the package, return TimeCostMethodVisitor, and change the methods in the class file in TimeCostMethodVisitor, So the logic for modifying the methods in the class file is in the TimeCostMethodVisitor, as follows:
class TimeCostMethodVisitor extends LocalVariablesSorter implements Opcodes {
// Local variables
int startTime, endTime, costTime, thisMethodStack;
public TimeCostMethodVisitor(MethodVisitor methodVisitor, int access, String desc) {
super(ASM7, access, desc, methodVisitor);
}
@Override
public void visitCode(a) {
super.visitCode();
/ /... Methods the beginning
//long startTime = System.currentTimeMillis();
}
@Override
public void visitInsn(int opcode) {
if(opcode == RETURN){
/ /... Methods the ending
//long endTime = System.currentTimeMillis();
//long costTime = endTime - startTime;
//if(costTime > 100){
// StackTraceElement thisMethodStack = (new Exception()).getStackTrace()[0]; // Get the StackTraceElement for the current method
// Log.e("TimeCost", String.format(
// "===> %s.%s(%s:%s) ",
/ / thisMethodStack getClassName (), / / the fully qualified name of the class
/ / thisMethodStack getMethodName (), / / the method name
/ / thisMethodStack getFileName (), / / the class file name
/ / thisMethodStack getLineNumber (), / / line number
// costTime // The method is time-consuming
/ /)
/ /);
/ /}
}
super.visitInsn(opcode); }}Copy the code
What we need to do is insert the code for the function time detection logic before and after the method, and the visitCode method is called when the bytecode of the method is first generated, which is called when the method begins, and the visitInsn method is called when the RETURN directive is called, which is called when the method ends normally, Asm will automatically convert the ASM code into bytecode for us, so that the generated method bytecode will contain the bytecode for our function time detection logic. TimeCostMethodVisitor inherits from LocalVariablesSorter, and LocalVariablesSorter inherits from MethodVisitor, which extends MethodVisitor, This makes it convenient to use local variables such as startTime, endTime, costTime, and thisMethodStack in the visitXX method of the MethodVisitor through the ASM code.
So we can use the ASM plugin described above to generate the ASM code for function time detection, as follows:
Because the generated ASM code is too long and the screenshots are incomplete, the asm code of the onCreate method header and end and the super.onCreate(savedInstanceState) code are removed, and the rest of the ASM code belongs to the function time detection logic. I have made some simplification. Remove some of the unnecessary visitLabel and visitLineNumber and copy it into the TimeCostMethodVisitor as follows:
class TimeCostMethodVisitor extends LocalVariablesSorter implements Opcodes {
// Local variables
int startTime, endTime, costTime, thisMethodStack;
public TimeCostMethodVisitor(MethodVisitor methodVisitor, int access, String desc) {
super(ASM7, access, desc, methodVisitor);
}
@Override
public void visitCode(a) {
super.visitCode();
//long startTime = System.currentTimeMillis();
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System"."currentTimeMillis"."()J".false);
startTime = newLocal(Type.LONG_TYPE);
mv.visitVarInsn(LSTORE, startTime);
}
@Override
public void visitInsn(int opcode) {
if(opcode == RETURN){
//long endTime = System.currentTimeMillis();
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System"."currentTimeMillis"."()J".false);
endTime = newLocal(Type.LONG_TYPE);
mv.visitVarInsn(LSTORE, endTime);
//long costTime = endTime - startTime;
mv.visitVarInsn(LLOAD, endTime);
mv.visitVarInsn(LLOAD, startTime);
mv.visitInsn(LSUB);
costTime = newLocal(Type.LONG_TYPE);
mv.visitVarInsn(LSTORE, costTime);
// Check whether costTime is greater than sThreshold
mv.visitVarInsn(LLOAD, costTime);
mv.visitLdcInsn(new Long(TimeCostPlugin.sThreshold));// The threshold is controlled by the TimeCostPlugin extension attribute threshold
mv.visitInsn(LCMP);
//if costTime <= sThreshold, jump to the end marker, otherwise continue to execute
Label end = new Label();
mv.visitJumpInsn(IFLE, end);
//StackTraceElement thisMethodStack = (new Exception()).getStackTrace()[0]
mv.visitTypeInsn(NEW, "java/lang/Exception");
mv.visitInsn(DUP);
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Exception"."<init>"."()V".false);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Exception"."getStackTrace"."()[Ljava/lang/StackTraceElement;".false);
mv.visitInsn(ICONST_0);
mv.visitInsn(AALOAD);
thisMethodStack = newLocal(Type.getType(StackTraceElement.class));
mv.visitVarInsn(ASTORE, thisMethodStack);
/ / the e (" rain ", the String. Format (" = = = > % s. % s (% s: % s) method takes % d ms ", thisMethodStack. GetClassName (), thisMethodStack.getMethodName(),thisMethodStack.getFileName(),thisMethodStack.getLineNumber(),costTime));
mv.visitLdcInsn("TimeCost");
mv.visitLdcInsn("===> %s.%s(%s:%s)\u65b9\u6cd5\u8017\u65f6 %d ms");
mv.visitInsn(ICONST_5);
mv.visitTypeInsn(ANEWARRAY, "java/lang/Object");
mv.visitInsn(DUP);
mv.visitInsn(ICONST_0);
mv.visitVarInsn(ALOAD, thisMethodStack);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StackTraceElement"."getClassName"."()Ljava/lang/String;".false);
mv.visitInsn(AASTORE);
mv.visitInsn(DUP);
mv.visitInsn(ICONST_1);
mv.visitVarInsn(ALOAD, thisMethodStack);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StackTraceElement"."getMethodName"."()Ljava/lang/String;".false);
mv.visitInsn(AASTORE);
mv.visitInsn(DUP);
mv.visitInsn(ICONST_2);
mv.visitVarInsn(ALOAD, thisMethodStack);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StackTraceElement"."getFileName"."()Ljava/lang/String;".false);
mv.visitInsn(AASTORE);
mv.visitInsn(DUP);
mv.visitInsn(ICONST_3);
mv.visitVarInsn(ALOAD, thisMethodStack);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StackTraceElement"."getLineNumber"."()I".false);
mv.visitMethodInsn(INVOKESTATIC, "java/lang/Integer"."valueOf"."(I)Ljava/lang/Integer;".false);
mv.visitInsn(AASTORE);
mv.visitInsn(DUP);
mv.visitInsn(ICONST_4);
mv.visitVarInsn(LLOAD, costTime);
mv.visitMethodInsn(INVOKESTATIC, "java/lang/Long"."valueOf"."(J)Ljava/lang/Long;".false);
mv.visitInsn(AASTORE);
mv.visitMethodInsn(INVOKESTATIC, "java/lang/String"."format"."(Ljava/lang/String; [Ljava/lang/Object;)Ljava/lang/String;".false);
mv.visitMethodInsn(INVOKESTATIC, "android/util/Log"."e"."(Ljava/lang/String; Ljava/lang/String;) I".false);
mv.visitInsn(POP);
// End at the end of the method
mv.visitLabel(end);
}
super.visitInsn(opcode); }}Copy the code
Each of these comments represents the asm code below. For local variables, the newLocal method of LocalVariablesSorter is used. If you look closely at the generated ASM code, it is quite regular. When the method bytecodes are generated using the visitXX method of the MethodVisitor, they are called in the following order (ignore the annotation annotations) :
[visitCode]
[visitLabel | visitLineNumber | visitFrame | visitXXInsn | visitLocalVariable | visitTryCatchBlock]
[visitMax]
visitEnd
// Where [] is optional and | is level
Copy the code
Similar to ClassVisitor, but begins with visitCode to generate the method body bytecode, calls visitLabel, visitXXInsn and so on to generate the method body bytecode, and then ends with a visitMax, and always ends with a visitEnd, If the method does not have a method body, then just call a visitEnd.
At this point the function time detection plugin is complete, using the same method as the gradle plugin.
conclusion
The gradle plugin is still rudimentary and can be extended, such as ns time threshold support, printing out the call stack when a time consuming function is found, but the purpose of this article is to learn how to customize gradle plugins, as well as asm and Transform. Since android Gradle API 3.6, most of the built-in Transforms used in ApK packaging have been implemented using tasks directly. For example, DesugarTransform -> DesugarTask, MergeClassesTransform -> MergeClassesTask, etc., may be to improve the efficiency of the build, which also shows that the transform is essentially dependent on task to complete, It is not a new thing, it is just android Gradle API for external, convenient external operation of bytecode tools, while Android Gradle API also has a lot of apK build with plug-ins, such as AppPlugin, LibrayPlugin, etc. We can also select one as a reference when writing gradle plug-ins.
That’s all for this article!
Source address of this article
References:
Android Gradle Plugin packs the Transform API of Apk
Play with bytecode in your Android project
Read AOP
Android Gradle Plugin main process analysis