preface

When we start an Activity, we want to know how long it will take for it to fully load. If we want to analyze a page, we can modify it directly in the code.

At this time we can use the principle of AOP, on the basis of the existing class file to modify the generation of the class file we need.

We have already done custom plugins, this time we will use ASM to compile the peg operation.

The principle of

Packaging process

Let’s take a look at the packaging process:

We can see the above process:

  • The R files, source code, and Java code are all merged into the Java Compiler to generate the.class file. This and other contents are generated into a DEX file.
  • The kBuilder script generates an unsigned.apk file from the resource file and the.dex file.
  • Jarsigner signs the APK.

So what we’re going to do is we’re going to build on the.class file that was generated before dex. That’s where Teansform comes in.

Transform

Transform is an API for modifying class files during the project build phase. The Transform will be wrapped into a Task by Gradle after being registered, and executed after Java Compile Task has been executed.

Let’s look at some of the important ways it works

/** Specifies the task name of the transform */ @override StringgetName() {
        returnIndicate the input types: null} / * * CLASSES: class files, from the jar or folder RESOURCES: Java resource * / @ Override the Set < QualifiedContent. ContentType >getInputTypes() {
        returnNull} /** Specifies the scope of the input file: PROJECT: the current PROJECT code, SUB_PROJECTS: the subproject code, EXTERNAL_LIBRARIES: the external library code, TESTED_CODE: the test code, PROVIDED_ONLY: Provided library code, */ @override Set<? super QualifiedContent.Scope>getScopes() {
        returnNull} /** specifies whether it is an incremental build */ @override BooleanisIncremental() {
        return false
    }
Copy the code

The most important method is the Transform method, which obtains TransformInput, DirectoryInput, JarInput, and TransformOutputProvider through transformInvocation.

  • TransformInput: Input file abstraction, includingDirectoryInputThe collection andJarInputCollection.
  • DirectoryInput: represents the directory structure involved in source compilation and the source files below, which can be used to modify the structure of the output file and its bytecode files.
  • JarInput: All participating JAR files include local and remote JAR files.
  • TransformOutputProvider: The output of Transform, from which the output path can be obtained.

ASM

I’m using THE ASM approach for compile-time staking, which is a general-purpose Java bytecode manipulation and analysis framework. You can generate, transform, and analyze compiled Java Class files and use ASM tools to read, write, and transform JVM instruction sets. Jacac compiled class files.

Let’s take a look at the core classes of the ASM framework:

  • ClassReaderThis class parses a bytecode class file, takes an object that implements the ClassVisitor interface as an argument, and then calls the various methods of the ClassVisitor interface in turn for their own processing.
  • ClassWriter: subclass of ClassVisitor, used to output and generate class files. Pass when processing a class or methodFieldVisitorandMethodVisitorProcess. Each has its own important subclass:FiledWriterandMethodWriter. Calling visitMethod creates a new method in the class. Calling visitMethod indicates that the class has been created. Calling visitEnd indicates that the class is complete. The toByteArray method returns an array containing the complete bytecode content of the entire class file.
  • ClassAdapter: implements the ClassVisitor interface, which is constructed in a way that requires the ClassVisitor formation and saves the fields as protected ClassVisitor. In its implementation, each method is a static call to the classVisitor corresponding method, passing the same parameters. Filtering can be achieved by integrating the ClassAdapter and modifying some of its methods. It can be a filter for events.

implementation

Ok, now that we have the basics, let’s start implementing the functionality we need step by step.

First, let’s customize two annotations and a utility class for calculating time.

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface OnStartTime {
}
Copy the code
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface OnEndTime {
}
Copy the code

Time statistics are only done in methods annotated with these two.

Tools:

public class TimeCache {
    private static volatile TimeCache mInstance;

    private static byte[] mLock = new byte[0];

    private Map<String, Long> mStartTimes = new HashMap<>();

    private Map<String, Long> mEndTimes = new HashMap<>();

    private TimeCache() {}

    public static TimeCache getInstance() {
        if (mInstance == null) {
            synchronized (mLock) {
                if(mInstance == null) { mInstance = new TimeCache(); }}}return mInstance;
    }
    public void putStartTime(String className, long time) {
            mStartTimes.put(className, time);
    }

    public void putEndTime(String className, long time) {
        mEndTimes.put(className, time);
    }

    public void printlnTime(String className) {
        if(! mStartTimes.containsKey(className) || ! mEndTimes.containsKey(className)) { System.out.println("className ="+ className + "not exist");
        }
        long currTime = mEndTimes.get(className) - mStartTimes.get(className);
        System.out.println("className ="+ className + "Time consuming" + currTime+ " ns"); }}Copy the code

The elapsed time is calculated only after both onStart and onEnd are annotated.

Create a new Transform class to handle the Transform logic.

    @Override
    String getName() {
        return "custom_plugin"
    }

    @Override
    Set<QualifiedContent.ContentType> getInputTypes() {// Input type: class filereturn TransformManager.CONTENT_CLASS
    }

    @Override
    Set<? super QualifiedContent.Scope> getScopes() {// Input file range: project includes JAR packagesreturn TransformManager.SCOPE_FULL_PROJECT
    }
Copy the code
    @Override
    void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        println("//============asm visit start===============//")
        def startTime = System.currentTimeMillis()

        Collection<TransformInput> inputs = transformInvocation.inputs
        TransformOutputProvider outputProvider = transformInvocation.outputProvider

        if(outputProvider ! = null) { outputProvider.deleteAll() } inputs.each { TransformInput input -> input.directoryInputs.each { DirectoryInput  directoryInput -> handleDirectoryInput(directoryInput, outputProvider) } input.jarInputs.each { JarInput jarInput -> handleJarInput(jarInput, outputProvider) } } def customTime = (System.currentTimeMillis() - startTime) / 1000 println("plugin custom time = " + customTime + " s")
        println("//============asm visit end===============//")}Copy the code

