preface

I’ve been thinking about this for a long time and I’m not sure what to do with it, because AOP(aspect oriented programming) has been used on Android for a long time, but perhaps because of the difficulty or the lack of understanding of its application scenarios or other reasons, no one seems to like it very much. So today I’m going to give you a few examples on Android that I hope will be of some interest to you. Properly used, it can really reduce a lot of rework, and it’s better than doing it manually because it’s low coupling, and it’s almost non-invasive.

A simple introduction

Aspect Oriented Programming(AOP), Aspect Oriented Programming, is a hot topic. The main purpose of AOP implementation is to extract the aspects of the business process. It faces a certain step or stage in the process to achieve the isolation effect of low coupling between the parts of the logical process.

The above is from Baidu Encyclopedia. Do you understand? it doesn’t matter

To put it simply, let’s say we have a loaf of bread (object oriented) and we need to turn it into a hamburger. All we need to do is cut it in the middle and stuff it with meat and vegetables.

For Android, let’s say we have an Activity that needs to become an Activity with a Toolbar. Think about it. All we need to do is make a cut in the onCreate method and insert some toolbar creation and addition code.

With a little bit of clarity, we’ll get started.

Gradle access

Today we are using Aspectj, and the integration of Aspectj on Android was a bit complicated and had some problems, but fortunately someone helped us out.

Gradle_plugin_android_aspectjx project address

Post another article by Xu Yisheng to see AspectJ’s strong insertion into Android

This is easy to do according to the Github access guide, starting with the gradle file in the root directory

dependencies {
        classpath 'com. Hujiang. Aspectjx: gradle - android plugin - aspectjx: 1.1.0'
        }
Copy the code

Then apply the plugin to your App project or library gradle

apply plugin: 'android-aspectjx'
Copy the code

And you’re done. I use the latest 1.1.1 version error, use 1.1.0 normal.

Example 1: Add a Toolbar to your Activity

So without further ado, the MainActivity code, very simple, prints a log in onCreate.

class MainActivity : AppCompatActivity() {
    private val TAG = "MainActivity"
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        Log.d(TAG, " --> onCreate")}}Copy the code

Let’s start using Aspectj

1. First try

Create a new MyAspect class with the following code

@Aspect
public class MyAspect {
    private static final String TAG = "AOPDemo";
    
    @After("execution(* android.app.Activity.onCreate(..) )")
    public void addToolbar(JoinPoint joinPoint) throws Throwable {
        String signatureStr = joinPoint.getSignature().toString();
        Log.d(TAG, signatureStr + " --> addToolbar"); }}Copy the code

First, the MyAspect class has an @aspect annotation that tells the compiler that this is an Aspectj file and that the methods in the class will be parsed at compile time.

See below addToolbar this method, the @ After comments After a rather long string, the string is the most critical, it is used to indicate to the compiler, where we are going to “cut one knife”, I think it’s very similar to regular expressions, a regular expression is matched string, and it is the matching section, or match method or constructor, etc.

Execution: The execution of a method or constructor is indicated in parentheses. So if you look inside the parentheses, you start with an *, which is the return value, and you use * to match methods that can be any type of return value, you can specify a particular type; The next space is followed by the class name full path. Method name (parameter), indicating that we want to “cut” the Activity’s onCreate method, followed by (..) Is used to specify the number and type of arguments. The two points match any number and type.

Now that the section is defined, we need to specify whether we want to insert code Before or After the section. We want to add toolbars After onCreate, so we use the @after annotation, the @before annotation, and the @around annotation that can be handled Before and After and even intercepted.

The code in the addToolbar method is what we want to insert. We don’t actually create a Toolbar here, just a log instead, but anything you use to create the Toolbar, such as the parameters of the cut method, or the object it is in, can be obtained from JoinPoint.

Now that we are done, run it to see if it is the result we want!

01-06 12:42:06. 981, 7696-7696 / IO lot. Anotherjack. Aopdemo D/aopdemo: Void android. Support. The v4. App. FragmentActivity. OnCreate (Bundle) - > addToolbar 01-06 12:42:06. 981 7696-7696/io.github.anotherjack.aopdemo D/AOPDemo: Void android. Support. V7. App. AppCompatActivity. OnCreate (Bundle) - > addToolbar 01-06 12:42:07. 007 7696-7696/io.github.anotherjack.aopdemo D/MainActivity: -- - > onCreate 01-06 12:42:07. 008, 7696-7696 / IO. Making. Anotherjack. Aopdemo D/aopdemo: void io.github.anotherjack.aopdemo.MainActivity.onCreate(Bundle) --> addToolbarCopy the code

