Android ContentProvider 基本原理和使用詳解

huansky發表於2020-10-18

ContentProvider(內容提供者)是 Android 的四大元件之一,管理 Android 以結構化方式存放的資料,以相對安全的方式封裝資料(表)並且提供簡易的處理機制和統一的訪問介面供其他程式呼叫。

Android 的資料儲存方式總共有五種,分別是:Shared Preferences、網路儲存、檔案儲存、外儲儲存、SQLite。但一般這些儲存都只是在單獨的一個應用程式之中達到一個資料的共享,有時候我們需要操作其他應用程式的一些資料,就會用到 ContentProvider。而且 Android 為常見的一些資料提供了預設的 ContentProvider(包括音訊、視訊、圖片和通訊錄等)。

 
要實現與其他的 ContentProvider 通訊首先要查詢到對應的 ContentProvider 進行匹配。Android 中 ContenProvider 藉助 ContentResolver 通過 Uri 與其他的 ContentProvider 進行匹配通訊。 

URI(Uniform Resource Identifier)

其它應用可以通過 ContentResolver 來訪問 ContentProvider 提供的資料,而 ContentResolver 通過 uri 來定位自己要訪問的資料,所以我們要先了解 uri。URI(Universal Resource Identifier)統一資源定位符,如果您使用過安卓的隱式啟動就會發現,在隱式啟動的過程中我們也是通過 uri 來定位我們需要開啟的 Activity 並且可以在 uri 中傳遞引數。

URI 為系統中的每一個資源賦予一個名字,比方說通話記錄。每一個 ContentProvider 都擁有一個公共的 URI,用於表示 ContentProvider 所提供的資料。URI 的格式如下:

