ContentProvider与Uri权限:跨应用数据共享

Android 15 核心子系统系列 - 第26篇

本篇深入分析Android的跨应用数据共享机制,理解ContentProvider的生命周期、Uri权限授予和ContentObserver通知机制。

引言

想象一个场景:你的相机应用拍了一张照片,现在想通过微信分享。微信如何访问你相机应用的私有数据?如果直接给文件路径,会因为权限限制无法访问;如果把文件复制一份,浪费存储空间...

这就是ContentProvider 解决的核心问题------在保证安全的前提下,实现跨应用数据共享 。通过Uri权限机制,应用可以临时授权其他应用访问特定数据,无需暴露文件系统路径,也不需要用户授予运行时权限。

在上一篇任务调度中,我们看到Android如何智能管理后台任务;今天我们将看到,Android如何在多个应用之间安全共享数据

一、ContentProvider整体架构

1.1 架构设计哲学

ContentProvider的设计遵循几个核心原则:

统一接口 :使用标准的CRUD接口(query/insert/update/delete) Uri寻址 :通过Uri(content://authority/path)定位数据 权限控制 :支持静态Manifest权限和动态Uri权限 进程隔离 :Provider运行在独立进程,Binder IPC通信 数据观察:ContentObserver监听数据变化

1.2 四层架构

核心流程

  1. 客户端:通过ContentResolver发起CRUD操作
  2. Framework:AMS管理Provider生命周期和权限检查
  3. Provider进程:执行实际数据操作
  4. 数据存储:SQLite、文件系统、SharedPreferences等
  5. 通知机制:数据变化时通知所有观察者

1.3 核心组件分析

ContentResolver - 客户端代理

java 复制代码
// frameworks/base/core/java/android/content/ContentResolver.java
public abstract class ContentResolver {
    // 查询数据
    public final Cursor query(
            Uri uri,
            String[] projection,
            String selection,
            String[] selectionArgs,
            String sortOrder) {
        // 1. 通过AMS获取Provider
        IContentProvider provider = acquireProvider(uri);

        // 2. 跨进程调用Provider的query方法
        Cursor cursor = provider.query(
            mPackageName,
            uri,
            projection,
            selection,
            selectionArgs,
            sortOrder,
            null  // CancellationSignal
        );

        return cursor;
    }

    // 插入数据
    public final Uri insert(Uri url, ContentValues values) {
        IContentProvider provider = acquireProvider(url);
        Uri result = provider.insert(mPackageName, url, values);

        // 通知数据变化
        notifyChange(result, null);
        return result;
    }

    // 更新数据
    public final int update(
            Uri uri,
            ContentValues values,
            String where,
            String[] selectionArgs) {
        IContentProvider provider = acquireProvider(uri);
        int count = provider.update(
            mPackageName, uri, values, where, selectionArgs);

        // 通知数据变化
        notifyChange(uri, null);
        return count;
    }

    // 删除数据
    public final int delete(
            Uri url,
            String where,
            String[] selectionArgs) {
        IContentProvider provider = acquireProvider(url);
        int count = provider.delete(mPackageName, url, where, selectionArgs);

        // 通知数据变化
        notifyChange(url, null);
        return count;
    }
}

ActivityManagerService - Provider管理

java 复制代码
// frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java
public class ActivityManagerService extends IActivityManager.Stub {
    // Provider缓存:authority → ContentProviderRecord
    private final ProviderMap mProviderMap = new ProviderMap();

    // 获取ContentProvider
    public ContentProviderHolder getContentProvider(
            IApplicationThread caller,
            String callingPackage,
            String authority,
            int userId,
            boolean stable) {
        synchronized (this) {
            // 1. 从缓存查找
            ContentProviderRecord cpr = mProviderMap.getProviderByName(
                authority, userId);

            if (cpr != null && cpr.proc != null) {
                // Provider已存在且进程存活
                return cpr.newHolder(null);
            }

            // 2. 需要启动Provider进程
            ProcessRecord proc = startProcessLocked(
                cpi.processName,
                cpr.appInfo,
                "content provider",
                new ComponentName(cpi.packageName, cpi.name),
                userId
            );

            // 3. 安装Provider(在目标进程)
            proc.thread.scheduleInstallProvider(cpi);

            // 4. 等待Provider发布
            synchronized (cpr) {
                while (cpr.provider == null) {
                    cpr.wait();  // 等待Provider启动完成
                }
            }

            return cpr.newHolder(null);
        }
    }

    // Provider发布回调
    public void publishContentProviders(
            IApplicationThread caller,
            List<ContentProviderHolder> providers) {
        synchronized (this) {
            for (ContentProviderHolder src : providers) {
                ContentProviderRecord dst = mProviderMap.getProviderByClass(
                    src.info.name, getUserIdFromUid(src.info.applicationInfo.uid));

                if (dst != null) {
                    dst.provider = src.provider;
                    dst.proc = getRecordForAppLocked(caller);

                    // 唤醒等待线程
                    synchronized (dst) {
                        dst.notifyAll();
                    }
                }
            }
        }
    }
}

Provider生命周期管理

  • 延迟加载:首次访问时才启动Provider进程
  • 单例模式:每个authority只有一个Provider实例
  • 进程绑定:Provider与宿主进程生命周期绑定
  • 自动清理:进程死亡时自动清理Provider记录

二、ContentProvider生命周期

2.1 Provider启动流程

java 复制代码
// frameworks/base/core/java/android/app/ActivityThread.java
public final class ActivityThread extends ClientTransactionHandler {
    // 应用启动时安装Provider
    private void handleBindApplication(AppBindData data) {
        // 1. 创建Application
        Application app = data.info.makeApplication(false, mInstrumentation);

        // 2. 安装ContentProvider(在Application.onCreate之前!)
        List<ProviderInfo> providers = data.providers;
        if (providers != null) {
            installContentProviders(app, providers);
        }

        // 3. 调用Application.onCreate()
        mInstrumentation.callApplicationOnCreate(app);
    }

    private void installContentProviders(
            Context context,
            List<ProviderInfo> providers) {
        final ArrayList<ContentProviderHolder> results = new ArrayList<>();

        for (ProviderInfo cpi : providers) {
            // 1. 实例化ContentProvider
            ContentProvider localProvider = installProvider(
                context, null, cpi, false, true);

            // 2. 创建Holder
            IContentProvider provider = localProvider.getIContentProvider();
            ContentProviderHolder cph = new ContentProviderHolder(cpi);
            cph.provider = provider;
            results.add(cph);
        }

        // 3. 发布到AMS
        ActivityManager.getService().publishContentProviders(
            mAppThread, results);
    }

    private ContentProvider installProvider(
            Context context,
            IContentProvider provider,
            ProviderInfo info,
            boolean noisy,
            boolean noReleaseNeeded) {
        // 1. 反射创建Provider实例
        ClassLoader cl = context.getClassLoader();
        ContentProvider localProvider = (ContentProvider)
            cl.loadClass(info.name).newInstance();

        // 2. 调用attachInfo初始化
        localProvider.attachInfo(context, info);

        // 3. 调用onCreate()
        localProvider.onCreate();

        return localProvider;
    }
}

关键时序

scss 复制代码
Application启动
    ↓
1. makeApplication()
    ↓
2. installContentProviders()  ← Provider.onCreate()在这里
    ↓
3. Application.onCreate()

⚠️ 重要:ContentProvider.onCreate()在Application.onCreate()之前调用!不要在Application.onCreate()中访问自己的Provider,会导致循环依赖。

2.2 Provider实现示例

kotlin 复制代码
class ContactsProvider : ContentProvider() {
    private lateinit var dbHelper: DatabaseHelper
    private lateinit var uriMatcher: UriMatcher

    companion object {
        const val AUTHORITY = "com.example.contacts"
        val CONTENT_URI: Uri = Uri.parse("content://$AUTHORITY/contacts")

        const val CONTACTS = 1
        const val CONTACT_ID = 2
    }

    override fun onCreate(): Boolean {
        // 1. 初始化数据库
        dbHelper = DatabaseHelper(context!!)

        // 2. 初始化UriMatcher
        uriMatcher = UriMatcher(UriMatcher.NO_MATCH).apply {
            addURI(AUTHORITY, "contacts", CONTACTS)
            addURI(AUTHORITY, "contacts/#", CONTACT_ID)
        }

        return true
    }

    override fun query(
        uri: Uri,
        projection: Array<String>?,
        selection: String?,
        selectionArgs: Array<String>?,
        sortOrder: String?
    ): Cursor? {
        val db = dbHelper.readableDatabase

        val cursor = when (uriMatcher.match(uri)) {
            CONTACTS -> {
                // 查询所有联系人
                db.query(
                    "contacts",
                    projection,
                    selection,
                    selectionArgs,
                    null, null,
                    sortOrder
                )
            }
            CONTACT_ID -> {
                // 查询单个联系人
                val id = uri.lastPathSegment
                db.query(
                    "contacts",
                    projection,
                    "_id = ?",
                    arrayOf(id),
                    null, null,
                    sortOrder
                )
            }
            else -> null
        }

        // 设置通知Uri
        cursor?.setNotificationUri(context?.contentResolver, uri)

        return cursor
    }

    override fun insert(uri: Uri, values: ContentValues?): Uri? {
        val db = dbHelper.writableDatabase

        val id = when (uriMatcher.match(uri)) {
            CONTACTS -> {
                db.insert("contacts", null, values)
            }
            else -> throw IllegalArgumentException("Unknown URI: $uri")
        }

        if (id > 0) {
            val newUri = ContentUris.withAppendedId(CONTENT_URI, id)
            // 通知数据变化
            context?.contentResolver?.notifyChange(newUri, null)
            return newUri
        }

        return null
    }

    override fun update(
        uri: Uri,
        values: ContentValues?,
        selection: String?,
        selectionArgs: Array<String>?
    ): Int {
        val db = dbHelper.writableDatabase

        val count = when (uriMatcher.match(uri)) {
            CONTACTS -> {
                db.update("contacts", values, selection, selectionArgs)
            }
            CONTACT_ID -> {
                val id = uri.lastPathSegment
                db.update(
                    "contacts",
                    values,
                    "_id = ?",
                    arrayOf(id)
                )
            }
            else -> 0
        }

        if (count > 0) {
            context?.contentResolver?.notifyChange(uri, null)
        }

        return count
    }

    override fun delete(
        uri: Uri,
        selection: String?,
        selectionArgs: Array<String>?
    ): Int {
        val db = dbHelper.writableDatabase

        val count = when (uriMatcher.match(uri)) {
            CONTACTS -> {
                db.delete("contacts", selection, selectionArgs)
            }
            CONTACT_ID -> {
                val id = uri.lastPathSegment
                db.delete("contacts", "_id = ?", arrayOf(id))
            }
            else -> 0
        }

        if (count > 0) {
            context?.contentResolver?.notifyChange(uri, null)
        }

        return count
    }

    override fun getType(uri: Uri): String? {
        return when (uriMatcher.match(uri)) {
            CONTACTS -> "vnd.android.cursor.dir/vnd.example.contacts"
            CONTACT_ID -> "vnd.android.cursor.item/vnd.example.contacts"
            else -> null
        }
    }
}

2.3 AndroidManifest声明

xml 复制代码
<provider
    android:name=".ContactsProvider"
    android:authorities="com.example.contacts"
    android:exported="true"
    android:readPermission="com.example.READ_CONTACTS"
    android:writePermission="com.example.WRITE_CONTACTS"
    android:grantUriPermissions="true">

    <!-- 路径权限:特定路径需要特殊权限 -->
    <path-permission
        android:path="/contacts/private"
        android:readPermission="com.example.READ_PRIVATE_CONTACTS"/>

    <!-- 元数据 -->
    <meta-data
        android:name="android.content.ContactDirectory"
        android:value="true"/>
</provider>

关键属性

  • authorities:唯一标识符,推荐使用包名
  • exported:是否对其他应用可见
  • readPermission/writePermission:静态权限声明
  • grantUriPermissions:是否支持临时Uri权限
  • multiprocess:是否在多个进程创建实例(通常false)

三、Uri权限授予机制

3.1 Uri权限设计理念

问题:传统权限模型的局限

  • Manifest权限:要么全部授予,要么全部拒绝,粒度太粗
  • 运行时权限:需要用户确认,体验不佳
  • 文件路径:SELinux限制跨应用文件访问

解决:Uri临时权限

  • 精细控制:针对单个Uri授予权限,不是整个Provider
  • 临时性:Activity/Service结束后自动撤销
  • 无需用户确认:应用间自动授权
  • 安全:不暴露文件系统路径

3.2 UriGrantsManager核心实现

java 复制代码
// frameworks/base/services/core/java/com/android/server/uri/UriGrantsManagerService.java
public class UriGrantsManagerService extends IUriGrantsManager.Stub {
    // 授权记录:uid → GrantUri集合
    private final SparseArray<ArrayMap<GrantUri, UriPermission>> mGrantedUriPermissions =
        new SparseArray<>();

    // 授予Uri权限
    void grantUriPermission(
            int callingUid,
            String targetPkg,
            Uri uri,
            int modeFlags,
            int targetUid) {
        // 1. 验证源应用有权限授予
        enforceNotIsolatedCaller("grantUriPermission");

        // 2. 创建GrantUri对象
        GrantUri grantUri = new GrantUri(uri.getAuthority(), uri, modeFlags);

        // 3. 记录授权
        UriPermission perm = findOrCreateUriPermissionLocked(
            callingUid, targetPkg, targetUid, grantUri);
        perm.grantModes(modeFlags, null);

        // 4. 写入授权表
        mGrantedUriPermissions.get(targetUid).put(grantUri, perm);
    }

    // 检查Uri权限
    int checkUriPermission(
            Uri uri,
            int uid,
            int modeFlags) {
        // 1. 查找授权记录
        UriPermission perm = findUriPermissionLocked(uid, new GrantUri(uri));

        if (perm == null) {
            return PackageManager.PERMISSION_DENIED;
        }

        // 2. 检查模式匹配
        if ((perm.modeFlags & modeFlags) == modeFlags) {
            return PackageManager.PERMISSION_GRANTED;
        }

        return PackageManager.PERMISSION_DENIED;
    }

    // 撤销Uri权限
    void revokeUriPermission(
            String targetPkg,
            int targetUid,
            Uri uri,
            int modeFlags) {
        synchronized (mLock) {
            ArrayMap<GrantUri, UriPermission> perms =
                mGrantedUriPermissions.get(targetUid);

            if (perms != null) {
                UriPermission perm = perms.get(new GrantUri(uri));
                if (perm != null) {
                    perm.revokeModes(modeFlags);

                    // 如果权限完全撤销,移除记录
                    if (perm.modeFlags == 0) {
                        perms.remove(new GrantUri(uri));
                    }
                }
            }
        }
    }
}

3.3 Uri权限授予场景

场景1:通过Intent传递Uri

kotlin 复制代码
// 发送方:授予读权限
fun shareImage(imageUri: Uri) {
    val intent = Intent(Intent.ACTION_SEND).apply {
        type = "image/*"
        putExtra(Intent.EXTRA_STREAM, imageUri)
        // 授予临时读权限
        addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
    }

    startActivity(Intent.createChooser(intent, "分享图片"))
}

// 接收方:自动获得imageUri的读权限
class ShareActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val imageUri = intent.getParcelableExtra<Uri>(Intent.EXTRA_STREAM)
        if (imageUri != null) {
            // 可以直接访问,无需额外权限
            val inputStream = contentResolver.openInputStream(imageUri)
            val bitmap = BitmapFactory.decodeStream(inputStream)
            imageView.setImageBitmap(bitmap)
        }
    }
}

