Android Jetpack組件之數據庫Room詳解(三)

本文涉及Library的版本以下:android

  • androidx.room:room-runtime:2.1.0-alpha03
  • androidx.room:room-compiler:2.1.0-alpha03(註解編譯器)

Room對LiveData擴展sql

下面先列一個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編譯時期自動生成的類實現的, 接着看代碼:app

//本文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觸發器本文不詳細說明,結合上面代碼,以更新觸發器爲例,簡單介紹一下:async

//結合上面代碼, 建立更新觸發器的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這個值就能夠知道哪一個表數據改變啦, 很少說接着看一下源碼怎麼監聽臨時表的。ide

//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的建立過程。post

//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等
  • 數據索引的建立, 子查詢等。
相關文章
相關標籤/搜索