设计模式之四:访问者模式

目录介绍

1.访问者模式介绍
2.访问者模式定义
3.访问者模式UML图
4.访问者模式简单案例
5.访问者模式之Android源码分析
5.1 注解简单介绍
5.2 注解与访问者模式关系
5.3 注解与性能的关系
6.访问者模式之实践
6.1 介绍
6.2 编译期注解之ButterKnife
6.3 编译期注解之Dagger2
6.4 自己写个简单支持View的ID注入的工具
6.4.0 基本逻辑思路
7.注解之ButterKnife源码分析
7.0 简单工作流程
7.1 首先看看Bind注解
7.2 看看支持的类型
7.3 编译时注解之ButterKnifeProcessor
7.4 接下来看看findAndParseTargets(env)代码
7.5 然后看看parseBind方法
7.6 看看parseBind中的parseBindMany方法
7.7 回到bindingClass.brewJava()方法中
7.8 接下来看注解过程,bind方法
7.9 看注解过程中bind中的findViewBinderForClass 方法

好消息

  • 博客笔记大汇总【16年3月到至今】,包括Java基础及深入知识点,Android技术博客,Python学习笔记等等,还包括平时开发中遇到的bug汇总,当然也在工作之余收集了大量的面试题,长期更新维护并且修正,持续完善……开源的文件是markdown格式的!同时也开源了生活博客,从12年起,积累共计47篇[近20万字],转载请注明出处,谢谢!
  • 链接地址:https://github.com/yangchong211/YCBlogs
  • 如果觉得好,可以star一下,谢谢!当然也欢迎提出建议,万事起于忽微,量变引起质变!

1.访问者模式介绍

访问者模式,是一种将数据操作和数据结构分离的设计模式.
大多数情况下,你并不需要使用访问者模式,但是当你一旦需要使用它时,那你就是真地需要它了.

2.访问者模式定义

定义:封装一些作用于某种数据结构中的各元素的操作,它可以在不改变这个数据结构的前提下定义作用于这些元素的新的操作.
优点:
1.各角色职责分离,符合单一职责的原则
2.具有优秀的扩展性
3.使得数据结构和作用于结构上的操作解耦,使得操作集合可以独立变化
4.灵活性
缺点:
1.具体元素对访问者公布细节,违反了迪米特原则
2.具体元素变更时导致修改成本变大
3.违反了依赖导致原则,为了达到”区别对待”而依赖了具体类,没有依赖抽象.
使用场景:
1.对象结构比较稳定,但经常需要在此对象结构上定义新的操作.
2.需要对一个对象结构中的对象进行很多不同的并且不相关的操作,它可以在不改变这个数据结构的前提下定义作用于这些元素的新的操作

3.访问者模式UML图

Visitor:接口或者抽象类,它定义了对每一个元素的访问行为,它的参数就是可以访问的元素
ConcreteVisitor:具体访问者,对每一个元素类访问的具体行为
Element:元素接口或者抽象类,定义了一个accept方法,每一个月uansu都要可以被访问者访问
ElementA:具体元素类,它提供了接受访问的具体方法实现
ObjectStructure:对象结构,内部管理元素集合,并且可以迭代这些元素供访问者访问


Image.png

4.访问者模式简单案例

5.访问者模式之Android源码分析

5.1 注解简单介绍
注解分类:运行时注解,编译期注解
通俗的分析如下所示:
1、标记一些信息,这么说可能太抽象,那么我说,你见过@Override、@SuppressWarnings等,这类注解就是用于标识,可以用作一些检验
2、运行时动态处理,这个大家见得应该最多,在运行时拿到类的Class对象,然后遍历其方法、变量,判断有无注解声明,然后做一些事情。类似上述三篇博文中的做法。
3、编译时动态处理,,一般这类注解会在编译的时候,根据注解标识,动态生成一些类或者生成一些xml都可以,在运行时期,这类注解是没有的会依靠动态生成的类做一些操作,因为没有反射,效率和直接调用方法没什么区别
运行时注解
编译期注解,编译注解的核心原理依赖APT,比如ButterKnife,Dagger,Retrofit等都是基于APT的
编译时,Annotation解析的原理:
原理是在某些代码元素上(如类型、函数、字段等)添加注解,在编译时编译器会检查AbstractProcessor的子类,并且调用该类型的process函数,然后将添加了注解的所有元素都传递到process函数中,使得开发人员可以在编译器进行相应的处理,例如,根据注解生成新的Java类,这也就是EventBus,Retrofit,Dragger等开源库的基本原理。

