Annotation module for Android xUtils3 source code analysis

This article has authorized the WeChat public account "Non-Famous Programmer" to be originally published. Please be sure to indicate the source when reprinting.

xUtils3 source code analysis series

1. The network module
of Android xUtils3 source code analysis 2. The picture module
of Android xUtils3 source code analysis 3. The annotation module
of Android xUtils3 source code analysis 4. The database module of Android xUtils3 source code analysis

initialization

public class BaseActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        x.view().inject(this);
    }
}

public class BaseFragment extends Fragment {

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        return x.view().inject(this, inflater, container);
    }
}

The initial initialization is not posted here x.Ext.init(this), because the function of this line of code is to obtain the ApplicationContext, and the annotation module does not need the ApplicationContext. The real initialization is here. In fact, it is somewhat inappropriate to call it "initialization" here, because the View annotations in xUtils3 are all @Retention(RetentionPolicy.RUNTIME)types, and the runtime is the real initialization, which x.view().inject(this)is where the annotations are parsed. There are only two parts of the annotation, so let's call them that. The following is x.view().inject(this)an example for analysis. Fragment and this belong to the same goal, so I won't repeat them.

View annotation

The function of the annotation can only be "flag". If there is an attribute defined in the annotation, then the specific value of the attribute can also be obtained. The value of the attribute does not have a default value, so this attribute is required when using annotations. And vice versa. Let's first look at the specific implementation of the two View annotations ContentView and ViewInject, and then look at the annotation parsing code in a unified manner.

ContentView tag

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ContentView {
    int value();
}

The scope of the object modified by the ContentView annotation is TYPE (used to describe a class, interface or enum declaration), the retention time is RUNTIME (valid at runtime), and an attribute value is also defined. Note: it is a property, not a method.

ViewInject

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ViewInject {

    int value();

    /* parent view id */
    int parentId() default 0;
}

The object scope modified by the ViewInject annotation is FIELD (used to describe properties), and the retention time is RUNTIME (valid at runtime).

View annotation analysis

The first thing to do in an Activity or Fragment is to initialize the xUtils3 annotation, ie x.view().inject(this). As mentioned earlier: this process is actually the process of View annotation parsing. This process is followed up below.

x.view()

public final class x {
    public static ViewInjector view() {
        if (Ext.viewInjector == null) {
            ViewInjectorImpl.registerInstance();
        }
        return Ext.viewInjector;
    }
}

public final class ViewInjectorImpl implements ViewInjector {
    public static void registerInstance() {
        if (instance == null) {
            synchronized (lock) {
                if (instance == null) {
                    instance = new ViewInjectorImpl();
                }
            }
        }
        x.Ext.setViewInjector(instance);
    }
}

Get the unique instance of ViewInjectorImpl and assign it to the ViewInjector object. Then call the ViewInjectorImpl.inject() method to parse the above two View annotations.

ViewInjectorImpl.inject()

public final class ViewInjectorImpl implements ViewInjector {

    @Override
    public void inject(Activity activity) {
        Class<?> handlerType = activity.getClass();
        try {
            // 获取ContentView标签,主要是为了获取ContentView.value(),即R.layout.xxx
            ContentView contentView = findContentView(handlerType);
            if (contentView != null) {
                // 获取R.layout.xxx
                int viewId = contentView.value();
                if (viewId > 0) {
                    // 获取setContentView()方法实例
                    Method setContentViewMethod = handlerType.getMethod("setContentView", int.class);
                    // 反射调用setContentView(),并设置R.layout.xxx
                    setContentViewMethod.invoke(activity, viewId);
                }
            }
        } catch (Throwable ex) {
            LogUtil.e(ex.getMessage(), ex);
        }
        // 遍历被注解的属性和方法
        injectObject(activity, handlerType, new ViewFinder(activity));
    }
}

Comments are added to almost every line, which should be clearer, so let's talk about it here. After the reflection of setContentView (), the role of the ContentView annotation is over. After all, the ContentView annotation has only one role: setting the Activity/Fragment layout.

ViewInjectorImpl.injectObject()

public final class ViewInjectorImpl implements ViewInjector {

    private static final HashSet<Class<?>> IGNORED = new HashSet<Class<?>>();

    static {
        IGNORED.add(Object.class);
        IGNORED.add(Activity.class);
        IGNORED.add(android.app.Fragment.class);
        try {
            IGNORED.add(Class.forName("android.support.v4.app.Fragment"));
            IGNORED.add(Class.forName("android.support.v4.app.FragmentActivity"));
        } catch (Throwable ignored) {
        }
    }