Unfortunately, the addToolbar log prints three times, which would be ridiculous to add three toolbars. Compatactivity can be compatActivity, AppCompatActivity, and MainActivity.

Aspectj is code that is inserted at compile time. Note that compile time, our app code, and library are packaged at compile time, and things on mobile systems cannot be changed at compile time, such as Android.app.Activity, which exists on Android. It is also easy to understand, you just pack an APK, how can you change the user’s mobile phone system. And AspectJ is pretty realistic about matching methods, so as long as you’re an Activity and you have an onCreate method, THEN I’ll insert code for you. Compatactivity is a compatactivity that inherits from AppCompatActivity, which in turn inherits from FragmentActivity, which inherits from Activity. All three are activities, so their onCreate methods are inserted into the addToolbar method. The onCreate of MainActivity calls super.oncreate, and the other two do the same, so addToolbar three times.

It’s not gonna work that way, so what’s the solution?

2. Make adjustments

If you think about it, the problem is that the matching scope is too wide, so what we need to do is to add a qualification to it. We need to narrow the matching condition so that not all the activities match, and only insert code for the activities with certain conditions.

Now I’ll qualify it with an annotation, creating an annotation called ToolbarActivity

@Target(ElementType.TYPE)
public @interface ToolbarActivity {

}
Copy the code

Then modify the @After annotation above the addToolbar method

@After("execution(* android.app.Activity.onCreate(..) ) && within(@io.github.anotherjack.testlib.annotation.ToolbarActivity *)")
Copy the code

Execution of the execution of the execution of the execution of the execution of the execution of the execution. Inside, this is the class that defines the @ToolbarActivity annotation.

Finally, add @toolbarActivity to MainActivity and run it again. You should see that it works. Thus, if we want an Activity to have a Toolbar, we can annotate it with @toolbarActivity… Well, not really. Notice that the compiler is really, really honest, it’s really just going to look in your class to see if there’s an onCreate method, not an onCreate method that you inherit from your parent class, and a lot of people wrap BaseActivity around the onCreate method, If you expose the subclass to an initView method, the compiler will assume that the subclass Activity does not have an onCreate method and will not insert code into it.

Example 2: Intercept and modify toast

1. Intercept Toast’s show method with @before

Let’s try to block toast. As mentioned earlier, since Android.Widget.toast belongs to the system, there is no way to insert code into Toast’s show method through execution at compile time. While the “execute” code is in the system, the “call” code is written by us. So it’s call’s turn! On the first code

In MainActivity, click the button to pop up toast.

beforeShowToast.setOnClickListener {
            Toast.makeText(this,"Original Toast",Toast.LENGTH_SHORT).show()
        }
Copy the code

In the MyAspect

@Before("call(* android.widget.Toast.show())")
    public void changeToast(JoinPoint joinPoint) throws Throwable {
        Toast toast = (Toast) joinPoint.getTarget();
        toast.setText("Revised toast");
        Log.d(TAG, " --> changeToast");
    }
Copy the code

The biggest difference with @before is that it is no longer execution. Instead, it is call. Inside the method we get the target toast object in joinPoint.gettarget () and change the text by setText. Run it and you will see the “modified toast” pop up. To complete. This example should give you an idea of the difference between execution and call.

2. Use @around setText to handle Toast

For toast, instead of show, use setText.

MainActivity code, normally should pop up “unprocessed toast”

handleToastText.setOnClickListener {
            val toast = Toast.makeText(this,"origin",Toast.LENGTH_SHORT)
            toast.setText("Unprocessed toast")
            toast.show()
        }
Copy the code

The code in MyAspect, remember to comment out the previous interception of the show method

@Around("call(* android.widget.Toast.setText(java.lang.CharSequence))")
    public void handleToastText(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        Log.d(TAG," start handleToastText");
        proceedingJoinPoint.proceed(new Object[]{"Processed toast"}); // change the log. d(TAG," end handleToastText");

    }
Copy the code

The ProceedingJoinPoint method can call the intercepted method using its Proceed method, insert code before and after the call, or even intercept the method without calling the proceed method.

In this example, a log is typed before and after, and the proceed method is changed to the new argument “processed toast”. Of course, you can also get the toast object using the getTarget method, get the text from the toast object, and do the corresponding processing. When we run it, “processed toast” pops up and two lines of log are printed, which is what we expect.

Example 3: Dynamically requesting permissions

This example is more practical than the previous two.