Input falls into two categories: one in a project and one in a JAR package. We are currently only dealing with projects.

    static void handleDirectoryInput(DirectoryInput directoryInput, TransformOutputProvider outputProvider) {
        if(directoryInput.file.isDirectory()) { directoryInput.file.eachFileRecurse { File file -> def name = file.name // Exclude classes that do not need to be modifiedif (name.endsWith(".class") && !name.startsWith("R\$") &&!"R.class".equals(name) && !"BuildConfig.class".equals(name)) {
                    println("name =="+ name + "===is changing...") ClassReader classReader = new ClassReader(file.bytes) // ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS) // ClassVisitor classVisitor = new CustomClassVisitor(classWriter) classReader.accept(classVisitor, EXPAND_FRAMES) byte [] code = classWriter.toByteArray() FileOutputStream fos = new FileOutputStream (file. ParentFile. AbsolutePath + file. The separator + name) fos. Write (code) fos. Close ()}}} / / after processing the input file, The output to the next task def dest = outputProvider. GetContentLocation (directoryInput. Name, directoryInput contentTypes, directoryInput.scopes, Format.DIRECTORY) FileUtils.copyDirectory(directoryInput.file, dest) }Copy the code

The class we want to filter is processed in the ClassVisitor and then modified.

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        MethodVisitor methodVisitor = super.visitMethod(access, name, desc, signature, exceptions);

        methodVisitor = new AdviceAdapter(Opcodes.ASM5, methodVisitor, access, name, desc) {

            private boolean isStart = false;

            private boolean isEnd = false;
            @Override
            public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
                if ("Lcom/cn/lenny/annotation/OnStartTime;".equals(desc)) {
                    isStart = true;
                }
                if ("Lcom/cn/lenny/annotation/OnEndTime;".equals(desc)) {
                    isEnd = true;
                }
                return super.visitAnnotation(desc, visible);
            }

            @Override
            protected void onMethodEnter() {// the method startsif (isStart) {
//                    mv.visitLdcInsn(name);
                    mv.visitMethodInsn(INVOKESTATIC, "com/cn/lenny/annotation/TimeCache"."getInstance"."()Lcom/cn/lenny/annotation/TimeCache;".false);
                    mv.visitVarInsn(ALOAD, 0);
                    mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Object"."getClass"."()Ljava/lang/Class;".false);
                    mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Class"."getSimpleName"."()Ljava/lang/String;".false);
                    mv.visitMethodInsn(INVOKESTATIC, "java/lang/System"."currentTimeMillis"."()J".false);
                    mv.visitMethodInsn(INVOKEVIRTUAL, "com/cn/lenny/annotation/TimeCache"."putStartTime"."(Ljava/lang/String; J)V".false); } super.onMethodEnter(); } @override protected void onMethodExit(int opcode) {// The method endsif (isEnd) {
                    mv.visitLdcInsn(name);
                    mv.visitMethodInsn(INVOKESTATIC, "com/cn/lenny/annotation/TimeCache"."getInstance"."()Lcom/cn/lenny/annotation/TimeCache;".false);
                    mv.visitVarInsn(ALOAD, 0);
                    mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Object"."getClass"."()Ljava/lang/Class;".false);
                    mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Class"."getSimpleName"."()Ljava/lang/String;".false);
                    mv.visitMethodInsn(INVOKESTATIC, "java/lang/System"."currentTimeMillis"."()J".false);
                    mv.visitMethodInsn(INVOKEVIRTUAL, "com/cn/lenny/annotation/TimeCache"."putEndTime"."(Ljava/lang/String; J)V".false);
                    mv.visitLdcInsn(name);
                    mv.visitMethodInsn(INVOKESTATIC, "com/cn/lenny/annotation/TimeCache"."getInstance"."()Lcom/cn/lenny/annotation/TimeCache;".false);
                    mv.visitVarInsn(ALOAD, 0);
                    mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Object"."getClass"."()Ljava/lang/Class;".false);
                    mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Class"."getSimpleName"."()Ljava/lang/String;".false);
                    mv.visitMethodInsn(INVOKEVIRTUAL, "com/cn/lenny/annotation/TimeCache"."printlnTime"."(Ljava/lang/String;) V".false); } super.onMethodExit(opcode); }};return methodVisitor;
    }
Copy the code

For adding bytecodes, see a documentation on bytecodes, or use the ASM Bytecode Outline plug-in to help.

Let’s see if the compiled class does what we want it to do

public class TestActivity extends Activity {
    public TestActivity() {
    }

    @OnStartTime
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        TimeCache.getInstance().putStartTime(this.getClass().getSimpleName(), System.currentTimeMillis());
        super.onCreate(savedInstanceState);
        this.setContentView(2131296285);
    }

    @OnEndTime
    protected void onResume() {
        super.onResume();
        String var10000 = "onResume";
        TimeCache.getInstance().putEndTime(this.getClass().getSimpleName(), System.currentTimeMillis());
        String var10001 = "onResume"; TimeCache.getInstance().printlnTime(this.getClass().getSimpleName()); }}Copy the code

Wow, it worked.

Seeing this, I think you can write your own code to compile the peg.

conclusion

Using AOP thinking to count time, avoid the modification of the original code, reduce a lot of repetitive work, and reduce the coupling of the code; The disadvantage is that ASM operations are difficult to understand and interfere with the APK packaging process, resulting in slow compilation.

reference

Transform api

In-depth understanding of Android Gradle