以前寫項目的時候,對於數據庫的操做不是特別多,能避免就儘可能避免,而且一直想不到比較好的方法去組織網絡數據、本地數據的邏輯。因此在最近的面試中時,問及項目中的數據庫實現,以及比較好用的數據庫的框架及其實現原理時,我就只答道以前在《第一行代碼》中看到了的LitePal,但源碼就...因此此次來惡補一次數據庫。幾經搜索,雲比較,比較青睞官方Jetpack組件中的Room。html
Room框架是使用生成代碼的方式在編譯時生成CRUD的代碼,所以性能是遠遠好過經過反射實現的ORM框架。可是事實上,Room最吸引個人地方不止是性能,Room對架構組件(LiveData)、RxJava等流行框架作了適配。例如,Room中的查詢操做能夠返回一個LiveData,而且,每一次RUD操做,都會更新LiveData。這能夠大大簡化本地、內存、網絡多級緩存的實現,具體官方也給出了一系列Demo,而且隨時都在隨着框架或者根據PR更新,強烈推薦研究這些Demo!java
本文主要是對Room中與LiveData的聯動做出分析,閱讀本文前建議先熟悉Room的基本使用,建議看一下與LiveData配合使用的Demo。android
AppDatabase.ktgit
@Database(entities = [Book::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun bookDao(): BookDao
}
複製代碼
Book.ktgithub
@Dao
interface BookDao {
@Insert
fun insert(book: Book): Long
@Delete
fun delete(book: Book)
@Query("select * from book where id = :id")
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的數據修改操做了。sql
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>() {
@Override
public 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();
}
}
@Override
protected void finalize() {
_statement.release();
}
});
}
複製代碼
注意這一行緩存
return __db.getInvalidationTracker().createLiveData(...);
複製代碼
咱們跟進去,最終建立的是一個RoomTrackingLiveData,是一個繼承了LiveData的類。下面是它的構造方法。從構造方法來看,比較可疑的對象的是InvalidationTracker.Observer這個類,而且實現十有八九是觀察者模式。而最後的回調也多半是onInvalidated方法。bash
@SuppressLint("RestrictedApi")
RoomTrackingLiveData(
RoomDatabase database,
InvalidationLiveDataContainer container,
Callable<T> computeFunction,
String[] tableNames) {
mDatabase = database;
mComputeFunction = computeFunction;
mContainer = container;
mObserver = new InvalidationTracker.Observer(tableNames) {
@Override
public void onInvalidated(@NonNull Set<String> tables) {
ArchTaskExecutor.getInstance().executeOnMainThread(mInvalidationRunnable);
}
};
}
複製代碼
而在RoomTrackingLiveData中,重寫了onActive方法。其中mContainer是InvalidationLiveDataContainer,文檔上有寫僅僅是維護LiveData的強引用,防止正在使用的LiveData被回收,跟本文目標不要緊,可忽略。然後面的就有意思了,經過Excutor執行了一個任務,因此,咱們來看一下這個任務把。
@Override
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(@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;
}
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);
}
@Override
public void onInvalidated(@NonNull 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,最終會更新LiveData
synchronized (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 TryFinallyCanBeTryWithResources
try {
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) {
@Override
public void onInvalidated(@NonNull Set<String> tables) {
ArchTaskExecutor.getInstance().executeOnMainThread(mInvalidationRunnable);
}
}
複製代碼
final Runnable mInvalidationRunnable = new Runnable() {
@MainThread
@Override
public void run() {
boolean isActive = hasActiveObservers();
if (mInvalid.compareAndSet(false, true)) {
if (isActive) {
mDatabase.getQueryExecutor().execute(mRefreshRunnable);
}
}
}
};
複製代碼
可見,最後會在線程池中執行RoomTrackingLiveData的mRefreshRunnable任務。這個任務前文已經分析過了,經過CAS的方式查詢數據,並post給LiveData,這樣就實現了數據更新的通知。到這裏,Room和LiveData聯動的工做原理就大體分析完畢。