void MemoryFile::reloadFromFile() { if (m_fileType == MMFILE_TYPE_ASHMEM) { return; } if (isFileValid()) { MMKVWarning("calling reloadFromFile while the cache [%s] is still valid", m_name.c_str()); MMKV_ASSERT(0); clearMemoryCache(); }
if (!loadFromFile) { auto strategic = onMMKVCRCCheckFail(m_mmapID); if (strategic == OnErrorRecover) { loadFromFile = true; needFullWriteback = true; } MMKVInfo("recover strategic for [%s] is %d", m_mmapID.c_str(), strategic); } } } else { MMKVError("check [%s] error: %zu size in total, file size is %zu", m_mmapID.c_str(), m_actualSize, fileSize);
checkLastConfirmedInfo();
if (!loadFromFile) { auto strategic = onMMKVFileLengthError(m_mmapID); if (strategic == OnErrorRecover) { // make sure we don't over read the file m_actualSize = fileSize - Fixed32Size; loadFromFile = true; needFullWriteback = true; } MMKVInfo("recover strategic for [%s] is %d", m_mmapID.c_str(), strategic); } } }
The changes will be rolled back if any transaction is ended without being marked as clean (by calling setTransactionSuccessful). Otherwise they will be committed.
WAL (Write-Ahead Log): 作法与 Rollback Journal 刚好相反。原内容仍保留在原 DB 之中,但新的变动则 append 至 WAL 文件。而当 COMMIT 发生时,仅代表某个 Transaction 已 append 进 WAL 文件了,但并不一定有写入原 DB (当 WAL 文件大小到达 checkpoint 的阈值时才会写入)。如此可让其他 DB 连接继续对原 DB 内容进行读取操作,而其他连接也可同时将变动 COMMIT 进 WAL 文件。
这种模式下的文件目录为一个db文件+一个db-shm文件(all SQLite database connections associated with the same database file need to share some memory that is used as an index for the WAL file)+一个db-wal文件
ROOM 对 API16 以上机型默认开启 WAL 模式
getDao 方法都是线程安全的
例如:
1 2 3 4 5 6 7 8 9 10 11 12
public ConfigDao getConfigDao(){ if (_configDao != null) { return _configDao; } else { synchronized(this) { if(_configDao == null) { _configDao = new ConfigDao_Impl(this); } return _configDao; } } }
public SQLiteDatabase getReadableDatabase(){ synchronized (this) { return getDatabaseLocked(false); } }
@SuppressWarnings("unused") private SQLiteDatabase getDatabaseLocked(boolean writable){ if (mDatabase != null) { if (!mDatabase.isOpen()) { // Darn! The user closed the database by calling mDatabase.close(). mDatabase = null; } elseif (!writable || !mDatabase.isReadOnly()) { // The database is already open for business. return mDatabase; } }
if (mIsInitializing) { thrownew IllegalStateException("getDatabase called recursively"); }
SQLiteDatabase db = mDatabase; try { mIsInitializing = true;
if (db != null) { if (writable && db.isReadOnly()) { db.reopenReadWrite(); } } elseif (mName == null) { db = SQLiteDatabase.create(null); } else { int connectionPoolSize = mForcedSingleConnection ? 1 : 0; try { if (DEBUG_STRICT_READONLY && !writable) { final String path = mContext.getDatabasePath(mName).getPath(); db = SQLiteDatabase.openDatabase(path, mPassword, mCipher, mFactory, SQLiteDatabase.OPEN_READONLY, mErrorHandler, connectionPoolSize); } else { mNeedMode = true; mMode = mEnableWriteAheadLogging ? Context.MODE_ENABLE_WRITE_AHEAD_LOGGING : 0; db = Context.openOrCreateDatabase(mContext, mName, mPassword, mCipher, mMode, mFactory, mErrorHandler, connectionPoolSize); } } catch (SQLiteException ex) { if (writable) { throw ex; } Log.e(TAG, "Couldn't open " + mName + " for writing (will try read-only):", ex); final String path = mContext.getDatabasePath(mName).getPath(); db = SQLiteDatabase.openDatabase(path, mPassword, mCipher, mFactory, SQLiteDatabase.OPEN_READONLY, mErrorHandler); } }
return getDatabaseLockedLast(db);
} finally { mIsInitializing = false; if (db != null && db != mDatabase) { db.close(); } } }
voidsyncTriggers(SupportSQLiteDatabase database){ if (database.inTransaction()) { // we won't run this inside another transaction. return; } try { // This method runs in a while loop because while changes are synced to db, another // runnable may be skipped. If we cause it to skip, we need to do its work. while (true) { Lock closeLock = mDatabase.getCloseLock(); closeLock.lock(); try { // there is a potential race condition where another mSyncTriggers runnable // can start running right after we get the tables list to sync. finalint[] tablesToSync = mObservedTableTracker.getTablesToSync(); if (tablesToSync == null) { return; } finalint 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(); } } } 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); } }
A different temporary file is created each time, so that just like as with the special ":memory:" string, two database connections to temporary databases each have their own private database. Temporary databases are automatically deleted when the connection that created them closes.
voidinternalInit(SupportSQLiteDatabase database){ synchronized (this) { if (mInitialized) { Log.e(Room.LOG_TAG, "Invalidation tracker is initialized twice :/."); return; }
// These actions are not in a transaction because temp_store is not allowed to be // performed on a transaction, and recursive_triggers is not affected by transactions. database.execSQL("PRAGMA temp_store = MEMORY;"); database.execSQL("PRAGMA recursive_triggers='ON';"); database.execSQL(CREATE_TRACKING_TABLE_SQL); syncTriggers(database); mCleanupStatement = database.compileStatement(RESET_UPDATED_TABLES_SQL); mInitialized = true; } }
voidsyncTriggers(SupportSQLiteDatabase database){ if (database.inTransaction()) { // we won't run this inside another transaction. return; } try { // This method runs in a while loop because while changes are synced to db, another // runnable may be skipped. If we cause it to skip, we need to do its work. while (true) { Lock closeLock = mDatabase.getCloseLock(); closeLock.lock(); try { // there is a potential race condition where another mSyncTriggers runnable // can start running right after we get the tables list to sync. finalint[] tablesToSync = mObservedTableTracker.getTablesToSync(); // #1 if (tablesToSync == null) { return; } finalint 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(); } } } 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); } }
Runnable mRefreshRunnable = new Runnable() { @Override publicvoidrun(){ final Lock closeLock = mDatabase.getCloseLock(); Set<Integer> invalidatedTableIds = null; try { closeLock.lock();
if (!ensureInitialization()) { return; }
if (!mPendingRefresh.compareAndSet(true, false)) { // no pending refresh // 防止重入 return; }
if (mDatabase.inTransaction()) { // current thread is in a transaction. when it ends, it will invoke // refreshRunnable again. mPendingRefresh is left as false on purpose // so that the last transaction can flip it on again. return; }
if (mDatabase.mWriteAheadLoggingEnabled) { // #1 // 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 { invalidatedTableIds = checkUpdatedTable(); db.setTransactionSuccessful(); } finally { db.endTransaction(); } } else { invalidatedTableIds = 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 (invalidatedTableIds != null && !invalidatedTableIds.isEmpty()) { synchronized (mObserverMap) { for (Map.Entry<Observer, ObserverWrapper> entry : mObserverMap) { entry.getValue().notifyByTableInvalidStatus(invalidatedTableIds); // #2 } } } }
final Runnable mInvalidationRunnable = new Runnable() { @MainThread publicvoidrun(){ boolean isActive = RoomTrackingLiveData.this.hasActiveObservers(); if (RoomTrackingLiveData.this.mInvalid.compareAndSet(false, true) && isActive) { RoomTrackingLiveData.this.getQueryExecutor().execute(RoomTrackingLiveData.this.mRefreshRunnable); // #2 }
} };
final Runnable mRefreshRunnable = new Runnable() { @WorkerThread publicvoidrun(){ if (RoomTrackingLiveData.this.mRegisteredObserver.compareAndSet(false, true)) { RoomTrackingLiveData.this.mDatabase.getInvalidationTracker().addWeakObserver(RoomTrackingLiveData.this.mObserver); }
boolean computed; do { computed = false; if (RoomTrackingLiveData.this.mComputing.compareAndSet(false, true)) { try { Object value = null;
@SuppressWarnings("WeakerAccess") publicvoidrefreshVersionsAsync(){ // TODO we should consider doing this sync instead of async. if (mPendingRefresh.compareAndSet(false, true)) { mDatabase.getQueryExecutor().execute(mRefreshRunnable); } }
实际上,在[ROOM 的线程安全是如何保证的]这节的第三部分,我们已经能发现 Dao 层的 DB 操作都是依赖 beginTransaction, endTransaction 来完成 DB 事务的。也就是说,只要 DAO 层有 DB 事务发生,那么 ROOM 必定会在 getQueryExecutor()的线程池中,执行 mRefreshRunnable, 如果发现了有数据更新的 table,就将这些 table 信息全部抛给Observer#onInvalidated处理
InvalidationTracker 小结
InvalidationTracker的职责即建立了业务对 DB 数据写的观察者模式, 业务的 Observer 被InvalidationTracker聚合持有,同时建立一个临时表room_table_modification_log
DAO 层业务代码委托RoomDatabase通过beginTransaction, endTransaction来完成 DB 的事务提交,这些函数被调用时,顺带触发了InvalidationTracker内部对表room_table_modification_log的异步查询,查询到有数据更新(invalidated=1)的表名信息, 将这些表名信息带给Observer#onInvalidated的参数中。例如 RoomTrackingLiveData 中, 利用InvalidationTracker建立的这套机制,实现了 DAO 层 LiveData 的持有数据实时更新的特性
Boolean that controls whether a view can take focus while in touch mode. If this is true for a view, that view can gain focus when clicked on, and can keep focus if another view is clicked on that doesn't have this attribute set to true.
+------------------------------------------------------------------------------------------+ | Constant Value Description | +------------------------------------------------------------------------------------------+ | afterDescendants 1 The ViewGroup will get focus only if | | none of its descendants want it. | +------------------------------------------------------------------------------------------+ | beforeDescendants 0 The ViewGroup will get focus before | | any of its descendants. | +------------------------------------------------------------------------------------------+ | blocksDescendants 2 The ViewGroup will block its descendants from | | receiving focus. | +------------------------------------------------------------------------------------------+
publicvoidonItemSelected(AdapterView<?> parent, View view, int position, long id){ ListView listView = getListView(); Log.d(TAG, "onItemSelected gave us " + view.toString()); Button b = (Button) view.findViewById(R.id.button); EditText et = (EditText) view.findViewById(R.id.editor); if (b != null || et != null) { // Use afterDescendants to keep ListView from getting focus listView.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS); if(et!=null) et.requestFocus(); elseif(b!=null) b.requestFocus(); } else { if (!listView.isFocused()) { // Use beforeDescendants so that previous selections don't re-take focus listView.setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS); listView.requestFocus(); } }
/** * Return the offset of the first character to be ellipsized away, * relative to the start of the line. (So 0 if the beginning of the * line is ellipsized, not getLineStart().) */
public abstract int getEllipsisStart(int line);
/** * Returns the number of characters to be ellipsized away, or 0 if * no ellipsis is to take place. */
public abstract int getEllipsisCount(int line);
In functional programming, continuation-passing style (CPS) is a style of programming in which control is passed explicitly in the form of a continuation. (from Wikipedia.)
2019-12-02 21:21:39.615 21445-21445/com.example.android.kotlincoroutines D/MainViewModel: test in IO thread: Thread[main,5,main] 2019-12-02 21:21:40.620 21445-21445/com.example.android.kotlincoroutines D/MainViewModel: do we really sleep about 1000ms? Thread[main,5,main]
suspendfuntest() : String { viewModelScope.async { Log.d(TAG, "test in IO thread: " + Thread.currentThread()) delay(1_000) Log.d(TAG, "do we really sleep about 1000ms? " + Thread.currentThread()) }.await() return"Test" }
/** * Wait one second then display a snackbar. */ funonMainViewClicked() {
viewModelScope.launch { val rst = test() _snackBar.postValue(rst) } }
我们会发现一旦调用了 await(),编译器就会要求我们声明 test 为 suspend 函数,原因很简单。如上文所述,await()是被声明为 suspend,它被声明为耗时函数,那么你需要放在一个 suspend 函数去调用
结果:
1 2
2019-12-02 21:26:10.198 21809-21809/com.example.android.kotlincoroutines D/MainViewModel: test in IO thread: Thread[main,5,main] 2019-12-02 21:26:11.202 21809-21809/com.example.android.kotlincoroutines D/MainViewModel: do we really sleep about 1000ms? Thread[main,5,main]
postValue 将会在第二条日志打印时调用,满足延时效果
withContext()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
suspendfuntest() : String { withContext(viewModelScope.coroutineContext) { Log.d(TAG, "test in IO thread: " + Thread.currentThread()) delay(1_000) Log.d(TAG, "do we really sleep about 1000ms? " + Thread.currentThread()) } return"Test"
} /** * Wait one second then display a snackbar. */ funonMainViewClicked() {
viewModelScope.launch { val rst = test() _snackBar.postValue(rst) } }
funshowUser() { viewModelScope.launch { val user = getUser() // Main Thread _user.postValue(user) } }
我们认为network.fetchUser()是一个耗时操作,那么我们用 withContext(Dispatchers.IO)包裹起来,丢给 IO 线程处理,当 network.fetchUser()返回时,withContext 自动将线程切回 MAIN 线程,回到 showUser()的 user 变量处,这里也是 MAIN 线程
For example, if you define style=@style/MyStyle in your XML, this method resolves MyStyle and adds its attributes to the mix. In the end, obtainStyledAttributes() returns a TypedArray which you can use to access the attributes.
1. Any value defined in the AttributeSet.
2. The style resource defined in the AttributeSet (i.e. style=@style/blah).
3. The default style attribute specified by defStyleAttr.
4. The default style resource specified by defStyleResource (if there was no defStyleAttr).
5. Values in the theme.
var GL= { // ... init:function () { GL.miniTempBuffer = newFloat32Array(GL.MINI_TEMP_BUFFER_SIZE); for (var i = 0; i < GL.MINI_TEMP_BUFFER_SIZE; i++) { GL.miniTempBufferViews[i] = GL.miniTempBuffer.subarray(0, i+1); } }, //... createContext:function (canvas, webGLContextAttributes) { var ctx = (canvas.getContext("webgl", webGLContextAttributes) || canvas.getContext("experimental-webgl", webGLContextAttributes)); return ctx && GL.registerContext(ctx, webGLContextAttributes); },registerContext:function (ctx, webGLContextAttributes) { var handle = _malloc(8); // Make space on the heap to store GL context attributes that need to be accessible as shared between threads. assert(handle, 'malloc() failed in GL.registerContext!'); var context = { handle: handle, attributes: webGLContextAttributes, version: webGLContextAttributes.majorVersion, GLctx: ctx }; // Store the created context object so that we can access the context given a canvas without having to pass the parameters again. if (ctx.canvas) ctx.canvas.GLctxObject = context; GL.contexts[handle] = context; if (typeof webGLContextAttributes.enableExtensionsByDefault === 'undefined' || webGLContextAttributes.enableExtensionsByDefault) { GL.initExtensions(context); } return handle; },makeContextCurrent:function (contextHandle) { GL.currentContext = GL.contexts[contextHandle]; // Active Emscripten GL layer context object. Module.ctx = GLctx = GL.currentContext && GL.currentContext.GLctx; // Active WebGL context object. return !(contextHandle && !GLctx); }, // ... }
var asmLibraryArg = { "_emscripten_glBindTexture": _emscripten_glBindTexture // .... 上文wasm中的函数注册,这里略去,只保留_emscripten_glBindTexture } Module['asm'] = function(global, env, providedBuffer) { // memory was already allocated (so js could use the buffer) env['memory'] = wasmMemory ; // import table env['table'] = wasmTable = new WebAssembly.Table({ 'initial': 1155075, 'maximum': 1155075, 'element': 'anyfunc' }); env['__memory_base'] = 1024; // tell the memory segments where to place themselves env['__table_base'] = 0; // table starts at 0 by default (even in dynamic linking, for the main module)
createContext:function (canvas, webGLContextAttributes) { var ctx = (canvas.getContext("webgl", webGLContextAttributes) || canvas.getContext("experimental-webgl", webGLContextAttributes)); return ctx && GL.registerContext(ctx, webGLContextAttributes); },registerContext:function (ctx, webGLContextAttributes) { var handle = _malloc(8); // Make space on the heap to store GL context attributes that need to be accessible as shared between threads. assert(handle, 'malloc() failed in GL.registerContext!'); var context = { handle: handle, attributes: webGLContextAttributes, version: webGLContextAttributes.majorVersion, GLctx: ctx }; // Store the created context object so that we can access the context given a canvas without having to pass the parameters again. if (ctx.canvas) ctx.canvas.GLctxObject = context; GL.contexts[handle] = context; if (typeof webGLContextAttributes.enableExtensionsByDefault === 'undefined' || webGLContextAttributes.enableExtensionsByDefault) { GL.initExtensions(context); } return handle; },makeContextCurrent:function (contextHandle) { GL.currentContext = GL.contexts[contextHandle]; // Active Emscripten GL layer context object. Module.ctx = GLctx = GL.currentContext && GL.currentContext.GLctx; // Active WebGL context object. // GLCtx变量 return !(contextHandle && !GLctx); }