权限授予流程

java 复制代码
// frameworks/base/services/core/java/com/android/server/am/ActivityStarter.java
int startActivityLocked(..., Intent intent, ...) {
    // 检查Intent中的FLAG_GRANT_*标志
    if ((intent.getFlags() & Intent.FLAG_GRANT_READ_URI_PERMISSION) != 0) {
        Uri data = intent.getData();
        if (data != null) {
            // 授予目标Activity读权限
            mUriGrantsManager.grantUriPermission(
                callingUid,
                targetPkg,
                data,
                Intent.FLAG_GRANT_READ_URI_PERMISSION,
                targetUid
            );
        }
    }

    // 处理ClipData中的多个Uri
    ClipData clipData = intent.getClipData();
    if (clipData != null) {
        for (int i = 0; i < clipData.getItemCount(); i++) {
            Uri uri = clipData.getItemAt(i).getUri();
            if (uri != null) {
                mUriGrantsManager.grantUriPermission(..., uri, ...);
            }
        }
    }
}

场景2:PendingIntent延迟授权

kotlin 复制代码
// 创建PendingIntent并授予权限
fun createNotificationWithImage(imageUri: Uri) {
    val intent = Intent(this, ViewImageActivity::class.java).apply {
        data = imageUri
        addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
    }

    val pendingIntent = PendingIntent.getActivity(
        this, 0, intent,
        PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
    )

    val notification = NotificationCompat.Builder(this, CHANNEL_ID)
        .setContentTitle("新图片")
        .setContentText("点击查看")
        .setSmallIcon(R.drawable.ic_image)
        .setContentIntent(pendingIntent)  // PendingIntent携带权限
        .build()

    notificationManager.notify(NOTIFICATION_ID, notification)
}

