The Previous Android Gradle Plugin(hereafter referred to as the Plugin)AGP) Development Guideone,two,threeWe took a more complete look at AGP development, along with AOP, our main character today.

AOP(short for Aspect Oriented Programming), meaning:Section-oriented programmingAnd OOP(Object Oriented Programming,Object-oriented programmingInstead of focusing on objects, AOP targets the same or similar code logic in business processes (section), and then unified processing, it is facing a certain step or stage in the processing process. These two design ideas are fundamentally different in their goals, but AOP and OOP are not antithetical. On the contrary, a clever combination of the two ideas to guide code will allow our code to remain reusable while significantly reducing the coupling between the parts.

OOP and AOP are both methodologies [1], which I personally think are the most accurate descriptions and summaries of both ideas.

Android AOP: Print logs when executing all Activity onResume and onPause methods in App.

Using inheritance

Using object inheritance, we can abstract a BaseActivity and print the log in the body of the BaseActivity’s onResume and onPause methods. Then all other Activity objects inherit from BaseActivity, which implements the requirements:

// BaseActivity.java
public abstract class BaseActivity extends AppCompatActivity {
    private static final String TAG = "lenebf";

    @Override
    protected void onPause(a) {
        super.onPause();
        Log.d(TAG, getClass().getSimpleName() + ": onPause");
    }

    @Override
    protected void onResume(a) {
        super.onResume();
        Log.d(TAG, getClass().getSimpleName() + ": onResume"); }}// MainActivity.java
public class MainActivity extends BaseActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); }}// SecondActivity.java
public class SecondActivity extends BaseActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); }}Copy the code

This is fine for new projects. If you’re working on a large project with dozens or hundreds of activities, isn’t it foolish to change the inheritance of each Activity one by one?

ActivityLifecycle

Jetpack’s lifecycle aware components can perform actions in response to changes in the lifecycle state of another component, such as an Activity or Fragment. Our onPause, onResume are part of the lifecycle callback, By registering ActivityLifecycleCallbacks we can get to all the Activity’s lifecycle callback, also easily achieve our requirements.

// DemoApplication.java
public class DemoApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
            ......
            @Override
            public void onActivityResumed(@NonNull Activity activity) {
                Log.d(TAG, "${activity.javaClass.simpleName}: onResume");
            }

            @Override
            public void onActivityPaused(@NonNull Activity activity) {
                Log.d(TAG, "${activity.javaClass.simpleName}: onPause"); }... }); }}Copy the code

Let’s take a look at the above code, the same logic in the same place, this is the legend of AOP? That’s right, you’re really smart! Not all requirements have a ready-made aspect like the above example. What if we require that activities in the tripartite library also need to print logs? How can we implement these requirements using AOP before Lifecycle libraries are available?

Transform

Android Gradle Plugin supports the Transform API since 1.5.0 to allow third-party plug-ins to manipulate compiled.class files before they are converted to.dex files. It’s easy to use. Create a Gradle Plugin Module and create a class that implements the Transform interface. Get AppExtension and use android. RegisterTransform (theTransform) or android. RegisterTransform (theTransform, Dependencies) register Transform to the Android Gradle Plugin extension property. The Transform reference documentation: developer.android.com/reference/t…

ASM

In order to modify the.class file, we first need to understand the.class file structure, which is not a simple task. ASM is a general-purpose Java bytecode manipulation and analysis framework. It can be used to modify existing classes or to generate classes dynamically directly in binary form. IO/ASm4-guide…. . In simple terms, ASM abstracts.class files into objects like ClassVisitor, MethodVisitor, etc. The ClassVisitor reads and analyzes the.class file, and when it discovers methods, You hand it to the MethodVisitor to analyze and modify the method logic, and of course you can add methods to the class visitor.

ASM Bytecode Outline

While ASM makes it easier to parse and modify.class files, the API itself has some learning costs that are not covered in detail in this article. You can read the ASM development documentation for yourself. Here we use another artifact ASM Bytecode Outline, this is an IDEA plug-in, compatible with IntelliJ IDEA, Android Studio, through this plug-in we can view the Java file corresponding ASM code. You can also compare the ASM code between two Java files. In our example, you can compare the ASM code between the logless and log-printed logic to get the ASM code you need.

  • ASM Bytecode Outline

