你手机里的通讯录,存储了所有联系人的信息。如果你想把这些联系人信息分享给其他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);}}}
}