特点

  • 权限绑定到PendingIntent,不是当前进程
  • 点击通知时,目标Activity自动获得权限
  • 适用于跨进程延迟操作

场景3:持久化Uri权限

kotlin 复制代码
// 请求持久化权限(存活超过Activity生命周期)
fun takePersistablePermission(uri: Uri) {
    try {
        contentResolver.takePersistableUriPermission(
            uri,
            Intent.FLAG_GRANT_READ_URI_PERMISSION or
            Intent.FLAG_GRANT_WRITE_URI_PERMISSION
        )
        // 权限已持久化,可以保存Uri到数据库
        saveUriToDatabase(uri)
    } catch (e: SecurityException) {
        // 源应用未设置FLAG_GRANT_PERSISTABLE_URI_PERMISSION
        Log.e(TAG, "无法持久化权限", e)
    }
}

// 释放持久化权限
fun releasePersistablePermission(uri: Uri) {
    contentResolver.releasePersistableUriPermission(
        uri,
        Intent.FLAG_GRANT_READ_URI_PERMISSION or
        Intent.FLAG_GRANT_WRITE_URI_PERMISSION
    )
}

// 查询已持久化的权限
fun listPersistedPermissions() {
    val permissions = contentResolver.persistedUriPermissions
    for (permission in permissions) {
        Log.d(TAG, "Uri: ${permission.uri}, " +
                   "Read: ${permission.isReadPermission}, " +
                   "Write: ${permission.isWritePermission}")
    }
}

