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

在应用 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 | 最全选型考量 + 边剖析经典开源库边实践,美滋滋

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

推荐阅读更多精彩内容