美文网首页Android知识Android开发Android开发经验谈
如何实现自定义Java编译时注解功能--定制自己的Android

如何实现自定义Java编译时注解功能--定制自己的Android

作者: 皮球二二 | 来源:发表于2016-06-04 22:11 被阅读853次

    上一篇,我们基本了解了整个编译时注解的实现流程,现在我们就要开始来真正写一个有意义的功能了

    本文最终demo已经上传到github上,欢迎大家star,follow

    再次声明本文参考了鸿洋的demo

    项目结构

    同之前一样你要创建一个Java Library,但是这里你需要将android.jar拷贝进libs里面,因为我们这里需要使用到android的相关类

    Library的编写

    首先你需要将android.jar拷贝到java Library里面来,因为我们需要一些android的类

    1 注解类的实现 ViewInject.java

    @Retention(RetentionPolicy.CLASS)
    @Target({ElementType.FIELD, ElementType.TYPE})
    public @interface ViewInject {    
        int value();
    }
    

    我们将通过注解的id进行相应的处理

    2 具体实现的功能 Finder.java

    public enum Finder {    
        Activity {        
            @Override        
            public void setContentView(Object activity, int id) {            
                ((Activity) activity).setContentView(id);        
           }    
        };    
        public abstract void setContentView(Object activity, int id);
    }
    

    本次功能主要是针对Activity去实现setContentView的功能。在枚举类里面定义了抽象方法,这样枚举对象就一定要实现才行。这样写的好处是方便扩展,后续扩展其他功能以及支持Fragment、View的话比较方便

    3 动态生成类功能接口 InjectInterface.java

    public interface InjectInterface<T> {    
        void inject(Finder finder, T target);
    }
    

    每一个动态生成的类需要实现这个接口,这样activity就可以直接通过这个接口使用Finder来实现真正的setContentView功能

    4 编译时执行类的实现 ViewInjectProcessor.java

    提前介绍ProxyInfo.java,这个类存储着Activity的信息,我们声明一个map对象去用键值对存储不同Activity的信息,做到不混乱

    //存储需要循环生成的注解类信息
    HashMap<String, ProxyInfo> proxyInfoHashMap;
    
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {    
        super.init(processingEnv);    
        proxyInfoHashMap=new HashMap<>(); 
    }
    

    在初始化方法里面初始化一下

    下面开始重写process方法

    首先定义三个对象用来存储完整类路径、类名、包名

    String fqClassName, className, packageName;
    

    fqClassName是用来当键用的,因为包名或者类名都有可能出现重复的情况
    包名跟类名是后续ProxyInfo需要使用的

    for (Element element : roundEnv.getElementsAnnotatedWith(ViewInject.class)) {
        if (element.getKind()== ElementKind.CLASS) {
            TypeElement classElement= (TypeElement) element;
            PackageElement packageElement= (PackageElement) classElement.getEnclosingElement();
            fqClassName=classElement.getQualifiedName().toString();
            className=classElement.getSimpleName().toString();
            packageName=packageElement.getQualifiedName().toString();
            int layoutId=classElement.getAnnotation(ViewInject.class).value();
        }
    }
    

    遍历获得ViewInject下的所有被注解class的信息,同时获取到注解内容的值,也就是view的id。这边通过之前的学习,应该没有什么难度看不懂了

    if (proxyInfoHashMap.containsKey(fqClassName)) {    
        proxyInfoHashMap.get(fqClassName).setLayoutId(layoutId);
    } else {    
        ProxyInfo proxyInfo=new ProxyInfo(packageName, className);    
        proxyInfo.setClassElement(classElement);    
        proxyInfo.setLayoutId(layoutId);    
        proxyInfoHashMap.put(fqClassName, proxyInfo);
    }
    

    把每次取到的classElement信息放到那个map里面去,等全部加完之后,我们要开始手动去创建class文件了,这些文件就是提供给activity里面去操作的方法

    5 存储Activity信息的类 ProxyInfo.java

    public class ProxyInfo {    
        //原始包名    
        String packageName;    
        //注解上的class名称    
        String className;    
        //生成注解类名称    
        String proxyClassName;    
        //类元素    
        TypeElement classElement;    
        //布局Id    
        int layoutId;    
        public static final String PROXY = "PROXY";    
        public ProxyInfo(String packageName, String className) {        
            this.packageName = packageName;        
            this.className = className;        
            //设置新生成的注解类名        
            this.proxyClassName=className+"$$"+PROXY;    
        }    
        public TypeElement getClassElement() {        
            return classElement;    
        }    
        public void setClassElement(TypeElement classElement) {        
            this.classElement = classElement;    
        }    
        public int getLayoutId() {        
            return layoutId;    
        }    
        public void setLayoutId(int layoutId) {        
            this.layoutId = layoutId;    
        }    
        public String getProxyClassName() {        
            return proxyClassName;    
        }    
    
        /**     
        * 创建模板代码     
        * @return     
        */    
        public String generateJavaCode() {        
            StringBuilder builder = new StringBuilder();        
            builder.append("// Generated code from ViewInjectProcessor. Do not modify!\n"); 
            builder.append("package ").append(packageName).append(";\n\n");        
            builder.append("import com.example.annotation.Finder;\n");        
            builder.append("import com.example.annotation.InjectInterface;\n");        
            builder.append('\n');        
            builder.append("public class ").append(proxyClassName);        
            builder.append("<T extends ").append(getClassElement()).append(">");        
            builder.append(" implements InjectInterface<T>");        
            builder.append(" {\n\n");        
            generateInjectMethod(builder);        
            builder.append("\n}\n");        
            return builder.toString();    
        }    
    
        /**     
        * 具体方法实现代码     
        */    
        private void generateInjectMethod(StringBuilder builder) {        
            builder.append("  @Override\n");        
            builder.append("  public void inject(Finder finder, T target) {");        
            if (layoutId > 0) {            
                builder.append("    finder.setContentView(target, "+layoutId+");\n");        
            }        
            builder.append("\n  }\n");    
        }
    }
    

    其实虽然代码看起来好像不少,但是实际上很简单,就是在class包下面创建好相应的类class
    生成的代码是这样的

    //
    // Source code recreated from a .class file by IntelliJ IDEA
    // (powered by Fernflower decompiler)
    //
    package com.rg.annotationdemo;
    
    import com.example.annotation.Finder;
    import com.example.annotation.InjectInterface;
    import com.rg.annotationdemo.MainActivity;
    
    public class MainActivity$$PROXY<T extends MainActivity> implements InjectInterface<T> {    
        public MainActivity$$PROXY() {    
        }    
        public void inject(Finder finder, T target) {        
            finder.setContentView(target, 2130968601);    
        }
    }
    

    只要能调用到这个方法,那么setContentView的功能就能实现


    动态class生成

    6 提供给Activity传递参数的类 ButterKnife.java

    public class ButterKnife {    
        static final Map<Class<?>, InjectInterface<Object>> INJECTORS = new LinkedHashMap<>();    
        public void bind(Activity activity) {        
            InjectInterface<Object> injectInterface=findInjector(activity);     
            injectInterface.inject(Finder.Activity, activity);    
        }    
    
        private InjectInterface<Object> findInjector(Object object) {        
            Class class_=object.getClass();        
            InjectInterface<Object> injectInterface=INJECTORS.get(class_);        
            if (injectInterface==null) {            
                try {               
                    Class injectClass=Class.forName(class_.getName()+"$$"+ProxyInfo.PROXY);
                    injectInterface= (InjectInterface<Object>) injectClass.newInstance();    
                    INJECTORS.put(class_, injectInterface);            
                } catch (ClassNotFoundException e) {                
                    e.printStackTrace();            
                } catch (InstantiationException e) {                
                    e.printStackTrace();            
                } catch (IllegalAccessException e) {                
                    e.printStackTrace();            
                }        
            }        
            return injectInterface;    
        }
    }
    

    这个类其实也是很简单就能理解的,通过反射将之前动态生成的类找到,然后传递参数Finder跟Activity,再调用inject方法,完成setContentView的真正实现

    Activity的使用

    通过上述步骤,我们就完成了编译时注解,同上篇文章一样,打个包丢进去

    @ViewInject(R.layout.activity_main)
    public class MainActivity extends AppCompatActivity {    
        TextView textview;    
        @Override    
        protected void onCreate(Bundle savedInstanceState) {      
            super.onCreate(savedInstanceState);        
            ButterKnife butterKnife=new ButterKnife();        
            butterKnife.bind(this);    
        }    
    
        public void click() {        
            Toast.makeText(this, "HHH", Toast.LENGTH_SHORT).show();    
        }
    }
    

    实际效果不截图了,也没什么意思,无非就是一个页面而已,大家可以根据我的demo自行尝试

    注意点

    没事多gradle clean/gradle build,有些不能理解的异常,比如说动态生成的类不符合JUnit文件规范之列的,都可以通过clean build解决

    相关文章

      网友评论

        本文标题:如何实现自定义Java编译时注解功能--定制自己的Android

        本文链接:https://www.haomeiwen.com/subject/srfldttx.html