实现机制

java 复制代码
// frameworks/base/services/core/java/com/android/server/uri/UriGrantsManagerService.java
void takePersistableUriPermission(Uri uri, int modeFlags, int uid) {
    synchronized (mLock) {
        UriPermission perm = findUriPermissionLocked(uid, new GrantUri(uri));

        if (perm == null) {
            throw new SecurityException("No permission grant found for URI");
        }

        // 标记为持久化
        perm.persistedModeFlags = modeFlags;

        // 写入磁盘(XML文件)
        writeGrantedUriPermissions();
    }
}

3.4 前缀匹配权限

kotlin 复制代码
// 授予目录级别权限
fun grantDirectoryAccess(directoryUri: Uri) {
    val intent = Intent(Intent.ACTION_SEND).apply {
        data = directoryUri
        addFlags(
            Intent.FLAG_GRANT_READ_URI_PERMISSION or
            Intent.FLAG_GRANT_PREFIX_URI_PERMISSION  // 前缀匹配
        )
    }
    startActivity(intent)
}

前缀匹配规则

  • content://com.example/files 授权
  • content://com.example/files/image.jpg ✓ 匹配
  • content://com.example/files/docs/report.pdf ✓ 匹配
  • content://com.example/other/data.txt ✗ 不匹配

四、ContentObserver数据观察

4.1 ContentObserver机制

kotlin 复制代码
class ContactsObserver(handler: Handler) : ContentObserver(handler) {
    override fun onChange(selfChange: Boolean, uri: Uri?) {
        // 数据变化回调
        Log.d(TAG, "Data changed at: $uri")

        // 重新查询数据
        refreshData(uri)
    }
}

// 注册观察者
fun observeContacts() {
    val observer = ContactsObserver(Handler(Looper.getMainLooper()))

    contentResolver.registerContentObserver(
        ContactsProvider.CONTENT_URI,
        true,  // notifyForDescendants: 监听子Uri
        observer
    )
}

// 取消注册
fun unregisterObserver(observer: ContentObserver) {
    contentResolver.unregisterContentObserver(observer)
}

4.2 通知机制实现

java 复制代码
// frameworks/base/core/java/android/content/ContentResolver.java
public void notifyChange(Uri uri, ContentObserver observer) {
    notifyChange(uri, observer, true /* syncToNetwork */);
}

public void notifyChange(
        Uri uri,
        ContentObserver observer,
        boolean syncToNetwork) {
    try {
        // 调用ContentService的notifyChange
        getContentService().notifyChange(
            uri,
            observer == null ? null : observer.getContentObserver(),
            observer != null && observer.deliverSelfNotifications(),
            syncToNetwork,
            UserHandle.getCallingUserId()
        );
    } catch (RemoteException e) {
    }
}
java 复制代码
// frameworks/base/services/core/java/com/android/server/content/ContentService.java
public class ContentService extends IContentService.Stub {
    // 观察者注册表:Uri → Observer列表
    private final ObserverNode mRootNode = new ObserverNode("");