    private static void injectObject(Object handler, Class<?> handlerType, ViewFinder finder) {
            if (handlerType == null || IGNORED.contains(handlerType)) {
                return;
            }
            // 从父类到子类递归
            injectObject(handler, handlerType.getSuperclass(), finder);
            // 获取class中所有属性
            Field[] fields = handlerType.getDeclaredFields();
            if (fields != null && fields.length > 0) {
                for (Field field : fields) {
                    // 获取字段类型
                    Class<?> fieldType = field.getType();
                    if (
                /* 不注入静态字段 */     Modifier.isStatic(field.getModifiers()) ||
                /* 不注入final字段 */    Modifier.isFinal(field.getModifiers()) ||
                /* 不注入基本类型字段 */  fieldType.isPrimitive() ||
                /* 不注入数组类型字段 */  fieldType.isArray()) {
                        continue;
                    }
                    // 字段是否被ViewInject注解修饰
                    ViewInject viewInject = field.getAnnotation(ViewInject.class);
                    if (viewInject != null) {
                        try {
                            // 通过ViewFinder查找View
                            View view = finder.findViewById(viewInject.value(), viewInject.parentId());
                            if (view != null) {
                                // 暴力反射,设置属性可使用
                                field.setAccessible(true);
                                // 关联被ViewInject修饰的属性和View
                                field.set(handler, view);
                            } else {
                                throw new RuntimeException("Invalid @ViewInject for "
                                        + handlerType.getSimpleName() + "." + field.getName());
                            }
                        } catch (Throwable ex) {
                            LogUtil.e(ex.getMessage(), ex);
                        }
                    }
                }
            } // end inject view

            // 方法注解Event的解析,下文会讲
            ...
    }
}

Because Activity/Fragment may also have BaseActivity/BaseFragment. So injectObject() is a recursive method. The exit of the recursion lies in the top judgment and the classes whose parent class is not equal to the system. finder.findViewById(id,pid)The parameter id is R.id.xxx, and the pid defaults to 0. The code to find View in ViewFinder is as follows:

/*package*/ final class ViewFinder {

    public View findViewById(int id, int pid) {
        View pView = null;
        if (pid > 0) {
            pView = this.findViewById(pid);
        }

        View view = null;
        if (pView != null) {
            view = pView.findViewById(id);
        } else {
            view = this.findViewById(id);
        }
        return view;
    }

    public View findViewById(int id) {
        if (view != null) return view.findViewById(id);
        if (activity != null) return activity.findViewById(id);
        return null;
    }
}

Or through activity.findViewById(id)to find the control. The role of the View annotation is to write the findViewById line of code instead of us, which is generally used for agile development. At the cost of adding a reflection, every control will. Reflection is a way of sacrificing performance, so using View annotations has advantages and disadvantages.

event annotation

Event

/**
 * 事件注解.
 * 被注解的方法必须具备以下形式:
 * 1. private 修饰
 * 2. 返回值类型没有要求
 * 3. 参数签名和type的接口要求的参数签名一致.
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Event {

    /** 控件的id集合, id小于1时不执行ui事件绑定. */
    int[] value();
    /** 控件的parent控件的id集合, 组合为(value[i], parentId[i] or 0). */
    int[] parentId() default 0;
    /** 事件的listener, 默认为点击事件. */
    Class<?> type() default View.OnClickListener.class;
    /** 事件的setter方法名, 默认为set+type#simpleName. */
    String setter() default "";
    /** 如果type的接口类型提供多个方法, 需要使用此参数指定方法名. */
    String method() default "";
}

There are more properties in Event than View annotations. After all, Event also needs the process of findViewById, and also handles parameters, events, and so on. The default type property is View.OnClickListener.class, which is the click event.

public final class ViewInjectorImpl implements ViewInjector {

    private static void injectObject(Object handler, Class<?> handlerType, ViewFinder finder) {
            // 获取类中所有的方法
            Method[] methods = handlerType.getDeclaredMethods();
            if (methods != null && methods.length > 0) {
                for (Method method : methods) {
                    // 方法是静态或者不是私有则验证不通过
                    if (Modifier.isStatic(method.getModifiers())
                            || !Modifier.isPrivate(method.getModifiers())) {
                        continue;
                    }

                    //检查当前方法是否是event注解的方法
                    Event event = method.getAnnotation(Event.class);
                    if (event != null) {
                        try {
                            // R.id.xxx数组(可能多个控件点击事件共用同一个方法)
                            int[] values = event.value();
                            int[] parentIds = event.parentId();
                            int parentIdsLen = parentIds == null ? 0 : parentIds.length;
                            //循环所有id,生成ViewInfo并添加代理反射
                            for (int i = 0; i < values.length; i++) {
                                int value = values[i];
                                if (value > 0) {
                                    ViewInfo info = new ViewInfo();
                                    info.value = value;
                                    info.parentId = parentIdsLen > i ? parentIds[i] : 0;
                                    // 设置可反射访问
                                    method.setAccessible(true);
                                    EventListenerManager.addEventMethod(finder, info, event, handler, method);
                                }
                            }
                        } catch (Throwable ex) {
                            LogUtil.e(ex.getMessage(), ex);
                        }
                    }
                }
            } // end inject event
    }
}

Here is mainly to find the method modified by the Event annotation, and then set the accessible (method.setAccessible(true)), it seems that it is still a reflection call.

EventListenerManager.addEventMethod(finder, info, event, handler, method)

