1. 项目概述为什么Unity安卓打包的权限问题如此棘手最近在社区里看到不少朋友尤其是刚接触Unity移动端开发的同学都在抱怨同一个问题明明在Unity编辑器里跑得好好的怎么一打包成安卓APK游戏就各种“罢工”要么是存档文件写不进去要么是读取不到配置文件要么是访问外部资源时直接崩溃。折腾半天最后发现十有八九是栽在了“文件权限”这个坑里。这确实是个老生常谈但又常谈常新的问题尤其是在Android系统权限模型不断收紧的背景下2024年的今天很多几年前的“解决方案”可能已经失效或者引入了新的兼容性问题。我自己在最近的一个跨平台项目中就花了整整两天时间跟安卓的文件权限“斗智斗勇”。从AndroidManifest.xml的配置到运行时动态权限申请的逻辑再到不同Android版本特别是Android 10/11的Scoped Storage和Android 13的细粒度权限下的适配每一步都可能有“惊喜”。这篇文章就是把我踩过的坑、验证过的有效方案以及一些容易忽略的细节系统地梳理出来。无论你是正在为权限问题头疼的开发者还是想提前避坑的学习者希望这篇基于2024年最新环境和Unity版本以Unity 2022 LTS为例的实测指南能帮你节省大量排查时间。2. 核心思路拆解理解Android权限模型是避坑的第一步在动手改代码之前我们必须先搞清楚Unity在打包安卓时文件权限管理的“底层逻辑”是什么。很多问题之所以复杂是因为我们混淆了“安装时权限”、“运行时权限”和“文件系统访问范围”这几个概念。2.1 安装时权限Declared Permissions vs. 运行时权限Runtime Permissions这是最容易混淆的一对概念。简单来说安装时权限在AndroidManifest.xml文件中通过标签声明的权限。这些权限在用户安装APK时会在应用商店的安装页面以列表形式展示对于旧版本Android或者只是告知系统“我这个应用可能需要这些能力”。从Android 6.0API 23开始这里声明的危险权限并不会在安装时自动授予而需要我们在应用运行时动态申请。运行时权限特指那些被归类为“危险权限”Dangerous Permission的权限例如读写外部存储、访问相机、录音等。这些权限必须由应用在运行时通常是需要用到该功能的前一刻弹出系统对话框由用户手动点击“允许”才能获得。对于文件操作最相关的就是READ_EXTERNAL_STORAGE和WRITE_EXTERNAL_STORAGE这两个危险权限。一个关键认知是即使在AndroidManifest.xml里声明了它们也只是拿到了“申请资格”真正的读写能力必须经过用户同意才能生效。2.2 Android文件存储空间演变重点在“外部存储”Unity中我们常用的文件路径比如Application.persistentDataPath在安卓平台下具体指向哪里这直接决定了我们需要申请什么权限。内部存储Internal Storage每个应用私有的目录路径类似于/data/data/package_name/。应用无需任何权限即可在此目录下自由读写文件且其他应用和用户无法直接访问除非设备已Root。Application.persistentDataPath在Android上默认就指向这里的一个子目录如files。所以如果你的文件只存在这里那么恭喜你根本不需要处理本文讨论的权限问题。外部存储External Storage这是“坑”最多的地方。它通常指设备的共享存储空间如手机的“内部存储/机身存储”不是SD卡。外部存储又分为应用私有外部存储App-specific external storage位于外部存储中一个以你应用包名命名的专属目录如Android/data/package_name/。从Android 4.4开始应用在自己的这个私有目录下读写也不需要WRITE_EXTERNAL_STORAGE权限。Unity的Application.persistentDataPath在某些旧版本或特定配置下可能会指向这里但现在更倾向于指向内部存储。共享外部存储Shared external storage就是用户常见的/storage/emulated/0/DCIM,/storage/emulated/0/Download等目录。只要你想访问这些共享区域就必须申请并获取对应的运行时权限。问题的复杂性在于我们常常会使用一些插件或代码去访问共享外部存储比如让用户选择图片、导出日志到下载目录、读取公共文件夹的音乐等但却没有正确配置和申请权限。2.3 Unity的权限处理机制自动生成与手动覆盖Unity在打包APK时会自动生成一个基础的AndroidManifest.xml文件。它会根据你在Player Settings里的一些选项比如勾选Internet Access自动添加部分权限。但对于文件权限Unity的自动处理是保守且可能不完整的。例如如果你在脚本中使用了System.IO命名空间下的File.ReadAllText去尝试读取一个绝对路径如/storage/emulated/0/Download/test.txtUnity的打包过程不会自动帮你把READ_EXTERNAL_STORAGE权限加到Manifest里。这就导致了“编辑器正常打包后崩溃”的典型症状——因为权限根本没声明。因此我们的核心思路就是手动接管关键权限的声明和申请流程确保覆盖所有可能的文件访问场景并适配不同Android版本的要求。3. 实操全流程从配置到代码的完整避坑方案下面我将按照一个完整的流程从项目配置到代码编写一步步说明如何正确设置。3.1 第一步创建并配置自定义的 AndroidManifest.xml这是所有工作的基础。你不能依赖Unity自动生成的那个。定位模板文件在你的Unity安装目录下例如C:\Program Files\Unity\Hub\Editor\2022.3.20f1\Editor\Data\PlaybackEngines\AndroidPlayer找到Apk子目录。里面有一个AndroidManifest.xml文件这就是Unity使用的默认模板。把它复制出来。放入项目在你的Unity项目Assets目录下创建或已有一个名为Plugins/Android的文件夹。将复制出来的AndroidManifest.xml文件粘贴进去。编辑权限声明用任何文本编辑器打开这个文件。找到标签内部。你需要添加或确认以下权限!-- 如果应用需要从共享存储空间读取文件如用户相册 -- uses-permission android:nameandroid.permission.READ_EXTERNAL_STORAGE / !-- 如果应用需要向共享存储空间写入文件如保存图片到相册、导出文件到下载目录 -- uses-permission android:nameandroid.permission.WRITE_EXTERNAL_STORAGE / !-- 针对 Android 11 (API 30) 及以上如果需要在所有文件管理权限MANAGE_EXTERNAL_STORAGE -- !-- 注意此权限审核严格仅用于真正的文件管理器类应用上架Google Play需声明用途。普通应用应避免使用。 -- !-- uses-permission android:nameandroid.permission.MANAGE_EXTERNAL_STORAGE / -- !-- 从 Android 10 (API 29) 开始为了兼容Scoped Storage即使申请了读写权限默认也无法访问媒体文件。需要添加以下属性 -- application ... android:requestLegacyExternalStoragetrue !-- 关键用于在Android 10上暂时禁用Scoped Storage争取适配时间 -- 重要提示android:requestLegacyExternalStoragetrue这个属性是Android 10/11兼容性的关键。但它从Android 11API 30开始逐渐失效对于以API 30为目标的应用Google Play会有警告。对于新项目长远之计是必须适配Scoped Storage使用MediaStoreAPI或Storage Access Framework。但这属于更进阶的内容本文聚焦于让应用先跑起来。设置这个属性可以为你争取时间。3.2 第二步处理 Android 13 的细粒度媒体权限Android 13引入了更细分的媒体权限。以前一个READ_EXTERNAL_STORAGE就能读所有媒体文件现在不行了。如果你的应用需要读取用户的图片、视频、音频文件在AndroidManifest.xml中应该用新的细分权限替换旧的READ_EXTERNAL_STORAGE!-- 替换或补充旧的 READ_EXTERNAL_STORAGE -- uses-permission android:nameandroid.permission.READ_MEDIA_IMAGES / uses-permission android:nameandroid.permission.READ_MEDIA_VIDEO / uses-permission android:nameandroid.permission.READ_MEDIA_AUDIO / !-- 如果仍需要访问非媒体类文件如PDF、TXT或需要写入则仍需保留以下 -- uses-permission android:nameandroid.permission.READ_EXTERNAL_STORAGE android:maxSdkVersion32 / uses-permission android:nameandroid.permission.WRITE_EXTERNAL_STORAGE android:maxSdkVersion32 /注意android:maxSdkVersion32这表示这些旧权限只对Android 12LAPI 32及以下的设备生效。对于Android 13的设备系统会忽略它们转而使用上面的细分媒体权限。这是Google推荐的做法。3.3 第三步编写C#运行时权限申请脚本权限声明好了接下来就是在游戏运行时动态申请。我们需要一个脚本来管理这一切。using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.Android; // 引入Unity的Android权限工具类 using System.IO; public class AndroidPermissionManager : MonoBehaviour { // 定义你需要申请的权限列表 private readonly string[] permissions { // 根据你的Manifest配置和Target API Level动态决定 // 示例同时申请旧版和细分权限系统会根据API级别处理 Permission.ExternalStorageRead, Permission.ExternalStorageWrite, // Android 13 的细分权限 android.permission.READ_MEDIA_IMAGES, android.permission.READ_MEDIA_VIDEO, android.permission.READ_MEDIA_AUDIO }; IEnumerator Start() { // 等待几帧确保游戏初始化完成 yield return new WaitForEndOfFrame(); // 检查并申请所有必要的权限 foreach (string perm in permissions) { // 使用Unity封装的Permission类进行检查和申请 if (!Permission.HasUserAuthorizedPermission(perm)) { Debug.Log($申请权限: {perm}); Permission.RequestUserPermission(perm); // 重要在移动端尤其是Android申请权限是异步的且会弹出系统对话框。 // 这里不能立即检查结果需要等待用户操作。 // 一个简单的做法是在关键操作前再检查权限或者设置一个“权限检查点”场景。 yield return new WaitForSeconds(0.5f); // 给对话框弹出留点时间 } else { Debug.Log($权限已授予: {perm}); } } // 所有权限请求发出后可以进入下一步但实际文件操作前仍需验证 Debug.Log(权限申请流程已发起。); } // 一个在需要进行文件操作前调用的检查方法 public static bool CheckStoragePermission() { // 针对Android 13优先检查细分权限 if (CheckPermissionForAndroid13Plus()) { return true; } // 针对旧版本检查传统权限 else if (Permission.HasUserAuthorizedPermission(Permission.ExternalStorageRead) Permission.HasUserAuthorizedPermission(Permission.ExternalStorageWrite)) { return true; } return false; } private static bool CheckPermissionForAndroid13Plus() { #if UNITY_ANDROID UNITY_2022_2_OR_NEWER // 简化的检查逻辑如果你的应用只关心媒体文件可以这样检查。 // 实际应根据应用具体访问的文件类型来检查对应的权限。 if (Permission.HasUserAuthorizedPermission(android.permission.READ_MEDIA_IMAGES)) { return true; } #endif return false; } // 示例一个安全的文件写入方法 public void SafeWriteToPublicDownload(string filename, string content) { if (!CheckStoragePermission()) { Debug.LogError(没有文件存储权限无法执行写入操作); // 这里可以触发UI提示引导用户去设置页开启权限 ShowPermissionDeniedDialog(); return; } try { // 获取安卓的公共下载目录路径 string downloadPath GetAndroidPublicDownloadPath(); string fullPath Path.Combine(downloadPath, filename); File.WriteAllText(fullPath, content); Debug.Log($文件已成功写入: {fullPath}); } catch (System.Exception e) { Debug.LogError($写入文件失败: {e.Message}); // 即使有权限在Android 11的Scoped Storage下直接写共享目录也可能失败。 // 此时应考虑使用MediaStore API或SAFStorage Access Framework。 } } private string GetAndroidPublicDownloadPath() { // 注意此方法在Android 10上可能无法直接访问。 // 仅作为示例实际生产环境应使用更兼容的方法。 return /storage/emulated/0/Download; } private void ShowPermissionDeniedDialog() { // 实现一个UI弹窗告知用户权限不足并提供跳转系统设置页的按钮 // 可以使用Unity的NativeDialog插件或自己调用Android原生API Debug.Log(请提示用户去应用设置中开启存储权限。); } }3.4 第四步Player Settings 关键配置回到Unity编辑器打开File - Build Settings - Player Settings...。Other Settings 区域:Minimum API Level: 根据你的用户群体设置。如果要支持Android 13的细分权限Target API Level至少需要设置为33。但Minimum可以设低一些如API 23Android 6.0。Target API Level:强烈建议设置为你要支持的最高版本如API 34对应Android 14。设为最新可以让Unity应用最新的行为变更如Scoped Storage。如果设低系统会以“兼容模式”运行可能掩盖一些权限问题但不利于应用上架和长期兼容。Publishing Settings 区域:确保Custom Main Manifest和Custom Main Gradle Template等选项没有被误勾选除非你确实提供了自定义文件。我们已经在Plugins/Android下放了AndroidManifest.xmlUnity会自动使用它。4. 深度避坑与疑难排查即使按照上述步骤操作你可能还是会遇到一些奇怪的问题。下面是我总结的几个高频“坑点”和解决方案。4.1 坑点一Permission.HasUserAuthorizedPermission 在编辑器下总是返回True这是最常见的困惑之一。在Unity编辑器中运行游戏无论你怎么测试Permission.HasUserAuthorizedPermission检查存储权限几乎总是返回true。这是因为编辑器环境并没有真正的Android权限系统。避坑技巧永远不要依赖编辑器环境的权限检查结果。测试权限逻辑的正确性必须在真机或安卓模拟器上打包测试。你可以通过#if !UNITY_EDITOR预处理指令来包裹关键的权限申请和检查代码避免在编辑器下干扰你的调试。4.2 坑点二Android 10/11 (API 29/30) 上有权限也无法写入共享目录这就是前面提到的Scoped Storage的影响。即使你申请并获得了WRITE_EXTERNAL_STORAGE权限在Android 10及以上版本应用默认也无法直接通过File.WriteAllText向/storage/emulated/0/Download这样的共享路径写文件。系统会抛出UnauthorizedAccessException。解决方案临时方案不推荐长期在AndroidManifest.xml的标签中设置android:requestLegacyExternalStoragetrue。这能让应用在Android 10API 29和针对旧版SDKtargetSdkVersion 30构建时暂时绕过Scoped Storage限制。但记住以API 30为目标的应用此属性在Android 11上无效且上架Google Play可能受影响。正确方案必须掌握使用MediaStoreAPI如果要保存图片、视频、音频到公共媒体库应使用Android原生的MediaStoreAPI。这需要通过Unity的AndroidJavaClass/AndroidJavaObject进行交互或者使用成熟的插件如NativeFilePicker、SimpleAndroidPermissionsForUnity的扩展功能。使用Storage Access Framework如果要让用户选择保存位置如下载目录、文档目录应该使用SAF。通过Intent.ACTION_CREATE_DOCUMENT或Intent.ACTION_OPEN_DOCUMENT启动一个系统文件选择器让用户自己指定保存位置然后通过返回的URI进行读写。这是Android官方推荐的、向前兼容的方式。4.3 坑点三权限申请对话框不弹出或者只弹出一次不弹出首先检查AndroidManifest.xml是否正确放置在Assets/Plugins/Android下并且权限声明无误。其次确保你的Target API Level设置正确。最后在真机上检查“应用信息 - 权限”看看权限列表里是否有你声明的项。如果根本没有说明Manifest没被正确合并。只弹出一次这是Android系统的设计。如果用户点击了“拒绝”下次再调用Permission.RequestUserPermission系统可能不会再次弹出对话框而是直接返回“拒绝”的结果。此时你需要引导用户手动到系统设置页为你的应用开启权限。你的应用应该有一个友好的提示界面并调用以下代码打开设置页// 打开当前应用的系统设置页面 Application.OpenURL(package: Application.identifier);4.4 坑点四不同Unity版本和构建系统的差异Unity的安卓构建系统从旧的Internal内部到Gradle再到现在的Gradle或Gradle (Unity as a Library)处理Manifest和资源的方式可能有细微差别。使用Gradle构建系统这是当前推荐的方式。当你使用Gradle时Unity会将你的自定义AndroidManifest.xml与它自动生成的内容进行合并。如果合并失败或冲突可能会导致权限丢失。务必检查打包日志Build窗口看是否有关于Manifest合并的警告或错误。如果遇到合并冲突你可能需要创建一个AndroidManifest.xml的合并规则文件merge.xml或者检查你的Manifest中是否有重复或冲突的属性比如定义了两次。4.5 坑点五第三方插件引入的权限问题很多Asset Store上的插件尤其是涉及文件访问、相机、麦克风的会在它们自己的AndroidManifest.xml中声明权限。当Unity打包时它会将所有插件的Manifest与主Manifest合并。问题插件声明的权限可能过于宽泛比如申请了所有权限或者与你自定义的Manifest冲突。排查在打出的APK包上右键用压缩软件打开找到AndroidManifest.xml文件用文本编辑器查看。这是最终合并后的结果。检查里面是否包含了你期望的权限以及是否有你不想要的权限。解决如果插件声明了不必要的权限你可以尝试找到插件自带的Manifest文件通常在插件的Android文件夹里修改或删除其中的权限声明。但要注意这可能会影响插件功能。5. 2024年实测有效的检查清单在你打包APK并测试之前对照这个清单快速过一遍[ ]Manifest文件自定义的AndroidManifest.xml是否已放入Assets/Plugins/Android/[ ]权限声明Manifest中是否已正确声明READ_EXTERNAL_STORAGE、WRITE_EXTERNAL_STORAGE或Android 13的细分媒体权限[ ]Scoped Storage兼容如果Target API Level 29是否添加了android:requestLegacyExternalStoragetrue仅作为临时过渡方案需知悉其限制[ ]Player SettingsTarget API Level是否已设置为预期最高版本推荐最新[ ]运行时申请代码中是否在合适的时机如应用启动后、执行文件操作前调用了权限申请逻辑[ ]真机测试是否在安卓真机或模拟器上进行了测试而非仅依赖编辑器[ ]权限引导当用户拒绝权限后是否有UI引导用户前往系统设置页手动开启[ ]路径检查你的文件操作代码是访问Application.persistentDataPath相对安全还是硬编码了共享存储路径需要权限且Android 10可能失败[ ]插件审查是否检查了第三方插件可能引入的额外权限或冲突6. 总结与个人心得跟Unity安卓打包的权限问题打交道感觉就像是在跟一个不断更新规则的守门人博弈。Android系统每年都在收紧权限和隐私政策这意味着我们开发者的适配工作不能一劳永逸。我个人的体会是尽早放弃“直接操作共享存储路径”的旧习惯。对于新项目在架构设计时就应该把“通过MediaStore或SAF访问公共文件”作为首选方案。虽然学习成本和初期编码复杂度高一些但这是通往未来版本的唯一稳定通道。android:requestLegacyExternalStoragetrue这根“拐杖”迟早要被收走。对于现有老项目的维护本文提供的方案正确声明动态申请临时兼容属性能解决大部分燃眉之急让你快速打出可用的APK。但务必在项目计划中为“Scoped Storage完全适配”留出足够的技术债偿还时间。最后一个小技巧在调试权限相关问题时善用adb logcat命令。在电脑上连接真机在命令行运行adb logcat -s Unity然后在手机上操作你的应用。Unity打印的Debug.Log以及Android系统抛出的权限拒绝异常都会清晰地显示在这里比在手机上抓日志方便得多。看到Permission Denial之类的关键字就能快速定位问题根源。文件权限虽是小问题却直接影响应用的核心功能。希望这篇结合了最新环境实测的指南能帮你填平这些坑让打包过程更加顺畅。