    @Override
    public void registerContentObserver(
            Uri uri,
            boolean notifyForDescendants,
            IContentObserver observer,
            int userHandle) {
        synchronized (mRootNode) {
            mRootNode.addObserverLocked(
                uri,
                observer,
                notifyForDescendants,
                mRootNode,
                userHandle
            );
        }
    }

    @Override
    public void notifyChange(
            Uri uri,
            IContentObserver observer,
            boolean observerWantsSelfNotifications,
            boolean syncToNetwork,
            int userHandle) {
        synchronized (mRootNode) {
            // 收集匹配的观察者
            ArrayList<ObserverCall> calls = new ArrayList<>();
            mRootNode.collectObserversLocked(
                uri,
                0,
                observer,
                observerWantsSelfNotifications,
                calls
            );

            // 通知所有观察者
            for (ObserverCall call : calls) {
                try {
                    call.mObserver.onChange(
                        call.mSelfChange,
                        uri,
                        userHandle
                    );
                } catch (RemoteException e) {
                    // 观察者进程已死亡
                }
            }
        }

        // 触发网络同步
        if (syncToNetwork) {
            SyncManager syncManager = getSyncManager();
            if (syncManager != null) {
                syncManager.scheduleLocalSync(null, userHandle, uri.getAuthority());
            }
        }
    }
}

ObserverNode树形结构

ini 复制代码
content://
  └─ com.example.contacts/
      ├─ contacts/ (Observer A, notifyForDescendants=true)
      │   ├─ 1 (Observer B)
      │   └─ 2
      └─ groups/

匹配规则

  • 通知 content://com.example.contacts/contacts/1
  • Observer A 收到通知(因为notifyForDescendants=true)
  • Observer B 收到通知(精确匹配)

4.3 批量通知优化

kotlin 复制代码
class ContactsProvider : ContentProvider() {
    private val batchOperations = ThreadLocal<Boolean>()

    override fun bulkInsert(uri: Uri, values: Array<ContentValues>): Int {
        val db = dbHelper.writableDatabase
        var count = 0

        // 开启批量模式
        batchOperations.set(true)

        db.beginTransaction()
        try {
            for (value in values) {
                val id = db.insert("contacts", null, value)
                if (id > 0) count++
            }
            db.setTransactionSuccessful()
        } finally {
            db.endTransaction()
            batchOperations.set(false)
        }

        // 批量操作完成后统一通知一次
        context?.contentResolver?.notifyChange(uri, null)

        return count
    }

    override fun insert(uri: Uri, values: ContentValues?): Uri? {
        // ... 插入逻辑

        // 仅在非批量模式通知
        if (batchOperations.get() != true) {
            context?.contentResolver?.notifyChange(newUri, null)
        }

        return newUri
    }
}

五、FileProvider文件共享

5.1 FileProvider设计理念

问题:直接共享file:// Uri的风险

  • file:///storage/emulated/0/image.jpg 暴露文件系统路径
  • 目标应用需要READ_EXTERNAL_STORAGE权限
  • Android 7.0+ 抛出FileUriExposedException

解决:FileProvider将文件转换为content:// Uri

  • content://com.example.app.fileprovider/images/image.jpg
  • 通过Uri权限临时授权,无需存储权限
  • 符合Scoped Storage要求

5.2 FileProvider配置

xml 复制代码
<!-- AndroidManifest.xml -->
<provider
    android:name="androidx.core.content.FileProvider"
    android:authorities="com.example.app.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 -->
<paths>
    <!-- 内部存储 Context.getFilesDir() -->
    <files-path
        name="internal"
        path="images/"/>

    <!-- 缓存目录 Context.getCacheDir() -->
    <cache-path
        name="cache"
        path="."/>

    <!-- 外部存储 Context.getExternalFilesDir() -->
    <external-files-path
        name="external"
        path="Download/"/>

    <!-- 外部缓存 Context.getExternalCacheDir() -->
    <external-cache-path
        name="external_cache"
        path="."/>

    <!-- 外部存储根目录 Environment.getExternalStorageDirectory() -->
    <external-path
        name="external_root"
        path="."/>
</paths>

5.3 FileProvider使用示例

分享文件

kotlin 复制代码
fun shareFile(file: File) {
    // 将File转换为content Uri
    val uri = FileProvider.getUriForFile(
        this,
        "${applicationContext.packageName}.fileprovider",
        file
    )

    val intent = Intent(Intent.ACTION_SEND).apply {
        type = "application/pdf"
        putExtra(Intent.EXTRA_STREAM, uri)
        addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
    }

    startActivity(Intent.createChooser(intent, "分享文件"))
}

拍照保存

kotlin 复制代码
fun takePicture() {
    // 创建图片文件
    val photoFile = File(
        getExternalFilesDir(Environment.DIRECTORY_PICTURES),
        "photo_${System.currentTimeMillis()}.jpg"
    )

    // 转换为content Uri
    val photoUri = FileProvider.getUriForFile(
        this,
        "${applicationContext.packageName}.fileprovider",
        photoFile
    )

    val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE).apply {
        putExtra(MediaStore.EXTRA_OUTPUT, photoUri)
        addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
    }

    startActivityForResult(intent, REQUEST_IMAGE_CAPTURE)
}

