你手機里的通訊錄,存儲了所有聯系人的信息。如果你想把這些聯系人信息分享給其他App,就可以通過ContentProvider來實現。。
一、什么是 ContentProvider
?ContentProvider? 是 Android 四大組件之一,負責實現?跨應用程序的數據共享與訪問?,通過統一接口封裝數據存儲細節,提供標準化操作方式。其中主要功能包括:
- 數據抽象層:將應用內部的數據(如 SQLite 數據庫、文件等)封裝成統一的接口對外提供。
- 跨應用數據共享:允許其他應用安全地訪問和操作本應用的數據。
- 數據權限控制:通過 URI 和權限機制,精確控制數據的訪問范圍。
- 統一數據訪問:提供類似數據庫的 CRUD 操作接口,簡化數據使用。
二、ContentProvider 的核心概念
-
URI(統一資源標識符)
- 格式:
content://authority/path/id
- 示例:
content://com.example.provider/users/1
authority
:標識 ContentProvider,通常為應用包名 + provider 名path
:標識要訪問的數據集合id
:可選,標識具體記錄
- 格式:
-
ContentResolver
- 應用通過 ContentResolver 與 ContentProvider 通信
- 提供 query ()、insert ()、update ()、delete () 等方法
-
Cursor
- 查詢結果的返回類型,類似數據庫查詢結果集
- 通過 Cursor 獲取和遍歷數據
三、ContentProvider 的實現步驟
以下是實現一個簡單 ContentProvider 的完整步驟:
1.創建數據模型
// User.java
public class User {private int id;private String name;private int age;// getters and setters
}
2.創建 SQLiteOpenHelper 管理數據庫
// DatabaseHelper.java
public class DatabaseHelper extends SQLiteOpenHelper {private static final String DB_NAME = "user.db";private static final int DB_VERSION = 1;public static final String TABLE_NAME = "users";public DatabaseHelper(Context context) {super(context, DB_NAME, null, DB_VERSION);}@Overridepublic void onCreate(SQLiteDatabase db) {db.execSQL("CREATE TABLE " + TABLE_NAME + " (" +"_id INTEGER PRIMARY KEY AUTOINCREMENT, " +"name TEXT, " +"age INTEGER);");}@Overridepublic void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);onCreate(db);}
}
3.實現 ContentProvider
// UserProvider.java
public class UserProvider extends ContentProvider {private DatabaseHelper dbHelper;public static final String AUTHORITY = "com.example.provider";public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/users");@Overridepublic boolean onCreate() {dbHelper = new DatabaseHelper(getContext());return true;}@Overridepublic Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {SQLiteDatabase db = dbHelper.getReadableDatabase();return db.query(DatabaseHelper.TABLE_NAME, projection, selection, selectionArgs, null, null, sortOrder);}@Overridepublic Uri insert(Uri uri, ContentValues values) {SQLiteDatabase db = dbHelper.getWritableDatabase();long id = db.insert(DatabaseHelper.TABLE_NAME, null, values);return ContentUris.withAppendedId(CONTENT_URI, id);}@Overridepublic int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {SQLiteDatabase db = dbHelper.getWritableDatabase();return db.update(DatabaseHelper.TABLE_NAME, values, selection, selectionArgs);}@Overridepublic int delete(Uri uri, String selection, String[] selectionArgs) {SQLiteDatabase db = dbHelper.getWritableDatabase();return db.delete(DatabaseHelper.TABLE_NAME, selection, selectionArgs);}@Overridepublic String getType(Uri uri) {return "vnd.android.cursor.dir/vnd.com.example.provider.users";}
}
4.在 AndroidManifest.xml 中注冊 Provider
<providerandroid:name=".UserProvider"android:authorities="com.example.provider"android:exported="true"android:grantUriPermissions="true">
</provider>
四、ContentProvider 的使用示例
其他應用通過 ContentResolver 訪問該 Provider:
// 查詢所有用戶
Cursor cursor = getContentResolver().query(UserProvider.CONTENT_URI, null, null, null, null
);// 插入新用戶
ContentValues values = new ContentValues();
values.put("name", "John");
values.put("age", 30);
Uri newUri = getContentResolver().insert(UserProvider.CONTENT_URI, values);// 更新用戶
ContentValues updateValues = new ContentValues();
updateValues.put("age", 31);
int count = getContentResolver().update(UserProvider.CONTENT_URI, updateValues, "name=?", new String[]{"John"}
);// 刪除用戶
int deleted = getContentResolver().delete(UserProvider.CONTENT_URI, "age > ?", new String[]{"40"}
);
五、跨應用權限控制
配置目標 | 實現方式 |
跨應用調用權限 | 調用方聲明<uses-permission>,Porvider方配置android:exported="true"。 |
動態權限申請 | 針對dangerous級別權限,調用方需在運行時請求用戶授權 |
路徑級訪問控制 | Provider方通過<path-permission>細化權限,調用方需匹配聲明 |
1. 聲明 Provider 權限
<!-- 定義自定義權限 -->
<permission android:name="com.example.READ_USERS" android:protectionLevel="dangerous" />
<permission android:name="com.example.WRITE_USERS" android:protectionLevel="dangerous" /> <!-- 應用權限到 Provider -->
<provider android:name=".UserProvider" <!-- Provider 實現類的全路徑 --> android:authorities="com.example.provider" <!-- 唯一標識符,與Contract類一致 --> android:exported="true" <!-- 是否允許其他應用訪問(默認 false) --> android:readPermission="com.example.READ_USERS" android:writePermission="com.example.WRITE_USERS" />
protectionLevel
?設為?dangerous
?表示需用戶手動授權。readPermission
/writePermission
:自定義權限控制。
2.?路徑級權限細化(可選)
若 Provider 方通過?<path-permission>
?限制特定路徑,調用方需確保擁有對應權限:
<!-- Provider 方配置 -->
<provider ...> <path-permission android:pathPrefix="/admin" android:permission="com.example.ADMIN_PERMISSION" />
</provider>
3. 調用方配置
<manifest ...> <!-- 聲明權限 --> <uses-permission android:name="com.example.READ_USERS" /> <uses-permission android:name="com.example.WRITE_USERS" /> <!-- 如果存在路徑細化,調用方需聲明額外權限 --> <uses-permission android:name="com.example.ADMIN_PERMISSION" /> <application ...> <!-- 無 Provider 聲明,直接通過 ContentResolver 調用 --> </application>
</manifest>
4. 動態權限申請?
在調用方的 Activity/Fragment 中實現動態權限申請流程:
public class MainActivity extends AppCompatActivity { private static final int REQUEST_READ_PERMISSION = 100; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // 檢查權限 if (ContextCompat.checkSelfPermission(this, "com.example.READ_USERS") != PackageManager.PERMISSION_GRANTED) { // 權限未授予,顯示申請彈窗 ActivityCompat.requestPermissions(this, new String[]{"com.example.READ_USERS"}, REQUEST_READ_PERMISSION); } else { // 已授權,執行數據訪問 queryData(); } } // 處理權限申請結果 @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode == REQUEST_READ_PERMISSION) { if (grantResults.length > 0 && grantResults[0]== PackageManager.PERMISSION_GRANTED) { queryData(); } else { // 權限被拒絕,提示用戶 Toast.makeText(this, "權限被拒絕,無法讀取數據",Toast.LENGTH_SHORT).show(); } } } private void queryData() { // 通過 ContentResolver 訪問 Provider 數據 Cursor cursor = getContentResolver().query( UserContract.CONTENT_URI, null, null, null, null ); // 處理查詢結果... }
}
同一權限組內的權限只需申請一次(如?
READ_CONTACTS
?和?WRITE_CONTACTS
?屬于同一組)
4.??用戶拒絕后引導設置?
若用戶勾選“不再詢問”,需引導用戶前往系統設置手動開啟權限(可通過?shouldShowRequestPermissionRationale
?判斷)。
if (ActivityCompat.shouldShowRequestPermissionRationale(this,"com.example.READ_USERS")) { // 用戶之前可能拒絕過權限但未勾選“不再詢問”// 展示解釋性彈窗后再次申請
} else { // 用戶勾選“不再詢問”或系統禁止權限(如廠商定制 ROM 限制)// 跳轉系統設置界面手動開啟權限 Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); intent.setData(Uri.parse("package:" + getPackageName())); startActivity(intent);
}
若用戶?從未請求過該權限?,shouldShowRequestPermissionRationale() 也會返回 false。但此時代碼通常不會進入此分支(因首次請求時直接調用 requestPermissions())。
部分設備可能不支持 Settings.ACTION_APPLICATION_DETAILS_SETTINGS,需增加異常捕獲并提示用戶手動查找權限設置 。?
六、數據變更通知
角色 | 職責 |
客戶端 | 注冊ContentObserver并實現onChange回調邏輯(如刷新UI) |
ContentProvider | 數據變更時調用notifyChange觸發通知 |
系統服務 | 通過ContentService統一管理觀察者,完成消息分發 |
?1. 客戶端注冊觀察者
在使用數據的客戶端(如 Activity、Fragment)中,通過?ContentResolver
?注冊?ContentObserver
,并指定監聽的目標 URI,從而實時更新UI。
// 使用者(Activity)通過ContentResolver注冊觀察者
getContentResolver().registerContentObserver( UserContract.CONTENT_URI, true, // 是否監聽子 URI new ContentObserver(new Handler()) { @Override public void onChange(boolean selfChange) { // 數據變化時觸發回調} }
);
registerContentObserver
?是客戶端主動調用的方法,用于綁定觀察者與目標數據 URI。true
?表示監聽該 URI 及其所有子路徑(如?content://com.example.provider/users/
)的數據變更。
2. 提供者觸發通知
在 ?ContentProvider? 中,當數據發生變更(如?insert
、update
、delete
)時,需調用?notifyChange
?方法觸發回調:
// 在 Provider 的 insert/update/delete 方法中
getContext().getContentResolver().notifyChange(uri, null);
notifyChange
?會通知所有注冊了該 URI 的觀察者。- 可通過第二個參數?
observer
?指定跳過特定觀察者(通常設為?null
)。
?3. 系統級支持?
- ?ContentService?:負責管理所有注冊的觀察者,以樹形結構維護 URI 監聽關系,實現高效的跨進程通知分發。
- ?Binder 機制?:底層通過 Binder 傳遞觀察者對象(封裝為?
Transport
?代理),確保跨進程通信的可行性。
客戶端需主動注冊觀察者監聽 URI,而通知觸發由 Provider 發起,兩者通過系統服務協同實現實時數據同步。
七、ContentProvider 的性能優化
1.使用 SQLite 事務
- 批量操作時使用事務提高性能
SQLiteDatabase db = dbHelper.getWritableDatabase();
db.beginTransaction();
try {// 執行多個操作db.setTransactionSuccessful();
} finally {db.endTransaction();
}
索引優化
- 對經常查詢的字段添加索引
db.execSQL("CREATE INDEX IF NOT EXISTS idx_name ON users(name);");
避免在主線程進行耗時操作
- 使用 Loader 或異步任務執行查詢
getSupportLoaderManager().initLoader(0, null, this);
八、ContentProvider 的安全注意事項
-
謹慎設置 android:exported
- 僅在需要對外共享數據時設置為 true
- 默認值為 false,可防止外部訪問
-
輸入驗證
- 對傳入的 selection 和 projection 參數進行驗證
private void validateProjection(String[] projection) {if (projection != null) {for (String col : projection) {if (!allowedColumns.contains(col)) {throw new IllegalArgumentException("Invalid column: " + col);}}}
}