diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 00000000..e69de29b diff --git a/404.html b/404.html new file mode 100644 index 00000000..5dcb5bde --- /dev/null +++ b/404.html @@ -0,0 +1 @@ +
This "assistant" is a plug-in for Android Studio, which is equivalent to a small helper when you use this library. It only helps you generate some AOP code, and has no effect on your code.
The plug-in generates AOP auxiliary code for the target class, including the following functions:
Although there is such a plug-in, you also need to understand how to use this library to identify and select the generated code, and don't copy it blindly~
Plugin Market, search for the plug-in AndroidAOP Code Viewer in Android Studio and install it
👆The plug-in market needs to be reviewed and may not be the latest version
Click here to download the plugin, then search for how to install the local plugin
👆Download link here to keep up with the latest features
After installation, a plug-in named AOPCode will be displayed on the right side of the IDE
Right-click the mouse on the code you want to cut into -> Click AndroidAOP Code -> Click AOPCode on the right to view the generated code, as shown in the figure:
@AndroidAopReplaceClass
, @AndroidAopReplaceMethod
and The class name and function signature in the @AndroidAopMatchClassMethod
code are absolutely correct (Please correct me if you have any questions).@AndroidAopReplaceMethod
Java method does not include the suspend function of the Kotlin source code@AndroidAopReplaceMethod
method may have some deviations, so you need to compare it yourself. For example: nullable?
, Is it the type of Kotlin source code
, variable parameter type becomes array type
, etc., these cannot be guaranteed to be accurately copiedThis resource library comes with obfuscation rules, and it will be automatically imported. Under normal circumstances, there is no need to import it manually.
Some friends will find that before obfuscation, the line number after the error can locate the error position, but after obfuscation, the original line number cannot be mapped out through the ProGuard
tool, but it can be mapped out before using this library. That's right! Let's talk about the solution below.
Regarding the mapping file configuration, add the following configuration to the obfuscation configuration file
This aspect collects inherited classes or classes that match regular expressions. Its annotated methods will be automatically called back when you use the class for the first time
@AndroidAopCollectMethod(
+ /**
+ * Collection type
+ */
+ collectType = CollectType.DIRECT_EXTENDS,
+ /**
+ * This item is a regular expression
+ * After setting the regular expression, the parameter of the annotated method can be Object or Any. If it is not set, the type must be specified
+ * After setting the regular expression, the regular expression you set will be used to find the class name that meets the requirements
+ */
+ regex = ""
+)
+
The modified method must be a static method, and the return value type is not set
It directly modifies the method, and the modified method has one and only one parameter. This function is to collect all classes in the application that inherit this parameter. If the parameter type is:
In addition, each class collected by its annotated method will only be initialized and called back once through this static method.
The last point is that this method should only contain relevant saving code, and do not perform other operations, and try to avoid abnormal behavior (because you only have one chance to receive...)
DIRECT_EXTENDS
, and the following three types can be set¶EXTENDS
means that it matches all classes inherited from annotation method parameters set
DIRECT_EXTENDS
means that it matches directly inherited from annotation method parameters set
LEAF_EXTENDS
means that it matches terminal inheritance (that is, no subclasses) annotation method parameters set
💡💡💡If the parameter is set to Object or Any, this setting will be ignored, but
regex
must be filled in
Regular expression
¶After setting the regular expression, the class name that meets the requirements will be found according to the regular expression you set
After setting the regular expression, the parameter of the annotation method can be Object or Any, as shown in the example below
If you do not set the regular expression, you must specify the type
It is extremely simple to use, and the sample code has already explained it
object InitCollect {
+ private val collects = mutableListOf<SubApplication>()
+ private val collectClazz: MutableList<Class<out SubApplication>> = mutableListOf()
+
+ @AndroidAopCollectMethod
+ @JvmStatic
+//Collect classes inherited from SubApplication and call back its instance object
+ fun collect(sub: SubApplication) {
+ collects.add(sub)
+ }
+
+ @AndroidAopCollectMethod
+ @JvmStatic
+//Collect classes inherited from SubApplication and call back its class object
+ fun collect2(sub: Class<out SubApplication>) {
+ collectClazz.add(sub)
+ }
+
+ @AndroidAopCollectMethod(regex = ".*?\\$\\\$Router")
+ @JvmStatic
+//Collect classes that match the regex regular expression and call back their class objects. Can also be used in conjunction with inheritance
+ fun collectRouterClassRegex(sub: Class<out Any>) {
+ Log.e("InitCollect", "----collectRouterClassRegexClazz----$sub")
+ }
+
+ @AndroidAopCollectMethod(regex = ".*?\\$\\\$Router")
+ @JvmStatic
+//Collect classes that match the regex regular expression and call back their instance objects. Can also be used in combination with inheritance
+ fun collectRouterClassRegex(sub: Any) {
+ Log.e("InitCollect", "----collectRouterClassRegexObject----$sub")
+ }
+
+ //Directly call this method (method name is not limited) The above functions will be called back
+ fun init(application: Application) {
+ for (collect in collects) {
+ collect.onCreate(application)
+ }
+ }
+}
+
public class InitCollect2 {
+ private static final List<SubApplication2> collects = new ArrayList<>();
+ private static final List<Class<? extends SubApplication2>> collectClazz = new ArrayList<>();
+
+ @AndroidAopCollectMethod
+ public static void collect(SubApplication2 sub) {
+ collects.add(sub);
+ }
+
+ @AndroidAopCollectMethod
+ public static void collect3(Class<? extends SubApplication2> sub) {
+ collectClazz.add(sub);
+ }
+
+ @AndroidAopCollectMethod(regex = ".*?\\$\\$Router")
+ public static void collectRouterClassRegex(Object sub) {
+ Log.e("InitCollect2", "----collectRouterClassRegexObject----" + sub);
+ }
+
+ @AndroidAopCollectMethod(regex = ".*?\\$\\$Router")
+ public static void collectRouterClassRegex(Class<?> sub) {
+ Log.e("InitCollect2", "----collectRouterClassRegexClazz----" + sub);
+ }
+
+ //Directly call this method (method name is not limited) The above functions will be called back in full
+ public static void init(Application application) {
+ Log.e("InitCollect2", "----init----");
+ for (SubApplication2 collect : collects) {
+ collect.onCreate(application);
+ }
+ }
+}
+
Use this collection class
public class MyApp extends Application {
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ InitCollect.init(this);
+ }
+}
+
Multiple modules need to use Application, such as the above example
For those who are interested in implementing Router by themselves, this aspect can help you find what you want
Adapting routing library such as this ARouter adapts AGP8 Example
Another new way to use singletons that are both thread-safe and lazy-loaded!
public class TestInstance {
+ private static TestInstance instance;
+
+ @AndroidAopCollectMethod(regex = "^com.flyjingfish.lightrouter.TestInstance$")
+ public static void collectInstance(Object any) {
+ instance = (TestInstance) any;
+ }
+
+ public static TestInstance getInstance() {
+ return instance;
+ }
+
+ public void test() {
+ Log.e("TestInstance", "=====test=");
+ }
+}
+
This aspect is used to match a class and its corresponding method. This aspect focuses on the execution of the method (Method Execution). Please note the difference between it and @AndroidAopReplaceClass.
@AndroidAopMatchClassMethod(
+ targetClassName = target class name(including package name),
+ methodName = method name array,
+ type = match type, optional, default EXTENDS
+ excludeClasses = array of classes to exclude in inheritance relationship (valid only when type is not SELF), optional
+)
+
When targetClassName is an inner class, do not use the $
character, but use .
There are four types of type (default EXTENDS
is not set):
SELF
means that the match is itself of the class set by targetClassNameEXTENDS
means that the match is all classes inherited from the class set by targetClassNameDIRECT_EXTENDS
means that the match is directly inherited from the class set by targetClassNameLEAF_EXTENDS
means that the match is Terminal inheritance (no subclasses) The class set by targetClassNameSimply put, LEAF_EXTENDS
and DIRECT_EXTENDS
are two extremes. The former focuses on the last node in the inheritance relationship, while the latter focuses on the first node in the inheritance relationship. Also note that EXTENDS
This type of match has a wide range, and all inherited intermediate classes may also add aspect codes
excludeClasses
overrideMethod defaults to false
Set to true, if there is no matching method in the subclass (non-interface, can be an abstract class), the parent class method is overwritten
Set to false, if there is no matching method in the subclass, no processing is done
Note
In addition, not all classes can be hooked in
type
Type is SELF
When targetClassName
is set, the class must be the code in the installation package. For example: if this class (such as Toast) is in android.jar, it will not work. If you have such a requirement, you should use @AndroidAopReplaceClasstype
When the type is not SELF
, this aspect needs to have a matching method to work. If the subclass does not override the matching method, the subclass will not be matched. Use overrideMethod to ignore this restrictionThe aspect processing class needs to implement the MatchClassMethod interface and handle the aspect logic in invoke
interface MatchClassMethod {
+ fun invoke(joinPoint: ProceedJoinPoint, methodName: String): Any?
+}
+
Note
If the point function is suspend Click here to view
You can see that in the following examples, some of the method names are set with only the method name, while others also have the return value type and parameter type. The following is an introduction
.*
and has other characters, and type = MatchType.SELF
, it matches all classes under the package, including subpackages as shown in Example 9 below*
, it matches all methods in the class as shown in Example 8 belowNote
For matching package names, I strongly recommend not doing this, as it intrudes too much code and significantly reduces the packaging speed. It is recommended to use it only for debugging and logging, and the original method should be released in full
Write the return type and parameter type on the methodName method name, so that you can accurately find a method to add a cut point (it will automatically degenerate to fuzzy matching when the precise matching is abnormal)
Matching formula: Return value type method name (parameter type, parameter type...)
suspend
, then the return value type is written regardless of the type suspend
, parameter types should still be written according to the above pointsFor generic information (such as collection List), the generic information must be erased
Different from targetClassName, if the method parameter and return value type are inner classes, they need to be replaced with $
instead of .
Note
AOP Code Generation Assistant, which can help you generate code with one click
Below is a comparison table of Kotlin and Java with different types. If it is Kotlin code, please check the corresponding code
(If you find any incomplete information, please give me feedback)
Kotlin type | Java type |
---|---|
Int | int |
Short | short |
Byte | byte |
Char | char |
Long | long |
Float | float |
Double | double |
Boolean | boolean |
Int? | java.lang.Integer |
Short? | java.lang.Short |
Byte? | java.lang.Byte |
Char? | java.lang.Character |
Long? | java.lang.Long |
Float? | java.lang.Float |
Double? | java.lang.Double |
Boolean? | java.lang.Boolean |
String | java.lang.String |
Unit(Or do not write) | void |
Unit? | java.lang.Void |
Any | java.lang.Object |
Other data types not listed above are reference types, and are written as package name.class name
Note
vararg str : String
in Kotlin is equivalent to String...
in Java. In this matching, no matter what kind of code is used, it is represented by String[]
(String is used as an example here, and other types are the same)java.lang.List<String> methodName(java.lang.List<String>)
should be directly written as java.lang.List methodName(java.lang.List)
Want to monitor all startActivity jumps inherited from the AppCompatActivity class
@AndroidAopMatchClassMethod(
+ targetClassName = "androidx.appcompat.app.AppCompatActivity",
+ methodName = {"startActivity"},
+ type = MatchType.EXTENDS
+)
+public class MatchActivityMethod implements MatchClassMethod {
+ @Nullable
+ @Override
+ public Object invoke(@NonNull ProceedJoinPoint joinPoint, @NonNull String methodName) {
+// Write your logic here
+ return joinPoint.proceed();
+ }
+}
+
⚠️Note: For matching subclass methods, if the subclass does not override the matching method, it is invalid. Use overrideMethod to ignore this limitation
If you want to hook all android.view.View.OnClickListener onClick, to put it simply, is to globally monitor all click events set onClickListener, the code is as follows:
@AndroidAopMatchClassMethod(
+ targetClassName = "android.view.View.OnClickListener",
+ methodName = ["onClick"],
+ type = MatchType.EXTENDS //type must be EXTENDS because you want to hook all classes that inherit OnClickListener
+)
+class MatchOnClick : MatchClassMethod {
+ // @SingleClick(5000) //Combined with @SingleClick, add multi-click protection to all clicks, 6 or not
+ override fun invoke(joinPoint: ProceedJoinPoint, methodName: String): Any? {
+ Log.e("MatchOnClick", "=====invoke=====$methodName")
+ return joinPoint.proceed()
+ }
+}
+
Here is a reminder for those who use lambda click monitoring;
ProceedJoinPoint The target is not android.view.View.OnClickListener - For Java, the target is the object of the class that sets the lambda expression - For Kotlin, the target is null
The methodName of the invoke callback is not onClick, but the method name automatically generated at compile time, similar to onCreate\(lambda\)14, which contains the lambda keyword
For the view of onClick(view:View) - If it is Kotlin code, ProceedJoinPoint.args[1] - If it is Java code, ProceedJoinPoint.args[0]
I will not go into details about this, you will know it after using it yourself;
To summarize: In fact, for all lambda's ProceedJoinPoint.args
The target class has multiple methods with the same name, and you only want to match one method (the exact matching rule is mentioned above)
package com.flyjingfish.test_lib;
+
+public class TestMatch {
+ public void test(int value1) {
+
+ }
+
+ public String test(int value1, String value2) {
+ return value1 + value2;
+ }
+}
+
package com.flyjingfish.test_lib.mycut;
+
+@AndroidAopMatchClassMethod(
+ targetClassName = "com.flyjingfish.test_lib.TestMatch",
+ methodName = ["java.lang.String test(int,java.lang.String)"],
+ type = MatchType.SELF
+)
+class MatchTestMatchMethod : MatchClassMethod {
+ override fun invoke(joinPoint: ProceedJoinPoint, methodName: String): Any? {
+ Log.e("MatchTestMatchMethod",
+ "======" + methodName + ",getParameterTypes=" + joinPoint.getTargetMethod()
+ .getParameterTypes().length
+ );
+// Write your logic here
+// If you don't want to execute the original method logic, 👇 don't call the following sentence
+ return joinPoint.proceed()
+ }
+}
+
When there are many levels of inheritance relationships, you don't want to add aspects to each level
@AndroidAopMatchClassMethod(
+ targetClassName = "android.view.View.OnClickListener",
+ methodName = ["onClick"],
+ type = MatchType.EXTENDS // type must be EXTENDS because you want to hook all classes that inherit OnClickListener
+)
+class MatchOnClick : MatchClassMethod {
+ // @SingleClick(5000) //Join @SingleClick to add anti-multiple clicks to all clicks, 6 or 6
+ override fun invoke(joinPoint: ProceedJoinPoint, methodName: String): Any? {
+ Log.e("MatchOnClick", "=====invoke=====$methodName")
+ return joinPoint.proceed()
+ }
+}
+
public abstract class MyOnClickListener implements View.OnClickListener {
+ @Override
+ public void onClick(View v) {
+ ...
+ //This is the necessary logic code
+ }
+}
+
binding.btnSingleClick.setOnClickListener(object : MyOnClickListener() {
+ override fun onClick(v: View?) {
+ super.onClick(v)//Especially this sentence calls the parent class onClick and wants to retain the logic of executing the parent class method
+ onSingleClick()
+ }
+})
+
Writing this way will cause the MyOnClickListener onClick above to also be added to the aspect, which is equivalent to a click that calls back twice the invoke of the aspect processing class, which may not be what we want, so we can change it like this
@AndroidAopMatchClassMethod(
+ targetClassName = "android.view.View.OnClickListener",
+ methodName = ["onClick"],
+ type = MatchType.EXTENDS,
+ excludeClasses = ["com.flyjingfish.androidaop.test.MyOnClickListener"]//Adding this can exclude some classes
+)
+class MatchOnClick : MatchClassMethod {
+ override fun invoke(joinPoint: ProceedJoinPoint, methodName: String): Any? {
+ Log.e("MatchOnClick", "=====invoke=====$methodName")
+ return joinPoint.proceed()
+ }
+}
+
What if the entry point is a companion object?
Suppose there is such a code
package com.flyjingfish.androidaop
+
+class ThirdActivity : BaseActivity() {
+ companion object {
+ fun start() {
+ ...
+ }
+ }
+}
+
@AndroidAopMatchClassMethod(
+ targetClassName = "com.flyjingfish.androidaop.ThirdActivity.Companion",
+ methodName = ["start"],
+ type = MatchType.SELF
+)
+class MatchCompanionStart : MatchClassMethod {
+ override fun invoke(joinPoint: ProceedJoinPoint, methodName: String): Any? {
+ Log.e("MatchCompanionStart", "======$methodName")
+ return joinPoint.proceed()
+ }
+}
+
The entry point is Kotlin Member variables of the code, want to monitor the assignment and retrieval operations
In the code, we will have such operations
You can write like this
@AndroidAopMatchClassMethod(
+ targetClassName = "com.flyjingfish.androidaop.test.TestBean",
+ methodName = ["setName", "getName"],
+ type = MatchType.SELF
+)
+class MatchTestBean : MatchClassMethod {
+ override fun invoke(joinPoint: ProceedJoinPoint, methodName: String): Any? {
+ Log.e("MatchTestBean", "======$methodName");
+ ToastUtils.makeText(ToastUtils.app, "MatchTestBean======$methodName")
+ return joinPoint.proceed()
+ }
+}
+
If the cut point method is suspend
What about modified functions?
You can directly use Fuzzy Matching
If you want to use Exact Matching, the writing is as follows. For specific rules, see Exact Matching
package com.flyjingfish.androidaop
+
+class MainActivity : BaseActivity2() {
+ suspend fun getData(num: Int): Int {
+ return withContext(Dispatchers.IO) {
+ getDelayResult()
+ }
+ }
+}
+
The exact match is written as follows. Regardless of the return value type of the matching function, write suspend
. For details, see the Exact Matching Part
@AndroidAopMatchClassMethod(
+ targetClassName = "com.flyjingfish.androidaop.MainActivity",
+ methodName = ["suspend getData(int)"],
+ type = MatchType.SELF
+)
+class MatchSuspend : MatchClassMethod {
+ override fun invoke(joinPoint: ProceedJoinPoint, methodName: String): Any? {
+ Log.e("MatchSuspend", "======$methodName") return joinPoint.proceed()
+ }
+}
+
Want to match all methods of a class
@AndroidAopMatchClassMethod(
+targetClassName = "com.flyjingfish.androidaop.SecondActivity",
+methodName = ["*"],
+type = MatchType.SELF
+)
+class MatchAllMethod : MatchClassMethod {
+override fun invoke(joinPoint: ProceedJoinPoint, methodName: String): Any? {
+Log.e("MatchMainAllMethod", "AllMethod======$methodName");
+return joinPoint.proceed()
+}
+}
+
Want to match all methods of all classes in a package
@AndroidAopMatchClassMethod(
+ targetClassName = "com.flyjingfish.androidaop.*",
+ methodName = ["*"],
+ type = MatchType.SELF
+)
+class MatchAll : MatchClassMethod {
+ override fun invoke(joinPoint: ProceedJoinPoint, methodName: String): Any? {
+ Log.e(
+ "MatchAll",
+ "---->${joinPoint.targetClass}--${joinPoint.targetMethod.name}--${joinPoint.targetMethod.parameterTypes.toList()}"
+ );
+ return joinPoint.proceed()
+ }
+}
+
*
replaces class name
Or replace part of the package name + class name
, this example represents all classes under the com.flyjingfish.androidaop
package and its subpackages @AndroidAopModifyExtendsClass(value)
This function is relatively simple. It modifies the inherited class of a class. Fill in the full name of the class to be modified in the value
position. The annotated class is the modified inherited class.
In addition, if the class name is an internal class, do not use the $
character, but .
⚠️⚠️⚠️But it should be noted that the modified inherited class cannot inherit the modified class. The inherited class of the modified class is generally set to the inherited class of the class before modification
As shown in the following example, the inherited class of AppCompatImageView
is replaced with ReplaceImageView
Application scenario: non-invasively implement the function of monitoring large image loading
@AndroidAopModifyExtendsClass("androidx.appcompat.widget.AppCompatImageView")
+public class ReplaceImageView extends ImageView {
+ public ReplaceImageView(@NonNull Context context) {
+ super(context);
+ }
+
+ public ReplaceImageView(@NonNull Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public ReplaceImageView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ @Override
+ public void setImageDrawable(@Nullable Drawable drawable) {
+ super.setImageDrawable(drawable);
+//Do some monitoring or modify again
+ }
+}
+
@AndroidAopPointCut is a method-based annotation. This annotation method focuses on the execution of the method (Method Execution). The built-in annotations in this library are all made in this way
In addition, please try not to place the annotation on the system method, such as: Activity's onCreate() onResume(), etc. Even if it is added, there should be no time-consuming operations during the section processing. JoinPoint.proceed() should be executed normally, otherwise unexpected problems will occur, such as: ANR
Create an annotation named CustomIntercept and add @AndroidAopPointCut to your annotation
@AndroidAopPointCut(CustomInterceptCut::class)
+@Target(
+ AnnotationTarget.FUNCTION,
+ AnnotationTarget.PROPERTY_GETTER,
+ AnnotationTarget.PROPERTY_SETTER
+)
+@Retention(
+ AnnotationRetention.RUNTIME
+)
+annotation class CustomIntercept(vararg val value: String = [])
+
@AndroidAopPointCut's CustomInterceptCut.class is the class that handles the section for you (described below)
@Target only works on methods, and setting other ones has no effect
ElementType.METHOD
For Kotlin, you can set AnnotationTarget.FUNCTION
,AnnotationTarget.PROPERTY_GETTER
,AnnotationTarget.PROPERTY_SETTER
@Retention can only use RetentionPolicy.RUNTIME
The section processing class needs to implement the BasePointCut interface and handle the section logic in invoke
interface BasePointCut<T : Annotation> {
+ fun invoke(joinPoint: ProceedJoinPoint, anno: T): Any?
+}
+
Note
If the point cutting function is suspend Click here to view
CustomInterceptCut inherits from BasePointCut. You can see that there is a generic on BasePointCut. This generic is the @CustomIntercept annotation above. The two are related to each other.
class CustomInterceptCut : BasePointCut<CustomIntercept> {
+ override fun invoke(
+ joinPoint: ProceedJoinPoint,
+ annotation: CustomIntercept//annotation is the annotation you added to the method
+ ): Any? {
+ // Write your logic here
+ return joinPoint.proceed()
+ }
+}
+
Add the annotation you wrote directly to any method, such as onCustomIntercept(). When onCustomIntercept() is called, it will first enter the invoke method of CustomInterceptCut mentioned above.
This function belongs to the advanced function, special attention should be paid when using it, otherwise it will be invalid
This aspect is used to replace the method call in the code, and needs to be used in conjunction with @AndroidAopReplaceMethod. When the replaced method is called, it will enter the method annotated with @AndroidAopReplaceMethod
Note
⚠️⚠️⚠️ Note: The defined replacement class should be placed within the scan rules you set here include exclude Rules, it will not work if written outside the scope
@AndroidAopReplaceClass(
+ value = "Full name of the class (including package name)"
+ type = Matching type, not required, default SELF
+ excludeClasses = An array of classes that exclude inheritance(valid only when type is not SELF), not required
+)
+
$
character, but .
The annotated class is the replacement class; the parameter is the class to be replaced
If the corresponding replaced method exists in the replacement method of the annotated class, it will not participate in the method replacement
There are four types of type (if the default SELF
is not set, please note the difference from @AndroidAopMatchClassMethod
, the default types are different when they are not set):
SELF
means that the class set by value is matched itselfEXTENDS
means that all classes inherited from the class set by value are matchedDIRECT_EXTENDS
means that the class set by directly inherited is matchedLEAF_EXTENDS
means matching the class set by terminal inheritance (no subclasses) valueIn simple terms, LEAF_EXTENDS
and DIRECT_EXTENDS
are two extremes. The former focuses on the last node in the inheritance relationship, while the latter focuses on the first node in the inheritance relationship.
Warning
Also note that when type is not [SELF], it will slow down the packaging speed. Please use it as appropriate and try not to use it if possible.
For example, this method changes new Thread()
to new MyThread()
The annotated method must be public and static, but the method name can be defined arbitrarily
There can only be one method parameter, and the parameter is the replaced class
Is the method return type empty?
The annotated method must be public and static, but the method name can be defined arbitrarily
The annotated method is the replacement method; the parameter is the method to be replaced, which must include the return type and parameter type. The matching rules are as follows matching rules
If the replaced method is a static method of the class, the parameter type, order and number of the replacement method you define should be consistent
If the replaced method is a member method of a class, the first parameter of the replacement method you define must be the type of the replaced class (this is the meaning of the Toast.show example below), and the remaining parameter types, order, and number are consistent with the replaced method.
The return type of the annotated method is consistent with the replaced method, regardless of whether the replaced method is static or not
The replaced method must belong to the replaced class filled in by @AndroidAopReplaceClass
If the replaced method starts with <init>
, the function is similar to @AndroidAopReplaceNew, except that it will only call back the new class, will not change the new class name, and can specify the construction method.
For specific writing requirements, please refer to the usage method below
You can see that the return value type and parameter type are written in the example below. The following is an introduction
The difference from @AndroidAopMatchClassMethod is that this must be an exact match, and the writing is as follows:
Matching writing formula: Return value type method name (parameter type, parameter type...)
suspend
, then the return value type should be written as suspend
regardless of the type, and the parameter type should still be written according to the above pointsFor generic information (such as the collection List), the generic information must be erased
Different from the replacement class name filled in, if the method parameter and return value type are inner classes, they need to be replaced with $
instead of .
Note
AOP Code Generation Assistant, can help you generate code with one click
Below is a table showing different types of Kotlin vs. Java. If it is Kotlin code, please check the corresponding code
(If you find any incomplete information, please give me feedback)
Kotlin type | Java type |
---|---|
Int | int |
Short | short |
Byte | byte |
Char | char |
Long | long |
Float | float |
Double | double |
Boolean | boolean |
Int? | java.lang.Integer |
Short? | java.lang.Short |
Byte? | java.lang.Byte |
Char? | java.lang.Character |
Long? | java.lang.Long |
Float? | java.lang.Float |
Double? | java.lang.Double |
Boolean? | java.lang.Boolean |
String | java.lang.String |
Unit(Or do not write) | void |
Unit? | java.lang.Void |
Any | java.lang.Object |
Other data types not in the table above are reference types, and are written as package name.class name
Note
vararg str : String
in Kotlin is equivalent to String...
in Java. In this matching, no matter what kind of code is used, it is represented by String[]
(String is used as an example here, and other types are the same)java.lang.List<String> methodName(java.lang.List<String>)
should be directly written as java.lang.List methodName(java.lang.List)
@AndroidAopReplaceClass(
+ "android.widget.Toast"
+)
+public class ReplaceToast {
+ @AndroidAopReplaceMethod(
+ "android.widget.Toast makeText(android.content.Context, java.lang.CharSequence, int)"
+ )
+// Because the replaced method is static, the parameter type and order correspond to the replaced method
+ public static Toast makeText(Context context, CharSequence text, int duration) {
+ return Toast.makeText(context, "ReplaceToast-" + text, duration);
+ }
+
+ @AndroidAopReplaceMethod(
+ "void setGravity(int , int , int )"
+ )
+// Because the replaced method is not a static method, the first parameter is the replaced class, and the subsequent parameters correspond to the replaced method
+ public static void setGravity(Toast toast, int gravity, int xOffset, int yOffset) {
+ toast.setGravity(Gravity.CENTER, xOffset, yOffset);
+ }
+
+ @AndroidAopReplaceMethod(
+ "void show()"
+ )
+// Although the replaced method has no parameters, because it is not a static method, the first parameter is still the replaced class
+ public static void show(Toast toast) {
+ toast.show();
+ }
+}
+
This example means that all places where Toast.makeText
and Toast.show
in the code are replaced with ReplaceToast.makeText
and ReplaceToast.show
@AndroidAopReplaceClass("android.util.Log")
+object ReplaceLog {
+ @AndroidAopReplaceMethod("int e(java.lang.String,java.lang.String)")
+ @JvmStatic
+ fun e(tag: String, msg: String): Int {
+ return Log.e(tag, "ReplaceLog-$msg")
+ }
+}
+
This example means that all places where Log.e
are written in the code are replaced with ReplaceLog.e
suspend
, then you can only write it in Kotlin code, and the replacement function must also be modified with suspend
¶@AndroidAopReplaceClass("com.flyjingfish.androidaop.MainActivity")
+object ReplaceGetData {
+ //The only change in the annotation parameter is the return type, which is changed to suspend, and the rest remain unchanged
+ @AndroidAopReplaceMethod("suspend getData(int)")
+ @JvmStatic
+// The function definition writing rules here remain unchanged, just add an additional suspend modifier
+ suspend fun getData(mainActivity: MainActivity, num: Int): Int {
+ Log.e("ReplaceGetData", "getData")
+ return mainActivity.getData(num + 1)
+ }
+}
+
@AndroidAopReplaceClass(value = "com.flyjingfish.test_lib.TestMatch", type = MatchType.EXTENDS)
+object ReplaceTestMatch {
+
+ @AndroidAopReplaceNew
+ @JvmStatic
+ fun newTestMatch1(testBean: TestMatch3) {
+//Replace the class name after new, the parameter type is the replaced type, the return type of this method is empty, and this method will not be called back
+ }
+
+ @AndroidAopReplaceNew
+ @JvmStatic
+ fun newTestMatch2(testBean: TestMatch3): TestMatch {
+//Replace the class name after new, the parameter type is the replaced type, and the return type of this method is not empty, then this method will be called back, and the returned object will replace the new object
+ return new TestMatch ()
+ }
+
+ @AndroidAopReplaceMethod("<init>(int)")
+ @JvmStatic
+ fun getTestBean(testBean: TestMatch): TestMatch {
+//Only one parameter can be the replaced class, the return type cannot be empty, and the object returned by the method will replace the newly created object
+ return TestMatch(2)
+ }
+
+}
+
The above three usage methods can replace the new object. The difference is
The first method directly replaces the new class name (directly replaces the type)
The second method not only replaces the new class name, but also calls back to the method. The object returned here will also replace the newly created object (the difference between the two is whether the return type is empty)
The third method is different from the first two in that it does not replace the new class name, but calls back to the method. The object returned here will replace the newly created object. And the defined parameter must be one and only one type defined by @AndroidAopReplaceClass, and the return type cannot be null
The function defined by @AndroidAopReplaceNew has one and only one parameter, and the parameter type can be any type except the basic type
@AndroidAopReplaceClass
to replace the method call, and use @ProxyMethod
to add annotations to the replacement methodThe replaced method needs to be called in the method implementation
package com.flyjingfish.test_lib.replace;
+
+@AndroidAopReplaceClass(
+ "android.widget.Toast"
+)
+public class ReplaceToast {
+ @AndroidAopReplaceMethod(
+ "android.widget.Toast makeText(android.content.Context, java.lang.CharSequence, int)"
+ )
+ @ProxyMethod(proxyClass = Toast.class, type = ProxyType.STATIC_METHOD)
+ public static Toast makeText(Context context, CharSequence text, int duration) {
+ return Toast.makeText(context, text, duration);
+ }
+
+ @AndroidAopReplaceMethod(
+ "void setGravity(int , int , int )"
+ )
+ @ProxyMethod(proxyClass = Toast.class, type = ProxyType.METHOD)
+ public static void setGravity(Toast toast, int gravity, int xOffset, int yOffset) {
+ toast.setGravity(gravity, xOffset, yOffset);
+ }
+
+ @AndroidAopReplaceMethod(
+ "void show()"
+ )
+ @ProxyMethod(proxyClass = Toast.class, type = ProxyType.METHOD)
+ public static void show(Toast toast) {
+ toast.show();
+ }
+}
+
@AndroidAopMatchClassMethod
to define the ReplaceToast
proxy class@AndroidAopMatchClassMethod(
+ targetClassName = "com.flyjingfish.test_lib.replace.ReplaceToast",
+ type = MatchType.SELF,
+ methodName = ["*"]
+)
+class ReplaceToastProxy : MatchClassMethodProxy() {
+ override fun invokeProxy(joinPoint: ProceedJoinPoint, methodName: String): Any? {
+ Log.e(
+ "ReplaceToastProxy",
+ "methodName=$methodName," + "parameterNames=${joinPoint.targetMethod.parameterNames.toList()}," + "parameterTypes=${joinPoint.targetMethod.parameterTypes.toList()}," + "returnType=${joinPoint.targetMethod.returnType}," + "args=${joinPoint.args?.toList()},target=${joinPoint.target},targetClass=${joinPoint.targetClass},"
+ )
+
+ return joinPoint.proceed()
+ }
+}
+
So you can For some system methods, ProceedJoinPoint
is used to control the method call. The key is to use @ProxyMethod
to mark the method, so that the information returned by ProceedJoinPoint
is the method information of the replaced class>
Note
AOP code generation assistant, which can help you generate proxy usage code with one click
Migrating from AspectJ is also very simple
Take click annotation as an example, you may have such a code
Click annotation
@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+public @interface SingleClick {
+
+ long DEFAULT_INTERVAL_MILLIS = 1000;
+
+ /**
+ * @return The interval between quick clicks (ms), the default is 1000ms
+ */
+ long value() default DEFAULT_INTERVAL_MILLIS;
+}
+
Click annotation aspect
@Aspect
+public final class SingleClick$$AspectJ {
+ @Pointcut("within(@com.flyjingfish.light_aop_core.annotations.SingleClick *)")
+ public final void withinAnnotatedClass() {
+ }
+
+ @Pointcut("execution(!synthetic * *(..)) && withinAnnotatedClass()")
+ public final void methodInsideAnnotatedType() {
+ }
+
+ @Pointcut("execution(@com.flyjingfish.light_aop_core.annotations.SingleClick * *(..)) || methodInsideAnnotatedType()")
+ public final void method() {
+ }
+
+ @Around("method() && @annotation(vSingleClick)")
+ public final Object cutExecute(final ProceedingJoinPoint joinPoint,
+ final SingleClick vSingleClick) {
+// Section processing logic
+ return result;
+ }
+}
+
First create a class to handle sections
class SingleClickCut : ClickCut<SingleClick>() {
+ //Fill in your original annotations for this pattern
+ override fun invoke(joinPoint: ProceedJoinPoint, anno: SingleClick): Any? {
+//Copy the logic code here and make some changes
+ return null
+ }
+
+}
+
Then add the @AndroidAopPointCut(SingleClickCut.class) annotation on top of your original annotation. The annotation @Retention
can only set RUNTIME
, and @Target
can only set METHOD
//Just add such an annotation. The parameter is the section processing class SingleClickCut.class created above
+@AndroidAopPointCut(SingleClickCut.class)
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+public @interface SingleClick {
+
+ long DEFAULT_INTERVAL_MILLIS = 1000;
+
+ /**
+ * @return The interval between quick clicks (ms), the default is 1000ms
+ */
+ long value() default DEFAULT_INTERVAL_MILLIS;
+}
+
@AndroidAopPointCut(SingleClickCut::class)
+@Retention(AnnotationRetention.RUNTIME)
+@Target(
+ AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER
+)
+annotation class SingleClick(
+ /**
+ * The interval between quick clicks (ms), the default is 1000ms
+ */
+ val value: Long = DEFAULT_INTERVAL_MILLIS
+) {
+ companion object {
+ const val DEFAULT_INTERVAL_MILLIS: Long = 1000
+ }
+}
+
@AndroidAopMatchClassMethod is similar to the execution matching type in AspectJ, focusing on the execution of methods
AndroidAOP currently only matches methods of a class, not just a method regardless of the class. Because the author thinks that doing so is almost meaningless, and doing so often leads to the addition of many classes that do not want to be added to the aspect, which is not conducive to everyone's management and control of their own code (a bit out of control~)
For example, you originally set the aspect code for threadTest
of MainActivity
, as shown below:
package com.flyjingfish.test
+
+class MainActivity : BaseActivity() {
+ fun threadTest() {
+ Log.e("threadTest", "------")
+ }
+}
+
The matching aspect code of AspectJ is as follows:
@Aspect
+public class CheckAspectJ {
+ private static final String TAG = "CheckAspectJ";
+
+ @Pointcut("execution(* com.flyjingfish.test.MainActivity.threadTest())")
+ public void pointcutThreadTest() {
+ }
+
+ @Around("pointcutThreadTest()")
+ public void calculateFunctionTime(ProceedingJoinPoint joinPoint) throws Throwable {
+ Log.i(TAG, "pointcut1 ---------calculateFunctionTime---------@Around");
+ long beginTime = System.currentTimeMillis();
+ joinPoint.proceed();
+ long endTime = System.currentTimeMillis();
+ Log.i(TAG, "pointcut1 ----------calculateFunctionTime-- -----Running time: " + (endTime - beginTime));
+ }
+}
+
@AndroidAopMatchClassMethod(
+ targetClassName = "com.flyjingfish.test.MainActivity",
+ methodName = ["threadTest"],
+ type = MatchType.SELF
+)
+class MatchActivityMethod : MatchClassMethod {
+ override fun invoke(joinPoint: ProceedJoinPoint, methodName: String): Any? {
+ Log.e("MatchActivityMethod", "=====invoke=====$methodName")
+ long beginTime = System . currentTimeMillis ();
+ joinPoint.proceed();
+ long endTime = System . currentTimeMillis ();
+ return null
+ }
+}
+
@AndroidAopReplaceClass is similar to the call matching type in AspectJ, focusing on the method call
Those who have used AspectJ should know that some system methods can only be matched through call. For example, you originally targeted e
of android.util.Log
sets the aspect code, and the matching aspect code of AspectJ is as follows:
@Aspect
+public final class TestAspectJ {
+ @Pointcut("call(* android.util.Log.e(..))")
+ public void pointcutThreadTest() {
+ }
+
+ @Around("pointcutThreadTest()")
+ public final Object cutExecute(final JoinPoint joinPoint) throws Throwable {
+ Log.e("TestAspectJ", "====cutExecute");
+ return null;
+ }
+}
+
Click here to see detailed usage of @AndroidAopReplaceClass
@AndroidAopReplaceClass("android.util.Log")
+object ReplaceLog {
+ @AndroidAopReplaceMethod("int e(java.lang.String tag, java.lang.String msg)")
+ @JvmStatic
+ fun logE(String tag, String msg): Int {
+ return Log.e(tag, msg)
+ }
+}
+
Note
Unlike AspectJ, AndroidAOP does not retain the way to execute the original method, but you can call the original method yourself without causing infinite recursive calls (indirect calls to the original method will cause infinite recursion here is a solution), Click here for detailed usage
All of the above can be indirectly implemented through several existing annotation aspects click here to refer to FAQ #5 Want to insert code before and after the method
plugins {
+...
+}
+androidAopConfig {
+ //Closed by default, enabled after build or packaging, a cut information json file will be generated in app/build/tmp/cutInfo.json
+ cutInfoJson true
+}
+android {
+...
+}
+
@AndroidAopMatchClassMethod(
+ targetClassName = "com.flyjingfish.test_lib.TestMatch",
+ methodName = ["test2"],
+ type = MatchType.SELF
+)
+class MatchTestMatchMethod : MatchClassMethod {
+ override fun invoke(joinPoint: ProceedJoinPoint, methodName: String): Any? {
+ //Insert code before the method
+ val value = joinPoint.proceed()
+ //Insert code after the method
+ return value
+ }
+}
+
class CustomInterceptCut : BasePointCut<CustomIntercept> {
+ override fun invoke(
+ joinPoint: ProceedJoinPoint,
+ annotation: CustomIntercept //annotation is the annotation you add to the method
+ ): Any? {
+ //Insert code before the method
+ val value = joinPoint.proceed()
+ //Insert code after the method
+ return value
+ }
+}
+
@AndroidAopReplaceClass("android.util.Log")
+object ReplaceLog {
+ @AndroidAopReplaceMethod("int e(java.lang.String,java.lang.String)")
+ @JvmStatic
+ fun e(tag: String, msg: String): Int {
+ //Insert code before the method
+ val log = Log.e(tag, "ReplaceLog-$msg")
+ //Insert code after the method
+ return log
+ }
+}
+
AspectJ
's @AfterReturning
and @AfterThrowing
We will match the aspect Let's take an example@AndroidAopMatchClassMethod(
+ targetClassName = "com.flyjingfish.test_lib.TestMatch",
+ methodName = ["test2"],
+ type = MatchType.SELF
+)
+class MatchTestMatchMethod : MatchClassMethod {
+ override fun invoke(joinPoint: ProceedJoinPoint, anno: TryCatch): Any? {
+ return try {
+ val value = joinPoint.proceed()
+ // Here is @AfterReturning
+ value
+ } catch (e: Throwable) {
+ // Here is @AfterThrowing
+ throw RuntimeException(e)
+ }
+ }
+}
+
The aspect processing class is bound to the corresponding method of the class, which can be divided into two cases
No matter which type a or b is, the aspect processing class object will only be created when the method is executed.
In short, the object of the aspect processing class is bound to the class or the object of the class, and its life cycle is slightly longer than the object of the class of the pointcut non-static method.
This is different from Aspectj, because we often want to set some member variables in the aspect processing class to facilitate the use of the next aspect processing; if you want to do this in Aspectj, you need to save the "member variable" as a "static variable", and you also need to distinguish what object executes the pointcut method. You need to write a lot of code. AndroidAOP just optimizes and solves this problem.
Gradle
to 8.7 or above
to see if it can be solvedid 'android.aop'
is in the last line of build.gradle
of the app moduleFirst, make sure that running ./gradlew --stop
directly can succeed. If it fails, please check it online and then proceed to the following steps
Click on the run configuration
Add the Run External tool
task type based on the original one
Configure as follows
Parameters:
Program:The absolute path of the project\gradlew.bat
Arguments:./gradlew --stop
Working directory:The absolute path of the project\
Adjust the order to the top
Click OK to complete
Run the project directly. If the following situation occurs, it means that the configuration is successful
In addition, some netizens mentioned that changing ksp
to kapt
can also solve the problem
The jar package you added contains the following files, please delete them and import the jar package locally instead
Operation steps
Open the directory where the jar package is locatedcd /Users/a111/Downloads/ida-android-new/app/libs
Unzip the jar package jar -xvf bcprov-jdk15on-1.69.jar
jar -cfm0 bcprov-jdk15on-1.69.jar META-INF/MANIFEST.MF org
androidAop.debugMode = true
?¶The main reason for this is that you may have used some Router
libraries or other plugins that change the packaging method. You can refer to here to transform your project click here, here is how to remove the plugin part of these libraries and use AndroidAOP to complete its plugin work, so you can delete these plugins to speed up packaging
vararg str : String
in Kotlin is equivalent to String...
in Java. In this matching, no matter what kind of code is represented by String[]
(String is used as an example here, and other types are the same)
Check whether the configuration in your build.gradle contains Chinese. If there is Chinese, please try to change it to English and install it again
If it is a direct call, it will not cause recursion, and the framework has already handled it
If it is an indirect call, it will cause recursion, such as calling methods of other classes that contain the original method. The framework does not handle this. If you need to do this, you can combine exclude To use Homepage access, step 4 is introduced, use exclude to exclude the indirect call class
Please upgrade to version 2.1.5 or later, and check the access step 3
//👇This item is undoubtedly, it must be turned on! !
+androidAop.debugMode=true
+//👇This item needs to be turned off when you release the aar package. You can't turn it on again, because the release of aar is actually the release of the release package, don't you think?
+androidAop.debugMode.variantOnlyDebug=false
+
//👇Turn it on to use reflection
+androidAop.reflectInvokeMethod=true
+//👇This item is similar to androidAop.debugMode.variantOnlyDebug. If you use reflection in the release package, turn this item off! !
+androidAop.reflectInvokeMethod.variantOnlyDebug=false
+
In summary, releasing aar is actually the same as releasing apk, and the understanding of the above configurations is actually the same
Some people may still have questions, how should they be used in the final packaging?
androidAopConfig {
+ //👇 Exclude the aar packages that have been processed by AOP, and you can still read the aspect configuration of these packages
+ exclude 'aar package name 1', 'aar package name 2'
+ //❗️❗️❗️It is worth mentioning that when you publish aar, do not configure the package name of the aar you want to publish here, otherwise the aar will not be processed by AOP
+}
+
In fact, for this kind of demand, you can make an annotation aspect. When processing the aspect, you can pass the data back to the aspect method after requesting it, for example:
@AndroidAopPointCut(CommonDataCut::class)
+@Target(
+ AnnotationTarget.FUNCTION
+)
+@Retention(AnnotationRetention.RUNTIME)
+@Keep
+annotation class CommonData
+
class CommonDataCut : BasePointCut<CommonData> {
+ override fun invoke(
+ joinPoint: ProceedJoinPoint,
+ anno: CommonData
+ ): Any? {
+ if (joinPoint.args[0] != null) {
+// If there is data, continue to execute the method directly
+ joinPoint.proceed()
+ } else {
+// If there is no data, write the network request data here, and call joinPoint.proceed(data) after the data is returned to pass the data back to the method
+ HttpData.getInstance().getCountryList(req, new HttpResponeListener < Data >() {
+
+ @Override
+ public void onSuccess(String url, Data response) {
+ joinPoint.proceed(response)
+ }
+
+ });
+ }
+
+ return null
+ }
+}
+
@CommonData
+fun onTest(data: Data) {
+//Because the facet has passed the data back, the data is no longer null
+}
+//When calling the method, just pass null, and get the data after entering the facet. After entering the method, the data will be available
+binding.btnSingleClick.setOnClickListener {
+ onTest(null)
+}
+
@AndroidAopPointCut(CommonDataCut::class)
+@Target(
+ AnnotationTarget.FUNCTION
+)
+@Retention(AnnotationRetention.RUNTIME)
+@Keep
+annotation class CommonData
+
class CommonDataCut : BasePointCut<CommonData> {
+ override fun invoke(
+ joinPoint: ProceedJoinPoint,
+ anno: CommonData
+ ): Any? {
+ if (!joinPoint.args.isNullOrEmpty()) {
+ val arg1 =
+ joinPoint.args[0] // This is the incoming data, so you can pass data to the slice at will
+
+ }
+ return joinPoint.proceed()
+ }
+}
+
@CommonData
+fun onTest(number: Int) {//num is the dynamic data of the incoming slice, regardless of type
+
+}
+
+binding.btnSingleClick.setOnClickListener {
+//Input dynamic data when calling the method
+ onTest(1)
+}
+
@AndroidAopMatchClassMethod(
+ targetClassName = "android.view.View.OnClickListener",
+ methodName = ["onClick"],
+ type = MatchType.EXTENDS //type must be EXTENDS because you want to hook all classes that inherit OnClickListener
+)
+class MatchOnClick : MatchClassMethod {
+ override fun invoke(joinPoint: ProceedJoinPoint, methodName: String): Any? {
+ Log.e("MatchOnClick", "=====invoke=====$methodName")
+ return joinPoint.proceed()
+ }
+}
+
The target of ProceedJoinPoint is not android.view.View.OnClickListener
The methodName of the invoke callback is not onClick but the method name automatically generated during compilation, similar to onCreate\(lambda\)14, which contains the lambda keyword
For the view of onClick(view:View)
I will not go into details about this, you will know it after using it yourself;
To summarize: In fact, for all lambda's ProceedJoinPoint.args
@Permission
, you may think that now you only get permission to enter the method, but there is no callback without permission. The following example teaches you how to do it¶@Permission
and the other as the result returned by the permission framework (here I use rxpermissions, you can use it at will) @Permission
permission annotation and implement the PermissionRejectListener
interface for the object where its method is located // Implement PermissionRejectListener on the object using @Permission Interface
+class MainActivity : BaseActivity2(), PermissionRejectListener {
+ lateinit var binding: ActivityMainBinding
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState) binding = ActivityMainBinding . inflate (layoutInflater) setContentView (binding.root) binding . btnPermission . setOnClickenerListener { toGetPicture() } binding . btnPermission2 . setOnClickListener { toOpenCamera() }
+ }
+
+ @Permission(
+ tag = "toGetPicture",
+ value = [Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE]
+ )
+ fun toGetPicture() { //Find pictures } @Permission(tag = "toOpenCamera",value = [Manifest.permission.CAMERA]) fun toOpenCamera(){
+//Open the camera
+ }
+
+ override fun onReject(
+ permission: Permission,
+ permissionResult: com.tbruyelle.rxpermissions3.Permission
+ ) {
+//Use the tag to distinguish which method's permission is denied
+ if (permission.tag == "toGetPicture") {
+
+ } else if (permission.tag == "toOpenCamera") {
+
+ }
+ }
+}
+
Application
class MyApp2 : Application() {
+ override fun onCreate() {
+ super.onCreate()
+ AndroidAop.setOnPermissionsInterceptListener(object : OnPermissionsInterceptListener {
+ @SuppressLint("CheckResult")
+ override fun requestPermission(
+ joinPoint: ProceedJoinPoint,
+ permission: Permission,
+ call: OnRequestPermissionListener
+ ) {
+ val target = joinPoint.getTarget()
+ val permissions: Array<out String> = permission.value
+ when (target) {
+ is FragmentActivity -> {
+ val rxPermissions = RxPermissions((target as FragmentActivity?)!!)
+ rxPermissions.requestEach(*permissions)
+ .subscribe { permissionResult: com.tbruyelle.rxpermissions3.Permission ->
+ call.onCall(permissionResult.granted)
+ //👇The key is here👇
+ if (!permissionResult.granted && target is PermissionRejectListener) {
+ (target as PermissionRejectListener).onReject(
+ permission,
+ permissionResult
+ )
+ }
+ }
+ }
+
+ is Fragment -> {
+ val rxPermissions = RxPermissions((target as Fragment?)!!)
+ rxPermissions.requestEach(*permissions)
+ .subscribe { permissionResult: com.tbruyelle.rxpermissions3.Permission ->
+ call.onCall(permissionResult.granted)
+ //👇The key is here👇
+ if (!permissionResult.granted && target is PermissionRejectListener) {
+ (target as PermissionRejectListener).onReject(
+ permission,
+ permissionResult
+ )
+ }
+ }
+ }
+
+ else -> {
+ // TODO: target is not FragmentActivity or Fragment, indicating that the method where the annotation is located is not in it. Please handle this situation yourself.
+ // Suggestion: The first parameter of the cut point method can be set to FragmentActivity or Fragment, and then joinPoint.args[0] can be obtained
+ }
+ }
+ }
+ })
+
+ }
+}
+
Note
The core of this technique is "solving the problem of not being able to call the method of the object where the pointcut method is located", solving this problem by adding an interface to the object where the pointcut method is located, and most importantly, making it universal
What does the ARouter plugin library mainly do? Why can AndroidAOP solve this problem? In fact, its plugin library does two things:
No more nonsense, let’s go straight to the code
object AlibabaCollect {
+ private val classNameSet =
+ mutableSetOf<String>() @AndroidAopCollectMethod(regex = "com.alibaba.android.arouter.routes.*?", collectType = CollectType.DIRECT_EXTENDS) @JvmStatic
+ fun collectIRouteRoot(sub: Class<out IRouteRoot>) {
+ Log.e("A libabaCollect", "collectIRouteRoot=$sub") classNameSet . add (sub.name)
+ }
+ @AndroidAopCollectMethod(
+ regex = "com.alibaba.android.arouter.routes.*?",
+ collectType = CollectType.DIRECT_EXTENDS
+ )
+ @JvmStatic
+ fun collectIProviderGroup(sub: Class<out IProviderGroup>) {
+ Log.e("AlibabaCollect", "collectIProviderGroup=$sub")
+ classNameSet.add(sub.name)
+ }
+
+ @AndroidAopCollectMethod(
+ regex = "com.alibaba.android.arouter.routes.*?",
+ collectType = CollectType.DIRECT_EXTENDS
+ )
+ @JvmStatic
+ fun collectIInterceptorGroup(sub: Class<out IInterceptorGroup>) {
+ Log.e("AlibabaCollect", "collectIInterceptorGroup=$sub")
+ classNameSet.add(sub.name)
+ }
+
+ fun getClassNameSet(): MutableSet<String> {
+ return classNameSet
+ }
+}
+
The code above is the first thing that the ARouter plugin does, which is to search for these three categories
@AndroidAopMatchClassMethod(
+ targetClassName = "com.alibaba.android.arouter.core.LogisticsCenter",
+ methodName = ["loadRouterMap"],
+ type = MatchType.SELF
+)
+class ARouterMatch : MatchClassMethod {
+ override fun invoke(joinPoint: ProceedJoinPoint, methodName: String): Any? {
+ val any = joinPoint.proceed()
+ val registerMethod = LogisticsCenter::class.java.getDeclaredMethod(
+ "register",
+ java.lang.String::class.java
+ ) registerMethod . isAccessible = true
+ val classNameSet = AlibabaCollect.getClassNameSet() classNameSet . forEach {
+ registerMethod.invoke(
+ null,
+ it
+ ) Log . e ("ARouterMatch", "registerMethod=$it")
+ } return any
+ }
+}
+
The above is the second thing done by the ARouter plug-in.
ARouter.init(this)
code callsLogisticsCenter.loadRouterMap()
to register all three types. That's all.
After these two steps of configuration, you can delete classpath "com.alibaba:arouter-register:?"
. It is worth noting that the above configuration is only effective if you turn off androidAop.debugMode = false
. In addition, because ARouter can be used without plugins, you can still turn on debugMode after testing and turn it off when you build the release package (the latest version adds this sentence androidAop.debugMode.variantOnlyDebug = true
without manual closing)
Note
Finally, when you call ARouter.init(this)
, you will see the log below, which means the code has taken effect! Finally, don't forget to do anti-obfuscation processing on com.alibaba.android.arouter.core.LogisticsCenter
, because reflection is used above
For @AndroidAopPointCut and @AndroidAopMatchClassMethod, both aspects have their aspect callback processing classes respectively
Note
You can see that both invoke methods have a return value, which will replace the return value of the entry point method and will be automatically converted to the return type of the original method, but the following two types have no return value
@MyAnno
+public int numberAdd(int value1,int value2){
+ int result=value1+value2;
+ return result;
+ }
+
public class MyAnnoCut implements BasePointCut<MyAnno> {
+ @Nullable
+ @Override
+ public Object invoke(@NonNull ProceedJoinPoint joinPoint, @NonNull MyAnno anno) {
+ int value1 = (int) joinPoint.args[0];
+ int value2 = (int) joinPoint.args[1];
+ int result = value1 * value2;
+ return result;
+ }
+}
+
Note
For the suspend function of the cut point function, it is better to use the above two types. If you continue to use BasePointCut
and MatchClassMethod
, its return value must be joinPoint.proceed()
The return value of the onReturn function. If you need to modify the return value, please see the following code:
class MyAnnoCut5 : BasePointCutSuspend<MyAnno5> {
+ override suspend fun invokeSuspend(joinPoint: ProceedJoinPointSuspend, anno: MyAnno5) {
+ withContext(Dispatchers.IO) {
+//Modify the return value by setting OnSuspendReturnListener The return value of onReturn is the return value of the suspend point function
+ joinPoint.proceed(object : OnSuspendReturnListener {
+ override fun onReturn(proceedReturn: ProceedReturn): Any? {
+ return (proceedReturn.proceed() as Int) + 100
+ }
+ })
+ }
+
+ }
+}
+
Note
Here onReturn The explanation is the same as What is the return value here
Information related to the pointcut method, including pointcut method parameters, pointcut object, and continued execution of the original method logic, etc.
In this introduction, proceed()
or proceed(args)
of the ProceedJoinPoint
object is used to execute the logic of the original method. The difference is:
proceed()
does not pass parameters, indicating that the original incoming parameters are not changedproceed(args)
has parameters, indicating that the parameters passed in at the time are rewritten. Note that the number of parameters passed in and the type of each parameter must be consistent with the aspect methodproceed
is not called, the code in the interception aspect method will not be executedWhen there are multiple annotations or matching aspects for the same method, proceed
means entering the next aspect. How to deal with it specifically?
proceed
is called, and the code in the entry method will be called only after the last facet among multiple facets executes proceed
proceed(args)
in the previous facet can update the parameters passed in by the method, and the next facet will also get the parameters updated in the previous layerproceed
, the return value of the first asynchronous call proceed
facet (that is, the return value of invoke) is the return value of the entry method; otherwise, if there is no asynchronous call proceed
, the return value is the return value of the last facetProceedJoinPointSuspend
's proceed
method¶ProceedJoinPointSuspend adds two new methods including OnSuspendReturnListener
proceed
method, two proceedIgnoreOther
methods containing OnSuspendReturnListener2
are added
proceed
methods and the original proceed
method is different from that of ordinary functions. The return value after calling is not the return value of the pointcut function, but the other logic is the same as the two points mentioned aboveOnSuspendReturnListener
passed in by the two new proceed
methods can get the return value of the pointcut function through the callback ProceedReturn
, and the return value of the pointcut function can be modified through onReturn
proceedIgnoreOther
methods are to stop executing the code in the pointcut function and modify the return value of the pointcut function Click here for details ## getArgsAll the parameters passed in when the pointcut method is called
This is an introduction to the args of lambda expressions
Same as args, but with a different reference address. The object reference addresses in the array are the same. When there are multiple annotations or matching aspects in the same method, calling proceed(args) will change the reference address of args, or change the reference address in args. Through getOriginalArgs(), you can get the parameters when the pointcut method is first entered
If the pointcut method is not a static method, target is the object where the pointcut method is located. If the pointcut method is a static method, target is null
PS: If ProceedJoinPoint.target is null, it is because the injected method is static, usually Java This situation will occur in static methods of Kotlin and functions modified by @JvmStatic, top-level functions, and lamba expressions
Here is an introduction to the target of lambda expressions
Returns information related to the point-cut method, such as method name, parameter name, parameter type, return type, etc. ... You can check the specific information in the class returned by the method (AopMethod)
Here is an introduction to the getTargetMethod of lambda expressions
Returns the Class<?> object of the class where the point-cut method is located
Continue to execute the code of the return value code block. The return value is the actual return value of the suspend function. If you need to modify the passed parameters, you can still use ProceedJoinPoint
to modify it
When you use @AndroidAopPointCut
and @AndroidAopMatchClassMethod
, if the entry function is modified by suspend
, you have two choices of aspect processing classes
suspend
¶When you choose the first option, AndroidAOP will treat it as a normal function, but remember that the return value cannot be modified. It can only return the result of joinPoint.proceed()
, so this method cannot modify the return result, for example:
class MyAnnoCut3 : BasePointCut<MyAnno3> {
+ override fun invoke(joinPoint: ProceedJoinPoint, anno: MyAnno3): Any? {
+ Log.e("MyAnnoCut3", "====invoke=====")
+ return joinPoint.proceed()
+ }
+}
+
Note
Although the return result cannot be modified, you can call joinPoint.proceed()
instead, and you can also modify the input parameters, such as joinPoint.proceed(1,2)
suspend
¶When you choose the second option, you need to specify the thread in invokeSuspend, such as using the withContext
function, for example
class MyAnnoCut3 : BasePointCutSuspend<MyAnno3> {
+ override suspend fun invokeSuspend(joinPoint: ProceedJoinPointSuspend, anno: MyAnno3) {
+ withContext(Dispatchers.Main) {
+ ...
+ joinPoint.proceed(object : OnSuspendReturnListener {
+ override fun onReturn(proceedReturn: ProceedReturn): Any? {
+ val result = proceedReturn.proceed()
+ Log.e(
+ "MyAnnoCut3",
+ "====onReturn=====${proceedReturn.returnType},result=$result"
+ )
+ return (result as Int) + 100
+ }
+
+ })
+ }
+
+ }
+}
+
Note
withContext
function, a ClassCastException
exception may occur when the cut point function returns the result. The advantage of using this aspect processing class is that you can modify the return result (detailed introduction), and call other suspend functions joinPoint.proceed
or joinPoint.proceedIgnoreOther
in the last line of the withContext functionWarning
If the cut point function is not a suspend
function, even if BasePointCutSuspend
and MatchClassMethodSuspend
are used, the invoke
method will still be called back instead of the invokeSuspend
method
proceed()
is not called and returns directly, for example:¶class MyAnnoCut3 : BasePointCut<MyAnno3> {
+ override fun invoke(joinPoint: ProceedJoinPoint, anno: MyAnno3): Any? {
+ Log.e("MyAnnoCut3", "====invoke=====")
+ return null
+ }
+}
+
class MyAnnoCut3 : BasePointCutSuspend<MyAnno3> {
+ override suspend fun invokeSuspend(joinPoint: ProceedJoinPointSuspend, anno: MyAnno3) {
+ withContext(Dispatchers.Main) {
+ ...
+ joinPoint.proceedIgnoreOther(object : OnSuspendReturnListener2 {
+ override fun onReturn(proceedReturn: ProceedReturn2): Any? {
+ Log.e("MyAnnoCut3", "====invokeSuspend=====") return null
+ }
+ })
+ }
+ }
+}
+
dependencies {
+ //Optional 👇This package provides some common annotation aspects
+ implementation 'io.github.FlyJingFish.AndroidAop:android-aop-extra:2.2.5'
+}
+
Annotation name | Parameter description | Function description |
---|---|---|
@SingleClick | value = interval of quick clicks, default 1000ms | Click the annotation and add this annotation to make your method accessible only when clicked |
@DoubleClick | value = maximum time between two clicks, default 300ms | Double-click annotation, add this annotation to make your method enterable only when double-clicked |
@IOThread | ThreadType = thread type | Switch to the sub-thread operation. Adding this annotation can switch the code in your method to the sub-thread for execution |
@MainThread | No parameters | The operation of switching to the main thread. Adding this annotation can switch the code in your method to the main thread for execution |
@OnLifecycle* | value = Lifecycle.Event | Monitor life cycle operations. Adding this annotation allows the code in your method to be executed only during the corresponding life cycle |
@TryCatch | value = a flag you customized | Adding this annotation can wrap a layer of try catch code for your method |
@Permission* | value = String array of permissions | The operation of applying for permissions. Adding this annotation will enable your code to be executed only after obtaining permissions |
@Scheduled | initialDelay = delayed start time interval = interval repeatCount = number of repetitions isOnMainThread = whether to be the main thread id = unique identifier | Scheduled tasks, add this annotation to make your method Executed every once in a while, call AndroidAop.shutdownNow(id) or AndroidAop.shutdown(id) to stop |
@Delay | delay = delay time isOnMainThread = whether the main thread id = unique identifier | Delay task, add this annotation to delay the execution of your method for a period of time, call AndroidAop.shutdownNow(id) or AndroidAop .shutdown(id) can be canceled |
@CheckNetwork | tag = custom tag toastText = toast prompt when there is no network invokeListener = whether to take over the check network logic | Check whether the network is available, adding this annotation will allow your method to enter only when there is a network |
@CustomIntercept | value = a flag of a string array that you customized | Custom interception, used with AndroidAop.setOnCustomInterceptListener, is a panacea |
( * Supports suspend functions, returns results when conditions are met, and supports suspend functions whose return type is not Unit type)
All examples of the above annotations are here,Also This
@TryCatch Using this annotation you can set the following settings (not required)
AndroidAop.INSTANCE.setOnThrowableListener(new OnThrowableListener() {
+ @Nullable
+ @Override
+ public Object handleThrowable(@NonNull String flag, @Nullable Throwable throwable,TryCatch tryCatch) {
+ // TODO: 2023/11/11 If an exception occurs, you can handle it accordingly according to the flag you passed in at the time. If you need to rewrite the return value, just return at return
+ return 3;
+ }
+});
+
@Permission Use of this annotation must match the following settings (⚠️This step is required, otherwise it will have no effect)Perfect usage inspiration
AndroidAop.INSTANCE.setOnPermissionsInterceptListener(new OnPermissionsInterceptListener() {
+ @SuppressLint("CheckResult")
+ @Override
+ public void requestPermission(@NonNull ProceedJoinPoint joinPoint, @NonNull Permission permission, @NonNull OnRequestPermissionListener call) {
+ Object target = joinPoint.getTarget();
+ if (target instanceof FragmentActivity){
+ RxPermissions rxPermissions = new RxPermissions((FragmentActivity) target);
+ rxPermissions.request(permission.value()).subscribe(call::onCall);
+ }else if (target instanceof Fragment){
+ RxPermissions rxPermissions = new RxPermissions((Fragment) target);
+ rxPermissions.request(permission.value()).subscribe(call::onCall);
+ }else{
+ // TODO: target is not FragmentActivity or Fragment, which means the method where the annotation is located is not among them. Please handle this situation yourself.
+ // Suggestion: The first parameter of the pointcut method can be set to FragmentActivity or Fragment, and then joinPoint.args[0] can be obtained
+ }
+ }
+});
+
@CustomIntercept To use this annotation you must match the following settings (⚠️This step is required, otherwise what’s the point?)
AndroidAop.INSTANCE.setOnCustomInterceptListener(new OnCustomInterceptListener() {
+ @Nullable
+ @Override
+ public Object invoke(@NonNull ProceedJoinPoint joinPoint, @NonNull CustomIntercept customIntercept) {
+ // TODO: 2023/11/11 在此写你的逻辑 在合适的地方调用 joinPoint.proceed(),
+ // joinPoint.proceed(args)可以修改方法传入的参数,如果需要改写返回值,则在 return 处返回即可
+
+ return null;
+ }
+});
+
AndroidAop.INSTANCE.setOnCheckNetworkListener(new OnCheckNetworkListener() {
+ @Nullable
+ @Override
+ public Object invoke(@NonNull ProceedJoinPoint joinPoint, @NonNull CheckNetwork checkNetwork, boolean availableNetwork) {
+ return null;
+ }
+});
+
👆The above three monitors are best placed in your application