最近对我Xcode中的工程进行了整理,这不Xcode 9也要出来了,先总结一下做个记录。
做这些事,是为了解决我目前遇到的问题。简单描述下问题,一个工程,里面有多个target(20+而且还会增长),每个发行的App都用对应的target打包。这些App,bundle id不同,icon不同,launch screen不同,以及连接的服务器不同,有个别的几个由于特殊的需求,代码中有宏进行分支。这些icon,launch screen,和服务器等配置都是通过别的方式动态提供,并不放在我工程的源码里(因为每个target都不同,且随时可能发生变化,我的源码不需要维护这个变化)。
我原来的方法是完成下面几步:
1,在xcode中depulicate一个模板target;
2,获取这个新target对应的icon等配置;
3,menu中File->Add将Assets,launchScreen.storyboard,plist等文件都添加给这个target;
4,Podfile中加入这个target,并执行pod install
这样的方法,很麻烦,步骤多,容易错,而且,当需求比较密集的时候,我真的是很烦这种重复的手工劳动。
不得已,我必须改变现在工程配置的方式,现在问题已经完美解决,整个工程里,只有一个target,配合一个脚本,可以进行茫茫多target的管理了。
废话不多说,下面是这次实践中的一些关键点的总结:
1,Cocoa Pods 中,多个target依赖相同的第三方库,Podfile文件的写法:
abstract_target 'defaults' do
platform:ios,'8.0'
# Podfile是Ruby脚本,此处列出所有需要使用第三方库依赖的target
targetsList = ['target1', 'target2']
targetsList.each do |t|
target t do
# 这些target需要依赖的第三方库
pod 'AFNetworking'
end
end
end
或者也可以写成这样,方便对个别target进行单独依赖库配置:
abstract_target 'defaults' do
platform:ios,'8.0'
pod 'AFNetworking'
targetsList = ['target1', 'target2']
targetsList.each do |t|
target t do
pod 'XXXXX'
end
end
end
然后在控制台执行:
pod install
生成静态库自动加入到target中,可以通过TARGETS->General->Linked Frameworks and Libraries 中查看被加入文件libPods-default-target1.a
2,对工程文件的修改
工程文件位于MyProject/MyProject.xcodeproj中。右键->显示包内容,可以看到里面的文件project.pbxproj。这个文件就是xcode的工程文件,可以用编辑器打开。注意到其中的编译configuration:
xxxxxyyyyyzzzzz /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = aaaaabbbbbbcccc /* Pods-defaults-target1.debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage;
CODE_SIGN_ENTITLEMENTS = MyProject/target1.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
DEVELOPMENT_TEAM = XXXXXXXX;
ENABLE_BITCODE = NO;
HEADER_SEARCH_PATHS = "$(inherited)";
INFOPLIST_FILE = "$(SRCROOT)/Info.plist";
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
LIBRARY_SEARCH_PATHS = "$(inherited)";
OTHER_CFLAGS = "";
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
"-framework",
"\"AVFoundation\"",
"-framework",
"\"UIKit\"",
"-all_load",
);
PRODUCT_BUNDLE_IDENTIFIER = "com.myproject.target-1";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE = "";
};
name = Debug;
};
以上是工程文件中的其中一段,是编译Debug版本时使用的配置:
# App icon使用的assets文件,在TARGETS->General->App Icons and Launch Images中选中的值
ASSETCATALOG_COMPILER_APPICON_NAME
# Launch Images,同上
ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME
# 签名文件,如果有push这种需要单独签名的功能,在Capabilities中打开push开关后,即生成一个entitlements文件,与target同名。如果target要使用另外的entitlement文件,在Xcode中配置即可,配置路径TARGETS->Build Settings->Signing->Code Signing Entitlements
CODE_SIGN_ENTITLEMENTS
# 开发者ID,TARGETS->General->Signing中配置的开发者
DEVELOPMENT_TEAM
# target使用的Info.plist,可以放在其他路径。加入方式是在menu->File->Add添加plist文件进工程,然后将target正在使用的plist文件删掉,这时Xcode->TARGETS->General会提示没有plist,点击button,在弹出的列表中选择刚才加入的plist文件即可。选中后,General中会列出plist中的信息,而且与plist有关的文件的path也会被同步更新,不需要手动进行修改。
INFOPLIST_FILE
# 编译选项,可以加入宏控制语句,写法为-DXXXXX,在代码中就可以使用 #ifdef XXXXX #endif 或者 #define #endif。与C相同。
OTHER_CFLAGS
# link选项,一般使用pod第三方库后,会自动被加入一些链接选项
OTHER_LDFLAGS
# App bundle ID,在TARGETS->General中显示的Bundle Identifier。Xcode 8中,Info.plist中对应的字段为:
# <key>CFBundleIdentifier</key>
# <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
# 即plist中使用project文件中的这个字段,所以如果要修改bundle id,最好是通过project文件修改此字段,而不要直接修改plist文件
PRODUCT_BUNDLE_IDENTIFIER
我现在的管理工程和打包指定target的方法就是,在终端运行一个我自己写的脚本,主要动作是:
1,获取指定target的配置包(包含icon,launch screen,plist等),plist中的display name每个target不同,其它内容都相同;
2,把配置包中的文件都拷贝到相应的路径,确保project文件能找到这些文件(如果找不到,在打开xcode工程后,就开始报错,提示找不到文件);
3,替换project文件中的PRODUCT_BUNDLE_IDENTIFER, 我的经验是,不要用编辑器打开后,手动替换,那样保存后,project可能就不能用了(没有具体研究,猜想是编码等的问题),使用awk和sed进行替换,bash脚本如下:
BUNDLE_ID=com.myproject.target-x
# 从工程文件 MyProject.xcodeproj/project.pbxproj 中获取旧的bundle id
OLD_BUNDLE_ID=$(awk -F '=' '/PRODUCT_BUNDLE_IDENTIFIER/ {print $2; exit}' MyProject.xcodeproj/project.pbxproj | awk -F'"' '{print $2}')
# 替换,注意Mac上sed -i 后需要跟一个空串"",而且如果是在脚本中,后面最好使用双引号(在控制台上测试语句时用单引号没问题,但是在脚本中也用单引号就不行了,必须双引号)
sed -i "" "s/${OLD_BUNDLE_ID}/${BUNDLE_ID}/g" MyProject.xcodeproj/project.pbxproj
# 获取新的bundle id
NEW_BUNDLE_ID=$(awk -F '=' '/PRODUCT_BUNDLE_IDENTIFIER/ {print $2; exit}' MyProject.xcodeproj/project.pbxproj | awk -F'"' '{print $2}')
4,Xcode中的scheme是在打开Xcode时自动创建的(auto create schemes),但是,如果是从SVN中新check out出来的代码,不打开Xcode工程,要用fastlane gym, xcodebuild等工具直接编译,尤其在有workspace,编译时需要提供的是scheme而不是target的情况下,新拿回来的工程中没有scheme,必须要打开一次Xcode生成scheme么?答案是,可以自己用Ruby生成一下scheme。以下是检查有没有scheme,如果没有就recreate的脚本,包含一个bash和一个ruby:
# bash,用于检查project中是否有所需的scheme
#!/bin/bash
# 此脚本用于检查工程中是否包含指定scheme
# 参数:
# $1: 指定的scheme,待检查的scheme
# $2: 使用的用户
# 返回值: 0 找到scheme; -1 未找到
SCHEME=$1
USER=$2
function check_schemes {
scheme_exist=0
ALL_TARGETS_AND_SHCHEMS=$(sudo -u ${USER} xcodebuild -list -project MyProject.xcodeproj)
KEY_STRING="Schemes:"
CONTAIN_SCHEMES=$(echo ${ALL_TARGETS_AND_SHCHEMS} | grep ${KEY_STRING})
if [ -z "${CONTAIN_SCHEMES}" ]; then
echo "此工程没有Schemes"
else
echo "找到全部Schemes"
ALL_SCHEMES=($(echo ${ALL_TARGETS_AND_SHCHEMS##*${KEY_STRING}}))
for one_scheme in "${ALL_SCHEMES[@]}"; do
if [ "${one_scheme}" == "${SCHEME}" ]; then
scheme_exist=1
break
fi
done
fi
return ${scheme_exist}
}
check_schemes
RESULT=$?
if [ ${RESULT} -eq 0 ];then
# 没有找到对应scheme, 重新生成schemes
echo "没有找到对应的scheme: ${SCHEME}, 重新生成全部schemes"
ruby RecreateSchemes.rb
else
echo "找到对应的scheme: ${SCHEME}"
exit 0
fi
# 重新生成schemes后再次检查
check_schemes
RESULT=$?
if [ ${RESULT} -eq 0 ];then
# 没有找到对应的schemes
echo "不存在scheme: ${SCHEME}"
exit -1
fi
Ruby 文件, 用于重新生成schemes。
# RecreateSchemes.rb
#!/usr/bin/env ruby
require 'xcodeproj'
xcproj = Xcodeproj::Project.open("MyProject.xcodeproj")
xcproj.recreate_user_schemes
xcproj.save
可以在build之前先使用上面的脚本检查是否有可以编译的scheme。
5,针对个别需要在编译时加入CFLAG进行条件控制的target,可以使用下面的编译选项:
# 1) 使用xcodebuild进行编译
xcodebuild -project MyProject.xcodeproj -scheme target1 OTHER_CFLAGS='${inherited} -DTARGET1=1'
########
# 输出包含:
Build settings from command line:
OTHER_CFLAGS = ${inherited} -DTARGET1 =1
export OTHER_CFLAGS=" -DTARGET1 =1"
# 可见${inherited}为空
# 注意:OTHER_CFLAGS的参数需要有单引号
# 2) 使用fastlane gym,代码中使用方式是 #if TARGET1 #endif ,通过设置-DTARGET1 =1或-DTARGET1 =0进行条件编译
fastlane gym --workspace MyProject.xcworkspace --scheme target1 --clean --configuration Release --archive_path ~/Desktop/temp/target1 --export_method enterprise --output_directory ~/Desktop/temp --output_name target1-xxx.ipa --xcargs OTHER_CFLAGS="'${inherited} -DTARGET1 =1'"
#########
# 输出包含:
$ xcodebuild -list -workspace MyProject.xcworkspace -configuration Release
$ xcodebuild -showBuildSettings -workspace MyProject.xcworkspace -scheme target1 -configuration Release
+------------------+---------------------------------------------------------+
| Summary for gym 2.57.0 |
+------------------+---------------------------------------------------------+
| workspace | MyProject.xcworkspace |
| scheme | target1 |
| clean | true |
| configuration | Release |
| archive_path | /Users/xxx/Desktop/temp/target1 |
| export_method | enterprise |
| output_directory | /Users/xxx/Desktop/temp |
| output_name | target1-xxx |
| xcargs | OTHER_CFLAGS=' -DTARGET1 =1' |
| destination | generic/platform=iOS |
| build_path | /Users/xxx/Library/Developer/Xcode/Archives/2017-09-21 |
| silent | false |
| skip_package_ipa | false |
| buildlog_path | ~/Library/Logs/gym |
| xcode_path | /Applications/Xcode.app |
+------------------+---------------------------------------------------------+
[11:30:00]: $ set -o pipefail && xcodebuild -workspace MyProject.xcworkspace -scheme target1 -configuration Release -destination 'generic/platform=iOS' -archivePath /Users/xxx/Desktop/temp/target1.xcarchive OTHER_CFLAGS=' -DTARGET1 =1' clean archive | tee /Users/xxx/Library/Logs/gym/target1-target1.log | xcpretty
# 可见fastlane最后在xcodebuild中的CFLAGS参数需要单引号才能正常执行
# 3) 或者,代码中使用方式是 #ifdef TARGET1 #else #endif
fastlane gym --workspace MyProject.xcworkspace --scheme target1 --clean --configuration Release --archive_path ~/Desktop/temp/target1 --export_method enterprise --output_directory ~/Desktop/temp --output_name target1-xxx.ipa --xcargs OTHER_CFLAGS="'${inherited} -DTARGET1'"
注意:
1)${inherited}或者写为${value},是继承工程中配置的CFLAGS,如果没有则为空串;
2)引号的使用,如果使用xcodebuild,直接使用单引号括住CFLAGS;如果使用fastlane gym则需要在单引号外再加上双引号,保证传入fastlane中调用的xcodebuild时,依然OTHER_CFLAGS后参数有单引号。
6,最后是关于在member center上申请push证书和provisioning profile的。一直以来的操作是先在Xcode上建好target,填好新的bundle后,一选signing,自动就在member center对应帐号的App ID中创建出来了。这次反着来,在member center直接创建对应的App ID,申请push证书和profile,下载profile安装。然后再直接用脚本替换project文件中的BUNDLE_ID,直接做出对应的app。这样,每次添加一个新的target(即对应添加一个新的app)只需要在member center中进行设置-配置-下载证书-安装证书,代码和工程不需要发生任何改变,也不需要因为新添加了target而提交代码了:)