Java注解、反射,安卓IOC(二)

在项目开发中,大部分 Android 开发者都使用过 ButterKnife 这个通过注解简化代码的框架。部分 Android 开发者也使用过 xUtils 这种快速开发的框架。
两者均使用到了注解。在上一篇 Java注解、反射,安卓IOC(一) 中我们知道了注解及反射的用法。本篇会分别介绍下其实现机制。

运行时注解

首先我们自己简单实现类似 xUtils 这种运行时注解框架。

绑定 View 控件

创建注解:


@Retention(RetentionPolicy.RUNTIME)//运行时注解
@Target(ElementType.FIELD)//Target为属性
public @interface FindView {
    int value() default -1;
}

View解析代码:


public class ViewInject {

    public static void bind(Activity activity) {
        inject(new ViewFinder(activity), activity);
    }

    public static void bind(View view) {
        inject(new ViewFinder(view), view);
    }

    public static void bind(View view, Object obj) {
        inject(new ViewFinder(view), obj);
    }

    private static void inject(ViewFinder finder, Object obj) {
        injectFields(finder, obj);
        injectMethods(finder, obj);
    }

    private static void injectFields(ViewFinder finder, Object obj) {
        Class<?> clazz = obj.getClass();
        Field[] fields = clazz.getDeclaredFields();//获取所有变量
        for (Field field : fields) {
            if (field.isAnnotationPresent(FindView.class)) {
                FindView findView = field.getAnnotation(FindView.class);//获取注解
                if (findView.value() < 0) {
                    throw new IllegalArgumentException("The id can't be -1.");
                } else {
                    View view = finder.findViewById(findView.value());
                    try {
                        field.setAccessible(true);//破坏封装
                        field.set(obj, view); //设置属性
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}

主要流程为通过反射获取并遍历所有变量,若变量被注解修饰,则将注解的 ID 赋值给指定方法并调用。

绑定 OnClick 事件

创建注解:


@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD) //Target为方法
public @interface OnClick {
    int[] id();
}

OnClick 事件注入:


private static void injectMethods(ViewFinder finder, final Object obj) {
    Method[] methods = obj.getClass().getDeclaredMethods();
    for (final Method method : methods) {
        if (method.isAnnotationPresent(OnClick.class)) {
            OnClick onClick = method.getAnnotation(OnClick.class);
            if (onClick.id().length != 0) {
                for (int i : onClick.id()) {
                    View view = finder.findViewById(i);
                    method.setAccessible(true);
                    view.setOnClickListener(new View.OnClickListener() {
                        @Override
                        public void onClick(View v) {
                            try {
                                method.invoke(obj, v);
                            } catch (IllegalAccessException e) {
                                e.printStackTrace();
                            } catch (InvocationTargetException e) {
                                e.printStackTrace();
                            }
                        }
                    });
                }
            }
        }
    }
}

主要流程为通过反射获取并遍历所有方法,若方法被注解修饰,遍历所有的 ID ,将注解的 ID 赋值给 findViewById 方法,然后在 setOnClickListener 调用 method 方法。

在 Activity 中的使用:


public class IocActivity extends AppCompatActivity {

    @FindView(R.id.txt_ioc_test)
    private TextView mTxtTest;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_ioc);
        ViewInject.bind(this);
        mTxtTest.setText("测试");
    }

    @OnClick(id = {R.id.btn_ioc_test, R.id.btn_ioc_test2})
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.btn_ioc_test:
                Toast.makeText(this, "Click1", Toast.LENGTH_SHORT).show();
                break;
            case R.id.btn_ioc_test2:
                Toast.makeText(this, "Click2", Toast.LENGTH_SHORT).show();
                break;
        }
    }
}

以上为运行时注解的简单实现,即 xUtils 使用的注解方法。但是这种方法因为通过一系列反射获取属性、方法等,对性能会有所影响,所以不建议在实际项目中使用,下边介绍下轻量级的编译时注解。

编译时注解

