Android annotations and Reflection – Hand-written ButterKnife

Android Advanced series

Knowledge summary, sorting is also the process of learning, such as mistakes, welcome criticism pointed out. This article covers annotations, reflection, dynamic proxies, etc. If you are not familiar with this part, check out the following articles

Introduction to Java reflection and the use of proxy patterns in Android and the use of Java annotations in Android

One, foreword

This article mainly focuses on the previous annotations, reflection and dynamic proxy knowledge of the actual practice, equivalent to a simple summary, handwritten a simple version of ButterKnifeDemo, this part uses a lot of reflection, will certainly affect certain performance. However, the ButterKnife library is implemented by generating auxiliary code during compilation to achieve the purpose of View injection. If you are interested in its source code, I will also compile a copy later.

2. A brief introduction to ButterKnife

ButterKnife is a library that is easy to learn, so using it in your project saves you a lot of unnecessary code, so you don’t have to keep findviewByid there, and you don’t have to keep findviewByid there. This almost covers the use of ButterKnife.

As you can see, the normal findViewById() operation has been replaced by the @bindView annotation, and the various click events have been replaced by the corresponding annotation. Of course, the following line of code is the key for the above code to do its corresponding function.

1ButterKnife.bind(this);

Copy the code

With this line of code, you bind the various views.

Third, open the whole

Write a simple ButterKnife that performs much the same functionality as the third-party library ButterKnife.

3.1 BindLayoutId

Instead of using the setContentView() method, we’ll write a BindLayoutId that uses annotations to inject the current Activity layout.

To implement this functionality, we must first define an annotation that can be applied to the Activity and set the layout Id through the property.

Define the BindLayoutId annotation

This annotation is first set to be used on the class via Target, and the lifecycle is saved to run. Since a layout ID is passed in, the member variable defines an Int.

2. Use of annotations

The above operation, by annotation, binds the ID, but it’s just a facade, it doesn’t have any effect yet, because we know that we’re setting up the layout by setContentView(XXXX), so we need to get the ID inside the BindLayoutId, Perform the real operation by executing setContentView(XXXX) via reflection.

3. Reflection execution.

This step is the implementation of concrete logic, more critical, I break it down step by step. First of all, we need to understand what we’re doing here.

  • Must be to get what we need to take care ofActivityOnly through thisActivitySo we can get the annotation on it, and get the annotation information, and the reflection performs thisActivitythesetContentView(xxxx)Methods.
 1public class MyButterKnifeUtil {

2

3    private static final String TAG = "MyButterKnifeUtil";

4    / * *

5* Annotation information is processed

6     *

7     * @paramActivity Indicates the activity to be operated on

8* /


9    public static void injectLayoutId(Activity activity) {

10

11   }

12 }

Copy the code

We define a utility class and injectLayoutId method, which we use to retrieve the Activity we want to process

  • ActivityYes, we definitely need to get this Activity firstBindLayoutIdAnnotation, and get the attribute and layout ID on the annotation
 1// Reflect the Activity Class object to be processed

2Class classzz = activity.getClass();

3// Check if there is a BindLayoutId annotation

4boolean isBindLayoutId = classzz.isAnnotationPresent(BindLayoutId.class);

5if (isBindLayoutId) {    

6// Get the annotation object

7BindLayoutId bindLayoutIdzz = (BindLayoutId) 

8classzz.getAnnotation(BindLayoutId.class);    

9// Get the member variable value and attribute ID of our annotation object

10int layoutId = bindLayoutIdzz.value();   

11LogUtil.d(TAG + "--injectLayoutId layoutId=" + layoutId);   

12}

Copy the code

As you can see, we get the layout Id that we set by reflection.

  • Once we get the Id, we’re definitely gonna do it nextActivitythesetContentViewMethod, we’ve already got the Activity as a parameter passed in, so execute its method directly through reflection.
