android动态编程


在Android开发中,我们经常会遇到一些需要大量编写重复代码的情景,比如

  • findViewById(),从布局文件中加载UI组件
  • cursot.getColumnIndex()和cursor.getInt(),从数据库中读取数据

这样做其实没有错,但是编程本身是作为一种工具属性将一些简单重复的事情交由计算机来处理以提高效率的,所以作为程序员的我们就应该尽可能的把一些重复冗余的操作交由计算机来做。于是上面说的两种情景就可以用动态编程的方式来实现。在Android里面我们主要使用java语言进行开发,所以使用java的注解特性可以帮助我们完成动态编程的任务,java的注解使得我们可以将一些通用或者重复的东西使用程序以模板的形式来自动生成和处理,而将一些需要改变的东西使用注解配置来处理,这使得我们可以从编写大量重复冗余的代码工作中抽离出来,专注于核心业务代码的处理。

注解又分为两种:编译时注解和运行时注解。编译时注解只在编译器期有效,当程序运行时我们无法获得代码里面的注解信息,而运行时注解可以将注解的信息保留在运行阶段,一般我们会在运行阶段使用反射的方式来获得注解的相关信息。这里以通过注解来代替findViewById为例来谈下这两种方式的具体实现。

反射

/**
     * 通过反射的方式初始化activity内部的ui控件
     * @param context
     */
    public static final void bindView(Activity context) {
        Class<? extends Activity> contextClass = context.getClass();
        Field[] fields = contextClass.getDeclaredFields();
        for (Field field : fields) {
            if (field.isAnnotationPresent(DIView.class)) {
                field.setAccessible(true);
                Annotation[] annotations = field.getDeclaredAnnotations();
                int resId = 0;
                for (Annotation annotation : annotations) {
                    if (annotation instanceof DIView) {
                        DIView bindAnnotation = (DIView) annotation;
                        resId = bindAnnotation.value();
                        break;
                    }
                }
                try {
                    field.set(context, context.findViewById(resId));
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }
    }

这里的DIView的定义如下:

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DIView {
    int value() default 0;
}

从上面的代码可以看出,通过反射我们可以容易的获取到一个class的所有信息。当然这这么也是有代价的,那就是会影响你的应用程序的性能。所以一般我们在选择第三方的注解工具类库时,会尽量避免使用通过反射实现的类库。

Processor

Processor可以算是对反射的一种优化,它将注解的处理放在了编译阶段,这样可以消除对程序运行时性能的影响。我们还是以通过注解来代替findViewById为例,我们来看看Processor是怎么做的:

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

Retention决定了一个注解是编译时注解还是运行时注解,target决定了该注解修饰的是类,类成员还是方法等。这里是定义了一个修饰activity的注解,方便在编译过程中注解处理器识别该activity,并自动生成一段初始化ui组件的util类和方法。

@AutoService(Processor.class)
//@SupportedSourceVersion(SourceVersion.RELEASE_7)
//@SupportedAnnotationTypes("com.okgays.annotation.DIActivity")
public class BindViewProcessor extends AbstractProcessor {
    private Elements elementUtils;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        elementUtils = processingEnv.getElementUtils();
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "DIProcessor start");
        Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(DIActivity.class);
        for (Element element : elements) {
            // 因为DIActivity的target为ElementType.TYPE,所以这里才可以强转
            TypeElement typeElement = (TypeElement) element;
            List<? extends Element> members = elementUtils.getAllMembers(typeElement);
            //创建一个publid finla static bindView(Activity activity)方法
            MethodSpec.Builder bindViewMethodSpecBuilder = MethodSpec.methodBuilder("bindView")
                    .addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL)
                    .returns(TypeName.VOID)
                    .addParameter(ClassName.get(typeElement.asType()), "activity");
            for (Element item : members) {
                DIView diView = item.getAnnotation(DIView.class);
                if (diView == null){
                    continue;
                }
                bindViewMethodSpecBuilder.addStatement(String.format("activity.%s = (%s) activity.findViewById(%s)",item.getSimpleName(),ClassName.get(item.asType()).toString(),diView.value()));
            }
            //生成一个DIUtil类,该类为public final的
            TypeSpec typeSpec = TypeSpec.classBuilder("DIUtil")
                    .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                    .addMethod(bindViewMethodSpecBuilder.build())
                    .build();
            //生成一个DIUtil.java文件
            JavaFile javaFile = JavaFile.builder(getPackageName(typeElement), typeSpec).build();
            try {
                javaFile.writeTo(processingEnv.getFiler());
                processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "DIProcessor success");
            } catch (IOException e) {
                e.printStackTrace();
            }

            processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "DIProcessor end");
        }
        return true;
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        //也可以通过SupportedAnnotationTypes注解来指定该注解处理器支持的注解类型
        return Collections.singleton(DIActivity.class.getCanonicalName());
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        //也可以通过SupportedSourceVersion注解来指定该注解处理器支持的java版本
        return SourceVersion.RELEASE_7;
    }

    private String getPackageName(TypeElement type) {
        return elementUtils.getPackageOf(type).getQualifiedName().toString();
    }
}

这里是自定义的注解处理器,主要是处理DIActivity注解,并自动生成一个DIUtil类来实现findViewById的功能,实际上就是将程序中大量findViewById的编码工作由手动改为程序自动生成。

@DIActivity
public class MainActivity extends AppCompatActivity {

    @DIView(R.id.label)
    TextView label;
    @DIView(R.id.button)
    Button button;
    @DIView(R.id.listview)
    ListView listView;

    boolean useReflect = false;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        if (useReflect) {
            DIUtil.bindView(this);
        } else {
            BindUtil.bindView(this);
        }

        label.setText("annotation test");
    }
}

这个是我们app的主界面activity,可以看到我们在里面使用了两种注解,其中DIActivity为编译时注解,DIView为运行时注解。DIUtil.bindView()通过反射处理DIView注解来封装和简化findViewById的操作。DIActivity通过自定义的BindViewProcessor注解处理器来自动生成一个util类来封装和简化findViewById的操作。

通过编译时注解使得我们可以在不影响程序运行性能的前提下大大提高程序员的编码效率,目前很多知名的第三方开源类库,如DBflow、Dagger2、ButterKnife等都使用这种方式。

项目代码示例如下

自定义注解处理器

参考文档

动态android编程

Android APT(编译时代码生成)最佳实践

Android编译时注解框架5-语法讲解

Java注解(3)-源码级框架

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

推荐阅读更多精彩内容