override fun onActivityResult(
    requestCode: Int,
    resultCode: Int,
    data: Intent?
) {
    super.onActivityResult(requestCode, resultCode, data)

    if (requestCode == REQUEST_IMAGE_CAPTURE && resultCode == RESULT_OK) {
        // 照片已保存到photoFile
        val bitmap = BitmapFactory.decodeFile(photoFile.absolutePath)
        imageView.setImageBitmap(bitmap)
    }
}

安装APK

kotlin 复制代码
fun installApk(apkFile: File) {
    val apkUri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
        // Android 7.0+ 使用FileProvider
        FileProvider.getUriForFile(
            this,
            "${applicationContext.packageName}.fileprovider",
            apkFile
        )
    } else {
        // Android 7.0以下使用file Uri
        Uri.fromFile(apkFile)
    }

    val intent = Intent(Intent.ACTION_VIEW).apply {
        setDataAndType(apkUri, "application/vnd.android.package-archive")
        addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
        addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
    }

    startActivity(intent)
}

5.4 FileProvider实现原理

java 复制代码
// androidx/core/content/FileProvider.java
public class FileProvider extends ContentProvider {
    private PathStrategy mStrategy;

    @Override
    public boolean onCreate() {
        return true;
    }

    @Override
    public Cursor query(Uri uri, String[] projection, ...) {
        // 将content Uri转换回File
        File file = mStrategy.getFileForUri(uri);

        if (projection == null) {
            projection = COLUMNS;
        }

        String[] cols = new String[projection.length];
        Object[] values = new Object[projection.length];
        int i = 0;

        for (String col : projection) {
            if (OpenableColumns.DISPLAY_NAME.equals(col)) {
                cols[i] = OpenableColumns.DISPLAY_NAME;
                values[i++] = file.getName();
            } else if (OpenableColumns.SIZE.equals(col)) {
                cols[i] = OpenableColumns.SIZE;
                values[i++] = file.length();
            }
        }

        return new MatrixCursor(cols, 1).addRow(values);
    }

    @Override
    public ParcelFileDescriptor openFile(Uri uri, String mode)
            throws FileNotFoundException {
        // 打开文件返回文件描述符
        File file = mStrategy.getFileForUri(uri);
        int fileMode = modeToMode(mode);
        return ParcelFileDescriptor.open(file, fileMode);
    }

    // 将File转换为content Uri
    public static Uri getUriForFile(
            Context context,
            String authority,
            File file) {
        PathStrategy strategy = getPathStrategy(context, authority);
        return strategy.getUriForFile(file);
    }
}

Uri转换过程

javascript 复制代码
File: /data/user/0/com.example/files/images/photo.jpg
       ↓
PathStrategy解析: <files-path name="internal" path="images/"/>
       ↓
content Uri: content://com.example.fileprovider/internal/photo.jpg

六、调试与问题诊断

6.1 dumpsys查看Provider状态

bash 复制代码
# 查看所有ContentProvider
adb shell dumpsys activity providers

# 输出示例
ACTIVITY MANAGER CONTENT PROVIDERS (dumpsys activity providers)
  ContentProviderRecord{a1b2c3d com.example.contacts/.ContactsProvider}
    package=com.example.contacts process=com.example.contacts
    proc=ProcessRecord{e4f5g6h 12345:com.example.contacts/u0a123}
    authority=com.example.contacts
    singleton=true
    External clients:
      - ProcessRecord{h8i9j0k 12346:com.example.app/u0a124}

# 查看Uri权限
adb shell dumpsys activity permissions

# 输出示例
URI GRANTS:
  Granted from uid 10123 to uid 10124:
    Uri: content://com.example.contacts/contacts/1
    Mode flags: READ

6.2 ContentResolver日志

kotlin 复制代码
// 开启ContentResolver日志
adb shell setprop log.tag.ContentResolver VERBOSE

// 查看日志
adb logcat -s ContentResolver

6.3 常见问题诊断

问题1:SecurityException: Permission Denial

症状

ini 复制代码
java.lang.SecurityException: Permission Denial: opening provider
com.example.ContactsProvider from ProcessRecord{...} (pid=12345, uid=10124)
requires com.example.READ_CONTACTS or com.example.WRITE_CONTACTS

排查

bash 复制代码
# 1. 检查Provider权限配置
adb shell dumpsys package com.example | grep -A 20 "Provider"

# 2. 检查客户端权限声明
adb shell dumpsys package com.example.client | grep "requested permissions"

# 3. 检查运行时权限授予状态
adb shell dumpsys package com.example.client | grep "granted=true"

解决方案

  • 在AndroidManifest声明权限
  • 请求运行时权限
  • 使用Uri临时权限代替Manifest权限

问题2:IllegalArgumentException: Unknown URI

症状

arduino 复制代码
java.lang.IllegalArgumentException: Unknown URI: content://com.example/contacts/999

原因:UriMatcher未匹配到对应路径

解决

kotlin 复制代码
// 检查UriMatcher配置
override fun query(...): Cursor? {
    val matchCode = uriMatcher.match(uri)
    Log.d(TAG, "Uri: $uri, Match code: $matchCode")  // 调试输出

    return when (matchCode) {
        CONTACTS -> { /* ... */ }
        CONTACT_ID -> { /* ... */ }
        else -> {
            Log.e(TAG, "Unknown URI: $uri")
            throw IllegalArgumentException("Unknown URI: $uri")
        }
    }
}

问题3:Cursor窗口泄漏

症状

arduino 复制代码
StrictMode policy violation: android.os.strictmode.LeakedClosableViolation:
A resource was acquired at attached stack trace but never released.

解决

