preface

I introduced APT technology in (1).

In the second part of Android, I introduced AspectJ technology.

This article, the final in a series, shows you how to use Javassist to generate bytecode at compile time. Same old rule. Straight up.

A, Javassist

Javassist is a very convenient library for manipulating bytecodes. It enables Java programs to add or modify classes at run time. Javassist is not the only option for manipulating bytecode; ASM is also commonly used. Javassist is less efficient than ASM. However, Javassist provides a more user-friendly API that developers can use without knowing anything about bytecode. ASM cannot do this. Javassist is very simple, and let’s get a sense of it with two examples.

1.1 First example

This example demonstrates how to generate a class binary using Javassist.

public class Main {

    static ClassPool sClassPool = ClassPool.getDefault();

    public static void main(String[] args) throws Exception {
        // Construct a new Class MyThread.
      	CtClass myThread = sClassPool.makeClass("com.javassist.example.MyThread");
	// Set MyThread to public
        myThread.setModifiers(Modifier.PUBLIC);
        //继承Thread
        myThread.setSuperclass(sClassPool.getCtClass("java.lang.Thread"));
        // Implement the Cloneable interface
        myThread.addInterface(sClassPool.get("java.lang.Cloneable"));

        // Generate the private member variable I
        CtField ctField = new CtField(CtClass.intType,"i",myThread);
        ctField.setModifiers(Modifier.PRIVATE);
        myThread.addField(ctField);

        // Generate the constructor
        CtConstructor constructor = new CtConstructor(new CtClass[]{CtClass.intType}, myThread);
        constructor.setBody("this.i = $1;");
        myThread.addConstructor(constructor);

        // Construct a method declaration for the run method
        CtMethod runMethod = new CtMethod(CtClass.voidType,"run".null,myThread);
        runMethod.setModifiers(Modifier.PROTECTED);
        // Add the Override annotation to the run method
        ClassFile classFile = myThread.getClassFile();
        ConstPool constPool = classFile.getConstPool();
        AnnotationsAttribute overrideAnnotation = new AnnotationsAttribute(constPool,AnnotationsAttribute.visibleTag);
        overrideAnnotation.addAnnotation(new Annotation("Override",constPool));
        runMethod.getMethodInfo().addAttribute(overrideAnnotation);
        // Construct the body of the run method.
      	runMethod.setBody("while (true){" +
                " try {" +
                " Thread.sleep(1000L);" +
                " } catch (InterruptedException e) {" +
                " e.printStackTrace();" +
                "}" +
                " i++;" +
                "}");

        myThread.addMethod(runMethod);

        // Output files to the current directory
        myThread.writeFile(System.getProperty("user.dir")); }}Copy the code

Running the program, the current project produces the following:

Decompile myThread. class as follows:

package com.javassist.example;

public class MyThread extends Thread implements Cloneable {
    private int i;
    public MyThread(int var1) {
        this.i = var1;
    }

    @Override
    protected void run(a) {
        while(true) {
            try {
                Thread.sleep(1000L);
            } catch(InterruptedException var2) { var2.printStackTrace(); } + +this.i; }}}Copy the code

1.2 Second Example

This example demonstrates how to modify class bytecode. We extend some functionality to the MyTread.class generated in the first example.

public class Main {

    static ClassPool sClassPool = ClassPool.getDefault();

    public static void main(String[] args) throws Exception {
        // Specify the search path for the ClassPool.
        sClassPool.insertClassPath(System.getProperty("user.dir"));

        / / get MyThread
        CtClass myThread = sClassPool.get("com.javassist.example.MyThread");

        // Make the member variable I static
        CtField iField = myThread.getField("i");
        iField.setModifiers(Modifier.STATIC|Modifier.PRIVATE);

        // Get the run method
        CtMethod runMethod = myThread.getDeclaredMethod("run");
        // Insert code at the beginning of the run method.
        runMethod.insertBefore("System.out.println(\" start executing \");");
      
        // Outputs the new binary
        myThread.writeFile(System.getProperty("user.dir")); }}Copy the code

Run myThread. class and decompile myThread. class as follows:

package com.javassist.example;

public class MyThread extends Thread implements Cloneable {
    private static int i;
    public MyThread(int var1) {
        this.i = var1;
    }

    @Override
    protected void run(a) {
        System.out.println("Commence execution");
        while(true) {
            try {
                Thread.sleep(1000L);
            } catch(InterruptedException var2) { var2.printStackTrace(); } + +this.i; }}}Copy the code