ButterKnife 源码解析网上已经有很多不错的文章了,例如这篇 ButterKnife源码分析 讲的就很好。这里主要介绍下自己的大致实现以及编译时注解在 Android Studio 中的使用。

首先介绍下大概的项目结构,如下图所示:

APT结构.png
  • annotation module: Java library - 定义一系列注解。
  • injetc module: Android library - 定义注解的接口及调用方法。
  • compiler module: Java library - 自定义编译时注解 AbstractProcessor 在编译期间生成 java 代码。
  • app: 使用方法。

本篇文章主要为介绍及学习,所以此处仅实现 setContentView 的编译时注解。

1、声明注解


@Retention(RetentionPolicy.CLASS) //编译时注解
@Target(ElementType.TYPE) //修饰类
public @interface ContentView {
    int value();
}

2、声明外界接口及方法

声明接口:


public interface ContentInjector<T> {
    void injectContent(T obj, Activity activity); //此处仅用 Activity 参数即可实现文章的 demo
}

提供方法:


public class ContentViewInject {
    public static void bind(Activity activity) {//绑定
        injectContentView(activity);
    }
    private static void injectContentView(Activity activity) {
        Class<? extends Activity> clazz = activity.getClass();
        try {
            ContentInjector injector = (ContentInjector) Class.forName(clazz.getName()
                    + "$$ViewBinder").newInstance();
            injector.injectContent(activity, activity);
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

以上代码为简单使用,butterknife 中的 UnBinder 解绑,使用 Map 缓存等暂不考虑。
此处的 Class.forName("") 以及 class.newInstance() 会对性能略有影响,butterknife 在此进行了 map 缓存优化。

3、自定义 AbstractProcessor,此处需将 module 设置为 Java library 才可继承 AbstractProcessor。


@AutoService(Processor.class)
@SupportedSourceVersion(value = SourceVersion.RELEASE_7)
public class ContentViewInjectProcessor extends AbstractProcessor {


    //可用 @SupportedAnnotationTypes("com.lauzy.ContentView") 注解 ContentViewInjectProcessor 代替
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> supportTypes = new LinkedHashSet<>();
        supportTypes.add(ContentView.class.getCanonicalName());
        return supportTypes;
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        String packageName;
        String className;
        //遍历每个被 ContentView 修饰的 class 文件
        for (Element element : roundEnvironment.getElementsAnnotatedWith(ContentView.class)) {
            if (element.getKind() == ElementKind.CLASS) {
                TypeElement typeElement = (TypeElement) element;
                PackageElement packageEle = (PackageElement) element.getEnclosingElement();
                packageName = packageEle.getQualifiedName().toString();//获取包名
                //因为是 ElementKind.CLASS 类型,所以可以直接强制转换,获取类名
                className = typeElement.getSimpleName().toString();

                int layoutId = typeElement.getAnnotation(ContentView.class).value();//获取注解的 id
                
                //拼接 Java 类的字符串
                StringBuilder builder = new StringBuilder();
                builder.append("package ").append(packageName).append(";\n");
                builder.append("import android.view.View;\n");
                builder.append("import android.app.Activity;\n");
                builder.append("import com.freedom.lauzy.inject.ContentInjector;\n");
                builder.append('\n');

                builder.append("public class ").append(className + "$$ViewBinder");
                builder.append("<T extends ").append(className).append(">");
                builder.append(" implements ContentInjector<T>");
                builder.append(" {\n");
                builder.append("@Override\n")
                        .append("public void injectContent(final T source, Activity activity) {\n");
                builder.append("    ((Activity) source).setContentView(" + layoutId);
                builder.append(");\n");
                builder.append("}\n\n}\n");


                //写入 Java 文件
                try {
                    JavaFileObject fileObject = processingEnv.getFiler().createSourceFile(
                            packageName + "." + className + "$$ViewBinder",
                            typeElement);
                    Writer writer = fileObject.openWriter();
                    writer.write(builder.toString());
                    writer.flush();
                    writer.close();
                } catch (IOException e) {
                    e.printStackTrace();
                    System.out.println("error");
                }
            }
        }
        return true;
    }
}

butterknife 中使用了 javapoet 生成 Java 代码文件

此 module 的 gradle 配置如下:


apply plugin: 'java'

dependencies {
    compile fileTree(include: ['*.jar'], dir: 'libs')
    compile 'com.google.auto.service:auto-service:1.0-rc2' // google 的生成源代码库
    compile project(':annotation')
}

sourceCompatibility = "1.7"
targetCompatibility = "1.7"

4、app 使用

app 的 gradle 配置如下:


dependencies {
    ...
    annotationProcessor project(':compiler')
    compile project(':annotation')
    compile project(':inject')
}

activity 中使用:


@ContentView(R.layout.activity_ioc)
public class IocActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ContentViewInject.bind(this);
    }
}

