androidxutils3源码解析之注解模块(代码片段)

一口仨馍 一口仨馍     2022-11-30     235

关键词:

本文已授权微信公众号《非著名程序员》原创首发,转载请务必注明出处。

xUtils3源码解析系列

一. Android xUtils3源码解析之网络模块
二. Android xUtils3源码解析之图片模块
三. Android xUtils3源码解析之注解模块
四. Android xUtils3源码解析之数据库模块

初始化

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);
    

这里没有贴最开始的初始化x.Ext.init(this),因为这行代码的作用是获取ApplicationContext,而注解模块并不需要ApplicationContext。真正的初始化是在这里。实际上这里称作“初始化”有些不太合适,因为xUtils3中View注解都是@Retention(RetentionPolicy.RUNTIME)类型的,运行时才是真正的初始化,x.view().inject(this)是解析注解的地方。注解一共就这俩部分,先姑且这么称呼吧。下文以x.view().inject(this)为例进行分析,Fragment中和这个属于殊途同归,不再赘述。

View注解

注解的作用只能是“标志”,如果注解里定义的有属性,那么还能获取属性具体的值。属性的值没有default值,那么使用注解时此属性为必填项。反之亦反。我们先看下两个View注解ContentView和ViewInject的具体实现,之后统一查看注解解析相关代码。

ContentView标签

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

ContentView注解修饰的对象范围为TYPE(用于描述类、接口或enum声明),保留的时间为RUNTIME(运行时有效),此外还定义了一个属性value,注意:是属性,不是方法。

ViewInject

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

    int value();

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

ViewInject注解修饰的对象范围为FIELD(用于描述属性),保留的时间为RUNTIME(运行时有效)。

View注解解析

在Activity或者Fragment中首先要做的就是初始化xUtils3注解,即x.view().inject(this)。前文也说过:这个过程实际是View注解解析的过程。下面就以这一过程跟进。

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);
    

获取ViewInjectorImpl唯一实例,并赋值给ViewInjector对象。之后调用ViewInjectorImpl.inject()方法解析上面两个View注解。

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));
    

几乎每行都添加了注释,应该比较清晰了,这里还是大概说下吧。在反射setContentView ()之后,ContentView注解的作用就结束了,毕竟ContentView注解的作用只有一个:设置Activity/Fragment布局。

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的解析,下文会讲
            ...
    

因为Activity/Fragment可能还有BaseActivity/BaseFragment。所以injectObject()是个递归方法,递归的出口在于最上面的判断,及父类不等于系统的那几个类。finder.findViewById(id,pid)参数id为R.id.xxx,pid默认为0。在ViewFinder中查找View的代码如下:

/*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;
    

还是通过activity.findViewById(id)来查找控件的。View注解的作用是代替我们写了findViewById这行代码,一般用于敏捷开发。代价是增加了一次反射,每个控件都会。而反射是比较牺牲性能的做法,所以使用View注解算是有利有弊吧。

事件注解

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 "";

Event中的属性,比View注解要多一些,毕竟Event也需要findViewById过程,并且还要处理参数,事件等等。默认type属性为View.OnClickListener.class,即点击事件。

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
    

这里主要是查找被Event注解修饰的方法,之后设置可访问(method.setAccessible(true)),看样子还是反射调用咯。

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);
        
    

使用动态代理DynamicHandler实例化listenerType(例如:new OnClickListener),之后通过反射设置事件(例如点击事件,btn.setOnClickListener(new OnClickListener))。这么一套流程流程下来,我惊讶的发现,我们定义的方法好像完全没被调用!!

其实猫腻都在DynamicHandler这个动态代理中。注意一个细节,在实例化DynamicHandler的时候穿递的是Activity/Fragment。然后调用dynamicHandler.addMethod(methodName, method)方法的时候,将method(当前注解方法)传递进去了。完整类名有,方法名字有。齐活儿~

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;
        
    

首先调用method = methodMap.get(eventMethod),由key查找方法名,之前我们传进来的是“”。以OnClickListener void onClick()为例,由onClick为key查找,当然查找不到咯。然后遍历methodMap设置method为我们在Activity/Fragment中定义的方法名。if (AVOID_QUICK_EVENT_SET.contains(eventMethod))这行代码是防止快速双击的,设置间隔为300ms,最后通过反射调用在Activity/Fragment中特定被Event注解的方法。这里巧在没有调用OnClicklistener#onClick(),而是在调用OnClicklistener#onClick()的时候,真正调用的是我们在Activity/Fragment中定义的方法。体会一下这个过程。这里还需要注意一个地方,因为return method.invoke(handler, args),最后需要return返回值。所以在Activity/Fragment中定义方法的返回值,必须要和目标方法(例如:onClick())的返回值一样。

androidxutils3源码解析之注解模块(代码片段)

...首发,转载请务必注明出处。xUtils3源码解析系列一.AndroidxUtils3源码解析之网络模块二.AndroidxUtils3源码解析之图片模块三.AndroidxUtils3源码解析之注解模块四.AndroidxUtils3源码解析之数据库模块初始化publicclassBaseActivityextendsAppCompat... 查看详情

androidxutils3源码解析之图片模块(代码片段)

...首发,转载请务必注明出处。xUtils3源码解析系列一.AndroidxUtils3源码解析之网络模块二.AndroidxUtils3源码解析之图片模块三.AndroidxUtils3源码解析之注解模块四.AndroidxUtils3源码解析之数据库模块初始化x.Ext.init(this);publicstaticvoidinit(A... 查看详情

androidxutils3源码解析之数据库模块(代码片段)

...首发,转载请务必注明出处。xUtils3源码解析系列一.AndroidxUtils3源码解析之网络模块二.AndroidxUtils3源码解析之图片模块三.AndroidxUtils3源码解析之注解模块四.AndroidxUtils3源码解析之数据库模块配置数据库DbManager.DaoConfigdaoConfig=... 查看详情

androidxutils3源码解析之数据库模块(代码片段)

...首发,转载请务必注明出处。xUtils3源码解析系列一.AndroidxUtils3源码解析之网络模块二.AndroidxUtils3源码解析之图片模块三.AndroidxUtils3源码解析之注解模块四.AndroidxUtils3源码解析之数据库模块配置数据库DbManager.DaoConfigdaoConfig=... 查看详情

androidxutils3源码解析之图片模块(代码片段)

...首发,转载请务必注明出处。xUtils3源码解析系列一.AndroidxUtils3源码解析之网络模块二.AndroidxUtils3源码解析之图片模块三.AndroidxUtils3源码解析之注解模块四.AndroidxUtils3源码解析之数据库模块初始化x.Ext.init(this);publicstaticvoidinit(A... 查看详情

arouter之注解处理器

...成路由、路由表等java文件。注解处理器是在编译期解析源码中的注解信息,在根据规则、模板自动生成相应的java文件,那么注解处理器是在编译期的哪个阶段执行呢?下图是AndroidGradle源码编译的大概流程:Gradle源码编译是会调... 查看详情

spring注解之@import用法解析(代码片段)

前言:最近在回顾阅读Springboot源码时发现框架层面大量使用@Import注解,特别是Springboot自动装配机制更是大量使用该注解,搜索部分结果图如下。简单来说就是Springboot中用到了Spring中的@Import注解来帮助实现自... 查看详情

zookeeper源码之服务端启动模块

  服务端启动模块主要负责解析配置文件,启动服务器监听并执行zookeeper命令。类图  QuorumPeerMain  QuorumPeerMain是服务端主程序,主要功能是解析配置文件,启动zookeeper服务。内部使用QuorumPeerConfig来解析配置文件;使用Quor... 查看详情

深入理解stream之foreach源码解析

...ambda的奥秘深入理解Stream之原理剖析深入理解Stream之foreach源码解析深入浅出NPE神器Optional谈谈接口默认方法与静态方法深入浅出重复注解与类型注解深入浅出JVM元空间metaspace深入理解Complet 查看详情

floodlight之forwarding模块源码解析

...流表项”这个过程为例,分析floodlight中forwarding模块源码。 说明:floodlight采用事件驱动的异步框架。有三个基本组件module、service和listener,floodli 查看详情

mybatis源码分析之@resultmap注解详解(代码片段)

MyBatis源码分析之@ResultMap注解详解在前一篇文章讲**@MapKey注解时,我原想将@ResultMap注解也一起拿出来说一下,但是发现@ResultMap解析加载源码非常多,想想就不在一篇文章中讲了,分开单独来说,这... 查看详情

springboot之启动原理解析及源码阅读

原文地址:https://www.cnblogs.com/shamo89/p/8184960.html正文我们开发任何一个SpringBoot项目,都会用到如下的启动类@SpringBootApplication                                      //Annotation(注解)... 查看详情

zookeeper源码之客户端

...启动模块  启动程序,接收和解析命令行。详见zookeeper源码之客户端启动模块。核心执行模块  客户端操作ZooKeeper服务端的核心类,详见zookeeper源码之客户端核心执行模块。    类图  ZooKeeper  ZooKeepe 查看详情

nacos源码之auth(权限)模块-1(授权过滤器与控制器缓存)(代码片段)

下一篇:《nacos源码之Auth(权限)模块-2(权限管理与权限配置)》Nacos的Auth模块Nacos的Auth模块授权过滤器(权限核心注解)注解实现讲解权限操作类型控制器缓存获取方法初始化两个对象表Naco读后感上一篇《nacos源码构建与总览》浏览器... 查看详情

spring源码之@import

@Import注解的作用为解析指定的class类,如果是简单的java类就直接作为一个bean放入容器中,如果是@Configuration就正常当做配置类解析,如果实现了ImportSelector接口,就会调用selectImports接口方法将返回的字符串数组对应的class加载进... 查看详情

spring源码解析之ioc容器(代码片段)

  学习优秀框架的源码,是提升个人技术水平必不可少的一个环节。如果只是停留在知道怎么用,但是不懂其中的来龙去脉,在技术的道路上注定走不长远。最近,学习了一段时间的spring源码,现在整理出来,以便日后温故知... 查看详情

xutils3源码阅读之网络模块

关于xUtils3xUtils包含了很多实用的android工具.xUtils支持超大文件(超过2G)上传,更全面的http请求协议支持(11种谓词),拥有更加灵活的ORM,更多的事件注解支持且不受混淆影响…xUtils最低兼容Android4.0(apilevel14).(Android2.3?)xUtils3变化较... 查看详情

springboot2从入门到入坟|请求参数处理篇:源码分析之各种类型参数解析原理

在前面几讲,我花了些时间为大家详细介绍了一些SpringMVC底层常用的参数注解,想必大家也能熟练使用它们了。在使用这些常用参数注解的过程中,不知大家有没有过这样一个疑惑,即为何我们只需要给Controller中... 查看详情