AndroidAOP Framework

repository·master·Indexed 21 days ago

https://github.com/flyjingfish/androidaop

A framework for Aspect-Oriented Programming in Android applications using static weaving. It enables the implementation of cross-cutting concerns—such as permission handling, thread switching, and lifecycle monitoring—via annotations. Key features include method interception with @AndroidAopMatchClassMethod, method replacement with @AndroidAopReplaceClass, and class modification via @AndroidAopModifyExtendsClass. Requires Gradle 7.6+ and minSdkVersion 21+.

Tokens
33.4K
Snippets
78
Records
111
Agent score
75%

What's inside AndroidAOP

  1. Overview of AndroidAOP

    master

    AndroidAOP is a framework designed to help Android developers transform their apps into an AOP (Aspect-Oriented Programming) architecture. It allows you to handle common tasks like requesting permissions, switching threads, disabling multi-clicks, monitoring click events, and tracking lifecycles using simple annotations.

    Key features include:

    • Built-in Annotations: Ready-to-use annotations for common development tasks.
    • Custom Aspects: Simple syntax for creating your own custom aspects.
    • Language Support: Full support for both Java and Kotlin projects.
    • Third-party Library Support: Ability to intercept methods in third-party libraries.
    • Advanced Support: Supports Lambda expressions and suspend coroutine functions as join points.
    • Low Intrusion: Uses pure static code weaving (not based on AspectJ), resulting in minimal code injection and low overhead.
    • Developer Productivity: Supports multiple fast-development modes to maintain build speeds and supports componentized development.
  2. Handle return values for ordinary functions in Aspect callbacks

    master

    When implementing aspect callbacks for @AndroidAopPointCut or @AndroidAopMatchClassMethod, the invoke method's return value will replace the original method's return value. The library automatically converts this value to the original method's return type.

    Rules for return values:

    1. If the target method has a return value: The value returned by invoke becomes the new return value of the target method. The type must be compatible with the target method's return type.
    2. If the target method does not have a return value: The return value of invoke is ignored.
    @MyAnno
    public int numberAdd(int value1, int value2) {
        int result = value1 + value2;
        return result;
    }
    
    // Aspect implementation to change addition to multiplication
    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;
        }
    }
  3. Avoid recursion when using @AndroidAopReplaceMethod

    master

    When using @AndroidAopReplaceMethod to replace a method:

    1. Direct calls: Calling the original method directly within the replacement method will not cause recursion; the framework handles this.
    2. Indirect calls: If you call a method in another class that eventually calls the original method, it will cause recursion.

    Solution: To prevent indirect recursion, use excludeWeaving in @AndroidAopReplaceClass or use the exclude configuration in androidAopConfig to exclude the class responsible for the indirect call.

  4. Generated AOP annotations and limitations

    master

    The plugin generates auxiliary code for several specific AOP functions. When using the generated code, be aware of the following technical details and limitations:

    Supported Functions

    The plugin generates code for:

    • @AndroidAopReplaceClass
    • @AndroidAopMatchClassMethod
    • @AndroidAopModifyExtendsClass
    • @AndroidAopCollectMethod

    Technical Limitations & Accuracy

    While the plugin is highly accurate for class names and signatures, you should manually verify the following when copying @AndroidAopReplaceMethod code:

    • Kotlin Suspend Functions: The generated Java method for @AndroidAopReplaceMethod does not include the suspend modifier/logic from Kotlin source code.
    • Type Deviations: Check for discrepancies in nullability (e.g., nullable?), Kotlin-specific types, or cases where a variable parameter type has been converted to an array type.
    • Verification: Always compare the generated @AndroidAopReplaceMethod against your original source code to ensure accuracy before implementation.
  5. Handling suspend functions in AndroidAOP

    master

    When applying AOP to functions marked with the suspend keyword using @AndroidAopPointCut and @AndroidAopMatchClassMethod, you must choose the correct base classes for your aspect implementation. The choice depends on whether you need to modify the function's return value or call other suspend functions.

    Option 1: Non-suspend implementation

    Use BasePointCut and MatchClassMethod.

    • Behavior: AndroidAOP treats the target as a normal function.
    • Limitation: You cannot modify the return value. You must return the result of joinPoint.proceed() directly.
    • Capabilities: You can still modify input parameters by passing them to joinPoint.proceed(args...).

    Option 2: Suspend-aware implementation

    Use BasePointCutSuspend and MatchClassMethodSuspend.

    • Behavior: Specifically designed for suspend functions.
    • Capabilities: You can modify the return value and call other suspend functions within the aspect.
    • Requirement: You must specify a thread context (e.g., using withContext) inside invokeSuspend to avoid ClassCastException when the function returns.
    • Warning: If the target function is not a suspend function, using these classes will result in the invoke method being called instead of invokeSuspend.
    // Option 1: For non-suspend or when return value modification is not needed
    class MyAnnoCut3 : BasePointCut<MyAnno3> {
        override fun invoke(joinPoint: ProceedJoinPoint, anno: MyAnno3): Any? {
            return joinPoint.proceed()
        }
    }
    
    // Option 2: For suspend functions where you need to modify the return value
    class MyAnnoCut3 : BasePointCutSuspend<MyAnno3> {
        override suspend fun invokeSuspend(joinPoint: ProceedJoinPointSuspend, anno: MyAnno3) {
            withContext(Dispatchers.Main) {
                // ... logic ...
            }
        }
    }
  6. Collect classes using Regular Expressions

    master

    You can use the regex parameter in @AndroidAopCollectMethod to find classes whose names match a specific pattern.

    When using regex:

    1. The parameter of the annotated method can be Object or Any.
    2. The regex is used to find class names that meet the requirements.
    3. You can combine regex with inheritance settings.

    Example: Collecting all classes that end with $$Router using the regex .*?\$\$Router.

    @AndroidAopCollectMethod(regex = ".*?\$\$\$Router")
    @JvmStatic
    fun collectRouterClassRegex(sub: Any) {
        Log.e("InitCollect", "----collectRouterClassRegexObject----$sub")
    }
  7. How to use @OnLifecycle

    master

    The @OnLifecycle annotation monitors lifecycle operations. For it to work, the annotated method must belong to an object that is either:

    1. Directly or indirectly inherited from FragmentActivity or Fragment.
    2. An object that implements LifecycleOwner.

    If the object does not meet these criteria, you must pass the lifecycle owner as the first parameter of the annotated method. For example, in a static class, you can pass the MainActivity instance:

    public class StaticClass {
         @SingleClick(5000)
         @OnLifecycle(Lifecycle.Event.ON_RESUME)
         public static void onStaticPermission(MainActivity activity, int maxSelect, ThirdActivity.OnPhotoSelectListener back){
             back.onBack();
         }
    }
    @OnLifecycle(Lifecycle.Event.ON_RESUME)
    public void myMethod() { ... }
  8. Understand @AndroidAopReplaceClass and its related annotations

    master

    The @AndroidAopReplaceClass aspect is an advanced feature used to replace method calls in your code. Unlike other AOP methods that intercept execution and allow you to proceed with the original logic via ProceedJoinPoint, this aspect replaces the call sites themselves with static methods from a replacement class.

    Key Characteristics:

    • It does not automatically retain the original method execution.
    • It is specifically designed to monitor or replace calls to system methods (e.g., code within android.jar) where standard AOP weaving or reflection might be restricted by the Android version.
    • Requirement: The replacement class must be located within the scan rules defined in your androidAopConfig in build.gradle. If it is outside the scope, it will not work.
    • Important: After modifying the configuration for this aspect, you must clean the project before rebuilding.
    // This aspect must be used in conjunction with @AndroidAopReplaceMethod
    // to define how the replaced calls are handled.
  9. Understand the lifecycle of aspect processing classes

    master

    The lifecycle of an aspect processing class depends on whether the target method is static or not:

    • Non-static methods: The aspect processing class is bound to the instance of the class where the method resides. It is recycled when the object containing the method is recycled. Each object instance of the target class corresponds to a unique instance of the aspect processing class.
    • Static methods: The aspect processing class is created once and persists for the lifetime of the application. A static class method corresponds to exactly one aspect processing class.

    Note: Aspect processing class objects are only created when the target method is actually executed.

  10. Handle Kotlin lambdas and suspend functions in AOP

    master

    Lambda Monitoring

    When intercepting lambdas (e.g., setOnClickListener):

    • Method Name: The methodName in invoke will be a compiler-generated name containing the keyword lambda (e.g., onCreate$lambda$14), not the original method name.
    • Arguments (joinPoint.args):
      • Kotlin: The first argument is the object of the class that set the lambda. Subsequent arguments are the actual parameters of the intercepted method.
      • Java: All arguments in the array are the parameters of the intercepted method.

    Suspend Functions

    To intercept a suspend function using precise matching, include the suspend keyword in the methodName string, regardless of the return type.

    Example: Intercepting suspend fun getData(num: Int): Int

    @AndroidAopMatchClassMethod(
        targetClassName = "com.flyjingfish.androidaop.MainActivity",
        methodName = ["suspend getData(int)"],
        type = MatchType.SELF
    )
    class MatchSuspend : MatchClassMethod {
        override fun invoke(joinPoint: ProceedJoinPoint, methodName: String): Any? {
            return joinPoint.proceed()
        }
    }
  11. Understand aspect execution order and the proceed chain

    master

    When multiple annotations or matching aspects are applied to the same method, they form a chain.

    1. Precedence: Annotations take precedence over matching facets and are executed from top to bottom.
    2. Chain Flow: The next aspect in the chain is only triggered after proceed() is called in the current aspect. The original method is only reached after the last aspect in the chain calls proceed().
    3. Parameter Updates: Calling proceed(args) in one aspect allows you to update parameters for all subsequent aspects in the chain.
    4. Return Values:
      • In a synchronous chain, the return value of the last aspect (the one that calls the original method) propagates back through all aspects.
      • In an asynchronous call, the return value of the first asynchronous proceed facet (the invoke return value) becomes the return value of the entry method; otherwise, the return value is that of the last facet.
  12. Handle multiple annotations or matching aspects on a single method

    master

    When multiple aspects are applied to the same method, the following rules apply:

    1. Precedence: Annotations take precedence over matching aspects.
    2. Execution Order: Annotation aspects are executed from top to bottom.
    3. Chain of Execution: The next aspect in the chain is only executed after proceed() is called in the current aspect. The code in the aspect method is only called after proceed() has been executed on the last aspect in the chain.
    4. Parameter Updates: Calling proceed(args) in a previous aspect can update the parameters. These updated parameters are then passed to the next aspect in the chain.
    5. Return Values:
      • If there is an asynchronous call to proceed(), the return value of the first asynchronous proceed() (the invoke return value) becomes the return value of the cut-in method.
      • If there are no asynchronous calls, the return value is simply the return value of the last cut-in method.