解惑AndroidAndroid技术知识android技术专栏

android动态编程

2017-06-21  本文已影响151人  codeKeeper

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

这样做其实没有错,但是编程本身是作为一种工具属性将一些简单重复的事情交由计算机来处理以提高效率的,所以作为程序员的我们就应该尽可能的把一些重复冗余的操作交由计算机来做。于是上面说的两种情景就可以用动态编程的方式来实现。在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)-源码级框架

上一篇下一篇

猜你喜欢

热点阅读