1try {  

2// Reflection gets the setContentView() method

3Method setContentViewMethod = classzz.getMethod("setContentView".int.class);   

4// Execute method

5setContentViewMethod.invoke(activity, layoutId);

6catch ( Exception e ) {  

7LogUtil.e(TAG + "--injectLayoutId error=" + e.getMessage());   

8 e.printStackTrace();

9}

Copy the code

Post the full code:

Ok, the specific execution logic is implemented, we just need to inject in the Activity is done.

injection

Inject in the corresponding activity, and our layout ID is annotated.

1MyButterKnifeUtil.injectLayoutId(this);

Copy the code

When the program runs, you can see that our layout was successfully injected via BindLayoutId.

3.2 MyBindView

The layout ID is injected, so let’s inject the control ID. The basic steps are the same.

Define the MyBindView annotation

The annotation for the control ID is used on the property, so here we use elementType. FIELD for @target

2. Use of annotations

InjectViewId = BindLayoutId = injectViewId = BindLayoutId = injectViewId

 1    public static void injectViewId(Activity activity) {

2        / * *

3* :

4* 1. We first need to get all the controls on the current Activity annotated by the MyBindView annotation

5* And get the property information in the annotation (control ID)

6Reflection executes the findViewById() method in the Activity, passing in our ID.

7* /


8        // Reflect the Activity Class object to be processed

9        Class classzz = activity.getClass();

10        // get all the member variables of the current Activity

11        Field[] fields = classzz.getDeclaredFields();

12        for (Field field : fields) {

13            // Iterate to see if the current member variable has the MyBindView annotation modifier

14            boolean isMyBindView = field.isAnnotationPresent(MyBindView.class);

15            LogUtil.d(TAG + "--injectViewId isMyBindView=" + isMyBindView);

16            if(! isMyBindView) {

17                // Member variables without MyBindView annotations are filtered directly.

18                continue;

19            }

20            // Get the MyBindView annotation object from the member variable

21            MyBindView myBindViewzz = field.getAnnotation(MyBindView.class);

22            // Get the annotation's member variable and control Id

23            int myViewId = myBindViewzz.value();

24            LogUtil.d(TAG + "--injectViewId id=" + myViewId);

25            try {

26                // Execute findViewById() in the Activity by reflection

27                Method method = classzz.getMethod("findViewById".int.class);

28                // reflection executes and retrieves the returned control object

29                // =View view=mainActivity.findViewById(valueId);

30                View view = (View) method.invoke(activity, myViewId);

31                // We have already got the actual control object via id, we need to assign to us

32                // Assign to the member variable of the obtained control

33                field.setAccessible(true);

34                field.set(activity, view);

35            } catch (Exception e) {

36                e.printStackTrace();

37                LogUtil.e(TAG + "--injectViewId error=" + e.getMessage());

38            }

39        }

40    }

Copy the code

Through MyButterKnifeUtil. InjectViewId (this) into the Activity, the results are as follows.

You can see that the control is set up successfully, indicating that our control injection is OK.

3.3 Event Handling (OnClick,onLongClick)

The above two processes are relatively simple, almost the same, but the event processing part is relatively more complex, which will also involve the dynamic proxy part, I try to go to the details of each step.

Of course, before we start, we need to analyze the work we have to do.

1, based on our previous MyBindView idea, the first must be through annotation to get the actual control object (through reflection); 2, get the control, to dynamically handle the execution of various events (click, long press, etc.). 3. Call the method back to the user after execution (dynamic proxy)

In the above figure, we need to deal with several parameters ABCD, including the control object, we have gone through the last example, in order to make the event processing part more perfect, compatible with different trigger events, we can create a new annotation to bind the three dynamic parameters BCD.


Let’s define a BaseEvent annotation to dynamically manage these three parameters, and then extend the various listening events.

Note that the @target for this annotation is ANNOTATION_TYPE, and the annotation is on the annotation.

Create a new annotation for the event we clicked like this:

Use:

Again, the above is just injection, the real implementation logic needs to be implemented by us, again in MyButterKnifeUtil added methods to implement our logic.

1public void injectListener(Activity activity) {}

2

3}

