
1. 项目概述为什么Unity里“选图”和“存图”不能靠C#原生搞定做Unity移动端开发的朋友大概率都踩过这个坑在编辑器里写好Texture2D.LoadImage()、File.WriteAllBytes()测试一切正常一打包到安卓或iOS真机上点开相册选张图——没反应想把游戏截图保存到手机相册——提示“权限拒绝”或直接崩溃。这不是你代码写错了而是Unity的C#层根本不直接暴露操作系统级的媒体访问能力。它跑在虚拟机IL2CPP或Mono之上和Android的MediaStore、iOS的PHPhotoLibrary之间隔着整整一层系统沙盒隔离。NativeGallery这个插件本质上就是一座“桥”——它用原生代码Android端是Java/KotliniOS端是Objective-C/Swift在Unity底层打通了这条通路。它不处理图片解码、不负责UI渲染只干两件事调起系统相册选择器返回一个本地路径或字节数组以及把二进制数据安全写入系统相册目录并触发系统媒体扫描。这正是标题里“调用获取手机图片和下载图片功能”的真实含义不是Unity自己去“找图”或“存图”而是让Unity有能力“喊一声”让手机系统自己来执行。我最早在做一个AR涂鸦Demo时被逼着用上它。当时需要用户从相册选一张底图叠加AR特效后保存回相册。试过纯C#方案用Application.persistentDataPath硬存结果安卓10设备根本找不到文件也试过自己写JNI调用光是处理Android 6.0的运行时权限和Android 11的分区存储Scoped Storage就折腾掉三天最后发现NativeGallery已经把所有这些坑都填平了。它现在支持Android 4.1和iOS 8.0覆盖99%的活跃设备而且更新非常勤快——去年底刚适配完Android 13的媒体权限变更。关键词“Unity”“手机图片”“下载图片”“NativeGallery”全中靶心这不是一个玩具插件而是移动Unity开发的事实标准组件。2. 核心设计逻辑为什么它不做成“一键导入导出”而坚持“桥接”模式NativeGallery的设计哲学非常务实它坚决不做“全能管家”只做“精准信使”。很多新手第一次看文档会疑惑“为什么选图后只返回一个路径字符串而不是直接给我一个Texture2D” 这恰恰是它最聪明的地方。我们来拆解三层逻辑2.1 系统权限与沙盒的不可绕过性Android从6.0开始强制要求运行时权限iOS从10开始收紧相册访问控制。如果插件内部自动帮你申请权限、自动加载图片那它就必须在AndroidManifest.xml里声明所有可能用到的权限比如READ_EXTERNAL_STORAGE、WRITE_EXTERNAL_STORAGE而这些权限在Google Play审核中会被严格审查。NativeGallery选择“最小权限原则”它只在你明确调用NativeGallery.GetImageFromGallery()时才动态申请READ_MEDIA_IMAGESAndroid 12或PHPhotoLibrary授权iOS用完即弃。你作为开发者完全掌控权限申请时机和失败降级逻辑——比如用户拒绝后弹出友好提示而不是让插件替你做决定。2.2 图片加载策略必须由业务层决定返回路径而非Texture2D给了你三重自由内存控制权一张4000×3000的JPEG解码成RGBA32纹理内存占用直奔48MB。如果你只是要获取缩略图做预览完全可以先用WWW或UnityWebRequest加载低分辨率版本线程安全Texture2D.LoadImage()必须在主线程调用而NativeGallery.GetImageFromGallery()是异步回调。如果插件内部强行加载就会卡住主线程导致UI冻结格式兼容性用户可能选HEICiOS、WebP安卓、甚至RAW格式。LoadImage()对HEIC支持极差而NativeGallery只管传路径后续用StbImageSharp或ImageSharp等专业库处理才是正解。2.3 “下载图片”本质是媒体库注册不是文件复制标题里的“下载图片”在移动端语境下其实是“保存到相册并被系统识别”。NativeGallery的SaveImageToGallery()方法背后Android端调用的是MediaStore.Images.Media.insertImage()将Bitmap插入媒体数据库iOS端调用PHPhotoLibrary.shared().performChanges()创建PHAsset。它不简单地把文件cp到DCIM目录——因为那样系统相册App根本看不到新图片。这种“注册式保存”才是用户真正需要的“下载”体验。提示NativeGallery不处理网络图片下载。如果你的需求是“从URL下载图片再存相册”必须分两步先用UnityWebRequest.Get()拉取二进制数据再传给SaveImageToGallery()。插件名里的“Gallery”已暗示其边界——它只管相册不管网络。3. 实操全流程从零配置到真机验证的每一步细节下面以Unity 2021.3 LTS Android真机为例手把手带你走通完整链路。iOS流程逻辑一致仅权限配置和Xcode设置不同我会在关键节点标注差异。3.1 插件导入与基础配置第一步永远是“清空缓存再导入”。NativeGallery最新版v3.4.0已支持URP/HDRP但如果你项目还在使用Built-in RP务必删除旧版残留删除Assets/Plugins/Android下所有.jar或.aar文件删除Assets/Plugins/iOS下所有.m、.h文件清理Library/ScriptAssemblies菜单栏Assets → Reimport All。然后从GitHub Release页下载NativeGallery.unitypackage双击导入。导入后你会看到Assets/Plugins/Android/nativegallery.jar核心Android库Assets/Plugins/iOS/NativeGallery.mmiOS原生实现Assets/Scripts/NativeGallery.csC#桥接脚本Assets/Examples/官方示例场景注意不要手动修改AndroidManifest.xmlNativeGallery通过[PostProcessBuild]属性自动注入权限声明。如果你项目已有自定义AndroidManifest需在application节点内添加provider 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并确保res/xml/file_paths.xml存在插件会自动生成。3.2 权限申请与运行时校验NativeGallery不自动申请权限你必须显式调用。这是最佳实践也是避免审核风险的关键。在调用选图前插入权限检查// 检查是否已有相册读取权限 if (!NativeGallery.IsMediaPickerAvailable()) { Debug.LogError(设备不支持媒体选择器如老旧Android 4.1); return; } // Android 6.0 需要运行时权限 if (Application.platform RuntimePlatform.Android !Permission.HasUserAuthorizedPermission(Permission.ExternalStorageRead)) { Permission.RequestUserPermission(Permission.ExternalStorageRead); // 注意RequestUserPermission是异步的需等待回调 return; } // iOS需检查PHPhotoLibrary授权状态 if (Application.platform RuntimePlatform.IPhonePlayer) { var authStatus NativeGallery.GetPhotosPermission(); if (authStatus NativeGallery.PermissionStatus.Denied || authStatus NativeGallery.PermissionStatus.Restricted) { Debug.Log(iOS相册权限被拒绝请前往设置开启); return; } }这里有个易错点Permission.RequestUserPermission()在Unity 2021中返回Permission.PermissionResult枚举但它不会立即返回结果。你必须监听Permission.permissionCallbacks事件或者用协程轮询IEnumerator RequestPermissionAndWait() { Permission.RequestUserPermission(Permission.ExternalStorageRead); while (Permission.GetStatus(Permission.ExternalStorageRead) Permission.PermissionStatus.Unknown) yield return null; if (Permission.GetStatus(Permission.ExternalStorageRead) Permission.PermissionStatus.Granted) OpenGallery(); else ShowPermissionDeniedDialog(); }3.3 调用相册选择器路径、尺寸与回调陷阱调用GetImageFromGallery()时参数设计直接影响用户体验// 最简调用无压缩返回原始路径 string imagePath NativeGallery.GetImageFromGallery((path) { if (path ! null) { Debug.Log(选中的图片路径 path); // 此处加载图片 LoadImageAtPath(path); } else { Debug.Log(用户取消选择); } }); // 推荐调用指定最大宽度/高度避免大图OOM string imagePath NativeGallery.GetImageFromGallery((path) { if (path ! null) { // 加载前先获取图片尺寸信息NativeGallery提供工具方法 long fileSize new FileInfo(path).Length; Debug.Log($图片大小{fileSize / 1024} KB); LoadImageAtPath(path, maxWidth: 1920, maxHeight: 1080); // 自动缩放 } }, 请选择一张图片, image/*); // MIME类型过滤关键细节解析回调时机Android端回调在主线程iOS端在主线程无需MainThreadDispatcher路径可靠性返回的path在Android上是/storage/emulated/0/...格式在iOS上是file:///var/mobile/...。绝对不要用System.IO.File.Exists(path)校验——iOS沙盒路径无法被C#直接访问应改用NativeGallery.IsPathValid(path)MIME类型image/*匹配所有图片image/jpeg只匹配JPEG。实测发现某些国产ROM如MIUI对image/*支持不稳定建议明确指定image/jpeg,image/png中文标题请选择一张图片在Android 12上可能显示为方块需确保AndroidManifest.xml中application节点有android:supportsRtltrue。3.4 加载图片的三种安全姿势拿到路径后加载方式决定成败。以下是经过千次真机测试的可靠方案方案AUnity原生加载适合小图/快速预览void LoadImageAtPath(string path, int maxWidth 0, int maxHeight 0) { if (!NativeGallery.IsPathValid(path)) return; byte[] bytes File.ReadAllBytes(path); Texture2D tex new Texture2D(2, 2); if (tex.LoadImage(bytes)) // 自动识别格式 { // 可选按需缩放 if (maxWidth 0 maxHeight 0) { tex ResizeTexture(tex, maxWidth, maxHeight); } // 应用到UI或材质 rawImage.texture tex; } }方案B异步流式加载防卡顿推荐IEnumerator LoadImageAsync(string path, RawImage target) { using (UnityWebRequest www UnityWebRequestTexture.GetTexture(file:// path)) { yield return www.SendWebRequest(); if (www.result UnityWebRequest.Result.Success) { Texture2D tex DownloadHandlerTexture.GetContent(www); target.texture tex; } } }方案C专业图像库加载处理HEIC/WebP/RAW// 需引入ImageSharp包NuGet using SixLabors.ImageSharp; using SixLabors.ImageSharp.Formats.Jpeg; async TaskTexture2D LoadWithImageSharp(string path) { using (var image await Image.LoadAsync(path)) { // 转换为RGBA格式 image.Mutate(x x.AutoOrient()); // 修正EXIF方向 var rgbaBytes image.CloneAsRgba32().Encode(new JpegEncoder()).ToArray(); Texture2D tex new Texture2D(2, 2); tex.LoadImage(rgbaBytes); return tex; } }实操心得在小米12Android 12上测试发现File.ReadAllBytes()对HEIC格式会抛NotSupportedException但UnityWebRequestTexture.GetTexture()能正确加载。所以优先用方案B除非你需要像素级操作如滤镜再上方案C。3.5 保存图片到相册命名、格式与元数据SaveImageToGallery()的签名看似简单但参数组合影响最终效果// 基础保存自动生成文件名 string savePath NativeGallery.SaveImageToGallery( screenshotBytes, MyGame, screenshot.png ); // 推荐保存指定JPEG质量添加EXIF时间戳 string savePath NativeGallery.SaveImageToGallery( screenshotBytes, MyGame, $screenshot_{DateTime.Now:yyyyMMdd_HHmmss}.jpg, 0.9f // JPEG质量0.0~1.0 );深度解析文件夹名MyGameAndroid端会在/DCIM/MyGame/下创建目录iOS端会归入“最近项目”相册无法指定子相册文件名规则iOS对文件名长度敏感超过255字符会截断。实测screenshot_20231015_143022.jpg完全安全JPEG质量参数设为0.9f时4000×3000图约存为2.1MB视觉无损设为0.7f则压至850KB适合流量敏感场景EXIF写入NativeGallery会自动写入拍摄时间当前时间、设备型号Unity Game、软件NativeGallery v3.4.0。若需自定义需改写原生代码——但99%场景无需。保存成功后savePath返回的是系统媒体库中的URI如content://media/external/images/media/12345不是文件路径。这意味着你无法用File.Exists()检查它但可以用NativeGallery.IsPathValid(savePath)确认有效性在Android上此URI可直接用于UnityWebRequest加载缩略图。4. 真机避坑指南那些文档没写的血泪经验NativeGallery文档写得清晰但真机环境千奇百怪。以下是我在5款安卓旗舰、3代iPhone、2个鸿蒙设备上踩过的坑附带解决方案。4.1 安卓权限崩溃Target SDK 33的致命陷阱现象Unity 2022.3打包targetSdkVersion33调用GetImageFromGallery()直接闪退Logcat报java.lang.SecurityException: getDataNetworkTypeForSubscriber。原因Android 13强制要求即使你不用网络只要targetSdkVersion33就必须在AndroidManifest.xml中声明android.permission.READ_MEDIA_IMAGES且不能只在uses-permission中声明必须同时在application内添加uses-permission-sdk-23。解决方案!-- 在AndroidManifest.xml的application节点内添加 -- uses-permission-sdk-23 android:nameandroid.permission.READ_MEDIA_IMAGES / uses-permission-sdk-23 android:nameandroid.permission.READ_MEDIA_VIDEO /注意uses-permission-sdk-23是Android Gradle Plugin 7.2新增标签旧版AGP会报错。升级AGP或改用uses-permission android:nameandroid.permission.READ_MEDIA_IMAGES tools:nodereplace/需加xmlns:toolshttp://schemas.android.com/tools。4.2 iOS相册黑屏PHPhotoLibrary未初始化现象iPhone 14iOS 16.5首次调用GetImageFromGallery()相册界面全黑点击无响应。原因iOS 15要求PHPhotoLibrary必须在主线程初始化而Unity的Awake()可能不在主线程。解决方案在Start()中强制初始化void Start() { #if UNITY_IOS // 强制触发PHPhotoLibrary初始化 NativeGallery.GetPhotosPermission(); #endif }4.3 华为/荣耀设备白屏EMUI的私有相册拦截现象华为Mate 40EMUI 12打开相册后显示空白返回时pathnull。原因华为相册App会拦截第三方调用需在AndroidManifest.xml中添加华为特有配置application !-- 华为相册兼容 -- meta-data android:namecom.huawei.hms.version android:value6.10.0.300 / /application同时调用前增加华为设备检测if (SystemInfo.deviceModel.Contains(HUAWEI) || SystemInfo.deviceModel.Contains(HONOR)) { // 华为设备使用备用方案先保存临时文件再用Intent调起 string tempPath Path.Combine(Application.temporaryCachePath, temp.jpg); File.WriteAllBytes(tempPath, screenshotBytes); NativeGallery.GetImageFromGallery(null, image/*, tempPath); }4.4 小米/OPPO后台杀进程回调丢失现象小米13MIUI 14后台运行游戏调用相册后切到其他App返回时pathnull。原因MIUI的“神隐模式”会杀死非白名单App的后台进程导致Unity主线程被杀回调无法执行。解决方案在AndroidManifest.xml中添加小米白名单声明application meta-data android:namecom.xiaomi.mipush.sdk.USE_XIAOMI_PUSH android:valuetrue / /application并在Unity C#中调用前启动前台服务需额外插件// 使用AndroidJavaObject调用小米SDK using (var activity new AndroidJavaClass(com.unity3d.player.UnityPlayer).GetStaticAndroidJavaObject(currentActivity)) { activity.Call(startForegroundService, ...); }4.5 通用问题速查表问题现象根本原因解决方案GetImageFromGallery()返回null无错误日志用户取消操作或系统相册未安装检查NativeGallery.IsMediaPickerAvailable()提供“手动选择文件”备选入口保存图片后相册App不显示新图Android媒体扫描未触发调用NativeGallery.ScanMediaFile(savePath)强制扫描iOS保存后图片方向错误EXIF Orientation未处理加载时用Texture2D的Apply(true)或ImageSharp的AutoOrient()多次调用后内存泄漏Texture2D未Destroy()所有new Texture2D()后必须Destroy(tex)或用RenderTexture替代鸿蒙设备不兼容HarmonyOS 3.0使用ohos.permission.MEDIA_LOCATION暂不支持需联系插件作者或改用鸿蒙原生API实操心得每次发版前我必做“三机验证”一台Pixel原生Android、一台小米主流国产ROM、一台iPhoneiOS最新版。NativeGallery的稳定性在于它把所有平台差异封装在原生层但你必须为每个平台准备Plan B——比如华为设备用临时文件IntentiOS用PHPhotoLibrary直接写入这才是工业级项目的正确姿势。5. 进阶技巧超越基础功能的实战延伸NativeGallery的潜力远不止“选图存图”。结合Unity生态你能解锁更多实用场景。5.1 批量图片选择绕过系统限制的变通方案Android原生相册选择器默认只允许单选但用户常需一次选多张。NativeGallery本身不支持但我们能用“多次调用状态管理”模拟public class MultiImageSelector : MonoBehaviour { public Liststring selectedPaths new Liststring(); private int maxSelectCount 5; public void OpenMultiSelect() { if (selectedPaths.Count maxSelectCount) { Debug.Log($已达上限{maxSelectCount}张); return; } NativeGallery.GetImageFromGallery(OnImageSelected); } void OnImageSelected(string path) { if (path ! null !selectedPaths.Contains(path)) { selectedPaths.Add(path); Debug.Log($已选{selectedPaths.Count}/{maxSelectCount}张); if (selectedPaths.Count maxSelectCount) OpenMultiSelect(); // 递归调用 } } }注意iOS的PHPickerViewController原生支持多选但NativeGallery未暴露该API。如需真多选需自行扩展iOS原生代码替换NativeGallery.mm中的showImagePicker方法。5.2 截图自动保存游戏内快捷键绑定玩家按F12截图并自动存相册提升分享体验void Update() { if (Input.GetKeyDown(KeyCode.F12)) { StartCoroutine(AutoSaveScreenshot()); } } IEnumerator AutoSaveScreenshot() { yield return new WaitForEndOfFrame(); Texture2D screenShot new Texture2D(Screen.width, Screen.height, TextureFormat.RGB24, false); screenShot.ReadPixels(new Rect(0, 0, Screen.width, Screen.height), 0, 0); screenShot.Apply(); byte[] bytes screenShot.EncodeToJPG(0.9f); string fileName $screenshot_{DateTime.Now:yyyyMMdd_HHmmss}.jpg; string savePath NativeGallery.SaveImageToGallery(bytes, MyGame, fileName); Debug.Log($截图已保存至{savePath}); Destroy(screenShot); // 立即释放内存 }5.3 相册内容预览不用打开系统相册想在游戏内展示用户相册缩略图NativeGallery提供GetImagesFromGallery()需付费版或用Android原生MediaStore查询// Android专用查询最近10张图片 #if UNITY_ANDROID string query content://media/external/images/media; string[] projection { _id, _data, date_added, width, height }; string sortOrder date_added DESC LIMIT 10; using (var cursor AndroidJavaObject(android.database.Cursor, ...)) { while (cursor.Callbool(moveToNext)) { string path cursor.Callstring(getString, cursor.Callint(getColumnIndex, _data)); // 加载缩略图 LoadThumbnail(path, 200, 200); } } #endif5.4 权限引导优化从“弹窗轰炸”到“场景化教育”粗暴的权限请求会导致30%用户直接拒绝。更优方案是“场景前置教育”在用户点击“从相册导入”按钮前先显示半透明浮层“需要访问您的照片来选取底图这是系统级权限我们不会上传任何图片”若用户拒绝下次进入时展示“设置教程GIF”手指滑动演示如何在系统设置中开启权限对连续两次拒绝的用户提供“手动文件导入”降级方案用NativeGallery.OpenFilePicker()。我在某教育类App中实施该策略后相册权限接受率从42%提升至79%。技术是手段用户体验才是目标。6. 替代方案对比什么情况下不该用NativeGallery没有银弹。NativeGallery虽强但以下场景建议换方案6.1 纯WebGL项目NativeGallery依赖原生代码WebGL构建会直接报错。此时应用HTML5input typefile acceptimage/*FileReader或接入云存储SDK如Firebase Storage让用户上传到云端再下载。6.2 需要深度图片编辑如果需求是“选图→裁剪→滤镜→保存”NativeGallery只解决第一步。应集成AndroiduCrop库需JNI桥接iOSTOCropViewController需Objective-C混编跨平台Unity-Image-Editor基于SkiaSharp。6.3 企业级合规要求金融/医疗类App常需图片不落地全程内存处理保存前加密AES-256记录完整操作审计日志。此时应弃用所有第三方插件用AndroidJavaObject直调MediaStore或接入企业级MAMMobile Application ManagementSDK。6.4 极简轻量需求如果项目只需“保存截图”且不需相册选择可用更轻量方案Application.CaptureScreenshot()File.WriteAllBytes()存到Application.persistentDataPath再用NativeGallery.ScanMediaFile()通知系统扫描。体积减少85%适合超轻量小游戏。最后分享一个小技巧NativeGallery的源码完全开源MIT协议当你遇到定制化需求如华为相册深度适配直接fork仓库修改NativeGallery.java或NativeGallery.mm比写新插件快十倍。我维护的某个AR项目就在原生层增加了“选图后自动旋转90度”的逻辑三小时搞定比提issue等作者回复快得多。工具的价值永远在于你能否掌控它。