Android Jetpack组件之数据库Room详解(三)

3,866 阅读9分钟

本文涉及Library的版本如下:

  • androidx.room:room-runtime:2.1.0-alpha03
  • androidx.room:room-compiler:2.1.0-alpha03(注解编译器)

Room对LiveData扩展

下面先列一个room中使用livedata的例子:

@Dao
public interface UserDao {
    @Query("SELECT * FROM user")
    LiveData<List<User>> getUsersLiveData();
}    

public class RoomActivity extends AppCompatActivity {
    
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mRoomModel = ViewModelProviders.of(this).get(RoomModel.class);
        mRoomModel.getUsersLiveData().observe(this, new Observer<List<User>>() {
            @Override
            public void onChanged(List<User> users) {
                adapter.setData(users); // 数据回调,这里只要数据库User有数据变化,每次都会回调
            }
        });
    }
}    

public class RoomModel extends AndroidViewModel {
    private final AppDatabase mAppDatabase; //AppDatabase是继承RoomDatabase的抽象类
    public RoomModel(@NonNull Application application) {
        super(application);
        mAppDatabase = AppDatabase.getInstance(this.getApplication());
    }

    public LiveData<List<User>> getUsersLiveData() {
        return mAppDatabase.userDao().getUsersLiveData();
    }
}    

只要数据库的数据有变化, 上面代码中onChanged就会回调,但是, 不是什么时候都回调,当activity处理onstop是不会回调,但是activity重新走onstart后,数据库有增删改还是会回调的。这里的效果有点类似安卓里的Loader, 使用过Loader的都知道,Loader是会监听contentprovier的一条uri, 有数据变更, 处于onstart状态的activity,Loader会重新加载一个数据。接下来看一下Room是怎么监听数据库变化的。

UserDao_Impl.getUsersLiveData方法代码如下:

  @Override
  public LiveData<List<User>> getUsersLiveData() {
    final String _sql = "SELECT * FROM user";
    //通过sql创建SQLite查询执行程序
    final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 0);
    //__db.getInvalidationTracker()返回是InvalidationTracker类,是RoomDatabase的一个成员变量
    //调用InvalidationTracker.createLiveData方法创建LiveData对象
    return __db.getInvalidationTracker().createLiveData(new String[]{"user"}, new Callable<List<User>>() {
      @Override
      public List<User> call() throws Exception {
        //下面代码不多说,执行sql语句,组装成List<User>返回
        final Cursor _cursor = DBUtil.query(__db, _statement, false);
        try {
          final int _cursorIndexOfFirstName = CursorUtil.getColumnIndexOrThrow(_cursor, "first_name");
          final int _cursorIndexOfName = CursorUtil.getColumnIndexOrThrow(_cursor, "name");
          final int _cursorIndexOfId = CursorUtil.getColumnIndexOrThrow(_cursor, "id");
          final List<User> _result = new ArrayList<User>(_cursor.getCount());
          while(_cursor.moveToNext()) {
            final User _item;
            _item = new User();
            _item.firstName = _cursor.getString(_cursorIndexOfFirstName);
            _item.name = _cursor.getString(_cursorIndexOfName);
            _item.id = _cursor.getInt(_cursorIndexOfId);
            _result.add(_item);
          }
          return _result;
        } finally {
          _cursor.close();
        }
      }

      @Override
      protected void finalize() {
        _statement.release();
      }
    });
  }

从上面的代码可以看出监听数据库变化核心类是InvalidationTracker,InvalidationTracker类是RoomDatabase的构造器创建的, RoomDatabase中的createInvalidationTracker方法是抽象类,是由开发者继承RoomDatabase,最终createInvalidationTracker实现是apt编译时期自动生成的类实现的, 接着看代码:

//本文createInvalidationTracker()的真正实现是AppDatabase_Impl类,不了解的可以看一下上一篇文章

  @Override
  protected InvalidationTracker createInvalidationTracker() {
    final HashMap<String, String> _shadowTablesMap = new HashMap<String, String>(0);
    HashMap<String, Set<String>> _viewTables = new HashMap<String, Set<String>>(0);
    //User, Favorite是表名
    return new InvalidationTracker(this, _shadowTablesMap, _viewTables, "User","Favorite");
  }

//InvalidationTracker的构造器
public InvalidationTracker(RoomDatabase database, Map<String, String> shadowTablesMap,
                           Map<String, Set<String>> viewTables, String... tableNames) {
    mDatabase = database;
    //看名字ObservedTableTracker是一个观察表的跟踪者, 先跳过
    mObservedTableTracker = new ObservedTableTracker(tableNames.length);
    //一个Map, Key是表名,Value是表id
    mTableIdLookup = new ArrayMap<>();
    mShadowTableLookup = new SparseArrayCompat<>(shadowTablesMap.size());
    mViewTables = viewTables;
    //看名字是一个失效LiveData容器,先跳过
    mInvalidationLiveDataContainer = new InvalidationLiveDataContainer(mDatabase);
    final int size = tableNames.length;
    mTableNames = new String[size];
    // 遍历数据表个数
    for (int id = 0; id < size; id++) { 
        final String tableName = tableNames[id].toLowerCase(Locale.US);
        /Key是表名,并且转成全小写,Value是表id, id是数据表数组里的index
        mTableIdLookup.put(tableName, id);
        mTableNames[id] = tableName;
        String shadowTableName = shadowTablesMap.get(tableNames[id]);
        if (shadowTableName != null) {
            mShadowTableLookup.append(id, shadowTableName.toLowerCase(Locale.US));
        }
    }
    //一个set, 存储boolean
    mTableInvalidStatus = new BitSet(tableNames.length);
}

//InvalidationTracker的internalInit方法
//该方法会在数据库打开的时候调用, 是在SQLiteOpenHelper.onOpen方法时调用
void internalInit(SupportSQLiteDatabase database) {
        synchronized (this) {
            if (mInitialized) {
                Log.e(Room.LOG_TAG, "Invalidation tracker is initialized twice :/.");
                return;
            }

            database.beginTransaction();
            try {
                //PRAGMA是一个特殊命令,通常用于改变数据库的设置
                //临时存储设置为内存模式
                database.execSQL("PRAGMA temp_store = MEMORY;");
                //启用递归触发器
                database.execSQL("PRAGMA recursive_triggers='ON';");
                //CREATE_TRACKING_TABLE_SQL是个sql语句字符串, 语句如下:
                //CREATE TEMP TABLE room_table_modification_log (table_id INTEGER PRIMARY 					//KEY, invalidated INTEGER NOT NULL DEFAULT 0 )
                //创建一个临时表room_table_modification_log
                database.execSQL(CREATE_TRACKING_TABLE_SQL);
                database.setTransactionSuccessful();
            } finally {
                database.endTransaction();
            }
            //同步数据库触发器
            syncTriggers(database);
            mCleanupStatement = database.compileStatement(RESET_UPDATED_TABLES_SQL);
            mInitialized = true; // 初始化的标志,只初始化一次
        }
    }   

从上面的代码可以知道当数据打开是调用internalInit方法,执行sql语句把临时存储设置为内存模式, 创建了一个名叫room_table_modification_log的临时表,临时表使用CREATE TEMP TABLE 语句创建的,临时表不会持久化,数据库关闭就不存在啦。这个临时表只有两个字段,分别是table_id和invalidated(是否无效的标志)。接着看syncTriggers方法

