之前看過一篇文章,上面說了解Android的四大組件的小夥伴纔算是真正的入門了,仔細想想按照這個標準自己好像都還沒有入門,那麼便想通過幾篇文章來好好研究下四大組件,這裏便作爲開篇先研究ContentProvider吧,至於爲什麼先研究ContentProvider,原因是我對它最不熟悉,所以就拿最不熟悉的開刀吧。
既然是談使用,那麼就想來從使用流程上來做一個總結吧,注意這裏不會詳細的貼出所有代碼,只會講使用大致流程,想看詳細代碼的小夥伴,請看別的小夥伴寫的這篇文章Android基礎學習之Provider(內容提供器)。
1、說說ContentProvider的使用步驟
從ContentProvider的名字,來看我們都知道它是幹啥的,就是提供內容的嘛,這裏的內容就是我們要訪問的數據,這裏以一個以一個ContentProvider來訪問Menu數據爲例來說明使用步驟。
第一步、定義Menu
創建一個Menu類來存放數據,並定義id、name、price三個字段以及字段對應的get/set方法,在Menu類中出了定義的字段之外,定義以下代碼
// 以下定義provider的MIME類型常量
public static final String MIME_DIR_PREFIX = "vnd.android.cursor.dir";// MIME類型 多條
public static final String MIME_ITEM_PREFIX = "vnd.android.cursor.item";// 單條
public static final String MIME_ITEM = "vnd.pub.menus";// 自定義MIME類型字符串
// 將固定前綴+自定義字符串生成兩個類型
public static final String MIME_TYPE_SINGLE = MIME_ITEM_PREFIX + "/" + MIME_ITEM;
public static final String MIME_TYPE_MULTIPLE = MIME_DIR_PREFIX + "/" + MIME_ITEM;
// 用來定義uri常量 content://<authority>/<data path>/..../id
public static final String AUTHORITY = "myfs.pub.menusprovider";// 授權者 表示用哪個provider,要跟清單文件中一致
public static final String PATH_SINGLE = "menus/#"; // (menus:數據庫表名)路徑下單條記錄 #代表數字id
public static final String PATH_MULTIPLE = "menus";// 路徑下多條記錄 數據庫將對應表名
public static final String PATH_MULTIPLE_NAME = "menus/*"; // *代表文本 例如按名稱訪問等
// 組合成所需要的uri字符串
public static final String CONTENT_URI_STRING = "content://" + AUTHORITY + "/" + PATH_MULTIPLE;
// 將uri字符串轉換爲uri對象
public static final Uri CONTENT_URI = Uri.parse(CONTENT_URI_STRING);
// 涉及sqlite底層實現的內容
public static final String CREAT_TABLE = "create table menus(id integer primary key autoincrement ,name varchar(50),price integer)";
public static final String DBNAME = "pub.db";//數據庫名
public static final String TABLENAME = "menus";//表名
public static final int DB_VER = 1;//數據庫版本,用於升級用
//常量字符串,對應數據庫中的字段
public static final String KEY_ID = "id";
public static final String KEY_NAME = "name";
public static final String KEY_PRICE = "price";
//定義字段名的數組,ContentResolver在執行查詢類型操作時會用到
public static final String[] COLUNMS = { KEY_ID, KEY_NAME, KEY_PRICE };
我們看到在Menu中定義了一大堆的變量,這裏可以大概理一下這裏的變量目的可以分爲
a、MIME類型定義
b、URI類型定義
c、數據庫初始化參數
d、Menu字段數組
這裏的a、b類型參數是必須的(當然你要強制放其它地方也行,不過我覺得放這裏比較好),而c、d其實並不是必須的,定義c類型參數原因是因爲這裏Provider存取數據是通過數據庫來進行的,如果不是通過數據庫來,顯然這裏肯定不需要數據庫相關參數,比如如果通過服務器來存取數據那麼這裏有可能就是定義一些網絡訪問參數了(當然也可以不放Menu裏,參數可以放Provider中,或者其它地方),定義d參數的目的是爲了方便ContentResolver的query類型方法方便獲取對應字段的傳參,所以d類型參數一般是放在Menu中的,調用ContentResover調用query代碼如下所示
Cursor cursor = cr.query(Menu.CONTENT_URI, Menu.COLUNMS, null, null, null);
第二步、自定義ContentProvider
Android SDK提供了ContentProvider類,但是不能直接拿來用啊,畢竟SDK也不知道你要對外提供什麼數據,於是需要我們自定義一個MenuProvider, 自定義Provider來重載ContentProvider的六個方法
1、public abstract boolean onCreate();
着這個方法中進行一些初始化操作,比如初始化數據庫,方便Provider從數據庫拿到需要對外提供的數據
2、public abstract @Nullable String getType(@NonNull Uri uri);
這個方法獲取Uri類型參數,輸出MIME類型字符串,它會將不同的Uri映射成爲不通的MIMI字符串,這個方法後面還會細說
3、public abstract @Nullable Uri insert(@NonNull Uri uri, @Nullable ContentValues values);
根據ContentResover中傳遞過來的Uri中包含的數據進行insert操作
4、public abstract int delete(@NonNull Uri uri, @Nullable String selection,
@Nullable String[] selectionArgs);
刪除操作
public abstract int update(@NonNull Uri uri, @Nullable ContentValues values,
@Nullable String selection, @Nullable String[] selectionArgs);
修改操作
public abstract @Nullable Cursor query(@NonNull Uri uri, @Nullable String[] projection,
@Nullable String selection, @Nullable String[] selectionArgs,
@Nullable String sortOrder);
查詢操作
這裏可以看出需要重載crud對應的四個方法,然後有一個getType和onCreate方法。
針對getType定義了一下代碼
// 構造一個matcher對象
private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
// 定義code用來找對對應各種MIME類型的編碼
private static final int MULTIPLE_MENUS = 1;
private static final int SINGLE_MENUS = 2;
// 靜態塊,在類加載時執行一次,將uri和code綁定起來,以便解析
static {
sURIMatcher.addURI(Menu.AUTHORITY, Menu.PATH_MULTIPLE, MULTIPLE_MENUS);
sURIMatcher.addURI(Menu.AUTHORITY, Menu.PATH_SINGLE, SINGLE_MENUS);
}
UriMatcher對象可對Uri進行操作,這裏的addURI操作可將Uri地址和這裏定義的MULTIPLE_MENUS 、SINGLE_MENUS 行成一個一一對應的關係,方便在getType方法中根據Uri來返回MIME字符串類型。
@Nullable
@Override
public String getType(@NonNull Uri uri) {
// 返回uri對應的MIME類型
int match = sURIMatcher.match(uri);
switch (match) {
case MULTIPLE_MENUS:
return Menu.MIME_TYPE_MULTIPLE;
case SINGLE_MENUS:
return Menu.MIME_TYPE_SINGLE;
default:
throw new IllegalArgumentException("Unknown uri:" + uri);
}
}
第三步、配置AndroidManifest.xml
到第二步,就已經定義好了自定義Provider,同其它四大組件一樣,使用Provider需要在AndroidManifest中進行對應的配置。
<provider
android:name="com.yoryky.demo.provider.MenuProvider"
android:authorities="myfs.pub.menusprovider"
android:exported="true"
android:readPermission="com.ql.provider.READ" >
<!-- 指定授權者 -->
<!-- 給予其他應用程序只讀取自己數據的權限 -->
</provider>
這裏的的exported表示其它應用程序可以訪問該Provider,而readPermission表示其它應用數據只有讀數據的權限,並且還需要在自己的AndroidManifest.xml文件中配置
<uses-permission android:name="com.ql.provider.READ" />
才能夠讀該Provider提供的數據。
第四步、ContentResolver調用數據
我們使用Provider的目的是通過Uri來向外部提供一個統一的數據訪問接口,那麼我們便不能通過直接調用Provider中的crud方法來實現數據訪問,不然我們各種定義Uri什麼的就前功盡棄,並且也達不到屏蔽數據來源的目的,所以這裏請出來了ContentResolver來使用不同的Provider訪問以及操作數據,這樣不同的Provider不管是其中數據來自數據庫還是服務器什麼的,這裏的ContentResolver都不需要管,ContentResolver只需要通過在crud方法中傳入作爲唯一標識符Uri,然後代碼就會執行到對應的Provider中去,並調用對應的方法返回結果。這裏舉一個通過ContentResolver來執行insert的操作吧。
private ContentResolver = getContentResolver();
private void addData(String name, int price) {
Menu[] mms = { new Menu(name, price)};
for (Menu m : mms) {
ContentValues cv = new ContentValues();
cv.put(Menu.KEY_NAME, m.getName());
cv.put(Menu.KEY_PRICE, m.getPrice());
Uri uri = cr.insert(Menu.CONTENT_URI, cv); // 最終
// cp.insert(url,values)
Log.d(TAG, "add data:" + uri.toString());
}
}
這裏的ContentResolver個人感覺是全局變量,而且是個全局的單例,這個沒有驗證,如果這句話有問題,請指教。
到這裏ContentProvider的使用就算是告一段落了。
2、URI/MIME以及與getType方法之間的關係
可能之前沒怎麼了解ContentProvider的同學看到這裏已經一頭霧水了,前面講的Uri是啥、MIME又是啥,這裏就先來說說這兩個概念吧。
Uri
URI是一個用來標示唯一路徑的類,前面我們說了ContentResolver通過這個URI去尋找對應Provider執行對應方法並返回結果,要是先這個目的,就得確保URI標示的路徑必須是唯一的,不然JVM怎麼能知道你要去執行哪個Provider、哪個方法呢。這裏我們來看看URI作爲唯一標識符的路徑格式。
這裏我們知道了其格式爲scheme://authority/path這樣表示的,scheme爲固定的content(也不知道有沒有別的scheme),而authority和path爲自定義路徑,自定義不代表隨便定義,一定要確保定義的URI在實際使用中的唯一性,所以鑑於android中不同apk的包名不能相同所以authority一般定義爲包名,而path則定義對應應用中的數據類名(當然可以帶上/id這個索引,就像圖片中的/2來表示單條數據)。
Menu類中關於Uri的定義有
// 用來定義uri常量 content://<authority>/<data path>/..../id
public static final String AUTHORITY = "myfs.pub.menusprovider";// 授權者 表示用哪個provider,要跟清單文件中一致
public static final String PATH_SINGLE = "menus/#"; // (menus:數據庫表名)路徑下單條記錄 #代表數字id
public static final String PATH_MULTIPLE = "menus";// 路徑下多條記錄 數據庫將對應表名
public static final String PATH_MULTIPLE_NAME = "menus/*"; // *代表文本 例如按名稱訪問等
// 組合成所需要的uri字符串
public static final String CONTENT_URI_STRING = "content://" + AUTHORITY + "/" + PATH_MULTIPLE;
// 將uri字符串轉換爲uri對象
public static final Uri CONTENT_URI = Uri.parse(CONTENT_URI_STRING);
這裏我們看到最有生成的CONTENT_URI就是我們上面講的那種格式,根據這個CONTENT_URI,ContentResover就能夠找到對應的MenuProvider中去。
MIME
那麼這個MIME是個啥,根據維基百科上的解釋,MIME是多用途互聯網郵件擴展(MIME,Multipurpose Internet Mail Extensions)是一個互聯網標準,這句話說了等於沒說是吧,實際上知道HTTP協議的童鞋應該知道MIME是用來表示文檔類型是txt、js、html、css、png、pdf等的,也就是說MIME就是用來表示文檔類型的。關於MIME想了解更多的童鞋可以去看MIME 參考手冊。
這裏我們再來看看Menu類中定義的MIME相關的變量吧
public static final String MIME_DIR_PREFIX = "vnd.android.cursor.dir";// MIME類型 多條
public static final String MIME_ITEM_PREFIX = "vnd.android.cursor.item";// 單條
public static final String MIME_ITEM = "vnd.pub.menus";// 自定義MIME類型字符串
// 將固定前綴+自定義字符串生成兩個類型
public static final String MIME_TYPE_SINGLE = MIME_ITEM_PREFIX + "/" + MIME_ITEM;
public static final String MIME_TYPE_MULTIPLE = MIME_DIR_PREFIX + "/" + MIME_ITEM;
一個MIME Type由媒體類型(type)與子類型(subtype)組成,它們之間使用反斜槓/分割,形式如下:
type/subtype
常見的如text/html,而我們這裏的vnd.android.cursor.dir以及vnd.android.cursor.item表示android中的自定義MIME,其含義分別爲
vnd.android.cursor.dir 代表返回結果爲多列數據
vnd.android.cursor.item 代表返回結果爲單列數據
這裏的subtype就隨意了,我們這裏使用vnd.pub.menus來作爲subtype。
Provider的getType方法
上面講了Uri以及MIME,前面也提到了getType方法是講輸入Uri輸出對應的MIME類型
@Nullable
@Override
public String getType(@NonNull Uri uri) {
// 返回uri對應的MIME類型
int match = sURIMatcher.match(uri);
switch (match) {
case MULTIPLE_MENUS:
return Menu.MIME_TYPE_MULTIPLE;
case SINGLE_MENUS:
return Menu.MIME_TYPE_SINGLE;
default:
throw new IllegalArgumentException("Unknown uri:" + uri);
}
}
爲什麼要說這個getType,因爲不說這個方法,我們壓根不知道爲什麼需要在Menu類中定義MIME,實際上我倒現在都沒搞明白這個getType的使用原理,因爲我測試用ContentResolver各種調用Provider這個getType都沒有觸發。
這裏就直接貼出來其他童鞋得出來的結論吧
作用1:提高性能
getType的作用應該是這樣的,以指定的兩種方式開頭,android可以順利識別出這是單條數據還是多條數據,比如我們的查詢結果是一個Cursor,我們可以根據getType方法中傳進來的Uri判斷出query方法要返回的Cursor中只有一條數據還是有多條數據,這個有什麼用呢?如果我們在getType方法中返回一個null或者是返回一個自定義的android不能識別的MIME類型,那麼當我們在query方法中返回Cursor的時候,系統要對Cursor進行分析,進而得出結論,知道該Cursor只有一條數據還是有多條數據,但是如果我們按照Google的建議,手動的返回了相應的MIME,那麼系統就不會自己去分析了,這樣可以提高一丟點的系統性能。
作用2:瞭解ContentProvider返回類型(單條還是多條數據)
我們有可能不知道ContentProvider返回給我們的是什麼,這個時候我們可以先調用ContentProvider的getType,根據getType的不同返回值做相應的處理。
好吧,看到這兩條作用,我還是勉強接受getType是有用的吧(雖然心裏還是不服,畢竟我的getType斷點沒有捕獲啊,哈哈哈)。
3、Uri 操作類介紹
Android SDK提供了兩個工具類來對Uri進行操作,分別是UriMatcher以及ContentUris,這裏也來簡單的介紹下吧。
UriMatcher
UriMatcher工具類主要是用來匹配Uri,它的使用比較簡單,第一步就是把需要使用到Uri路徑全給添加上,將Uri和自定義的SINGLE_MENUS以及MULTIPLE_MENUS綁定在一起。
// 靜態塊,在類加載時執行一次,將uri和code綁定起來,以便解析
static {
sURIMatcher.addURI(Menu.AUTHORITY, Menu.PATH_MULTIPLE, MULTIPLE_MENUS);
sURIMatcher.addURI(Menu.AUTHORITY, Menu.PATH_SINGLE, SINGLE_MENUS);
}
然後通過調用math方法就可以獲取uri對應的SINGLE_MENUS或者MULTIPLE_MENUS,來執行不同的操作
@Override
public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
int count = 0;
switch (sURIMatcher.match(uri)) {
case SINGLE_MENUS:
String id = uri.getPathSegments().get(1);// 從uri取出id字符串
count = sqdb.update(Menu.TABLENAME, values, Menu.KEY_ID + "=" + id, selectionArgs);
getContext().getContentResolver().notifyChange(uri,null);
break;
case MULTIPLE_MENUS:
count = sqdb.update(Menu.TABLENAME, values, selection, selectionArgs);
getContext().getContentResolver().notifyChange(uri,null);
break;
default:
throw new IllegalArgumentException("Unknown uri:" + uri);
}
return count;
}
ContentUris
ContentUris類用於操作Uri路徑後面的id,它有兩個比較實用的方法:
withAppendedId(uri, id)用於爲路徑加上id
//生成後的Uri爲:content://myfs.pub.menusprovider/menus/1
Uri uri = Uri.parse("content://myfs.pub.menusprovider/menus")
Uri insertedUserUri = ContentUris.withAppendedId(uri, 1);
parseId(uri)方法用於從路徑中獲取id
//獲取的結果爲2
Uri insertedUserUri = Uri.parse("content://myfs.pub.menusprovider/menus/2")
long userId = ContentUris.parseId(insertedUserUri);
4、ContentObserver和ContentResolver的結合使用
看到ContentObserver這個類名應該能夠反應過來這是一個觀察者吧,那麼這裏的ContentResolver就只能很不好意思的作爲被觀察者了,所以這兩個類可以組成一個觀察訂閱模式。
先給出一個ContentObserver的自定義類的實現吧
class MyContentObserver extends ContentObserver {
public MyContentObserver(Handler handler) {
super(handler);
}
@Override
public void onChange(boolean selfChange) {
super.onChange(selfChange);
/Log.e(TAG, "remote observer find content provider data change!!!");
queryAll();
}
}
然後我們來看看這個觀察訂閱模式如何構建
首先,實例化ContentObserver以及ContentResolver這兩個類
observer=new MyContentObserver(new Handler());//實例化ContentObserver對象
cr = getContentResolver(); //實例化ContentResolver 對象
然後,訂閱或者說註冊
cr.registerContentObserver(Menu.CONTENT_URI, true, observer);//註冊內容提供器觀察者對象
最後,在Activity onDestroy生命週期中記得取消註冊
cr.unregisterContentObserver(observer);//取消監聽
好運行代碼,添加數據,怎麼ContentObserver的onChange方法沒反應呢,原來還需要手動觸發監聽,在MenuProvider的增、刪、改操作中添加如下代碼
getContext().getContentResolver().notifyChange(uri,null);
即可觸發ContentObserver中的onChange方法(這裏也加深了我認爲getContentResolver獲取到的是一個全局變量的想法)。
到這裏這邊文章的主要內容就結束了,不得不說,關於ContentProvider不是我學習中覺得最難的知識,但一定是最難寫博客的知識,不知道有多少童鞋能夠堅持看到這裏。
總得來說個人覺得看完這篇文章希望你能搞清楚一下幾點
1、Provider主要意義在於通過Uri屏蔽數據獲取細節,向其它App開放數據
2、ContentProvider、ContentResolver以及ContentObserver這幾個類之間的關係
3、UriMatcher以及ContentUris這兩個工具類的作用
4、爲了實現getType我們得去自定義單列和多列的MIME;
搞清楚了這幾個問題,也就算這篇文章沒有白寫吧,還望各位多交流。
5、參考文獻
1、ContentProvider數據庫共享之——讀寫權限與數據監聽
3、android之ContentProvider和Uri詳解