Customize Android IOC framework

Keywords: Android

1, IOC Simple Science Popularization

IOC is the Inversion of Control (IOC)

If you need to use many member variables F1 and F2 in a class A.

Traditional writing: if you want to use these member variables, you can use new F1(), new F2(), etc.

The IOC principle is: No! We don't want new. In this way, the coupling is too high. Once the construction methods of dependent F1 and F2 are changed, all places requiring new F1() and new F2() need to be modified!

According to this principle, in order to decouple dependency callers (A) and dependency providers (F1, F2, etc.), IOC decoupling has two ideas: 1. Write configuration files; 2. Use annotations

Of course, with configuration files and annotations, how to inject? That is, how to turn the configuration or annotation information into the required classes?

OK, here we go! In other words, a long time ago, the reflection was very slow. Well, that was a long time ago. Now it is not too slow. I simply tested it. A reflection takes about 2ms, which is basically negligible. PS: don't say what happens if you use 10000 annotations? Because you can't call 10000 methods at the same time. Therefore, performance can be assured.

The so-called: no reflection, no frame!

As for annotation, there are two ways: runtime annotation and compile time annotation.

  1. Runtime annotation is to use reflection at runtime to dynamically obtain objects, attributes, methods, etc. This is the case with general IOC frameworks, which may sacrifice a little efficiency.
  2. Compile time annotation is to perform some additional operations according to the annotation during program compilation. The famous ButterKnife uses compile time annotation. ButterKnife automatically generates some auxiliary classes according to the annotation when we compile. To play with compile time annotation, you must first rely on apt, r and then write a class to inherit AbstractProcessor, rewrite the process method, and implement how to turn the configuration or annotation information into the required class.

For example, the implementation of Butterknife is to call APT and create an auxiliary class with JavaFileObject according to the annotation during compilation, that is, the legendary code generator: generate code with code!

2, Use of custom IOC framework

1. Instructions

(1) Class annotation:

IContentView: inject ContentView

(2) Field annotation:

IView: inject View

IString: inject String

IColor: inject Color

(3) Method notes:

IClick: injection click event

(4) Tools:

InjectUtil.bind(this): bind Activity

InjectUtil.unbind(this): unbind Activity

2. Example code

package com.che.baseutil;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

import com.che.fast_ioc.InjectUtil;
import com.che.fast_ioc.annotation.IClick;
import com.che.fast_ioc.annotation.IColor;
import com.che.fast_ioc.annotation.IContentView;
import com.che.fast_ioc.annotation.IString;
import com.che.fast_ioc.annotation.IView;

import java.util.Random;

@IContentView(R.layout.activity_main)
public class MainActivity extends Activity {
    @IView(R.id.tv)
    TextView tv;
    @IView(R.id.bt)
    Button bt;
    @IString(R.string.text_home)
    String title;
    @IColor(R.color.turquoise)
    int turquoiseColor;
    @IColor(R.color.moccasin)
    int moccasinColor;
    private Random random;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        InjectUtil.bind(this);
        random = new Random();
        tv.setText(title);
        tv.setBackgroundColor(turquoiseColor);
        bt.setBackgroundColor(moccasinColor);
    }

    @Override
    protected void onDestroy() {
        InjectUtil.unbind(this);
        super.onDestroy();
    }

    @IClick({R.id.tv, R.id.bt})
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.tv:
                bt.setText(random.nextInt(100) + "");
                break;
            case R.id.bt:
                tv.setText("I want to change");
                Intent intent = new Intent();
                intent.setAction(IntentKey.ACTIVITY_SECOND);
                startActivity(intent);
                break;
        }

    }
}

3, How to implement a custom IOC framework

1. Define the annotations you need

Injection layout:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface IContentView {
    //Default field, when used: @ IView(R.id.tv)
    int value();

    //Other fields, when used: @ IView(id=R.id.tv)
//    int id() default 0;
}

Injection view:

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface IView {
    int value();
}

Injection string

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface IString {
    int value();
}

Injection color value

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface IColor {
    int value();
}

Injection click event

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface IClick {
    int[] value();
}

2. Write injection tool class

Backup knowledge:

  1. Get class: activity.getClass();
  2. Get fields: activity. Getdeclaraedfields();
  3. Get method: activity.getDeclaredMethods();
  4. Determine whether an annotation exists: isAnnotationPresent()
  5. Get annotation: getAnnotation()
  6. The field to get the annotation: annotation.value(), and other fields are the same
  7. Set the readability / Writeability of a field: field.setAccessible(true);
  8. Modify the field value of the object: field.set(object, value);

Train of thought analysis:

  1. Get the value of class annotation and field annotation, and perform relevant operations according to the annotation value: for example, setContentView(value), findViewById(value), etc.
  2. Get the value of the method annotation, generate a dynamic proxy class according to the annotation value, call setOnClickListener(), etc
package com.che.fast_ioc;

import android.app.Activity;
import android.view.View;

import com.che.base_util.LogUtil;
import com.che.fast_ioc.annotation.IClick;
import com.che.fast_ioc.annotation.IColor;
import com.che.fast_ioc.annotation.IContentView;
import com.che.fast_ioc.annotation.IString;
import com.che.fast_ioc.annotation.IView;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.List;

