资讯详情

Unity iOS原生相机相册桥接插件实战:从DllImport到UnitySendMessage

📅 2026/10/6 13:34:14 | 华诺云谱 👁 阅读
Unity iOS原生相机相册桥接插件实战:从DllImport到UnitySendMessage
做移动端的Unity项目十来个项目里总有一半绕不开这个需求在iOS上让玩家拍张照或者从相册挑一张图回传到游戏里。我以前每次接到这个需求第一反应都是打开Unity自带的WebCamTexture结果每次都被现实打脸——它只能拿来做实时预览画面系统相机的快门、闪光灯、切换摄像头那套UI一样都调不出来相册选择就更不用提。后来才明白想在iOS上真正调起系统相机和相册唯一靠谱的路只有一条Unity与原生交互自己写桥接插件。这篇文章我就把在真实项目中沉淀下来的完整方案写出来。不扯虚的直接上代码和配置适合用过Unity、熟悉C#但iOS原生刚起步的开发者也适合那种“需求周一就提、周五就要包”的紧张场景。你最终能拿到一套从Unity导出、到Xcode真机运行都能跑通的调用流程外加我踩了无数坑换来的经验。1. 项目场景与交互原理Unity到底怎么和iOS原生搭上话1.1 踩坑起点为什么WebCamTexture连系统相机界面都弹不出来很多Unity开发者第一次做拍照需求时直觉会去找相机相关的封装。WebCamTexture确实能唤起摄像头权限接着把画面渲染到一个RawImage上看起来也能拍照但它的定位是“视频预览数据流”不是“相机应用”。它没有系统快门按钮没有对焦动画没有闪光灯开关也不能让用户从相册里选一张已经存在的图。如果你要在UIGameObject上做一个快门按钮、再截取当前帧保存体验上和真正的系统相机相差甚远而且截图时的旋转、对焦、图片质量都很麻烦。另一个常见误区是直接联网搜一个“打包好的iOS相机插件”塞进工程里。第三方插件确实能快速跑通但项目一复杂问题就来了插件内部封装的黑盒逻辑没法和业务深度耦合比如有些需求要求扫描身份证件后自动裁切有些要求拍完照后贴滤镜这时候改插件源码的成本往往比自己写一套还高。最终我选择直接在iOS原生侧写一个.mm桥接文件通过Unity与iOS原生交互的标准链路打通所有逻辑都在自己手里出问题时能一行行跟。1.2 Unity与iOS原生互调的三件套extern C、DllImport、UnitySendMessageUnity与iOS原生交互的核心其实就三样东西理解了这三样整个桥接逻辑就有了骨架。第一样是原生侧用extern C导出一批全局函数。因为iOS工程的Unity部分编译后是Objective-C如果我直接在.mm文件里写一个Objective-C类方法Unity的C#侧没法直接找到它的符号。用extern C包裹函数定义等于告诉编译器“这个函数按C语言规则导出符号”这样符号是全局且唯一的Unity执行引擎才能在运行时靠函数名定位到它。第二样是C#侧的[DllImport(__Internal)]。在Windows或Android平台上DllImport通常指向某个具体动态库但在iOS上iOS不允许运行时动态加载自定义DLL所有插件代码在编译时就已经静态链接进了可执行文件。__Internal这个特殊名字就是明确告诉Unity“别去外部找库了直接在当前可执行文件里按函数名找符号。”两者一结合C#就能像调用普通静态方法一样调用原生函数。第三样是原生往Unity侧发消息的回调函数——UnitySendMessage。原生代码处理完相机或相册后需要把图片路径、取消状态这类信息传回Unity层。UnitySendMessage(const char *obj, const char *method, const char *msg)正是干这件事的第一个参数是Unity场景中的GameObject名字第二个参数是挂在它身上的方法名第三个参数是消息内容。Unity拿到消息后会在主线程查找对应对象并执行方法。这三样组合起来就是我项目里最稳定的架构方案C#侧声明原生函数并暴露给业务层调用原生侧负责弹系统UI、处理图片、再把结果通过UnitySendMessage回传给一个常驻的C#管理对象。2. iOS原生侧插件实现从弹窗到把图片传回来2.1 把Objective-C桥接文件塞进Unity工程在Unity工程里只要把.mm、.m、.h等文件放进Assets/Plugins/iOS目录Unity导出Xcode工程时会自动把这些文件加入原生工程并参与编译。这叫“iOS原生插件的自动识别机制”也是我推荐的做法因为你不需要每次导出Xcode后手动去添加文件不容易漏。我在项目中建了一个文件叫UnityNativeBridge.mm。为什么用.mm而不是.m因为Unity导出后的工程本身是Objective-C环境而且很多时候我需要混用C代码.mm文件可以同时编译Objective-C和C代码兼容性最好。文件顶部要导入必要的系统头#import UIKit/UIKit.h #import AVFoundation/AVFoundation.h #import Photos/Photos.h extern C void UnitySendMessage(const char *obj, const char *method, const char *msg); extern C UIViewController *UnityGetGLViewController(void);这里没有引入UnityFramework/UnityFramework.h之类的头文件而是直接声明了UnitySendMessage和UnityGetGLViewController两个全局C函数。为什么敢这么写因为Unity导出Xcode工程时这两个函数的符号一定存在于最终可执行文件里直接声明就能链接上不需要引入一堆Unity内部头文件也避免了不同Unity版本头文件路径不确定的坑。实际测试在Unity 2019到2022的版本中都正常。有一个大坑必须提前说Assets/Plugins/iOS下的文件每次重新导出工程都会被Unity整体重新生成如果你在Xcode里手改了原生代码一重新导出就会被覆盖。所以原生代码要维护在Unity工程里而不是维护在Xcode工程里。我用Unity自带的Jenkins打包脚本每次构建前会把Assets/Plugins/iOS下的代码和版本号一起打进去保证代码来源唯一。2.2 用UIImagePickerController弹出系统相机和相册弹系统相机和相册iOS上最直接的办法是UIImagePickerController。它就是一个现成的系统UI控制器只要指定sourceType就能分别弹出带快门界面的相机或带列表的相册。我在原生侧给C#暴露了两个函数_iOS_OpenCamera和_iOS_OpenPhotoLibrary。两者的实现非常相似但要注意几个关键细节。主线程问题。UIKit的所有UI操作都必须在主线程执行。C#侧调用原生函数的时机不固定所以原生函数内部统一使用dispatch_async(dispatch_get_main_queue(), ^{ ... })把UI弹出动作切到主线程。我第一次写的时候忽略了这个问题结果偶尔出现相机界面卡在半屏甚至直接崩溃后来加上主线程调度就好了。当前控制器的获取。弹窗需要一个present的父控制器我用UnityGetGLViewController()获取Unity当前最顶层控制器。这个函数在Unity导出工程中全局可用拿到的就是Unity根视图控制器比通过[[[[UIApplication sharedApplication] delegate] window] rootViewController]这种链式查找更稳至少在我验证过的Unity 2019.3之后的版本中都很可靠。相机源不可用的判断。模拟器上UIImagePickerControllerSourceTypeCamera不可用直接设置会抛异常。原生侧必须先通过[UIImagePickerController isSourceTypeAvailable:]检查extern C void _iOS_OpenCamera() { dispatch_async(dispatch_get_main_queue(), ^{ if (![UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) { UnitySendMessage(NativeBridge, OnSourceUnavailable, camera); return; } if (!s_cameraDelegate) { s_cameraDelegate [[NativeCameraDelegate alloc] init]; } UIImagePickerController *picker [[UIImagePickerController alloc] init]; picker.sourceType UIImagePickerControllerSourceTypeCamera; picker.delegate s_cameraDelegate; UIViewController *root UnityGetGLViewController(); [root presentViewController:picker animated:YES completion:nil]; }); } extern C void _iOS_OpenPhotoLibrary() { dispatch_async(dispatch_get_main_queue(), ^{ if (!s_cameraDelegate) { s_cameraDelegate [[NativeCameraDelegate alloc] init]; } UIImagePickerController *picker [[UIImagePickerController alloc] init]; picker.sourceType UIImagePickerControllerSourceTypePhotoLibrary; picker.delegate s_cameraDelegate; UIViewController *root UnityGetGLViewController(); [root presentViewController:picker animated:YES completion:nil]; }); }注意我写了一个静态变量s_cameraDelegate来强引用delegate对象。这很重要UIImagePickerController.delegate是弱引用如果你只是局部创建了一个delegate对象弹窗还在系统界面层时局部对象可能已经被释放用户选完图后回调会过期表现为Unity侧收不到任何消息。这个静态持有会让delegate在整个应用生命周期内都有效虽然有点“粗”但在相机/相册这类低频交互场景下非常实用。2.3 图片处理方向纠正、压缩与临时目录落盘用户选完照片后UIImagePickerControllerDelegate会走didFinishPickingMediaWithInfo回调此时拿到的UIImage是原始图片对象。直接把这个UIImage用UIImageJPEGRepresentation导出成JPEG再通过回调传给Unity是最快的方案但里面有几个坑需要处理。方向问题。iOS相机拍出的照片在UIImage里经常自带一个imageOrientation属性表示图片的EXIF方向。虽然直接显示时UIKit会自动处理方向但如果我们把这张图压缩、落盘或做像素级操作方向就可能错乱。我在delegate里先做一次方向修复用重绘方式把图片统一成UIImageOrientationUp- (UIImage *)fixOrientation:(UIImage *)image { if (image.imageOrientation UIImageOrientationUp) { return image; } UIGraphicsBeginImageContextWithOptions(image.size, NO, image.scale); [image drawInRect:CGRectMake(0, 0, image.size.width, image.size.height)]; UIImage *normalizedImage UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return normalizedImage; }尺寸压缩。现在的iPhone主摄像头的原图动辄三四千万像素直接把原图传回Unity做纹理会占掉上百MB显存和内存很容易导致闪退。常规做法是限制最大边长。我在原生侧写了一个缩放函数把最长边限制在2048像素以内。这个值能覆盖绝大多数的UI展示和人像上传需求同时把内存压力控制在一个非常合理的范围内。- (UIImage *)scaleImage:(UIImage *)image maxPixel:(CGFloat)maxPixel { CGFloat width image.size.width; CGFloat height image.size.height; CGFloat maxSide MAX(width, height); if (maxSide maxPixel) { return image; } CGFloat scale maxPixel / maxSide; CGSize newSize CGSizeMake(width * scale, height * scale); UIGraphicsBeginImageContextWithOptions(newSize, NO, 1.0); [image drawInRect:CGRectMake(0, 0, newSize.width, newSize.height)]; UIImage *scaledImage UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return scaledImage; }落盘到临时目录。为什么不把图片数据直接通过UnitySendMessage传回去因为UnitySendMessage的参数是const char *意味着我们要先把NSData转成Base64字符串再在Unity侧解码。一张图片的Base64字符串体积会膨胀三分之一跨语言传递大字符串还会增加不必要的内存拷贝。最省事的方式是把图片写到本地临时目录只把文件路径传回UnityUnity侧自己去读文件。临时文件我统一写到NSCachesDirectory因为缓存目录不会被系统自动清理规则视为“用户数据”而且系统StoreKit、iCloud备份也不会管它。文件名用时间戳拼上随机数避免重复拍照时互相覆盖NSData *imageData UIImageJPEGRepresentation(image, 0.8); NSArray *paths NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); NSString *cachePath [paths firstObject]; NSString *fileName [NSString stringWithFormat:unity_image_%lld.jpg, (long long)[[NSDate date] timeIntervalSince1970]]; NSString *filePath [cachePath stringByAppendingPathComponent:fileName]; [imageData writeToFile:filePath atomically:YES];文件写好后在dismiss完成的completion回调里把路径传给Unity这样能确保系统UI已经完全收起Unity侧界面已经恢复到可交互状态[picker dismissViewControllerAnimated:YES completion:^{ UnitySendMessage(NativeBridge, OnImageSelected, [filePath UTF8String]); }];用户取消选择时也要处理避免Unity侧一直卡在等待状态- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker { [picker dismissViewControllerAnimated:YES completion:^{ UnitySendMessage(NativeBridge, OnImageCanceled, ); }]; }顺便说一句UIImagePickerControllerDelegate的方法名在不同iOS版本里是一致的UIImagePickerControllerInfoKey在iOS 11以后的SDK中叫这个名字老系统是UIImagePickerControllerOriginalImage字符串常量。我的工程最低支持iOS 12所以直接用新API没问题。3. Unity侧接收与图片回显DllImport与回调完整代码3.1 C#声明原生函数与回调方法原生函数导出后C#侧用[DllImport(__Internal)]声明。我在项目中单独写了一个NativeBridge.cs挂在名为“NativeBridge”的GameObject上这个GameObject常驻场景用DontDestroyOnLoad保护避免场景切换时被销毁导致回调丢失。using System; using System.IO; using System.Collections; using System.Runtime.InteropServices; using UnityEngine; using UnityEngine.UI; public class NativeBridge : MonoBehaviour { [DllImport(__Internal)] private static extern void _iOS_OpenCamera(); [DllImport(__Internal)] private static extern void _iOS_OpenPhotoLibrary(); [DllImport(__Internal)] private static extern int _iOS_HasCameraPermission(); [DllImport(__Internal)] private static extern int _iOS_HasPhotoPermission(); }调用原生函数的入口我习惯做一层封装方便后续加权限检查、日志和Editor模拟public void OpenCamera() { #if UNITY_IOS !UNITY_EDITOR _iOS_OpenCamera(); #else Debug.LogWarning(当前只在iOS真机支持调用系统相机); #endif } public void OpenPhotoLibrary() { #if UNITY_IOS !UNITY_EDITOR _iOS_OpenPhotoLibrary(); #else Debug.Log(编辑器环境不调用原生相册); #endif }回调方法名必须和UnitySendMessage里传的名字严格一致。原生侧传了NativeBridge这个对象名OnImageSelected和OnImageCanceled这两个方法名C#侧就要对应实现public void OnImageSelected(string path) { StartCoroutine(LoadImageAndShow(path)); } public void OnImageCanceled(string empty) { Debug.Log(用户取消了拍照/选图); } public void OnSourceUnavailable(string source) { Debug.LogError(当前设备不支持该功能: source); }3.2 读取图片文件并转换为Texture2DOnImageSelected收到的是原生侧写好的临时文件路径接下来要读取图片并转成Unity纹理。我这里使用File.ReadAllBytes加Texture2D.LoadImage的组合这个组合的好处是代码量少、依赖少而且在图片已经被原生侧限制到2048像素以内后同步解码的性能损失完全可接受。private IEnumerator LoadImageAndShow(string path) { if (string.IsNullOrEmpty(path) || !File.Exists(path)) { Debug.LogError(图片路径无效: path); yield break; } byte[] bytes File.ReadAllBytes(path); Texture2D texture new Texture2D(2, 2, TextureFormat.RGBA32, false); if (!texture.LoadImage(bytes)) { Debug.LogError(纹理加载失败: path); Destroy(texture); yield break; } if (previewImage ! null) { var oldTexture previewImage.texture; previewImage.texture texture; if (oldTexture ! null) { Destroy(oldTexture); } } else { Destroy(texture); } yield return null; }这里有一个细节处理完纹理显示后要主动销毁RawImage上旧的Texture对象否则反复拍照选图会造成内存只增不减。我见过不少项目在这块漏掉连续操作十几张图之后内存直接翻倍后续操作卡成幻灯片。Texture2D.LoadImage会自动处理JPEG的解析而且它有一个隐含的行为对带有方向信息的JPEG会尽量按正确的方向加载。不过既然原生侧已经统一做了方向修复这个依赖关系就不那么重要了。如果你在某个版本里发现图片方向仍然不对排查顺序是先确认fixOrientation有没有执行再确认PNG/JPEG格式不要直接怀疑Texture2D的加载逻辑。3.3 常驻管理对象与Editor兼容处理为了让UnitySendMessage能稳定找到回调对象我在场景初始化时创建了一个专门的GameObjectprivate static GameObject _bridgeInstance; [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)] private static void CreateBridge() { if (_bridgeInstance ! null) return; _bridgeInstance new GameObject(NativeBridge); _bridgeInstance.AddComponentNativeBridge(); DontDestroyOnLoad(_bridgeInstance); }这样哪怕场景里忘记手动摆放这个对象运行时也会自动创建而且不随场景切换销毁。编辑器兼容方面我建议写一套Editor模拟接口。在UNITY_EDITOR下可以用UnityEditor.EditorUtility.OpenFilePanel模拟相册选择用UnityWebRequestTexture.GetTexture加载本地图片。注意iPhone上UnitySendMessage的回调只会命中真实的对象方法编辑器下原生代码不会执行。所以在Editor模式里OpenPhotoLibrary方法要单独处理private IEnumerator LoadImageFromEditor(string filePath) { yield return null; // 读取本地文件并用Texture2D.LoadImage加载显示到UI }这样你在没有真机的时候也能把UI流程、按钮交互、加载纹理的代码先跑通最后阶段再集中处理原生部分排期压力会小很多。4. 权限配置与真机发布Info.plist、开发者模式和签名4.1 Info.plist权限声明与文案心机iOS对相机和相册的隐私要求非常严格。UIImagePickerController在弹系统相机时如果应用没有在Info.plist里声明NSCameraUsageDescription系统会直接杀死App。相册读取对应NSPhotoLibraryUsageDescription如果后续还有“把图片保存到相册”的需求要再加NSPhotoLibraryAddUsageDescription。Unity导出工程时支持直接在Player Settings里配置这些字段。路径是Project Settings - Player - Other Settings - Camera Usage Description和Photo Usage Description。只要你在这里填了文案导出Xcode工程后Info.plist会自动带上对应key。我建议所有人都用这种方式不要每次去Xcode里手改Info.plist因为重新导出工程又会掉。权限文案这里有个小技巧不要写“我们需要访问相机”。用户看到这种文案大概率会因为不清楚用途而拒绝。我一般会写业务场景比如“为了保存你的专属头像需要访问相机拍照”或者“为了从相册选择图片作为你的作品封面需要访问照片库”。实测转化率会明显高一些这对数据分析团队也有价值。4.2 导出Xcode工程、开发者模式与签名Unity导出iOS工程的流程是标配File - Build Settings - Switch Platform - iOS - Export。我提醒几个容易翻车的点。开发者模式。真机调试前iPhone上需要开启设置 - 隐私与安全性 - 开发者模式否则安装完包后点击图标会闪退Xcode里的报错是“The app cannot be opened because the developer mode is not enabled.”这个坑在iOS 16以后特别常见新手机或者重刷系统的测试机最容易踩到别问我怎么知道的。签名方面如果只是内测可以用个人免费Apple ID签名但最多7天有效期而且不支持NSCameraUsageDescription之外的部分系统能力装到不同设备上容易出问题。团队项目建议直接配置付费开发者账号用Automatically manage signing自动管理证书Xcode会自动创建描述文件。如果是企业签名的流水线Unity导出时选“埋入Xcode工程但不要自动签名”最后统一由CI配置签名这个模式最稳定。模拟器调试。模拟器上不能调系统相机但可以用相册里预置的照片。如果你的业务交付节点很紧先用模拟器把相册链路和Unity回显流程全部跑通真机到了再只测相机能省不少时间。我在团队里定过一个规矩所有UI层面的交互必须先支持模拟器否则一律不算“可验收”。4.3 权限被拒后的引导处理UIImagePickerController首次弹出时会自动触发权限请求但用户一旦在系统弹窗里点了“不允许”后续再调用相机或相册就会直接失败。我们不能让玩家点完按钮后毫无反应所以要在Unity逻辑里做引导。我增加了两个原生函数用于在调用相机前检查权限状态extern C int _iOS_HasCameraPermission() { AVAuthorizationStatus status [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]; return (status AVAuthorizationStatusAuthorized) ? 1 : 0; } extern C int _iOS_HasPhotoPermission() { PHAuthorizationStatus status [PHPhotoLibrary authorizationStatus]; return (status PHAuthorizationStatusAuthorized || status PHAuthorizationStatusLimited) ? 1 : 0; }Unity侧在OpenCamera被调用时先检查权限如果被拒绝就弹一个原生Alertprivate void ShowPermissionAlert(string message) { // 通过原生弹窗提示用户去设置打开权限 }弹窗需要原生侧配合我在.mm里加了一个_iOS_ShowPermissionAlert函数用UIAlertController展示同时在弹窗上放一个“去设置”按钮点击后打开UIApplicationOpenSettingsURLString。这样用户可以直接跳到系统设置修改权限不需要自己去翻设置菜单。如果这里不处理用户被拒绝后再次点击相机按钮UIImagePickerController会弹出来但界面是黑色的或者直接无效体验很差。权限引导是这类插件项目里最容易被忽视、但对留存影响极大的一环。5. 踩坑实录五次翻车现场与排查手册5.1 点击原生按钮没有任何反应这是最常见的现象点了UIButton日志里什么都没有也没有崩溃。排查顺序我建议严格按下面来。先确认脚本有没有被真机编译。#if UNITY_IOS !UNITY_EDITOR这个宏在Xcode工程的编译目标里是否正确。用Debug.Log在OpenCamera第一行打日志如果连日志都没有说明按钮事件没绑对或编译宏判断有问题。再确认原生函数是否存在。在Xcode里用nm命令查符号或者在Xcode导航栏里能直接看到.mm文件被编译进Compile Sources列表。用nm -gU匹配函数名找不到就说明符号没导出重点检查extern C是否漏了。最后确认调用时机。iOS的dispatch_async确实执行了但UnityGetGLViewController返回的控制器为nullpresentViewController就没有反应。这个情况在Unity刚冷启动、界面还没完全初始化时偶发。我的处理方式是在C#侧延迟到Start或按钮点击场景已经稳定后再调用不要在Awake里直接调原生。5.2 相机界面一闪而过或崩溃相机弹出来但马上消失八成是两种原因。第一种是UIImagePickerController的delegate没有强引用用户还没操作呢delegate已经被释放了系统读取delegate方法时内存野指针导致崩溃。第二种是弹窗的父控制器传错了比如用旧式rootViewController获取到了Unity的UnityViewControllerBase而它当前正好处于一个正在切换的状态。解决起来就一条用UnityGetGLViewController()取父控制器并且保证全局静态持有delegate对象。我自己的工程中这个崩溃在iOS 17上的表现是直接闪退日志只有一行“Thread 0 crashed”特别难查。后来我在delegate初始化时加了自定义Tag标记再通过Xcode的NSZombieEnabled排查才定位到问题。已经踩过一次后续所有版本都统一按这个写法没有再翻车。5.3 返回的图片方向错乱、纹理发黑方向错乱一般出现在从相册选择图片时因为相册里的照片本身就可能带EXIF方向。原生侧已经做了fixOrientation如果Unity侧看到的仍不对八成是图片落盘前用的不是重绘后的Image而是原图。我见过有人把fixOrientation和scaleImage写反先缩放原图再修复方向结果重绘基于错误的长宽比图片被拉伸。正确顺序永远是先修方向再缩放。纹理发黑通常是因为Texture2D.LoadImage解码失败了但代码没检查返回值。我在LoadImageAndShow里加了判断失败时直接Debug.LogError并销毁对象。还有就是路径问题File.ReadAllBytes(path)如果读取的是沙盒外的路径或者路径含中文字符导致转码异常会抛异常。遇到这种问题先打印path在Xcode调试时用lldb执行file命令确认路径存在基本能定位。5.4 回调不到Unity侧GameObject已销毁UnitySendMessage虽然能在运行时找到对象但它没有“创建对象”的能力。如果名为NativeBridge的GameObject已经被销毁原生侧发消息时会静默失败Unity侧连警告都不会有。最典型的是在场景切换后点击“拍照”按钮新场景里根本没有这个对象。解决办法就是前面说的DontDestroyOnLoad常驻方案。我统一挂到一个独立的桥接对象上并且让这个对象不依赖场景中的任何GameObject这样无论怎么切场景回调对象永远存在。还有一个更隐蔽的情况Unity程序还没完全启动UnitySendMessage就被原生代码调用了。比如App冷启动后立刻从APNs推送消息跳到相机逻辑。这种时序问题我建议原生侧在收到AppDidFinishLaunching之后至少延迟一帧再调用UnitySendMessage或者用一个dispatch_after做200毫秒保护等Unity主循环跑起来。5.5 内存暴涨与闪退内存暴涨主要由三个原因叠加导致原图没压缩、Base64字符串没做降级、旧纹理没销毁。我的方案已经在前面对齐了其中两个剩下的就是在Unity侧务必实现Destroy(oldTexture)和及时手动Resources.UnloadUnusedAssets不过后者不建议频繁调用每帧调用会严重影响性能。实测在iPhone上用我给出的原生压缩方案最长边2048、JPEG质量0.8一张图片落地文件大小通常在800KB到1.5MB之间转成Unity纹理后显存占用大约16MB。一张一张处理内存曲线完全平稳。如果哪次我发现内存漂移第一检查点就是RawImage上是否还绑着旧纹理第二检查点才是原生侧有没有泄漏。6. 更进一步与个人体会6.1 如果项目还要拍视频或自定义相机界面UIImagePickerController不只是能拍照片把picker.cameraCaptureMode设为UIImagePickerControllerCameraCaptureModeVideo再用picker.videoQuality控制视频质量就能直接拍视频。如果是拍短视频封面这种需求可以进相册模式选视频然后用AVAssetImageGenerator抽帧这个我在短视频类项目里验证过比先转码再抽帧省很多流程。如果需要给相机加一个自定义贴纸或遮罩比如人脸识别框那种效果UIImagePickerController提供了cameraOverlayView属性。你可以在这个Overlay上放一个透明的UIView画各种标记。由于系统相机的顶部工具栏是独立的用cameraOverlayView自定义UI时可以隐藏showsCameraControls再自己接管快门逻辑灵活性高很多。这块如果展开又是一篇文章但我至少可以说在自绘滤镜、AR面具这类产品需求里这套方案完全够用不用上原生相机AVCaptureSession那种重模式。6.2 我的个人经验与最终建议如果非要让我总结一条最值得记住的经验那就是原生侧处理完图片后只传路径不要传大数据本体。这能规避掉一整套内存和跨语言编码的问题。另外别再犹豫要不要用第三方插件对于相机/相册桥接这种高度依赖系统私有UI层的小功能自己写原生插件反而是最稳、最可控的选择。把代码放进Assets/Plugins/iOS、维护好回调对象常驻、做好权限文案和降采样一套流程下来我还没有遇到真正解决不了的问题。我也建议你做之前先去读一读Unity官方文档里关于iOS Native Plugin的说明它很短但能帮你快速建立对__Internal链接机制的准确认知。不要把原生插件想成什么黑魔法它本质上就是一套公开的C函数接口加上系统UI框架的调用组合越简单反而越可靠。如果你按照这篇文章走通一遍再回头看最初那个“客户端要调相机相册”的需求会发现它已经从一个看起来很吓人的跨平台难题变成了一条纯线性的执行链路按钮触发、原生弹窗、图片处理、路径回调、纹理加载。希望这篇文章能帮你少走几趟弯路把时间省下来处理真正重要的业务逻辑。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