The latest version of ASM Bytecode Outline is not available on Android Studio 4.1. It only supports IntelliJ IDEA. We used IntelliJ IDEA to install. Install the ASM Bytecode Outline plugin and restart IntelliJ IDEA.

  • Viewing ASM code

Right-click (class file or code) -> Show Bytecode Outline -> ASMified

  • Display ASM code differences

Right-click (class file or code) -> Show Bytecode Outline -> ASMified -> Show differences

Create Gradle Plugin

This Plugin is called ac_logger. For details on how to create a Gradle Plugin Module, see Android Gradle Plugin Development Guide (1). Because the Transform are Android Gradle Plugin API, so our Plugin needs to rely on com. Android. View the build: Gradle; We also need to use ASM, so the plugin also needs to rely on org.ow2. ASM: ASM :9.0 for details, see the Android Gradle Plugin Development Guide (2).

Create the Transform implementation class

Note that the plugin can be written in groovy or Java, and the location of the class varies depending on the language used:

  • java – src/main/java/…
  • groovy – src/main/groovy/…

Logic is relatively simple, directly on the code, notes to write more clear:

public class LoggerTransform extends Transform {

    @Override
    public String getName(a) {
        // The name of the converter
        return "ac_logger";
    }

    @Override
    public Set<QualifiedContent.ContentType> getInputTypes() {
        // Returns the type of data that the converter needs to consume. We need to process all the class content
        return TransformManager.CONTENT_CLASS;
    }

    @Override
    public Set<? super QualifiedContent.Scope> getScopes() {
        // Returns the scope of the converter, that is, the processing scope. We only deal with the classes in Project
        return TransformManager.PROJECT_ONLY;
    }

    @Override
    public boolean isIncremental(a) {
        // Do you support increments
        return false;
    }

    @Override
    public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        super.transform(transformInvocation);
        // TODO implements conversion logic}}Copy the code

Register the Transform

public class ActivityLoggerPlugin implements Plugin<Project> {
    public void apply(Project project) {
        // Register a transform
        def android = project.extensions.getByType(AppExtension)
        android.registerTransform(new LoggerTransform())
    }
}
Copy the code

Iterate through all.class files

The Trasnform API has two input types: directory and Jar file (corresponding to three-party libraries). I’m only dealing with directory input here. We need to filter out all.class files from the directory input.

@Override
public void transform(TransformInvocation transformInvocation) throws TransformException,
        InterruptedException, IOException {
    super.transform(transformInvocation);
    TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();
    if (outputProvider == null) {
        return;
    }
    // Since we do not support incremental compilation, empty the OutputProvider contents
    outputProvider.deleteAll();
    Collection<TransformInput> transformInputs = transformInvocation.getInputs();
    transformInputs.forEach(transformInput -> {
        // There are two conversion inputs, one is a directory and the other is a Jar file.
        // Process directory input
        transformInput.getDirectoryInputs().forEach(directoryInput -> {
            File directoryInputFile = directoryInput.getFile();
            // Find all the class files in the transformation input, see github code for logic
            List<File> files = filterClassFiles(directoryInputFile);
            // TODO edits the class file to add log printing logic
            // Input must be output, otherwise there will be a class missing problem,
            // With or without conversion, we need to copy the input directory to the destination directory
            File dest = outputProvider.getContentLocation(directoryInput.getName(),
                    directoryInput.getContentTypes(),
                    directoryInput.getScopes(),
                    Format.DIRECTORY);
            try {
                FileUtils.copyDirectory(directoryInput.getFile(), dest);
            } catch(IOException e) { e.printStackTrace(); }});// Process the JAR input
        transformInput.getJarInputs().forEach(jarInput -> {
            // Input must be output, otherwise there will be a class missing problem,
            // We don't need to modify the Jar file here, just print it
            File jarInputFile = jarInput.getFile();
            File dest = outputProvider.getContentLocation(jarInput.getName(),
                    jarInput.getContentTypes(),
                    jarInput.getScopes(),
                    Format.JAR);
            try {
                FileUtils.copyFile(jarInputFile, dest);
            } catch(IOException e) { e.printStackTrace(); }}); }); }Copy the code

The important thing to note here is that the Transform API requires input and output, so even files that don’t need to be processed need to be output to the target directory.

Modify the.class file

In ASM, a ClassReader class is provided. This class can obtain bytecode data directly from byte arrays or indirectly from class files. It can correctly analyze bytecode and build an abstract tree to represent bytecode in memory. It calls the Accept method, which takes as an argument an object instance inherited from the ClassVisitor abstract class, and then calls the various methods of the ClassVisitor abstract class in turn. The ClassWriter ClassWriter, inherited from the ClassVisitor, implements specific bytecode editing. Each ClassVisitor, in chain-of-responsibility mode, can encapsulate changes to bytecode very simply, without paying attention to byte offsets of bytecode, because these implementation details are hidden from the user. All the user has to do is override the corresponding visit function. We first need to implement our own ClassVisitor that implements the associated visit function to add the log print code:

public class ActivityClassVisitor extends ClassVisitor {

