记一次orm库性能优化,谈谈android的注解处理和代码生成

很早的时候因为业务的需要,找不到一款合适的orm框架,当时自己写了一个。很长时间过去了,一直想对这个orm库做一次性能优化,换掉原有的反射的注解处理方式,而是通过代码生成,从而大幅提高性能。

原有的orm库通过反射的注解处理方式来实现。原有的Table注解声明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Table {
public String name();
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@interface Column {
public String name();
public boolean nullable() default true;
public boolean isUnionKey() default false;
......
}
......
}

这里简单介绍下注解的知识。通过关键字@interface来定义注解,这里定一个了一个名为Table的注解,Table声明本身上面也有两个注解,这个叫做元注解,一共有四种元注解,分别为:

  • @Retention 注解的有效范围
    • SOURCE,只在源码中可用,一般用来增加代码理解和代码检查等,常见的如Override
    • CLASS, 默认选项,在源码和字节码中可用,但仅存于字节码文件中,运行时无法获取
    • RUNTIME, 在源码,字节码,运行时均可用
  • @Target 指定注解修饰的程序元素,如 TYPE, METHOD, CONSTRUCTOR, FIELD, PARAMETER等,未标注则表示可修饰所有
  • @Inherited 是否可以被继承,默认为false
  • @Documented 是否会保存到 Javadoc 文档中

回到上述的表注解定义,可以看到定义了一个Target为TYPE的Table类注解和一个Type为FIELD的Column成员变量注解。

完成以后,我们就可以如下来定一个对象的同时创建一张表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Table(name = User.TABLE_NAME)
public class User extends Table{
public static final String TABLE_NAME = "user";
public static final String COLUME_ID = "id";
public static final String COLUME_NAME = "name";
public static final String COLUME_AGE = "age";
public static final String COLUME_MALE = "male";
@Table.Column(name = COLUME_ID, isPrimaryKey = true)
String id;
@Table.Column(name = COLUME_NAME, defaultValue = "unknown")
String name;
@Table.Column(name = COLUME_AGE)
int age;
@Table.Column(name = COLUME_MALE)
boolean isMale;
}

一个简单的User对象,有id,name,age,isMale四个成员变量,通过注解声明表名为有四个colunm的’user’表。

使用orm库让插入和建表一样简单,不用写sql操作:

1
2
3
4
5
User user = new User();
user.id = 0;
user.name = "Tom";
user.age =10;
user.save();

上面代码定义了一个User对象,并调用了User对象的save方法,save()方法来自父类Table类。这里说下整个建表和插入一个user数据的核心逻辑。

首先建表过程,关于获取列信息的部分如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Field[] fields = columnType.getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(Column.class)) {
Column annotationColumn = field.getAnnotation(Column.class);
if (annotationColumn == null) {
return null;
}
String columnName = columnAnnotation.name();
String isPrimaryKey = columnAnnotation.isPrimaryKey();
.........
Class<?> type = field.getType();
SQLiteType columnType = JavaToSQLiteType.getJavaSQLiteType(type);
}
}

为了控制篇幅,不讲具体orm实现,只贴出部分核心代码。首先通过反射获取User里所有的fields,判断field是否有Column注解,有则获取注解上的column信息,包括每个column的columnName,isPrimaryKey等。拿到了每一个列的具体信息,再完成java类型到sqlite类型的转换,表的列信息就全部获取到了。最后将其生成sql代码并执行,这样一张表就建好了。

插入操作也是通过反射来完成,通过反射获取user对象的所有field的值,再通过之前建表时就获取的表名,列名就可以用sql语句完成数据的插入。

可以看到整个orm的核心逻辑都是通过反射来实现的,包括表的创建以及增删改查。这也是这个库1.x版本的性能瓶颈,实测下来插入查询都会比一些流行库慢很多。所以这次优化的核心点就是去反射。也就是要替换原有通过反射来获取用户声明的表结构的部分。实现的方法就是接下来要讲的通过编译时注解进行代码生成,完成表结构获取,增删改查的代码生成。从而去掉大量反射逻辑,大幅提高性能。

首先改变注解定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface Table {
public String name();
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
@interface Column {
public String name();
public boolean nullable() default true;
public boolean isUnionKey() default false;
......
}
......
}

注意唯一改变的就是@Retention注解,从原有的RetentionPolicy.RUNTIME改成@Retention(RetentionPolicy.CLASS)。前面已经提到,也就是从运行时注解改为编译时注解,从而在编译时通过注解处理生成代码,完成表信息,增删改查接口的代码生成。