/*package*/ final class EventListenerManager {

    public static void addEventMethod(
            //根据页面或view holder生成的ViewFinder
            ViewFinder finder,
            //根据当前注解ID生成的ViewInfo
            ViewInfo info,
            //注解对象
            Event event,
            //页面或view holder对象
            Object handler,
            //当前注解方法
            Method method) {
        try {
            // 查找指定控件
            View view = finder.findViewByInfo(info);
            if (view != null) {
                // 注解中定义的接口,比如Event注解默认的接口为View.OnClickListener
                Class<?> listenerType = event.type();
                // 默认为空,注解接口对应的Set方法,比如setOnClickListener方法
                String listenerSetter = event.setter();
                if (TextUtils.isEmpty(listenerSetter)) {
                    // 拼接set方法名,例如:setOnClickListener
                    listenerSetter = "set" + listenerType.getSimpleName();
                }
                // 默认为""
                String methodName = event.method();
                boolean addNewMethod = false;
                DynamicHandler dynamicHandler = null;
                ...
                // 如果还没有注册此代理
                if (!addNewMethod) {
                    dynamicHandler = new DynamicHandler(handler);
                    dynamicHandler.addMethod(methodName, method);
                    // 生成的代理对象实例,比如View.OnClickListener的实例对象
                    listener = Proxy.newProxyInstance(
                            listenerType.getClassLoader(),
                            new Class<?>[]{listenerType},
                            dynamicHandler);

                    listenerCache.put(info, listenerType, listener);
                }
                // 获取set方法,例如:setOnClickListener
                Method setEventListenerMethod = view.getClass().getMethod(listenerSetter, listenerType);
                // 反射调用set方法。例如setOnClickListener(new OnClicklistener)
                setEventListenerMethod.invoke(view, listener);
            }
        } catch (Throwable ex) {
            LogUtil.e(ex.getMessage(), ex);
        }
    }

}

Use the dynamic proxy DynamicHandler to instantiate listenerType (for example: new OnClickListener), and then set events (such as click events, btn.setOnClickListener(new OnClickListener)) through reflection. After such a set of processes, I was surprised to find that the methods we defined seemed to have not been called at all! !

In fact, all the tricks are in the dynamic agent of DynamicHandler. Pay attention to a detail, when instantiating DynamicHandler, what is passed is Activity/Fragment. Then dynamicHandler.addMethod(methodName, method)when the method is called, the method (the current annotation method) is passed in. The full class name is there, and the method name is there. work together~

DynamicHandler

    public static class DynamicHandler implements InvocationHandler {
        // 存放代理对象,比如Fragment或view holder
        private WeakReference<Object> handlerRef;
        // 存放代理方法
        private final HashMap<String, Method> methodMap = new HashMap<String, Method>(1);

        private static long lastClickTime = 0;

        public DynamicHandler(Object handler) {
            this.handlerRef = new WeakReference<Object>(handler);
        }

        public void addMethod(String name, Method method) {
            methodMap.put(name, method);
        }

        public Object getHandler() {
            return handlerRef.get();
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            Object handler = handlerRef.get();
            if (handler != null) {
                String eventMethod = method.getName();
                method = methodMap.get(eventMethod);
                if (method == null && methodMap.size() == 1) {
                    for (Map.Entry<String, Method> entry : methodMap.entrySet()) {
                        if (TextUtils.isEmpty(entry.getKey())) {
                            method = entry.getValue();
                        }
                        break;
                    }
                }

                if (method != null) {

                    if (AVOID_QUICK_EVENT_SET.contains(eventMethod)) {
                        long timeSpan = System.currentTimeMillis() - lastClickTime;
                        if (timeSpan < QUICK_EVENT_TIME_SPAN) {
                            LogUtil.d("onClick cancelled: " + timeSpan);
                            return null;
                        }
                        lastClickTime = System.currentTimeMillis();
                    }

                    try {
                        return method.invoke(handler, args);
                    } catch (Throwable ex) {
                        throw new RuntimeException("invoke method error:" +
                                handler.getClass().getName() + "#" + method.getName(), ex);
                    }
                } else {
                    LogUtil.w("method not impl: " + eventMethod + "(" + handler.getClass().getSimpleName() + ")");
                }
            }
            return null;
        }
    }

First call method = methodMap.get(eventMethod), find the method name by key, we passed in "" before. For OnClickListener{ void onClick()}example, the key is searched by onClick, but of course it cannot be found. Then traverse the methodMap to set the method to the method name we defined in Activity/Fragment. if (AVOID_QUICK_EVENT_SET.contains(eventMethod))This line of code is to prevent rapid double-click, set the interval to 300ms, and finally call the method specifically annotated by Event in Activity/Fragment through reflection. Here it happens that OnClicklistener#onClick() is not called, but when OnClicklistener#onClick() is called, the method we define in Activity/Fragment is really called. Experience the process. There is one more thing to pay attention to here, because return method.invoke(handler, args)the return value needs to be returned at the end. Therefore, the return value of the method defined in Activity/Fragment must be the same as the return value of the target method (for example: onClick()).

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325543591&siteId=291194637