Xcode——建立你自己的Framework,xcodeframework
(註:以下內容是基於Xcode7.2.1操作的,版本不一,可能介面內容不同!)
如果你想將你開發的控制項與別人分享,一種方法是直接提供原始碼檔案。然而,這種方法並不是很優雅。它會暴露所有的實現細節,而這些實現你可能並不想開源出來。此外,開發人員也可能並不想看到你的所有代碼,因為他們可能僅僅希望將你的這份漂亮代碼的一部分植入自己的應用中。
另一種方法是將你的代碼編譯成靜態庫(library),讓其他開發人員添加到自己的項目中。然而,這需要你一併公布所有的公開的標頭檔,實在是非常不方便。
你需要一種簡單的方法來編譯你的代碼,這種方法應該使得你的代碼易分享,並且在多個工程中易複用。你需要的是一種方法來打包你的靜態庫,將所有的標頭檔放到一個單元中,這樣你就可以立刻將其加入到你的項目中並使用。
非常幸運,這正是本篇教程所要解決的問題。你將會學到製作並使用Framework,協助你解決這個頭疼的問題。OS X完美地支援這一點,因為Xcode就提供了一個項目模板,包含著預設構建目標(target)和可以容納類似於圖片、聲音、字型等資源的檔案。你可以為iOS建立Framework,不過這是一個比較複雜的手工活,如果你跟著教程走,你將學到怎麼樣跨過路障,順利地完成Framework的建立。
什麼是Framework?
Framework是資源的集合,將靜態庫和其標頭檔包含到一個結構中,讓Xcode可以方便地把它納入到你的項目中。
在OS X上,可能會建立一個動態串連(Dynamically Linked)的framework。通過動態串連,framework可以更新,不需要應用重新串連。在運行時,庫中代碼的一份拷貝被分享出來,整個工程都可以使用它,因此,這樣減少了記憶體消耗,提高了系統的效能。正如你看到的,這是一個功能強大的特性。
在iOS上,你不能用這種方式添加為系統添加自訂的framework,因此僅有的動態連結的framework只能是Apple提供的那些。
建立一個靜態庫工程
開啟Xcode,點擊File - New - Project,選擇iOS - Framework and Library - Cocoa Touch Static Library建立一個靜態庫工程.
將工程命名為你要建立的靜態庫名(這裡我用ZYResource命名),然後將工程儲存到一個空目錄下
一個靜態庫工程由標頭檔和實現檔案組成,這些檔案將被編譯為庫本身。
為了方便其他開發人員使用你的庫和framework,你將進行一些操作,讓他們僅需要匯入一個標頭檔便可以訪問所有你想公開的類。
當建立靜態庫工程時,Xcode會自動添加ZYResource.h和ZYResource.m。你不需要實現檔案,因此按右鍵ZYResource.m選擇delete,將它刪除到廢紙簍中。
匯入UIKit的標頭檔,這是建立一個庫所需要的。當你在建立不同的組成類時,你將會將它們添加到這個檔案中,確保它們能夠被庫的使用者擷取到。
你所構建的項目依賴於UIKit,然而Xcode的靜態庫工程不會自動連接到UIKit。要解決這個問題,就要將UIKit作為依賴庫添加到工程中。
在工程導覽列中選擇工程名,然後在中央面板中選擇ZYResource目標。
點擊BuildPhases,展開Link Binary with Libraries這一部分,點擊+添加一個新的framework,找到UIKit.framework,點擊add添加進來。
開啟ZYResource.h,將所有內容替換為:
接下來,你需要在build欄中添加新的phase,來包含所有標頭檔,並將它們放到編譯器可以擷取到的某個地方。然後,你將會拷貝這些到你的framework中。
依然是在Xcode的Build Phases介面,選擇Editor - Add Build Phase - Add Headers Build Phase。
Note:如果你發現按上面找到的功能表項目是灰色的(不可點擊的),點擊下方Build Phases介面的白色地區來擷取Xcode的應用焦點,然後重新試一下。
把ZYResource.h從項目導覽列中拖到中央面板的Headers下的Public部分。這一步確保任何使用你的庫的使用者均可以擷取該標頭檔
Note:顯然,所有包含在你的公用標頭檔中的標頭檔必須是對外公開的,這一點非常重要。否則,開發人員在使用你的庫時會得到編譯錯誤。
如果Xcode在讀取公用標頭檔時不能讀到你忘記設為public的標頭檔,這實在是太令人沮喪了。
既然你已經設定好你的工程了,是時候為你的庫添加一些功能了。由於本篇教程的關鍵在於教你怎麼樣建立一個framework,
而不是怎麼樣構建一個UI控制項,這裡你將要匯入Framework的檔案從Finder中拖到Xcode下工程目錄下。
選擇Copy items info needed,點擊下方的選擇框,確保ZYResource靜態庫目標被選中。
這一步預設把實現檔案添加到編譯列表,把標頭檔添加到Project組。這意味著它們目前是私人的
Note:在你弄清楚之前,這三個組的名稱可能會讓你迷惑,Public是你期望的,Private下的標頭檔依然是可以暴露出來的,
因此名字可能有些誤導。諷刺的是,在Project下的標頭檔對你的工程來說才是“私人”的,因此,你將會更多地希望你的標頭檔或者在Public下,或者在Project下。
現在,你需要將控制項的標頭檔分享出來(這裡我用UICombox.h),有幾種方式可以實現這一點,首先是在Headers面板中將這個標頭檔從Project欄拖到Public欄。
或者,你可能會發現,更簡單的方法是,編輯檔案,改變Target Membership面板下的membership。這個選項更方便一些,可以讓你不斷添加檔案,擴充你的庫。
Note:如果你不斷往庫中添加新的類,記得及時更新這些類的關係(membership),使儘可能少的類成為public,並確保其他非public的標頭檔都在Project下。
另外,注意拖進來的檔案或檔案夾不要有圖片,靜態庫裡面是不包含圖片的,圖片的後面需要另做處理
對你的控制項的標頭檔需要做的另一件事是將其添加到庫的主標頭檔ZYResource.h中。
在這個主標頭檔的協助下,開發人員使用你的庫僅僅需要匯入一個標頭檔,就像你使用UIKit一樣,只用匯入<UIKit/UIKit.h>,而不是自己去選擇自己需要的一塊匯入。
配置Build Settings
現在距離構建這個項目、建立靜態庫已經非常接近了。不過,這裡要先進行一些配置,讓我們的庫對於使用者來說更友好。
首先,你需要提供一個目錄名,表示你將把拷貝的公用標頭檔存放到哪裡。這樣確保當你使用靜態庫的時候可以定位到相關標頭檔的位置。
在項目導覽列中點擊項目名,然後選擇ZYResource靜態庫目標,選擇Build Setting欄,然後搜尋public header,
雙擊Public Headers Folder Path,在彈出視圖中鍵入內容:include/$(PROJECT_NAME)
一會你就會看到這個目錄了。
現在你需要改變一些其他的設定,尤其是那些在二進位庫中遺留下的設定,編譯器提供給你一個選項,
來消除無效代碼:永遠不會被執行的代碼。當然你也可以移除掉一些debug用符號,例如某些函數名稱或者其他跟debug相關的細節。
因為你正在建立framework供他人使用,最好禁掉這些功能(無效代碼和debug用符號),
讓使用者自己選擇對自己的項目有利的部分使用。和之前一樣,使用搜尋方塊,改變下述設定:
編譯然後運行,到目前為止沒什麼可看的,不過確保項目可以成功構建,沒有錯誤和警報是非常好的。
選擇目標為iOS Device,按下command + B進行編譯,一旦成功,工程導覽列中Product目錄下libZYResource.a檔案將從紅色變為黑色,
表明現在該檔案已經存在了。按右鍵libZYResource.a,選擇Show in Finder。
再此目錄下,你將看到靜態庫,libZYResource.a,以及其他你為標頭檔指定的目錄。注意到,正如你所期望的,那些定為public的標頭檔可以在此看到。
建立一個依賴開發(Dependent Development)工程
在無法看到真實效果的情況下為iOS開發一個UI控制項陳列庫是極其困難的,而這是我們現在面臨的問題。
沒有人期望你閉著眼睛開發出一個UI控制項,因此在這一部分你將建立一個新的Xcode工程,該工程依賴於你剛剛建立好的庫。
這意味著允許你使用樣本app建立一個framework。當然,這部分代碼將和庫本身完全分離,結構會非常清晰。
選擇File - Close Project關閉之前的靜態庫工程,使用File - New - Project建立一個新的工程,
選擇iOS - Application - Single View Application,將新工程命名為ZTResource,將項目儲存到和之前的ZYResource相同的目錄下。
添加ZYResource依賴庫,將ZYResource.xcodeproj從Finder中拖到Xcode中ZTResource組下
現在你可以在你的工程中導航到庫工程了,這樣做非常好,因為這樣意味著你可以在庫中編輯代碼,並且運行樣本工程來測試你做的改變。
Note:你無法將同一工程在兩個Xcode視窗中同時開啟,如果你發現你無法在你的工程中導航到庫工程的話,檢查一下是否庫工程在其他Xcode視窗中開啟了。
現在,你將添加靜態庫作為執行個體項目的依賴庫:
在項目導覽列中選擇ZTResource。
導航到ZTResource目標下Build Phases面板下。
開啟Target Dependencies面板,點擊+按鈕調出選取器。
找到ZYResource靜態庫,選擇並點擊Add。這一步表明當構建應用時,Xcode會檢查是否靜態庫需要重新構建。
為了串連到靜態庫本身,展開Link Binary With Libraries面板,再次點擊+按鈕,
從Workspace組中選擇libZYResource.a然後點擊Add。
這一步確保Xcode可以串連到靜態庫,就像串連到系統framework(例如UIKit)一樣。
像這樣使用嵌套工程的好處是你可以對庫本身做出修改,而不用離開樣本工程,即使你同時改變兩個地方的代碼也一樣。
每次你編譯工程,你都要檢查是否將標頭檔的public/project關係設定正確。如果執行個體工程中缺失了任何需要的標頭檔,它都不能被編譯。
建立一個Framework
到現在,你可能迫不及待地點著腳趾頭,想著什麼時候framework可以出來。可以理解,
因為到現在為止你已經做了許多工作,然而卻沒有看到過framework的身影。
現在該有所改變了,你之所以到現在都沒有建立一個framework,
是因為framework本身就是靜態庫加上一組標頭檔——實際上正是你已經建立好的東西。
當然,framework也有幾點不同之處:
目錄結構。Framework有一個能被Xcode識別的特殊的目錄結構,你將會建立一個build task,由它來為你建立這種結構。
片段(Slice)。目前為止,當你構建庫時,僅僅考慮到當前需要的結構(architecture)。例如,i386、arm7等,為了讓一個framework更有用,對於每一個運行framework的結構,該framework都需要構建這種結構。一會你就會建立一個新的工程,構建所有需要的結構,並將它們包含到framework中。
這一部分非常神奇,不過我們會慢慢地來。實際上它並不像看起來那樣複雜。
Framework結構
一個framework有一個特殊的目錄結構,看起來像是這樣的:
現在你需要在靜態庫構建過程中添加指令碼來建立這種結構,在項目導覽列中選擇ZYResource,然後選擇ZYResource靜態庫目標,
選擇Build Phases欄,然後選擇Editor - Add Build Phase - Add Run Script Build Phase來添加一個新的指令碼。
這一步在build phases部分添加了一個新的面板,這允許你在構建時運行一個Bash指令碼。你希望讓指令碼在build的過程中何時執行,
就把這個面板拖動到列表中相對應的那一位置。對於該framework工程來說,指令碼最後執行,因此你可以讓它保留在預設的位置即可。
雙擊面板標題列Run Script,重新命名為Build Framework。
在指令碼文字框中粘貼下面的Bash指令碼代碼
12345678910111213141516 |
set -e export FRAMEWORK_LOCN= "${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.framework" # Create the path to the real Headers die mkdir -p "${FRAMEWORK_LOCN}/Versions/A/Headers" # Create the required symlinks /bin/ln -sfh A "${FRAMEWORK_LOCN}/Versions/Current" /bin/ln -sfh Versions/Current/Headers "${FRAMEWORK_LOCN}/Headers" /bin/ln -sfh "Versions/Current/${PRODUCT_NAME}" \ "${FRAMEWORK_LOCN}/${PRODUCT_NAME}" # Copy the public headers into the framework /bin/cp -a "${TARGET_BUILD_DIR}/${PUBLIC_HEADERS_FOLDER_PATH}/" \ "${FRAMEWORK_LOCN}/Versions/A/Headers" |
這個指令碼首先建立了ZYResource.framework - Versions - A - Headers目錄,然後建立了一個framework所需要的三個串連符號(symbolic links)。
最後,將公用標頭檔從你之前定義的公用標頭檔路徑拷貝到Versions - A - Headers目錄下,
-a參數確保修飾次數作為拷貝的一部分不會改變,防止不必要的重新編譯。
現在,選擇ZYResource靜態庫scheme,然後選擇iOS Device構建目標,然後使用cmd+B構建。
在ZYResource工程裡Products目錄下按右鍵libZYResource.a靜態庫,然後再一次選擇Show in Finder。
在這次構建目錄中你可以看到ZYResource.framework,可以確定一下這裡展示了正確的目錄結構:
這算是在完成你的framework的過程中邁出了一大步。不過你會注意到這裡並沒有一個靜態lib檔案。這就是我們下一步將要解決的問題。
多架構(Multi-Architecture)編譯
iOS app需要在許多不同的CPU架構下運行:
每個CPU架構都需要不同的位元據,當你編譯一個應用時,無論你目前正在使用那種架構,Xcode都會正確地依照對應的架構編譯。
例如,如果你想跑在虛擬機器上,Xcode只會編譯i386版本(或者是64位機的x86_64版本)。
這意味著編譯會儘可能快地進行,當你歸檔一款app或者構建app的發布版本(release mode)時,Xcode會構建上述三個用於真機的ARM架構。
因此這樣app就可以跑在所有裝置上了。不過,其他的編譯架構又如何呢?
當你建立你的framework時,你自然會想讓所有開發人員都能在所有可能的架構上運行它,不是嗎?你當然想,因為這樣可以從同行那兒得到尊敬與讚美。
因此你需要讓Xcode在所有架構下都進行編譯。這一過程實際上是建立了二進位FAT(File Allocation Table,檔案配置表),它包含了所有架構的片段(slice)。
Note:這裡實際上強調了建立依賴靜態庫的樣本項目的另一個原因:庫僅僅在樣本項目運行所需要的架構下編譯,
只有當有變化的時候才重新編譯,為什麼這一點會讓人激動?因為開發週期會儘可能地縮短。
這裡將使用在ZYResource工程中的一個新的目標來構建framework,在項目導覽列中選擇ZYResource
找到Other - Aggregate,點擊Next,將目標命名為Framework。
Note:為什麼使用集合(Aggregate)目標來建立一個framework呢?為什麼這麼不直接?因為OS X對庫的支援更好一些,事實上,
Xcode直接為每一個OS X工程提供一個Cocoa Framework編譯目標。基於此,你將使用集合編譯目標,作為Bash指令碼的串連串來建立神奇的framework目錄結構。
為了確保每當這個新的framework目標被建立時,靜態連結庫都會被編譯,你需要往靜態庫目標中添加依賴(Dependency)。
在庫工程中選擇Framework目標,在Build Phases中添加一個依賴。展開Target Dependencies面板,點擊 + 按鈕選擇ZYResource靜態庫。
這個目標的主要編譯部分是多平台編譯,你將使用一個指令碼來做到這一點。和你之前做的一樣,在Framework目標下,
選擇Build Phases欄,點擊Editor - Add Build Phase - Add Run Script Build Phase,建立一個新的Run Script Build Phase。
雙擊Run Script,重新命名指令碼的名字。這次命名為MultiPlatform Build。
在指令碼文字框中粘貼下面的Bash指令碼代碼:
1234567891011 |
set -e # If we're already inside this script then die if [ -n "$RW_MULTIPLATFORM_BUILD_IN_PROGRESS" ]; then exit 0 fi export RW_MULTIPLATFORM_BUILD_IN_PROGRESS=1 RW_FRAMEWORK_NAME=${PROJECT_NAME} RW_INPUT_STATIC_LIB= "lib${PROJECT_NAME}.a" RW_FRAMEWORK_LOCATION= "${BUILT_PRODUCTS_DIR}/${RW_FRAMEWORK_NAME}.framework" |
set –e確保指令碼的任何地方執行失敗,則整個指令碼都執行失敗。這樣可以避免讓你建立一個部分有效framework。
接著,用RW_MULTIPLATFORM_BUILD_IN_PROGRESS變數決定是否迴圈呼叫指令碼,如果有該變數,則退出。
然後設定一些變數。該framework的名字與項目的名字一樣。也就是RWUIControls,另外,靜態lib的名字是libRWUIControls.a。
接下來,用指令碼設定一些函數,這些函數一會項目就會用到,把下面的代碼加到指令碼的底部。
12345678910111213141516171819 |
function build_static_library { # Will rebuild the static library as specified # build_static_library sdk xcrun xcodebuild -project "${PROJECT_FILE_PATH}" \ -target "${TARGET_NAME}" \ -configuration "${CONFIGURATION}" \ -sdk "${1}" \ ONLY_ACTIVE_ARCH=NO \ BUILD_DIR= "${BUILD_DIR}" \ OBJROOT= "${OBJROOT}" \ BUILD_ROOT= "${BUILD_ROOT}" \ SYMROOT= "${SYMROOT}" $ACTION } function make_fat_library { # Will smash 2 static libs together # make_fat_library in1 in2 out xcrun lipo -create "${1}" "${2}" -output "${3}" |
build_static_library把SDK作為參數,例如iPhone7.0,然後建立靜態lib,大多數參數直接傳到當前的構建工作中來,
不同的是設定ONLY_ACTIVE_ARCH來確保為當前SDK構建所有的結構。
make_fat_library使用lipo將兩個靜態庫合并為一個,其參數為兩個靜態庫和結果的輸出位置。從這裡瞭解