最近對我Xcode中的工程進(jìn)行了整理,這不Xcode 9也要出來了,先總結(jié)一下做個(gè)記錄。
做這些事,是為了解決我目前遇到的問題。簡單描述下問題,一個(gè)工程,里面有多個(gè)target(20+而且還會(huì)增長),每個(gè)發(fā)行的App都用對應(yīng)的target打包。這些App,bundle id不同,icon不同,launch screen不同,以及連接的服務(wù)器不同,有個(gè)別的幾個(gè)由于特殊的需求,代碼中有宏進(jìn)行分支。這些icon,launch screen,和服務(wù)器等配置都是通過別的方式動(dòng)態(tài)提供,并不放在我工程的源碼里(因?yàn)槊總€(gè)target都不同,且隨時(shí)可能發(fā)生變化,我的源碼不需要維護(hù)這個(gè)變化)。
我原來的方法是完成下面幾步:
1,在xcode中depulicate一個(gè)模板target;
2,獲取這個(gè)新target對應(yīng)的icon等配置;
3,menu中File->Add將Assets,launchScreen.storyboard,plist等文件都添加給這個(gè)target;
4,Podfile中加入這個(gè)target,并執(zhí)行pod install
這樣的方法,很麻煩,步驟多,容易錯(cuò),而且,當(dāng)需求比較密集的時(shí)候,我真的是很煩這種重復(fù)的手工勞動(dòng)。
不得已,我必須改變現(xiàn)在工程配置的方式,現(xiàn)在問題已經(jīng)完美解決,整個(gè)工程里,只有一個(gè)target,配合一個(gè)腳本,可以進(jìn)行茫茫多target的管理了。
廢話不多說,下面是這次實(shí)踐中的一些關(guān)鍵點(diǎn)的總結(jié):
1,Cocoa Pods 中,多個(gè)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
或者也可以寫成這樣,方便對個(gè)別target進(jìn)行單獨(dú)依賴庫配置:
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
然后在控制臺執(zhí)行:
pod install
生成靜態(tài)庫自動(dòng)加入到target中,可以通過TARGETS->General->Linked Frameworks and Libraries 中查看被加入文件libPods-default-target1.a
2,對工程文件的修改
工程文件位于MyProject/MyProject.xcodeproj中。右鍵->顯示包內(nèi)容,可以看到里面的文件project.pbxproj。這個(gè)文件就是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版本時(shí)使用的配置:
# App icon使用的assets文件,在TARGETS->General->App Icons and Launch Images中選中的值
ASSETCATALOG_COMPILER_APPICON_NAME
# Launch Images,同上
ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME
# 簽名文件,如果有push這種需要單獨(dú)簽名的功能,在Capabilities中打開push開關(guān)后,即生成一個(gè)entitlements文件,與target同名。如果target要使用另外的entitlement文件,在Xcode中配置即可,配置路徑TARGETS->Build Settings->Signing->Code Signing Entitlements
CODE_SIGN_ENTITLEMENTS
# 開發(fā)者ID,TARGETS->General->Signing中配置的開發(fā)者
DEVELOPMENT_TEAM
# target使用的Info.plist,可以放在其他路徑。加入方式是在menu->File->Add添加plist文件進(jìn)工程,然后將target正在使用的plist文件刪掉,這時(shí)Xcode->TARGETS->General會(huì)提示沒有plist,點(diǎn)擊button,在彈出的列表中選擇剛才加入的plist文件即可。選中后,General中會(huì)列出plist中的信息,而且與plist有關(guān)的文件的path也會(huì)被同步更新,不需要手動(dòng)進(jìn)行修改。
INFOPLIST_FILE
# 編譯選項(xiàng),可以加入宏控制語句,寫法為-DXXXXX,在代碼中就可以使用 #ifdef XXXXX #endif 或者 #define #endif。與C相同。
OTHER_CFLAGS
# link選項(xiàng),一般使用pod第三方庫后,會(huì)自動(dòng)被加入一些鏈接選項(xiàng)
OTHER_LDFLAGS
# App bundle ID,在TARGETS->General中顯示的Bundle Identifier。Xcode 8中,Info.plist中對應(yīng)的字段為:
# <key>CFBundleIdentifier</key>
# <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
# 即plist中使用project文件中的這個(gè)字段,所以如果要修改bundle id,最好是通過project文件修改此字段,而不要直接修改plist文件
PRODUCT_BUNDLE_IDENTIFIER
我現(xiàn)在的管理工程和打包指定target的方法就是,在終端運(yùn)行一個(gè)我自己寫的腳本,主要?jiǎng)幼魇牵?br>
1,獲取指定target的配置包(包含icon,launch screen,plist等),plist中的display name每個(gè)target不同,其它內(nèi)容都相同;
2,把配置包中的文件都拷貝到相應(yīng)的路徑,確保project文件能找到這些文件(如果找不到,在打開xcode工程后,就開始報(bào)錯(cuò),提示找不到文件);
3,替換project文件中的PRODUCT_BUNDLE_IDENTIFER, 我的經(jīng)驗(yàn)是,不要用編輯器打開后,手動(dòng)替換,那樣保存后,project可能就不能用了(沒有具體研究,猜想是編碼等的問題),使用awk和sed進(jìn)行替換,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 后需要跟一個(gè)空串"",而且如果是在腳本中,后面最好使用雙引號(在控制臺上測試語句時(shí)用單引號沒問題,但是在腳本中也用單引號就不行了,必須雙引號)
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時(shí)自動(dòng)創(chuàng)建的(auto create schemes),但是,如果是從SVN中新check out出來的代碼,不打開Xcode工程,要用fastlane gym, xcodebuild等工具直接編譯,尤其在有workspace,編譯時(shí)需要提供的是scheme而不是target的情況下,新拿回來的工程中沒有scheme,必須要打開一次Xcode生成scheme么?答案是,可以自己用Ruby生成一下scheme。以下是檢查有沒有scheme,如果沒有就recreate的腳本,包含一個(gè)bash和一個(gè)ruby:
# bash,用于檢查project中是否有所需的scheme
#!/bin/bash
# 此腳本用于檢查工程中是否包含指定scheme
# 參數(shù):
# $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
# 沒有找到對應(yīng)scheme, 重新生成schemes
echo "沒有找到對應(yīng)的scheme: ${SCHEME}, 重新生成全部schemes"
ruby RecreateSchemes.rb
else
echo "找到對應(yīng)的scheme: ${SCHEME}"
exit 0
fi
# 重新生成schemes后再次檢查
check_schemes
RESULT=$?
if [ ${RESULT} -eq 0 ];then
# 沒有找到對應(yīng)的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,針對個(gè)別需要在編譯時(shí)加入CFLAG進(jìn)行條件控制的target,可以使用下面的編譯選項(xiàng):
# 1) 使用xcodebuild進(jìn)行編譯
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的參數(shù)需要有單引號
# 2) 使用fastlane gym,代碼中使用方式是 #if TARGET1 #endif ,通過設(shè)置-DTARGET1 =1或-DTARGET1 =0進(jìn)行條件編譯
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參數(shù)需要單引號才能正常執(zhí)行
# 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中調(diào)用的xcodebuild時(shí),依然OTHER_CFLAGS后參數(shù)有單引號。
6,最后是關(guān)于在member center上申請push證書和provisioning profile的。一直以來的操作是先在Xcode上建好target,填好新的bundle后,一選signing,自動(dòng)就在member center對應(yīng)帳號的App ID中創(chuàng)建出來了。這次反著來,在member center直接創(chuàng)建對應(yīng)的App ID,申請push證書和profile,下載profile安裝。然后再直接用腳本替換project文件中的BUNDLE_ID,直接做出對應(yīng)的app。這樣,每次添加一個(gè)新的target(即對應(yīng)添加一個(gè)新的app)只需要在member center中進(jìn)行設(shè)置-配置-下載證書-安裝證書,代碼和工程不需要發(fā)生任何改變,也不需要因?yàn)樾绿砑恿藅arget而提交代碼了:)