ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Unity iOS开发:调用系统相册与相机的原生桥接完整指南

Unity iOS开发:调用系统相册与相机的原生桥接完整指南 做Unity开发的人迟早会遇到一个绕不开的需求在iOS上让用户从系统相册挑一张图或者直接调起系统相机拍一张。我最早碰到这个需求是做一款生活记录类AppUnity里所有UI都是自己做好的唯独相册和相机这两个系统能力必须借助原生层弹出的UIImagePickerController才能完成。这篇文章我把从C#到Objective-C、从Unity工程到Xcode工程的完整链路拆开来讲包含可以直接用的源码和我踩过的坑。1. 为什么Unity项目绕不开原生弹窗这一关1.1 典型业务场景先说说在什么情况下会用到这个能力。Unity开发的App里凡是涉及用户头像照片分享社区发帖证件照拍摄这些功能都需要和系统相册、相机打交道。比如我做的那款App用户在个人中心点击头像弹出一个ActionSheet选拍照或从相册选择然后进入系统相机拍照或从相册挑图最后把图片回传到Unity场景里显示。这类需求在iOS上无法完全用Unity自身功能完成。Unity的C#运行在IL2CPP或Mono之上虽然能通过Texture2D加载图片但没有权限遍历用户的相册列表更没有能力弹出系统相机界面。系统UI必须通过UIKit访问而Unity引擎没有把UIImagePickerController暴露给C#。1.2 Unity与iOS的能力边界我刚做这个需求时也想过是不是有现成的Unity插件。Asset Store上确实有一些付费插件能搞定但问题很现实插件是一个黑盒出了问题不好排查而且只为了一个选照片的功能引入整套插件包体和复杂度都不划算。更核心的障碍在技术边界上。iOS的沙盒机制把App的数据隔离在一个独立目录里C#代码无法跨进程访问系统相册框架Photos.framework里的UIImagePickerController。系统相机和相册本质上是一个独立于Unity渲染管线的原生UI页面必须在Objective-C或Swift层创建和present。所以标准做法是写一个Objective-C桥接类在原生层弹出UIImagePickerController拿到图片后压缩成临时文件再把文件路径通过UnitySendMessage回传给C#。整体通信闭环是Unity C#调OCOC回调C#的双向通信。1.3 整体通信闭环预览先给一张全局图方便后面代码对上号Unity场景里挂一个名为NativeBridge的GameObject组件脚本里用[DllImport(__Internal)]声明原生函数。玩家点击按钮C#调用_OpenPhotoLibrary()或_OpenCamera()。Objective-C桥接类收到调用弹出系统相册或相机。用户选择或拍照完成后原生层把UIImage做方向校正、等比压缩写入NSTemporaryDirectory临时目录。原生层调用UnitySendMessage(NativeBridge, OnImageSelected, /tmp/xxx.jpg)把路径回传给C#。C#方法OnImageSelected(string path)读取文件、加载为Texture2D、显示到RawImage上。这个流程里最关键也最容易出问题的点是第5步的UnitySendMessage机制以及第4步的图片处理时序。下面逐层展开。2. 双向桥接UnitySendMessage与C#调OC的闭环是怎么跑通的2.1 C#调用Objective-Cextern C导出函数Unity调iOS原生方法标准做法是使用[DllImport(__Internal)]声明外部函数。__Internal告诉Unity运行时这个符号不在独立的动态库里而是链接在当前可执行文件里也就是Unity生成的Xcode工程中编译进App的原生代码。在Objective-C.mm文件里用extern C把C名字修饰去掉导出三个函数extern C { void _InitCameraGallery(const char *objectName); void _OpenPhotoLibrary(); void _OpenCamera(); }这样C#端就能用对应的函数签名直接调用[DllImport(__Internal)] private static extern void _InitCameraGallery(string gameObjectName); [DllImport(__Internal)] private static extern void _OpenPhotoLibrary(); [DllImport(__Internal)] private static extern void _OpenCamera();注意几个关键点第一[DllImport(__Internal)]只有在真机或模拟器运行时才会去找原生符号在Unity编辑器里点击运行不会执行所以必须用#if UNITY_IOS !UNITY_EDITOR把声明和调用包起来否则编辑器会报EntryPointNotFoundException或直接忽略。第二原生函数必须放在.mm文件里而不是.m文件。extern C是C语法纯Objective-C编译器的.m文件不认识。我一开始偷懒放在.m里编译直接报错。第三Unity传递字符串给原生时是UTF-8的const char*原生侧接收后要转成NSString再存储不要直接保存const char*指针因为C#字符串生命周期结束后指针就悬垂了。2.2 OC调用C#UnitySendMessage的三个关键坑原生层回传数据给C#用的就是Unity官方提供的UnitySendMessage接口UnitySendMessage(NativeBridge, OnImageSelected, /tmp/xxx.jpg);第一个参数是场景中GameObject的名字第二个参数是挂在那个GameObject上的MonoBehaviour方法名第三个参数是方法接收的字符串参数。这里有几个坑是必踩的GameObject必须真实存在且名字完全一致。脚本里如果用gameObject.name初始化那场景里这个物体叫什么名字原生侧就缓存什么名字。如果场景切换时这个物体被销毁UnitySendMessage会静默失败控制台没有任何日志。标准做法是Awake里DontDestroyOnLoad(gameObject)并且整个App生命周期内不销毁。方法名大小写必须完全一致参数必须是string。如果C#方法定义成void OnImageSelected(string path)原生传的方法名写成onImageSelected回调就不会触发。参数类型目前只支持string、int、float这些简单类型最实用的是string复杂的结构体就把它JSON序列化成一个字符串传过去。Message参数不能传null要传空字符串。我刚开始在取消选择时会传nulliOS端UnitySendMessage的第三参数传nil后C#端有时收到的是空字符串有时直接不触发行为很迷。后来统一传空字符串C#侧判空处理稳定多了。2.3 完整时序与主线程问题整个调用过程中的线程问题也需要注意。UnitySendMessage本身可以在任意线程调用Unity内部会把它投递到主线程的Update循环中执行所以原生层处理完图片、写临时文件后无论耗时多久回调到C#都是在主线程可以直接操作Unity API比如创建Texture2D、给RawImage赋值。但反过来C#调用_OpenPhotoLibrary()之后图片选择是异步的原生层不会阻塞等用户操作完用户选完图才会走回调。所以桥接层务必持有回调目标的名字而不能期望用返回值把UIImage传回C#。UIImagePickerController是异步回调原生侧至少要持有这些信息才能正确回调。3. iOS端原生实现相册选择器与拍照流程的完整代码3.1 为什么用UIImagePickerControlleriOS 14之后系统提供了PHPickerViewController它运行在独立进程中App拿不到相册权限也能选择照片隐私性和体验都更好。但PHPicker有一个限制它不支持直接拍照调起相机的需求还是得靠UIImagePickerController。我这个项目需要同时支持相册选择和拍照两个入口权衡代码量和兼容性决定先用一个UIImagePickerController同时覆盖两个场景。这样代码量小iOS 13及以下的老系统也兼容。后续如果要做纯相册选择的升级版再把相册入口替换成PHPicker也不迟。还有一个考虑UIImagePickerController在真机上弹出的就是系统原生界面体验最贴近系统玩家不觉得突兀。自己用AVFoundation写自定义相机弹窗效果虽然也能做但工程量大很多而且后续系统升级还要不断适配。3.2 头文件定义创建YLCameraBridge.h#import UIKit/UIKit.h interface YLCameraBridge : NSObject UIImagePickerControllerDelegate, UINavigationControllerDelegate // 缓存Unity侧接收回调的GameObject名字 property (nonatomic, copy) NSString *unityObjectName; // 防止快速点击导致弹出多个系统弹窗 property (nonatomic, assign) BOOL isProcessing; (instancetype)sharedInstance; - (void)openPhotoLibrary; - (void)openCamera; end用NSObject而不是继承UIViewController是因为这个桥接类只需要做picker的delegate和数据转发它自己不需要出现在视图层级里。弹出picker用的承载视图控制器直接用Unity的GLViewController。3.3 完整实现代码创建YLCameraBridge.mm#import YLCameraBridge.h #import UnityFramework/UnityFramework.h implementation YLCameraBridge (instancetype)sharedInstance { static YLCameraBridge *instance nil; static dispatch_once_t onceToken; dispatch_once(onceToken, ^{ instance [[YLCameraBridge alloc] init]; }); return instance; } - (UIViewController *)rootViewController { return UnityGetGLViewController(); } - (void)openPhotoLibrary { if (self.isProcessing) return; self.isProcessing YES; UIImagePickerController *picker [[UIImagePickerController alloc] init]; picker.sourceType UIImagePickerControllerSourceTypePhotoLibrary; picker.delegate self; picker.modalPresentationStyle UIModalPresentationFullScreen; [[self rootViewController] presentViewController:picker animated:YES completion:nil]; } - (void)openCamera { if (self.isProcessing) return; if (![UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) { NSLog(YLCameraBridge: 当前设备不支持相机); self.isProcessing NO; [self sendEmptyResult]; return; } self.isProcessing YES; UIImagePickerController *picker [[UIImagePickerController alloc] init]; picker.sourceType UIImagePickerControllerSourceTypeCamera; picker.delegate self; picker.modalPresentationStyle UIModalPresentationFullScreen; [[self rootViewController] presentViewController:picker animated:YES completion:nil]; } #pragma mark - UIImagePickerControllerDelegate - (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionaryUIImagePickerControllerInfoKey,id *)info { UIImage *image info[UIImagePickerControllerOriginalImage]; if (!image) { self.isProcessing NO; [self sendEmptyResult]; [picker dismissViewControllerAnimated:YES completion:nil]; return; } image [self normalizedImage:image]; NSString *path [self saveImageToTemp:image]; [picker dismissViewControllerAnimated:YES completion:^{ self.isProcessing NO; if (path) { [self sendResult:path]; } else { [self sendEmptyResult]; } }]; } - (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker { [picker dismissViewControllerAnimated:YES completion:^{ self.isProcessing NO; [self sendEmptyResult]; }]; } #pragma mark - 图片处理 // 把EXIF方向信息转成实际像素方向避免横图 - (UIImage *)normalizedImage:(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 *normalized UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return normalized; } // 等比压缩到1280以内写入临时文件 - (NSString *)saveImageToTemp:(UIImage *)image { CGFloat maxSide 1280.0f; CGFloat maxDimension MAX(image.size.width, image.size.height); CGFloat scale MIN(1.0f, maxSide / maxDimension); CGSize targetSize CGSizeMake(image.size.width * scale, image.size.height * scale); UIGraphicsBeginImageContextWithOptions(targetSize, YES, 1.0); [image drawInRect:CGRectMake(0, 0, targetSize.width, targetSize.height)]; UIImage *scaledImage UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); NSData *jpgData UIImageJPEGRepresentation(scaledImage, 0.8); NSString *fileName [NSString stringWithFormat:yl_%.jpg, [[NSUUID UUID] UUIDString]]; NSString *tempPath [NSTemporaryDirectory() stringByAppendingPathComponent:fileName]; BOOL ok [jpgData writeToFile:tempPath atomically:YES]; return ok ? tempPath : nil; } #pragma mark - Unity 回调 - (void)sendResult:(NSString *)path { if (self.unityObjectName.length 0) { NSLog(YLCameraBridge: unityObjectName 未设置回调丢失); return; } const char *objectName [self.unityObjectName UTF8String]; const char *methodName OnImageSelected; const char *arg [path UTF8String]; UnitySendMessage(objectName, methodName, arg); } - (void)sendEmptyResult { if (self.unityObjectName.length 0) return; UnitySendMessage([self.unityObjectName UTF8String], OnImageSelected, ); } end #pragma mark - 供C#调用的C接口 extern C { void _InitCameraGallery(const char *objectName) { NSString *name [NSString stringWithUTF8String:objectName]; [YLCameraBridge sharedInstance].unityObjectName name; } void _OpenPhotoLibrary() { [[YLCameraBridge sharedInstance] openPhotoLibrary]; } void _OpenCamera() { [[YLCameraBridge sharedInstance] openCamera]; } }三个函数和C#的声明严格对应。_InitCameraGallery接收场景里挂载脚本的GameObject名字缓存到属性里后续回调都发给这个物体。图片处理部分有两个细节值得多解释一下。normalizedImage必须放在压缩之前执行。因为UIImage的imageOrientation只是元数据标记如果不重绘直接压缩会得到一张旋转了90度或180度的图。saveImageToTemp里的maxSide 1280是基于Unity侧显示需要的经验值如果原图是1200万像素直接解码到内存约为48MB加载到Unity再做一次解码又会翻倍很容易触发iOS的内存警告。先压缩再传路径内存峰值能控制得很低。压缩参数里UIGraphicsBeginImageContextWithOptions(targetSize, YES, 1.0)的最后一个参数是scale传1.0可以避免Retina屏上生成2x或3x的大尺寸图片对头像和列表缩略图场景足够。3.4 Info.plist必须配置的权限声明少了权限描述App在真机上直接崩溃这是最容易被忽略的。在Xcode导出工程后Info.plist里补上keyNSCameraUsageDescription/key string需要使用相机拍摄图片用于头像和内容发布/string keyNSPhotoLibraryUsageDescription/key string需要访问相册以选择图片/string文案要写得具体说明用途。App Store审核会看这段文字如果只写需要访问相册这类含糊描述大概率被拒。iOS系统弹窗显示的也是这段文案要认真写。还有一点如果之后打算把处理后的图片保存回相册还需要加NSPhotoLibraryAddUsageDescription。本次需求只是读图不需要加。加上但没用到也不会被拒只是隐私清单里多一项。3.5 升级方案iOS 14换成PHPicker如果你的App最低版本支持iOS 14以上强烈建议把相册入口换成PHPickerViewController。PHPicker有几个明显优势对比项UIImagePickerControllerPHPickerViewController相册权限需要NSPhotoLibraryUsageDescription不需要相册权限独立进程运行支持Limited模式不支持支持用户可只授权部分照片搜索和筛选弱支持搜索、多选、类型过滤拍照能力支持不支持只用做选图实现差异不大PHPicker用PHPickerConfiguration设置过滤条件然后用PHPickerViewController呈现在delegate里通过loadObjectOfClass拿UIImage。我把这部分留作进阶优化当前方案先用UIImagePickerController跑通闭环。4. Unity侧C#封装从按钮点击到回传Image的完整链路4.1 封装类完整代码Unity侧创建NativeCameraGallery.cs挂在一个持久化的GameObject上这个GameObject的名字就作为回调目标using System; using System.IO; using System.Runtime.InteropServices; using UnityEngine; using UnityEngine.UI; public class NativeCameraGallery : MonoBehaviour { public static NativeCameraGallery Instance { get; private set; } [SerializeField] private RawImage previewImage; public event Actionstring OnImagePathReceived; #if UNITY_IOS !UNITY_EDITOR [DllImport(__Internal)] private static extern void _InitCameraGallery(string gameObjectName); [DllImport(__Internal)] private static extern void _OpenPhotoLibrary(); [DllImport(__Internal)] private static extern void _OpenCamera(); #endif private void Awake() { if (Instance ! null Instance ! this) { Destroy(gameObject); return; } Instance this; DontDestroyOnLoad(gameObject); #if UNITY_IOS !UNITY_EDITOR _InitCameraGallery(gameObject.name); #endif } public void PickFromGallery() { #if UNITY_IOS !UNITY_EDITOR _OpenPhotoLibrary(); #else Debug.Log(编辑器或非iOS环境模拟选择); OnImagePathReceived?.Invoke(); #endif } public void TakePhoto() { #if UNITY_IOS !UNITY_EDITOR _OpenCamera(); #else Debug.Log(编辑器或非iOS环境模拟拍照); OnImagePathReceived?.Invoke(); #endif } // 由UnitySendMessage回调触发 public void OnImageSelected(string path) { if (string.IsNullOrEmpty(path)) { Debug.LogWarning(用户取消或没有拿到图片); return; } if (previewImage ! null) { StartCoroutine(LoadImageToTexture(path)); } else { OnImagePathReceived?.Invoke(path); } } private System.Collections.IEnumerator LoadImageToTexture(string path) { byte[] bytes File.ReadAllBytes(path); Texture2D texture new Texture2D(2, 2, TextureFormat.RGBA32, false); if (!texture.LoadImage(bytes)) { Debug.LogError(图片解码失败: path); yield break; } if (previewImage ! null) { Texture2D old previewImage.texture as Texture2D; previewImage.texture texture; if (old ! null) Destroy(old); } OnImagePathReceived?.Invoke(path); yield return null; } private void OnDestroy() { if (Instance this) Instance null; } }这段代码在Awake里把gameObject.name传给原生层再通过DontDestroyOnLoad保证切换场景后这个物体还在。UnitySendMessage的回调是同步派发到主线程的所以OnImageSelected里可以直接创建Texture2D和操作UI。4.2 图片回传方式base64 vs 临时文件路径图片从原生回传Unity常见的方式有两种base64字符串和临时文件路径。我强烈建议用临时文件路径原因很实际对比维度base64字符串临时文件路径内存占用高图片转base64和C#解码都吃内存低文件IO可以流式处理传递速度慢数据量大时卡顿明显快只传一个路径字符串生命周期字符串生命周期短需立即解析文件在临时目录随时可读Unity侧加载需要额外解码File.ReadAllBytes LoadImage即可实测下来选一张1200万像素照片如果直接转base64传回Unity侧接收和Convert.FromBase64String的内存峰值能到200MB以上低配iPhone会闪退。临时文件方式原生侧已经压缩到1280以内最终Unity侧读取的jpg文件大概只有200KB内存压力完全可控。4.3 Texture加载时的内存控制C#侧用Texture2D.LoadImage解码jpg这个方法支持自动识别jpg和png不需要手工设置格式。注意创建Texture2D时传TextureFormat.RGBA32LoadImage会覆盖掉纹理尺寸和格式。替换贴图时我先把旧纹理存下来赋值新纹理后再Destroy(old)避免旧纹理内存泄漏。这个细节在Unity里很容易漏尤其是反复选图时旧Texture2D不销毁内存持续上涨最终触发iOS的清内存机制表现为App直接被杀。4.4 编辑器调试分支编辑器里没有iOS原生代码直接运行按钮会报找不到DllImport。所以封装类里Package了UNITY_EDITOR分支在编辑器里模拟选择输出日志并触发回调。开发联调阶段UI展示可以先接假数据等真机再验证完整链路。5. 构建配置与Xcode联调最容易忽略的权限项5.1 Unity Build Settings里的关键开关导出Xcode工程前有几个Build Settings必须检查缺一个都会在真机上翻车配置项推荐值原因Target DeviceiPhone iPad只用iPhone会限制iPad安装审核也可能要求适配Scripting BackendIL2CPPiOS平台标准配置Mono在iOS上性能差且提交审核易被拒ArchitectureARM64当前iOS真机都要求ARM64Strip Engine Code建议开启减小包体但注意不要Strip掉原生回调相关代码这些配置在Unity 2021.3 LTS和6000.0系列上都适用。版本太老的Unity比如2018生成的Xcode工程结构差异比较大UnityFramework.h的导入路径要相应调整。5.2 Xcode里必须手工完成的步骤Unity生成的Xcode工程默认没有权限描述也没有签名Team这些必须在Xcode里处理在Signing Capabilities里选择你的Team和Bundle Identifier。Info.plist里加NSCameraUsageDescription和NSPhotoLibraryUsageDescription。真机联调时在Build Settings里把Code Signing Identity设为Apple Development。如果使用PHPicker需要把iOS Deployment Target设为14.0以上否则编译报错。很多新人在这一步卡住主要原因是权限描述没加。App安装到真机上点击相册或相机按钮立刻崩溃查看Xcode控制台会看到Attempting to access camera without a usage description之类的信息就是这个原因。5.3 iOS 14的相册权限模式变化iOS 14开始系统相册权限除了允许访问所有照片还多了选择照片Limited模式。用户选择Limited后App拿到的相册列表只有用户勾选的部分。UIImagePickerController在选择照片时系统会直接弹UIImagePickerController自己的界面它在Limited模式下体验尚可但如果你的App后续自己枚举相册资源就会出现为什么看不到全部照片的问题。用PHPicker则可以完全绕开这个限制。我做这个项目时最低支持iOS 13所以选择了UIImagePickerController新版项目建议直接上PHPicker。5.4 联调日志怎么看真机联调时C#的Debug.Log在Xcode控制台会输出带Unity前缀的日志原生层的NSLog也会混在里面可以用关键词过滤po [YLCameraBridge sharedInstance].isProcessing如果你在原生代码里加了断点Xcode会停在真机调试这时候Unity画面会卡住这是正常现象。回调链路是否走通重点看三点原生API有没有被调用、UnitySendMessage的objectName有没有值、C#方法有没有触发。6. 真机踩坑清单内存、方向、弹窗冲突这些老问题6.1 大图OOM一张照片直接闪退第一次联调时我用iPhone 13拍了一张1200万像素的照片原生回调后Unity侧直接崩溃。Xcode控制台显示的崩溃日志指向ImageIO解码典型的内存峰值过高。原因是我最初没有在原生侧压缩而是把UIImage直接转base64给Unity。UIImage从相机出来时底层图像数据已经解码成ARGB位图1200万像素约48MB转base64又要把它编码成JPG原始数据再加一次base64膨胀Unity侧解码又一次三层叠加触发JetSam。解决思路就是原生侧一次性做到位方向校正、等比压缩、JPEG压缩到0.8质量、写入临时文件。压缩后的图片长边控制在1280文件大小200KB左右后续所有环节的内存开销都很小。顺带提一句临时文件写完后C#侧用File.ReadAllBytes读取用完不删除也没关系系统会清理但长时间高频使用建议用完就删。6.2 照片方向错乱竖拍变横图第二个坑是方向。用相机拍照后会拿到的UIImage如果检查imageOrientation很可能不是Up而是Right或Left。直接把这个UIImage写进JPEG得到的图片是横着的。我在最早版本里没做方向校正Unity侧显示的图片旋转了90度测试人员第一时间就提了bug。修法就是normalizedImage方法用UIGraphicsBeginImageContextWithOptions重绘一遍把EXIF方向信息在像素层面落地。注意这个方法在压缩之前调用因为重绘后image.size才是真实尺寸压缩比例才计算得准。6.3 横屏游戏下的相机弹窗冲突如果Unity工程是横屏游戏直接present UIImagePickerController相机的画面可能被拉伸或黑边。UIImagePickerController有自己支持的屏幕方向有些版本在横屏下预览是旋转的。我当时的处理是设置picker.modalPresentationStyle UIModalPresentationFullScreen大部分机型能正常显示。如果你做的是横屏游戏而且相机预览方向错乱可以考虑在present之前设置preferredInterfaceOrientationForPresentation或者干脆用AVFoundation自定义相机页面。自定义相机虽然工作量大但方向处理完全可控Unity端甚至可以直接用RenderTexture把摄像头画面渲染进游戏场景这是后话了。6.4 连续点击弹出多个系统弹窗玩家手指快的时候一个按钮可能被连续点击多次C#侧调用了多次_OpenPhotoLibrary()原生层就有多个UIImagePickerController同时present表现为上一个相册还没关下一个又弹出来界面卡死。我在原生侧加了isProcessing锁每次弹窗前判断如果已经在处理中就直接忽略。这个锁在picker关闭的时候复位。同样的问题在C#侧也可以做一层防护按钮点击后暂时置灰但最根本的还是原生侧锁住因为C#侧的网络延迟和输入延迟都不可控。6.5 UnitySendMessage回调不触发这个坑出现得最隐蔽排查了很久。症状是原生日志打印了UnitySendMessage也调用了但C#方法没反应。原因有三类第一初始化时unityObjectName传了空值或错误名字。我在Awake里用gameObject.name初始化但这个脚本所在物体在某个场景切换时被销毁重建名字变了导致原生侧缓存的名字失效。解决脚本挂在单例化的持久对象上且用DontDestroyOnLoad。第二方法名拼写不一致。C#侧是OnImageSelected原生侧写成了OnImageSelectd这种手滑比想象中更容易发生而且不会报错只会静默失败。第三回调时机在场景卸载期间。如果用户在选图过程中快速切场景比如点了Home键Unity的Application生命周期可能不活跃UnitySendMessage投递时物体还没创建。解决办法是初始化时机尽量提前最好是App启动后第一个场景就完成注册。6.6 调试工具与思路原生和Unity联调日志是最重要的线索。我习惯在原生代码里打NSLog标记每个关键节点比如进入openPhotoLibrary、拿到图片、压缩完成、UnitySendMessage调用。Xcode控制台用YLCameraBridge关键词过滤能快速定位问题出在哪个环节。如果是崩溃问题Xcode的崩溃堆栈里如果出现NativeCall、il2cpp字样说明Unity侧代码被原生调用时出了问题优先排查DllImport函数签名和参数类型是否一致。如果是内存问题Xcode的Memory Report工具能实时看到内存曲线大图加载时的飙升一眼就能看出来。这套方案我在两个项目里反复用过最后一次跑通后基本上一两个小时就能完整接入原生桥接代码写一次C#封装写一次后面再做其他原生能力比如推送、支付、扫码思路完全一致——init注册回调GameObject、暴露C接口给C#、原生操作完成后UnitySendMessage回传数据。这套三步走的桥接模式才是比选相册本身更值得沉淀的东西。
返回列表