Copy the code

1. First, you need to get all the methods of the current Activity, iterate over all the annotations on the fetch method, get the annotation’s Class object, and get the BaseEvent annotation by reflection to determine whether the current annotation is an event-handling annotation, and then operate on it.

2, with the annotated Class object, we can reflect the method to get it, and reflect the execution, get the return value, and set the ID array, through the ID, can reflect the control View

3. We get the control object and the method of the event via the BaseEvent attribute, but there is a problem. Method.invoke (Activity, view) cannot be invoked directly by reflecting the method in the Activity. Because we need to call back after the button is actually clicked, we need to use dynamic proxies to do this.

Let’s start by creating a dynamic proxy class.

When a user event is triggered by a dynamic proxy, the callback event will go to the invoke method. In the invoke method of the dynamic proxy, the actual method in the Activity is executed.

We bind dynamic proxies to events.

The complete code

 1   public static void injectListener(Activity activity) {

2Class<? > classzz = activity.getClass();

3        // reflection gets all methods

4        Method[] methods = classzz.getDeclaredMethods();

5        // Iterate over all the methods in the current Activity

6        for (Method method : methods) {

7            // Get all the annotations on each method

8            Annotation[] annotations = method.getAnnotations();

9            for (Annotation annotation : annotations) {

10                // Call annotationType to get the annotation Class object,

11Class<? > annotationzz = annotation.annotationType();

12                // Get its BaseEvent annotation by annotationClass reflection

13                BaseEvent baseEvent = annotationzz.getAnnotation(BaseEvent.class);

14                if (baseEvent == null) {

15                    continue;

16                }

17                // Get the baseEvent annotation and get all of its member variables.

18                String listenerSetter = baseEvent.listenerSetter();

19Class<? > listenerType = baseEvent.listenerType();

20                String callBackMethod = baseEvent.callBackMethod();

21

22                try {

23                    // Get its member variables by annotationzz reflection

24                    Method declaredMethod = annotationzz.getDeclaredMethod("value");

25                    // Reflection method execution

26                    int[] ids = (int[]) declaredMethod.invoke(annotation);

27                    if (ids == null) {

28                        continue;

29                    }

30                    for (int id : ids) {

31                        Method findViewById = classzz.getMethod("findViewById".int.class);

32                        // Get the specific control View

33                        View view = (View) findViewById.invoke(activity, id);

34                        LogUtil.d(TAG + "--injectListener id=" + id);

35                        if (view == null) {

36                            continue;

37                        }

38

39                        // Use dynamic proxy events to pass events after user actions to the proxy class, where the Activity is reflected to execute.

40                        ListenerInvocationHandler listenerInvocationHandler

41                                = new ListenerInvocationHandler(activity, method);

42                        // Make a proxy object, eg:new view.onClickListener () object

43                        Object proxy = Proxy.newProxyInstance(listenerType.getClassLoader()

44                                , new Class[]{listenerType}, listenerInvocationHandler);

45

46                        // Get the execution method, eg:setOnClickListener

47                        Method listenerSetterMethod = view.getClass()

48                                .getMethod(listenerSetter, listenerType);

49                        / / methods reflect perform eg: the setOnClickListener (new view. An OnClickListener () {})

50                        listenerSetterMethod.invoke(view, proxy);

51                    }

52                } catch (Exception e) {

53                    e.printStackTrace();

54                }

55            }

56

57        }

58    }

Copy the code

Once the logic is processed, we inject it and run it with the following result:

If we want to define long-press events, we just need to change the events in BaseEvent

Results:

Four,

This simple ButterKnife project will use the previous annotations, reflection, dynamic proxy knowledge points, this is a very simple demo, we often use a lot of knowledge points in the third-party library, so we can not ignore some small points, we have to learn to sort out. In this way, when looking at the source code of other excellent libraries, it will not feel so ignorant, so, together with the supervision of refueling.