一、背景
某天在看到google推荐的官方架构中对数据层的介绍提到:
数据层的最佳实践是离线优先,即单一数据来源来自数据库,以数据库的数据为驱动UI变化的核心
所以当网络数据返回插入数据库以后,怎么监听数据库的变化成为了解决问题的关键
二、监听数据库变化
我们知道在Android中可以使用ContentResolver监听URI的变化来实现对数据库变化的监听 juejin.cn/post/699330…
于是就想集成room以后应该可以快速创建URI来帮助我们实现对数据的监听,经过一顿查资料一顿搜索发现有点南辕北辙
按理说,伴随着MVVM架构一起出现的jitpack一部分的room理应也想到了这里,是否我们在返回的时候直接添加一个LiveData/Flow就能监听数据库的改变了呢,尝试了一下确实如此,于是好奇心趋势我开始探索实现原理
三、原理探究
首先room是一款ORM(对象-关系映射)型数据库,用法参考developer.android.com/training/da…,基于注解的方式也就意味着肯定是基于APT,会在编译时自动生成代码
我们编译之后看代码
可以看到关键点在于InvalidationTracker
(无效跟踪器),我们追踪源码看到通过SQL定义了增删改的三个触发器(触发器是一种机制,可以在insert,update,delete
之前或之后触发并执行触发器中定义的SQL语句,具体的操作可以自行百度)
private static final String[] TRIGGERS = new String[]{"UPDATE", "DELETE", "INSERT"};
private static final String UPDATE_TABLE_NAME = "room_table_modification_log";
private static final String TABLE_ID_COLUMN_NAME = "table_id";
private static final String INVALIDATED_COLUMN_NAME = "invalidated";
@VisibleForTesting
static final String RESET_UPDATED_TABLES_SQL = "UPDATE " + UPDATE_TABLE_NAME
+ " SET " + INVALIDATED_COLUMN_NAME + " = 0 WHERE " + INVALIDATED_COLUMN_NAME + " = 1 ";
@VisibleForTesting
static final String SELECT_UPDATED_TABLES_SQL = "SELECT * FROM " + UPDATE_TABLE_NAME
+ " WHERE " + INVALIDATED_COLUMN_NAME + " = 1;";
private void startTrackingTable(SupportSQLiteDatabase writableDb, int tableId) {
writableDb.execSQL(
"INSERT OR IGNORE INTO " + UPDATE_TABLE_NAME + " VALUES(" + tableId + ", 0)");
final String tableName = mTableNames[tableId];
StringBuilder stringBuilder = new StringBuilder();
for (String trigger : TRIGGERS) {
stringBuilder.setLength(0);
stringBuilder.append("CREATE TEMP TRIGGER IF NOT EXISTS ");
appendTriggerName(stringBuilder, tableName, trigger);
stringBuilder.append(" AFTER ")
.append(trigger)
.append(" ON `")
.append(tableName)
.append("` BEGIN UPDATE ")
.append(UPDATE_TABLE_NAME)
.append(" SET ").append(INVALIDATED_COLUMN_NAME).append(" = 1")
.append(" WHERE ").append(TABLE_ID_COLUMN_NAME).append(" = ").append(tableId)
.append(" AND ").append(INVALIDATED_COLUMN_NAME).append(" = 0")
.append("; END");
writableDb.execSQL(stringBuilder.toString());
}
}
private static void appendTriggerName(StringBuilder builder, String tableName,
String triggerType) {
builder.append("`")
.append("room_table_modification_trigger_")
.append(tableName)
.append("_")
.append(triggerType)
.append("`");
}
private void stopTrackingTable(SupportSQLiteDatabase writableDb, int tableId) {
final String tableName = mTableNames[tableId];
StringBuilder stringBuilder = new StringBuilder();
for (String trigger : TRIGGERS) {
stringBuilder.setLength(0);
stringBuilder.append("DROP TRIGGER IF EXISTS ");
appendTriggerName(stringBuilder, tableName, trigger);
writableDb.execSQL(stringBuilder.toString());
}
}
并且在数据改变的时候对room_table_modification_log
这个表进行了更新,标识哪个tab发生了改变
既然如此,下一步肯定就是查询更新的表并想办法通知LiveData的改变,于是通过SELECT_UPDATED_TABLES_SQL
的调用栈反向追踪到refreshVersionsAsync
,注释中也指明了源头是来自endTransaction()
方法的调用
而endTransation
顾名思义就是每次执行完SQL的事务结束方法,来自代码生成的dao层生成的文件中
于是整个链路就清晰了,每次数据库进行了增删改查操作之后,在真正插入数据之前触发器就会执行相关操作修改room_table_modification_log
表中的记录,记录发生变动的表的id和对应状态,等插入完成执行__db.endTransaction()
操作时,再去查询room_table_modification_log
表执行observer
中的逻辑
那么observe是从什么地方注册的呢?
让我们再回到文章一开始的截图生成查询语句的地方,在刚开始会执行createLiveData,创建一个RoomTrackingLiveData
并把我们的查询方法通过computeFunction
传进去
读到这里不禁要吐槽一下google的研发应该也会copy…好几处的runnable命名都一样不仔细看真容易陷进去…
四、总结
当我们执行数据库操作(增删改)时,Room 框架会自动触发 LiveData 通知数据的变化。LiveData 会自动检测数据库变化并通知已注册的观察者进行更新,Flow跟Rxjava也是同理(需要导入相对应的扩展包)。