拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Unity iOS游戏截图保存相册全攻略:原生插件开发与权限配置

1. 项目概述与核心需求在Unity开发中尤其是面向iOS平台时实现将游戏内生成的截图、渲染画面或UI元素保存到系统相册是一个既常见又充满“坑点”的需求。无论是让玩家分享精彩瞬间还是为应用添加内容导出功能这个功能都至关重要。然而iOS系统严格的沙盒机制和隐私权限管理使得这个过程不像在Android上调用一个MediaStore接口那么简单。你需要跨越从Unity的C#脚本到iOS原生Objective-C/Swift代码的桥梁并妥善处理权限请求和用户交互。这篇指南将为你拆解从原理到实现的完整路径涵盖权限配置、原生插件编写、图片处理以及错误排查确保你的功能既稳定又符合App Store的审核规范。2. 核心原理与架构设计2.1 为什么不能直接保存理解iOS的沙盒与相册权限iOS应用运行在一个严格的“沙盒”环境中。每个应用只能访问自己专属的文件目录即Application.persistentDataPath。用户相册Photos位于沙盒之外是一个受保护的系统共享资源区域。因此任何应用想要写入数据到相册都必须获得用户的明确授权并且必须通过系统提供的特定APIPhotos Framework或UIImageWriteToSavedPhotosAlbum来操作。这带来了两个核心挑战权限申请必须在Info.plist文件中声明用途并在运行时向用户请求NSPhotoLibraryAddUsageDescription权限iOS 11。对于仅写入不读取的需求这个权限是足够的。跨语言调用Unity使用C#而访问相册的API是Objective-C或Swift写的。我们需要建立一个通信桥梁。2.2 技术方案选型Unity与iOS原生代码交互主要有两种主流方案Unity iOS插件.a或.xcframework这是最标准、最灵活的方式。你需要编写一个Objective-C或Swift的类封装保存到相册的逻辑并将其编译为静态库.a或框架.xcframework。然后在Unity C#中通过[DllImport(“__Internal”)]来调用这个原生库中的函数。这种方式性能好控制力强是本文重点讲解的方案。使用第三方插件如Native Gallery等对于希望快速集成、避免编写原生代码的开发者Asset Store上有成熟的付费插件。它们封装了所有平台iOS/Android的细节提供统一的C# API。但你需要评估其成本、兼容性以及是否符合你项目的具体定制需求。为什么选择方案一自研插件对于功能明确、希望深度控制、避免引入额外依赖或学习插件内部机制的项目自研是最佳选择。它能让你透彻理解整个流程便于后续调试和功能扩展。2.3 整体工作流程设计一个完整的保存到相册功能其工作流如下Unity端将需要保存的Texture2D或屏幕内容编码为字节数组通常是PNG或JPEG格式。桥接调用C#脚本通过P/Invoke平台调用将图片字节数组和必要的回调信息传递给iOS原生代码。iOS原生端接收字节数据转换为UIImage对象。检查相册写入权限。如果未授权则向用户发起授权请求。使用Photos FrameworkPHPhotoLibrary或UIKit的UIImageWriteToSavedPhotosAlbum方法将图片保存到相册。将保存成功或失败的结果通过Unity提供的接口如UnitySendMessage回传给Unity的C#脚本。Unity端回调C#脚本接收到原生端的回调在UI上向用户显示保存结果如“保存成功”或“权限被拒绝”。3. 环境准备与项目配置3.1 Unity项目设置Player Settings在开始编码前必须正确配置Unity的iOS Player Settings这是很多新手容易忽略导致构建失败或功能异常的关键步骤。打开Player Settings在Unity编辑器中点击File - Build Settings选择iOS平台然后点击Player Settings...按钮。配置Info.plist权限声明在Player Settings的Other Settings区域找到Camera Usage Description等权限描述字段的下方你需要手动添加相册写入权限的描述。点击Info.plist列表下方的号添加一个新的键值对。Key:NSPhotoLibraryAddUsageDescription(注意是AddUsage这代表仅写入权限)。Value: 填写清晰、友好的描述告诉用户你为什么需要这个权限。例如“保存游戏截图至您的相册方便您分享精彩时刻”。这是App Store审核的硬性要求描述语必须准确且非空。配置脚本后端与API兼容性Scripting Backend: 必须选择IL2CPP。自Xcode 10以后Apple已不再接受基于Mono的32位应用上架IL2CPP能生成64位代码并且通常有更好的性能。API Compatibility Level: 推荐使用.NET Standard 2.0或.NET 4.x。确保你的代码和可能引用的第三方库与所选API级别兼容。配置目标设备与版本Target minimum iOS Version: 根据你使用的API设定。如果你要使用较新的Photos Framework特性可能需要设置为iOS 11或更高。对于基础保存功能iOS 9通常足够。Architecture: 选择Universal(包含armv7和arm64) 以确保兼容性。如果仅支持较新设备可选Arm64。3.2 创建iOS插件目录结构在Unity项目的Assets文件夹下创建一个标准的iOS插件目录结构Assets/ ├── Plugins/ │ └── iOS/ │ ├── PhotoSaver.mm (或 .m, .swift) │ ├── PhotoSaver.h │ └── Info.plist (可选通常不需要因为Unity会合并).mm文件是Objective-C文件允许你在其中混编C代码这对于处理从Unity传递过来的字节数据byte*非常方便。如果你使用Swift则需要额外配置一个UnityFramework桥接过程稍复杂本文以更通用的Objective-C为例。4. iOS原生插件实现详解4.1 编写头文件PhotoSaver.h头文件用于声明公开给Unity调用的C函数接口。// PhotoSaver.h #ifndef PhotoSaver_h #define PhotoSaver_h #ifdef __cplusplus extern C { #endif // 声明一个C函数供Unity C#调用 // 参数说明 // imageBytes: 指向图片字节数组的指针 // length: 字节数组的长度 // callbackTarget: Unity中接收回调的GameObject名称 // callbackMethod: Unity中接收回调的方法名称 void _SaveImageToAlbum(const unsigned char* imageBytes, int length, const char* callbackTarget, const char* callbackMethod); #ifdef __cplusplus } #endif #endif /* PhotoSaver_h */4.2 编写实现文件PhotoSaver.mm这是插件的核心包含了权限检查和保存逻辑。// PhotoSaver.mm #import Photos/Photos.h // iOS 8 使用Photos Framework #import UIKit/UIKit.h #import “PhotoSaver.h” // 声明一个内部函数用于将C字符串转换为NSString static inline NSString* CreateNSString(const char* string) { if (string) { return [NSString stringWithUTF8String:string]; } else { return [NSString string]; } } // 声明回调函数用于通知Unity结果 extern “C” { void UnitySendMessage(const char* obj, const char* method, const char* msg); } // 实现头文件中声明的函数 void _SaveImageToAlbum(const unsigned char* imageBytes, int length, const char* callbackTarget, const char* callbackMethod) { // 1. 将字节数据转换为NSData再转换为UIImage NSData *imageData [NSData dataWithBytes:imageBytes length:length]; UIImage *image [UIImage imageWithData:imageData]; if (!image) { // 图片数据无效立即回调失败 UnitySendMessage(callbackTarget, callbackMethod, “Image data is invalid”); return; } // 2. 检查相册写入权限 PHAuthorizationStatus status [PHPhotoLibrary authorizationStatusForAccessLevel: PHAccessLevelAddOnly]; // iOS 14仅添加权限 // 对于iOS 14以下可以使用 [PHPhotoLibrary authorizationStatus] if (status PHAuthorizationStatusAuthorized) { // 已授权直接保存 [self saveImage:image withCallbackTarget:callbackTarget andMethod:callbackMethod]; } else if (status PHAuthorizationStatusNotDetermined) { // 未决定发起权限请求 [PHPhotoLibrary requestAuthorizationForAccessLevel:PHAccessLevelAddOnly handler:^(PHAuthorizationStatus newStatus) { dispatch_async(dispatch_get_main_queue(), ^{ if (newStatus PHAuthorizationStatusAuthorized) { [self saveImage:image withCallbackTarget:callbackTarget andMethod:callbackMethod]; } else { // 用户拒绝授权 UnitySendMessage(callbackTarget, callbackMethod, “Permission denied by user”); } }); }]; } else { // 权限被明确拒绝或受限 UnitySendMessage(callbackTarget, callbackMethod, “Photo library access denied or restricted”); } } // 内部方法执行实际的保存操作 (void)saveImage:(UIImage *)image withCallbackTarget:(const char*)target andMethod:(const char*)method { // 使用Photos Framework进行保存 (iOS 8) [[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{ // 创建图片创建请求 PHAssetCreationRequest *creationRequest [PHAssetCreationRequest creationRequestForAsset]; // 从UIImage添加图片数据 [creationRequest addResourceWithType:PHAssetResourceTypePhoto data:UIImagePNGRepresentation(image) options:nil]; } completionHandler:^(BOOL success, NSError * _Nullable error) { dispatch_async(dispatch_get_main_queue(), ^{ if (success) { UnitySendMessage(target, method, “Save successful”); } else { NSString *errorMsg [NSString stringWithFormat:“Save failed: %“, [error localizedDescription]]; UnitySendMessage(target, method, [errorMsg UTF8String]); } }); }]; // 备选方案使用旧的UIKit API (不推荐用于新项目但更简单) // UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil); // 此方法无法获得精确的成功/失败回调且对于大图或频繁操作控制力较弱。 }关键点解析权限级别我们使用了PHAccessLevelAddOnly这对应NSPhotoLibraryAddUsageDescription。它只请求写入权限不请求读取权限对用户更友好也更容易通过隐私审核。主线程操作所有涉及UI包括权限弹窗和调用UnitySendMessage的回调都必须在主线程dispatch_get_main_queue()上执行否则可能导致崩溃或不可预知的行为。错误处理通过completionHandler的success和error参数我们可以将具体的错误信息传递回Unity便于调试。5. Unity C# 桥接与调用5.1 创建C#接口类在Unity的Assets/Scripts目录下创建一个C#脚本例如NativePhotoSaver.cs。// NativePhotoSaver.cs using System; using System.Runtime.InteropServices; using UnityEngine; public class NativePhotoSaver : MonoBehaviour { // 导入我们在iOS插件中编写的C函数 // 注意iOS平台上原生库名称为“__Internal” #if UNITY_IOS !UNITY_EDITOR [DllImport(“__Internal”)] private static extern void _SaveImageToAlbum(byte[] imageBytes, int length, string callbackTarget, string callbackMethod); #endif // 供其他C#代码调用的公共方法 public void SaveTextureToAlbum(Texture2D texture, string callbackTargetName, string callbackMethodName) { #if UNITY_IOS !UNITY_EDITOR // 1. 将Texture2D编码为PNG字节数组 byte[] imageBytes texture.EncodeToPNG(); // 或 EncodeToJPG(quality) if (imageBytes null || imageBytes.Length 0) { Debug.LogError(“Failed to encode texture to bytes.”); return; } // 2. 调用原生插件函数 _SaveImageToAlbum(imageBytes, imageBytes.Length, callbackTargetName, callbackMethodName); #else // 非iOS平台如编辑器、Android的模拟或提示 Debug.LogWarning(“SaveToAlbum is only supported on iOS platform.”); // 这里可以实现在PC上模拟保存到本地文件夹方便测试 #endif } // 一个更便捷的封装保存当前屏幕截图 public void SaveScreenshotToAlbum(string callbackTargetName, string callbackMethodName) { StartCoroutine(TakeScreenshotAndSave(callbackTargetName, callbackMethodName)); } private System.Collections.IEnumerator TakeScreenshotAndSave(string target, string method) { // 等待一帧确保所有渲染完成 yield return new WaitForEndOfFrame(); // 创建与屏幕同尺寸的Texture2D Texture2D screenTexture new Texture2D(Screen.width, Screen.height, TextureFormat.RGB24, false); // 读取屏幕像素 screenTexture.ReadPixels(new Rect(0, 0, Screen.width, Screen.height), 0, 0); screenTexture.Apply(); // 调用保存方法 SaveTextureToAlbum(screenTexture, target, method); // 清理临时纹理避免内存泄漏 Destroy(screenTexture); } // 提供给原生插件回调的方法 public void OnSaveResult(string message) { Debug.Log($“Photo Save Result from Native: {message}“); // 在这里可以根据message更新UI例如显示“保存成功”或“保存失败xxx” // 例如UIManager.Instance.ShowToast(message); } }5.2 在场景中使用在场景中创建一个空的GameObject命名为“PhotoManager”。将NativePhotoSaver脚本挂载到该GameObject上。在需要保存图片的代码中例如一个UI按钮的点击事件获取该组件并调用方法。// 示例在某个UI按钮的点击事件中 public void OnSaveButtonClicked() { NativePhotoSaver saver FindObjectOfTypeNativePhotoSaver(); // 建议用单例或依赖注入 if (saver ! null) { // 方式一保存一个已有的Texture2D // saver.SaveTextureToAlbum(myTexture, “PhotoManager”, “OnSaveResult”); // 方式二保存当前屏幕截图 saver.SaveScreenshotToAlbum(“PhotoManager”, “OnSaveResult”); } }6. 构建、部署与测试6.1 构建Xcode工程在Unity中完成所有配置和代码编写。点击File - Build Settings确保场景已添加然后点击Build。选择一个输出文件夹Unity会生成一个Xcode工程.xcodeproj文件。6.2 在Xcode中的必要检查打开工程双击生成的.xcodeproj文件在Xcode中打开。检查权限在Xcode中点击项目根目录选择Target-Info选项卡。在Custom iOS Target Properties中确认Privacy - Photo Library Additions Usage Description对应NSPhotoLibraryAddUsageDescription已存在且描述正确。这是Unity的Player Settings自动合并进来的。链接框架确保Photos.framework被添加到项目中。通常Unity的Post-Process Build脚本会自动处理但最好手动确认一下。在Target-General-Frameworks, Libraries, and Embedded Content中查看。如果没有点击号添加Photos.framework并将Embed设置为Do Not Embed。设置开发团队与签名在Signing Capabilities中选择正确的开发团队Team和Bundle Identifier确保自动签名Automatically manage signing已启用。6.3 真机测试将iOS设备连接到Mac并在Xcode顶部选择该设备作为运行目标。点击运行Run按钮将应用安装到设备上。首次触发保存功能时系统会弹出权限请求对话框显示你在Info.plist中设置的描述。用户必须点击“允许”才能继续。测试保存功能并观察Xcode的控制台输出和Unity的日志确认回调被正确触发。7. 常见问题、优化与排查技巧7.1 常见问题速查表问题现象可能原因解决方案构建Xcode失败提示符号未定义iOS插件函数声明与调用不匹配或.mm文件未正确编译。1. 检查C#中[DllImport]的函数名、参数类型与.h/.mm文件中的声明是否完全一致。2. 确保.mm文件在Assets/Plugins/iOS目录下且其Platform Settings在Unity Inspector中仅勾选iOS。应用崩溃日志显示EXC_BAD_ACCESS内存访问错误。常见于从C#传递到Objective-C的字节数组指针已失效。确保在C#端byte[]数组在调用原生函数期间不会被垃圾回收。一个稳妥的做法是使用GCHandle固定数组或在插件内部立即将数据拷贝到NSData中如示例代码所示。权限弹窗不出现或保存无反应Info.plist中权限描述键名错误或缺失回调GameObject或方法名错误。1. 仔细检查NSPhotoLibraryAddUsageDescription的拼写。2. 确认C#调用时传入的callbackTarget字符串与挂载了回调方法的GameObject名称完全一致区分大小写。3. 确认回调方法callbackMethod是public的。保存成功但相册中找不到图片保存操作是异步的completionHandler可能在保存完全完成前就回调了“成功”。用户可能保存到了“最近项目”而非特定相簿。1. 在回调成功后可以添加一个短暂延迟再提示用户。2. 告知用户图片保存在“照片”应用的“最近项目”或“所有照片”中。在Unity编辑器中运行报错DllImport(“__Internal”)只在真机或模拟器的iOS构建中有效。使用#if UNITY_IOS !UNITY_EDITOR预编译指令包裹平台相关代码并在Editor下提供替代实现或友好提示。7.2 性能与体验优化图片尺寸与格式保存前考虑对Texture2D进行缩放。全屏截图如1242x2688的PNG文件可能高达几MB保存和处理耗时。可以按需压缩尺寸或使用EncodeToJPG并指定质量如70来大幅减小文件体积加快处理速度。编码EncodeToPNG是一个CPU密集型操作避免在主线程进行。可以使用System.Threading.Tasks.Task或协程在后台线程处理完成后再回到主线程调用原生插件。异步与用户反馈保存到相册是I/O操作尤其是使用Photos Framework它是异步的。在保存期间务必在UI上给予明确的等待指示如转圈动画防止用户重复点击。在OnSaveResult回调中根据结果给出清晰的Toast或弹窗提示。内存管理如示例所示使用Destroy(screenTexture)及时销毁临时创建的Texture2D对象避免内存泄漏。在Objective-C端ARC会自动管理UIImage和NSData的内存无需手动释放。7.3 高级扩展保存到自定义相簿如果希望将图片保存到用户相册中一个特定的、由你应用创建的相簿Album而不是默认的“最近项目”可以使用Photos Framework的更高级功能(void)saveImageToCustomAlbum:(UIImage *)image albumName:(NSString *)albumName completion:(void(^)(BOOL, NSError*))completion { [[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{ // 1. 查找或创建自定义相簿 PHAssetCollection *assetCollection [self getAssetCollectionWithTitle:albumName]; PHAssetCollectionChangeRequest *collectionChangeRequest; if (assetCollection) { collectionChangeRequest [PHAssetCollectionChangeRequest changeRequestForAssetCollection:assetCollection]; } else { collectionChangeRequest [PHAssetCollectionChangeRequest creationRequestForAssetCollectionWithTitle:albumName]; } // 2. 创建图片资源 PHAssetCreationRequest *assetCreationRequest [PHAssetCreationRequest creationRequestForAsset]; [assetCreationRequest addResourceWithType:PHAssetResourceTypePhoto data:UIImagePNGRepresentation(image) options:nil]; // 3. 将图片资源添加到相簿变更请求中 PHObjectPlaceholder *placeholder [assetCreationRequest placeholderForCreatedAsset]; [collectionChangeRequest addAssets:[placeholder]]; } completionHandler:^(BOOL success, NSError * _Nullable error) { if (completion) { dispatch_async(dispatch_get_main_queue(), ^{ completion(success, error); }); } }]; } (PHAssetCollection *)getAssetCollectionWithTitle:(NSString *)title { PHFetchResult *collections [PHAssetCollection fetchAssetCollectionsWithType:PHAssetCollectionTypeAlbum subtype:PHAssetCollectionSubtypeAlbumRegular options:nil]; for (PHAssetCollection *collection in collections) { if ([collection.localizedTitle isEqualToString:title]) { return collection; } } return nil; }这需要你在C#接口中增加新的函数并处理相簿名称的传递。同时首次创建相簿也需要用户授权流程上会更复杂一些但能提供更好的用户体验。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门