Here we simulate the scene of clicking a button to take a photo. The system above 6.0 requires dynamic permission request. The code in MainActivity is as follows

takePhoto.setOnClickListener {
            takePhoto()
        }
Copy the code

The takePhoto method is coded as follows

/ / simulate the photo @ RequestPermissions (Manifest. Permission. The CAMERA, the Manifest. Permission. WRITE_EXTERNAL_STORAGE) private funtakePhoto(){
        Toast.makeText(this,"Click! Took a picture!",Toast.LENGTH_SHORT).show()
    }
Copy the code

As you can see, we have defined another @RequestPermissions annotation as follows

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestPermissions {
    String[] value() default {};
}
Copy the code

Value is an array of strings, which are the permissions we want to request, such as camera and external storage permissions in the takePhoto method.

Now let’s move on to the most important part, MyAspect

// Any annotation has the @requestpermissions method called @around ("call(* *.. *. * (..) ) && @annotation(requestPermissions)")
    public void requestPermissions(final ProceedingJoinPoint proceedingJoinPoint, RequestPermissions requestPermissions) throws Exception{
        Log.d(TAG,"----------request permission"); String[] permissions = requestPermissions.value(); / / get the permissions in the annotations array Object target = proceedingJoinPoint. GetTarget (); Activity activity = null;if (target instanceof Activity){
            activity = (Activity) target;
        }else if (target instanceof Fragment){
            activity = ((Fragment)target).getActivity();
        }

        RxPermissions rxPermissions = new RxPermissions(activity);
        final Activity finalActivity = activity;
        rxPermissions.request(permissions)
                .subscribe(new Consumer<Boolean>(){
                    @Override
                    public void accept(Boolean granted) throws Exception {
                        if(granted){ try { proceedingJoinPoint.proceed(); } catch (Throwable throwable) { throwable.printStackTrace(); }}else {
                            Toast.makeText(finalActivity,"Can't take pictures without permission.",Toast.LENGTH_LONG).show(); }}}); }Copy the code

The @annotation(requestPermissions) is the @annotation(requestPermissions) in the @around annotation. The @annotation(requestPermissions) in the @around annotation is the @annotation. If you look closely at the parentheses, the signature should be a full path, but instead it is requestPermissions. Yes, it is the parameter in the corresponding method, so that the full path of the parameter type is put there, and we can use the annotation directly in the method. We can also get annotations from JoinPoint using reflection, as shown below, but using parameters is obviously much more convenient, and reflection affects performance. In the same way, target, args, and so on can be converted to method parameters.

RequestPermissions requestPermissions1 = ((MethodSignature) proceedingJoinPoint.getSignature()).getMethod().getAnnotation(RequestPermissions.class);

Copy the code

Moving on to the detailed code within the method, we get the requested permissions from the annotation, then the target, then the activity by type, and then the request permissions, which I handled with RxPermissions. If you get the permissions proceedingJoinPoint. Proceed () let the intercept method to perform, otherwise the toast to remind users have no access permissions. Finally, add camera and external storage permissions to Manifest, run the project, and test it out.

After that, we need to request some permissions before a method call, just add the @requestpermissions annotation to the method and pass in the requested permissions.

The above are a few examples, mainly to give you a preliminary understanding of faceted programming, in the actual development can also try to use, I hope you can open your imagination, figure out more usage, make Android development easier and more fun.

The last

Some of you might think that what we’ve implemented is just like a hook into a method. In fact, I was first introduced to Aspectj when I was looking for a hook method, but gradually IT didn’t feel like a hook. Hook is usually run time, whereas Aspectj is more of a way of inserting code at compile time. The effect is the same as if we were inserting by hand, except that the compiler does the inserting for us.

The key to tangential programming is finding the right pointcut, and the pointcut matches not just execution, call, within, etc., but a lot more. Instead of using Pointcuts and Advice, I take an easy-to-understand approach, which is easy to accept, but has the disadvantage of not being systematic enough. So if this article has made you even remotely interested in AOP, Might as well go looking for a few on the net again “formal” the tutorial of a bit learns, have a cognition to a few of them concepts! 😊

reference

  • HujiangTechnology/gradle_plugin_android_aspectjx
  • Look at AspectJ’s strong insertion into Android
  • An in-depth understanding of Android AOP is highly recommended
  • Implement Aop in Android using AspectJ
  • Learn aspectJ with me at —– introduction

Finally is the address of demo, demo does not beg star, think the article is still ok in the nuggets on a like 😄 AOPDemo project address