Room是怎样和LiveData结合使用的?(源码分析)
前言
之前寫項目的時候,對于數據庫的操作不是特別多,能避免就盡量避免,并且一直想不到比較好的方法去組織網絡數據、本地數據的邏輯。所以在最近的面試中時,問及項目中的數據庫實現,以及比較好用的數據庫的框架及其實現原理時,我就只答道之前在《第一行代碼》中看到了的LitePal,但源碼就...所以這次來惡補一次數據庫。幾經搜索,云比較,比較青睞官方Jetpack組件中的Room。
Room簡介
Room框架是使用生成代碼的方式在編譯時生成CRUD的代碼,因此性能是遠遠好過通過反射實現的ORM框架。但是事實上,Room最吸引我的地方不止是性能,Room對架構組件(LiveData)、RxJava等流行框架做了適配。例如,Room中的查詢操作可以返回一個LiveData,并且,每一次RUD操作,都會更新LiveData。這可以大大簡化本地、內存、網絡多級緩存的實現,具體官方也給出了一系列Demo,并且隨時都在隨著框架或者根據PR更新,強烈推薦研究這些Demo!
注
本文主要是對Room中與LiveData的聯動作出分析,閱讀本文前建議先熟悉Room的基本使用,建議看一下與LiveData配合使用的Demo。
正文
創建相關類
AppDatabase.kt
abstract class AppDatabase : RoomDatabase() {abstract fun bookDao(): BookDao } 復制代碼Book.kt
interface BookDao {fun insert(book: Book): Longfun delete(book: Book)fun queryById(id: Long): LiveData<Book> } 復制代碼使用數據庫:
val db = Room.databaseBuilder(applicationContext, AppDatabase::class.java, "test.db").build()db.bookDao().queryById(1).observe(this, Observer {// do something when book update}) 復制代碼這樣在Observer里面就可以接收到任何時候數據庫id=1的數據修改操作了。
生成代碼并分析
Build -> Make Project 編譯,會生成Room相關代碼,如果是kapt的話,生成代碼目錄應該是{項目目錄}/app/build/generated/source/kapt/debug/{包路徑}/下。 我們可以看到生成了AppDatabase_Impl和BookDao_Impl兩個代碼文件,分別對應前面貼出來的AppDatabase的實現類和BookDao的實現類。
AppDatabase_Impl則是表的創建、刪除相關代碼,Dao則是具體表的CRUD操作。這里我們重點關系生成的查詢方法。 BookDao_Impl# @Override public LiveData<Book> queryById(final long id) {final String _sql = "select * from book where id = ?";final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 1);int _argIndex = 1;_statement.bindLong(_argIndex, id);return __db.getInvalidationTracker().createLiveData(new String[]{"book"}, new Callable<Book>() {@Overridepublic Book call() throws Exception {final Cursor _cursor = DBUtil.query(__db, _statement, false);try {final int _cursorIndexOfId = CursorUtil.getColumnIndexOrThrow(_cursor, "id");final int _cursorIndexOfName = CursorUtil.getColumnIndexOrThrow(_cursor, "name");final int _cursorIndexOfAuthor = CursorUtil.getColumnIndexOrThrow(_cursor, "author");final int _cursorIndexOfPrice = CursorUtil.getColumnIndexOrThrow(_cursor, "price");final Book _result;if (_cursor.moveToFirst()) {final long _tmpId;_tmpId = _cursor.getLong(_cursorIndexOfId);final String _tmpName;_tmpName = _cursor.getString(_cursorIndexOfName);final String _tmpAuthor;_tmpAuthor = _cursor.getString(_cursorIndexOfAuthor);final float _tmpPrice;_tmpPrice = _cursor.getFloat(_cursorIndexOfPrice);_result = new Book(_tmpId, _tmpName, _tmpAuthor, _tmpPrice);} else {_result = null;}return _result;} finally {_cursor.close();}}@Overrideprotected void finalize() {_statement.release();}}); } 復制代碼注意這一行
return __db.getInvalidationTracker().createLiveData(...); 復制代碼我們跟進去,最終創建的是一個RoomTrackingLiveData,是一個繼承了LiveData的類。下面是它的構造方法。從構造方法來看,比較可疑的對象的是InvalidationTracker.Observer這個類,并且實現十有八九是觀察者模式。而最后的回調也多半是onInvalidated方法。
@SuppressLint("RestrictedApi") RoomTrackingLiveData(RoomDatabase database,InvalidationLiveDataContainer container,Callable<T> computeFunction,String[] tableNames) {mDatabase = database;mComputeFunction = computeFunction;mContainer = container;mObserver = new InvalidationTracker.Observer(tableNames) {@Overridepublic void onInvalidated(@NonNull Set<String> tables) {ArchTaskExecutor.getInstance().executeOnMainThread(mInvalidationRunnable);}}; } 復制代碼而在RoomTrackingLiveData中,重寫了onActive方法。其中mContainer是InvalidationLiveDataContainer,文檔上有寫僅僅是維護LiveData的強引用,防止正在使用的LiveData被回收,跟本文目標沒關系,可忽略。而后面的就有意思了,通過Excutor執行了一個任務,所以,我們來看一下這個任務把。
protected void onActive() {super.onActive();mContainer.onActive(this);mDatabase.getQueryExecutor().execute(mRefreshRunnable); } 復制代碼mRefreshRunnable#run()
// mRegisteredObserver是否注冊的標志 if (mRegisteredObserver.compareAndSet(false, true)) {mDatabase.getInvalidationTracker().addWeakObserver(mObserver); } boolean computed; do {computed = false;if (mComputing.compareAndSet(false, true)) {try {T value = null;while (mInvalid.compareAndSet(true, false)) {computed = true;try {// Dao實現類中返回LiveData時傳入的一個參數,用于查詢,并將數據組裝成一個實體類value = mComputeFunction.call();} catch (Exception e) {throw new RuntimeException("Exception while computing database"+ " live data.", e);}}if (computed) {postValue(value);}} finally {mComputing.set(false);}} } while (computed && mInvalid.get()); 復制代碼這段代碼后段通過CAS去完成一次數據庫的查詢,組裝成實體類并postValue,即更新LiveData。 注意到這個代碼前段調用了InvalidationTracker的addWeakObserver,這個方法就應該就是訂閱了。
InvalidationTracker#addWeakObserver
public void addWeakObserver(Observer observer) {addObserver(new WeakObserver(this, observer)); } 復制代碼InvalidationTracker#addObserver
public void addObserver( 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;}ObserverWrapper wrapper = new ObserverWrapper(observer, tableIds, tableNames);ObserverWrapper currentObserver;synchronized (mObserverMap) {currentObserver = mObserverMap.putIfAbsent(observer, wrapper);}if (currentObserver == null && mObservedTableTracker.onAdded(tableIds)) {syncTriggers();} } 復制代碼InvalidationTracker$WeakObserver
static class WeakObserver extends Observer {final InvalidationTracker mTracker;final WeakReference<Observer> mDelegateRef;WeakObserver(InvalidationTracker tracker, Observer delegate) {super(delegate.mTables);mTracker = tracker;mDelegateRef = new WeakReference<>(delegate);}public void onInvalidated( Set<String> tables) {final Observer observer = mDelegateRef.get();if (observer == null) {mTracker.removeObserver(this);} else {observer.onInvalidated(tables);}} } 復制代碼可以看到,WeakObserver就是對Observer一個弱引用的包裝。而在addObserver中,根據observer中tableNames,對更新了InvalidationTracker的訂閱記錄。添加成功后,最后會調用onAdded。
boolean onAdded(int... tableIds) {boolean needTriggerSync = false;synchronized (this) {for (int tableId : tableIds) {final long prevObserverCount = mTableObservers[tableId];mTableObservers[tableId] = prevObserverCount + 1;if (prevObserverCount == 0) {mNeedsSync = true;needTriggerSync = true;}}}return needTriggerSync; } 復制代碼這里mTableObservers是對每個table的observer進行計數。為什么要計數呢?我們接著看。在發現了訂閱數從0->1的table時,這個方法會返回true,如果它返回true,會執行syncTriggers()方法,經過調用會執行這一段代碼:
final int[] tablesToSync = mObservedTableTracker.getTablesToSync(); if (tablesToSync == null) {return; } final int limit = tablesToSync.length; try {database.beginTransaction();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(); } 復制代碼InvalidationTracker#getTablesToSync()
int[] getTablesToSync() {synchronized (this) {if (!mNeedsSync || mPendingSync) {return null;}final int tableCount = mTableObservers.length;for (int i = 0; i < tableCount; i++) {final boolean newState = mTableObservers[i] > 0;if (newState != mTriggerStates[i]) {mTriggerStateChanges[i] = newState ? ADD : REMOVE;} else {mTriggerStateChanges[i] = NO_OP;}mTriggerStates[i] = newState;}mPendingSync = true;mNeedsSync = false;return mTriggerStateChanges;} } 復制代碼這個getTablesToSync方法很短,但這里就體現了observer計數的作用,它遍歷這個表,找出計數與之前不一樣的,如果由一個大于0的數變為->0,表明現在沒有observer訂閱它,返回REMOVE,0->n,返回ADD,否則NO_OP。對于返回ADD的表,就應該是會監聽變化的表了。它會執行startTrackingTable方法。
private void startTrackingTable(SupportSQLiteDatabase writableDb, int tableId) {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 INSERT OR REPLACE INTO ").append(UPDATE_TABLE_NAME).append(" VALUES(null, ").append(tableId).append("); END");writableDb.execSQL(stringBuilder.toString());} } 復制代碼到這里我們就很清楚了:實現監聽修改的方法是觸發器。 (不過我之前僅僅是聽說過觸發器,很少用過,如果不了解,這里有一份簡易的教程)。而觸發器關心的操作是這一些:
private static final String[] TRIGGERS = new String[]{"UPDATE", "DELETE", "INSERT"}; 復制代碼對應著更新、刪除、插入。當有這些操作時,根據上述觸發器語句,會更新一個由InvalidationTracker維護的表"UPDATE_TABLE_NAME"。 InvalidationTracker#UPDATE_TABLE_NAME
private static final String UPDATE_TABLE_NAME = "room_table_modification_log"; 復制代碼InvalidationTracker#internalInit
void internalInit(SupportSQLiteDatabase database) {synchronized (this) {if (mInitialized) {Log.e(Room.LOG_TAG, "Invalidation tracker is initialized twice :/.");return;}database.beginTransaction();try {database.execSQL("PRAGMA temp_store = MEMORY;");database.execSQL("PRAGMA recursive_triggers='ON';");database.execSQL(CREATE_TRACKING_TABLE_SQL);database.setTransactionSuccessful();} finally {database.endTransaction();}syncTriggers(database);mCleanupStatement = database.compileStatement(RESET_UPDATED_TABLES_SQL);mInitialized = true;} } 復制代碼注意到表中有這樣一列:
INVALIDATED_COLUMN_NAME + " INTEGER NOT NULL DEFAULT 0 復制代碼在觸發器設置的是更新操作時會被設置為1。所以,應該就是檢驗這個值來判斷表是否有更新。那么是哪里進行判斷呢?我們可以從一個更新操作開始找,例如BookDao_Impl#insert()
@Override public long insert(final Book book) {__db.beginTransaction();try {long _result = __insertionAdapterOfBook.insertAndReturnId(book);__db.setTransactionSuccessful();return _result;} finally {__db.endTransaction();} } 復制代碼最后發現在endTransaction中調用了InvalidationTracker的refreshVersionsAsync方法。而在這個方法中,最終會運行InvalidationTracker的mRefreshRunnable對象的run方法。(注意,和上文的mRefreshRunnbale屬于不同類,不是同一個對象。) RoomDatabase#endTransaction()
public void endTransaction() {mOpenHelper.getWritableDatabase().endTransaction();if (!inTransaction()) {// enqueue refresh only if we are NOT in a transaction. Otherwise, wait for the last// endTransaction call to do it.mInvalidationTracker.refreshVersionsAsync();} } 復制代碼InvalidationTracker#mRefreshRunnable#run()
inal Lock closeLock = mDatabase.getCloseLock(); boolean hasUpdatedTable = false; try {... 省略if (mDatabase.mWriteAheadLoggingEnabled) {// This transaction has to be on the underlying DB rather than the RoomDatabase// in order to avoid a recursive loop after endTransaction.SupportSQLiteDatabase db = mDatabase.getOpenHelper().getWritableDatabase();db.beginTransaction();try {hasUpdatedTable = checkUpdatedTable();db.setTransactionSuccessful();} finally {db.endTransaction();}} else {hasUpdatedTable = checkUpdatedTable();} } catch (IllegalStateException | SQLiteException exception) {// may happen if db is closed. just log.Log.e(Room.LOG_TAG, "Cannot run invalidation tracker. Is the db closed?",exception); } finally {closeLock.unlock(); } if (hasUpdatedTable) {// 分發給Observer,最終會更新LiveDatasynchronized (mObserverMap) {for (Map.Entry<Observer, ObserverWrapper> entry : mObserverMap) {entry.getValue().notifyByTableVersions(mTableInvalidStatus);}}// Reset invalidated status flags.mTableInvalidStatus.clear(); } 復制代碼注意,hasUpdatedTable = checkUpdatedTable();
private boolean checkUpdatedTable() {boolean hasUpdatedTable = false;Cursor cursor = mDatabase.query(new SimpleSQLiteQuery(SELECT_UPDATED_TABLES_SQL));//noinspection TryFinallyCanBeTryWithResourcestry {while (cursor.moveToNext()) {final int tableId = cursor.getInt(0);mTableInvalidStatus.set(tableId);hasUpdatedTable = true;}} finally {cursor.close();}if (hasUpdatedTable) {mCleanupStatement.executeUpdateDelete();}return hasUpdatedTable; } 復制代碼@VisibleForTesting static final String SELECT_UPDATED_TABLES_SQL = "SELECT * FROM " + UPDATE_TABLE_NAME+ " WHERE " + INVALIDATED_COLUMN_NAME + " = 1;"; 復制代碼果然,是查找"UPDATE_TABLE_NAME"這個表中"INVALIDATED_COLUMN_NAME"這列為1的記錄,然后設置自己的狀態。完成這個過程就分發給自己的Observers。
void notifyByTableVersions(BitSet tableInvalidStatus) {...if (invalidatedTables != null) {mObserver.onInvalidated(invalidatedTables);} } 復制代碼而在前文中有說到,注冊的Observer實際上是RoomTrackingLiveData的mObserver的包裝,最終會調用到它的onInvalidated。
mObserver = new InvalidationTracker.Observer(tableNames) {@Overridepublic void onInvalidated(@NonNull Set<String> tables) {ArchTaskExecutor.getInstance().executeOnMainThread(mInvalidationRunnable);} } 復制代碼final Runnable mInvalidationRunnable = new Runnable() {@MainThread@Overridepublic void run() {boolean isActive = hasActiveObservers();if (mInvalid.compareAndSet(false, true)) {if (isActive) {mDatabase.getQueryExecutor().execute(mRefreshRunnable);}}} }; 復制代碼可見,最后會在線程池中執行RoomTrackingLiveData的mRefreshRunnable任務。這個任務前文已經分析過了,通過CAS的方式查詢數據,并post給LiveData,這樣就實現了數據更新的通知。到這里,Room和LiveData聯動的工作原理就大致分析完畢。
寫文章不易,轉載請注明出處@漁船Mr_Liu
轉載于:https://juejin.im/post/5c8910fc6fb9a04a0956e7ab
總結
以上是生活随笔為你收集整理的Room是怎样和LiveData结合使用的?(源码分析)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: zabbix4.2学习笔记--新建用户组
- 下一篇: Flutter学习之认知基础组件