1. 安卓权限管理机制演进与Unity适配挑战
安卓6.0引入的动态权限系统彻底改变了应用获取敏感权限的方式。我记得第一次在Unity项目里调用相机功能时,明明在AndroidManifest.xml里声明了权限,却遭遇了闪退事故。后来才发现,像相机、存储这类危险权限(Dangerous Permissions)必须运行时动态申请。
动态权限的核心机制其实很简单:应用启动时只有基础权限,需要用户敏感数据时再弹窗申请。但Unity作为跨平台引擎,默认不处理这些原生系统特性,这就导致三个典型问题:
- 权限弹窗闪现消失:Unity的Activity会拦截系统弹窗
- Android 7.0文件共享崩溃:File Uri暴露路径被禁止
- Android 10分区存储:直接访问外部存储受限
这里有个容易忽略的细节:不同厂商ROM对权限弹窗的处理差异很大。比如小米会默认关闭某些权限的弹窗,需要引导用户去设置页手动开启。我在华为P30上测试时,就发现相机权限弹窗的样式和原生安卓有明显区别。
2. 基础权限配置与动态请求实战
2.1 清单文件声明
首先在Assets/Plugins/Android/AndroidManifest.xml中添加必要权限声明:
<uses-permission android:name="android.permission.CAMERA"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>注意Android 10+需要添加requestLegacyExternalStorage属性应对分区存储:
<application android:requestLegacyExternalStorage="true" ... >2.2 Unity中的动态权限请求
创建PermissionRequester.cs脚本处理动态逻辑:
#if UNITY_ANDROID using UnityEngine; using UnityEngine.Android; public class PermissionRequester : MonoBehaviour { const int CAMERA_CODE = 1001; const int STORAGE_CODE = 1002; public void RequestCameraPermission() { if (Permission.HasUserAuthorizedPermission(Permission.Camera)) { OpenCamera(); } else { var callbacks = new PermissionCallbacks(); callbacks.PermissionGranted += _ => OpenCamera(); Permission.RequestUserPermission(Permission.Camera, callbacks); } } public void RequestStoragePermission() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { // 6.0以下版本无需动态申请 AccessGallery(); return; } if (Permission.HasUserAuthorizedPermission(Permission.ExternalStorageRead)) { AccessGallery(); } else { var callbacks = new PermissionCallbacks(); callbacks.PermissionGranted += _ => AccessGallery(); Permission.RequestUserPermission(Permission.ExternalStorageRead, callbacks); } } void OpenCamera() { /* 相机逻辑 */ } void AccessGallery() { /* 相册逻辑 */ } } #endif这个方案比原生Android代码简洁很多,利用了Unity 2018.3引入的PermissionAPI。但要注意几个坑点:
- iOS需要额外处理
NSCameraUsageDescription描述 - 华为设备上可能需要检查
CanRequestPermission()返回值 - 用户拒绝后再次请求时需要解释必要性
3. Android 7.0+文件共享方案
3.1 FileProvider配置
在AndroidManifest.xml的<application>标签内添加:
<provider android:name="androidx.core.content.FileProvider" android:authorities="${applicationId}.fileprovider" android:exported="false" android:grantUriPermissions="true"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" /> </provider>创建Assets/Plugins/Android/res/xml/file_paths.xml:
<?xml version="1.0" encoding="utf-8"?> <paths> <external-path name="external_files" path="."/> <cache-path name="cache_files" path="."/> </paths>3.2 Unity与Android交互
修改相机调用代码处理版本差异:
void TakePhoto() { string path = Path.Combine(Application.persistentDataPath, "photo.jpg"); AndroidJavaObject intent = new AndroidJavaObject("android.content.Intent", "android.media.action.IMAGE_CAPTURE"); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { AndroidJavaClass fileProvider = new AndroidJavaClass("androidx.core.content.FileProvider"); AndroidJavaObject file = new AndroidJavaObject("java.io.File", path); AndroidJavaObject uri = fileProvider.CallStatic<AndroidJavaObject>( "getUriForFile", GetCurrentActivity(), Application.identifier + ".fileprovider", file); intent.Call<AndroidJavaObject>("putExtra", "android.media.action.IMAGE_CAPTURE", uri); } else { AndroidJavaObject uri = new AndroidJavaObject("android.net.Uri", "file://" + path); intent.Call<AndroidJavaObject>("putExtra", "android.media.action.IMAGE_CAPTURE", uri); } GetCurrentActivity().Call("startActivityForResult", intent, PHOTO_CODE); }这里有个实际项目中的经验:部分国产ROM会修改相机应用包名,建议先检查设备是否支持标准ACTION_IMAGE_CAPTURE:
bool IsCameraAvailable() { AndroidJavaObject packageManager = GetCurrentActivity() .Call<AndroidJavaObject>("getPackageManager"); AndroidJavaObject intent = new AndroidJavaObject( "android.content.Intent", "android.media.action.IMAGE_CAPTURE"); return intent.Call<bool>("resolveActivity", packageManager) != null; }4. 完整实现方案与优化建议
4.1 相册访问最佳实践
对于相册访问,推荐直接使用Intent.ACTION_GET_CONTENT避免文件路径问题:
void OpenGallery() { AndroidJavaObject intent = new AndroidJavaObject( "android.content.Intent", "android.intent.action.GET_CONTENT"); intent.Call<AndroidJavaObject>("setType", "image/*"); GetCurrentActivity().Call("startActivityForResult", intent, GALLERY_CODE); }处理返回结果时,建议使用内容解析器获取真实路径:
// 在Android插件代码中 public static String getRealPathFromUri(Context context, Uri uri) { String path = ""; if (context.getContentResolver() != null) { try (Cursor cursor = context.getContentResolver() .query(uri, null, null, null, null)) { if (cursor != null && cursor.moveToFirst()) { int idx = cursor.getColumnIndex(MediaStore.Images.Media.DATA); path = cursor.getString(idx); } } } return path; }4.2 性能优化技巧
纹理压缩:大图加载前先采样
Texture2D CompressTexture(Texture2D source, int maxSize) { int width = Mathf.Min(source.width, maxSize); int height = Mathf.Min(source.height, maxSize); var rt = RenderTexture.GetTemporary(width, height); Graphics.Blit(source, rt); var result = new Texture2D(width, height); RenderTexture.active = rt; result.ReadPixels(new Rect(0, 0, width, height), 0, 0); result.Apply(); RenderTexture.ReleaseTemporary(rt); return result; }异步加载:避免主线程卡顿
IEnumerator LoadImageAsync(string path) { using (UnityWebRequest request = UnityWebRequestTexture.GetTexture(path)) { yield return request.SendWebRequest(); if (request.result == UnityWebRequest.Result.Success) { Texture2D texture = DownloadHandlerTexture.GetContent(request); // 使用纹理... } } }缓存管理:使用
Application.persistentDataPath存储用户图片,定期清理缓存
4.3 厂商适配经验
小米设备:在
AndroidManifest.xml添加:<meta-data android:name="xiaomi.permission.CAMERA_USE_SYSTEM_UI" android:value="true"/>华为设备:检查
HuaweiApiAvailability.getInstance().isHuaweiMobileServicesAvailable()OPPO/Realme:需要单独申请
Settings.ACTION_MANAGE_OVERLAY_PERMISSION
在华为Mate40 Pro上测试时,发现相册返回的Uri格式与其他设备不同,需要额外处理content://com.huawei.hidisk.fileprovider/开头的路径。这种厂商差异在实际开发中很常见,建议准备主流设备的真机测试方案。