Apple提供的常规打包方式主要是由Xcode支持的,下面展开来聊聊
Xcode打包
Xcode的打包主要分为两步:
- Archive:对target进行编译、归档,生成.xcarchive 文件
- Export:对生成的.xcarchive 文件进行进一步的处理,生成不同渠道的ipa包,进行分发
Archive编译归档
Archive主要是对target进行编译、归档,生成.xcarchive 文件。可在Xcode -> Window -> Organizer -> Archives中查看
archive_02
这里所说的归档,主要就对项目源码进行编译后,再将编译生成的各种文件、资源、记录统一封装到一个文件中,便于管理和回溯。
随机选择一个.xcarchive文件,点击show in Finder,可以看到一个.xcarchive 后缀的文件
这个.xcarchive文件包含了应用、符号表信息以及其他的资源,可以右键 -> 显示包内容进行查看,主要包含以下文件:
- BCSymbolMaps:Xcode 对 BitCode 符号表进行混淆(Symbol Hiding)后生成的对照表,和 dSYM 文件会一一对应
- dSYMs:存储此次编译的符号表(debug symbols),用来符号化解析崩溃堆栈
- info.plist: 项目target的配置文件
- Products:存储此次编译生成的的 App 包(.app)。虽然这个文件包含了App 运行需要的可执行文件以及其它资源,但是和最终用户下载的版本会有所不同。后续的 export 操作会对其进行进一步处理。
- SCMBlueprint:如果 Xcode 打开了版本管理(Preferences -> Source Control -> Enable Source Control),SCMBlueprint 文件夹会存储此次编译的版本控制信息,包括使用的 git 版本、仓库、分支等。如果需要回溯此次编译的源码版本,可以在这个文件中找到对应的信息。
- SwiftSupport:在 Target 的 Build Settings 中打开了ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES,此次编译使用的 Swift 版本对应的标准库文件(.dylib)会被放到这个文件夹中。发布App时,也会被复制到ipa bundle中。在iOS 12.2及以上,swift的ABI趋于稳定,已经不用再自带链接库了,因此ipa包节省了一定的体积
Export打包分发
Export主要是的对生成的.xcarchive 文件进行进一步的处理,生成不同渠道的ipa包,进行分发,Organize -> Archive文件 -> Distribute App
这里对应4种分发渠道:AppStore、Ad Hoc、Enterprise和Development,然后一步步往下操作即可。
以上4中渠道对应的打包method分别是app-store、ad-hoc、enterprise、development,可以在导出的文件夹中ExportOptions.plist文件看到对应参数,如下所示
最终Export导出的文件夹中主要包含以下4种文件
- DistributionSummary.plist:包含ipa所支持的架构、bitcode、证书、embeddedBinaries(非系统的动态库相关)、entitlements(apns环境、application-identifier等)、profile等相关信息
- ExportOptions.plist:ipa包导出的配置文件,主要包含包导出方式、签名方式、App Developer team的id等
- MIStore.ipa:应用的ipa包,包含了App所需的签名、二进制包、资源等
- Packaging.log:打包相关的日志
如果想要查看ipa中的内容,可以将.ipa改成.zip,然后解压,也可使用命令解压
zip -0 -y -r myAppName.ipa Payload/,其中是.app的文件,查看其包内容,主要包含以下几部分
- MachO可执行文件:具体的介绍可查看iOS逆向 12:Mach-O文件(上)和iOS逆向 12:Mach-O文件(下)
- 签名文件:App 的签名信息会被放到 _CodeSignature 文件夹中。
- info.plist:存储 App 主要信息的 plist 文件也会被一并打包到 ipa 中。
- entitlements:App包含的相关权限,在这个文件中通过 xml 的格式将这些授权记录下来,例如Push notifications、App Group等
- App Plugins:如果App实现了 App Extension扩展,扩展的包会以 .appex 的后缀存储在 PlugIns 文件夹中,随着App的安装一起安装到用户手机上
- 链接库:App 运行所需要的各种链接库会被放入 Frameworks 文件夹。
- 资源文件:App 运行需要的各种资源文件也是 ipa 体积的大头。常见的有
- 各种多媒体资源:图片、音视频
- xib 文件:.nib.storyboardc
- 各种打包的资源 .bundle
- 其它类型的资源:字体、数据库、证书等等
除了Xcode自带的工具application loader上传ipa,还可以通过Transporter上传。Transporter可以以简单轻松的方式将内容交付到Apple,提供如下功能:
- 只需要将ipa包拖放到Transporter中即可开始使用
- 同时验证和上传多个文件以快速交付
- 查看交付进度(警告、错误和交付日志),以便快速修复交付问题
- 查看历史交付记录(包含日期、时间)
所以,综上所述,通过Xcode提供的打包方式比较繁琐,需要人为操作许多步骤,那么我们是否可以通过其他的方式进行简化呢?答案是当然可以呀。
简化打包操作的方式的主要有两种
- Shell打包脚本
- Jenkins部署自动打包
这里我们主要介绍Shell打包脚本的方式,主要分为以下几步
- 配置打包相关的名称:.xcworkspace的名字、scheme名称、编译方式、打包方式、profile文件、bundleID等
- 配置打包的相关路径:导出路径、archive文件路径、ipa文件路径、ExportOptions.plist文件路径
- 清理工程:通过xcodebuild clean清理
- 编译:通过xcodebuild archive编译、归档工程
- 导出ipa:通过xcodebuild命令将.xcarchive导出为ipa文件
- 校验并上传AppStore:如果是AppStore的包,通过xcrun altool校验并上传AppStore
使用方式:进入podfile所在目录,在终端执行.sh文件,例如:sh xxxx.sh
具体的shell脚本如下(注:使用时,需要自行填充其中的xxxxx所在的脚本代码)
#!/bin/bash # 前提:enterprise打包时,需要切换到 企业账户 及 BundleID # 使用方法: # step1: 将该脚本放在工程的根目录下(跟.xcworkspace文件or .xcodeproj文件同目录) # step2: 根据情况修改下面的参数 # step3: 打开终端,执行脚本。(输入sh,然后将脚本文件拉到终端,会生成文件路径,然后enter就可) echo "-----------开始执行脚本-----------" # =============项目自定义部分(自定义好下列参数后再执行该脚本)=================== # echo "请选择打包方式 ? [1:enterprise_debug 2:enterprise_release 3:ad_hoc 4:app_store]" read number while([[ $number != 1 ]] && [[ $number != 2 ]] && [[ $number != 3 ]] && [[ $number != 4 ]]) do echo "Error! Should enter 1 or 2 or 3 or 4" echo "请选择打包方式 ? [ 1:enterprise_debug 2:enterprise_release 3:ad_hoc 4:app_store]" read number done #-----------脚本配置信息----------- # .xcworkspace的名字,必填 workspace_name="xxxxx" # 指定项目的scheme名称(也就是工程的target名称),必填 scheme_name="xxxxx" # 指定要打包编译的方式 : Release,Debug。一般用Release。必填 build_configuration="Release" # method,打包的方式。方式分别为 development, ad-hoc 。必填 method="enterprise" # 下面两个参数只是在手动指定Pofile文件的时候用到,如果使用Xcode自动管理Profile,直接留空就好 # (跟method对应的)mobileprovision文件名,需要先双击安装.mobileprovision文件.手动管理Profile时必填 # mobileprovision_name="9d8c7290-4345-4ebf-82d4-a74cab2ea40b" mobileprovision_name="" # 项目的bundleID,手动管理Profile时必填 bundle_identifier="" # if [[ $number == 1 ]]; then # bundle_identifier="com.mi.global.sho" # else # bundle_identifier="com.mi.global.shop" # fi # 每次编译后是否Build自动加1, # 可以修改该常量的值,以决定编译后还是打包后Build自动加1 # # 0: 每次打包后Build自动加1 # # 1: 每次编译后Build自动加1 DEBUG_ENVIRONMENT_SYMBOL=0 # 根据选项配置不同的包 if [ $number == 1 ];then build_configuration="Debug" method="enterprise" DEBUG_ENVIRONMENT_SYMBOL=1 elif [[ $number == 2 ]]; then build_configuration="Release" method="enterprise" DEBUG_ENVIRONMENT_SYMBOL=1 elif [[ $number == 3 ]]; then build_configuration="Release" method="ad-hoc" DEBUG_ENVIRONMENT_SYMBOL=1 else build_configuration="Release" method="app-store" DEBUG_ENVIRONMENT_SYMBOL=1 fi echo "--------------------脚本配置参数检查--------------------" echo "\033[33;1mworkspace_name = ${workspace_name}" echo "scheme_name = ${scheme_name}" echo "build_configuration = ${build_configuration}" echo "bundle_identifier = ${bundle_identifier}" echo "method = ${method}" echo "mobileprovision_name = ${mobileprovision_name} \033[0m" # =======================脚本的一些固定参数定义(无特殊情况不用修改)====================== # # 获取当前脚本所在目录 script_dir="$( cd "$( dirname "$0" )" && pwd )" # 工程根目录 project_dir=$script_dir # 指定输出导出文件夹路径 export_path="$project_dir/Build" # 指定输出归档文件路径 export_archive_path="$export_path/$scheme_name.xcarchive" # 指定输出ipa文件夹路径 export_ipa_path="$export_path/" # 指定导出ipa包需要用到的plist配置文件的路径 export_options_plist_path="$project_dir/ExportOptions.plist" echo "--------------------脚本固定参数检查--------------------" echo "\033[33;1mproject_dir = ${project_dir}" echo "export_path = ${export_path}" echo "export_archive_path = ${export_archive_path}" echo "export_ipa_path = ${export_ipa_path}" echo "export_options_plist_path = ${export_options_plist_path}\033[0m" # =======================自动打包部分(无特殊情况不用修改)====================== # echo "------------------------------------------------------" echo "\033[32m开始构建项目 \033[0m" # 进入项目工程目录 cd ${project_dir} # 指定输出文件目录不存在则创建 if [ -d "$export_path" ]; then rm -rf "$export_path" fi /usr/bin/xcrun xcodebuild -UseNewBuildSystem=YES -xcconfig InnerXcconfig/innerInner/tt.xcconfig # 编译前清理工程 xcodebuild clean -workspace ${workspace_name}.xcworkspace \ -scheme ${scheme_name} \ -configuration ${build_configuration} xcodebuild archive -workspace ${workspace_name}.xcworkspace \ -scheme ${scheme_name} \ -configuration ${build_configuration} \ -archivePath ${export_archive_path} # 检查是否构建成功 # xcarchive 实际是一个文件夹不是一个文件所以使用 -d 判断 if [ -d "$export_archive_path" ] ; then echo "\033[32;1m项目构建成功 🚀 🚀 🚀 \033[0m" else echo "\033[31;1m项目构建失败 😢 😢 😢 \033[0m" exit 1 fi echo "------------------------------------------------------" echo "\033[32m开始导出ipa文件 \033[0m" # 先删除export_options_plist文件 if [ -f "$export_options_plist_path" ] ; then #echo "${export_options_plist_path}文件存在,进行删除" rm -f $export_options_plist_path fi # 根据参数生成export_options_plist文件 /usr/libexec/PlistBuddy -c "Add :method String ${method}" $export_options_plist_path /usr/libexec/PlistBuddy -c "Add :provisioningProfiles:" $export_options_plist_path /usr/libexec/PlistBuddy -c "Add :provisioningProfiles:${bundle_identifier} String ${mobileprovision_name}" $export_options_plist_path /usr/libexec/PlistBuddy -c "Add :compileBitcode bool NO" $export_options_plist_path xcodebuild -exportArchive \ -archivePath ${export_archive_path} \ -exportPath ${export_ipa_path} \ -exportOptionsPlist ${export_options_plist_path} \ -allowProvisioningUpdates # 检查文件是否存在 if [ -f "$export_ipa_path/$scheme_name.ipa" ] ; then echo "\033[32;1m导出 ${scheme_name}.ipa 包成功 🎉 🎉 🎉 \033[0m" open $export_path else echo "\033[31;1m导出 ${scheme_name}.ipa 包失败 😢 😢 😢 \033[0m" exit 1 fi # 删除export_options_plist文件(中间文件) if [ -f "$export_options_plist_path" ] ; then #echo "${export_options_plist_path}文件存在,准备删除" rm -f $export_options_plist_path fi # 输出打包总用时 echo "\033[36;1m使用AutoPackageScript打包总用时: ${SECONDS}s \033[0m" echo "------------------------------------------------------" # AppStore上传到xxx if [ $number == 4 ];then # 将包上传AppStore ipa_path="$export_ipa_path/$scheme_name.ipa" # 上传AppStore的密钥ID、Issuer ID api_key="xxxxx" issuer_id="xxxxx" echo "--------------------AppStore上传固定参数检查--------------------" echo "ipa_path = ${ipa_path}" echo "api_key = ${api_key}" echo "issuer_id = ${issuer_id}" # 校验 + 上传 方式1 # # 校验指令 # cnt0=`xcrun altool --validate-app -f ${ipa_path} -t ios --apiKey ${api_key} --apiIssuer ${issuer_id} --verbose` # echo $cnt0 # cnt=`echo $cnt0 | grep “No errors validating archive” | wc -l` # if [ $cnt = 1 ] ; then # echo "\033[32;1m校验IPA成功🎉 🎉 🎉 \033[0m" # echo "------------------------------------------------------" # cnt0=`xcrun altool --upload-app -f ${ipa_path} -t ios --apiKey ${api_key} --apiIssuer ${issuer_id} --verbose"` # echo $cnt0 # cnt=`echo $cnt0 | grep “No errors uploading” | wc -l` # if [ $cnt = 1 ] ; then # echo "\033[32;1m上传IPA成功🎉 🎉 🎉 \033[0m" # echo "------------------------------------------------------" # else # echo "\033[32;1m上传IPA失败😢 😢 😢 \033[0m" # echo "------------------------------------------------------" # fi # else # echo "\033[32;1m校验IPA失败😢 😢 😢 \033[0m" # echo "------------------------------------------------------" # fi # 校验 + 上传 方式2 # 验证 validate="xcrun altool --validate-app -f ${ipa_path} -t ios --apiKey ${api_key} --apiIssuer ${issuer_id} --verbose" echo "running validate cmd" $validate validateApp="$($validate)" if [ -z "$validateApp" ]; then echo "\033[32m校验IPA失败😢 😢 😢 \033[0m" echo "------------------------------------------------------" else echo "\033[32m校验IPA成功🎉 🎉 🎉 \033[0m" echo "------------------------------------------------------" # 上传 upload="xcrun altool --upload-app -f ${ipa_path} -t ios --apiKey ${api_key} --apiIssuer ${issuer_id} --verbose" echo "running upload cmd" $upload uploadApp="$($upload)" echo uploadApp if [ -z "$uploadApp" ]; then echo "\033[32m传IPA失败😢 😢 😢 \033[0m" echo "------------------------------------------------------" else echo "\033[32m上传IPA成功🎉 🎉 🎉 \033[0m" echo "------------------------------------------------------" fi fi fi exit 0
这里额外补充下xcodebuild、xcrun命令的知识:
xcodebuild
用于编译xcode中的projects和workspaces,可通过man xcodebuild查看文档
iOS_lowlevel_38_01_12.jpg
常见的命令格式如下:
- 清理工程:
xcodebuild clean -workspace [xcworkspace路径] \ -scheme [scheme名称] \ -configuration [编译方式Release或Debug]
- 编译工程
xcodebuild archive -workspace [xcworkspace路径] \ -scheme [scheme名称]\ -configuration [编译方式Release或Debug] \ -archivePath [.xcarchive文件路径]
- 导出ipa
xcodebuild -exportArchive \ -archivePath [.xcarchive文件路径] \ -exportPath [.ipa文件路径] \ -exportOptionsPlist [ExportOptions.plist文件路径] \ -allowProvisioningUpdates
xcrun
用于运行或定位开发工具以及属性,也可以通过man xcrun查看具体文档
xcrun文档.jpg
常见的命令格式如下:
- 校验ipa
xcrun altool --validate-app -f [.ipa的路径] -t ios --apiKey [api_key] --apiIssuer [issuer id] --verbose
- 上传ipa到AppStore
xcrun altool --upload-app -f [.ipa的路径] -t ios --apiKey [api_key] --apiIssuer [issuer id] --verbose
其中上传所需的api key 和 issuer id需要在AppStore Connect中配置和获取。使用具有账户管理权限的账号登陆App Store Connect 选择 用户与访问 >密钥 查询信息:issuser和apiKey(密钥 ID),如下所示,
将公钥下载到本地,并将下载好的p8文件保存到需要放到一个固定目录下
P8文件放入以下文件中 ./private_keys ~/private_keys ~/.private_keys ~/.appstoreconnect/private_keys
除了使用xcrun altool上传ipa,我们还可以使用Transpoter,但相对来说,Shell脚本更方便,实现了本地自动化打包上传。
综上所述,shell脚本打包整体流程如下