kotlin 复制代码
// ✗ 错误:未关闭Cursor
fun queryContacts(): List<Contact> {
    val cursor = contentResolver.query(uri, ...)
    val contacts = mutableListOf<Contact>()

    cursor?.let {
        while (it.moveToNext()) {
            contacts.add(parseContact(it))
        }
    }
    // 忘记关闭cursor!

    return contacts
}

// ✓ 正确:使用use自动关闭
fun queryContacts(): List<Contact> {
    return contentResolver.query(uri, ...)?.use { cursor ->
        generateSequence { if (cursor.moveToNext()) cursor else null }
            .map { parseContact(it) }
            .toList()
    } ?: emptyList()
}

七、最佳实践

7.1 Provider设计建议

使用异步查询

kotlin 复制代码
// ✗ 错误:主线程查询
fun loadContacts() {
    val cursor = contentResolver.query(uri, ...)  // 阻塞主线程!
    // 处理cursor...
}

// ✓ 正确:使用协程异步查询
suspend fun loadContacts(): List<Contact> = withContext(Dispatchers.IO) {
    contentResolver.query(uri, ...)?.use { cursor ->
        // 解析cursor
    } ?: emptyList()
}

// ✓ 正确:使用CursorLoader(已废弃,推荐协程)
class ContactsFragment : Fragment(), LoaderManager.LoaderCallbacks<Cursor> {
    override fun onCreateLoader(id: Int, args: Bundle?): Loader<Cursor> {
        return CursorLoader(
            requireContext(),
            ContactsProvider.CONTENT_URI,
            projection,
            null, null, null
        )
    }
}

批量操作优化

kotlin 复制代码
// ✗ 低效:逐条插入
fun insertContacts(contacts: List<Contact>) {
    for (contact in contacts) {
        contentResolver.insert(uri, contact.toContentValues())
    }
}

// ✓ 高效:使用bulkInsert
fun insertContactsBatch(contacts: List<Contact>) {
    val values = contacts.map { it.toContentValues() }.toTypedArray()
    contentResolver.bulkInsert(uri, values)
}

// ✓ 更高效:使用applyBatch事务
fun insertContactsTransaction(contacts: List<Contact>) {
    val operations = ArrayList<ContentProviderOperation>()

    for (contact in contacts) {
        operations.add(
            ContentProviderOperation.newInsert(uri)
                .withValues(contact.toContentValues())
                .build()
        )
    }

    try {
        contentResolver.applyBatch(AUTHORITY, operations)
    } catch (e: Exception) {
        Log.e(TAG, "Batch insert failed", e)
    }
}

7.2 Uri权限安全实践

kotlin 复制代码
// ✓ 仅授予必要权限
fun shareReadOnly(uri: Uri) {
    val intent = Intent(Intent.ACTION_SEND).apply {
        setDataAndType(uri, "image/*")
        // 仅授予读权限,不是写权限
        addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
    }
    startActivity(intent)
}

// ✓ 及时撤销权限
override fun onDestroy() {
    super.onDestroy()
    // Activity销毁时撤销权限
    sharedUris.forEach { uri ->
        revokeUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
    }
}

// ✓ 验证Uri来源
fun handleSharedUri(uri: Uri) {
    // 检查Uri是否来自可信来源
    val authority = uri.authority
    if (authority != TRUSTED_AUTHORITY) {
        Log.w(TAG, "Uri from untrusted source: $authority")
        return
    }

    // 验证权限
    val hasPermission = checkUriPermission(
        uri,
        Binder.getCallingPid(),
        Binder.getCallingUid(),
        Intent.FLAG_GRANT_READ_URI_PERMISSION
    ) == PackageManager.PERMISSION_GRANTED

    if (!hasPermission) {
        throw SecurityException("No permission for Uri: $uri")
    }

    // 安全处理Uri
    processUri(uri)
}

7.3 ContentObserver使用建议

kotlin 复制代码
// ✓ 在生命周期方法中注册/取消
class ContactsActivity : AppCompatActivity() {
    private lateinit var observer: ContactsObserver

    override fun onStart() {
        super.onStart()
        observer = ContactsObserver(Handler(Looper.getMainLooper()))
        contentResolver.registerContentObserver(
            ContactsProvider.CONTENT_URI,
            true,
            observer
        )
    }

    override fun onStop() {
        super.onStop()
        contentResolver.unregisterContentObserver(observer)
    }
}

// ✓ 使用防抖避免频繁刷新
class ContactsObserver(handler: Handler) : ContentObserver(handler) {
    private val refreshRunnable = Runnable { refreshData() }

    override fun onChange(selfChange: Boolean, uri: Uri?) {
        // 移除之前的刷新任务
        handler?.removeCallbacks(refreshRunnable)

        // 延迟300ms刷新,避免短时间内多次通知
        handler?.postDelayed(refreshRunnable, 300)
    }

    private fun refreshData() {
        // 重新查询数据
    }
}

八、Android 15新特性

8.1 增强的Uri权限控制

java 复制代码
// frameworks/base/services/core/java/com/android/server/uri/UriGrantsManagerService.java
// Android 15新增:细粒度时间控制
void grantUriPermissionWithExpiry(
        int callingUid,
        String targetPkg,
        Uri uri,
        int modeFlags,
        int targetUid,
        long expiryTimeMillis) {  // 新增过期时间
    UriPermission perm = findOrCreateUriPermissionLocked(...);
    perm.grantModes(modeFlags, null);
    perm.expiryTime = expiryTimeMillis;  // 设置过期时间

    // 注册过期检查
    mHandler.postDelayed(() -> {
        revokeExpiredPermissions();
    }, expiryTimeMillis - System.currentTimeMillis());
}

