Android Jetpack组件之数据库Room详解(三)
本文涉及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>>() {@Overridepublic 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方法代碼如下:
@Overridepublic 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>>() {@Overridepublic 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();}}@Overrideprotected void finalize() {_statement.release();}});} 復制代碼從上面的代碼可以看出監聽數據庫變化核心類是InvalidationTracker,InvalidationTracker類是RoomDatabase的構造器創建的, RoomDatabase中的createInvalidationTracker方法是抽象類,是由開發者繼承RoomDatabase,最終createInvalidationTracker實現是apt編譯時期自動生成的類實現的, 接著看代碼:
//本文createInvalidationTracker()的真正實現是AppDatabase_Impl類,不了解的可以看一下上一篇文章@Overrideprotected 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是表idmTableIdLookup = 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是數據表數組里的indexmTableIdLookup.put(tableName, id);mTableNames[id] = tableName;String shadowTableName = shadowTablesMap.get(tableNames[id]);if (shadowTableName != null) {mShadowTableLookup.append(id, shadowTableName.toLowerCase(Locale.US));}}//一個set, 存儲booleanmTableInvalidStatus = 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_logdatabase.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觀察一個表時就不為nullif (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)) {//異步執行一個RunnablemDatabase.getQueryExecutor().execute(mRefreshRunnable);} }Runnable mRefreshRunnable = new Runnable() {@Overridepublic 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方法 @Overridepublic LiveData<List<User>> getUsersLiveData() {...//調用InvalidationTracker.createLiveData方法創建LiveDatareturn __db.getInvalidationTracker().createLiveData(new String[]{"user"}, new Callable<List<User>>() {@Overridepublic 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繼承LiveDatareturn 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) {@Overridepublic void onInvalidated(@NonNull Set<String> tables) {//主線程執行一個Runnable, onInvalidated方法在某個表數據有變化是會觸發ArchTaskExecutor.getInstance().executeOnMainThread(mInvalidationRunnable);}};}//onActive方法繼承LiveData, 當activity或者fragment出入onstart會被觸發一次,//關于LiveData的不細說,可以看之前的文章@Overrideprotected void onActive() {super.onActive();mContainer.onActive(this);//異步執行mRefreshRunnablemDatabase.getQueryExecutor().execute(mRefreshRunnable);} } //RoomTrackingLiveData的mRefreshRunnable和mInvalidationRunnablefinal Runnable mRefreshRunnable = new Runnable() {@WorkerThread@Overridepublic 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是原子鎖, 初始值是truewhile (mInvalid.compareAndSet(true, false)) {computed = true;try {//執行Callable接口, 這里例子就是執行sql語句查詢Uservalue = 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@Overridepublic void run() {boolean isActive = hasActiveObservers();// mInvalid是原子鎖if (mInvalid.compareAndSet(false, true)) {if (isActive) {//異步執行mRefreshRunnablemDatabase.getQueryExecutor().execute(mRefreshRunnable);}}}}; 復制代碼mInvalidationRunnable 在某個表數據有變化是會觸發執行,而mInvalidationRunnable的實現又是添加一個mRefreshRunnable異步執行, 而mInvalid這個原子鎖, 鎖得是mComputeFunction.call(), 保證多線程下查詢只能執行一個。 上面代碼 mDatabase.getInvalidationTracker().addWeakObserver(mObserver)還沒解答,繼續看InvalidationTracker.addWeakObserver方法。
public void addWeakObserver(Observer observer) {//WeakObserver是對observer一個wrapper,作用是防止內存泄露, //WeakObserver的寫法不錯,又學到啦, 里面是一個弱引的observeraddObserver(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已經存在,就會返回前一個valuecurrentObserver = 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等
- 數據索引的創建, 子查詢等。
轉載于:https://juejin.im/post/5cb5534df265da039a3d64b6
總結
以上是生活随笔為你收集整理的Android Jetpack组件之数据库Room详解(三)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: TensorFlow从1到2(四)时尚单
- 下一篇: SQL-33 创建一个actor表,包含