
1. 項目概述為什么Android 13權限適配是Unity開發者的“必修課”如果你最近用Unity打包的Android游戲在Android 13設備上運行時突然發現相機打不開、相冊訪問不了或者通知不彈了別急著懷疑是自己的代碼寫錯了。這大概率是撞上了Android 13API 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開發者的直接影響是什么目標APITarget 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權限應用安裝后默認就擁有發送通知的能力?,F在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 Dictionarystring, PermissionCallback pendingCallbacks new Dictionarystring, PermissionCallback(); // 權限與其中文解釋的映射用于向用戶展示 private Dictionarystring, string permissionRationaleMap new Dictionarystring, 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.GetStaticAndroidJavaObject(currentActivity)) using (var intent new AndroidJavaObject(android.content.Intent)) { string packageName currentActivity.Callstring(getPackageName); using (var uri new AndroidJavaObject(android.net.Uri)) { // Android 8.0 (API 26) 之后跳轉到應用信息頁面的Intent Action intent.CallAndroidJavaObject(setAction, android.settings.APPLICATION_DETAILS_SETTINGS); string uriString package: packageName; var uriObj uri.CallStaticAndroidJavaObject(parse, uriString); intent.CallAndroidJavaObject(setData, uriObj); intent.CallAndroidJavaObject(addCategory, android.intent.category.DEFAULT); intent.CallAndroidJavaObject(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.GetStaticint(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 version1.0 encodingutf-8? manifest xmlns:androidhttp://schemas.android.com/apk/res/android packagecom.yourcompany.yourgame android:installLocationpreferExternal !-- 傳統存儲權限 (用于 API 33 兼容) -- uses-permission android:nameandroid.permission.READ_EXTERNAL_STORAGE android:maxSdkVersion32 / !-- 注意WRITE_EXTERNAL_STORAGE 在 Android 10 對媒體文件作用有限通常不需要了 -- !-- Android 13 (API 33) 細粒度媒體權限 -- uses-permission android:nameandroid.permission.READ_MEDIA_IMAGES / uses-permission android:nameandroid.permission.READ_MEDIA_VIDEO / uses-permission android:nameandroid.permission.READ_MEDIA_AUDIO / !-- Android 13 (API 33) 通知權限 -- uses-permission android:nameandroid.permission.POST_NOTIFICATIONS / !-- 其他常用權限 -- uses-permission android:nameandroid.permission.CAMERA / uses-permission android:nameandroid.permission.RECORD_AUDIO / uses-permission android:nameandroid.permission.ACCESS_FINE_LOCATION / !-- 如果你的游戲需要保持屏幕常亮 -- uses-permission android:nameandroid.permission.WAKE_LOCK / application android:labelstring/app_name android:iconmipmap/app_icon android:themestyle/UnityThemeSelector android:usesCleartextTraffictrue !-- 其他Activity配置特別是UnityPlayerActivity -- activity android:namecom.unity3d.player.UnityPlayerActivity android:configChangesfontScale|keyboard|keyboardHidden|locale|mnc|mcc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|uiMode|touchscreen android:launchModesingleTask android:resizeableActivitytrue intent-filter action android:nameandroid.intent.action.MAIN / category android:nameandroid.intent.category.LAUNCHER / /intent-filter meta-data android:nameunityplayer.UnityActivity android:valuetrue / /activity /application /manifest關鍵說明android:maxSdkVersion32這個屬性用于READ_EXTERNAL_STORAGE表示此權限只對API 32Android 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的??梢赃m當增加協程超時時間并添加更詳細的日志。一個更穩健的方法是使用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框架、日志系統和異常處理機制對其進行增強。比如將權限請求與游戲內的任務系統結合或者與數據分析平臺打通記錄用戶拒絕權限的比率和場景從而持續優化你的權限請求策略最終的目標是讓權限請求成為流暢用戶體驗的一部分而不是令人反感的阻礙。