注解

AOP学习总结-利用APT仿写ButterKnife

2019-05-08  本文已影响50人  L_Xian

在应用 AOP 之前,应该思考几个问题:

  1. 明确你应用 AOP 在什么项目

小范围试用,选择一个侵入性小的 AOP 方法

  1. 明确切入点的相似性

考虑切入点的数量和相似性,确定你是否愿意一个个在切点上加注。 解还是用相似性统一切入。

  1. 明确织入的粒度和织入的时机

怎么选择织入的时机?编译期间织入,还是编译后?载入时?或是运行时?通过比较各大 AOP 方法在织入时机方面的不同和优缺点,来获得对于如何选择织入时机进行判定的准则。

  1. 明确对性能的要求,明确对方法数的要求

除了动态织入,其他 AOP 方法对性能的影响可以忽略不计,看各自方法的优缺点进行权衡。

  1. 明确是否需要修改原有类

  2. 明确调用的时机

仿造 ButterKnife

步骤:

  1. 定义注解
  2. 编写注解处理器
  3. 扫描注解
  4. 编写代理类内容
  5. 生成代理类
  6. 调用代理类

第一步:

新建一个名为 annoation 的 Java Library,里面存放注解。

新建一个名为 compiler 的 Java Library,里面实现 APT,compiler 引用 annoation。

新建一个名为 code 的 android Library,里面封装对 APT 调用的门面,code 引用 annoation。

第二步:

在 annoation 中编写注解 BindView

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.SOURCE)
public @interface BindView {
    int value();
}

第三步:

在 compiler 中编写 APT 类 BindViewProcessor:

@AutoService(Processor.class) //自动注册
@SupportedAnnotationTypes("com.lzx.annoation.BindView") //指定解析注解
public class BindViewProcessor extends AbstractProcessor {

}

BindViewProcessor 类要继承 AbstractProcessor,并且添加 @AutoService 注解,参数填 Processor.class,这样它就能自动注册,为什么要自动注册,因为想要运行注解处理器,需要繁琐的步骤:

  1. 在 processors 库的 main 目录下新建 resources 资源文件夹;
  2. 在 resources文件夹下建立 META-INF/services 目录文件夹;
  3. 在 META-INF/services 目录文件夹下创建 javax.annotation.processing.Processor 文件;
  4. 在 javax.annotation.processing.Processor 文件写入注解处理器的全称,包括包路径;

注解 @SupportedAnnotationTypes 参数是我们刚刚编写的 BindView 注解的路径,这样的意思是指定解析 BindView。

然后重写 init 方法:

private Messager mMessager;
private Filer mFiler;
private Elements mElements;
private Map<String, List<Element>> classMap = new HashMap<>();

@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
    super.init(processingEnvironment);
    mMessager = processingEnvironment.getMessager();
    mFiler = processingEnvironment.getFiler();
    mElements = processingEnvironment.getElementUtils();
    mMessager.printMessage(Diagnostic.Kind.NOTE, "BindViewProcessor  init");
}

在 init 方法里面,一般是给一些变量赋值。以上的写法可以作为模版写法,就是说基本都会这样写。

重点在于重写 process 方法:

@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
    mMessager.printMessage(Diagnostic.Kind.NOTE, "BindViewProcessor  process");
    //typeElement就相当于com.lzx.annoation.BindView
    //通过roundEnvironment来获取所有被BindView注解注解了的字段
    for (TypeElement typeElement : set) {
        mMessager.printMessage(Diagnostic.Kind.NOTE, "typeElement = " + typeElement.toString());
        Set<? extends Element> elementsAnnotatedWith = roundEnvironment.getElementsAnnotatedWith(typeElement);
        for (Element element : elementsAnnotatedWith) {
            //如果在MainActivity中有这段代码 @BindView(R.id.textView)TextView textView;
            //此处的element就是TextView节点元素
            //element.getEnclosingElement()为获取其父节点元素,即MainActivty
            Element enclosingElement = element.getEnclosingElement();
            TypeMirror classTypeMirror = enclosingElement.asType();
            //className为MainActivty的全类名
            String className = classTypeMirror.toString();
            //上面只是拿MainActivty举个例子,但是真实的使用注解的可能还有SecondActivity等等,
            // 所有需要以类名为键保存里面所有使用了BindView注解的节点
            List<Element> elements = classMap.get(className);
            if (elements == null) {
                elements = new ArrayList<>();
                elements.add(element);
                classMap.put(className, elements);
            } else {
                elements.add(element);
            }
        }

        Set<Map.Entry<String, List<Element>>> entries = classMap.entrySet();
        for (Map.Entry<String, List<Element>> entry : entries) {
            String key = entry.getKey();
            List<Element> value = entry.getValue();
            //生成java代码
            generateViewBinding(key, value);
        }
    }
    return false;
}