还是说建表和插入操作。建表只需要获取声明的表信息,所以应该提供一个方法来获取表的所有列信息,而插入方法很直白,就是要用于数据插入。那代码生成需要一个initColumnInfoList方法和一个save方法。来看实现。

注解的处理只需继承AbstractProcessor类,主要实现process方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
@AutoService(Processor.class)
@SupportedOptions({OrmProcessor.TARGET_MODULE_NAME})
public class OrmProcessor extends AbstractProcessor {
........
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
handleTableAnnotation(roundEnv);
....
return true;
}
/**
* 处理 @Table 注解
*
* @param roundEnv
*/
private void handleTableAnnotation(RoundEnvironment roundEnv) {
Set<? extends Element> set = roundEnv.getElementsAnnotatedWith(Table.class);
for (Element element : set) {
if (isValid(element)) {
TypeElement typeElement = (TypeElement) element;
PackageElement packageElement = (PackageElement) element.getEnclosingElement();
Map<Table.Column, VariableInfo> columnMap = new TreeMap<>(AdapterGenerator.sTableColumnComparator);
List<VariableElement> variableElementList = ElementFilter.fieldsIn(typeElement.getEnclosedElements());
for (VariableElement ve : variableElementList) {
Table.Column column = ve.getAnnotation(Table.Column.class);
if (column == null) {
continue;
}
String variableName = ve.getSimpleName().toString();
// 获取属性类型
TypeMirror typeMirror = ve.asType();
columnMap.put(column, new VariableInfo(variableName, typeMirror));
}
String fullName = typeElement.getQualifiedName().toString();
String className = typeElement.getSimpleName().toString();
String packageName = packageElement.getQualifiedName().toString();
ClassInfo classInfo = new ClassInfo(fullName, className, packageName);
Table table = typeElement.getAnnotation(Table.class);
String tableName = table.name();
AdapterGenerator.generateTableAdapter(processingEnv.getFiler(), classInfo, tableName, columnMap);
tableMap.put(fullName, ADAPTER_PACKAGE + "." + className + TABLE_ADAPTER_POSTFIX);
}
}
}
.......
}

通过参数roundEnv拿到User表注解中的所有信息,包括表名,列名以及类型。其中AdapterGenerator.generateTableAdapter方法将获取到的表信息生成java代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public static void generateTableAdapter(Filer filer, ClassInfo classInfo, String tableName,
Map<Table.Column, VariableInfo> columnMap) {
.....
// initColumnInfoList方法
MethodSpec.Builder initListBuilder = MethodSpec.methodBuilder("initColumnInfoList")
.addAnnotation(Override.class)
.addModifiers(Modifier.PROTECTED)
.addStatement("columnInfoList = new $T()", arrayListClassName);
for (Map.Entry<Table.Column, VariableInfo> entry : columnMap.entrySet()) {
Table.Column column = entry.getKey();
VariableInfo variableInfo = entry.getValue();
initListBuilder.addStatement("$T info_$L = new $T($L, $S, $L, $S, $L, $L, $L, $L, $S, $L, $L)",
tableColumnInfoClassName,
variableInfo.name,
tableColumnInfoClassName,
variableInfo.typeName.toString() + ".class",
column.name(),
column.nullable(),
......
);
initListBuilder.addStatement("columnInfoList.add(info_$L)", variableInfo.name);
}
MethodSpec initList = initListBuilder.build();
// save方法
MethodSpec.Builder saveBuilder = MethodSpec.methodBuilder("save")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.returns(long.class)
.addParameter(modelClassName, "model")
.addStatement("$T contentValues = new $T()", contentValueClassName, contentValueClassName);
for (Map.Entry<Table.Column, VariableInfo> entry : columnMap.entrySet()) {
Table.Column column = entry.getKey();
VariableInfo variableInfo = entry.getValue();
// 自增字段不主动赋值
if (!column.isAutoincrement()) {
saveBuilder.addStatement("contentValues.put($S, $L)", column.name(),
getVariableGetter(variableInfo.name, variableInfo.typeName));
}
}
saveBuilder.addStatement("if (database == null) {getDatabase();}");
saveBuilder.addStatement("return database.insert(tableName, null, contentValues)");
MethodSpec save = saveBuilder.build();
......
}

