Android:写一个专注的编译时注解框架——ContentViewAnnotation

601 阅读5分钟

【转载请注明出处】
作者:DrkCore
原文:blog.csdn.net/drkcore/art…

Android 开发从业人员来说 setContentView()findViewById() 这两个方法肯定不会陌生,从我们写下第一行 android 代码开始到使用 Afinal、xUtils 等基于反射和动态注解的框架简化代码,再到最求性能使用基于编译时注解的 ButterKnife,可谓是一路伴随成长。

不过本文倒不是用来讲述视图注解框架的发展历史的,而是因为一个纠结的问题让笔者决定开发一个自己的框架出来。事情是这样的:

原先笔者一直使用 xUtils3 的视图注解来减少这些繁杂的代码,近日新开一个项目时便打定主意转用 ButterKnife,但在引入的过程中就碰到了问题——ButterKnife 并框架没有 ContentView 注解!

翻找文档笔者终于在 GitHub 的 ISSUE#8 中找到了原因:

ISSUE#8

大体的意思是添加 ContentView 注解功能只能节约一行,实际意义并不是很大,在 ISSUE 的最后原作者表示放弃实现这个需求:

GivingUp

对于作者的想法我们暂且不论,代码总归还是要写的,而当笔者在新项目中写下一个 Fragment 代码时……

public class BookFrag extends Fragment {

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
        return inflater.inflate(R.layout.frag_book, container, false);
    }

}

可以看到其中实际有作用的代码就一行,本来看多了也就习惯了,但是没有对比就没有伤害:

@ContentView(R.layout.frag_book)
public class BookFrag extends BaseFrag {// 在BaseFrag中实例化了视图
}

可以看到代码中没有一个字符是多余的。用更少的代码干更多的事,省下来的时间用于享受生活,这才是一个程序员应该干的事情!

说干就干。如果使用运行时注解开发的话倒是很简单,笔者曾写过一篇 xUtils3 视图注解模块的浅析(传送门),照搬也能解决问题。但已经使用 ButterKnife 了,索性就自己开发了一个基于编译时注解的框架来解决问题。

框架开发

网络上有很多讲编译时注解的博客,还没掌握相关知识点的同学可以先行了解一下。

这个框架很精简,一共也只 2 个类 100+ 行的代码。

首先是注解类:

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

注解只有一个方法,用于标记 layout id,接着是注解处理器:

@AutoService(Processor.class)
public class ContentViewProcessor extends AbstractProcessor {

    //省略部分代码

    //生成类的包路径
    public static final String API_PKG = "core.annotation.view";
    public static final String API_NAME = "ContentViews";
    public static final String API_PATH = API_PKG + "." + API_NAME;

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        // 收集所有 ContentView 注解保存到 map
        Map<String, Integer> map = new HashMap<>();
        Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(ContentView.class);
        for (Element element : elements) {
            TypeElement type = (TypeElement) element;
            ContentView annotation = type.getAnnotation(ContentView.class);
            String name = type.getQualifiedName().toString();
            map.put(name, annotation.value());
        }

        // 生成代码并写入文件
        try {
            JavaFileObject sourceFile = filer.createSourceFile(API_PATH);
            Writer writer = sourceFile.openWriter();
            writer.write(generateCode(map));
            writer.flush();
            writer.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

        return true;
    }

    // 构建代码,这里省略了不少代码,实际效果和注释请查看下方生成的类
    public String generateCode(Map<String, Integer> map) {
        StringBuilder builder = new StringBuilder();

        // 使用 map 保存注解值
        builder.append("    private static final Map<Class, Integer> map = new HashMap<>(").append(map.size()).append(");").append("\n\n");

        // 添加获取 layout id 的方法
        builder.append("    public static int get(Object obj) {").append("\n");
        builder.append("    }").append("\n\n");

        // 在静态初始化块中将 Class 和 layout id 的键值对填入 map
        builder.append("    static {").append("\n");
        for (Map.Entry<String, Integer> entry : map.entrySet()) {
            builder.append("        map.put(").append(entry.getKey()).append(".class, ").append(entry.getValue()).append(");").append("\n");
        }
        builder.append("    }").append("\n\n");

        builder.append("}\n");

        return builder.toString();
    }
}

实际生成的代码如下,中文注释是博客中添加上去的:

/*Generated code. Do not modify!!!*/
package core.annotation.view;

import java.util.HashMap;
import java.util.Map;

// Class generated by ContentViewAnnotation
// Fork or star me on GitHub: https://github.com/DrkCore/ContentViewAnnotation
public class ContentViews {

    // 使用 Map<Class, Integer>,由于用作 key 的是 Class 对象而不是类的路径所以该框架不受代码混淆的影响
    private static final Map<Class, Integer> map = new HashMap<>(21);

    // Get ContentView layout id
    // You are supposed to call this method only in the main thread.
    // 由于实例化布局肯定是在主线程所以不考虑并发的问题
    public static int get(Object obj) {
        Integer id = map.get(obj.getClass());
        if (id == null) {// 子类无映射,查找父类
            Class clz = obj.getClass();
            Class parent = clz;
            while (id == null && (parent = parent.getSuperclass()) != null) {
                id = map.get(parent);
            }
            if (id == null) {
                id = 0;
            }
            // Cache result
            // 保存为子类的值
            map.put(clz, id);
        }
        return id;
    }

    static {// 省略部分添加键值对的逻辑
        map.put(core.demo.app.MainActivity.class, 2130968616);
    }
}

性能

HashMap 的查找时间是常数级的,所以我们可以认为这样的实现方式对运行时的性能 0 影响

不过现在但凡使用了反射的框架都会进行相应的缓存,而且初始化视图也只运行一次而只执行一次的反射操作性能几乎和直接调用没什么差别了,所以相比旧的框架而言该框架的提升并不会很明显。

但是如此专注的框架你见过吗?它仍然有其存在的价值,值得一用。

如何使用

在 Module 的 build.gradle 文件中添加如下依赖:

dependencies {
    compile 'core.mate:contentview-annotation:1.0.0'
    annotationProcessor 'core.mate:contentview-compiler:1.0.0'
}

在类中使用 @ContentView 注解:

@ContentView(R.layout.activity_first)
public class FirstActivity extends BaseActivity {
}

Build 后注解处理器会自动生成可以用于获取 layout id 的辅助类 ContentViews ,接着在你的 BaseActivity 实例化布局即可:

import core.annotation.view.ContentViews;

public abstract class BaseActivity extends FragmentActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(ContentViews.get(this));
    }
}

需要注意的是在第一次集成该框架时或者 clean 了工程之后,必须添加至少一个注解并执行 Build,否则你是找不到 ContentViews 类的。

如你所见,ContentViewAnnotation 只能用于获取 layout id,你需要自行实现实例化布局的 BaseActivity 和 BaseFragment 基类,其他的视图和事件的注解依然推荐你使用 ButterKnife

开源地址:

github.com/DrkCore/Con…

如果你觉得这个框架确实能帮你省下了几行代码,别客气地给个 STAR 可好?