8.2 Photo Picker集成

kotlin 复制代码
// Android 15推荐使用Photo Picker代替READ_EXTERNAL_STORAGE
fun selectPhoto() {
    val intent = Intent(MediaStore.ACTION_PICK_IMAGES).apply {
        type = "image/*"
        putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, 5)  // 最多选5张
    }

    startActivityForResult(intent, REQUEST_PHOTO_PICKER)
}

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)

    if (requestCode == REQUEST_PHOTO_PICKER && resultCode == RESULT_OK) {
        // 获取选中的照片Uri(自动拥有权限)
        val clipData = data?.clipData
        if (clipData != null) {
            for (i in 0 until clipData.itemCount) {
                val uri = clipData.getItemAt(i).uri
                // 直接使用,无需存储权限
                loadImage(uri)
            }
        } else {
            val uri = data?.data
            uri?.let { loadImage(it) }
        }
    }
}

8.3 优化的Provider启动

java 复制代码
// Android 15:Provider并行启动优化
private void installContentProviders(
        Context context,
        List<ProviderInfo> providers) {
    final ArrayList<ContentProviderHolder> results = new ArrayList<>();

    // 按依赖关系排序
    List<ProviderInfo> sortedProviders = sortProvidersByDependency(providers);

    // 并行安装互不依赖的Provider
    ExecutorService executor = Executors.newFixedThreadPool(
        Math.min(4, Runtime.getRuntime().availableProcessors()));

    List<Future<ContentProvider>> futures = new ArrayList<>();
    for (ProviderInfo cpi : sortedProviders) {
        futures.add(executor.submit(() -> installProvider(context, null, cpi, ...)));
    }

    // 等待所有Provider安装完成
    for (Future<ContentProvider> future : futures) {
        ContentProvider provider = future.get();
        results.add(createProviderHolder(provider));
    }

    executor.shutdown();

    // 发布到AMS
    ActivityManager.getService().publishContentProviders(mAppThread, results);
}

九、总结

核心要点回顾

  1. ContentProvider架构

    • 四层架构:Client App → Framework (AMS) → Provider Process → Data Storage
    • 核心组件:ContentResolver客户端代理、AMS Provider管理、UriGrantsManager权限管理
    • 生命周期:Provider.onCreate()在Application.onCreate()之前调用
  2. Uri权限机制

    • 临时授权:通过Intent FLAG_GRANT_*授予临时权限
    • 精细控制:针对单个Uri授权,不是整个Provider
    • 自动管理:Activity/Service结束后自动撤销
    • 持久化支持:takePersistableUriPermission持久化权限
  3. FileProvider文件共享

    • content:// Uri代替file:// Uri
    • 无需存储权限,通过Uri临时授权
    • 符合Scoped Storage安全要求
  4. ContentObserver通知

    • ObserverNode树形结构管理观察者
    • notifyForDescendants控制子Uri通知
    • 批量操作优化:统一通知减少回调
  5. 最佳实践

    • 异步查询避免阻塞主线程
    • 批量操作使用bulkInsert/applyBatch
    • 及时关闭Cursor避免泄漏
    • 仅授予必要的Uri权限

与其他系统的协作

  • ActivityManagerService:管理Provider生命周期和Uri权限
  • PackageManagerService:解析Manifest中的Provider声明
  • Binder:跨进程通信基础
  • SELinux:强制访问控制,保护Provider进程

参考源码(基于Android 15 AOSP):

  • frameworks/base/core/java/android/content/ContentProvider.java
  • frameworks/base/core/java/android/content/ContentResolver.java
  • frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java
  • frameworks/base/services/core/java/com/android/server/uri/UriGrantsManagerService.java
  • frameworks/base/services/core/java/com/android/server/content/ContentService.java

调试命令速查

bash 复制代码
# ContentProvider状态
adb shell dumpsys activity providers

# Uri权限
adb shell dumpsys activity permissions

# ContentResolver日志
adb shell setprop log.tag.ContentResolver VERBOSE
adb logcat -s ContentResolver

系列导航


本文基于Android 15 (API Level 35)源码分析,不同厂商的定制ROM可能存在差异。 欢迎来我中的个人主页找到更多有用的知识和有趣的产品

相关推荐
峥嵘life2 小时前
Android16 【GTS】 GtsDevicePolicyTestCases 测试存在Failed项
android·linux·学习
aqi002 小时前
【送书活动】《鸿蒙HarmonyOS 6:应用开发从零基础到App上线》迎新送书啦
android·华为·harmonyos·鸿蒙
良逍Ai出海4 小时前
OpenClaw 新手最该先搞懂的 2 套命令
android·java·数据库
hindon4 小时前
一文读懂 ViewModel
android
程序员JerrySUN4 小时前
别再把 HTTPS 和 OTA 看成两回事:一篇讲透 HTTPS 协议、安全通信机制与 Mender 升级加密链路的完整文章
android·java·开发语言·深度学习·流程图
音视频牛哥4 小时前
Android平台GB28181设备接入模块架构解析、功能详解与典型应用场景分析
android·android gb28181·gb28181安卓端·gb28181对接·gb28181设备·gb28181语音广播·安卓gb28181设备对接
叁两5 小时前
前端开发如何快速上手安卓APP开发?
android
guodashen0075 小时前
在安卓端启动一个服务器接口,用于接收post请求的json数据
android·服务器·json
hindon5 小时前
一文读懂Android 中的 MVC、MVP、MVVM
android