编者按
本文已于作者 @Lohanry Le 授权转载,原载于知乎,如需转载请务必联系原作者。
iOS 打包需要的流程
整个打包流程与 Android 打包是类似的。
难点主要集中在插入必要的 FrameWork,修改 Plist
在Unity5.x 上 Unity 官方已经帮我们提供了一套 API 用来对 Xcode 工程的处理。并且提供了[PostProcessBuild]这个标签来完成插入工作。
在 Unity4.x 上使用这套 API 可以去下载 Unity 官方提供的代码 Unity-Technologies / XcodeAPI - Bitbucket
Unity 导出前的准备
手上的项目分多个地区,所以在导出前会有不同的资源切换和资源删除
代码如下:
static void ExportiOSHFProject_Online() { getCMDArgs(); PlayerSettings.defaultInterfaceOrientation = UIOrientation.AutoRotation; PlayerSettings.productName = "xxxx"; //设置韩服的Name //设置Deployment Target PlayerSettings.SetScriptingDefineSymbolsForGroup(BuildTargetGroup.iOS, "TWSDK;EXCLUDE_SHARESDK;KOREAN"); if (OnlySetScriptingDefineSymbols) { Debug.Log("本次打开Unity只是设置全局宏,打包请再次调用"); return; } string[] NeedToDel = {""};//指定要删除的目录 doDelWithRelativePath(NeedToDel); _LanguageBar.ToTraditionForAntBuild(_LanguageBar.UNIRES_KOREA); //切换语言版本 ExportProject(ExportPath, BuildTarget.iOS, BuildOptions.None); } static void ExportProject(string path,BuildTarget target,BuildOptions options) { if (path.Length != 0) { foreach (EditorBuildSettingsScene scene in EditorBuildSettings.scenes) { if (!scene.enabled) continue; levels.Add(scene.path); } float time = Time.realtimeSinceStartup; AssetDatabase.Refresh(); try { BuildPipeline.BuildPlayer(levels.ToArray(), path, target, options); } catch (System.Exception m) { Debug.LogError(m.Message); } time = Time.realtimeSinceStartup - time; Debug.Log("打包完成,共计耗时" + time); } }
踩坑注意点
- 1.OnlySetScriptingDefineSymbols
这个是在命令行打包时候外部传过来的值,为了判断是来导出工程还是设置宏。
本处这样设置是因为,在一开始我的打包脚本也是用宏来区分执行切换哪部分资源的,
但是后来出现了一个奇怪的问题,如果我刚刚 Checkout 出来的 Unity 默认宏是 A 的话, 但是我想打包的是B。
我直接设置 PlayerSettings.SetScriptingDefineSymbolsForGroup 为 B,
确实非 Editor 和非 [PostProcessBuild] 标签的脚本都被编译了为B。
但是在我 Unity 内部打包脚本的宏没有切换,也就导致了我Unity脚本都切换了为 B,但是我资源等切换都还是停留在A上,
所以这里设置为第一次打开 Unity 设置 PlayerSettings.SetScriptingDefineSymbolsForGroup 为 B,然后关闭再打开 Unity 这样所有的脚本都被切换到B了。
这个问题我自己被坑了挺久,当然也有可能是自己的设计有问题,希望大家有更加好的解决办法,或则是我的错误,希望大家能分享一下。 - 2.BuildPipeline.BuildPlayer()
options ios 需要切换到 BuildOptions.None,这个参数我也并没有特别的明白,但是如果选择其他或则与 Android 的类似,会出现些问题。
插入 FrameWork 和 Plis
接下来肯定是大家最关注的点了,我把代码直接贴出来吧,隐去了设置的的参数,宏因为是公司项目有些修改,可以根据自己的来修改。
public class AntXcodeProjectProcess : MonoBehaviour { [PostProcessScene] public static void OnPostprocessScene() { #if UNITY_ADS AdvertisementSettings.enabled = true; AdvertisementSettings.initializeOnStartup = false; #endif } [PostProcessBuild(100)] public static void OnPostprocessBuild(BuildTarget buildTarget, string path) { if (buildTarget == BuildTarget.iOS) { //plist string plistPath = Path.Combine(path, "info.plist"); PlistDocument plist = new PlistDocument (); plist.ReadFromFile (plistPath); plist.root.SetString ("CFBundleDevelopmentRegion", "zh_CN"); plist.root.SetString("NSCameraUsageDescription", "直播相关"); plist.root.SetString("NSAppleMusicUsageDescription", "直播相关"); #if TWSDK && !KOREAN plist.root.SetString("FacebookAppID", ""); plist.root.SetString("FacebookDisplayName",""); plist.root.CreateArray("CFBundleURLTypes").AddDict().CreateArray("CFBundleURLSchemes").AddString(""); string[] LSApplicationQueriesSchemesValueList = { "starcoin", "fbapi", "", "", "", "", "", "" ,"","","","","" ,""}; PlistElementArray LSApplicationQueriesSchemesArray = plist.root.CreateArray("LSApplicationQueriesSchemes"); for (int i = 0; i < LSApplicationQueriesSchemesValueList.Length; i++) { LSApplicationQueriesSchemesArray.AddString(LSApplicationQueriesSchemesValueList[i]); } plist.root.SetString("NSPhotoLibraryUsageDescription", "允許授權后遊戲體驗會更豐富"); #elif TWSDK && KOREAN plist.root.SetString("FacebookAppID", ""); plist.root.SetString("FacebookDisplayName", ""); plist.root.CreateArray("CFBundleURLTypes").AddDict().CreateArray("CFBundleURLSchemes").AddString(""); string[] LSApplicationQueriesSchemesValueList = { "starcoin", "fbapi", "", "", "", "", "", "" ,"","","","","" ,""}; PlistElementArray LSApplicationQueriesSchemesArray = plist.root.CreateArray("LSApplicationQueriesSchemes"); for (int i = 0; i < LSApplicationQueriesSchemesValueList.Length; i++) { LSApplicationQueriesSchemesArray.AddString(LSApplicationQueriesSchemesValueList[i]); } plist.root.SetString("NSPhotoLibraryUsageDescription", "允許授權后遊戲體驗會更豐富"); #endif PlistElementDict dictTransportSecurity = plist.root ["NSAppTransportSecurity"].AsDict (); dictTransportSecurity.SetBoolean("NSAllowsArbitraryLoads",true); #if !EXCLUDE_SHARESDK plist.root.CreateArray("LSApplicationQueriesSchemes").AddString("weixin"); // 微信分享 plist.root.CreateArray("CFBundleURLTypes").AddDict().CreateArray("CFBundleURLSchemes").AddString(""); #endif plist.WriteToFile (plistPath); //project string projPath = PBXProject.GetPBXProjectPath(path); PBXProject proj = new PBXProject(); proj.ReadFromString(File.ReadAllText(projPath)); string target = proj.TargetGuidByName("Unity-iPhone"); #if TWSDK && !KOREAN proj.SetTargetAttributes("ProvisioningStyle", "Manual");//关闭自动证书管理 #elif TWSDK && KOREAN proj.SetTargetAttributes("ProvisioningStyle", "Manual");//关闭自动证书管理 #endif //add common framework start proj.AddFrameworkToProject(target, "ReplayKit.framework", true); proj.AddFrameworkToProject(target, "ImageIO.framework", true); proj.AddFrameworkToProject(target, "Storekit.framework", false); proj.AddFrameworkToProject(target, "JavaScriptCore.framework", true); if (proj.ContainsFramework(target, "Metal.framework")){ proj.RemoveFrameworkFromProject(target, "Metal.framework"); proj.AddFrameworkToProject(target, "Metal.framework", true); } //add common framework end proj.AddBuildProperty(target, "FRAMEWORK_SEARCH_PATHS", "$(SRCROOT)"); proj.AddBuildProperty(target, "FRAMEWORK_SEARCH_PATHS", "$(SRCROOT)/Libraries"); string[] addBuildProperty = { "$(SRCROOT)/Libraries", "$(SRCROOT)" }; string[] removeBuildProperty = { "\"$(SRCROOT)/Libraries\"", "\"$(SRCROOT)\"" }; proj.UpdateBuildProperty(target, "LIBRARY_SEARCH_PATHS", addBuildProperty, removeBuildProperty); proj.AddBuildProperty(target, "OTHER_LDFLAGS", "-ObjC"); //custom frameworl start #if SDK proj.AddFileToBuild(target, proj.AddFile("/usr/bin/libsqlite3.tbd", "Frameworks/libsqlite3.tbd", PBXSourceTree.Sdk)); proj.AddFileToBuild(target, proj.AddFile("/usr/bin/libicucore.tbd", "Frameworks/libicucore.tbd", PBXSourceTree.Sdk)); #endif #if SDKSDK proj.AddFrameworkToProject(target, "Security.framework", false); proj.AddFrameworkToProject(target, "Storekit.framework", false); proj.AddFrameworkToProject(target, "SafariServices.framework", false); proj.AddFrameworkToProject(target, "CoreData.framework", true); proj.AddFrameworkToProject(target, "MobileCoreServices.framework", true); proj.AddFrameworkToProject(target, "EventKit.framework", true); proj.AddFrameworkToProject(target, "EventKitUI.framework", true); proj.AddFrameworkToProject(target, "Social.framework", true); proj.AddFrameworkToProject(target, "CoreTelephony.framework", true); proj.AddFrameworkToProject(target, "MessageUI.framework", true); proj.AddFileToBuild(target, proj.AddFile("/usr/bin/libsqlite3.tbd", "Frameworks/libsqlite3.tbd", PBXSourceTree.Sdk)); proj.AddFileToBuild(target, proj.AddFile("/usr/bin/libstdc++.tbd", "Frameworks/libstdc++.tbd", PBXSourceTree.Sdk)); #endif //custom frameworl end //custom SDKFile start #if SDK XcodeDirectoryProcessor xdp = new XcodeDirectoryProcessor(); xdp.CopyAndAddBuildToXcode(proj,target,"XcodeFiles/SDK/",path,"SDKFiles"); xdp.CopyAndReplace("XcodeFiles/SDK/Unity-iPhone/Images.xcassets/AppIcon.appiconset",Path.Combine(path, "Unity-iPhone/Images.xcassets/AppIcon.appiconset")); #endif #if TXWYSDK && !KOREAN Debug.Log("拷贝TXWYSDK文件夹"); XcodeDirectoryProcessor xdp = new XcodeDirectoryProcessor(); xdp.CopyAndAddBuildToXcode(proj,target,"XcodeFiles/TXWYSDK/",path,"SDKFiles"); xdp.CopyAndReplace("XcodeFiles/TWSDK/Unity-iPhone/Images.xcassets/AppIcon.appiconset",Path.Combine(path, "Unity-iPhone/Images.xcassets/AppIcon.appiconset")); #elif TXWYSDK && KOREAN XcodeDirectoryProcessor xdp = new XcodeDirectoryProcessor(); xdp.CopyAndAddBuildToXcode(proj,target,"XcodeFiles/KOREASDK/",path,"SDKFiles"); xdp.CopyAndReplace("XcodeFiles/KOREASDK/Unity-iPhone/Images.xcassets/AppIcon.appiconset",Path.Combine(path, "Unity-iPhone/Images.xcassets/AppIcon.appiconset")); #endif //custom SDKFile end File.WriteAllText(projPath, proj.WriteToString()); } } }
踩坑注意点
Plist 地方没有什么的,都非常的简单就直接根据需求进行设置就可以。
插入 Framework 就是全是坑了
- 1.插入Framework
如果你是直接在 Unity5.x 版本直接引用内部带有的 Api 来插入的时候,如果版本比较低,最新的 .tdb 动态库是插入不会成功的.
即使在 Xcode 中会显示,但是在编译时候还是会通不过的。
本人的 Unity 版本是5.2.4是必然不行的,所以可以使用 Unity 官方托管在 Bitbucket 的最新版本代码来使用,下载它的代码,然后修改命名空间直接使用就可以。 最新的已经支持了.tdb 的动态库添加,在添加动态库时候请选择 PBXSourceTree.Sdk。 动态库引用时候需要填写路径:”/usr/bin/“+“libicucore.tbd” - 2.自动证书管理
这个我们的项目比较特殊,在国服的 iOS 我们有 AppleID,有账号密码,所以在 XcodeBuild 时候可以直接进行签名等。
但是台服韩服等是有发行商给开发证书等,所以如果直接设置为自动管理证书无法签名会报错。
因为这个原因我也自己研究了一下.xcproject,其实这个是个文件夹,里面有配置文件可以直接修改。
最早的时候,我是直接写了一个 Py 脚本来修改这个,后来发现最新的API上有支持这个。
>proj.SetTargetAttributes(“ProvisioningStyle”, “Manual”);
这样既可修改为关闭自动证书管理,如果不修改这个ProvisioningStyle,Xcode会直接默认你是自动管理证书的。 - 3.Iphone 图标管理
Unity 直接导出 Xcode 之后,在 ICON 界面,你会发现,自己的图标并不齐全,所以直接提交 AppStore 可能会遇到直接被拒绝。
所以可以自己新建工程,把ICON补齐了,然后把Images.xcassets/AppIcon.appiconset文件夹备份一下。
每次打包时候直接
>xdp.CopyAndReplace(“XcodeFiles/KOREASDK/Unity-iPhone/Images.xcassets/AppIcon.appiconset”,Path.Combine(path, “Unity-iPhone/Images.xcassets/AppIcon.appiconset”)); - 4.项目外的 Framework 引用
我们常常会遇到我们的项目需要接入外部的 SDK,所以他们会有自己的静态库等,需要我们引用。
所以这个也可以在我们的 Unity 导出脚本中使用。
自己如果直接通过 AddFileToBuild 去添加一个文件夹,你会发现这个文件夹永远是蓝色的而不是以一个 Group 的形式,被引入到 Xcode 工程中。
但是如果说你先引入文件下的 framework 的话,你会发现文件夹已经直接被添加为 Group 了。
所以直接封装了 XcodeDirectoryProcessor 这个对文件夹的引用。
添加代码下载在这里 - 5.OC代码修改
我们的项目直接被解耦的比较好,所以不需要直接修改 Xcode 导出的代码,直接添加中间 bridge 的代码就好。
事实上如果有这个需求,可以直接写脚本对导出的 OC 代码进行替换。
理论上每次导出的 OC 代码是一样的,如果不同版本的就不保证一定相等。
签名
我盟直接使用 Jenkins 作为框架,直接安准 XcodeBuild 的插件来完成签名等工作,由于 Jenkins 直接都是配置一下就好了,基本没有坑,我也是直接一次调整通过,所以就不再重复。
提交IPA
我们是导出 Ipa 提交到发行商的网上,所以直接写脚本提交了,没有什么可以参考性,但是如果想直接提交到 Appstore 可以使用 XcodeBuild 但是我没有经验,应该也是比较简单的配置。
总结
至此基于 Jenkins 的 Unity 自动化构建 Android 和 iOS 文章都已近写完了。
从进公司到现在的一个半月多时间,全公司基本没有人对此方面有经验的情况下,通过自己一个人踩坑,一步步完成了整个流程。
对于我来说也是学到了许多,希望大家看完我的文章也能学到。
毕竟自己才刚刚毕业才疏学浅,文中可能出现比较多的错误,代码比较简陋,希望大家不要嫌弃。
代码部分全都乱了
@rxvincent:抱歉,刚刚修正
支持下技术向文章
你好,我最近也在研究ios自动打包,请问怎样使应用在appstore上的“语言”那一项只保留一个“简体中文”?
有没有修改Xcode中General里的属性的方法