Android的資料(包括files,
database等...)都是屬於應用程式自身,其他程式無法直接進行操作。因此,為了使其他程式能夠操作資料,在Android中,可以通過做成
ContentProvider提供資料操作的介面。其實對本應用而言,也可以將底層資料封裝成ContentProvider,這樣可以有效屏蔽底層
操作的細節,並且是程式保持良好的擴充性和開放性。
ContentProvider,顧名思義,就是資料內容的供應者。在Android中它是一
個資料來源,屏蔽了具體底層資料來源的細節,在ContentProvider內部你可以用Android支援的任何手段進行資料的儲存和操作,可能比較常用
的方式是基於Android的SQLite資料庫(恩,文檔中和範例程式碼都是以此為例)。無論如何,ContentProvider是一個重要的資料來源,
可以預見無論是使用和定製ContentProvider都會很多。於是花了點時間仔細看了看。
資料庫操作
從
我目前掌握的知識來看,SQLite比較輕量(沒有預存程序之類的繁雜手段),用起來也比較簡單。執行個體化一個SQLiteDatabase類對象,通過它
的APIs可以搞定大部分的操作。從sample中看,Android中對db的使用有一種比較簡單的模式,即派生一個
ContentProviderDatabaseHelper類來進行SQLiteDatabase對象執行個體的擷取工作。基本上,
ContentProviderDatabaseHelper類扮演了一個singleton的角色,提供單一的執行個體化進入點,並屏蔽了資料庫建立、開啟
升級等細節。在ContentProvider中只需要調用ContentProviderDatabaseHelper的openDatabase方法
擷取SQLiteDatabase的執行個體就好,而不需要進行資料庫狀態的判斷。
URI
像
進行資料庫操作需要用SQL一樣,對ContentProivder進行增刪改查等操作都是通過一種特定模式的URI來進行的(ig:content:
//provider/item/id),URI的能力與URL類似,具體細節可以查看SDK。建立自己的ContentProvider,只需要派生
ContentProivder類並實現insert, delete,
update等抽象函數即可。在這些介面中比較特殊的是getType(uri)。根據傳入的uri,該方法按照MIME格式返回一個字串(==!沒聽
過的詭異格式...)唯一標識該uri的類型。所謂uri的類型,就是描述這個uri所進行的操作的種類,比如content://xx/a與
content://xx/a/1不是一個類型(前者是多值操作,後者是單值),但content://xx/a/1和content://xx/a/2
就會是一個類型(只是id號不同而已)。
在ContentProvider通常都會執行個體化一個ContentURIPraser來輔助解析和操作
傳入的URI。你需要事先(在static域內)為該ContentURIPraser建立一個uri的文法樹,之後就可以簡單調用
ContentURIPraser類的相關方法進行uri類型判斷(match方法),擷取載入在uri中的參數等操作。但我看來,這隻是在使用上簡化了
相關操作(不然就需要自己做人肉解析了...),但並沒有改變類型判定的模式。你依然需要用switch...case...對uri的類型進行判斷,並
進行相關後續的操作。從模式來看,這樣無疑是具有強烈的壞味道,類似的switch...case...代碼要出現N此,每次一個
ContentProvider做uri類型的增減都會需要遍曆修改每一個switch...case...,當然,如果你使用模式(策略模式...)進
行改造對手機程式來說無疑是崩潰似的(類型膨脹,效率降低...),所以,只能是忍一忍了(恩,還好不會擴散到別的類中,維護性上不會有殺人性的麻
煩...)。
增刪改查
ContentProvider
和所有資料來源一樣,向外提供增刪改查操作介面,這些都是基於uri的指令。進行insert操作的時候,你需要傳入一個uri和
ContentValues。uri的作用基本就限於指明增減條目的類型(從資料庫層面來看就是table名),ContentValues是一個
key/value表的封裝,提供方便的API進行插入資料類型和資料值的設定和擷取。在資料庫層面上來看,這應該是column
name與value的對應。但為了屏蔽ContentProvider使用者涉及到具體資料庫的細節,在Android的樣本中,用了一個小小的模式。它
為每一個表建一個基於BaseColumn類的衍生類別(其實完全可以不派生自BaseColumn,特別當你的表不基於預設的自動id做主鍵的時候),這
個類通常包括一個描述該表的ContentURI對象和形如 public static final TITLE =
"title"這樣的column到類資料的對應。從改變上角度來看,你可以修改column的名字而不需要更改使用者上層代碼,增加了靈活性。
insert方法如果成功會返回一個uri,該uri會在原有的uri基礎上增加有一個row id。對於為什麼使用row id而不是key
id我想破了腦袋。到最後,我發現我傻了,因為ContentProvider不一定需要使用資料庫,使用資料庫對應的表也可以沒有主鍵,只有row
id,才能在任何底層介質下做索引標識。
但,基於row
id在刪除和修改操作是會造成一定的混亂。刪除和修改操作類似。刪除操作需要傳入一個uri,一個where字串,一組where的參數(做條件判
定...),而修改操作會多一個ContentValues做更新值。著兩個操作的uri都支援在末尾添加一個row
id。於是混亂就出現了。當在where參數中指明了key id,而在uri中提供了row id,並且row id和key
id所指函數不一致的時候,你聽誰的?範例程式碼中的做法是完全無視row
id(無語...),如此野蠻的方式我估計也只能在樣本中出現,在實際中該如何用,恩,我也不知道。幸運的是,我看了下上層對
ContentProvider的刪除操作,其實都不會直接進行,而是通過調用Cursor的delete方法進行,在這前提下,我想Cursor會處理
好這些東西吧。
最後一個操作是查詢操作,可以想見,查詢的參數是最多的,包括uri和一組條件參數。條件參數類型和標準的sql類似,包括
sort, projection
之類的。從這些參數到sql語句的產生,可以尋求QueryBuilder類的協助,它提供了一組操作介面,簡化了參數到sql的產生工作,哪怕你不懂
sql都完全沒有問題(這話說的我自己都覺得有點懸...)。查詢返回一個Cursor。Cursor是一個支援隨機讀寫的指標,不僅如此,它還提供了方
便的刪除和修改的API,是上層對ContentProvider進行操作一個重要對象,需要仔細掌握(Cursor還可以綁定到view上,直接送顯,
並與使用者進行互動,真是程式越往上,封裝越好,工作越機械沒有複雜性了...)。
資料模型
在
與介面打交道的Cursor、ContentResolver等資料操作層中,大量採用觀察者模式建立資料層與顯示層的聯絡。一個顯示層的視圖,可以做成
某一種觀察者註冊到Cursor或ContentResolver等資料中介層中,在實現底層ContentProvider中,我們需要特別注意在對數
據進行修改操作(包括增刪改...)後,調用相應類型的notify函數,協助表層對象進行重新整理(還有一種重新整理方式是從一個view發起的)。可以看到
Android的整體資料顯示架構有點像MVC的方式(貧瘠了...叫不出名)。Cursor、ContentResolver相當於控制層,資料層和顯
示層的互動通過控制層來掌管,而且控制層很穩定不需要特別定製,通常工作只在定製資料層和顯示層空間,還是比較方便和清晰的。
一個設計問題
現
在有個設計問題,比如我要擴充一個已有的ContentProvider(第三方提供),我是建立一個ContentProvider,只保留第三方
ContentProvider的key資訊,並為其添加更多的資訊,在表層維護這兩個ContentProvider的聯絡好;還是建議一個
ContentProvider,以第三方的ContentProvider做一部分底層資料來源,像表層提供一個ContentProvider好。
前者無疑在實現上簡單一些,如果第三方改變,靈活性也更好,只是需要仔細維護表層的相關代碼。後者實現上需要付出大量的苦力勞動,當表層使用會簡單多了。我舉棋不定,期待你的意見。。。