3. SQLite的Frameworks層實現
3.1 Frameworks層架構
Android系統方便應用使用,在Frameworks層中封裝了一套Content框架,之所以叫Content框架而不叫資料庫框架之類的,是因為這裡Content不一定是來自資料庫的內容,也可以是來自其他資料來源的內容,開發人員只需要知道如何使用ContentResovler和ContentProvider就可以在應用程式之間共享資料了。
這裡我們只討論資料來源是資料庫的ContentProvider,開發人員需要實現一個SQLiteOpenHelper的派生類,它使用了一系列SQLite相關的類封裝了native層的SQLite動態庫的介面方法,那麼SQLite在Frameworks層是如何封裝的,我們在使用SQLite時又需要注意些什麼呢?
我們先來看一看基於SQLite的Content框架的整體架構:
Android系統在Frameworks層
(1)不管是呼叫getWritableDatabase方法還是getReadableDatabase方法,SQLIteOpenHelper都會以可讀寫模式開啟資料庫。
(2)如果應用程式想以WAL模式開啟資料庫,可在自定義SQLiteOpenHelper類的構造方法中呼叫setWriteAheadLoggingEnabled(true)。
SQLiteOpenHelper.java
6、SQLiteConnectionPool:資料庫連線池,管理所有開啟的資料庫連線(Connection)。所有資料庫連線都是通過它來開啟,開啟後會加入連線池,在讀寫資料庫時需要從連線池中獲取一個資料庫連線來使用。
7、SQLiteConnection:代表了資料庫連線,每個Connection封裝了一個native層的sqlite3例項,通過JNI呼叫SQLite動態庫的介面方法運算元據庫,Connection要麼被Session持有,要麼被連線池持有。
8、CursorFactory:可選的Cursor工廠,可以提供自定義工廠來建立Cursor。
9、DatabaseErrorHandler:可選的資料庫異常處理器(目前僅處理資料庫Corruption),如果不提供,將會使用預設的異常處理器。
10、SQLiteDatabaseConfiguration:資料庫配置,應用程式可以建立多個到SQLite資料庫的連線,這個類用來保證每個連線的配置都是相同的。
11、SQLiteQuery和SQLiteStatement:從抽象類SQLiteProgram派生,封裝了SQL語句的執行過程,在執行時自動組裝待執行的SQL語句,並呼叫SQLiteSession來執行資料庫操作。這兩個類的實現應用了設計模式中的命令模式。
3.2 關鍵模組實現
本節介紹幾個關鍵模組的實現和使用時需要注意的事項。
3.2.1 SQLiteSession
Android系統Frameworks層的資料庫讀寫操作都是通過SQLiteSession完成的,SQLiteSession負責管理資料庫連線和事務的生命週期。
一個SQLiteDatabse例項可以同時持有多個活躍的Session(但是為防止死鎖,每個執行緒只能持有一個DB的Session),每個Session在執行SQL語句時獲取資料庫連線,在SQL語句執行結束後釋放資料庫連線,Session只有在只執行SQL語句期間保持資料庫連線,執行完後就釋放了。這個特性也是連線池的實現基礎。
SQLiteSession.java
如果連線池中所有連線都已分配出去了,那麼獲取連線的SQLiteSession會阻塞直到有可用連線為止。
所以,在使用SQLite時需要注意以下幾點:
(2)執行的事務儘量短。
(3)如果讀寫事務很長,可以考慮使用yieldTransaction()方法先提交部分事務,給其他事務執行的機會。
3.2.2 SQLiteConnectionPool
資料庫連線池保持所有開啟的資料庫連線,在任何時候,一個資料庫連線要麼被連線池持有,要麼被一個SQLiteSession持有,如果SQLiteSession使用完資料庫連線,必須把它還給連線池。如果連線池中所有的連線都已被佔用,則待執行的事務要等待有空閒的連線才能執行。
目前Android系統的實現中,如果以非WAL模式開啟資料庫,連線池中只會保持一個資料庫連線,如果以WAL模式開啟資料庫,連線池中的最大連線數量則根據系統配置決定,預設配置是兩個。
SQLiteConnectionPool.java:
雖然名為連線池,但是從原始碼來看,目前實現的池中只有一個資料庫連線(以後的Android版本可能會擴充套件),所以如果應用程式中有大量的併發資料庫讀和寫操作的話,每個操作的時長都可能受到影響,所以資料庫操作應放在工作執行緒中執行,以免影響UI響應。
3.2.4 Cursor
從ContentProvider查詢的資料結果是放在Cursor中返回給客戶端的,在客戶端看來Cursor就是一個資料容器,但隱藏在Cursor後面的實現方式很靈活,它的資料既可以不是從資料庫返回的,也可以是在使用時才真正載入的,很好的體現了物件導向程式設計的特性和優點。
在Frameworks中把Cursor定義為了一個介面,它的定位是可以隨機訪問的資料集,在介面中定義了訪問資料集的通用方法。業務可以根據自己的需要實現一個Cursor,具體怎麼實現介面的方法由具體實現決定,所以Cursor有很多子類來滿足不同場景的需要。
通過SQLiteDatabase返回的就是其中的SQLiteCursor,如果資料不是從資料庫返回的,開發人員也可以在ContentProvider中動態建立一個MatrixCursor,然後填充資料並返回給客戶端。
從Cursor介面和其派生類的定義來看它們都沒有實現Parcelable介面,那麼它是怎麼跨程式傳遞的呢?這需要Cursor首先要解決兩個問題。
第一個問題:Cursor沒有實現Parcelable介面,一個Cursor例項怎麼跨進稱傳遞呢?答案是傳遞的不是具體資料,而是Binder引用,即在ContentProvider端建立Cursor的Binder服務端例項,然後把Binder應用傳遞給客戶端,在客戶端通過這個Binder引用跨程式獲取查詢到的資料的。這裡Frameworks定義了一個介面:IBulkCursor。
IBulkCursor定義了跨程式的Cursor需要實現的介面方法,其中getWindow()用來獲得資料視窗,onMove()用來移動Cursor的位置。
那麼第二個問來了:我們知道通過Binder傳遞的資料大小有限(1MB),而查詢到的資料大小可能超出限制,那麼怎麼跨程式傳遞資料呢?既然資料大小不定,那麼我們就不通過Binder傳遞資料了,而是通過共享記憶體傳遞資料,這塊共享記憶體是封裝在CursorWindow中的。
CursorWindow就是資料視窗,它在服務端分配(視窗大小有Android系統配置決定)並傳遞到客戶端,客戶端再對映到自己的程式空間中,這樣,服務端填充的資料就可以被客戶端讀取到了。上面IBulkCursor介面中定義的getWindow()方法就是獲取CursorWindow的。
CursorWindow在初始化時是空的,在呼叫Cursor的moveToXXX方法時會通過IBulkCursor的onMove()方法呼叫服務端的Cursor去填充資料視窗的內容。
CursorWindow.java
frameworks\base\libs\androidfw\CursorWindow.cpp
服務端建立共享記憶體。
CursorWindow.java
frameworks\base\libs\androidfw\CursorWindow.cpp
客戶端對映共享記憶體到程式記憶體空間。
上面知道了Frameworks解決跨程式傳遞Cursor資料的思路,我們再來看下具體執行跨程式傳遞資料的類:CursorToBulkCursorAdapter(服務端)和BulkCursorToCursorAdapter(客戶端)。
服務端:
客戶端:
在ContentProviderNatvie類中可以看到Cursor的專遞過程。
服務端:
ContentProviderNative.onTransact()
CursorToBulkCursorAdaptor.java
服務端在通過ContentProvider得到Cursor後,用它建立一個CursorToBulkCursorAdaptor例項,然後把adaptor封裝在一個實現了Parcelable介面的BulkCursorDescriptor例項中返回給客戶端。
雖說是在使用時才填充資料視窗,但是實際上傳遞Cursor的過程中,從上面程式碼可以看到服務端已經替應用程式填充過一次資料了:mCursor.getCount()。
SQLiteCursor.java
客戶端:
ContentProviderProxy.query()
BulkCursorToCursorAdaptor.java
在得到服務端返回的資料後建立一個BulkCursorDescriptor例項,在用它初始化一個BulkCursorToCursorAdapter例項返回給應用程式使用。