下面来一一分析一下:

typeElement 就相当于 com.lzx.annoation.BindView,通过遍历 set 得到,通过roundEnvironment 来获取所有被 BindView 注解注解了的字段:

Set<? extends Element> elementsAnnotatedWith = roundEnvironment.getElementsAnnotatedWith(typeElement);

遍历所有被标记字段:

for (Element element : elementsAnnotatedWith) {
    //如果在 MainActivity 中有这段代码 @BindView(R.id.textView) TextView textView;
    //此处的 element 就是 TextView 节点元素
    //element.getEnclosingElement() 为获取其父节点元素,即 MainActivty
    Element enclosingElement = element.getEnclosingElement();
    TypeMirror classTypeMirror = enclosingElement.asType();
    
    //className 为 MainActivty 的全类名
    String className = classTypeMirror.toString();
    
    //上面只是拿 MainActivty 举个例子,但是真实的使用注解的可能还有 SecondActivity 等等,
    //所以需要以类名为键保存里面所有使用了 BindView 注解的节点
    List<Element> elements = classMap.get(className);
    if (elements == null) {
        elements = new ArrayList<>();
        elements.add(element);
        classMap.put(className, elements);
    } else {
        elements.add(element);
    }
}

接下来遍历节点去生成代码,生成代码是通过 JavaPoet 框架去完成:

Set<Map.Entry<String, List<Element>>> entries = classMap.entrySet();
for (Map.Entry<String, List<Element>> entry : entries) {
    String key = entry.getKey();
    List<Element> value = entry.getValue();
    //生成java代码
    generateViewBinding(key, value);
}

generateViewBinding方法:

private void generateViewBinding(String key, List<Element> value) {
    // 生成类元素节点
    TypeElement classTypeElement = mElements.getTypeElement(key);
    // 生成参数 final MainActivity target
    ParameterSpec targetParameterSpec = ParameterSpec
            .builder(ClassName.get(classTypeElement), "target", Modifier.FINAL)
            .build();
    //生成参数  View source
    ParameterSpec viewParameterSpec = ParameterSpec
            .builder(ClassName.get("android.view", "View"), "source")
            .build();
    MethodSpec methodSpec = null;
    // 生成构造函数
    MethodSpec.Builder constructorMethodBuilder = MethodSpec.constructorBuilder()
            .addParameter(targetParameterSpec)
            .addParameter(viewParameterSpec)
            .addAnnotation(ClassName.bestGuess("android.support.annotation.UiThread"))
            .addModifiers(Modifier.PUBLIC);
    // 构造函数中添加代码块
    constructorMethodBuilder.addStatement("this.target = target");
    for (Element element : value) {
        BindView bindView = element.getAnnotation(BindView.class);
        int id = bindView.value();
        Name simpleName = element.getSimpleName();
        constructorMethodBuilder.addStatement("target.$L = source.findViewById($L)", simpleName.toString(), id);
    }
    methodSpec = constructorMethodBuilder.build();
    // 生成unbind方法
    MethodSpec.Builder unbindMethodSpec = MethodSpec.methodBuilder("unbind")
            .addModifiers(Modifier.PUBLIC);
    unbindMethodSpec.addStatement("$T target = this.target", ClassName.get(classTypeElement));
    unbindMethodSpec.addStatement("this.target = null");
    for (Element element : value) {
        Name simpleName = element.getSimpleName();
        unbindMethodSpec.addStatement("target.$L = null", simpleName.toString());
    }
    // 生成MainActivity_ViewBinding类
    TypeSpec typeSpec = TypeSpec.classBuilder(classTypeElement.getSimpleName() + "_ViewBinding")
            .addField(ClassName.get(classTypeElement), "target", Modifier.PRIVATE)
            .addMethod(methodSpec)
            .addMethod(unbindMethodSpec.build())
            .addSuperinterface(ClassName.bestGuess("com.lzx.code.Unbinder"))
            .addModifiers(Modifier.PUBLIC)
            .build();
    // 获取包名
    String packageName = mElements.getPackageOf(classTypeElement).getQualifiedName().toString();
    mMessager.printMessage(Diagnostic.Kind.NOTE, "packageName = " + packageName);
    JavaFile javaFile = JavaFile.builder(packageName, typeSpec).build();
    //写入java文件
    try {
        javaFile.writeTo(mFiler);
    } catch (IOException e) {
        e.printStackTrace();
    }
}