    /** * the full class name of the Activity's parent class. Here we only deal with subclasses of AppcompatActivity, * other Activity subclasses need to be processed in production */
    private static final String ACTIVITY_SUPER_NAME = "androidx/appcompat/app/AppCompatActivity";
    private static final String ON_PAUSE = "onPause";
    private static final String ON_RESUME = "onResume";

    private String superName = null;
    private boolean visitedOnPause = false;
    private boolean visitedOnResume = false;

    public ActivityClassVisitor(ClassVisitor classVisitor) {
        // opcodes.asm9 represents the version of the ASM API we use, the latest version of API 9 used here
        super(Opcodes.ASM9, classVisitor);
    }

    @Override
    public void visit(int version, int access, String name, String signature, String superName,
                      String[] interfaces) {
        super.visit(version, access, name, signature, superName, interfaces);
        Name Indicates the full class name of the current class. SuperName indicates the full class name of the parent class. Access is accessible
        // Exclude abstract classes
        if((access & Opcodes.ACC_ABSTRACT) ! = Opcodes.ACC_ABSTRACT) {this.superName = superName; }}@Override
    public void visitEnd(a) {
        super.visitEnd();
        if(superName ! =null && superName.equals(ACTIVITY_SUPER_NAME)) {
            OnPause or onResume is not iterated through the class
            if(! visitedOnResume) { visitedOnResume =true;
                insertMethodAndLog(ON_RESUME);
            }
            if(! visitedOnPause) { visitedOnPause =true; insertMethodAndLog(ON_PAUSE); }}}@Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature,
                                     String[] exceptions) {
        MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
        if(superName ! =null && superName.equals(ACTIVITY_SUPER_NAME)) {
            // subclasses of AppcompatActivity
            if (ON_PAUSE.equals(name)) {
                / / onPause method
                visitedOnPause = true;
                addLogCodeForMethod(mv, name);
            } else if (ON_RESUME.equals(name)) {
                / / onResume method
                visitedOnResume = true; addLogCodeForMethod(mv, name); }}return mv;
    }

    private void addLogCodeForMethod(MethodVisitor mv, String methodName) {
        mv.visitLdcInsn("lenebf");
        // Create a new StringBuilder instance
        mv.visitTypeInsn(Opcodes.NEW, "java/lang/StringBuilder");
        mv.visitInsn(Opcodes.DUP);
        // Call the StringBuilder initialization method
        mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/StringBuilder"."<init>"."()V".false);
        mv.visitVarInsn(Opcodes.ALOAD, 0);
        // Get the SimpleName of the current class
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Object"."getClass"."()Ljava/lang/Class;".false);
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Class"."getSimpleName"."()Ljava/lang/String;".false);
        // Append the SimpleName of the current class to StringBuilder
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder"."append"."(Ljava/lang/String;) Ljava/lang/StringBuilder;".false);
        // Append the method name to StringBuilder
        mv.visitLdcInsn(":" + methodName);
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder"."append"."(Ljava/lang/String;) Ljava/lang/StringBuilder;".false);
        // call toString on StringBuilder to convert StringBuilder toString
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder"."toString"."()Ljava/lang/String;".false);
        // Call the log.d method
        mv.visitMethodInsn(Opcodes.INVOKESTATIC, "android/util/Log"."d"."(Ljava/lang/String; Ljava/lang/String;) I".false);
        mv.visitInsn(Opcodes.POP);
    }