void syncTriggers(SupportSQLiteDatabase database) {
    while (true) {
        Lock closeLock = mDatabase.getCloseLock();
        closeLock.lock();
        try {
            //tablesToSync存储了表相应触发器的状态
            final int[] tablesToSync = mObservedTableTracker.getTablesToSync();
            // 首次初始化tablesToSync为null, 当mObservedTableTracker观察一个表时就不为null
            if (tablesToSync == null) { 
                return;
            }
            final int limit = tablesToSync.length;
            database.beginTransaction();
            try {
                for (int tableId = 0; tableId < limit; tableId++) {
                    switch (tablesToSync[tableId]) {
                        case ObservedTableTracker.ADD:
                            //开始跟踪表
                            startTrackingTable(database, tableId);
                            break;
                        case ObservedTableTracker.REMOVE:
                            //停止跟踪表
                            stopTrackingTable(database, tableId);
                            break;
                    }
                }
                database.setTransactionSuccessful();
            } finally {
                database.endTransaction();
            }
            mObservedTableTracker.onSyncCompleted();
        } finally {
            closeLock.unlock();
        }
    }
}

//开始跟踪表
private void startTrackingTable(SupportSQLiteDatabase writableDb, int tableId) {
    //给临时表room_table_modification_log插入数据,(tableId, 0)
        writableDb.execSQL(
                "INSERT OR IGNORE INTO " + UPDATE_TABLE_NAME + " VALUES(" + tableId + ", 0)");
        final String tableName = mShadowTableLookup.get(tableId, mTableNames[tableId]);
        StringBuilder stringBuilder = new StringBuilder();
    	//TRIGGERS是一个字符串数组,分别是UPDATE、DELETE、INSERT
    	//遍历TRIGGERS,分别为该表创建3个触发器,分别是更新触发器、删除触发器、插入触发器
        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());
        }
    }

startTrackingTable方法为一个表创建3个触发器,分别是更新触发器、删除触发器、插入触发器。对应stopTrackingTablefang方法就是删除触发器,对sqlite触发器本文不详细说明,结合上面代码,以更新触发器为例,简单介绍一下:

//结合上面代码, 创建更新触发器的sql如下:
CREATE TEMP TRIGGER IF NOT EXISTS  room_table_modification_trigger_表名_类型 AFTER UPDATE ON 表名  
BEGIN 
  UPDATE room_table_modification_log SET invalidated = 1 WHERE table_id = ${tableId} AND invalidated = 0; 
END

上面sql语句中room_table_modification_trigger 加表名加类型是触发器名字, "AFTER UPDATE ON 表名"意思是当某个表更新数据后触发, 语句中 BEGIN 与 END之间语句是表更新后执行什么操作。细看BEGIN 与 END之间语句很好理解,根据tableId把临时room_table_modification_log表的invalidated由0改1。所以当某个表数据有更新、删除、插入操作时,利用触发器去修改一个临时日志表的一个值说明是该表的数据有改变,然后去监听这个表临时的表invalidated这个值就可以知道哪个表数据改变啦, 不多说接着看一下源码怎么监听临时表的。

//InvalidationTracker的refreshVersionsSync方法
public void refreshVersionsAsync() {
  // TODO we should consider doing this sync instead of async.
  if (mPendingRefresh.compareAndSet(false, true)) {
    //异步执行一个Runnable
    mDatabase.getQueryExecutor().execute(mRefreshRunnable);
  }
}

 Runnable mRefreshRunnable = new Runnable() {
        @Override
        public void run() {
          	...
            //省略了一些细节代码
            //checkUpdatedTable方法,检查表有没有数据变化
            hasUpdatedTable = checkUpdatedTable();
            if (hasUpdatedTable) {
                synchronized (mObserverMap) {
                    //有变化就遍历ObserverWrapper观察者
                    for (Map.Entry<Observer, ObserverWrapper> entry : mObserverMap) {
                        entry.getValue().notifyByTableVersions(mTableInvalidStatus);
                    }
                }
            }
        }
   		
   		private boolean checkUpdatedTable() {
            boolean hasUpdatedTable = false;
        		//执行一个sql语句, 这个语句是:
        		//SELECT * FROM room_table_modification_log WHERE invalidated = 1
        		//查询有变化的数据
            Cursor cursor = mDatabase.query(new SimpleSQLiteQuery(SELECT_UPDATED_TABLES_SQL));
            try {
                while (cursor.moveToNext()) {
                    final int tableId = cursor.getInt(0);
                    mTableInvalidStatus.set(tableId);
                    //一个标志,hasUpdatedTable=true,表有增删改的变化
                    hasUpdatedTable = true;
                }
            } finally {
                cursor.close();
            }
            if (hasUpdatedTable) {
                //mCleanupStatement在internalInit方法里初始化, mCleanupStatement是SupportSQLiteStatement对于,调用executeUpdateDelete()执行一个语句, 语句如下:
              	//UPDATE room_table_modification_log SET invalidated = 0 
                //WHERE invalidated = 1
                //从sql语句来看,是还原invalidated,还原临时表的状态
                mCleanupStatement.executeUpdateDelete();
            }
            return hasUpdatedTable;
        }
 }   