可以看到上面生成的类的类名是由 Activity 名字加上 _ViewBinding 组成的。

第四步:

在 code 中编写门面代码:

ButterKnife:

public class ButterKnife {
    static final Map<Class<?>, Constructor<? extends Unbinder>> BINDINGS = new LinkedHashMap<>();

    @NonNull
    @UiThread
    public static Unbinder bind(@NonNull Activity target) {
        View sourceView = target.getWindow().getDecorView();
        return createBinding(target, sourceView);
    }

    public static Unbinder bind(@NonNull Fragment target, View sourceView){
        return createBinding(target,sourceView);
    }

    public static Unbinder bind(@NonNull android.app.Fragment target, View sourceView){
        return createBinding(target,sourceView);
    }

    private static Unbinder createBinding(Object target, View sourceView) {
        Class<?> targetClass = target.getClass();
        Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);
        if (constructor == null) {
            return Unbinder.EMPTY;
        }
        try {
            return constructor.newInstance(target, sourceView);
        } catch (IllegalAccessException e) {
            throw new RuntimeException("Unable to invoke " + constructor, e);
        } catch (InstantiationException e) {
            throw new RuntimeException("Unable to invoke " + constructor, e);
        } catch (InvocationTargetException e) {
            Throwable cause = e.getCause();
            if (cause instanceof RuntimeException) {
                throw (RuntimeException) cause;
            }
            if (cause instanceof Error) {
                throw (Error) cause;
            }
            throw new RuntimeException("Unable to create binding instance.", cause);
        }
    }

    private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> targetClass) {
        Constructor<? extends Unbinder> constructor = BINDINGS.get(targetClass);
        if (constructor != null) {
            return constructor;
        }

        String targetClassName = targetClass.getName();
        try {
            Class<?> viewBindingClass = Class.forName(targetClassName + "_ViewBinding");
            constructor = (Constructor<? extends Unbinder>) viewBindingClass.getConstructor(targetClass, View.class);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
        BINDINGS.put(targetClass, constructor);

        return constructor;
    }
}

上面代码的逻辑就是通过 findBindingConstructorForClass 方法找到刚刚生成的类。然后在 createBinding 方法中通过反射去实例化它,这当中使用了缓存,提高了性能。

还有上面出现的 Unbinder 接口:

public interface Unbinder {
    @UiThread
    void unbind();

    Unbinder EMPTY = new Unbinder() {
        @Override public void unbind() { }
    };
}

第五步:

在 APP 工程去引用上面的代码:

dependencies {
    implementation project(':code')
    annotationProcessor project(':compiler')
}

最后看看生成的代码是怎么样的:
MainActivity_ViewBinding:

public class MainActivity_ViewBinding implements Unbinder {
  private MainActivity target;

  @UiThread
  public MainActivity_ViewBinding(final MainActivity target, View source) {
    this.target = target;
    target.mTextView = source.findViewById(2131165209);
  }

  public void unbind() {
    MainActivity target = this.target;
    this.target = null;
    target.mTextView = null;
  }
}

对应着生成的代码再看刚刚的 generateViewBinding 方法,是否就一目了然。

看看如何使用:

public class MainActivity extends AppCompatActivity {

    @BindView(R.id.add) TextView mTextView;

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

        mTextView.setText("Hello APT");
    }
}

回过头来看看 ButterKnife 是怎么使用 APT 的:

15572231486342.jpg

你可能发现了,最后一个步骤是在合适的时机去调用代理类或门面对象。这就是 APT 的缺点之一,在任意包位置自动生成代码但是运行时却需要主动调用。

参考文章:一文应用 AOP | 最全选型考量 + 边剖析经典开源库边实践,美滋滋

上一篇下一篇

猜你喜欢

热点阅读