    private void insertMethodAndLog(String methodName) {
        // Create a new method
        MethodVisitor mv = cv.visitMethod(Opcodes.ACC_PROTECTED, methodName, "()V".null.null);
        // Access the new method to populate the method logic,
        mv.visitCode();
        mv.visitVarInsn(Opcodes.ALOAD, 0);
        mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "androidx/appcompat/app/AppCompatActivity", methodName, "()V".false);
        mv.visitLdcInsn("lenebf");
        mv.visitTypeInsn(Opcodes.NEW, "java/lang/StringBuilder");
        mv.visitInsn(Opcodes.DUP);
        mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/StringBuilder"."<init>"."()V".false);
        mv.visitVarInsn(Opcodes.ALOAD, 0);
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Object"."getClass"."()Ljava/lang/Class;".false);
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Class"."getSimpleName"."()Ljava/lang/String;".false);
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder"."append"."(Ljava/lang/String;) Ljava/lang/StringBuilder;".false);
        mv.visitLdcInsn(":" + methodName);
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder"."append"."(Ljava/lang/String;) Ljava/lang/StringBuilder;".false);
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder"."toString"."()Ljava/lang/String;".false);
        mv.visitMethodInsn(Opcodes.INVOKESTATIC, "android/util/Log"."d"."(Ljava/lang/String; Ljava/lang/String;) I".false); mv.visitInsn(Opcodes.POP); mv.visitInsn(Opcodes.RETURN); mv.visitEnd(); }}Copy the code

Then use our ClassVisitor in the transform function:

@Override
public void transform(TransformInvocation transformInvocation) throws TransformException,
        InterruptedException, IOException {... transformInputs.forEach(transformInput -> {// There are two conversion inputs, one is a directory and the other is a Jar file.
        // Process directory input
        transformInput.getDirectoryInputs().forEach(directoryInput -> {
            File directoryInputFile = directoryInput.getFile();
            List<File> files = filterClassFiles(directoryInputFile);
            for (File file : files) {
                FileInputStream inputStream = null;
                FileOutputStream outputStream = null;
                try {
                    // Write to the class file
                    ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
                    // Access the corresponding contents of the class file, resolving to a structure notifies the corresponding methods of the ClassVisitor
                    ClassVisitor classVisitor = new ActivityClassVisitor(classWriter);
                    // Read and parse the class file
                    inputStream = new FileInputStream(file);
                    ClassReader classReader = new ClassReader(inputStream);
                    // Call each method of the ClassVisitor interface in turn
                    classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES);
                    The toByteArray method returns the final modified bytecode as a byte array.
                    byte[] bytes = classWriter.toByteArray();
                    // Overwrite the original content by writing to the file stream to implement class file rewriting.
                    outputStream = new FileOutputStream(file.getPath());
                    outputStream.write(bytes);
                    outputStream.flush();
                } catch (Throwable throwable) {
                    throwable.printStackTrace();
                } finally{ closeQuietly(inputStream); closeQuietly(outputStream); }}... }); . }); }Copy the code

At this point, our requirements are complete. What is the effect?

The inspection results

We have a number of ways to check that our plugin does what we need it to do. The most straightforward is to run our Demo and see if there is a corresponding log output:The correct log message was printed, as we expected. We can also directly to check the output of the Transform. The class file, located in the app/build/intermediates/transforms directory:Our log print code has been added correctly. You can also use the Apktool to decompile our Apkto see the code implementation.

supplement

In addition to ASM, the Java bytecode manipulation and analysis framework is also known as AspectJ. Personally, as a tool, there is no good or bad between the two frameworks. You can choose the one you are good at according to your specific needs. This article code address: github.com/lenebf/Andr…

The resources

  • [1] In-depth understanding of Android AOP
  • [2] AOP tools: INTRODUCTION to ASM 3.0