一、簡介
-
ContentProvider主要用於在不同的應用程序間實現數據共享的功能,允許一個程序訪問另外一個程序中的數據,還能保證數據訪問的安全性。
-
是Android跨進程實現數據共享的標準方式。
-
ContentProvider相當於進程間的搬運工,對數據一系列的操作(CRUD)
-
數據源可以是數據庫(SQLite等)、文件、xml、網絡等等。
在瞭解ContentProvider之前,需要對URI和運行時權限進行回顧下,在ContentProvider中涉及到這兩個知識點
二、統一資源標識符(URI)
URI:是給ContentProvider中的數據提供唯一的標識符。
URI分爲自定義和系統內置兩種類型,系統內置比如通訊錄、日程表、相冊等等。
1、自定義URI
自定義URI的標準命名:
- 主題名(Schema):ContentProvider的URI前綴,系統規定的。
- 授權信息(Authority):ContentProvider的唯一標識符。
- 表名(Path):ContentProvider指向數據庫中的某個表名。
- 記錄(ID):表中的某個記錄,如果無指定,默認返回的是全部記錄。
注意:
自定義URI格式末尾一般有兩種結束方式
- 一種是以表名(Path)即路徑爲結束點,該方式表示訪問表中所有的數據 如:content://com.hzw.progress/Book
- 另外一種是記錄(ID)爲結束點,則表示只訪問有相應ID屬性的數據 如:content://com.hzw.progress/Book/1
此外某些時候我們希望訪問任意一張表中所有數據或者某張表的任意一行數據,此時可以使用通配符的方式。
// * 匹配任意長度的任何有效字符的字符串
// 表示能訪問com.gwz.progress下所有表數據
content://com.gwz.progress/*
// # 匹配任意長度的數字字符的字符串
//表示能訪問com.gwz.progress下的表名爲Book中任意一行數據
content://com.gwz.progress/Book/#
自定義的URI的使用方法:
//通過Uri解析指定的Uri字符串得到URi對象
Uri uri = Uri.parse("content://com.hzw.progress/Book/1");
2、系統內置URI
在使用內置URI時,需注意在Mainfest文件中聲明相應的權限,否則會訪問失敗甚至程序崩潰現象。具體的所需權限可以搜索log日誌的“Permission Denial”得知。
運行時權限
在Android6.0開始系統加入運行時權限機制,就是在應用安裝使用中需要用戶同意一些系統權限,若拒絕授權,則無法使用該功能。通常這些權限是涉及到用戶的安全和隱私,爲此稱之爲危險權限,一些常用的危險:
下面通過調起手機系統撥號爲例,說明權限授權的流程
在未手動授權的情況:
public void onClick(View view) {
switch (view.getId()){
case R.id.but_call:
call();
break;
}
}
private void call() {
try {
Intent intent = new Intent(Intent.ACTION_CALL);
intent.setData(Uri.parse("tel:10086"));
startActivity(intent);
}catch (SecurityException e){
e.printStackTrace();
}
}
//在Mainfest中聲明打電話權限
<uses-permission android:name="android.permission.CALL_PHONE"/>
在Android5.0之前只要在Mainfest聲明權限就可以訪問成功,然而在Android6.0及以上環境,點擊後會發現程序沒有任何反應,說明調起撥號界面失敗,我們可以通過日誌發現具體錯誤信息:
通過Permission Denial搜索日誌也可得知具體所需權限
接下來修復處理這個運行時權限問題,實現調起撥號界面。
基本步驟:
- 獲取所需權限的授權許可值,通過checkSelfPermission可得。具體返回值有兩種:
(1)PERMISSION_GRANTED:表示已經得到相關權限
(2)PERMISSION_DENIED:未得到權限許可 - 通過授權許可值,檢查是否有授權(與PackageManager.PERMISSION_GRANTED進行比較)
(1)未授權:則申請授權,可用requestPermissions申請
(2)已有授權:直接調用業務方法。 - 重寫onRequestPermissionsResult方法,在其根據grantResults數組的元素值和請求code判斷是否授權成功。
(1)授權成功:可直接調用業務方法
(2)授權失敗或拒絕授權:下次再次申請授權時需前往應用信息界面手動打開需求
private static final int CALL_CODE=100;
public void onClick(View view) {
switch (view.getId()){
case R.id.but_call:
//獲取CALL_PHONE權限許可值
int selfPermission = ActivityCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE);
//檢查是否授權
if (selfPermission !=PackageManager.PERMISSION_GRANTED){
//沒有授權情況下,則申請CALL_CODE權限
ActivityCompat.requestPermissions(this,new String[]{Manifest.permission.CALL_PHONE},CALL_CODE);
}else {
//授權成功的情況下,直接調用call方法
call();
}
break;
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
//根據grantResults數組的元素值和請求code判斷是否授權成功
if (grantResults.length>0 && grantResults[0] == PackageManager.PERMISSION_GRANTED){
switch (requestCode){
case CALL_CODE:
call();
break;
}
}else {
Log.i(TAG, "onRequestPermissionsResult: "+"權限授權失敗,需要前往應用信息頁面設置");
showPermissionFailureDialog();
}
}
//授權失敗顯示的dialog
private void showPermissionFailureDialog(){
new AlertDialog.Builder(this)
.setMessage("您已拒絕了相關權限,將無法使用該功能,是否前往應用信息頁面手動打開該權限")
.setNegativeButton(android.R.string.no,null)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Intent intent=getAppDetailSettingIntent(ContentProviderActivity.this);
startActivity(intent);
}
}).create().show();
}
/**
* 獲取應用詳情頁面intent
*
* @return
*/
public static Intent getAppDetailSettingIntent(Context context) {
Intent localIntent = new Intent();
localIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
localIntent.setAction("android.settings.APPLICATION_DETAILS_SETTINGS");
localIntent.setData(Uri.fromParts("package", context.getPackageName(), null));
return localIntent;
}
以上代碼運行後初次點擊,會彈出一個授權提示對話框選擇
接着點擊允許,可直接撥號
如果點擊失敗,則會輸出失敗的log日誌
以上運行時申請授權的基本流程。
三、ContentProvider核心方法
進程間共享數據的本質就是CRUD(增刪改查),而ContentProvider正好也提供對應的方法。
public class BookProvider extends ContentProvider{
private static final String TAG = "BookProvider";
/**
* ContentProvider初始化被調用
* 一般用於創建數據庫或者升級等操作,只有在ContentResolver訪問的時候,纔會觸發onCreate方法
* 此方法是主線程執行,不能做耗時操作
* @return true:ContentProvider初始化成功,false則失敗
*/
@Override
public boolean onCreate() {
Log.d(TAG, "onCreate: 當前線程"+Thread.currentThread());
return false;
}
/**
* 插入一條新數據(添加數據)--增
* @param uri 根據uri插入到具體哪張數據表
* @param values ContentValues底層是key-value鍵值對結構,使用HashMap實現的
* key:表示列名,value:表示行名,如果value爲空,在表中則是空行,無內容
* @return 添加成功後,返回這條新數據的uri。
*/
@Nullable
@Override
public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
Log.d(TAG, "insert: 當前線程"+Thread.currentThread());
return null;
}
/**
* 刪除數據 -- 刪
* @param uri 根據uri刪除哪張表的數據
* @param selection 根據條件刪除具體哪行數據
* @param selectionArgs 與selection類似
* @return 返回被刪除的行數。
*/
@Override
public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
Log.d(TAG, "delete: 當前線程"+Thread.currentThread());
return 0;
}
/**
* 更新數據 -- 改
* @param uri 根據Uri修改具體哪張表的數據
* @param values 與insert的ContentValues一樣,key是列名,若傳入的value爲空,則會刪除原來的數據置空。
* @param selection 選擇符合該條件的行數據進行修改
* @param selectionArgs 與selection類似
* @return 更新的行數
*/
@Override
public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
Log.d(TAG, "update: 當前線程"+Thread.currentThread());
return 0;
}
/**
* 查詢數據 -- 查
* @param uri 根據uri查詢具體哪張數據表
* @param projection 確定查詢表中哪些列 ,傳null則返回所有的列
* @param selection 確定查詢哪行,傳null這返回所有的行
* @param selectionArgs 與selection類似
* @param sortOrder 用於對查詢結果進行排序,傳null則使用默認的排序方式,也可以是無序的。
* @return 查詢的返回值,是個Cursor對象,在取完數據需進行關閉。否則會內存泄漏
*/
@Nullable
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
Log.d(TAG, "query: 當前線程"+Thread.currentThread());
return null;
}
/**
* 返回指定內容URL的MIME類型
* @param uri 具體Url
* @return MIME類型 比如圖片、視頻等等,可直接返回null
*/
@Nullable
@Override
public String getType(@NonNull Uri uri) {
return null;
}
}
MIME類型命名規範:
- 必須以vnd爲開頭
- 如果URI以路徑爲結尾,則後接android.cursor.dir/,如果URi以記錄id爲結尾,則後接android.cursor.item/
- 最後接上vnd.<授權信息Authority>.<路徑Path>
示例:
//URi以路徑Path爲結尾:content://com.hzw.progress/Book
vnd.android.cursor.dir/vnd.com.hzw.progress.Book
//URi以id爲結尾:content://com.hzw.progress/Book/1
vnd.android.cursor.dir/vnd.com.hzw.progress.1
注意:
1、onCreate方法是在主線程中運行,不能做耗時操作
2、query、insert、delete、update四個核心方法是運行在Binder線程中,並不是主線程
3、當線程併發時,需在覈心方法中處理同步問題。
在Mainfest文件中聲明註冊ContentProvider
<provider
android:authorities="com.gwz.progress"
android:name=".provider.BookProvider"
android:permission="com.gwz.PROVIDER" //包括全部權限
android:process=":provider"/>
- authorities:ContetProvider唯一標識
- permission:聲明訪問ContentProvider需要的權限,一般是另一外應用需要訪問該ContentProvider需要聲明該權限,否則外界應用會異常終止。
- process:開啓一個新進程
關於權限,可分別聲明讀寫的所需不同的權限
android:writePermission=”” 寫權限
android:readPermission=”” 讀權限
android:permission=”” 全部權限
外部(Activity)調用
Uri uri = Uri.parse("content://com.hzw.progress");
getContentResolver().query(uri, null, null, null, null);
getContentResolver().query(uri, null, null, null, null);
getContentResolver().query(uri, null, null, null, null);
四、ContentProvider基本使用
ContentProvider的使用主要從系統內置、自定義這兩方面深入瞭解
- 系統內置的ContentProvider,比如Android的通訊錄、日程表、相冊等等,很多。
- 自定義ContentProvider,分爲兩種進程間和進程內,必須重寫上面的6個方法。
在使用ContentProvider之前,必須對ContentResolver這個類進行了解下,因爲ContentProvider 類並不會直接與外部進程交互,而是通過 ContentResolver 類。
1、ContentResolver內容解析器
ContentResolver是個抽象類,具體實例是Content中就已經初始化了,也就是說應用一啓動就初始化了。
public abstract class ContentResolver {
}
ContentResolver的作用:統一管理不同的ContentProvider間的操作,ContentResolver提供了與ContentProvider一樣的增刪改查方法。
// 外部進程向 ContentProvider 中添加數據
public Uri insert(Uri uri, ContentValues values)
// 外部進程 刪除 ContentProvider 中的數據
public int delete(Uri uri, String selection, String[] selectionArgs)
// 外部進程更新 ContentProvider 中的數據
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs)
// 外部應用 獲取 ContentProvider 中的數據
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder)
2、系統內置ContentProvider的使用
下面以獲取手機通訊錄信息爲例,先得到ContentResolver對象,緊接着調用其query方法,注意需要READ_CONTACTS
授權
public class ContentProviderActivity extends AppCompatActivity{
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_content_provider);
}
private static final int CALL_CODE=100;
private static final int CONTACTS_CODE=200;
public void onClick(View view) {
switch (view.getId()){
case R.id.but_contacts:
//檢查是否授權成功
int checkSelfPermission = ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS);
if (checkSelfPermission!=PackageManager.PERMISSION_GRANTED){
ActivityCompat.requestPermissions(this,new String[]{Manifest.permission.READ_CONTACTS},CONTACTS_CODE);
}else {
//讀取手機聯繫人
readContacts();
}
break;
}
}
//讀取手機聯繫人
private void readContacts(){
//通過ContentResolver對象,查詢所有的聯繫人
Cursor cursor = getContentResolver().query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, null, null, null, null);
try {
if (cursor!=null){
//遍歷Cursor ,讀取所有的信息
while (cursor.moveToNext()){
//聯繫人姓名
String contactName = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
//聯繫人號碼
String contactPhone = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
Log.i(TAG, "聯繫人姓名:"+contactName+" 號碼:"+contactPhone);
}
}
}catch (SecurityException e){
e.printStackTrace();
}finally {
//關閉光標cursor,避免內存泄漏
if (cursor!=null){
cursor.close();
}
}
}
//關於權限的代碼和上面是一樣的,這裏就不貼下
……………………
}
輸出結果:
3、自定義ContentProvider的使用
所謂自定義ContentProvider就是繼承於ContentProvider類,重寫核心方法,一般用於應用間的數據共享,就是我們常說的使用ContentProvider進行進程間通信,其實訪問系統內置的ContentProvider也屬於進程間訪問,一般應用內很少使用ContentProvider進行數據共享,在應用內進行數據共享有很多其他更便捷的方式,比如file文件、sp、db等等,如果在應用內使用ContentProvider數據共享感覺有點大題小做。
基本使用步驟,可大體分以下三個步驟:
- 設計數據存儲方式
(1)可選擇文件和數據庫進行存儲,一般情況下會用數據庫。
- 創建ContentProvider子類,實現核心方法
(1)使用UriMatcher對象統一管理不同的Uri
(2)在onCreate方法初始化一些數據,注意不要耗時操作,可開啓一個子線程執行。另外,只有外界調用了getContentResolver方法,纔會觸發onCreate的初始化。
(3)當數據發生了變化(比如增刪改操作)時,需通過ContentResolver的notifyChange方法通知外界訪問者ContentProvider中的數據已經改變了。
- 外界通過ContentResolver和Uri對ContentProvider操作
4、設計數據存儲
這裏我們使用SQLite進行數據存儲,實現SQLiteOpenHelper幫助類的子類
public class BookDbHelper extends SQLiteOpenHelper{
//數據庫名稱
private static final String DATABASE_NAME="Book.db";
//表名稱
public static final String USER_TABLE_NAME="user";
public static final String BOOK_TABLE_NAME="book";
//數據庫版本號
private static final int DATABASE_VERSION=1;
public BookDbHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
// 創建兩個表格:user表 和book表
//user表是個3列的結構,有id、name、sex三個屬性值
db.execSQL("create table if not exists " + USER_TABLE_NAME + "(_id integer primary key autoincrement," + "name TEXT,"+"sex TEXT)");
//book表是個2列結構,有id,book兩個屬性值
db.execSQL("create table if not exists " + BOOK_TABLE_NAME + "(_id integer primary key autoincrement," + "book TEXT)");
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
}
}
5、創建ContentProvider子類
public class BookProvider extends ContentProvider{
private static final String TAG = "BookProvider";
//1、ContentProvider唯一標識
public static final String AUTHORITY="com.hzw.progress";
//2、分別爲book和user表指定Uri
public static final Uri BOOK_CONTENT_URI=Uri.parse("content://"+AUTHORITY+"/book");
public static final Uri USER_CONTENT_URI=Uri.parse("content://"+AUTHORITY+"/user");
//3、創建UriMatcher對象,用於管理Uri
private static final UriMatcher uriMatcher=new UriMatcher(UriMatcher.NO_MATCH);
private static final int BOOK_URI_CODE=0;
private static final int USER_URI_CODE=1;
//4、初始化UriMatcher元素,將book表和User表的Uri添加到UriMatcher中,可根據code值取對應的表名
static {
uriMatcher.addURI(AUTHORITY,"book",BOOK_URI_CODE);
uriMatcher.addURI(AUTHORITY,"user",USER_URI_CODE);
}
// 5、根據Uri在UriMatcher中匹配得到對應的表名
private String getTableName(Uri uri){
String tableName="";
switch (uriMatcher.match(uri)){
case BOOK_URI_CODE:
tableName=BookDbHelper.BOOK_TABLE_NAME;
break;
case USER_URI_CODE:
tableName=BookDbHelper.USER_TABLE_NAME;
break;
}
return tableName;
}
private BookDbHelper mDbHelper;
private SQLiteDatabase mWritableDatabase;
private Context mContext;
/**
* ContentProvider初始化被調用
* 一般用於創建數據庫或者升級等操作,只有在ContentResolver訪問的時候,纔會觸發onCreate方法
* 此方法是主線程執行,不能做耗時操作
* @return true:ContentProvider初始化成功,false則失敗
*/
@Override
public boolean onCreate() {
Log.d(TAG, "onCreate: 當前線程:"+Thread.currentThread().getName());
mContext = getContext();
//6、初始化數據庫數據,不能在主線程做耗時操作
//創建數據庫幫助類,通過BookDbHelper得到SQLite數據庫寫入實例
mWritableDatabase = new BookDbHelper(mContext).getWritableDatabase();
new Thread(new Runnable() {
@Override
public void run() {
initProviderData();
}
}).start();
return true;
}
private void initProviderData() {
//先刪除清空Book、User表的數據
mWritableDatabase.execSQL("delete from "+BookDbHelper.BOOK_TABLE_NAME);
mWritableDatabase.execSQL("delete from "+BookDbHelper.USER_TABLE_NAME);
//接着在數據庫中添加數據,分別給book、user表插入數據
mWritableDatabase.execSQL("insert into book values(2,'Android開發藝術探索');");
mWritableDatabase.execSQL("insert into book values(3,'Android進階之光');");
mWritableDatabase.execSQL("insert into user values(3,'張衛健','男');");
mWritableDatabase.execSQL("insert into user values(4,'微微','女');");
}
/**
* 查詢數據 -- 查
* @param uri 根據uri查詢具體哪張數據表
* @param projection 確定查詢表中哪些列 ,傳null則返回所有的列
* @param selection 確定查詢哪行,傳null這返回所有的行
* @param selectionArgs 與selection類似
* @param sortOrder 用於對查詢結果進行排序,傳null則使用默認的排序方式,也可以是無序的。
* @return 查詢的返回值,是個Cursor對象,在取完數據需進行關閉。否則會內存泄漏
*/
@Nullable
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
Log.d(TAG, "query: 當前線程:"+Thread.currentThread().getName());
String tableName = getTableName(uri);
return mWritableDatabase.query(tableName, projection, selection, selectionArgs,null,null, sortOrder);
}
/**
* 插入一條新數據(添加數據)--增
* @param uri 根據uri插入到具體哪張數據表
* @param values ContentValues底層是key-value鍵值對結構,使用HashMap實現的
* key:表示列名,value:表示行名,如果value爲空,在表中則是空行,無內容
* @return 添加成功後,返回這條新數據的uri。
*/
@Nullable
@Override
public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
Log.d(TAG, "insert: 當前線程:"+Thread.currentThread().getName());
// a、通過Uri插入具體表中
String tableName = getTableName(uri);
mWritableDatabase.insert(tableName, null, values);
//b、數據發生改變,通知外部調用者
mContext.getContentResolver().notifyChange(uri,null);
return uri;
}
/**
* 刪除數據 -- 刪
* @param uri 根據uri刪除哪張表的數據
* @param selection 根據條件刪除具體哪行數據
* @param selectionArgs 與selection類似
* @return 返回被刪除的行數。
*/
@Override
public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
Log.d(TAG, "delete: 當前線程"+Thread.currentThread().getName());
String tableName = getTableName(uri);
int count = mWritableDatabase.delete(tableName, selection, selectionArgs);
//如果大於0,則刪除成功
if (count>0){
mContext.getContentResolver().notifyChange(uri,null);
}
return count;
}
/**
* 更新數據 -- 改
* @param uri 根據Uri修改具體哪張表的數據
* @param values 與insert的ContentValues一樣,key是列名,若傳入的value爲空,則會刪除原來的數據置空。
* @param selection 選擇符合該條件的行數據進行修改
* @param selectionArgs 與selection類似
* @return 更新的行數
*/
@Override
public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
Log.d(TAG, "update: 當前線程:"+Thread.currentThread().getName());
String tableName = getTableName(uri);
int rows = mWritableDatabase.update(tableName, values, selection, selectionArgs);
if (rows>0){
mContext.getContentResolver().notifyChange(uri,null);
}
return 0;
}
/**
* 返回指定內容URL的MIME類型
* @param uri 具體Url
* @return MIME類型 比如圖片、視頻等等 ,可直接返回null
*/
@Nullable
@Override
public String getType(@NonNull Uri uri) {
return null;
}
}
緊接着需要另外一個進程的Mainfest文件聲明註冊ContentProvider,並設置permission和authorities屬性值。
<!--自定義訪問ContentProvider的權限-->
<permission android:name="com.hzw.PROVIDER"/>
<!--在進程中註冊該權限-->
<uses-permission android:name="com.hzw.PROVIDER"/>
****
<provider
android:name=".provider.BookProvider"
android:authorities="com.hzw.progress"
android:permission="com.hzw.PROVIDER"
android:exported="true"
android:process=":provider"/>
6、通過 ContentResolver 和 URI 進行增刪改查
//檢查是否授權
int checkSelfPermission1 = ActivityCompat.checkSelfPermission(this, com.hzw.progress.Manifest.permission.PROVIDER);
if (checkSelfPermission1!= PackageManager.PERMISSION_GRANTED){
Log.d(TAG, "當前進程沒有獲取BookProvider的權限,不可進行訪問");
return;
}
Uri bookUri = Uri.parse("content://com.hzw.progress/book");
//在book表添加一條數據
ContentValues values = new ContentValues();
values.put("_id","6");
values.put("book","Android高級進階");
getContentResolver().insert(bookUri,values);
//查詢book表所有數據
Cursor bookCursor = getContentResolver().query(bookUri, new String[]{"_id","book"}, null, null, null);
if (bookCursor!=null){
while (bookCursor.moveToNext()){
int bookId = bookCursor.getInt(0);
String bookName = bookCursor.getString(1);
Log.d(TAG, "bookId: "+bookId+",bookName:"+bookName);
}
}
if (bookCursor!=null){
bookCursor.close();
}
//查詢user表所有數據
Uri userUri = Uri.parse("content://com.hzw.progress/user");
Cursor userCursor = getContentResolver().query(userUri, new String[]{"_id","name","sex"}, null, null, null);
if (userCursor!=null){
while (userCursor.moveToNext()){
int userId = userCursor.getInt(0);
String userName = userCursor.getString(1);
String sex = userCursor.getString(2);
Log.d(TAG, "userId: "+userId+",userName:"+userName+",sex:"+sex);
}
}
if (userCursor!=null){
userCursor.close();
}
以上運行結果:
到這裏ContentProvider內容提供者一些常用知識點基本差不多了,底層的實現同樣是Binder機制,ContentProvider對Binder進行了封裝。在使用ContentProvider跨進程通信也會存在一些安全性問題,比如SQL語句注入、線程同步等問題。
參考
第一行代碼2版
安卓開發藝術探索