// 規則
[scheme:][//host:port][path][?query]
// 示例
content://com.wang.provider.myprovider/tablename/id:
  1. 標準字首(scheme)——content://,用來說明一個Content Provider控制這些資料;
  2. URI 的標識 (host:port)—— com.wang.provider.myprovider,用於唯一標識這個 ContentProvider,外部呼叫者可以根據這個標識來找到它。對於第三方應用程式,為了保證 URI 標識的唯一性,它必須是一個完整的、小寫的類名。這個標識在元素的authorities屬性中說明,一般是定義該 ContentProvider 的包.類的名稱;

  3. 路徑(path)——tablename,通俗的講就是你要操作的資料庫中表的名字,或者你也可以自己定義,記得在使用的時候保持一致就可以了;

  4. 記錄ID(query)——id,如果URI中包含表示需要獲取的記錄的 ID,則返回該id對應的資料,如果沒有ID,就表示返回全部;

對於第三部分路徑(path)做進一步的解釋,用來表示要操作的資料,構建時應根據實際專案需求而定。如:

  • 操作tablename表中id為11的記錄,構建路徑:/tablename/11;

  • 操作tablename表中id為11的記錄的name欄位:tablename/11/name;

  • 操作tablename表中的所有記錄:/tablename;

  • 操作來自檔案、xml或網路等其他儲存方式的資料,如要操作xml檔案中tablename節點下name欄位:/ tablename/name;

  • 若需要將一個字串轉換成Uri,可以使用Uri類中的parse()方法,如:

Uri uri = Uri.parse("content://com.wang.provider.myprovider/tablename");

再來看一個例子:

http://www.baidu.com:8080/wenku/jiatiao.html?id=123456&name=jack

uri 的各個部分在安卓中都是可以通過程式碼獲取的,下面我們就以上面這個 uri 為例來說下獲取各個部分的方法:

  • getScheme():獲取 Uri 中的 scheme 字串部分,在這裡是 http

  • getHost():獲取 Authority 中的 Host 字串,即 www.baidu.com

  • getPost():獲取 Authority 中的 Port 字串,即 8080

  • getPath():獲取 Uri 中 path 部分,即 wenku/jiatiao.html

  • getQuery():獲取 Uri 中的 query 部分,即 id=15&name=du

MIME

MIME 是指定某個副檔名的檔案用一種應用程式來開啟,就像你用瀏覽器檢視 PDF 格式的檔案,瀏覽器會選擇合適的應用來開啟一樣。Android 中的工作方式跟 HTTP 類似,ContentProvider 會根據 URI 來返回 MIME 型別,ContentProvider 會返回一個包含兩部分的字串。MIME 型別一般包含兩部分,如:

text/html
text/css
text/xml
application/pdf

分為型別和子型別,Android 遵循類似的約定來定義MIME型別,每個內容型別的 Android MIME 型別有兩種形式:多條記錄(集合)和單條記錄。

  • 集合記錄(dir):
vnd.android.cursor.dir/自定義 
  • 單條記錄(item):
vnd.android.cursor.item/自定義 

vnd 表示這些型別和子型別具有非標準的、供應商特定的形式。Android中型別已經固定好了,不能更改,只能區別是集合還是單條具體記錄,子型別可以按照格式自己填寫。

在使用 Intent 時,會用到 MIME,根據 Mimetype 開啟符合條件的活動。

UriMatcher

Uri 代表要操作的資料,在開發過程中對資料進行獲取時需要解析 Uri,Android 提供了兩個用於操作 Uri 的工具類,分別為 UriMatcher 和 ContentUris 。掌握它們的基本概念和使用方法,對一個 Android 開發者來說是一項必要的技能。

UriMatcher 類用於匹配 Uri,它的使用步驟如下:

  • 將需要匹配的Uri路徑進行註冊,程式碼如下:
//常量UriMatcher.NO_MATCH表示不匹配任何路徑的返回碼
UriMatcher  sMatcher = new UriMatcher(UriMatcher.NO_MATCH);
//如果match()方法匹配“content://com.wang.provider.myprovider/tablename”路徑,返回匹配碼為1 sMatcher.addURI("content://com.wang.provider.myprovider", " tablename ", 1);
//如果match()方法匹配content://com.wang.provider.myprovider/tablename/11路徑,返回匹配碼為2 sMatcher.addURI("com.wang.provider.myprovider", "tablename/#", 2);

此處採用 addURI 註冊了兩個需要用到的 URI;注意,新增第二個 URI 時,路徑後面的 id 採用了萬用字元形式 “#”,表示只要前面三個部分都匹配上了就 OK。

  • 註冊完需要匹配的 Uri 後,可以使用 sMatcher.match(Uri) 方法對輸入的 Uri 進行匹配,如果匹配就返回對應的匹配碼,匹配碼為呼叫 addURI() 方法時傳入的第三個引數。 

switch (sMatcher.match(Uri.parse("content://com.zhang.provider.yourprovider/tablename/100"))) {
    case 1:
        //match 1, todo something
        break;
    case 2
        //match 2, todo something
        break;
    default:
        //match nothing, todo something
        break;
}

ContentUris

ContentUris 類用於操作 Uri 路徑後面的 ID 部分,它有兩個比較實用的方法:withAppendedId(Uri uri, long id) 和 parseId(Uri uri)。

  • withAppendedId(Uri uri, long id) 用於為路徑加上 ID 部分:

Uri uri = Uri.parse("content://cn.scu.myprovider/user")

//生成後的Uri為:content://cn.scu.myprovider/user/7
Uri resultUri = ContentUris.withAppendedId(uri, 7); 
  • parseId(Uri uri) 則從路徑中獲取 ID 部分:

Uri uri = Uri.parse("content://cn.scu.myprovider/user/7")

//獲取的結果為:7
long personid = ContentUris.parseId(uri);

ContentProvider 主要方法

ContentProvider 是一個抽象類,如果我們需要開發自己的內容提供者我們就需要繼承這個類並複寫其方法,需要實現的主要方法如下:
  • public boolean onCreate()在建立 ContentProvider 時使用

  • public Cursor query()用於查詢指定 uri 的資料返回一個 Cursor

  • public Uri insert():用於向指定uri的 ContentProvider 中新增資料

  • public int delete()用於刪除指定 uri 的資料

  • public int update()使用者更新指定 uri 的資料

  • public String getType()用於返回指定的 Uri 中的資料 MIME 型別

資料訪問的方法 insert,delete 和 update 可能被多個執行緒同時呼叫,此時必須是執行緒安全的。

如果操作的資料屬於集合型別,那麼 MIME 型別字串應該以 vnd.android.cursor.dir/ 開頭,

  • 要得到所有 tablename 記錄: Uri 為 content://com.wang.provider.myprovider/tablename,那麼返回的MIME型別字串應該為:vnd.android.cursor.dir/table。

如果要操作的資料屬於非集合型別資料,那麼 MIME 型別字串應該以 vnd.android.cursor.item/ 開頭,

  • 要得到 id 為 10 的 tablename 記錄,Uri 為 content://com.wang.provider.myprovider/tablename/10,那麼返回的 MIME 型別字串為:vnd.android.cursor.item/tablename 。

方法使用示例

使用 ContentResolver 對 ContentProvider 中的資料進行操作的程式碼如下:

ContentResolver resolver = getContentResolver();
Uri uri = Uri.parse("content://com.wang.provider.myprovider/tablename");
// 新增一條記錄 ContentValues values = new ContentValues(); values.put("name", "wang1"); values.put("age", 28); resolver.insert(uri, values);
// 獲取tablename表中所有記錄 Cursor cursor = resolver.query(uri, null, null, null, "tablename data"); while(cursor.moveToNext()){ Log.i("ContentTest", "tablename_id="+ cursor.getInt(0)+ ", name="+ cursor.getString(1)); }
// 把id為1的記錄的name欄位值更改新為zhang1 ContentValues updateValues = new ContentValues(); updateValues.put("name", "zhang1"); Uri updateIdUri = ContentUris.withAppendedId(uri, 2); resolver.update(updateIdUri, updateValues, null, null);
// 刪除id為2的記錄,即欄位age Uri deleteIdUri = ContentUris.withAppendedId(uri, 2); resolver.delete(deleteIdUri, null, null);

監聽資料變化

如果ContentProvider的訪問者需要知道資料發生的變化,可以在ContentProvider發生資料變化時呼叫getContentResolver().notifyChange(uri, null)來通知註冊在此URI上的訪問者。只給出類中監聽部分的程式碼:

public class MyProvider extends ContentProvider {
   public Uri insert(Uri uri, ContentValues values) {
      db.insert("tablename", "tablenameid", values);
      getContext().getContentResolver().notifyChange(uri, null);
   }
}

而訪問者必須使用 ContentObserver 對資料(資料採用 uri 描述)進行監聽,當監聽到資料變化通知時,系統就會呼叫 ContentObserver 的 onChange() 方法:

getContentResolver().registerContentObserver(Uri.parse("content://com.ljq.providers.personprovider/person"),
       true, new PersonObserver(new Handler()));
public class PersonObserver extends ContentObserver{
   public PersonObserver(Handler handler) {
      super(handler);
   }
   public void onChange(boolean selfChange) {
      //to do something
   }
}

例項說明

資料來源是 SQLite, 用 ContentResolver 操作 ContentProvider。

這裡寫圖片描述

Constant.java(儲存一些常量)

public class Constant {  
      
    public static final String TABLE_NAME = "user";  
      
    public static final String COLUMN_ID = "_id";  
    public static final String COLUMN_NAME = "name";  
       
       
    public static final String AUTOHORITY = "cn.scu.myprovider";  
    public static final int ITEM = 1;  
    public static final int ITEM_ID = 2;  
       
    public static final String CONTENT_TYPE = "vnd.android.cursor.dir/user";  
    public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/user";  
       
    public static final Uri CONTENT_URI = Uri.parse("content://" + AUTOHORITY + "/user");  
}  

DBHelper.java (運算元據庫)

public class DBHelper extends SQLiteOpenHelper {  
  
    private static final String DATABASE_NAME = "finch.db";    
    private static final int DATABASE_VERSION = 1;    
  
    public DBHelper(Context context) {  
        super(context, DATABASE_NAME, null, DATABASE_VERSION);  
    }  
  
    @Override  
    public void onCreate(SQLiteDatabase db)  throws SQLException {  
        //建立表格  
        db.execSQL("CREATE TABLE IF NOT EXISTS "+ Constant.TABLE_NAME + "("+ Constant.COLUMN_ID +" INTEGER PRIMARY KEY AUTOINCREMENT," + Constant.COLUMN_NAME +" VARCHAR NOT NULL);");  
    }  
  
    @Override  
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)  throws SQLException {  
        // 這裡知識簡單刪除並建立表格 
        // 如果需要保留原來的資料,需要先備份再刪除
        db.execSQL("DROP TABLE IF EXISTS "+ Constant.TABLE_NAME+";");  
        onCreate(db);  
    }  
}  

