Android原生开发 APT制作重复打点检测工具

·  阅读 191

背景

做数据打点的时候,如果打点字段重复了怎么办?

例如:

申明统计打点字段.

    /**
     * 页面1打开次数
     */
    public static final int COUNT_KEY_1 = 10007;
    /**
     * 页面2打开次数
     */
    public static final int COUNT_KEY_2 = 10008;
复制代码

其他团队做广告模块的同学不知道10007和10008字段被占用了.继续申明了

    /**
     * 广告打开次数
     */
    public static final int COUNT_KEY_AD = 10008;
复制代码

这个时候字段10008就被污染了,广告打开次数和页面2打开次数就无法准确统计到.

实际工作中,2个场景容易出现上诉事故.

  1. 多个程序员并行开发打点需求,代码合并的时候自动合并了打点文件.
  2. 不同模块之间打点,不知道对方模块已经使用了什么字段.

期望有个工具可以检测重复定义的字段

解决思路

转化为语法错误

iOS同学可以利用enum的语法特性+协议来解决.实例示例代码如下:

enum Model: Int: ModelProtocol {
    case home = 11
    case sounds = 12
    
    public func toCode() -> Int64 {
        self.rawValue
    }
}


enum SleepPlan: Int64, EventCodeProtocol {
    var model: Model {
        return .home
    }
    case click = 0001
    case show = 0002
    
    public func toCode() -> Int64 {
        self.rawValue
    }
    
    public func type() -> EventTypeCode {
        .count
    }
    public func immediately() -> Bool {
        true
    }
}

protocol ModelProtocol {
    func toCode() -> Int64
}

extension EventCodeProtocol {
    var model: ModelProtocol
}

func log(code: EventCodeProtocol) {
    code.model.toCode() * 1000 + code.toCode()
}
复制代码

交流了下实现原理,就是让定义重复打点的时候触发一次编译器语法错误,让编译失败,从而让开发人员知道有字段重复定义了.

那么Android上怎么开发,Java可以利用switch...case...语法不能有重复字段申明的特性,把上诉问题转化为下面的代码

那么问题是: 怎么来实现这个校验函数?,如果自己手写的话耗时,耗力,还容易写错.

APT实现

比起手动去维护这个函数,APT的优势是可以自动生成从而不易出错.

参考ButterKnife、EventBus的实现后,总结APT的实现步骤大致如下:

1.申明注解

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface Statistics {
    Type type();

    public enum Type {
        String, Int
    }
}
复制代码

然后在原项目引用

@Statistics(type = Statistics.Type.Int)
public class StaticDemo2 {
复制代码

这一步的目的是让APT知道需要处理哪些类.

2.构造处理函数

@AutoService(Processor.class)
public class StatisticsProcessor extends AbstractProcessor {
    private Filer mFilerUtils;       // 文件管理工具类
    private Types mTypesUtils;    // 类型处理工具类
    private Elements mElementsUtils;  // Element处理工具类
    static Set<String> typeSet = new HashSet<>();
    private Map<Statistics.Type, List<String>> dataMap = new HashMap<>();

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);

        mFilerUtils = processingEnv.getFiler();
        mTypesUtils = processingEnv.getTypeUtils();
        mElementsUtils = processingEnv.getElementUtils();

    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnv) {
        System.out.println("start process");
        if (set != null && set.size() != 0) {
            Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(Statistics.class);
            categories(elements);
            if (dataMap.size() > 0) {
                Element simpleElement = elements.iterator().next();
                String code = generateCode(simpleElement, dataMap.get(Statistics.Type.String), dataMap
                        .get(Statistics.Type.Int));
                String helperClassName = "StatisticsChecker"; // 构建要生成的帮助类的类名
                try {
                    JavaFileObject jfo = mFilerUtils.createSourceFile(helperClassName);
                    Writer writer = jfo.openWriter();
                    writer.write(code);
                    writer.flush();
                    writer.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                return true;
            }
        }
        return false;
    }


    private void categories(Set<? extends Element> elements) {
        if (typeSet.size() == 0) {
            Statistics.Type[] types = Statistics.Type.values();
            for (int i = 0; i < types.length; i++) {
                typeSet.add(types[i].name().toLowerCase());
            }
        }
        for (Element element : elements) {
            Statistics statistics = element.getAnnotation(Statistics.class);
            Symbol.ClassSymbol cE = (Symbol.ClassSymbol) element;
            String preName = cE.className();
            List eleSubList = element.getEnclosedElements();

            Statistics.Type type = statistics.type();
            List list = dataMap.get(type);
            if (list == null) {
                list = new ArrayList<String>();
                dataMap.put(type, list);
            }

            for (Object e : eleSubList) {
                if (e instanceof Symbol.VarSymbol) {
                    Symbol.VarSymbol el = (Symbol.VarSymbol) e;
                    if (typeSet.contains(el.type.tsym.name.toString().toLowerCase())) {
                        // 其实这里就可以知道有没有重复字段了   但是为了让结果更加直观  还是把这个代码生成出来看
                        list.add(preName + "." + (el).getSimpleName().toString());
                    }
                }
            }
        }
    }

    private String generateCode(Element typeElement, List<String> listStr, List<String> listInt) {
        String packageName = ((PackageElement) mElementsUtils.getPackageOf(typeElement))
                .getQualifiedName().toString(); // 获取要绑定包名
        String helperClassName = "StatisticsChecker";   // 要生成的帮助类的名称

        StringBuilder builder = new StringBuilder();
        builder.append("package ").append(packageName).append(";\n");
        builder.append("\n");
        builder.append("public class ").append(helperClassName);
        builder.append(" {\n");
        builder.append("\tvoid check() {\n");

        if (listStr != null && listStr.size() > 0) {
            builder.append("\t\tString countStr = null;\n");
            builder.append("\t\tswitch (countStr) {\n");

            for (String caseValue : listStr) {
                builder.append("\t\t\t");
                builder.append(String.format("case %s:\n", caseValue));
            }
            builder.append("\t\t}\n");
        }

        if (listInt != null && listInt.size() > 0) {
            builder.append("\t\tint countInt = 0;\n");
            builder.append("\t\tswitch (countInt) {\n");

            for (String caseValue : listInt) {
                builder.append("\t\t\t");
                builder.append(String.format("case %s:\n", caseValue));
            }
            builder.append("\t\t}\n");
        }


        builder.append("\t}\n");
        builder.append("}\n");

        return builder.toString();
    }
}
复制代码

这里是模板写法, 继承AbstractProcessor类实现他的process方法. 目的是在并编译的时候自动生成代码.

3.注入

看ButterKnife还有一个注入的过程,就是把自动生成的代码放到原来的项目里面调用,但是我们这里只是用来做静态检测,生成即可,不需要注入.

总结

到此APT自动检测重复字段就实现了,但是还有几个问题没有解决:

  1. 那个AbstractProcessor的process函数代码看起来很麻烦,怎么实现的? 我的方式是一边调试一边写,那么APT代码怎么调试?
  2. 我写好了这份代码,怎么复用到其他项目? APT模块化开发+生成jcenter依赖
  3. 这里只是Java的实现,如果集成项目里面是koltin写的或者java&kotlin混合开发怎么办? APT支持Koltin与项目依赖.
分类:
Android
分类:
Android
收藏成功!
已添加到「」, 点击更改