5.2 注解与访问者模式关系

5.3 注解与性能的关系
只要解析出来是注解和反射,必然的一个问题就是:这样会不会影响性能呀?会有性能的损耗

6.访问者模式之实践

6.1 介绍
实际开发中该模式用的少,具有实战意义的是编译期注解,就是APT示例。
6.2 编译期注解之ButterKnife
ButterKnife简单介绍
编译期注解的库【基于编译时注解,然后通过APT生成辅助类,在运行时通过bind函数调用那些生成的辅助类来完成功能】
针对View,资源ID等进行注解
6.3 编译期注解之Dagger2
Dagger2简单介绍
编译期注解的库
针对对象进行注解
6.4 自己写个简单案例
6.4.0 基本逻辑思路
通过ViewInject注解标识一些View成员变量;
通过ViewInjecyProcessor捕获添加了ViewInject注解的元素,并且按照宿主类进行分类;
为每个含有ViewInject注解的宿主类生成一个InjectAdapter辅助类,并且在它的inject函数中生成初始化View的代码;
在SimpleDagger的inject函数中构建生成的辅助类,此时内部会它这个InjectAdapter辅助类的inject函数,这个函数中又会初始化宿主类中的View成员变量,至此,View就已经被初始化了。

7.注解之ButterKnife源码分析

7.0 简单工作流程
开始它会扫描Java代码中所有的ButterKnife注解@Bind、@OnClick、@OnItemClicked等。
当它发现一个类中含有任何一个注解时,ButterKnifeProcessor会帮你生成一个Java类,名字类似$$ViewBinder,这个新生成的类实现了ViewBinder接口。
这个ViewBinder类中包含了所有对应的代码,比如@Bind注解对应findViewById(), @OnClick对应了view.setOnClickListener()等等。
最后当Activity启动ButterKnife.bind(this)执行时,ButterKnife会去加载对应的ViewBinder类调用它们的bind()方法。
7.1 首先看看Bind注解
@Retention保留时间,可选值SOURCE(源码时),CLASS(编译时),RUNTIME(运行时),默认为CLASS,值为SOURCE大都为MarkAnnotation,这类Annotation大都用来校验,比如Override,Deprecated,SuppressWarnings
@Target可以用来修饰哪些程序元素,如TYPE,METHOD,CONSTRUCTOR,FIELD,PARAMETER等,未标注则表示可修饰所有

import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.CLASS;
//说明就是编译时动态处理的    这个值是一个枚举:有三个:SOURCE、RUNTIME、CLASS
@Retention(CLASS)
//标明这个注解能标识哪些东西,比如类、变量、方法、甚至是注解本身(元注解)等
@Target(FIELD)
public @interface Bind {
  /** View ID to which the field will be bound. */
  int[] value();
}

7.2 看看支持的类型

Image.png

Image.png

7.3 编译时注解之ButterKnifeProcessor
Annotation processing是在编译阶段执行的,它的原理就是读入Java源代码,解析注解,然后生成新的Java代码。新生成的Java代码最后被编译成Java字节码,注解解析器(Annotation Processor)不能改变读入的Java 类,比如不能加入或删除Java方法。
这个是编译过程中很重要的一部分,对于butterKnife所有的注解,都是在编译时进行注解。其中ButterKnifeProcessor这个类是继承AbstractProcessor,并重写 process方法
来看看Process方法中的源代码

@Override
public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
    //获取所有注释的元素并且解析
    Map<TypeElement, BindingClass> targetClassMap = findAndParseTargets(env);
    //循环遍历,获取到注解中的键值
    for (Map.Entry<TypeElement, BindingClass> entry : targetClassMap.entrySet()) {
        TypeElement typeElement = entry.getKey();
        BindingClass bindingClass = entry.getValue();
        try {
            //写进文件,生成辅助类
            JavaFileObject jfo = filer.createSourceFile(bindingClass.getFqcn(), typeElement);
            Writer writer = jfo.openWriter();
            writer.write(bindingClass.brewJava());
            writer.flush();
            writer.close();
        } catch (IOException e) {
            error(typeElement, "Unable to write view binder for type %s: %s", typeElement,
                    e.getMessage());
        }
    }
    return true;
}

7.4 接下来看看findAndParseTargets(env)代码
遍历,然后比较并且解析

