Android面試一天一題(15 Day:ContentProvider)
有一次HR給我了一份簡歷,說是一個資深的工程師,比較特別的是翻譯過一本《Andorid XXXX》的書,基本涵蓋了Android開發的要點,而且還是有深度的。正好我看過此書的一些章節,面試了一下之後,這個面試者比較顯著的特點就是對自己翻譯過的章節的知識點也不太瞭解。這也引發了我對國內開發書籍的思考,確實太多的書一個抄一個,不然就是抄抄官方的API 用例。記得我看的第一本Android書叫《Android開發深入淺出》,很薄但把必要的知識講到位了,後來此書的作者被我們一位創業成功的同事請去做了講師。
面試題:有使用過ContentProvider碼?能說說Android為什麼要設計ContentProvider這個元件嗎?
ContentProvider應用程式間非常通用的共享資料的一種方式,也是Android官方推薦的方式。Android中許多系統應用都使用該方式實現資料共享,比如通訊錄、簡訊等。但我遇到很多做Android開發的人都不怎麼使用它,覺得直接讀取資料庫會更簡單方便。
那麼Android搞一個內容提供者在資料和應用之間,只是為了裝高大上,故弄玄虛?我認為其設計用意在於:
- 封裝。對資料進行封裝,提供統一的介面,使用者完全不必關心這些資料是在DB,XML、Preferences或者網路請求來的。當專案需求要改變資料來源時,使用我們的地方完全不需要修改。
- 提供一種跨程序資料共享的方式。
應用程式間的資料共享還有另外的一個重要話題,就是資料更新通知機制了。因為資料是在多個應用程式中共享的,當其中一個應用程式改變了這些共享資料的時候,它有責任通知其它應用程式,讓它們知道共享資料被修改了,這樣它們就可以作相應的處理。
ContentResolver介面的notifyChange函式來通知那些註冊了監控特定URI的ContentObserver物件,使得它們可以相應地執行一些處理。ContentObserver可以通過registerContentObserver進行註冊。
既然是對外提供資料共享,那麼如何限制對方的使用呢?
android:exported屬性非常重要。這個屬性用於指示該服務是否能夠被其他應用程式元件呼叫或跟它互動。如果設定為true,則能夠被呼叫或互動,否則不能。設定為false時,只有同一個應用程式的元件或帶有相同使用者ID的應用程式才能啟動或繫結該服務。
對於需要開放的元件應設定合理的許可權,如果只需要對同一個簽名的其它應用開放content provider,則可以設定signature級別的許可權。大家可以參考一下系統自帶應用的程式碼,自定義了signature級別的permission:
<permission android:name="com.android.gallery3d.filtershow.permission.READ"
android:protectionLevel="signature" />
<permission android:name="com.android.gallery3d.filtershow.permission.WRITE"
android:protectionLevel="signature" />
<provider
android:name="com.android.gallery3d.filtershow.provider.SharedImageProvider"
android:authorities="com.android.gallery3d.filtershow.provider.SharedImageProvider"
android:grantUriPermissions="true"
android:readPermission="com.android.gallery3d.filtershow.permission.READ"
android:writePermission="com.android.gallery3d.filtershow.permission.WRITE" />
如果我們只需要開放部份的URI給其他的應用訪問呢?可以參考Provider的URI許可權設定,只允許訪問部份URI,可以參考原生ContactsProvider2的相關程式碼(注意path-permission這個選項):
<provider android:name="ContactsProvider2"
android:authorities="contacts;com.android.contacts"
android:label="@string/provider_label"
android:multiprocess="false"
android:exported="true"
android:grantUriPermissions="true"
android:readPermission="android.permission.READ_CONTACTS"
android:writePermission="android.permission.WRITE_CONTACTS">
<path-permission
android:pathPrefix="/search_suggest_query"
android:readPermission="android.permission.GLOBAL_SEARCH" />
<path-permission
android:pathPrefix="/search_suggest_shortcut"
android:readPermission="android.permission.GLOBAL_SEARCH" />
<path-permission
android:pathPattern="/contacts/.*/photo"
android:readPermission="android.permission.GLOBAL_SEARCH" />
<grant-uri-permission android:pathPattern=".*" />
</provider>
ContentProvider介面方法執行在哪個執行緒中呢?
ContentProvider可以在AndroidManifest.xml中配置一個叫做android:multiprocess的屬性,預設值是false,表示ContentProvider是單例的,無論哪個客戶端應用的訪問都將是同一個ContentProvider物件,如果設為true,系統會為每一個訪問該ContentProvider的程序建立一個例項。
這點還是比較好理解的,那如果我要問每個ContentProvider的操作是在哪個執行緒中執行的呢(其實我們關心的是UI執行緒和工作執行緒)?比如我們在UI執行緒呼叫getContentResolver().query查詢資料,而當資料量很大時(或者需要進行較長時間的計算)會不會阻塞UI執行緒呢?
要分兩種情況回答這個問題:
- ContentProvider和呼叫者在同一個程序,ContentProvider的方法(query/insert/update/delete等)和呼叫者在同一執行緒中;
- ContentProvider和呼叫者在不同的程序,ContentProvider的方法會執行在它自身所在程序的一個Binder執行緒中。
但是,注意這兩種方式在ContentProvider的方法沒有執行完成前都會blocked呼叫者。所以你應該知道這個上面這個問題的答案了吧。
也可以看看CursorLoader這個類的原始碼,看Google自己是怎麼使用getContentResolver().query的。
ContentProvider是如何在不同應用程式之間傳輸資料的?
這個問題點有些深入,大家要對Binder程序間通訊機制比較瞭解。因為之前我們有提到過一個應用程序有16個Binder執行緒去和遠端服務進行互動,而每個執行緒可佔用的快取空間是128KB這樣,超過會報異常。ContentResolver雖然是通過Binder程序間通訊機制打通了應用程式之間共享資料的通道,但Content Provider元件在不同應用程式之間傳輸資料是基於匿名共享記憶體機制來實現的。有興趣的可以檢視一下老羅的文章Android系統匿名共享記憶體Ashmem(Anonymous Shared Memory)簡要介紹和學習計劃。
小結
初學開發或者開發經驗少的人,往往不喜歡在寫程式碼時思考如何應對以後可能產生的變化和相關介面的擴充套件能力,所以他們往往看不到ContentProvider的巨大好處。我覺得一般常見到的元件或概念,大家不一定需要每個都去償試寫寫程式碼Demo(當然這樣最好),但我認為對它們仍然需要有足夠的重視,要去理清它的計設思路和使用場景。這樣對你是極好的,之後你在面對問題時才會有多種解決方案的選擇機會。
人生,比的不就是誰有更多的選擇嗎?