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可能存在差异。 欢迎来我中的个人主页找到更多有用的知识和有趣的产品

相关推荐
帅得不敢出门18 小时前
Android Studio同一个工程根据不同芯片平台加载不同的framework.jar及使用不同的代码
android·android studio·jar
xiangxiongfly91518 小时前
Android LeakCanary源码分析
android·leakcanary
黄林晴18 小时前
紧急预警!Android 17 定位权限大改,你的 App 要适配了
android
夏沫琅琊19 小时前
Android API 发送短信技术文档
android·kotlin
周周不一样19 小时前
Android基础笔记1
android·笔记·gitee
取码网19 小时前
影视APP源码 SK影视 安卓+苹果双端APP 反编译详细视频教程+源码
android
musk121219 小时前
android webview 黑屏问题 , 页面加载时间有点长的情况下
android
夏沫琅琊19 小时前
Android 彩信导出技术文档
android·kotlin
sp42a19 小时前
安卓原生 MQTT 通讯 Java 实现
android·java·mqtt