AOP knowledge for Android engineers

Author: sloth is not lazy

If you have been exposed to Java background development, you must have heard the concept of AOP. What is it and what is its use for our Android development? This blog explores the familiar and unfamiliar concept of AOP from the perspective of Android Engineers:

What is AOP

AOP is the acronym of Aspect Oriented Program, which translates into aspect oriented programming. In this concept, the aspect is easy to understand and the programming can be understood. What is the most critical aspect?

Before understanding the aspect, first review our familiar OOP (Object-Oriented Programming), Object-Oriented Programming.

We know that the characteristics of object-oriented are inheritance, polymorphism and encapsulation. Encapsulation requires that functions be dispersed into different classes, which is often called responsibility assignment in software design. The advantage of this is to reduce the complexity of code and make classes reusable.

However, while dispersing the code, it also increases the repeatability of the code. What do you mean?

For example, suppose we want to add logs to each method of the two classes. According to the object-oriented design method, we must add the log code to the methods of these two classes. Maybe the added log code is exactly the same, but it is precisely because of the object-oriented design that classes cannot be connected with each other, and these repeated codes cannot be unified.

Maybe you think of a way: we can write this code in a method of a separate class, such as tool class, and then call it in two classes. However, in this way, the two classes are coupled with the newly added independent classes, that is, the change of independent class code will directly affect the two classes.

So, is there any way to realize the function without interfering with the original relationship between the two classes?

We can think about this problem with abstract thinking. Adding logs to each method of the class is a relatively more general operation.

It is different from adding logs to method B of class A, which is biased towards specific points. So it's more like operating on a face. This kind of operation is called slice.

Generally speaking, we call the code snippets that cut into the specified classes and methods as facets, while the classes and methods we cut into are called pointcuts. With AOP, we can extract the common code of several classes into a slice, and then cut into the object when necessary, so as to change its original behavior.

Because the programming idea of OOP can not help us realize this kind of aspect operation, and we do have this kind of demand, we have the concept of AOP. AOP, like OOP, is just a programming paradigm. It itself does not specify how to implement it. From the above series of explanations, we can also see that AOP is actually a supplement to OOP.

To sum up, aspect oriented programming can manage a certain type of functions of multiple unrelated classes.

Implementation of AOP

Static AOP

In the compiler, the aspect is directly compiled into the target bytecode file in the form of bytecode.

1.AspectJ

AspectJ belongs to static AOP. It is enhanced at compile time and will weave AOP logic into the code at compile time.

Because it is woven in the compiler, its advantage is that it does not affect the runtime performance, but its disadvantage is not flexible enough.

2.AbstractProcessor

Customize an AbstractProcessor, parse the compiled class during compilation, and generate a subclass (proxy class) that implements a specific interface according to the requirements

Dynamic AOP

1.JDK dynamic agent

By implementing the InvocationHandler interface, you can realize the dynamic proxy of a class. Through the dynamic proxy, you can generate the proxy class. Therefore, in the proxy class method, you can add your own implementation content before and after the execution of the proxy class method, so as to realize AOP.

2. Dynamic bytecode generation

In the runtime, after the target class is loaded, the bytecode file is dynamically built to generate the subclass of the target class, and the section logic is added to the subclass. It can be woven without interface, but it cannot be woven when the instance method of the extension class is final. Like Cglib

CGLIB is a powerful and high-performance code generation package. It provides proxies for classes that do not implement interfaces, and provides a good supplement to the dynamic proxy of JDK. You can usually use Java's dynamic proxy to create a proxy, but CGLIB is a good choice when the class to proxy does not implement an interface or for better performance.

3. Custom class loader

During the run-time, before the target is loaded, the slice logic is added to the target bytecode. For example: Javassist

Javassist is a class library that can dynamically edit Java bytecode. It can define a new class when the Java program is running and load it into the JVM; You can also modify a class file when the JVM loads.

4.ASM

ASM can directly modify the compiled bytecode file during compilation, or, like Javassit, modify the bytecode before loading the class file during runtime.

Application of AspectJ

Introduction to AspectJ

AspectJ provides two powerful mechanisms:

The first set is faceted grammar. It is the use method of AspectJ searched on the Internet, which returns the right to decide whether to use aspect to aspect. That is, when writing the aspect, you can decide which methods of which classes will be proxied, so there is no need to invade the business code logically.

Because this set of syntax is so famous, many people mistakenly think that AspectJ is this set of aspect syntax, but it is not.

The second set is weaving tools. The aspect syntax described above can decouple the aspect from the business code logically, but from the perspective of operation, when the JVM runs the business code, he has no way to know that there is a class next to him who wants to stab. The solution is to give priority to the aspect code during compilation (or class loading) and insert the aspect code into the business code in some form, so that the business code doesn't know that it has been "cut"? An implementation of this idea is aspectjweaver, which is the weaving tool here.

AspectJ provides two sets of description methods for facets:

One is our common method based on java annotation section description. This method is compatible with java syntax and is very convenient to write. It does not need additional syntax detection support of IDE.

The other is the section description method based on aspect file. This syntax itself is not java syntax. Therefore, when writing, it needs the support of IDE plug-in to check the syntax.

How to use AspectJ

This article mainly introduces the common use of java annotations.

First understand the following comments provided by AspectJ:

  • @Aspect: indicates that this is an AspectJ file. When compiling, the compiler will automatically parse it, and then inject the code into the corresponding JPonit
  • @Pointcut: represents a specific entry point, which can determine the specific weaving place into the code. You can specify points through wildcards, regular expressions, and so on
  • @Before: indicates that the method is called before calling the point.
  • @After: indicates that the method is called after the call point
  • @Around: use this method to replace the execution of this point

Join Point refers to the connection point, that is, the point that AOP can weave into the code:

Pointcuts are specific entry points that can determine where specific code is woven into. You can specify points through wildcards, regular expressions, etc. commonly used are:

Distinguish between execution and call:

Execution: the connection point used to match the execution of the method, which means that it is added directly before or after the method.

call: called when the matched method is called

Application of AspectJ in Android

AspectJ is the most common application we see in Spring, so is there a place for it in Android?

There must be. Next, we will use two chestnuts to see what application scenarios will need it

To prepare the environment first, you need to use the AspectJX plug-in: AspectJX plug-in address

project build Gradle add dependency

classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.10'

module build Gradle application plug-in

plugins {
    id 'android-aspectjx'
}

AspectJX is an AOP framework based on AspectJ and extended on this basis, which can be applied to Android development platform. It can act on java source code, class files and jar packages, and support kotlin applications.

Why use AspectJX instead of basic AspectJ or other?

At present, other AspectJ related plug-ins and frameworks do not support AAR or JAR entry, so they are more powerless for kotlin (the following chestnuts are implemented by kotlin)

Chestnut 1: implement a way to prevent continuous and fast click of View

I believe that the students on the client side should be devastated by the test method: clicking a button many times quickly leads to xxx

For this chestnut, preventing view from clicking quickly is a facet, so you can use AspectJ for facet programming.

Define an annotation @ FastClickView. The parameter interval indicates how long only one click takes effect:

@Target(AnnotationTarget.FUNCTION)
@Retention(value = AnnotationRetention.RUNTIME)
annotation class FastClickView(val interval: Long = 3000L)

Define the FastClickViewAspect class and take this class as the Aspect class: add @ Aspect annotation to this class

Determine the entry point: the method takes effect as long as the FastClickView annotation is used.

Execute using the execution matching method

// @com.example.aopdemo.FastClickView * *(..)  Represents any method that supports fastclickview annotation
@Pointcut("execution(@com.example.aopdemo.FastClickView * *(..))")

The complete code is as follows:

@Aspect
class FastClickViewAspect {
    @Pointcut("execution(@com.example.aopdemo.FastClickView * *(..))")
    fun executeFastClickViewLimit() { }
​
    @Around("executeFastClickViewLimit()")
    @Throws(Throwable::class)
    fun aroundExecuteFastClickViewLimit(joinPoint: ProceedingJoinPoint) {
        Log.d(TAG, "aroundClickCountLimit: ")
        val signature: MethodSignature = joinPoint.signature as MethodSignature
        // Method of removing JoinPoint
        val method = signature.method
        if (method.isAnnotationPresent(FastClickView::class.java)) {
            val annotation: FastClickView? = method.getAnnotation(FastClickView::class.java)
            annotation?.let {
                val interval = annotation.interval
                val view = joinPoint.args[0] as View
                if (!FastClickCheckUtil.isFastClick(view, interval)) {
                    joinPoint.proceed()
                }
            }
        }
    }
}
​
​
object FastClickCheckUtil {
    private const val TAG = "FastClickCheckUtil"
    /**
     * Determine whether it belongs to quick click
     *
     * @param view     Click View
     * @param interval Fast click threshold
     * @return true: Quick click
     */
    fun isFastClick(view: View, interval: Long): Boolean {
        val key: Int = view.id
        Log.d(TAG, "isFastClick: $view $interval")
        val currentClickTime: Long = System.currentTimeMillis()
        // If the interval between two clicks exceeds the threshold, it is not a fast click
        if (view.getTag(key) == null || currentClickTime - (view.getTag(key) as Long) > interval) {
            // Save last click time
            view.setTag(key, currentClickTime)
            return false
        }else{
            return true
        }
​
    }
}
​
// Test: prevent quick click within 2s
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_aop)
    btnFastClick.setOnClickListener(object: View.OnClickListener{
        @FastClickView(2000)
        override fun onClick(view: View?) {
            Log.d(TAG, "onClick: click me...")
        }
    })
}
Chestnut II: time-consuming statistical methods

During application startup optimization, we need to count the time-consuming of the method. The conventional method is to manually add code before and after the method to calculate the time-consuming.

This method is highly invasive and the code is repeated. At this point, the AOP method is very elegant:

Define annotation @ timeconsumption: as long as the annotation is added to the method, the method time can be counted and printed through the log

@Target(AnnotationTarget.FUNCTION)
@Retention(value = AnnotationRetention.RUNTIME)
annotation class TimeConsume

Define facet class TimeConsumeAspect:

Pointcut: any method that supports timeconsumption annotations

Cut in time: @ Before and @ After the annotated method

@Aspect
class TimeConsumeAspect {
​
    companion object {
        private const val TAG = "TimeConsumeAspect"
    }
​
    var startTime: Long = 0
​
    @Pointcut("execution(@com.example.aopdemo.TimeConsume * *(..))")
    fun methodTimeConsumePoint() {}
​
    @Before("methodTimeConsumePoint()")
    fun doBefore(joinPoint: JoinPoint) {
        val signature: MethodSignature = joinPoint.signature as MethodSignature
        val method = signature.method
        Log.d(TAG, "doBefore: $method")
        startTime = System.currentTimeMillis()
    }
​
    @After("methodTimeConsumePoint()")
    fun doAfter() {
        val endTime = System.currentTimeMillis()
        val consumeTime = endTime - startTime
        Log.d(TAG, "Start at ${startTime},End at $endTime, time consuming $consumeTime ms")
    }
}

Test code:

//test: 
@TimeConsume
override fun onStart() {
    try {
        Thread.sleep(3000)
    }catch (e: Exception){
        e.printStackTrace()
    }
    super.onStart()
}
​
@TimeConsume
override fun onResume() {
    super.onResume()
}

View print results:

D/TimeConsumeAspect: doBefore: protected void com.example.aopdemo.MainActivity.onStart()
D/TimeConsumeAspect: It starts at 1645418155237 and ends at 1645418158240, Time consuming 3003 ms
D/TimeConsumeAspect: doBefore: protected void com.example.aopdemo.MainActivity.onResume()
D/TimeConsumeAspect: It starts at 1645418158247 and ends at 16454118158263, Time consuming 16 ms

demo address

Other applications of AOP in Android

There are many application scenarios of AOP in Android, such as:

  1. Application of APT: Dagger2, ButterKnife, ARouter
  2. Javassist: Hot update

Keywords: Java Android Design Pattern AOP

Added by SerpentSword on Wed, 09 Mar 2022 08:20:54 +0200