ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Unity调用安卓内部安装APK完整指南:从配置到踩坑

Unity调用安卓内部安装APK完整指南:从配置到踩坑 做Unity安卓开发的朋友迟早都会碰到一个需求在应用内部直接安装另外一个APK。比如游戏开服后检测到客户端有新版本下载完升级包要引导用户安装比如企业内部做应用分发平台Unity应用就是下载器和安装入口再比如很多工具类App会把语音包、地图数据包这类体积较大的资源单独打成APK运行时按需下载安装。标题里这六个字Unity调用安卓内部安装apk遇到过的同学都知道这不是简单一句startActivity就能解决的事特别是Android 7.0之后的FileProvider、Android 8.0之后的未知来源安装权限、以及各家ROM对安装权限的额外约束任何一个环节没处理好真机上就是“点击没反应”或者“解析包错误”。这篇东西不打算讲什么花活把我实际做过的Unity调用安卓内部安装APK的完整流程、工程配置、C#和Java代码、以及踩坑记录都梳理清楚。适合正在做Unity自动更新、企业分发、独立资源包安装的同学直接参考复刻。1. 先搞清楚需求边界你是要“调起安装器”还是“静默安装”1.1 这个需求常见的几种业务场景我在项目里遇过Unity调用安卓APK安装的场景大致分三类。第一类是应用内自动更新这是最常见的。游戏或者应用启动时请求版本接口发现有新版本就下载APK到本地下载完成后弹出系统安装界面让用户确认升级。这种方案比跳转到应用市场更可控因为你可以自己控制更新节奏和灰度比例下载服务器也完全是自己这边。第二类是企业内部的应用分发。很多公司有内部工具包、测试包、或者只对特定员工开放的App不方便上公开应用商店就会做一个“企业应用中心”外面套一层Unity壳里面列出所有APK供员工下载和安装。这种情况下Unity应用本质上承担了安装器的入口角色。第三类是模块化资源包。有些游戏会把语音包、高清贴图包、DLC内容单独打成APK游戏启动时检测本地没有对应版本就直接下载并安装。这种方案比把大资源塞进主包要灵活很多不需要频繁发整包。1.2 普通应用并没有“静默安装”的能力先说一个很多人一上来就搞错的点。经常会有人问“能不能在Unity里直接静默安装APK不经过用户确认”。答案是不行除非你的应用是用系统签名签的、或者设备已经Root、或者你的应用本身是系统应用放在/system/priv-app目录下。普通第三方应用在Android 6.0之后的所有版本上都不允许直接静默安装。所以这里的“内部安装”准确说是“在应用内部触发系统安装器”。Unity应用负责把APK文件准备好然后调起Android系统的PackageInstaller界面用户点击“安装”按钮后由系统完成后续动作。整个流程合法、不需要Root、也不会被应用商店下架。搞清楚了这一点后面所有代码就都围绕“构造一个正确的安装Intent”来展开。2. 方案选型AndroidJavaClass硬刚还是导AAR插件2.1 直接使用AndroidJavaClass的轻量方案Unity调用Java层API有个现成的桥接机制就是AndroidJavaClass和AndroidJavaObject。你可以在C#脚本里直接new一个Java类调用它的静态方法或者直接实例化Android的Intent对象全程不写Java代码纯C#搞定。这样做的好处是开发速度极快不用开Android Studio不用维护两套工程一个Unity工程里改改就能跑起来。适合功能简单、逻辑不复杂的情况比如“点击按钮安装一个已知路径的APK”。坏处也很明显。一旦你要处理运行时权限、FileProvider的Uri授权、安装完成后的广播回调、多种机型的状态判断所有逻辑堆在C#里会变得非常难维护。字符串拼Intent、反射调API哪一步出了问题排查起来都想砸电脑。2.2 用Android Studio打包AAR的完整方案更稳妥的做法是在Android Studio里写一个专门的工具类把所有安卓侧逻辑封装好导出AAR包放进Unity的Assets/Plugins/Android目录。Unity侧只需要调用几个简单的C#方法传参进去接收返回值或者回调事件就行。AAR方案的工程结构大概是这样的Assets/Plugins/Android/ ├── AndroidManifest.xml ├── libs/ │ └── unity-classes.jar └── MyInstallPlugin.aarAndroid Studio侧的Module就是一个普通的Library Module里面写一个静态工具类。这个方案代码结构清晰、可以处理回调、权限、多版本兼容等复杂逻辑后期维护也方便。代价就是需要同时维护Android和Unity两端工程构建链路长一些。2.3 我的建议如果只是验证功能、做原型Demo直接用AndroidJavaClass写C#半集成方案半小时就能出效果。如果是正式上线、要长期维护老老实实走AAR方案。我下面会把两种方案的代码都给出你可以根据项目阶段选。顺便提一句Unity 2019.3之后的版本支持“Custom Main Manifest”可以在Player Settings里直接生成并编辑AndroidManifest.xml不用每次导出工程再手动改。这个对纯C#方案来说是大大的福音后面会讲。3. 工程配置权限、FileProvider、Target API一个都不能漏3.1 Unity导出设置与Target API Level先说Unity侧的基础配置。在Build Settings - Player Settings - Android里重点关注这几个Package Name必须和最终签名保持一致后面FileProvider的authority会用到。Target API Level建议设置在Android 13API 33或你实际开发时的最新稳定版本。原因很简单应用市场要求新应用targetSdk至少到一定版本而且高targetSdk会强制你走FileProvider等新机制反而避免了很多老代码兼容问题。Internet Access如果需要从服务器下载APKInternet Access要选Require。Write Permission如果要把APK写到外部存储选External (SDCard)但如果用getExternalFilesDir路径其实可以不申请外部存储权限这点后面单独讲。3.2 权限声明REQUEST_INSTALL_PACKAGES无论哪种方案Android清单文件里都必须声明这个权限uses-permission android:nameandroid.permission.REQUEST_INSTALL_PACKAGES /这个权限是Android 8.0API 26引入的用于让应用“请求安装应用包”的能力。注意它和普通危险权限不一样它是一个特殊权限不能像READ_EXTERNAL_STORAGE那样直接requestPermissions弹窗申请你需要跳转到系统设置页让用户手动打开“允许安装未知应用”开关。具体跳转代码我用Java写一遍public static void openInstallSetting(Context context) { if (Build.VERSION.SDK_INT Build.VERSION_CODES.O) { Intent intent new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES); intent.setData(Uri.parse(package: context.getPackageName())); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent); } }3.3 FileProvider配置与file_paths.xmlAndroid 7.0API 24开始系统禁止应用向其他应用暴露file://类型的Uri否则会直接抛FileUriExposedException。想在应用内调起安装器安装APK就必须通过FileProvider把APK文件的content://Uri共享给系统安装器。在AndroidManifest.xml的application节点内注册FileProviderprovider android:nameandroidx.core.content.FileProvider android:authorities${applicationId}.fileprovider android:exportedfalse android:grantUriPermissionstrue meta-data android:nameandroid.support.FILE_PROVIDER_PATHS android:resourcexml/file_paths / /provider注意${applicationId}会自动替换成Build.gradle里配置的applicationId如果你是在Unity里用Custom Main Manifest方式配置要手写成实际包名比如com.mycompany.myapp.fileprovider。res/xml/file_paths.xml里定义允许暴露的目录?xml version1.0 encodingutf-8? paths external-path nameexternal_files path. / external-files-path nameexternal_app_files path. / cache-path namecache_path path. / files-path namefiles_path path. / /paths这段配置的意思是整个外部存储根目录、应用专属外部文件目录、缓存目录、内部文件目录都可以被FileProvider共享出去。线上项目里最好收敛一点只开放你实际要存放APK的那几个子目录减少暴露范围。3.4 Unity 2019.3的Custom Main Manifest如果你用的是Unity纯C#方案不打算导出工程改清单可以在Player Settings - Publishing Settings - Build里勾选Custom Main ManifestUnity会在Assets/Plugins/Android/下生成一个AndroidManifest.xml。你直接改这个文件把权限、provider节点加进去Unity打包时会自动合并。同理Custom Main Gradle Template和Custom Gradle Properties Template也可以生成后按需调整。这个功能出现之前纯C#方案想加权限和provider只能导出工程再手动改非常麻烦。现在方便了很多。4. 核心代码实现C#侧、Java侧、完整调用链4.1 Java侧一个稳一点的安装Helper如果你的项目是AAR方案Android Studio里这个Java类可以直接抄。我写了一个兼顾老版本和新版本的版本package com.example.installhelper; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Build; import androidx.core.content.FileProvider; import java.io.File; public class ApkInstallHelper { public static boolean canRequestInstall(Context context) { if (Build.VERSION.SDK_INT Build.VERSION_CODES.O) { return context.getPackageManager().canRequestPackageInstalls(); } return true; } public static void installApk(Context context, String apkPath) { if (context null || apkPath null) { return; } File apkFile new File(apkPath); if (!apkFile.exists()) { return; } Intent intent new Intent(Intent.ACTION_VIEW); if (Build.VERSION.SDK_INT Build.VERSION_CODES.N) { String authority context.getPackageName() .fileprovider; Uri apkUri FileProvider.getUriForFile(context, authority, apkFile); intent.setDataAndType(apkUri, application/vnd.android.package-archive); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); } else { intent.setDataAndType(Uri.fromFile(apkFile), application/vnd.android.package-archive); } intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent); } }几个关键点重点说一下。application/vnd.android.package-archive是APK的MIME类型必须带上否则安装器不知道拿什么来打开。FLAG_GRANT_READ_URI_PERMISSION是给系统安装器临时授予读取该文件的权限少了这个权限安装器就算拿到了content://Uri也没法读文件。FLAG_ACTIVITY_NEW_TASK是因为Unity的Activity和工具类之间没有Activity栈关联不加这个flag从非Activity上下文启动时会报错。4.2 C#侧AndroidJavaClass直接调用方案如果不打AARUnity侧用AndroidJavaClass调Java原生API也可以完成安装代码写起来会稍微绕一点。要给getMainExecutor()传Runnable来保证在主线程跑Intent的构造、FileProvider的调用都要通过AndroidJavaObject完成。这里我给出一个完整可跑的C#版本直接用AndroidJavaObject构造Intent不依赖Java侧自定义类using System; using System.IO; using UnityEngine; public static class ApkInstaller { private static AndroidJavaObject _mainActivity; private static AndroidJavaObject MainActivity { get { if (_mainActivity null) { using (var unityPlayer new AndroidJavaClass(com.unity3d.player.UnityPlayer)) { _mainActivity unityPlayer.GetStaticAndroidJavaObject(currentActivity); } } return _mainActivity; } } public static void InstallApk(string apkPath) { if (string.IsNullOrEmpty(apkPath) || !File.Exists(apkPath)) { Debug.LogError([ApkInstaller] APK文件不存在: apkPath); return; } using (var intent new AndroidJavaObject(android.content.Intent, android.intent.action.VIEW)) { using (var file new AndroidJavaObject(java.io.File, apkPath)) { var buildVersion new AndroidJavaClass(android.os.Build$VERSION); int sdkInt buildVersion.GetStaticint(SDK_INT); if (sdkInt 24) // Android 7.0 { string authority GetPackageName() .fileprovider; using (var fileProvider new AndroidJavaClass(androidx.core.content.FileProvider)) { using (var uri fileProvider.CallStaticAndroidJavaObject( getUriForFile, MainActivity, authority, file)) { intent.CallAndroidJavaObject(setDataAndType, uri, application/vnd.android.package-archive); } } intent.CallAndroidJavaObject(addFlags, 0x00000001); // FLAG_GRANT_READ_URI_PERMISSION } else { using (var uri new AndroidJavaObject(android.net.Uri)) { using (var fromFile uri.CallStaticAndroidJavaObject(fromFile, file)) { intent.CallAndroidJavaObject(setDataAndType, fromFile, application/vnd.android.package-archive); } } } intent.CallAndroidJavaObject(addFlags, 0x10000000); // FLAG_ACTIVITY_NEW_TASK MainActivity.Call(startActivity, intent); } } } private static string GetPackageName() { using (var context MainActivity.CallAndroidJavaObject(getApplicationContext)) { return context.Callstring(getPackageName); } } }这段代码不需要写任何Java但要确保项目里引入了AndroidX的Core库FileProvider类才能被找到。Unity 2021以上版本内置了AndroidX支持老版本需要自己在Gradle里加依赖。4.3 判断并引导未知来源安装权限Android 8.0以上如果应用没打开“允许安装未知应用”开关你直接startActivity去调安装器系统会有弹窗拦截。很多新手以为是自己写错了其实是被这个特殊权限卡住了。在Unity C#侧判断和引导的代码我也写出来public static bool CanRequestInstall() { #if UNITY_ANDROID !UNITY_EDITOR using (var pm MainActivity.CallAndroidJavaObject(getPackageManager)) { return pm.Callbool(canRequestPackageInstalls); } #else return true; #endif } public static void OpenInstallSetting() { #if UNITY_ANDROID !UNITY_EDITOR using (var intent new AndroidJavaObject(android.content.Intent, android.settings.MANAGE_UNKNOWN_APP_SOURCES)) { using (var uri new AndroidJavaObject(android.net.Uri)) { using (var parsedUri uri.CallStaticAndroidJavaObject(parse, package: GetPackageName())) { intent.CallAndroidJavaObject(setData, parsedUri); } } intent.CallAndroidJavaObject(addFlags, 0x10000000); MainActivity.Call(startActivity, intent); } #endif }canRequestPackageInstalls()是PackageManager下的方法只有在Android 8.0以上才有所以调用前要做好版本判断。还有一点要注意部分ROM上用户即使打开了这个总开关安装时可能仍然会额外弹一次“安全提醒”这个属于ROM层面的加固逻辑应用改不了提示用户继续下一步就可以。4.4 Android 11包可见性的坑targetSdk 30Android 11开始系统引入“包可见性”机制。你的应用默认看不到设备上安装的其他应用除非在清单文件里加queries声明。最典型的症状是你调intent.resolveActivity()检查系统是否有安装器时返回结果是null导致你自己逻辑里拦住了没去调startActivity。服务器下载的APK要放在应用自己的专属目录下。我们项目最开始的版本用的是/storage/emulated/0/Download/xxx.apk然后发现Android 10以上要读别人Download目录得申请MANAGE_EXTERNAL_STORAGE这个权限审核很严普通应用基本拿不到。而且就算在Android 9及以下版本往公共Download目录写文件也需要WRITE_EXTERNAL_STORAGE动态权限用户拒绝就写不进去。后面改成只用自己的内部路径问题一下少了很多/data/data/包名/files/apk/xxx.apk /storage/emulated/0/Android/data/包名/files/apk/xxx.apk这两个路径分别对应Context.getFilesDir()和Context.getExternalFilesDir()不需要任何存储权限应用自己独占卸载时一并清除。唯一的缺点是如果用户从文件管理器里想手动找到这个APK路径藏得比较深但其实绝大多数场景根本不希望用户看到这个文件。5.2 下载完整性校验别让文件不完整就安装APK下载到一半断网、存储空间不足、CDN节点返回了错误页这些都可能导致安装时“解析包错误”。所以下载完成后建议做一个校验最常用的是MD5。一个简单的C# MD5校验方法using System.Security.Cryptography; public static bool CheckMd5(string filePath, string expectedMd5) { if (string.IsNullOrEmpty(expectedMd5)) return true; using (var md5 MD5.Create()) { using (var stream File.OpenRead(filePath)) { byte[] hashBytes md5.ComputeHash(stream); string realMd5 BitConverter.ToString(hashBytes).Replace(-, ).ToLower(); return realMd5.Equals(expectedMd5.ToLower()); } } }注意计算MD5会把整个APK读一遍安卓手机上安装包动不动上百MB主线程跑会卡住一定要放到子线程或协程里做。下载完先比对MD5不一致就删掉重新下一致才调安装。5.3 安装完成后的文件清理安装调起之后用户可能在安装界面点“完成”也可能点“取消”。但无论哪种情况APK文件都建议不要长期留在应用目录里。尤其是应用体积比较大的时候一个几百MB的安装包躺在那里占用空间不说应用商店在审核时如果检测到安装包残留也会有风险。比较稳妥的做法是调起安装器之后先记录一个待清除标记然后在收到安装成功或安装取消的回调后把APK文件删掉。或者更简单粗暴一点在应用启动时扫描固定目录把超过某个时间戳的APK文件清理掉。有些团队的做法是安装完成后调用MediaScannerConnection.scanFile让APK出现在相册/下载列表里这大可不必。企业内部隐藏安装包天经地义别自己增加暴露面。6. 安装结果怎么回传给Unity侧6.1 广播监听安装状态因为Unity应用只是调起系统安装器用户装没装成功、装完是否点了“打开”Unity应用本身是感知不到的。想让Unity侧知道安装结果常见方案是注册广播接收器监听ACTION_PACKAGE_ADDED和ACTION_PACKAGE_REPLACED。Java侧写一个简单的广播接收器public class InstallReceiver extends BroadcastReceiver { Override public void onReceive(Context context, Intent intent) { String action intent.getAction(); Uri data intent.getData(); if (data null) return; String packageName data.getSchemeSpecificPart(); if (Intent.ACTION_PACKAGE_ADDED.equals(action) !intent.getBooleanExtra( Intent.EXTRA_REPLACING, false)) { Log.d(InstallReceiver, 新安装: packageName); } else if (Intent.ACTION_PACKAGE_REPLACED.equals(action)) { Log.d(InstallReceiver, 覆盖更新: packageName); } } }注意ACTION_PACKAGE_ADDED在应用覆盖安装的时候也会触发只是带了EXTRA_REPLACINGtrue所以判断条件里要排除重复计数。注册广播在清单文件里做的话需要通过intent-filter配置action和data schemereceiver android:name.InstallReceiver intent-filter action android:nameandroid.intent.action.PACKAGE_ADDED / action android:nameandroid.intent.action.PACKAGE_REPLACED / data android:schemepackage / /intent-filter /receiver6.2 C#侧用AndroidJavaProxy接收回调Unity没法直接给Java传一个C#委托但可以通过AndroidJavaProxy实现一个Java接口让广播接收器反向调用Unity层的方法。稍微有点绕但从Unity 5.x时代开始这就是标准做法。首先在Java侧定义一个接口public interface IInstallCallback { void onInstalled(String packageName); void onInstallFailed(String message); }广播接收器里持有这个接口并回调public static class Notifier { public static IInstallCallback callback; }C#侧实现对应接口public class InstallCallbackProxy : AndroidJavaProxy { public InstallCallbackProxy() : base(com.example.installhelper.IInstallCallback) { } public void onInstalled(string packageName) { // Unity主线程外处理时注意线程切换 Debug.Log(安装成功: packageName); } public void onInstallFailed(string message) { Debug.LogError(安装失败: message); } }然后在C#中把这个Proxy传给Java侧var proxy new InstallCallbackProxy(); using (var helper new AndroidJavaClass(com.example.installhelper.ApkInstallHelper)) { helper.CallStatic(setInstallCallback, proxy); }这里有个坑必须提一下广播回调不是发生在Unity主线程上的。你在回调里直接操作Unity对象、调用UI更新大概率会看到“UnityException: Cant be called from background thread”。解决办法是用UnityMainThreadDispatcher或者SynchronizationContext把回调内容切到主线程再处理。6.3 安装进度提示和界面处理下载APK的阶段可以用UnityWebRequest的downloadProgress做进度条。但是调起系统安装器之后Unity应用会退到后台Unity侧做的进度UI是用不上的。这时进度就交给系统安装器自己的界面展示你的Unity应用只需要在恢复前台时做一次检测目标包名是否已经安装、版本是否已经更新。我做过的最顺的流程是Unity内弹出下载确认框显示下载进度。下载完成、MD5校验通过调起系统安装器。OnApplicationPause(false)时检查目标包版本号如果已更新显示“更新完成”清理APK文件如果没更新提示“安装未完成”提供重试按钮。这套流程简单可靠比强行接广播回调要省心不少。7. 真机调试踩坑实录与排查速查7.1 “解析包错误”最常见原因五花八门解析包错误可以说是排名第一的安装问题。我排查过的案例里最多的原因是下载文件不完整APK实际字节数比服务器上的小很多。解决办法就是MD5校验不要盲目相信文件存在就认为文件是对的。其次是目标APK的最低系统版本高于当前设备系统版本。如果你用Unity导出APK时把Min API Level设得比较高在老设备上安装必然报解析失败。这个一般在版本接口里就应该拦下来客户端系统版本过低就不该继续引导用户安装。还有一种情况是APK签名不一致。设备上已经装了同一个包名的旧版本你想覆盖安装的新包签名跟旧包不一样系统也会拒绝并提示“解析包错误”或者“应用未安装”。这种问题通常出现在测试签名和正式签名搞混时排查时先确认两个包的签名是否一致。7.2 点安装没反应多半是FileProvider的Uri问题点击按钮后没有弹出系统安装界面也没有任何异常日志这种情况十有八九是Uri这块出了问题。第一种是FileUriExposedException。安装了小部分设备上代码走的是Uri.fromFile()老逻辑触发异常后程序崩溃或者什么都不发生。解决方式就是统一走FileProvider。第二种是FileProvider的authority写错了。你在Manifest里配的是com.mycompany.myapp.fileprovider代码里getUriForFile传的却是别的字符串或者干脆用的是applicationId自动替换导致和实际包名对不上都会导致IllegalArgumentException。第三种是没有加FLAG_GRANT_READ_URI_PERMISSION。系统安装器虽然拿到了content://Uri但没有授权读不到文件表现就是安装器闪退或者根本起不来。把这个flag加到Intent上基本都能解决。7.3 FileProvider authority冲突的连锁反应如果你的Unity工程里还有其他插件也注册了FileProvider而且都用默认的${applicationId}.fileprovider就会发生authority冲突。症状是打包时直接编译失败提示Conflict with dependency。解决方式有两种在Manifest中给其中一个provider手动改成带后缀的名字比如${applicationId}.fileprovider.install然后代码里同步使用这个新authority。用tools:replaceandroid:authorities在合并时覆盖。但要注意改了authority之后Java/C#里所有用到authority的地方都必须同步改否则一定安装失败。7.4 国产ROM在安装权限上的特殊表现国内几家主流厂商对安装权限的管理比原生安卓更严格。小米MIUI一般会弹出“安全守护”或者“安装拦截”的提示需要用户手动确认“本次安装”或者“永久允许”。华为EMUI/HarmonyOS在用户从“未知来源”安装时可能要求输入锁屏密码或者华为账号密码。OPPO和vivo在某些系统版本上如果没有登录账号会明确提示“禁止安装未知来源应用”。这些问题在代码层面基本没有完美的绕过方案能做的是在UI上给足引导。建议在下载安装包之前就做一次检测如果设备不支持直接安装先弹一个说明页告诉用户“接下来需要在系统设置中允许本应用安装未知来源应用否则无法完成更新”。7.5 应用商店的合规提醒如果你的Unity应用是要上传到成熟安卓应用商店必须注意REQUEST_INSTALL_PACKAGES权限会因为应用内下载安装APK而被审核平台重点关注。很多商店要求你说明用途甚至要求提供企业资质。从合规角度我建议正式上架版本如果功能是自动更新优先走各商店自己的更新通道保留应用内直接安装APK的逻辑放在企业内部版或者定向分发渠道。这块踩过坑的同行应该不少提前规划比事后整改省心得多。8. 最后分享一个我在实际项目中沉淀的小流程这个功能打磨到最后真正稳定下来的流程其实很短请求更新接口下载APK到应用外私有目录MD5校验检查未知来源权限没有就跳设置页引导开通过后调起系统安装器回到前台时检测版本号确认是否更新成功成功后清理APK。整个链路里没有哪个环节特别难但任何一环出错用户感知到的就是“App崩了”或者“装不上”。我个人最大的体会是Unity调安卓APK安装这件事能把黑盒变成白盒的都是细心人。日志一定要打全从下载开始记录文件大小、文件路径、权限状态、Uri信息和安装结果这样真机出问题的时候才不会两眼一抹黑。你的测试设备也不应该只有一台主流旗舰机找一台老安卓版本的低端机、一台最新系统的设备再把主流的国产ROM过一遍能提前发现很多隐藏雷区。希望这篇文章能让你少走点弯路。如果你在接入过程中遇到这篇没覆盖到的问题不妨按我前面给的排查思路一步一步排除大概率都能定位到是文件、权限还是Uri这三个环节里某一个没做好。
返回列表