private Map<TypeElement, BindingClass> findAndParseTargets(RoundEnvironment env) {
    //创建一个map集合,用来存储所有注释的元素的键值对
    Map<TypeElement, BindingClass> targetClassMap = new LinkedHashMap<TypeElement, BindingClass>();
    //创建删除删除目标名称的set集合
    Set<String> erasedTargetNames = new LinkedHashSet<String>();
    //遍历
    // Process each @Bind element.
    for (Element element : env.getElementsAnnotatedWith(Bind.class)) {
        try {
            //解析
            parseBind(element, targetClassMap, erasedTargetNames);
        } catch (Exception e) {
            logParsingError(element, Bind.class, e);
        }
    }
}

7.5 然后看看parseBind方法

Image.png

重点了解一下几个方法

isInaccessibleViaGeneratedCode
验证方法修饰符不能为private和static
验证包含类型不能为非Class
验证包含类的可见性并不是private

isBindingInWrongPackage
它判断了这个类的包名,包名不能以android.开头
它判断了这个类的包名,包名不能以java.开头

private boolean isInaccessibleViaGeneratedCode(Class<? extends Annotation> annotationClass,
                                              String targetThing, Element element) {
    boolean hasError = false;
    TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
    Set<Modifier> modifiers = element.getModifiers();
    //验证方法修饰符不能为private和static。
    if (modifiers.contains(PRIVATE) || modifiers.contains(STATIC)) {
        error(element, "@%s %s must not be private or static. (%s.%s)",
                annotationClass.getSimpleName(), targetThing, enclosingElement.getQualifiedName(),
                element.getSimpleName());
        hasError = true;
    }

    //验证包含类型不能为非Class
    if (enclosingElement.getKind() != CLASS) {
        error(enclosingElement, "@%s %s may only be contained in classes. (%s.%s)",
                annotationClass.getSimpleName(), targetThing, enclosingElement.getQualifiedName(),
                element.getSimpleName());
        hasError = true;
    }

    //验证包含类的可见性并不是private。
    if (enclosingElement.getModifiers().contains(PRIVATE)) {
        error(enclosingElement, "@%s %s may not be contained in private classes. (%s.%s)",
                annotationClass.getSimpleName(), targetThing, enclosingElement.getQualifiedName(),
                element.getSimpleName());
        hasError = true;
    }
    return hasError;
}

private boolean isBindingInWrongPackage(Class<? extends Annotation> annotationClass,
                                        Element element) {
    TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
    String qualifiedName = enclosingElement.getQualifiedName().toString();

    //它判断了这个类的包名,包名不能以android.开头
    if (qualifiedName.startsWith(ANDROID_PREFIX)) {
        error(element, "@%s-annotated class incorrectly in Android framework package. (%s)",
                annotationClass.getSimpleName(), qualifiedName);
        return true;
    }
    //它判断了这个类的包名,包名不能以java.开头
    if (qualifiedName.startsWith(JAVA_PREFIX)) {
        error(element, "@%s-annotated class incorrectly in Java framework package. (%s)",
                annotationClass.getSimpleName(), qualifiedName);
        return true;
    }
    return false;
}

7.6 看看parseBind中的parseBindMany方法
如下所示