MyProvider.java (自定義的 ContentProvider ) 

public class MyProvider extends ContentProvider {    
    
    DBHelper mDbHelper = null;    
    SQLiteDatabase db = null;    
    
    private static final UriMatcher mMatcher;    
    static{    
        mMatcher = new UriMatcher(UriMatcher.NO_MATCH); 
     // 註冊 uri mMatcher.addURI(Constant.AUTOHORITY,Constant.TABLE_NAME, Constant.ITEM); mMatcher.addURI(Constant.AUTOHORITY, Constant.TABLE_NAME
+"/#", Constant.ITEM_ID); } @Override public String getType(Uri uri) {
     // 根據匹配規則返回對應的型別
switch (mMatcher.match(uri)) { case Constant.ITEM: return Constant.CONTENT_TYPE; case Constant.ITEM_ID: return Constant.CONTENT_ITEM_TYPE; default: throw new IllegalArgumentException("Unknown URI"+uri); } } @Override public Uri insert(Uri uri, ContentValues values) { // TODO Auto-generated method stub long rowId; if(mMatcher.match(uri)!=Constant.ITEM){ throw new IllegalArgumentException("Unknown URI"+uri); } rowId = db.insert(Constant.TABLE_NAME,null,values); if(rowId>0){ Uri noteUri=ContentUris.withAppendedId(Constant.CONTENT_URI, rowId); getContext().getContentResolver().notifyChange(noteUri, null); return noteUri; } throw new SQLException("Failed to insert row into " + uri); } @Override public boolean onCreate() { // TODO Auto-generated method stub mDbHelper = new DBHelper(getContext()); db = mDbHelper.getReadableDatabase(); return true; } @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { // TODO Auto-generated method stub Cursor c = null; switch (mMatcher.match(uri)) { case Constant.ITEM: c = db.query(Constant.TABLE_NAME, projection, selection, selectionArgs, null, null, sortOrder); break; case Constant.ITEM_ID: c = db.query(Constant.TABLE_NAME, projection,Constant.COLUMN_ID + "="+uri.getLastPathSegment(), selectionArgs, null, null, sortOrder); break; default: throw new IllegalArgumentException("Unknown URI"+uri); } c.setNotificationUri(getContext().getContentResolver(), uri); return c; } @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { // TODO Auto-generated method stub return 0; } @Override public int delete(Uri uri, String selection, String[] selectionArgs) { // TODO Auto-generated method stub return 0; } }

MainActivity.java(ContentResolver操作)

public class MainActivity extends Activity {
    private ContentResolver mContentResolver = null; 
    private Cursor cursor = null;  
         @Override
        protected void onCreate(Bundle savedInstanceState) {
            // TODO Auto-generated method stub
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            
               TextView tv = (TextView) findViewById(R.id.tv);
                
                mContentResolver = getContentResolver();  
                tv.setText("新增初始資料 ");
                for (int i = 0; i < 10; i++) {  
                    ContentValues values = new ContentValues();  
                    values.put(Constant.COLUMN_NAME, "fanrunqi"+i);  
                    mContentResolver.insert(Constant.CONTENT_URI, values);  
                } 
                
                tv.setText("查詢資料 ");
                cursor = mContentResolver.query(Constant.CONTENT_URI, new String[]{Constant.COLUMN_ID,Constant.COLUMN_NAME}, null, null, null);  
                if (cursor.moveToFirst()) {
                    String s = cursor.getString(cursor.getColumnIndex(Constant.COLUMN_NAME));
                    tv.setText("第一個資料: "+s);
                }
        }
         
}  

最後在manifest申明 :

<provider android:name="MyProvider" android:authorities="cn.scu.myprovider" />

總結:

  • 如何通過 ContentProvider 查詢資料?   通過 ContentResolver 進行uri匹配

  • 如何實現自己的ContentProvider? 繼承 ContentProvider,實現對應的方法。在 manifest 中宣告

額外補充:隱式 Intent 中 <data> 標籤

該部分內容與 ContentProvider 沒關係,只是這裡講到了 URI,就順便此處在插入另外一個知識點:Intent 中 <data> 標籤。看不懂的可以直接略過,看下一步分的內容,此處內容與 activity 相關。

Data 的匹配規則:如果過濾規則 intent-filter 中定義了 data,那麼 Intent 中必須也要攜帶可匹配的 data

data 的語法如下所示:

<data android:scheme=“string”
      android:host=“string”
      android:port=“string”
      android:path=“string”
      android:pathPattern=“string”
      android:pathPrefix=“string”
      android:mimeType=“string”>

data 由兩部分組成:mimeType 和 URI。mimeType 可以為空,URI 一定不會為空,因為有預設值。mimeType 指媒體型別,比如 image/jpeg,video/* 等,可表示圖片,視訊等不同的媒體格式

示例1 

data 的匹配:

<intent-filter>
     <action android:name="com.action.demo1"></action>
     <category android:name="android.intent.category.DEFAULT" />
     <data
         android:scheme="x7"
         android:host="www.360.com"
            />
 </intent-filter> 

清單檔案 intent-filter 定義的 data 中,只有 URI, 沒有 mimeType 型別,匹配如下

intent.setData(Uri.parse("x7://www.360.com"))

示例2

<intent-filter>
       <action android:name="com.action.demo1"></action>
       <category android:name="android.intent.category.DEFAULT" />
       <data android:mimeType="image/*" />
 </intent-filter>

清單檔案 intent-filter 定義的 data 中,沒有定義 URI,只有 mimeType 型別,但是 URI 卻有預設值,URI 中的 scheme 預設為 content 或者 file,host 一定不能為空,隨便給個字串abc 都可以,匹配如下

intent.setDataAndType(Uri.parse("content://abc"),"image/png");

注意:
content://abc 換成 file://abc 在 7.0 以上的版本(即把 targetSdkVersion 指定成 24 及之上並且在 API>=24 的裝置上執行)會出現 FileUriExposedException 異常,google 提供了FileProvider 解決這個異常,使用它可以生成 content://Uri 來替代 file://Uri.

示例3

<intent-filter>
      <action android:name="com.action.demo1"></action>
      <category android:name="android.intent.category.DEFAULT" />
        <data
               android:mimeType="image/*"
               android:scheme="x7"
               android:host="www.360.com"
         />
        <data
             android:mimeType="video/*"
             android:scheme="x7"
             android:host="www.360.com"
         />
</intent-filter>

清單檔案 intent-filter 定義的 data 中,URI 和 mimeType 都有, 匹配如下

intent.setDataAndType(Uri.parse("x7://www.360.com"),"image/png");
// 或者
intent.setDataAndType(Uri.parse("x7://www.360.com"),"video/mpeg");

 

Inent 中攜帶的 data 標籤對應的資料,在某一組 intent-filter 中可以找到,即匹配成功。data 標籤資料在 intent-filter 中也可以有多組

隱示啟動,防止匹配失敗的可以提前檢測:

判斷的方法如下:

PackageManager mPackageManager = getPackageManager();
//返回匹配成功中最佳匹配的一個act資訊,intent 需要按照前面的把 action, data 等都設定好
ResolveInfo info = mPackageManager.resolveActivity(intent,PackageManager.MATCH_DEFAULT_ONLY);
//返回所有匹配成功的act資訊,是一個集合
 List<ResolveInfo> infoList = mPackageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);

只要上述 2 個方法的返回值不為 null,那麼 startActivity 一定可以成功

 

參考文章:

ContentProvider 資料訪問詳解

ContentProvider全解析和使用

相關文章