上一节仅仅是做到了有这个功能,这次我们对上一次的代码进行优化
主要有两个方面可以优化
1.利用数据库事务进行优化
我们作如下修改:
IDaoSupport增加批量插入接口
// 批量插入数据
public void inert(List<T> t);
在实现类中实现该方法 并使用数据库事务
@Override
public void inert(List<T> data) {
mSqLiteDatabase.beginTransaction();
for (T datum: data){
inert(datum);
}
mSqLiteDatabase.setTransactionSuccessful();
mSqLiteDatabase.endTransaction();
}
对数据库插入测试方法稍作修改
IDaoSupport<Person> daoSupport = DaoSupportFactory.getFactoryInstance(MainActivity.this).getDao(Person.class);
// 最少的知识原则
new Thread(() -> {
int totalNum = 1000;
List<Person> personList = new ArrayList<>();
long startTime = System.currentTimeMillis();
for (int i = 0; i < totalNum; i++) {
personList.add(new Person("hjcai", i));
}
daoSupport.inert(personList);
long endTime = System.currentTimeMillis();
Log.e(TAG, " insert " + totalNum + " cost time -> " + (endTime - startTime));
}).start();
以下是修改后执行的时间
2021-03-17 21:08:52.164 8977-9012/com.example.learneassyjoke E/MyActivity: insert 1000 cost time -> 541
2021-03-17 21:09:02.490 9027-9052/com.example.learneassyjoke E/MyActivity: insert 1000 cost time -> 394
2021-03-17 21:09:07.316 9063-9088/com.example.learneassyjoke E/MyActivity: insert 1000 cost time -> 414
还记得我们上一节的操作时间都在5s以上
因此批量操作数据库时一定要使用数据库事务进行优化 数据库操作效率可以提高很多倍
参考链接 https://blog.csdn.net/hfsime/article/details/39502699
QUESTION: 为什么事务能够显著的提高数据库操作效率?
在整个数据库中,执行任何一类操作诸如sqliteDatabase.delete()都会创建一个数据库事务,存在这样一种影响效率的情况:需要一次删除N条数据,程序就会创建N次事务。而当自身使用显式的数据库事务时,这些删除数据的操作,就在一次数据库事务中执行,从而在删除(插入、更新)数据特别大的时候大大的提高效率。
2.减少反射的调用次数
在DaoSupport新增两个类变量
private static final Object[] mPutMethodArgs = new Object[2];//存储key value 如 name "hjcai"//个人觉得这个变量意义不大
// 数据库优化 缓存数据类型 减少反射的调用次数
private static final Map<String, Method> mPutMethods = new ArrayMap<>();
对contentValuesByObj稍作修改 只有第一次插入类型T时才进行反射,否则可以从mPutMethodArgs取得该类的各个filed的put方法 这样在批量操作时大大降低了反射调用的次数
private ContentValues contentValuesByObj(T obj) {
ContentValues contentValues = new ContentValues();
// 通过反射获取mClazz定义的filed(以Person为例 返回的是age 和 name字段)
Field[] fields = mClazz.getDeclaredFields();
for (Field field : fields) {
Method putMethod;
try {
// 设置权限,私有和共有都可以访问
field.setAccessible(true);
// 获取field的名称(如age)
String key = field.getName();
// 获取field的value(如30)
Object value = field.get(obj);
mPutMethodArgs[0] = key;
mPutMethodArgs[1] = value;
// 虽然使用反射会有一点性能的影响 但是影响很小 而且源码里面 activity实例的创建 View创建反射等都使用了反射 因此这里也会使用反射 获取put方法
String filedTypeName = field.getType().getName();//获取filed的数据类型(如Integer)
// filedTypeName的作用是作为存储mPutMethods的key
// 因此使用的key类似 java.lang.String int boolean等
putMethod = mPutMethods.get(filedTypeName);
if (putMethod == null) {
putMethod = ContentValues.class.getDeclaredMethod("put",
String.class, value.getClass());
// 缓存PutMethods 下次就不需要再反射了
mPutMethods.put(filedTypeName, putMethod);
}
// 通过反射执行ContentValues的putXXX方法
// 相当于调用类似 contentValues.put("age",30);
putMethod.invoke(contentValues, mPutMethodArgs);
} catch (Exception e) {
e.printStackTrace();
} finally {
mPutMethodArgs[0] = null;
mPutMethodArgs[1] = null;
}
}
return contentValues;
}
以下是测试结果
2021-03-18 19:35:27.379 11895-11920/com.example.learneassyjoke E/MyActivity: insert 1000 cost time -> 411
2021-03-18 19:35:32.796 11930-11954/com.example.learneassyjoke E/MyActivity: insert 1000 cost time -> 427
2021-03-18 19:35:38.003 11968-11993/com.example.learneassyjoke E/MyActivity: insert 1000 cost time -> 402
2021-03-18 19:35:43.261 12002-12026/com.example.learneassyjoke E/MyActivity: insert 1000 cost time -> 402
2021-03-18 19:35:48.241 12039-12063/com.example.learneassyjoke E/MyActivity: insert 1000 cost time -> 392
相对于事务,这次好像没有多大提升,但是能优化的地方也优化一下吧。
PS:以上减少反射调用的思路来自于源码View的创建过程中的inflate部分
setContentView(R.layout.activity_main); ->
getDelegate().setContentView(layoutResID);->
LayoutInflater.from(mContext).inflate(resId, contentParent);
return inflate(resource, root, root != null);
return inflate(parser, root, attachToRoot);
rInflateChildren(parser, temp, attrs, true);
rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
final View view = createViewFromTag(parent, name, context, attrs);
return createViewFromTag(parent, name, context, attrs, false);
在比较老版的API中createViewFromTag还是利用反射创建View的
private final Object[] mConstructorArgs = new Object[2];
private View createViewFromTag(Context context, String name, AttributeSet attrs) {
if (name.equals("view")) {
name = attrs.getAttributeValue((String)null, "class");
}
View view;
try {
this.mConstructorArgs[0] = context;
this.mConstructorArgs[1] = attrs;
View var4;
if (-1 == name.indexOf(46)) {
for(int i = 0; i < sClassPrefixList.length; ++i) {
view = this.createViewByPrefix(context, name, sClassPrefixList[i]);
if (view != null) {
View var6 = view;
return var6;
}
}
var4 = null;
return var4;
}
var4 = this.createViewByPrefix(context, name, (String)null);
return var4;
} catch (Exception var10) {
view = null;
} finally {
this.mConstructorArgs[0] = null;
this.mConstructorArgs[1] = null;
}
return view;
}
private static final SimpleArrayMap<String, Constructor<? extends View>> sConstructorMap = new SimpleArrayMap();
private View createViewByPrefix(Context context, String name, String prefix) throws ClassNotFoundException, InflateException {
Constructor constructor = (Constructor)sConstructorMap.get(name);
try {
if (constructor == null) {
Class<? extends View> clazz = Class.forName(prefix != null ? prefix + name : name, false, context.getClassLoader()).asSubclass(View.class);
constructor = clazz.getConstructor(sConstructorSignature);
sConstructorMap.put(name, constructor);
}
constructor.setAccessible(true);
return (View)constructor.newInstance(this.mConstructorArgs);
} catch (Exception var6) {
return null;
}
}
有没有觉得比较熟悉 没错 contentValuesByObj的缓存putMethod的思路就来自这里
3.补全删除 修改 查询的部分
查询:没有使用反射
不使用反射有2点弊端
1.数据库查询的地方必须知道想要查询的数据的定义(如Person.java)
2.如果数据的定义(如Person.java)发生修改 比如增加一个字段 删除一个字段 那么我们数据库的查询语句也需要修改
代码:在数据库查询的地方Copy一份Person.java
//该类是为了测试不使用反射而写的 写在这里是不合理的,使用反射的时候可以删除
@Deprecated
public class Person {
private String name;
private String address;
private int age;
public Person(String name, String address, int age) {
this.name = name;
this.age = age;
this.address = address;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", address='" + address + '\'' +
", age=" + age +
'}';
}
}
IDaoSupport增加方法
// 查询所有
public List<T> queryAll();
DaoSupport实现新增方法
// 查询所有
@Override
public List<T> queryAll() {
Cursor cursor = mSqLiteDatabase.query(DaoUtil.getTableName(mClazz),null,null,null,null,null,null);
return cursorToList(cursor);
}
private List<T> cursorToList(Cursor cursor) {
List<T> list = new ArrayList<>();
while (cursor.moveToNext()) {
Person person = new Person(cursor.getString(cursor
.getColumnIndex("name")),
cursor.getString(cursor
.getColumnIndex("address")),
cursor.getInt(cursor
.getColumnIndex("age"))
);
list.add((T) person);//这里也是有问题的 不够通用
}
Log.e(TAG, "cursorToList: "+list);
return list;
}
在activity增加测试方法
private void queryAll(View view) {
new Thread(() -> {
long startTime = System.currentTimeMillis();
IDaoSupport<Person> daoSupport = DaoSupportFactory.getFactoryInstance(MainActivity.this).getDao(Person.class);
List<Person> people = daoSupport.queryAll();
Log.e(TAG, "queryAll: "+people.size());
long endTime = System.currentTimeMillis();
Log.e(TAG, " queryAll cost time -> " + (endTime - startTime));
}).start();
}
实际上 不使用反射读取数据有很多弊端,之所以这里还是列出写法 是为了对比使用反射的情况,否则反射的代码很难看懂
查询:使用反射
IDaoSupport增加方法
// 查询所有 使用反射
List<T> queryAllByReflect();
DaoSupport实现新增方法
@Override
public List<T> queryAllByReflect() {
Cursor cursor = mSqLiteDatabase.query(DaoUtil.getTableName(mClazz), null, null, null, null, null, null);
return cursorToListByReflect(cursor);
}
// 多次调用反射 查询所有列表
private List<T> cursorToListByReflect(Cursor cursor) {
List<T> list = new ArrayList<>();
while (cursor.moveToNext()) {
// 通过反射创建T对象 第一次反射 以Person为例 相当于调用Person p = new Person()
T instance = null;// 要调用此方法 必须有无参构造方法
try {
instance = mClazz.newInstance();
// 反射获取T对象所有的属性 第二次反射
Field[] fields = mClazz.getDeclaredFields();
// 遍历field 从数据库取出数据填充到instance
for (Field field : fields) {
Object value = null;
field.setAccessible(true);
// 以Person为例 它有个属性String name, fieldName 则是 name
String fieldName = field.getName();
// 查询当前属性在数据库中所在的列 下面则相当于调用cursor.getColumnIndex("name")
int index = cursor.getColumnIndex(fieldName);
if (index != -1) {
// 根据对象T的属性类型推算cursor的方法 如cursor.getString cursor.getInt
Method cursorGetColumnMethod = convertType2CursorMethod(field.getType());// 在该方法进行第三次反射
// 通过反射获取 value 第四次反射 相当于cursor.getString(cursor.getColumnIndex("name"))//cursor.getColumnIndex("name")在上面已经调用
value = cursorGetColumnMethod.invoke(cursor, index);
if (value == null) {
continue;
}
// 处理一些特殊的部分
if (field.getType() == boolean.class || field.getType() == Boolean.class) {
// sqlite不支持bool类型 使用0代表false 1代表true
if ("0".equals(String.valueOf(value))) {
value = false;
} else if ("1".equals(String.valueOf(value))) {
value = true;
}
} else if (field.getType() == char.class || field.getType() == Character.class) {
// sqlite不支持char类型 取第0位即可
value = ((String) value).charAt(0);
} else if (field.getType() == Date.class) {
// sqlite不支持Date类型 存储的是时间戳
long date = (Long) value;
if (date <= 0) {
value = null;
} else {
value = new Date(date);
}
}
} else {
Log.e(TAG, "cursorToList: 该属性没有存储在数据库中");
continue;
}
// 反射注入属性的值(以person为例,类似调用person.setName(value))
// 第五次反射
field.set(instance, value);
}
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
list.add(instance);
}
cursor.close();
Log.e(TAG, "cursorToList: " + list.size());
return list;
}
// 获取cursor的方法
private Method convertType2CursorMethod(Class<?> type) throws NoSuchMethodException {
// 根据数据类型 得到不同cursor方法
String methodName = getColumnMethodName(type);
// 第三次反射 根据方法名和参数类型调用
Method method = Cursor.class.getMethod(methodName, int.class);
return method;
}
private String getColumnMethodName(Class<?> fieldType) {
String typeName;
if (fieldType.isPrimitive()) { // 如果是基本数据类
// 将int boolean float等转换为对象的形式 即首字母大写
/*
* @see java.lang.Boolean#TYPE
* @see java.lang.Character#TYPE
* @see java.lang.Byte#TYPE
* @see java.lang.Short#TYPE
* @see java.lang.Integer#TYPE
* @see java.lang.Long#TYPE
* @see java.lang.Float#TYPE
* @see java.lang.Double#TYPE
* @see java.lang.Void#TYPE
*/
typeName = DaoUtil.capitalize(fieldType.getName());
} else {
typeName = fieldType.getSimpleName();
}
// 上面获取对象T的Java的get方法,如Integer String Boolean 下面需要转成SQLite里面的数据类型
// 如getBoolen转换为数据库的getInt getChar转换为数据库的getString
String methodName = "get" + typeName;
if ("getBoolean".equals(methodName)) {
methodName = "getInt";
} else if ("getChar".equals(methodName) || "getCharacter".equals(methodName)) {
methodName = "getString";
} else if ("getDate".equals(methodName)) {
methodName = "getLong";
} else if ("getInteger".equals(methodName)) {
methodName = "getInt";
}
return methodName;
}
还有个工具类方法
// 使得参数string开头字母大写
public static String capitalize(String string) {
if (!TextUtils.isEmpty(string)) {
// 前面把首字符大写 后面将剩余部分拼接上
return string.substring(0, 1).toUpperCase(Locale.US) + string.substring(1);
}
return string == null ? null : "";
}
这里面多次用到反射,其实逻辑不是很复杂 但是要又耐心看,可以对比没有反射的方法,更容易理解
注意要给Person类增加无参构造方法 否则反射调用不到无参构造方法会报错
public Person(){
}
最后就是测试了
private void queryAllReflect(View view) {
new Thread(() -> {
long startTime = System.currentTimeMillis();
IDaoSupport<Person> daoSupport = DaoSupportFactory.getFactoryInstance(MainActivity.this).getDao(Person.class);
List<Person> people = daoSupport.queryAllByReflect();
Log.e(TAG, "queryAllReflect: " + people);
long endTime = System.currentTimeMillis();
Log.e(TAG, " queryAllReflect cost time -> " + (endTime - startTime));
}).start();
}
后面还有按条件查询 更新 条件删除没有做,还有将网络与数据库结合起来的缓存机制,最后一篇数据库部分会说这些东西