1. 项目概述:为什么Android 13权限适配是Unity开发者的“必修课”
如果你最近用Unity打包的Android游戏,在Android 13设备上运行时,突然发现相机打不开、相册访问不了,或者通知不弹了,别急着怀疑是自己的代码写错了。这大概率是撞上了Android 13(API 33)引入的新一轮权限策略收紧。从Android 6.0的动态权限,到Android 10的作用域存储,再到Android 13的细粒度媒体权限和运行时通知权限,Google每一次大版本更新,都在把“用户隐私和数据安全”的紧箍咒再念紧一圈。对于Unity开发者来说,这不再是简单的调用几个API的问题,而是一套需要从设计、编码到测试全流程贯通的“合规性工程”。
我经历过不止一次因为权限问题导致的线上事故:新版本上线后,差评突然增多,核心功能失效,排查半天才发现是目标API等级(Target API Level)升级后,新的权限模型生效了。所以,今天我们不聊虚的,直接切入实战,把Android 13下Unity游戏开发中那些“坑”一个个填平。我会带你理解背后的机制,并给出一套完整的、可复用的代码方案,让你能优雅地处理权限问题,而不是在用户拒绝后让游戏卡死或崩溃。记住,优雅的权限处理,是提升用户体验和留存率的关键一步,绝不是可有可无的边缘功能。
2. Android 13权限模型的核心变化与Unity适配要点
Android 13的权限变化不是小修小补,而是对开发者习惯的一次“颠覆”。如果你还停留在“在Start()里一次性请求所有权限”的旧思维,那你的应用在Android 13上会举步维艰。我们必须先理解这些变化,才能写出正确的代码。
2.1 细粒度媒体权限:告别“读写存储”一刀切
在Android 13之前,我们通常使用READ_EXTERNAL_STORAGE和WRITE_EXTERNAL_STORAGE这两个“粗放”的权限来访问用户的媒体文件。这相当于你向用户要了一把能打开他整个媒体库(包括照片、视频、音乐)的万能钥匙。用户自然会警惕。
Android 13引入了更细分的媒体权限,将访问权限与具体的媒体类型挂钩:
READ_MEDIA_IMAGES:仅读取图片和照片。READ_MEDIA_VIDEO:仅读取视频。READ_MEDIA_AUDIO:仅读取音频文件。
这对Unity开发者的直接影响是什么?
- 目标API(Target API Level)>= 33时,新规则强制生效。如果你的游戏需要保存截图到相册,或者让用户选择头像,你不能再只请求
READ_EXTERNAL_STORAGE。如果你只需要读取图片,就应该只请求READ_MEDIA_IMAGES。 WRITE_EXTERNAL_STORAGE在Android 13上几乎被废弃。对于媒体文件,应用现在拥有其创建的文件的完全访问权限,无需写入权限。向共享存储写入媒体文件,应使用MediaStoreAPI。- Unity引擎的兼容性:较老版本的Unity(如2020.3 LTS之前的版本)可能没有预定义这些新权限的常量。你需要手动声明它们。
注意:如果你的应用需要同时访问图片和视频,你必须同时请求
READ_MEDIA_IMAGES和READ_MEDIA_VIDEO两个权限。系统会弹出一个合并的对话框,但逻辑上是两个独立的权限请求。
2.2 运行时通知权限:从安装时授予到用时申请
这是另一个重大变化。以前,只要你在AndroidManifest.xml里声明了POST_NOTIFICATIONS权限,应用安装后默认就拥有发送通知的能力。现在,POST_NOTIFICATIONS被提升为运行时危险权限。
这意味着:
- 应用必须在向用户发送任何通知之前,主动请求并获得该权限。
- 请求的时机至关重要。最好的实践是在用户可能希望收到通知的上下文场景中请求,例如在游戏设置中开启“战斗奖励提醒”时,或者在社交功能中请求“好友消息通知”权限时。不要在游戏一启动就弹窗,那会被用户视为骚扰而拒绝。
2.3 邻近Wi-Fi权限与身体传感器权限
这两项对特定品类的游戏影响较大:
NEARBY_WIFI_DEVICES:用于发现和连接附近的Wi-Fi设备。如果你的游戏涉及本地多人联机(如通过Wi-Fi直连),需要此权限。BODY_SENSORS:访问身体传感器(如心率)。主要针对健康类应用,普通游戏较少涉及。
Unity适配的核心思路转变:从“一次性索要所有权限”转变为按需、分场景、有解释地请求权限。你的代码逻辑需要变得更加动态和智能。
3. 实战:构建一个优雅的Unity Android权限管理器
理解了理论,我们开始动手。我将构建一个名为AndroidPermissionManager的单例类,它负责所有权限相关的逻辑。这样做的好处是职责清晰,便于在整个项目中复用和维护。
3.1 定义权限常量与管理器框架
首先,我们需要定义Android 13的新权限常量。因为Unity的UnityEngine.Android.Permission类可能还未更新包含它们。
// AndroidPermissionConstants.cs public static class AndroidPermissionConstants { // 传统存储权限 (用于 API < 33 或作为回退) public const string ReadExternalStorage = "android.permission.READ_EXTERNAL_STORAGE"; public const string WriteExternalStorage = "android.permission.WRITE_EXTERNAL_STORAGE"; // Android 13 (API 33+) 细粒度媒体权限 public const string ReadMediaImages = "android.permission.READ_MEDIA_IMAGES"; public const string ReadMediaVideo = "android.permission.READ_MEDIA_VIDEO"; public const string ReadMediaAudio = "android.permission.READ_MEDIA_AUDIO"; // Android 13 (API 33+) 通知权限 public const string PostNotifications = "android.permission.POST_NOTIFICATIONS"; // 其他常用权限 public const string Camera = "android.permission.CAMERA"; public const string RecordAudio = "android.permission.RECORD_AUDIO"; public const string AccessFineLocation = "android.permission.ACCESS_FINE_LOCATION"; }接下来是权限管理器的核心框架。我们使用单例模式确保全局只有一个权限管理器。
// AndroidPermissionManager.cs using UnityEngine; using System; using System.Collections.Generic; #if UNITY_ANDROID using UnityEngine.Android; #endif public class AndroidPermissionManager : MonoBehaviour { public static AndroidPermissionManager Instance { get; private set; } // 定义一个委托,用于权限请求结果的回调 public delegate void PermissionCallback(bool granted); // 存储正在进行的权限请求的回调 private Dictionary<string, PermissionCallback> pendingCallbacks = new Dictionary<string, PermissionCallback>(); // 权限与其中文解释的映射,用于向用户展示 private Dictionary<string, string> permissionRationaleMap = new Dictionary<string, string>() { { AndroidPermissionConstants.ReadMediaImages, "需要访问您的相册,以便您选择游戏头像或上传截图。" }, { AndroidPermissionConstants.ReadMediaVideo, "需要访问您的视频文件,用于录制和分享游戏精彩时刻。" }, { AndroidPermissionConstants.PostNotifications, "希望向您发送游戏更新、活动提醒和好友消息,以免您错过重要内容。" }, { AndroidPermissionConstants.Camera, "需要使用摄像头进行AR游戏体验或扫描二维码添加好友。" }, { AndroidPermissionConstants.RecordAudio, "需要麦克风权限用于游戏内语音聊天或语音指令。" }, { AndroidPermissionConstants.AccessFineLocation, "需要获取您的位置信息,用于基于位置的游戏玩法或附近玩家匹配。" } }; private void Awake() { if (Instance != null && Instance != this) { Destroy(this.gameObject); return; } Instance = this; DontDestroyOnLoad(this.gameObject); // 常驻场景,方便全局调用 } }3.2 核心方法:检查、请求与处理回调
管理器的核心是三个方法:检查权限状态、发起权限请求、处理系统返回的结果。
// 在 AndroidPermissionManager.cs 中继续添加 public bool CheckPermission(string permission) { #if UNITY_ANDROID return Permission.HasUserAuthorizedPermission(permission); #else Debug.LogWarning($"[PermissionManager] 非Android平台,默认授予权限: {permission}"); return true; // 在编辑器或其他平台默认返回true #endif } public void RequestPermission(string permission, PermissionCallback callback) { #if UNITY_ANDROID // 1. 先检查是否已有权限 if (CheckPermission(permission)) { callback?.Invoke(true); return; } // 2. 检查用户是否之前选择了“不再询问” if (ShouldShowRequestPermissionRationale(permission)) { // 用户拒绝过但未勾选“不再询问”,可以展示解释性UI ShowRationaleDialog(permission, callback); } else { // 首次请求或用户已选择“不再询问”,直接向系统发起请求 DoRequestPermission(permission, callback); } #else callback?.Invoke(true); // 非Android平台直接模拟成功 #endif } // 处理系统权限请求结果(这个函数需要由Android系统回调) private void OnPermissionRequestResult(string permission, bool granted) { Debug.Log($"[PermissionManager] 权限 {permission} 请求结果: {granted}"); if (pendingCallbacks.TryGetValue(permission, out var callback)) { callback?.Invoke(granted); pendingCallbacks.Remove(permission); } } // 实际调用Unity API发起请求 private void DoRequestPermission(string permission, PermissionCallback callback) { #if UNITY_ANDROID pendingCallbacks[permission] = callback; // 注意:这里我们使用一个协程来等待并捕获回调,因为Unity的Permission类回调方式比较原始。 // 更优雅的方式是使用Android原生插件,但为简化,此处用Unity内置方式演示。 StartCoroutine(RequestPermissionCoroutine(permission)); #endif } private System.Collections.IEnumerator RequestPermissionCoroutine(string permission) { var callbacks = PermissionCallbacks.Instance; bool requestCompleted = false; bool resultGranted = false; callbacks.PermissionGranted += (string perm) => { if (perm == permission) { resultGranted = true; requestCompleted = true; } }; callbacks.PermissionDenied += (string perm) => { if (perm == permission) { requestCompleted = true; } }; callbacks.PermissionDeniedAndDontAskAgain += (string perm) => { if (perm == permission) { requestCompleted = true; } }; Permission.RequestUserPermission(permission); // 等待请求完成(简单轮询,实际项目可用更高级的异步等待) float timeout = 5f; // 超时时间 float timer = 0; while (!requestCompleted && timer < timeout) { timer += Time.deltaTime; yield return null; } OnPermissionRequestResult(permission, resultGranted); }关键点解析:
ShouldShowRequestPermissionRationale:这是一个关键方法。它返回true表示用户之前拒绝过此权限,但没有勾选“不再询问”。这是你向用户解释权限用途的黄金窗口期。如果返回false,要么是用户从未被询问过,要么是用户已经勾选了“不再询问”。对于后者,你只能引导用户去系统设置页手动开启。- 回调处理:Unity的
Permission.RequestUserPermission是异步的,但没有提供方便的async/await或回调参数。上面的代码通过PermissionCallbacks来监听结果,并用协程等待。这是Unity API比较别扭的地方,我们通过封装来简化上层调用。
3.3 实现解释性对话框与引导至系统设置
当ShouldShowRequestPermissionRationale返回true时,我们应该弹出一个自定义对话框,友好地解释为什么需要这个权限,从而提高用户的授权率。
// 在 AndroidPermissionManager.cs 中添加 private void ShowRationaleDialog(string permission, PermissionCallback callback) { // 这里应该触发你的UI系统,弹出一个模态对话框。 // 例如,使用UGUI创建一个简单的对话框Prefab。 // 以下为逻辑示例: string rationaleText = GetRationaleText(permission); string dialogTitle = "权限请求"; // 假设你有一个UI管理器,可以显示对话框 UIManager.Instance.ShowConfirmDialog( dialogTitle, rationaleText, "去设置", "取消", () => { // 用户点击“去设置”,再次发起系统请求 DoRequestPermission(permission, callback); }, () => { // 用户点击“取消”,直接回调拒绝 callback?.Invoke(false); } ); } private string GetRationaleText(string permission) { if (permissionRationaleMap.TryGetValue(permission, out string rationale)) { return rationale; } return "此功能需要相关权限才能正常使用,请授权。"; }如果用户已经选择了“不再询问”,Permission.RequestUserPermission将不会弹出系统对话框。此时,我们必须引导用户手动去系统设置页面开启。
public void OpenAppSettings() { #if UNITY_ANDROID && !UNITY_EDITOR try { using (var unityClass = new AndroidJavaClass("com.unity3d.player.UnityPlayer")) using (var currentActivity = unityClass.GetStatic<AndroidJavaObject>("currentActivity")) using (var intent = new AndroidJavaObject("android.content.Intent")) { string packageName = currentActivity.Call<string>("getPackageName"); using (var uri = new AndroidJavaObject("android.net.Uri")) { // Android 8.0 (API 26) 之后,跳转到应用信息页面的Intent Action intent.Call<AndroidJavaObject>("setAction", "android.settings.APPLICATION_DETAILS_SETTINGS"); string uriString = "package:" + packageName; var uriObj = uri.CallStatic<AndroidJavaObject>("parse", uriString); intent.Call<AndroidJavaObject>("setData", uriObj); intent.Call<AndroidJavaObject>("addCategory", "android.intent.category.DEFAULT"); intent.Call<AndroidJavaObject>("setFlags", 0x10000000); // FLAG_ACTIVITY_NEW_TASK currentActivity.Call("startActivity", intent); } } } catch (System.Exception e) { Debug.LogError($"[PermissionManager] 打开系统设置失败: {e.Message}"); } #else Debug.Log("[PermissionManager] 非Android运行时,无法打开系统设置。"); #endif }3.4 针对Android 13的智能权限请求策略
现在,我们将所有部分组合起来,实现一个智能的、兼容Android 13的权限请求方法。这个方法会自动根据设备API等级选择正确的权限字符串。
// 在 AndroidPermissionManager.cs 中添加 public void RequestMediaPermission(PermissionCallback callback, MediaType mediaType) { string permissionToRequest; #if UNITY_ANDROID // 获取当前Android API级别 int apiLevel = GetAndroidAPILevel(); if (apiLevel >= 33) // Android 13 (T) 及以上 { switch (mediaType) { case MediaType.Image: permissionToRequest = AndroidPermissionConstants.ReadMediaImages; break; case MediaType.Video: permissionToRequest = AndroidPermissionConstants.ReadMediaVideo; break; case MediaType.Audio: permissionToRequest = AndroidPermissionConstants.ReadMediaAudio; break; default: permissionToRequest = AndroidPermissionConstants.ReadExternalStorage; // 回退 break; } } else { // Android 13 以下,使用传统的存储权限 permissionToRequest = AndroidPermissionConstants.ReadExternalStorage; // 注意:在Android 10-12上,即使有READ_EXTERNAL_STORAGE,访问媒体文件也受作用域存储限制。 // 实际开发中,应使用Unity的NativeGallery等插件或FilePicker来安全访问。 } RequestPermission(permissionToRequest, callback); #else callback?.Invoke(true); #endif } public void RequestNotificationPermission(PermissionCallback callback) { #if UNITY_ANDROID int apiLevel = GetAndroidAPILevel(); if (apiLevel >= 33) { RequestPermission(AndroidPermissionConstants.PostNotifications, callback); } else { // Android 13以下,通知权限非运行时权限,默认拥有(需在Manifest声明) callback?.Invoke(true); } #else callback?.Invoke(true); #endif } private int GetAndroidAPILevel() { #if UNITY_ANDROID && !UNITY_EDITOR using (var version = new AndroidJavaClass("android.os.Build$VERSION")) { return version.GetStatic<int>("SDK_INT"); } #else return 0; // 编辑器环境 #endif } public enum MediaType { Image, Video, Audio }4. 在游戏业务逻辑中集成权限管理
有了强大的AndroidPermissionManager,我们在业务层调用就变得非常清晰和安全。以下是一些典型场景的示例。
4.1 场景一:用户上传头像
假设有一个按钮,点击后允许用户从相册选择头像。
// AvatarUploadHandler.cs using UnityEngine; using UnityEngine.UI; public class AvatarUploadHandler : MonoBehaviour { public Button selectAvatarButton; void Start() { selectAvatarButton.onClick.AddListener(OnSelectAvatarClicked); } private void OnSelectAvatarClicked() { // 1. 请求读取图片的权限 AndroidPermissionManager.Instance.RequestMediaPermission(OnImagePermissionResult, MediaType.Image); } private void OnImagePermissionResult(bool granted) { if (granted) { // 权限已获取,打开图片选择器 // 这里需要接入一个原生图片选择器插件,例如 NativeGallery // NativeGallery.GetImageFromGallery(...); Debug.Log("权限获取成功,开始选择图片..."); // TODO: 调用具体的图片选择逻辑 } else { // 权限被拒绝 Debug.LogWarning("读取相册权限被拒绝,无法选择头像。"); // 可以在这里显示一个提示,告知用户功能不可用,并提供一个按钮引导至设置。 ShowPermissionDeniedTip("相册访问"); } } private void ShowPermissionDeniedTip(string featureName) { UIManager.Instance.ShowConfirmDialog( "权限不足", $"您拒绝了{featureName}权限,相关功能将无法使用。您可以去系统设置中手动开启。", "去设置", "稍后再说", () => { AndroidPermissionManager.Instance.OpenAppSettings(); }, null ); } }4.2 场景二:请求通知权限
不要在游戏启动时请求通知权限。更好的时机是在用户完成新手引导、进入主城后,弹出一个温馨的提示。
// NotificationPermissionRequester.cs using UnityEngine; public class NotificationPermissionRequester : MonoBehaviour { [SerializeField] private string triggerPrefKey = "HasAskedNotificationPermission"; // 用于记录是否已询问过 void Start() { // 例如,在玩家首次进入游戏大厅时触发 if (!PlayerPrefs.HasKey(triggerPrefKey)) { // 可以延迟几秒,避免干扰初始体验 Invoke(nameof(PromptNotificationPermission), 5f); } } private void PromptNotificationPermission() { // 先检查是否已有权限(用户可能在其他地方已经授予) if (AndroidPermissionManager.Instance.CheckPermission(AndroidPermissionConstants.PostNotifications)) { return; } // 展示一个自定义的、吸引人的解释界面,而不是直接弹系统窗 UIManager.Instance.ShowNotificationPermissionDialog( "开启游戏通知", "及时接收好友消息、体力恢复提醒和限时活动通知,让你永不掉队!", "立即开启", "暂时不要", () => { // 用户点击“立即开启”,发起系统权限请求 AndroidPermissionManager.Instance.RequestNotificationPermission(OnNotificationPermissionResult); PlayerPrefs.SetInt(triggerPrefKey, 1); }, () => { // 用户点击“暂时不要”,记录并关闭 PlayerPrefs.SetInt(triggerPrefKey, 1); } ); } private void OnNotificationPermissionResult(bool granted) { if (granted) { Debug.Log("通知权限已开启,可以开始安排本地通知了。"); // TODO: 初始化游戏内通知调度系统 } else { Debug.Log("用户拒绝了通知权限。"); } } }4.3 场景三:使用相机进行AR扫描
// ARScanner.cs using UnityEngine; public class ARScanner : MonoBehaviour { public void StartScan() { AndroidPermissionManager.Instance.RequestPermission( AndroidPermissionConstants.Camera, (granted) => { if (granted) { InitializeARCamera(); } else { Debug.LogError("相机权限被拒绝,AR功能无法使用。"); // 提示用户,并可能提供一个“重试”或“去设置”的按钮 } } ); } private void InitializeARCamera() { // 初始化AR Foundation或其他AR SDK的代码 Debug.Log("相机权限OK,启动AR扫描..."); } }5. 清单文件(AndroidManifest.xml)的关键配置
代码写好了,但AndroidManifest.xml的配置同样重要,否则权限请求可能根本不会触发。你需要通过Unity的Plugins/Android文件夹来定制清单文件。
- 创建或修改清单文件:在项目的
Assets/Plugins/Android目录下,创建或找到AndroidManifest.xml文件。如果没有,可以从Unity安装目录下的Editor/Data/PlaybackEngines/AndroidPlayer/Apk中复制一个模板。 - 声明权限:在
<manifest>标签内添加你需要的所有权限。注意,即使Android 13使用了新权限,为了向后兼容,有时也需要声明旧权限。
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.yourcompany.yourgame" android:installLocation="preferExternal"> <!-- 传统存储权限 (用于 API < 33 兼容) --> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" /> <!-- 注意:WRITE_EXTERNAL_STORAGE 在 Android 10+ 对媒体文件作用有限,通常不需要了 --> <!-- Android 13 (API 33+) 细粒度媒体权限 --> <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" /> <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" /> <uses-permission android:name="android.permission.READ_MEDIA_AUDIO" /> <!-- Android 13 (API 33+) 通知权限 --> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <!-- 其他常用权限 --> <uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <!-- 如果你的游戏需要保持屏幕常亮 --> <uses-permission android:name="android.permission.WAKE_LOCK" /> <application android:label="@string/app_name" android:icon="@mipmap/app_icon" android:theme="@style/UnityThemeSelector" android:usesCleartextTraffic="true"> <!-- 其他Activity配置,特别是UnityPlayerActivity --> <activity android:name="com.unity3d.player.UnityPlayerActivity" android:configChanges="fontScale|keyboard|keyboardHidden|locale|mnc|mcc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|uiMode|touchscreen" android:launchMode="singleTask" android:resizeableActivity="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> <meta-data android:name="unityplayer.UnityActivity" android:value="true" /> </activity> </application> </manifest>关键说明:
android:maxSdkVersion="32":这个属性用于READ_EXTERNAL_STORAGE,表示此权限只对API 32(Android 12L)及以下的设备生效。在API 33+的设备上,系统会忽略此权限声明。这是Google推荐的兼容性写法。- 务必设置正确的Target API:在Unity的
Player Settings > Android > Other Settings中,将Target API Level设置为最新(如Android 13.0 (API Level 33))。这不仅是商店的要求,也是确保新权限规则生效的前提。
6. 测试、调试与常见问题排查
权限问题在测试阶段尤其棘手,因为不同Android版本、不同厂商设备的行为可能有差异。以下是我总结的一套测试方法和常见问题清单。
6.1 在Unity编辑器和真机上的测试策略
- 编辑器模拟:我们的
AndroidPermissionManager在非Android平台会直接返回true,方便在编辑器中跑通逻辑流程。但真正的回调行为和系统对话框无法模拟。 - ADB命令快速测试:这是最高效的测试方法。在电脑上连接Android设备后,使用命令行:
adb shell pm list permissions -d -g:查看危险权限组。adb shell pm grant <package_name> <permission>:授予权限。例如:adb shell pm grant com.yourcompany.yourgame android.permission.CAMERAadb shell pm revoke <package_name> <permission>:撤销权限。adb shell am start -a android.settings.APPLICATION_DETAILS_SETTINGS -d package:<package_name>:直接跳转到你应用的系统设置页。
- 测试用例覆盖:
- 首次安装:所有权限都应该是未授予状态。
- 允许权限:点击请求,选择“允许”。检查功能是否正常。
- 拒绝权限:点击请求,选择“拒绝”。检查你的解释性对话框是否弹出,再次请求是否有效。
- 拒绝并不再询问:在系统对话框勾选“不再询问”后拒绝。检查你的逻辑是否会引导用户去系统设置。
- 从系统设置中开启/关闭权限:在应用运行时,去系统设置里开关权限,返回应用后,检查状态是否被正确检测到(可能需要监听
OnApplicationPause/OnApplicationFocus事件来重新检查)。
6.2 常见问题与解决方案速查表
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
在Android 13设备上,请求READ_EXTERNAL_STORAGE无效,无法访问相册。 | Target API Level >= 33,系统已启用新权限模型。READ_EXTERNAL_STORAGE对媒体文件无效。 | 使用RequestMediaPermission方法,它会根据API等级自动请求READ_MEDIA_IMAGES等权限。确保清单文件已声明新权限。 |
用户点击“拒绝并不再询问”后,再次调用RequestUserPermission,系统对话框不弹出。 | 这是Android系统的预期行为。当用户勾选“不再询问”后,该权限的请求通道被系统屏蔽。 | 调用ShouldShowRequestPermissionRationale(permission),如果返回false,且CheckPermission也返回false,则应显示自定义界面,引导用户点击按钮,通过OpenAppSettings()跳转到系统设置页。 |
| 在Unity编辑器中运行正常,打包到Android后权限相关代码不执行。 | 条件编译指令#if UNITY_ANDROID和#if !UNITY_EDITOR使用有误,或者AndroidManifest.xml未正确合并。 | 检查所有平台相关代码是否被正确包裹。在Player Settings中查看最终的合并后的Manifest文件(构建后可以在Temp目录找到),确认权限声明已存在。 |
| 通知权限请求了,但用户收不到通知。 | 1. 未在AndroidManifest.xml中声明POST_NOTIFICATIONS。2. 在Android 13以下设备上,没有正确配置通知渠道(Notification Channel)。 3. 发送通知的代码逻辑有误。 | 1. 确认清单文件已声明该权限。 2. 对于API 26+的设备,创建通知时必须指定有效的通知渠道ID。确保你的通知发送代码(如使用 Unity.Notifications包)正确创建了渠道。3. 检查通知的标题、内容、图标等是否有效。 |
权限回调OnPermissionRequestResult没有被调用。 | PermissionCallbacks的事件订阅可能因为对象生命周期问题被意外取消,或者协程超时。 | 确保PermissionCallbacks实例在场景中持久存在。在我们的封装中,AndroidPermissionManager是DontDestroyOnLoad的。可以适当增加协程超时时间,并添加更详细的日志。一个更稳健的方法是使用Android原生插件(如UnityAndroidPermissions)来获得更可靠的回调。 |
| 不同手机厂商(小米、华为、OPPO等)权限弹窗样式或行为不一致。 | 这是Android碎片化的典型问题。厂商可能会修改权限对话框的UI,甚至增加额外的“后台弹出界面”等权限。 | 进行充分的真机兼容性测试。对于厂商特有的权限(如自启动、电池优化忽略),需要在你的应用内增加额外的引导说明,告诉用户如何手动设置。这超出了标准权限API的范围,属于“中国特色”的适配。 |
6.3 一个实用的调试工具类
在开发过程中,可以创建一个简单的调试UI,实时显示所有关键权限的状态。
// PermissionDebugUI.cs using UnityEngine; using UnityEngine.UI; #if UNITY_ANDROID using UnityEngine.Android; #endif public class PermissionDebugUI : MonoBehaviour { public Text debugText; void OnGUI() { if (debugText != null) { string status = "=== 权限状态 ===\n"; #if UNITY_ANDROID status += $"Camera: {Permission.HasUserAuthorizedPermission(Permission.Camera)}\n"; status += $"Microphone: {Permission.HasUserAuthorizedPermission(Permission.Microphone)}\n"; // 检查Android 13权限需要用到我们定义的常量 if (GetAndroidAPILevel() >= 33) { status += $"ReadMediaImages: {Permission.HasUserAuthorizedPermission(AndroidPermissionConstants.ReadMediaImages)}\n"; status += $"PostNotifications: {Permission.HasUserAuthorizedPermission(AndroidPermissionConstants.PostNotifications)}\n"; } else { status += $"ExternalStorage: {Permission.HasUserAuthorizedPermission(Permission.ExternalStorageRead)}\n"; } #endif debugText.text = status; } // 在屏幕上画几个测试按钮 if (GUI.Button(new Rect(10, 100, 200, 60), "请求相机权限")) { AndroidPermissionManager.Instance.RequestPermission(AndroidPermissionConstants.Camera, (granted) => { Debug.Log($"Camera: {granted}"); }); } if (GUI.Button(new Rect(10, 170, 200, 60), "请求图片权限")) { AndroidPermissionManager.Instance.RequestMediaPermission((granted) => { Debug.Log($"Image Permission: {granted}"); }, MediaType.Image); } if (GUI.Button(new Rect(10, 240, 200, 60), "打开应用设置")) { AndroidPermissionManager.Instance.OpenAppSettings(); } } private int GetAndroidAPILevel() { // ... 同前文 GetAndroidAPILevel 方法 return 0; } }踩过几次坑之后,我最大的体会是:权限适配不是一次性任务,而是一个持续的过程。每次Google发布新的Android版本,都需要重新审视你的权限逻辑。将权限管理模块化、服务化,是应对这种变化的最佳策略。本文提供的AndroidPermissionManager只是一个起点,你可以根据自己项目的UI框架、日志系统和异常处理机制对其进行增强。比如,将权限请求与游戏内的任务系统结合,或者与数据分析平台打通,记录用户拒绝权限的比率和场景,从而持续优化你的权限请求策略,最终的目标是让权限请求成为流畅用户体验的一部分,而不是令人反感的阻碍。