Compile-time staking doesn’t require much from Javassist, and with the two examples above we can do most of what we need. If you want to learn more advanced usage, please go here. Next, I’ll cover just two classes: CtClass and ClassPool.

1.3 CtClass

CtClass represents a class in bytecode. CtClass provides an API for constructing a complete Class, such as inheriting a parent Class, implementing an interface, adding fields, adding methods, and so on. CtClass also provides the writeFile() method, which allows us to output binary files directly.

1.4 the ClassPool

ClassPool is the container for CtClass. ClassPool can create (makeClass) or get (GET)CtClass objects. When the CtClass object is retrieved, the classpool.get () method is called, and the search path needs to be specified in the ClassPool. Otherwise, the ClassPool wouldn’t know where to load the bytecode files. ClassPool maintains these lookup paths through the linked list, and we can insert paths into the head/end of the linked list with insertClassPath()\appendClassPath().

Javassist is just a tool for manipulating bytecode. To implement compile-time bytecode generation, Android Gradle also needs to provide an entry point, and Transform is that entry point. Next we move on to Transform.

Second, the Transform

Transform is a way of manipulating bytecodes provided by Android Gradle. When the App is compiled, our source code is first compiled into class and then into dex. When a class is compiled into a dex, it goes through a series of transforms.

Above is a list of transforms defined by Android Gradle. Jacoco, Proguard, InstantRun, Muti-dex, and more are all implemented by inheriting Transform. Currently, we can also customize the Transform.

2.1 Working principle of Transform

Let’s start by looking at how multiple transforms work together. Go straight to the diagram above.

The flow processing method is adopted between the transforms. Each Transform takes an input, and when it’s finished processing produces an output, which in turn serves as input to the next Transform. In this way, all the Transforms in turn complete their mission.

The input and output of the Transform are class/ JAR files.

2.1.1 Input (Input)

When the Transform receives input, it wraps the received content into a TransformInput collection. TransformInput consists of a JarInput collection and a DirectoryInput collection. JarInput represents the Jar file and DirectoryInput represents the directory.

2.1.2 Output (Output)

The output path of the Transform is not freely specified and must be generated by the TransformOutputProvider based on the name, scope, type, and so on. The specific code is as follows:

 String dest = outputProvider.getContentLocation(directoryInput.name,
                        directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
Copy the code

2.2 User-defined Transform

2.2.1 inherit the Transform

Let’s start by looking at the methods inherited Transform needs to implement.

public class CustomCodeTransform extends Transform {
    @Override
    public String getName(a) {
        return null;
    }

    @Override
    public Set<QualifiedContent.ContentType> getInputTypes() {
        return null;
    }

    @Override
    public Set<? super QualifiedContent.Scope> getScopes() {
        return null;
    }

    @Override
    public boolean isIncremental(a) {
        return false;
    }
  
    @Override
    public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        super.transform(transformInvocation); }}Copy the code
  • GetName () : Give the Transform a name.

  • GetInputTypes () : Transform The input type to be processed. DefaultContentType provides two types of input:

    1. CLASSES: Java compiled bytecode, which can be jar packages or directories.
    2. RESOURCES: Annotated Java RESOURCES.

    The TransformManager encapsulates InputTypes for us. Details are as follows:

        public static final Set<ContentType> CONTENT_CLASS = ImmutableSet.of(CLASSES);
        public static final Set<ContentType> CONTENT_JARS = ImmutableSet.of(CLASSES, RESOURCES);
        public static final Set<ContentType> CONTENT_RESOURCES = ImmutableSet.of(RESOURCES);
    Copy the code
  • GetScopes () : Scope of the Transform. It specifies the range of Input to receive. Scope defines the following ranges:

    1. PROJECT: Only the current PROJECT is processed.
    2. SUB_PROJECTS: Only subprojects are processed.
    3. PROJECT_LOCAL_DEPS: Only project local dependencies libraries (local jars, AAR) are dealt with.
    4. PROVIDED_ONLY: Processes only dependent libraries provided by a provided.
    5. EXTERNAL_LIBRARIES: Processes only all external dependent libraries.
    6. SUB_PROJECTS_LOCAL_DEPS: Local dependent libraries (local jars, AAR) that work only with subprojects
    7. TESTED_CODE: Only test code is processed.

    TransformManager also encapsulates common scopes for us. Details are as follows:

    public static final Set<ScopeType> PROJECT_ONLY = 
            ImmutableSet.of(Scope.PROJECT);
    
    public static final Set<Scope> SCOPE_FULL_PROJECT =
            Sets.immutableEnumSet(
                    Scope.PROJECT,
                    Scope.SUB_PROJECTS,
                    Scope.EXTERNAL_LIBRARIES);
    
    public static final Set<ScopeType> SCOPE_FULL_WITH_IR_FOR_DEXING =
            new ImmutableSet.Builder<ScopeType>()
                    .addAll(SCOPE_FULL_PROJECT)
                    .add(InternalScope.MAIN_SPLIT)
                    .build();
    
    public static final Set<ScopeType> SCOPE_FULL_LIBRARY_WITH_LOCAL_JARS =
            ImmutableSet.of(Scope.PROJECT, InternalScope.LOCAL_DEPS);
    Copy the code
  • IsIncremental () : Whether incremental updates are supported.

  • Transform () : Here’s our processing logic. With the TransformInvocation parameter, we get both the input and the TransformOutputProvider that determines the output.

    public interface TransformInvocation {
       /**
         * Returns the inputs/outputs of the transform.
         * @return the inputs/outputs of the transform.
         */
        @NonNull
        Collection<TransformInput> getInputs(a);
      	
       /**
         * Returns the output provider allowing to create content.
         * @return he output provider allowing to create content.
         */
        @Nullable
        TransformOutputProvider getOutputProvider(a);
    }
    Copy the code
