四大组件-ContentProvider

36 阅读5分钟

1.1 什么是ContentProvider?

  • Android四大组件之一
  • 用于应用程序间的数据共享
  • 提供统一的数据访问接口,封装底层数据存储细节

1.2 主要特性

  • 跨进程数据访问:不同应用间安全共享数据
  • 统一接口:使用URI标识数据资源
  • 权限控制:通过权限机制保护数据访问
  • 数据类型:支持多种数据类型(SQLite、文件等)

1.3 应用场景

  • 系统内置数据访问(联系人、日历、媒体库等)
  • 应用间数据共享(如社交应用分享数据)
  • 插件化架构中的数据交互
  • 数据备份和恢复

二、核心组件

2.1 URI(统一资源标识符)

// URI格式
content://<authority>/<path>/<id>
// 示例
content://com.example.provider/user/123
  • Authority:唯一标识ContentProvider,格式为包名.provider
  • Path:数据路径,通常对应表名
  • ID:可选,标识特定记录

2.2 ContentProvider类

public class MyProvider extends ContentProvider {
    // 必须实现的6个核心方法
    public boolean onCreate() { }
    public Cursor query(Uri uri, String[] projection, String selection,
                        String[] selectionArgs, String sortOrder) { }
    public Uri insert(Uri uri, ContentValues values) { }
    public int update(Uri uri, ContentValues values, String selection,
                      String[] selectionArgs) { }
    public int delete(Uri uri, String selection, String[] selectionArgs) { }
    public String getType(Uri uri) { }
}

2.3 ContentResolver类

  • 客户端应用通过ContentResolver访问ContentProvider
  • 提供与ContentProvider对应的CRUD方法
  • 系统服务,通过getContentResolver()获取

2.4 ContentObserver

  • 观察数据变化的监听器
  • 实现数据变更的通知机制

三、具体实现

3.1 创建ContentProvider步骤

步骤1:定义URI和常量

public final class UserContract {
    public static final String AUTHORITY = "com.example.userprovider";
    public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/users");
    
    public static final class UserEntry {
        public static final String TABLE_NAME = "users";
        public static final String _ID = "_id";
        public static final String COLUMN_NAME = "name";
        public static final String COLUMN_AGE = "age";
        public static final String COLUMN_EMAIL = "email";
    }
}

步骤2:实现ContentProvider

public class UserProvider extends ContentProvider {
    private static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
    private static final int USERS = 100;
    private static final int USER_ID = 101;
    
    static {
        URI_MATCHER.addURI(UserContract.AUTHORITY, "users", USERS);
        URI_MATCHER.addURI(UserContract.AUTHORITY, "users/#", USER_ID);
    }
    
    private DatabaseHelper dbHelper;
    
    @Override
    public boolean onCreate() {
        dbHelper = new DatabaseHelper(getContext());
        return true;
    }
    
    @Override
    public Cursor query(Uri uri, String[] projection, String selection,
                        String[] selectionArgs, String sortOrder) {
        SQLiteDatabase db = dbHelper.getReadableDatabase();
        SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
        queryBuilder.setTables(UserContract.UserEntry.TABLE_NAME);
        
        int match = URI_MATCHER.match(uri);
        switch (match) {
            case USERS:
                // 查询所有用户
                break;
            case USER_ID:
                // 查询单个用户
                queryBuilder.appendWhere(UserContract.UserEntry._ID + "=" + 
                                        uri.getLastPathSegment());
                break;
            default:
                throw new IllegalArgumentException("Unknown URI: " + uri);
        }
        
        return queryBuilder.query(db, projection, selection, 
                                 selectionArgs, null, null, sortOrder);
    }
    
    // 实现其他方法...
}

步骤3:配置AndroidManifest.xml

<provider
    android:name=".UserProvider"
    android:authorities="com.example.userprovider"
    android:exported="true"
    android:permission="com.example.READ_USER"
    android:readPermission="com.example.READ_USER"
    android:writePermission="com.example.WRITE_USER" >
    
    <!-- 可选:为特定路径设置不同权限 -->
    <path-permission
        android:pathPrefix="/admin"
        android:permission="com.example.ADMIN" />
</provider>

3.2 客户端使用示例

查询数据

// 查询所有用户
Cursor cursor = getContentResolver().query(
    UserContract.CONTENT_URI,
    new String[] { UserContract.UserEntry._ID, 
                   UserContract.UserEntry.COLUMN_NAME },
    null,
    null,
    UserContract.UserEntry.COLUMN_NAME + " ASC"
);

if (cursor != null && cursor.moveToFirst()) {
    do {
        String name = cursor.getString(
            cursor.getColumnIndex(UserContract.UserEntry.COLUMN_NAME)
        );
        // 处理数据...
    } while (cursor.moveToNext());
    cursor.close();
}

插入数据

ContentValues values = new ContentValues();
values.put(UserContract.UserEntry.COLUMN_NAME, "张三");
values.put(UserContract.UserEntry.COLUMN_AGE, 25);
values.put(UserContract.UserEntry.COLUMN_EMAIL, "zhangsan@example.com");

Uri newUri = getContentResolver().insert(
    UserContract.CONTENT_URI,
    values
);

更新数据

ContentValues values = new ContentValues();
values.put(UserContract.UserEntry.COLUMN_AGE, 26);

String selection = UserContract.UserEntry.COLUMN_NAME + " = ?";
String[] selectionArgs = { "张三" };

int rowsUpdated = getContentResolver().update(
    UserContract.CONTENT_URI,
    values,
    selection,
    selectionArgs
);