这样,整个流程就结束了。此时 build 整个项目则会在 app/build/generated/source/apt/debug/com.lauzy.freedom.lauzycode/IOC 文件夹下生成一个IocActivity$$ViewBinder 的 Java 文件,代码如下:


package com.lauzy.freedom.lauzycode.IOC;
import android.app.Activity;
import com.freedom.lauzy.inject.ContentInjector;

public class IocActivity$$ViewBinder<T extends IocActivity> implements ContentInjector<T> {
@Override
public void injectContent(final T source, Activity activity) {
    ((Activity) source).setContentView(2130968606);
}
}

可以看到,其实是在编译时生成了一个 Java 文件,并在 activity 的 onCreate 方法中调用了 setContentView 方法。

注意事项:

1、若不使用 com.google.auto.service:auto-service:1.0-rc2 这个 google 的生成源代码库,则需要手动创建一个 META_INF 文件来指定注解。
需要在 compiler 中创建 compiler/src/main/resources/META-INF/services 目录(注意 META-INF 中间不是下划线,减号即可),并新建 txt 文件 javax.annotation.processing.Processor,
文件中写入自定义 AbstractProcessor 的全名称,如: com.lauzy.ContentViewInjectProcessor 。多个的话换行写入。

如下图所示:


META_INF

2、本文使用 annotationProcessor 的注解处理器代替 android-apt ,Google 内置的注解处理器,建议使用。

分析

编译时注解的优点 :在于对性能影响很小的情况下,大量简化程序员的代码,像 butterknife 在首次查找类的时候对性能稍有影响,其他情况下影响微乎其微。
编译时注解的缺点 :build 过程生成更多的代码,增加了类和方法的数量;对性能影响很小,但是多少会有的。

本人认为编译时注解在优化代码,提高效率方面是有很大优势的,远远大于其缺点。

本篇文章主要是分析及梳理大致的实现方式,仅为学习使用,所有代码均托管在 我的Github 上。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,718评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,683评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,207评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,755评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,862评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,050评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,136评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,882评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,330评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,651评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,789评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,477评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,135评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,864评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,099评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,598评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,697评论 2 351

推荐阅读更多精彩内容

  • 什么是注解注解分类注解作用分类 元注解 Java内置注解 自定义注解自定义注解实现及使用编译时注解注解处理器注解处...
    Mr槑阅读 1,071评论 0 3
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,858评论 25 707
  • 一、什么是注解? 注解对于开发人员来讲既熟悉又陌生,熟悉是因为只要你是做开发,都会用到注解(常见的@Overrid...
    _Justin阅读 1,351评论 0 10
  • 读这本书时没有按照书的顺序读,而是捡着目录中感兴趣的章节读的。这里我想说明一下,我把书分为两类,一类是有趣的,一类...
    七月下雨天阅读 204评论 0 0
  • 在春风清曲的水流中 千年回首弹指过 如同去年撒下的花籽 已然在跟我诉说它和阳光,雨露,土地的故事 我最悔的不是不曾...
    齐鲁师范学院王浩宇阅读 272评论 1 3