private void parseBindMany(Element element, Map<TypeElement, BindingClass> targetClassMap,
                           Set<String> erasedTargetNames) {
    //是否有错误,默认是false
    boolean hasError = false;
    //获取被包含的元素
    TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
    //验证的类型是一个列表List或数组array。
    TypeMirror elementType = element.asType();
    //删除类型
    String erasedType = doubleErasure(elementType);
    TypeMirror viewType = null;
    FieldCollectionViewBinding.Kind kind = null;
    //比较获取的类型是否是ARRAY类型
    if (elementType.getKind() == TypeKind.ARRAY) {
        ArrayType arrayType = (ArrayType) elementType;
        viewType = arrayType.getComponentType();
        kind = FieldCollectionViewBinding.Kind.ARRAY;
    } else if (LIST_TYPE.equals(erasedType)) {
        DeclaredType declaredType = (DeclaredType) elementType;
        List<? extends TypeMirror> typeArguments = declaredType.getTypeArguments();
        if (typeArguments.size() != 1) {
            error(element, "@%s List must have a generic component. (%s.%s)",
                    Bind.class.getSimpleName(), enclosingElement.getQualifiedName(),
                    element.getSimpleName());
            hasError = true;
        } else {
            viewType = typeArguments.get(0);
        }
        kind = FieldCollectionViewBinding.Kind.LIST;
    } else {
        //如果都不是list或者array类型,那么直接抛出异常
        throw new AssertionError();
    }
    if (viewType != null && viewType.getKind() == TypeKind.TYPEVAR) {
        TypeVariable typeVariable = (TypeVariable) viewType;
        viewType = typeVariable.getUpperBound();
    }

    //验证目标类型是否从视图扩展.
    if (viewType != null && !isSubtypeOfType(viewType, VIEW_TYPE) && !isInterface(viewType)) {
        error(element, "@%s List or array type must extend from View or be an interface. (%s.%s)",
                Bind.class.getSimpleName(), enclosingElement.getQualifiedName(), element.getSimpleName());
        hasError = true;
    }

    //如果有错误,执行结束
    if (hasError) {
        return;
    }

    // 收集实地信息.
    String name = element.getSimpleName().toString();
    int[] ids = element.getAnnotation(Bind.class).value();
    if (ids.length == 0) {
        error(element, "@%s must specify at least one ID. (%s.%s)", Bind.class.getSimpleName(),
                enclosingElement.getQualifiedName(), element.getSimpleName());
        return;
    }

    Integer duplicateId = findDuplicate(ids);
    if (duplicateId != null) {
        error(element, "@%s annotation contains duplicate ID %d. (%s.%s)", Bind.class.getSimpleName(),
                duplicateId, enclosingElement.getQualifiedName(), element.getSimpleName());
    }
    assert viewType != null; // Always false as hasError would have been true.
    String type = viewType.toString();
    boolean required = isRequiredBinding(element);
    BindingClass bindingClass = getOrCreateTargetClass(targetClassMap, enclosingElement);
    FieldCollectionViewBinding binding = new FieldCollectionViewBinding(name, type, kind, required);
    //将生成的BindingClass存入数组中
    bindingClass.addFieldCollection(ids, binding);
    erasedTargetNames.add(enclosingElement.toString());
}

7.7 回到bindingClass.brewJava()方法中
继续回到process代码中,思考write是做什么用的呢?
这个函数通过将我们绑定类的信息写入到文件外还负责创建绑定和创建解除绑定。在将绑定函数写入到文件后,整个编译器的注解方法就结束。

Image.png

Image.png

7.8 接下来看注解过程,bind方法
如图所示

Image.png

我们可以看到最终都是调用这个方法

static void bind(Object target, Object source, ButterKnife.Finder finder) {
    //获取class
    Class<?> targetClass = target.getClass();
    try {
        if (debug) Log.d(TAG, "Looking up view binder for " + targetClass.getName());
        //通过findViewBinderForClass生成每个类
        ButterKnife.ViewBinder<Object> viewBinder = findViewBinderForClass(targetClass);
        if (viewBinder != null) {
            //如果viewBinder不为空,则进行绑定
            viewBinder.bind(finder, target, source);
        }
    } catch (Exception e) {
        throw new RuntimeException("Unable to bind views for " + targetClass.getName(), e);
    }
}

7.9 看注解过程中bind中的findViewBinderForClass 方法

private static ButterKnife.ViewBinder<Object> findViewBinderForClass(Class<?> cls)
        throws IllegalAccessException, InstantiationException {
    //从内存中查找
    ButterKnife.ViewBinder<Object> viewBinder = BINDERS.get(cls);
    if (viewBinder != null) {
        if (debug) Log.d(TAG, "HIT: Cached in view binder map.");
        return viewBinder;
    }
    String clsName = cls.getName();
    //检查是否为framework class
    if (clsName.startsWith(ANDROID_PREFIX) || clsName.startsWith(JAVA_PREFIX)) {
        if (debug) Log.d(TAG, "MISS: Reached framework class. Abandoning search.");
        return NOP_VIEW_BINDER;
    }
    try {
        //实例化“MainActivity$$ViewBinder”这样的类
        Class<?> viewBindingClass = Class.forName(clsName + ButterKnifeProcessor.SUFFIX);
        //noinspection unchecked
        viewBinder = (ButterKnife.ViewBinder<Object>) viewBindingClass.newInstance();
        if (debug) Log.d(TAG, "HIT: Loaded view binder class.");
    } catch (ClassNotFoundException e) {
        if (debug) Log.d(TAG, "Not found. Trying superclass " + cls.getSuperclass().getName());
        //异常,则去父类查找
        viewBinder = findViewBinderForClass(cls.getSuperclass());
    }
    //放入内存并返回
    BINDERS.put(cls, viewBinder);
    return viewBinder;
}

其他

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

推荐阅读更多精彩内容