删除数据

String selection = UserContract.UserEntry.COLUMN_AGE + " < ?";
String[] selectionArgs = { "18" };

int rowsDeleted = getContentResolver().delete(
    UserContract.CONTENT_URI,
    selection,
    selectionArgs
);

四、进阶功能

4.1 批量操作

ArrayList<ContentProviderOperation> operations = new ArrayList<>();
operations.add(ContentProviderOperation.newInsert(UserContract.CONTENT_URI)
    .withValue(UserContract.UserEntry.COLUMN_NAME, "李四")
    .withValue(UserContract.UserEntry.COLUMN_AGE, 30)
    .build());
    
operations.add(ContentProviderOperation.newUpdate(UserContract.CONTENT_URI)
    .withSelection(UserContract.UserEntry.COLUMN_NAME + " = ?", 
                  new String[]{"张三"})
    .withValue(UserContract.UserEntry.COLUMN_AGE, 27)
    .build());

try {
    ContentProviderResult[] results = getContentResolver().applyBatch(
        UserContract.AUTHORITY, 
        operations
    );
} catch (RemoteException | OperationApplicationException e) {
    e.printStackTrace();
}

4.2 文件共享(FileProvider)

// 继承FileProvider
public class MyFileProvider extends FileProvider {
    // 使用android.support.v4.content.FileProvider
}

// AndroidManifest配置
<provider
    android:name="androidx.core.content.FileProvider"
    android:authorities="com.example.fileprovider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_paths" />
</provider>

// XML配置(res/xml/file_paths.xml)
<?xml version="1.0" encoding="utf-8"?>
<paths>
    <files-path name="files" path="." />
    <cache-path name="cache" path="." />
    <external-files-path name="external_files" path="." />
    <external-cache-path name="external_cache" path="." />
</paths>

4.3 数据变更通知

// 注册ContentObserver
getContentResolver().registerContentObserver(
    UserContract.CONTENT_URI,
    true, // 是否监听所有子URI
    new ContentObserver(new Handler()) {
        @Override
        public void onChange(boolean selfChange) {
            // 数据发生变化,更新UI
            refreshData();
        }
    }
);

// 在Provider中通知变化
getContext().getContentResolver().notifyChange(uri, null);

4.4 自定义权限

<!-- 定义权限 -->
<permission
    android:name="com.example.READ_USER"
    android:protectionLevel="dangerous"
    android:label="读取用户数据"
    android:description="允许应用读取用户数据" />

<permission
    android:name="com.example.WRITE_USER"
    android:protectionLevel="dangerous"
    android:label="写入用户数据"
    android:description="允许应用写入用户数据" />

五、最佳实践与优化

5.1 性能优化

  1. 使用索引:为查询条件创建数据库索引
  2. 批量操作:使用applyBatch减少事务开销
  3. 异步查询:使用CursorLoader或异步任务
  4. 分页查询:实现limit和offset支持
  5. 缓存策略:合理缓存频繁访问的数据

5.2 线程安全

// 使用单例确保线程安全
private static final Object sLock = new Object();
private DatabaseHelper mDbHelper;

private SQLiteDatabase getDatabase() {
    synchronized (sLock) {
        if (mDbHelper == null) {
            mDbHelper = new DatabaseHelper(getContext());
        }
        return mDbHelper.getWritableDatabase();
    }
}

5.3 安全防护

// 输入验证
@Override
public Cursor query(Uri uri, String[] projection, String selection,
                    String[] selectionArgs, String sortOrder) {
    // 验证URI
    validateUri(uri);
    
    // 防止SQL注入
    if (selection != null) {
        // 验证selection参数
        validateSelection(selection);
    }
    
    // 权限检查
    checkPermission(uri, "read");
    
    // 实际查询逻辑...
}

5.4 错误处理

@Override
public Uri insert(Uri uri, ContentValues values) {
    try {
        // 插入逻辑
        long id = db.insert(/* 参数 */);
        if (id == -1) {
            throw new SQLException("Failed to insert row into " + uri);
        }
        
        // 通知变化
        getContext().getContentResolver().notifyChange(uri, null);
        
        return ContentUris.withAppendedId(uri, id);
    } catch (Exception e) {
        Log.e(TAG, "Insert failed", e);
        // 根据具体情况处理异常
        if (e instanceof SQLiteConstraintException) {
            throw new IllegalArgumentException("Data conflict: " + e.getMessage());
        } else {
            throw new RuntimeException("Database error", e);
        }
    }
}

六、常见问题与解决方案

6.1 权限问题

  • 问题:Permission Denial accessing ContentProvider
  • 解决:
    1. 检查AndroidManifest中权限声明
    2. 运行时权限请求(Android 6.0+)
    3. 使用grantUriPermissions临时授权

6.2 跨进程性能

  • 优化:
    1. 减少跨进程调用次数(批量操作)
    2. 只传输必要数据
    3. 使用Binder传输优化

6.3 数据类型转换

  • 注意:
    1. URI中ID为字符串类型
    2. 使用ContentValues进行数据包装
    3. 注意数据类型的兼容性

6.4 版本兼容性

  • 策略:
    1. 保持URI格式向后兼容
    2. 数据库迁移时注意数据格式
    3. 使用@RequiresApi注解标注API要求

七、总结

ContentProvider是Android数据共享的核心组件,掌握它需要理解:

  1. 核心概念:URI、ContentResolver、权限机制
  2. 实现细节:CRUD操作、线程安全、性能优化
  3. 进阶功能:批