上述代码通过javapoet来生成java代码,遍历传进来的所有column名称以及类型,生成了initColumnInfoListsave方法。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Override
protected void initColumnInfoList() {
columnInfoList = new ArrayList();
TableColumnInfo info_name = new TableColumnInfo(java.lang.String.class, "name", true, "unknown", false, false, false, false, "", new String[]{}, -2147483648);
columnInfoList.add(info_name);
TableColumnInfo info_isMale = new TableColumnInfo(boolean.class, "male", true, "", false, false, false, false, "", new String[]{}, -2147483648);
columnInfoList.add(info_isMale);
TableColumnInfo info_id = new TableColumnInfo(java.lang.String.class, "id", true, "", true, false, false, false, "", new String[]{}, -2147483648);
columnInfoList.add(info_id);
TableColumnInfo info_age = new TableColumnInfo(int.class, "age", true, "", false, false, false, false, "", new String[]{}, -2147483648);
columnInfoList.add(info_age);
}
@Override
public long save(User model) {
ContentValues contentValues = new ContentValues();
contentValues.put("name", model.getName());
contentValues.put("male", model.isMale());
contentValues.put("id", model.getId());
contentValues.put("age", model.getAge());
if (database == null) {getDatabase();};
return database.insert(tableName, null, contentValues);
}

initColumnInfoList构造了columnInfoList,有了他自然可以建表了,而save方法很清晰就是常用的一个插入操作了。

当然,生成代码的initColumnInfoList方法还是需要通过反射来调一次。

这样优化后,效率大幅提升,数据表插入效率提升30%,查询效率提升60%。

再总结下,其实大部分依赖反射的实现就都可以通过代码生成来解决。代码生成提高了库本身的复杂度,也增加了使用者的编译速度,但是相比性能的大幅提示来说,还是非常值得的。

写完回头看看觉得有点杂糅,因为既涉及到orm的实现,又涉及到注解处理。有空再单独写篇注解处理吧。

Share Comments

用vim开发react native的环境搭建

续费博客域名的时候发现,一晃竟然两年没更新了……只能说时间过的真是好快。前几天把博客切换到了hexo,因为octopress的构建速度实在是已经不能支持继续写更多的博客了。

最近有时间就会去研究下react native,打算接下来写一系列react native相关的文章。

写react编辑器自然还是用我最喜欢的vim,可以配置下语法高亮,语法检查和代码片段自动生成。

Read More

Share Comments

vim中的语法检查-syntastic

最近比较忙,有几天没更新了。今天有个同事问我一个语法插件的问题,向他介绍了Sytanstic.vim。那今天就来介绍下这个必备的插件吧。

很多人喜欢IDE就是因为他的语法检查,有了Sytanstic.vim,这个问题就不复存在了。(当然,仅仅是语法检查)

功能

上一张官方图:

Read More

Share Comments

vim 重复操作的利器--点命令

之前介绍过可以重复motion的插件space.vim,有朋友留言说.不是也可以?其实.确实可以重复很多动作,但是无法重复motion。其实;,倒是可以重复motion。不过space.vim可以重复更多操作,之前的博客有全部列出来。

今天主要就介绍.命令。

这个命令就用来重复上一次的操作。比如:dw,再按.就会再删除一个单词。他可以重复的命令非常多,比如插入操作a,i,比如替换删除操作c,s, r还有J,~等常用的操作。但是我却很难概括他到底可以重复哪些操作,我基本归纳为重复对当前buffer造成改变的操作,虽然并不准确.

了解了基本的用途,来看看使用场景吧。我说说我一般的用法:

Read More

Share Comments

每日vim插件--平滑滚动accelerated-smooth-scroll.vim

今天介绍一个简单的插件:accelerated-smooth-scroll.
很简单,就是让<C-D>/<C-U><C-F>/<C-B>不再突然出现,而是出现滚动效果。同时,在连续滚动时,还有加速效果。
做了个gif图,真实效果比图片要更流畅一些,如图所示:

今天就介绍它啦。几乎是用的最多的插件……

Share Comments

每日vim插件--强大的自动补全neocomplete.vim和supertab

#neocomplete.vim

今天介绍两个个必备的vim插件,自动补全插件——neocomplete.vim和superTab。

neocomplete.vim是来自shougo的作品。该插件维护了当前buffer的一个关键词列表,从而提供强大的关键词补全功能。

该插件是他前作neocomplcache的升级版,速度更快,功能更强大。不过该插件需要if_lua的支持。

mac下安装:

brew install macvim --with-cscope --with-lua --HEAD

或者不用macvim(真的不用么?赶紧试试吧):

brew install vim --with-lua

不需要过多的介绍,看作者给的图:

Read More

Share Comments