Unreal Plugin Language 是 UE 提供的以 XML 语法为基础的语言,用来可以控制构建 Apk 以及 ipa 的过程,如实现修改 AndroidManifest.xml
或者 info.plist
等。
分析 plist 的生成流程 在UEDeployIOS.cs 的 GeneratePList
函数中通过传入进来的 UPLScripts
来构造出 UPL 对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 public virtual bool GeneratePList (FileReference ProjectFile, UnrealTargetConfiguration Config, string ProjectDirectory, bool bIsUE4Game, string GameName, bool bIsClient, string ProjectName, string InEngineDir, string AppDirectory, List<string > UPLScripts, VersionNumber SdkVersion, string BundleID, bool bBuildAsFramework, out bool bSupportsPortrait, out bool bSupportsLandscape, out bool bSkipIcons ){ GameName = GameName.Split("-" .ToCharArray())[0 ]; List<string > ProjectArches = new List<string >(); ProjectArches.Add("None" ); string BundlePath; if (bIsUE4Game) { BundlePath = Path.Combine(UnrealBuildTool.EngineDirectory.ToString(), "Intermediate" , "IOS-Deploy" , "UE4Game" , Config.ToString(), "Payload" , "UE4Game.app" ); } else { BundlePath = AppDirectory; } string RelativeEnginePath = UnrealBuildTool.EngineDirectory.MakeRelativeTo(DirectoryReference.GetCurrentDirectory()); UnrealPluginLanguage UPL = new UnrealPluginLanguage(ProjectFile, UPLScripts, ProjectArches, "" , "" , UnrealTargetPlatform.IOS); UPL.Init(ProjectArches, true , RelativeEnginePath, BundlePath, ProjectDirectory, Config.ToString(), false ); return GenerateIOSPList(ProjectFile, Config, ProjectDirectory, bIsUE4Game, GameName, bIsClient, ProjectName, InEngineDir, AppDirectory, SdkVersion, UPL, BundleID, bBuildAsFramework, out bSupportsPortrait, out bSupportsLandscape, out bSkipIcons); }
在最后调用的 GeneratedIOSList 中,构造出默认的 pliat 内容、从 Additional Plist Data
中读取的内容、以及调用 UPL 来处理 plist 的内容,UPL 的过程是最后处理的。
而且需要注意的是,在 GeneratedPList
函数中,通过 GeneratedIOSList 获取所有模块中添加的 UPL.xml 文件,然后把这些 xml 文件合并成一个,注意合并的顺序是 AdditionalProperties
的顺序,最后添加的 UPL 会放在最后执行,在一个项目中如果有多个使用 UPL 的操作要注意顺序问题。
介入 ipa 生成过程:操作 plist ios 的 ipa 包中都会有 plist 文件,可以用来配置 app 的一些属性,apple 的开发者文档里对每个支持的 key 有详细的描述:iOS Keys
UE 4.25.1 默认打包会产生下面这样一个 plist 文件:info.plist ,在一些特殊的需求中,需要往这个 plist 中添加元素或者修改以及删除。
在 UE 的项目设置中,可以给 plist 添加元素,在 Project Settings
-Platform
-iOS
-Additional Plist data
中可以填入一个字符串,它会被插入到 plist 文件中:
1 <key > AdditionalElementAAA</key > \n<string > this key is a test element.</string >
中间的 \n
是格式化代码,用于另起一行。
如果想要修改或者删除 plist 的元素,需要通过 UPL 来写逻辑(当然也可以使用 UPL 来添加元素,建议使用这种做法)。
1 2 3 4 5 6 7 8 9 10 11 12 13 <?xml version="1.0" encoding="utf-8"?> <root > <init > <log text ="UPL Exalpme adding element to plist..." /> </init > <trace enable ="true" /> <iosPListUpdates > <addElements tag ="dict" once ="true" > <key > AdditionalElementAAA</key > <string > this key is a test element.</string > </addElements > </iosPListUpdates > </root >
上面是用来添加元素的,上面的内容和直接写到 Additional Plist data
是一样的。
遍历 plist 中的 key:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <?xml version="1.0" encoding="utf-8"?> <root > <init > <log text ="UPL Exalpme..." /> </init > <trace enable ="true" /> <iosPListUpdates > <loopElements tag ="dict" > <loopElements tag ="$" > <setStringFromTag result ="TagName" tag ="$" /> <setBoolIsEqual result ="bIsKey" arg1 ="$S(TagName)" arg2 ="key" /> <if condition ="bIsKey" > <true > <log text ="$S(TagName):$S(TagValue)" /> </true > </if > </loopElements > </loopElements > </iosPListUpdates > </root >
注意:当前元素以 tag = "$"
方式引用。
编译时就会有以下 log:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 UPL: /Users/buildmachine/UE4/Builds/lipengzha-PC1/C/BuildAgent/workspace/PackageFGameClient/FGame/Plugins/UPLExample/Source/UPLExample/ThirdParty/IOS/IOS_UPL.xml UPL Init: None UPLExalpme adding element to plist... key : CFBundleURLTypes key : CFBundleDevelopmentRegion key : CFBundleDisplayName key : CFBundleExecutable key : CFBundleIdentifier key : CFBundleInfoDictionaryVersion key : CFBundleName key : CFBundlePackageType key : CFBundleSignature key : CFBundleVersion key : CFBundleShortVersionString key : LSRequiresIPhoneOS key : UIStatusBarHidden key : UIFileSharingEnabled key : UIRequiresFullScreen key : UIViewControllerBasedStatusBarAppearance key : UIInterfaceOrientation key : UISupportedInterfaceOrientations key : UIRequiredDeviceCapabilities key : CFBundleIcons key : CFBundleIcons~ipad key : UILaunchStoryboardName key : CFBundleSupportedPlatforms key : MinimumOSVersion key : ITSAppUsesNonExemptEncryption key : NSLocationAlwaysAndWhenInUseUsageDescription key : NSLocationWhenInUseUsageDescription key : CFBundleURLName key : CFBundleURLSchemes key : CFBundlePrimaryIcon key : CFBundleIconFiles key : CFBundleIconName key : UIPrerenderedIcon key : CFBundlePrimaryIcon key : CFBundleIconFiles key : CFBundleIconName key : UIPrerenderedIcon
对于新增比较简单,但是对于删除和修改就比较麻烦了,需要遍历一遍所有的节点,然后根据匹配来删掉当前的元素(注意 plist 的是键值对的,一个 <key></key>
下面还对应着一个 value 元素,这两个都要删掉,不然会打包不过):
1 2 <key > BuildMachineOSBuild</key > <string > 19C57</string >
我写了个方便删除 plist 中元素的流程,可以方便删除多组元素:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 <?xml version="1.0" encoding="utf-8"?> <root > <init > <log text ="UPLExalpme adding element to plist..." /> </init > <trace enable ="true" /> <iosPListUpdates > <addElements tag ="dict" once ="true" > <key > AdditionalElementA</key > <string > this key is a AdditionalElementA element.</string > <key > AdditionalElementB</key > <string > this key is a AdditionalElementB element.</string > </addElements > <setString result ="NeedDeleteKey_1" value ="AdditionalElementA" /> <setString result ="NeedDeleteKey_2" value ="AdditionalElementB" /> <setInt result ="loopSumNum" value ="2" /> <setInt result ="loopCount" value ="1" /> <setBoolIsLessEqual result ="loopRun" arg1 ="$I(loopCount)" arg2 ="$I(loopSumNum)" /> <while condition ="loopRun" > <log text ="count:$I(loopCount) SearchKey:$S(NeedDeleteKey_$I(loopCount))" /> <setBool result ="bIsDeleteElement" value ="false" /> <loopElements tag ="dict" > <loopElements tag ="$" > <if condition ="bIsDeleteElement" > <true > <setBool result ="bIsDeleteElement" value ="false" /> <log text ="bIsDeleteElement is true!!!" /> <setStringFromTag result ="TagName" tag ="$" /> <setStringFromTagText result ="TagValue" tag ="$" /> <log text ="Delete element value,tagname:$S(TagName) value:$S(TagValue)" /> <removeElement tag ="$" once ="true" /> </true > </if > <setStringFromTag result ="TagName" tag ="$" /> <setBoolIsEqual result ="bIsKey" arg1 ="$S(TagName)" arg2 ="key" /> <if condition ="bIsKey" > <true > <setStringFromTagText result ="TagValue" tag ="$" /> <log text ="tagname:$S(TagName) tagvalue:$S(TagValue)" /> <setBoolIsEqual result ="bIs_NeedDeleteKey_$I(loopCount)" arg1 ="$S(TagValue)" arg2 ="$S(NeedDeleteKey_$I(loopCount))" /> <if condition ="bIs_NeedDeleteKey_$I(loopCount)" > <true > <log text ="Match key $S(NeedDeleteKey_$I(loopCount))." /> <log text ="Delete element key,tagname:$S(TagName) value:$S(TagValue)." /> <removeElement tag ="$" once ="true" /> <setBool result ="bIsDeleteElement" value ="true" /> </true > </if > </true > </if > </loopElements > </loopElements > <setIntAdd result ="loopCount" arg1 ="$I(loopCount)" arg2 ="1" /> <setBoolIsLessEqual result ="loopRun" arg1 ="$I(loopCount)" arg2 ="$I(loopSumNum)" /> <if condition ="loopRun" > <true > <log text ="add loopCount to $I(loopCount)" /> </true > <false > <log text ="the loop is finished!" /> </false > </if > </while > </iosPListUpdates > </root >
脚本最开始 Add 了两个元素对,后面则是删除的代码,使用时需要关注的是下面三行:
1 2 3 <setString result ="NeedDeleteKey_1" value ="AdditionalElementA" /> <setString result ="NeedDeleteKey_2" value ="AdditionalElementB" /> <setInt result ="loopSumNum" value ="2" />
头两行是要删除的元素的变量,值是要删除的 key 的字符串,注意命名规则都是以 NeedDeleteKey_
开头,要遵守这个命名规则。 第三行是创建了一个 loopSumNum
的变量,用于记录有多少个需要删除的元素对,这里我测试删除两个,它的值就是 2。
当打包构建的时候,这个脚本执行起来就会有下面的输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 UPL: /Users/buildmachine/UE4/Builds/lipengzha-PC1/C/BuildAgent/workspace/PackageFGameClient/FGame/Plugins/UPLExample/Source/UPLExample/ThirdParty/IOS/IOS_UPL.xml UPL Init: None UPLExalpme adding element to plist... count:1 SearchKey:AdditionalElementA tagname:key tagvalue:CFBundleURLTypes tagname:key tagvalue:CFBundleDevelopmentRegion tagname:key tagvalue:CFBundleDisplayName tagname:key tagvalue:CFBundleExecutable tagname:key tagvalue:CFBundleIdentifier tagname:key tagvalue:CFBundleInfoDictionaryVersion tagname:key tagvalue:CFBundleName tagname:key tagvalue:CFBundlePackageType tagname:key tagvalue:CFBundleSignature tagname:key tagvalue:CFBundleVersion tagname:key tagvalue:CFBundleShortVersionString tagname:key tagvalue:LSRequiresIPhoneOS tagname:key tagvalue:UIStatusBarHidden tagname:key tagvalue:UIFileSharingEnabled tagname:key tagvalue:UIRequiresFullScreen tagname:key tagvalue:UIViewControllerBasedStatusBarAppearance tagname:key tagvalue:UIInterfaceOrientation tagname:key tagvalue:UISupportedInterfaceOrientations tagname:key tagvalue:UIRequiredDeviceCapabilities tagname:key tagvalue:CFBundleIcons tagname:key tagvalue:CFBundleIcons~ipad tagname:key tagvalue:UILaunchStoryboardName tagname:key tagvalue:CFBundleSupportedPlatforms tagname:key tagvalue:MinimumOSVersion tagname:key tagvalue:ITSAppUsesNonExemptEncryption tagname:key tagvalue:NSLocationAlwaysAndWhenInUseUsageDescription tagname:key tagvalue:NSLocationWhenInUseUsageDescription tagname:key tagvalue:AdditionalElementA Match key AdditionalElementA. Delete element key,tagname:key value:AdditionalElementA. bIsDeleteElement is true!!! Delete element value,tagname:string value:this key is a AdditionalElementA element. tagname:key tagvalue:AdditionalElementB tagname:key tagvalue:CFBundleURLName tagname:key tagvalue:CFBundleURLSchemes tagname:key tagvalue:CFBundlePrimaryIcon tagname:key tagvalue:CFBundleIconFiles tagname:key tagvalue:CFBundleIconName tagname:key tagvalue:UIPrerenderedIcon tagname:key tagvalue:CFBundlePrimaryIcon tagname:key tagvalue:CFBundleIconFiles tagname:key tagvalue:CFBundleIconName tagname:key tagvalue:UIPrerenderedIcon add loopCount to 2 count:2 SearchKey:AdditionalElementB tagname:key tagvalue:CFBundleURLTypes tagname:key tagvalue:CFBundleDevelopmentRegion tagname:key tagvalue:CFBundleDisplayName tagname:key tagvalue:CFBundleExecutable tagname:key tagvalue:CFBundleIdentifier tagname:key tagvalue:CFBundleInfoDictionaryVersion tagname:key tagvalue:CFBundleName tagname:key tagvalue:CFBundlePackageType tagname:key tagvalue:CFBundleSignature tagname:key tagvalue:CFBundleVersion tagname:key tagvalue:CFBundleShortVersionString tagname:key tagvalue:LSRequiresIPhoneOS tagname:key tagvalue:UIStatusBarHidden tagname:key tagvalue:UIFileSharingEnabled tagname:key tagvalue:UIRequiresFullScreen tagname:key tagvalue:UIViewControllerBasedStatusBarAppearance tagname:key tagvalue:UIInterfaceOrientation tagname:key tagvalue:UISupportedInterfaceOrientations tagname:key tagvalue:UIRequiredDeviceCapabilities tagname:key tagvalue:CFBundleIcons tagname:key tagvalue:CFBundleIcons~ipad tagname:key tagvalue:UILaunchStoryboardName tagname:key tagvalue:CFBundleSupportedPlatforms tagname:key tagvalue:MinimumOSVersion tagname:key tagvalue:ITSAppUsesNonExemptEncryption tagname:key tagvalue:NSLocationAlwaysAndWhenInUseUsageDescription tagname:key tagvalue:NSLocationWhenInUseUsageDescription tagname:key tagvalue:AdditionalElementAAA tagname:key tagvalue:AdditionalElementB Match key AdditionalElementB. Delete element key,tagname:key value:AdditionalElementB. bIsDeleteElement is true!!! Delete element value,tagname:string value:this key is a AdditionalElementB element. tagname:key tagvalue:CFBundleURLName tagname:key tagvalue:CFBundleURLSchemes tagname:key tagvalue:CFBundlePrimaryIcon tagname:key tagvalue:CFBundleIconFiles tagname:key tagvalue:CFBundleIconName tagname:key tagvalue:UIPrerenderedIcon tagname:key tagvalue:CFBundlePrimaryIcon tagname:key tagvalue:CFBundleIconFiles tagname:key tagvalue:CFBundleIconName tagname:key tagvalue:UIPrerenderedIcon the loop is finished!
其实只要可以删除,就可以在删除之后自己通过 addElements
来再把已删除的元素添加一遍了,从而实现要修改的目的。
为 IOS 添加 Framawork IOS 上的 Framework
有点类似于静态链接库的意思,相当于把.a
+.h
+ 资源打包到一块的集合体。更具体的区别描述请看:iOS 库 .a 与.framework 区别
在 UE 中以集成 IOS 上操作 Keycahin 的 SSKeychain
为例,在 Module 的 build.cs
中使用 PublicAdditionalFrameworks
来添加:
1 2 3 4 5 6 7 PublicAdditionalFrameworks.Add( new Framework( "SSKeychain" , "ThirdParty/IOS/SSKeychain.embeddedframework.zip" , "SSKeychain.framework/SSKeychain.bundle" ) );
构造 Framework 的第一个参数是名字,第二个是 framework 的路径(相对于 Module),第三个则是解压之后的 Framework 的 bundle
路径(如果 framework 没有 bundle 则可以忽略这个参数,而且就算有 bundle,但是不写这第三个参数貌似也没什么问题)。
这个可以打开 SSKeychain.embeddedframework.zip
文件看:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 SSKeychain.embeddedframework └─SSKeychain.framework │ Info.plist │ SSKeychain │ ├─Headers │ SSKeychain.h │ SSKeychainQuery.h │ ├─Modules │ module .modulemap │ ├─SSKeychain.bundle │ └─en.lproj │ SSKeychain.strings │ └─_CodeSignature CodeDirectory CodeRequirements CodeRequirements-1 CodeResources CodeSignature
相对于 .framework
的路径,这个路径一定要填正确,不然是不能用的,因为打包时会把这个 zip 解压出来,然后拷贝到包体中,路径指定错了就无法拷贝了。
1 [2020.05 .14 -11.04 .48 :324 ][988 ]UATHelper: Packaging (iOS): [2 /183 ] sh Unzipping : /Users/zyhmac/UE4/Builds/ZHALIPENG/C/Users/imzlp/Documents/UnrealProjectSSD/MicroEnd_423/Plugins/PlatformUtils/Source/PlatformUtils/ThirdParty/IOS/SSKeychain.embeddedframework.zip -> /Users/zyhmac/UE4/Builds/ZHALIPENG/D/UnrealEngine/Epic/UE_4.23 /Engine/Intermediate/UnzippedFrameworks/SSKeychain/SSKeychain.embeddedframework
注意:不要在两个不同的模块里同时引入一个相同的第三方 framework 文件,不然会有以下错误(如我在插件 A 中引入了 SSKeychain.embeddedframework.zip
然后在相同工程的另一个插件 B 中也引入了它)。
1 Unable to merge actions producing SSKeychain.embeddedframework.extracted: prerequisites are different.