2.2.2 Customize plug-ins, integrate Transform

Next comes the integration Transform. Integrating Transform requires a custom Gradle plug-in. This tutorial describes how to customize Gradle plugins. You can follow it to create a plugin. You can then register the CustomCodeTransform into gradle’s build process.

class CustomCodePlugin implements Plugin<Project> {
    @Override
    void apply(Project project) {
         AppExtension android = project.getExtensions().getByType(AppExtension.class);
      	 android.registerTransform(newRegisterTransform()); }}Copy the code

A simple componentized Activity routing framework

In the Android field, componentization has become a very mature technology after years of development. Componentization is a project architecture that separates an APP project into multiple components that do not depend on each other.

Our Activity routing framework consists of two modules. One module provides the API, which we’ll call common; The other module, which handles compile-time bytecode injection, is called Plugin.

Let’s take a look at Common. It has only two classes, as follows:

public interface IRouter {
    void register(Map<String,Class> routerMap);
}
Copy the code
public class Router {

    private static Router INSTANCE;
    private Map<String, Class> mRouterMap = new ConcurrentHashMap<>();

    / / the singleton
    private static Router getInstance(a) {
        if (INSTANCE == null) {
            synchronized (Router.class) {
                if (INSTANCE == null) {
                    INSTANCE = newRouter(); }}}return INSTANCE;
    }

    private Router(a) {
        init();
    }
    // Here is bytecode injection.
    private void init(a) {}/** * Activity jump *@param context
     * @paramActivityUrl Activity Routing path. * /
    public static void startActivity(Context context, String activityUrl) { Router router = getInstance(); Class<? > targetActivityClass = router.mRouterMap.get(activityUrl); Intent intent =newIntent(context,targetActivityClass); context.startActivity(intent); }}Copy the code

The two classes of Common are quite simple. IRouter is an interface. The only method external to the Router is startActivity.

Next, let’s skip the plugin and learn how to use the framework. Suppose our project is divided into three modules app, A and B. App is A shell project, which is only responsible for packaging and depends on A and B. A and B are common service components and do not depend on each other. Now component A has an AActivity in it, and component B wants to jump to AActivity. So how do we do that?

Create A new ARouterImpl implementation IRouter in the A component.

public class ARouterImpl implements IRouter {

    private static final String AActivity_PATH = "router://a_activity";

    @Override
    public void register(Map<String, Class> routerMap) { routerMap.put(AActivity_PATH, AActivity.class); }}Copy the code

When called in the B component, only

Router.startActivity(context,"router://a_activity");
Copy the code

Isn’t that amazing? The secret is in the plugin. At compile time, the plugin injects the following code into the Router init() :

private void init(a) { 
		ARouterImpl var1 = new ARouterImpl();
  	var.register(mRouterMap);
}
Copy the code

Plugin is a bit too much code, SO I won’t post it. The code for this section is all there.

The Demo is very simple, but it is useful for understanding the mechanics of routing frameworks like ARouter and WMRouter. Both use compile-time bytecode injection for routing table registration, but instead of Using Javassit, they use ASM, which is more efficient. They are easier to use because they use APT technology to make the mapping between paths and activities transparent. Code like ARouterImpl in Demo is generated by APT.