/**
 * Annotation tool class
 * <p>
 */
public class InjectUtil {

    /**
     * binding
     *
     * @param activity
     */
    public static void bind(Activity activity) {
        Class<? extends Activity> clazz = activity.getClass();
        //Processing class
        processType(activity, clazz);
        //Traverse all fields
        for (Field field : clazz.getDeclaredFields()) {
            //Processing field
            processField(activity, field);
        }
        //Traverse all methods
        for (Method method : clazz.getDeclaredMethods()) {
            //processing method
            processMethod(activity, method);
        }
    }

    /**
     * Unbound
     *
     * @param activity
     */
    public static void unbind(Activity activity) {
        try {
            Class<? extends Activity> clazz = activity.getClass();
            //Traverse all fields
            for (Field field : clazz.getDeclaredFields()) {
                LogUtil.print("field=" + field.getName() + "\t" + field.getType());
                //Leave all fields blank
                boolean accessible = field.isAccessible();
                field.setAccessible(true);
                field.set(activity, null);
                field.setAccessible(accessible);
            }
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    /**
     * Processing class annotations
     */
    private static void processType(Activity activity, Class<? extends Activity> clazz) {
        List<Class<? extends Annotation>> annoList = new ArrayList<>();
        annoList.add(IContentView.class);

        for (Class<? extends Annotation> annotationType : annoList) {
            //Determine whether the IContentView annotation exists
            if (clazz.isAnnotationPresent(annotationType)) {
                dispatchType(activity, clazz, annotationType);
            }
        }
    }

    /**
     * Distribution class annotation
     */
    private static void dispatchType(Activity activity, Class<? extends Activity> clazz, Class<? extends Annotation> annotationType) {
        if (annotationType == IContentView.class) {
            IContentView annotation = clazz.getAnnotation(IContentView.class);
            int value = annotation.value();
            activity.setContentView(value);
        }
    }

    /**
     * Process field annotations
     */
    private static void processField(Activity activity, Field field) {
        List<Class<? extends Annotation>> annoList = new ArrayList<>();
        annoList.add(IView.class);
        annoList.add(IColor.class);
        annoList.add(IString.class);

        for (Class<? extends Annotation> annotationType : annoList) {
            if (field.isAnnotationPresent(annotationType)) {
                boolean accessible = field.isAccessible();
                field.setAccessible(true);
                dispatchFiled(activity, field, annotationType);
                field.setAccessible(accessible);
            }
        }
    }

    /**
     * Distribution field annotation
     */
    private static void dispatchFiled(Activity activity, Field field, Class<?> annotationType) {
        try {
            if (annotationType == IView.class) {
                IView anno = field.getAnnotation(IView.class);
                int value = anno.value();
                field.set(activity, activity.findViewById(value));
            }
            if (annotationType == IString.class) {
                IString anno = field.getAnnotation(IString.class);
                int value = anno.value();
                field.set(activity, activity.getString(value));
            }
            if (annotationType == IColor.class) {
                IColor anno = field.getAnnotation(IColor.class);
                int value = anno.value();
                field.set(activity, activity.getResources().getColor(value));
            }
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }

    }

    /**
     * Processing method annotation
     */
    private static void processMethod(Activity activity, Method method) {
        List<Class<? extends Annotation>> annoList = new ArrayList<>();
        annoList.add(IClick.class);

        for (Class<? extends Annotation> annotationType : annoList) {
            //Determine whether the IContentView annotation exists
            if (method.isAnnotationPresent(annotationType)) {
                dispatchMethod(activity, method, annotationType);
            }
        }
    }

    /**
     * Distribution method annotation
     */
    private static void dispatchMethod(Activity activity, Method method, Class<? extends Annotation> annotationType) {
        try {
            if (annotationType == IClick.class) {
                IClick annotation = method.getAnnotation(IClick.class);
                // Get the id in the annotation
                int[] ids = annotation.value();
                // Generate dynamic proxy when annotated
                ClassLoader classLoader = View.OnClickListener.class.getClassLoader();
                Class<?>[] interfaces = {View.OnClickListener.class};
                Object proxy = Proxy.newProxyInstance(classLoader, interfaces, new DynaHandler(activity, method));
                for (int id : ids) {
                    View view = activity.findViewById(id);
                    Method onClickMethod = view.getClass().getMethod("setOnClickListener", View.OnClickListener.class);
                    // Call setOnClickListener method to callback in dynamic class
                    onClickMethod.invoke(view, proxy);
                }
            }
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}

3. Dynamic proxy class of method annotation

/**
 * Dynamic proxy, call the method in the activity: for example, onClick, etc
 * <p>
 */
public class DynaHandler implements InvocationHandler {

    private Activity activity;
    private Method method;

    public DynaHandler(Activity activity, Method method) {
        this.activity = activity;
        this.method = method;
    }

    @Override
    public Object invoke(Object o, Method method, Object[] args) throws Throwable {
        // The method of dynamic injection is called here
        return this.method.invoke(activity, args);
    }
}

Relevant video recommendations

[Android IOC framework design] what is IOC? What is DI?

This article is transferred from https://juejin.cn/post/6955771529342746638 , in case of infringement, please contact to delete.

Posted by dearmoawiz on Mon, 29 Nov 2021 00:23:43 -0800