经过上面源码,思路已经比较清晰啦,利用触发器监听某个表的更新、删除、插入, 监听到日志记录在一个临时日志表里,然后再去不停地监听临时日志表就可以知道某个表数据是否改变啦。InvalidationTracker的refreshVersionsSync方法就是监听临时日志表的方法,这个方法调用时机是在RoomDatabase.endTransaction方法里,为什么要放事务结束的方法里呢?再简单仔细看了一源码,发现Room的所有增删改的操作都是通过开启事务来执行的。 回头看一下LiveData的创建过程。

//UserDao_Impl.getUsersLiveData方法
@Override
  public LiveData<List<User>> getUsersLiveData() {
    ...
    //调用InvalidationTracker.createLiveData方法创建LiveData
    return __db.getInvalidationTracker().createLiveData(new String[]{"user"}, new Callable<List<User>>() {
      @Override
      public List<User> call() throws Exception {
        ....
        //省略这代码,这里方法逻辑,执行sql语句查询,组装成List<User>返回
      }
    });
  }

//InvalidationTracker.createLiveData方法会调用InvalidationLiveDataContainer.create
<T> LiveData<T> create(String[] tableNames, Callable<T> computeFunction) {
  //computeFunction是Callable接口回调, tableNames是表名, RoomTrackingLiveData继承LiveData
  return new RoomTrackingLiveData<>(mDatabase, this, computeFunction, tableNames);
}

//RoomTrackingLiveData类
class RoomTrackingLiveData<T> extends LiveData<T> {
  
    //RoomTrackingLiveData构造器
    RoomTrackingLiveData(
            RoomDatabase database,
            InvalidationLiveDataContainer container,
            Callable<T> computeFunction,
            String[] tableNames) {
        mDatabase = database;
        mComputeFunction = computeFunction;
        mContainer = container;
      
        //初始化一个InvalidationTracker的观察者
        mObserver = new InvalidationTracker.Observer(tableNames) {
            @Override
            public void onInvalidated(@NonNull Set<String> tables) {
                //主线程执行一个Runnable, onInvalidated方法在某个表数据有变化是会触发
                ArchTaskExecutor.getInstance().executeOnMainThread(mInvalidationRunnable);
            }
        };
    }

    //onActive方法继承LiveData, 当activity或者fragment出入onstart会被触发一次,
    //关于LiveData的不细说,可以看之前的文章
    @Override
    protected void onActive() {
        super.onActive();
        mContainer.onActive(this);
        //异步执行mRefreshRunnable
        mDatabase.getQueryExecutor().execute(mRefreshRunnable);
    }
} 

