Understand the AOP
In previous articles, we have introduced several technical solutions of AOP in detail. Since AOP technology is complex and diverse, and the actual requirements are not the same, how should we choose the technology?
This article will make a unified introduction to the existing AOP technology, especially focusing on the implementation of the Android direction, hoping to help you, the content of the article, examples are mostly from the work summary, if there are biased, welcome to correct.
Here first unified the basic noun, in order to express.
- Aspect: An abstraction of a class of behavior that is a collection of pointcuts, such as permission authentication before a user accesses all modules.
- Pointcut: Describes a specific business scenario of the pointcut.
- Advice types: usually pre-cut, post-cut, and in-cut, such as pre-cut when code is woven before method.
AOP is a general term for the technology of section-oriented programming. AOP framework will ultimately revolve around the operation of class bytecode. Whether the operation of bytecode is added, deleted or changed, for the convenience of description, we collectively refer to code weaving.
Although AOP translates as section-oriented programming, in practical use, aspects can degrade to a point, such as when we want to count the cold start time of an app, which is very specific. If we use AOP technology to achieve statistics of all the functions of the time, naturally can count up to similar to the start of this phase of the time.
In a narrow sense, an AOP framework must be able to abstract aspect programming into a tool or API that the upper layer can use directly, but when we reduce the dimension of the aspect, we end up facing the pointcut. In other words, as long as the code can be woven into a certain point, AOP can be implemented by such a technology, thus expanding the domain covered by AOP technology, because in a narrow sense only AspectJ currently meets this standard.
In a broad sense, AOP techniques can be any technology or framework that enables code weaving, where changes to code are ultimately reflected in bytecode. These techniques can also be called bytecode enhancement.
Here we introduce some common AOP techniques.
First, from the perspective of weaving timing, can be divided into source stage, class stage, DEX stage, runtime weaving.
For the first three source phase, class phase, dex weaving, because they all take place before the class is loaded into the VIRTUAL machine, we collectively call it static weaving, while changes in the run phase, we collectively call it dynamic weaving.
Common technical frameworks are as follows:
Woven into the timing | Technical framework |
---|---|
Static weave | APT, AspectJ, ASM, Javassit |
Dynamic weave | Java dynamic proxy, Cglib, Javassit |
Static weaving occurs at the compiler, so it has little impact on runtime efficiency; Dynamic weaving occurs at run time, bytecode can be written directly to memory, and classes can be loaded through reflection, so it is less efficient but more flexible.
Dynamic weaving is only possible if the class has not yet been loaded. You cannot load an already loaded class with modifications. This is a ClassLoader limitation. The virtual machine allows two classes with the same class name to be loaded by different classLoaders, which are considered to be two different classes at runtime. Therefore, do not assign values to each other, or ClassCastException will be thrown.
Java dynamic proxy, Cglib only creates a new proxy class rather than directly modifying the bytecode of the original class. Javassit can modify the bytecode of the original class.
In fact, the use of reflection or hook technology can also achieve code behavior change, but because this kind of technology did not really change the original bytecode, so not to talk about the scope, such as Xposed, dexposed.
Second, we need to focus on what aspects of programming capabilities these frameworks have. This will help me with my technology selection. Since AspectJ, ASM, and Javassit are relatively well-developed AOP frameworks, I will only compare them.
Ability to | AspectJ | ASM | Javassit |
---|---|---|---|
Section of the abstract | ✓ | ||
The point of tangency abstract | ✓ | ||
Notification type abstraction | ✓ | ✓ | ✓ |
Among them:
-
Aspect abstraction: The ability to filter a class. For example, if we want to weave code into an Activity’s entire lifecycle, do you first need the ability to filter an Activity and its subclasses?
-
Pointcut abstraction: specific to a class, the ability to access methods, fields, and annotations.
-
Notification type abstraction: Whether it is directly supported to weave code directly before, after, and in methods.
Of course, just because you don’t have the ability doesn’t mean you can’t do AOP programming, you can do it in other ways, just a matter of ease of use.
Let’ go~~~
APT
APT (Annotation Processing Tool) is an Annotation processor, which is replaced by Annotation Processor after Gradle version >=2.2.
It is used to scan and process annotations at compile time, using auto-service to simplify the configuration of finding annotations, and to generate Java files in the process (Java files are often created by relying on the Javapoet library). Often used to generate some template code or runtime dependent class files such as ButterKnife, Dagger, ARouter, it has the advantage of being simple and convenient.
Take ButterKnife as an example:
public class MainActivity extends AppCompatActivity {
@BindView(R.id.toolbar)
Toolbar toolbar;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main); ButterKnife.bind(this); }}Copy the code
How does a simple butterknip.bind (this) implement control assignment?
In fact, the @bind annotation generates a MainActivity_ViewBinding class at compile time, and the butterknife. Bind (this) call eventually creates a MainActivity_ViewBinding object by reflection, And pass it a reference to the activity.
# ButterKnife
public static Unbinder bind(@NonNull Object target, @NonNull View source) { Class<? > targetClass = target.getClass(); Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass); . // Create an xxx_Binding object and pass in the activityreturn constructor.newInstance(target, source); } private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<? > cls) { ... Try {// The runtime uses reflection to load classes generated at compile time Class<? > bindingClass = cls.getClassLoader().loadClass(clsName +"_ViewBinding"); bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls, View.class); }...return bindingCtor;
}
Copy the code
This finally completes the control assignment in the constructor of the MainActivity_ViewBinding.
public class MainActivity_ViewBinding<T extends MainActivity> implements Unbinder {
protected T target;
public MainActivity_ViewBinding(final T target, Finder finder, Object source) {... / / assignment for control Which controls the search target was optimized. The toolbar = finder. FindRequiredViewAsType (source, R.id.toolbar, "field 'toolbar'", Toolbar.class); . }}Copy the code
In order to access properties declared in MainActivity in this class, the ButterKnife framework requires that properties declared with the @bind annotation not be private.
You can see that reflection is still used in ButterKnife as a sacrifice to unify the API with Butterknip.bind (this), while Dagger dynamically generates a different method name via the Component, Module name, Therefore, you need to build the project before using it.
This is because of the deficiency of APT technology, which is usually only used to create new classes, but cannot change the original class. In the case that cannot be changed, dynamic can only be realized through reflection.
AspectJ
AspectJ is an AOP technique in the strictest sense because it provides complete annotations for faceted programming, allowing the user to weave code without caring about the principles of bytecode, since the faceted code written is the actual code to weave into.
AspectJ implements code weaving either by writing your own.ajC files or by using @aspect, @pointcut annotations provided by AspectJ, both of which are ultimately woven through the AJC compiler.
As a simple example, let’s say we want to count all view click events. Using AspectJ, we only need to write a class.
@Aspect
public class MethodAspect {
private static final String TAG = "MethodAspect5"; @pointcut (@pointcut);"execution(* android.view.View.OnClickListener+.onClick(..) )")
public void callMethod() {} //before means to weave @before(before) before method call"callMethod()") public void beforeMethodCall(ProceedingJoinPoint joinPoint) {// Write business code}}Copy the code
The notes are concise and intuitive, and the difficulty of getting started is almost zero.
Hugo, a commonly used function time statistics tool, is a practical application of AspectJ, and The Open-source AspectJX plug-in of Hujiang on Android platform is also inspired by Hugo. For details, see Hugo, an old Android function time statistics tool.
While AspectJ is useful, it has some serious problems.
- Repeat weaving, no weaving
AspectJ aspect expressions support inheritance syntax, which is handy for development, but fatal in that classes on the inheritance tree may all be woven into code, which is not applicable in most business scenarios, such as unburied points.
In addition, Java8 syntax is supported in aspectjx version 2.0.0.
See Android AspectJ for more details.
ASM
ASM is a very low-level AOP framework for bytecode-oriented programming that can theoretically implement any modifications to bytecode, very hardcore. Many bytecode generation apis, such as Groovy and Cglib, are implemented with ASM at the bottom, so using ASM on Android does not require additional dependencies. An understanding of bytecode and the JVM is essential to complete ASM learning.
Say you want to weave in a simple log output
Log.d("tag"." onCreate");
Copy the code
Writing with ASM looks like this, and that’s right because the JVM is stack-based, function calls require arguments to be pushed onto the stack, function calls to be executed, and function calls to be pushed off the stack. There are four JVM instructions in total.
mv.visitLdcInsn("tag");
mv.visitLdcInsn("onCreate");
mv.visitMethodInsn(INVOKESTATIC, "android/util/Log"."d"."(Ljava/lang/String; Ljava/lang/String;) I".false);
mv.visitInsn(POP);
Copy the code
You can see that ASM is very different from AspectJ in that the code that AspectJ weaves into is the code that is actually written, but ASM must use the API it provides to write instructions. One line of Java code may correspond to multiple lines of ASM API code, as multiple JVM instructions may be hidden behind one line of Java code.
You don’t have to worry about not being able to write ASM code. The ASM Bytecode Outline plugin is available to generate ASM code directly from Java code.
ASM is used in a wide range of practical scenarios, taking Matrix as an example.
Matrix is an open source APM framework of wechat, in which TraceCanary sub-module is used to monitor scenarios such as low frame rate, lag, and ANR, with the function of time consumption statistics.
In order to realize the time statistics of the function, it is usually done at the beginning and end of the function execution, and finally the time difference between the two points of the function execution is taken as the execution time.
# -> MethodTracer.TraceMethodAdapter
@Override
protected void onMethodEnter() {
TraceMethod traceMethod = mCollectedMethodMap.get(methodName);
if(traceMethod ! = null) { traceMethodCount.incrementAndGet(); mv.visitLdcInsn(traceMethod.id); / / entry into piles mv. VisitMethodInsn (INVOKESTATIC, TraceBuildConstants MATRIX_TRACE_CLASS,"i"."(I)V".false); } } @Override protected void onMethodExit(int opcode) { TraceMethod traceMethod = mCollectedMethodMap.get(methodName); . traceMethodCount.incrementAndGet(); mv.visitLdcInsn(traceMethod.id); / / export pile mv. VisitMethodInsn (INVOKESTATIC, TraceBuildConstants MATRIX_TRACE_CLASS,"o"."(I)V".false);
}
Copy the code
Basically, you add one line of code at the beginning and one line at the end of each method and hand it over to TraceMethod for statistics and calculations.
For details, see Trace Canary, a Caton analysis tool.
Next, let’s examine ASM’s shortcomings.
- The aspect code is hard coded and often written manually to filter conditions, which is not flexible enough. Imagine using ASM to implement a lifecycle method for counting all activities.
- It is difficult to weave in new code before and after method calls, whereas in AspectJ a call keyword solves the problem.
See the Android ASM framework for more details.
javassit
Javassit is an open source bytecode creation and editing library that is now a submodule of the Jboss Web container. It is simple, fast and, like AspectJ, requires no knowledge of bytecode and virtual machine instructions.
The javassit core class libraries include ClassPool, CtClass, CtMethod, and CtField.
- ClassPool: A CtClass object container based on the HashMap implementation.
- CtClass: Represents a class that can be retrieved from the ClassPool by the full class name.
- CtMethods: Represents methods in a class.
- CtFields: fields in a class.
The Javassit API is neat and intuitive, for example if we want to create a class dynamically and add a helloWorld method.
ClassPool pool = ClassPool.getDefault();
//通过makeClass创建类
CtClass ct = pool.makeClass("test.helloworld.Test"); Make (helloMethod = ctnewmethod.make (helloMethod = ctnewmethod.make (helloMethod = ctnewmethod.make ("public void helloWorld(String des){ System.out.println(des); }",ct); ct.addMethod(helloMethod); // writeFile ct.writefile (); // load into memory // ct.toclass ();Copy the code
Then, we want to weave in the code before and after the helloWorld method.
ClassPool pool = ClassPool.getDefault(); Class CtClass ct = pool.getctclass ("test.helloworld.Test"); CtMethod m = ct.getDeclaredMethod("helloWorld"); // Start the method with m.insertbefore ("{ System.out.print(\"before insert\");"); // At the end of the method, we can use this keyword m.insertafter ("{System.out.println(this.x); }"); // writeFile ct.writefile ();Copy the code
The syntax of Javassit is intuitive and concise, making it a feature of many open source projects.
For example, the hot fix of QQ Zone, the problem was that when the patch package was loaded for Odex optimization, the classes in the patch package were labeled is_Preverfied because the differential patch package did not depend on other dex (which helps to improve the performance at runtime). But in the actual patch runtime to reference other classes in dex, error will be thrown Java. Lang. IllegalAccessError: Class ref pre – verified Class resovled to unexpected implement.
The qzone team’s solution was to stake out all class constructors at compile time, refer to a pre-defined AnalyseLoad class, and then intervene in the subcontracting process to keep the class in a separate DEX, thus avoiding this problem.
The AOP solution used here is Javassit, see the details of the QQ space patch solution analysis.
There is also the recent open source plug-in framework Shadow. One of the requirements in shadow framework is that the plug-in package has the ability to run independently. When running the plug-in project, ShadowActivity, the parent class of Activity in the plug-in, inherits the Activity. ShadowActivity doesn’t have to inherit from the system Activity when the plug-in is loaded as a submodule, just as a proxy class. At this point, the Shadow team encapsulates the JavassistTransform to dynamically modify the Activity’s parent class at compile time.
See debugging Shadow’s correct posture for bytecode editing.
A dynamic proxy
Dynamic proxy is an implementation of the proxy pattern used to dynamically enhance the behavior of the original class at run time by generating class bytecode directly at run time and loading it into the virtual machine.
The JDK itself provides a Proxy class to implement dynamic proxies. We typically create proxy classes using the following API.
# java.lang.reflect.Proxypublic static Object newProxyInstance(ClassLoader loader, Class<? >[] interfaces, InvocationHandler h)Copy the code
The core pointcut code is defined in the InvocationHandler implementation class.
Public class InvocationHandlerImpl implements InvocationHandler {/** private Object mObj = null; public InvocationHandlerImpl(Object obj){ this.mObj = obj; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {// Pointcut Object result = method.invoke(this.mobj, args); // Post-pointcutreturnresult; }}Copy the code
This allows you to write the code to weave in at the locations of the front and back pointcuts.
Dynamic proxies are used in our common Retrofit framework. Retrofit provides a set of annotations that make it easy to develop web requests in which the parameters declared are the web requests that have been wrapped by the broker.
# Retrofit.create
public <T> T create(final Class<T> service) {
...
return(T) Proxy.newProxyInstance(service.getClassLoader(), new Class<? >[] { service }, newInvocationHandler() {
private final Platform platform = Platform.get();
private final Object[] emptyArgs = new Object[0];
@Override public Object invoke(Object proxy, Method method, @Nullable Object[] args)
throws Throwable {
// If the method is a method from Object then defer to normal invocation.
if (method.getDeclaringClass() == Object.class) {
return method.invoke(this, args);
}
if (platform.isDefaultMethod(method)) {
returnplatform.invokeDefaultMethod(method, service, proxy, args); } / / agentreturn loadServiceMethod(method).invoke(args != null ? args : emptyArgs);
}
});
}
Copy the code
The biggest problem with Java dynamic proxy is that it can only delegate interfaces, but not ordinary or abstract classes. This is because the default created proxy class inherits From Porxy, and Java does not support multiple inheritance, which greatly limits the use of dynamic proxy scenarios. Cglib can delegate ordinary classes.
See Proxy Patterns for design patterns for more details.
conclusion
Finally, we summarize the features and pros and cons of the AOP frameworks mentioned above so you can choose the technology you want.
Technical framework | The characteristics of | The development of the difficulty | advantage | insufficient |
---|---|---|---|---|
APT | Often used to reduce template code through annotations, the creation of enhanced classes depends on other frameworks. | End to end | Develop annotations to simplify upper-level coding. | Using annotations is intrusive to the original project. |
AspectJ | Provides complete annotations for faceted programming. | End to end | True AOP, AOP that supports wildmatch, inherited structures, does not require hard-coding aspects. | Repeat weaving, non – weaving problems |
ASM | Bytecode instruction – oriented programming, powerful. | U u u | Efficient, ASM5 began to support java8. | The cutting ability is insufficient, and some scenes need to be hard-coded. |
Javassit | The API is easy to understand and fast to develop. | u | Quick to handle, friendly to new people, with the ability to load class at runtime. | Note the class path loading problem when writing pointcut code. |
Java dynamic proxy | Extension proxy interface functionality at run time. | u | Runtime dynamic enhancement. | Only proxy interface is supported, poor scalability, poor reflection performance. |