//RoomTrackingLiveData的mRefreshRunnable和mInvalidationRunnable
    final Runnable mRefreshRunnable = new Runnable() {
        @WorkerThread
        @Override
        public void run() {
            if (mRegisteredObserver.compareAndSet(false, true)) {
               //添加一个观察者, 为什么需要添加一个观察者,等会解析,先往下看
                mDatabase.getInvalidationTracker().addWeakObserver(mObserver);
            }
            boolean computed;
           //do 循环
            do {
                computed = false;
                //mComputing是原子锁,保证多个线程执行时,只能一个线程执行循环
                if (mComputing.compareAndSet(false, true)) {
                   //mComputing初始值是false, 一次能进来
                    try {
                        T value = null;
                       //mInvalid是原子锁, 初始值是true
                        while (mInvalid.compareAndSet(true, false)) {
                            computed = true;
                            try {
                               //执行Callable接口, 这里例子就是执行sql语句查询User
                                value = mComputeFunction.call();
                            } catch (Exception e) {
                            }
                        }
                        if (computed) {
                           //执行完查询语句,通知livedata的观察者,会把value转回主线程
                           //看回本文的开头,会触发onChanged(value),通知ui刷新
                            postValue(value);
                        }
                    } finally {
                        // 释放锁
                        mComputing.set(false);
                    }
                }
            } while (computed && mInvalid.get());
        }
    };

    final Runnable mInvalidationRunnable = new Runnable() {
        @MainThread
        @Override
        public void run() {
            boolean isActive = hasActiveObservers();
            // mInvalid是原子锁
            if (mInvalid.compareAndSet(false, true)) {
                if (isActive) {
                   //异步执行mRefreshRunnable
                    mDatabase.getQueryExecutor().execute(mRefreshRunnable);
                }
            }
        }
    };

mInvalidationRunnable 在某个表数据有变化是会触发执行,而mInvalidationRunnable的实现又是添加一个mRefreshRunnable异步执行, 而mInvalid这个原子锁, 锁得是mComputeFunction.call(), 保证多线程下查询只能执行一个。 上面代码 mDatabase.getInvalidationTracker().addWeakObserver(mObserver)还没解答,继续看InvalidationTracker.addWeakObserver方法。

public void addWeakObserver(Observer observer) {
  //WeakObserver是对observer一个wrapper,作用是防止内存泄露, 
  //WeakObserver的写法不错,又学到啦, 里面是一个弱引的observer
  addObserver(new WeakObserver(this, observer));
}

public void addObserver(@NonNull Observer observer) {
  final String[] tableNames = resolveViews(observer.mTables);
  int[] tableIds = new int[tableNames.length];
  final int size = tableNames.length;

  for (int i = 0; i < size; i++) {
    Integer tableId = mTableIdLookup.get(tableNames[i].toLowerCase(Locale.US));
    if (tableId == null) {
      throw new IllegalArgumentException("There is no table with name " + tableNames[i]);
    }
    tableIds[i] = tableId;
  }
  //tableIds数组存储了所有表id
  //ObserverWrapper是对observer进行包装
  ObserverWrapper wrapper = new ObserverWrapper(observer, tableIds, tableNames);
  ObserverWrapper currentObserver;
  synchronized (mObserverMap) {
    //mObserverMap是一个hashMap, observer为key,putIfAbsent方法,如果key不存在
    //就返回null, 如果key已经存在,就会返回前一个value
    currentObserver = mObserverMap.putIfAbsent(observer, wrapper);
  }
  if (currentObserver == null && mObservedTableTracker.onAdded(tableIds)) {
    //syncTriggers这个方法本文前面分析过了,创建数据库监听触发器,
    //syncTriggers在InvalidationTracker初始化调用过,但是初始化时, mObservedTableTracker里没有表id,要等mObservedTableTracker.onAdded调用后,才真正能创建触发器监听表
    syncTriggers();
  }
}

LiveData在Room的实现整个流程分析结束。来总结一下

总结

利用触发器监听某个表的更新、删除、插入, 监听到日志记录在一个临时日志表里,然后在增删改操作后去查询临时日志表,查某个表数据有改变后,去通知Livedata重新执行Callable.call方法,然后重新查询数据库,最后通知UI更新数据。

Room还有许多额外功能还可以学习:

  • Room还支持多个进程监听表变更,具体可以细看MultiInstanceInvalidationClient, MultiInstanceInvalidationClient会涉及到一些aidl等
  • Room在建表时还有很多其他注解在某些场景用,还有数据库视图等, 例如@Embedded, @DatabaseView @ForeignKey等
  • 数据索引的创建, 子查询等。