Xcode——建立你本身的Framework

(注:如下內容是基於Xcode7.2.1操做的,版本不一,可能界面內容不一樣!)

若是你想將你開發的控件與別人分享,一種方法是直接提供源代碼文件。然而,這種方法並非很優雅。它會暴露全部的實現細節,而這些實現你可能並不想開源出來。此外,開發者也可能並不想看到你的全部代碼,由於他們可能僅僅但願將你的這份漂亮代碼的一部分植入本身的應用中。html

另外一種方法是將你的代碼編譯成靜態庫(library),讓其餘開發者添加到本身的項目中。然而,這須要你一併公佈全部的公開的頭文件,實在是很是不方便。ios

你須要一種簡單的方法來編譯你的代碼,這種方法應該使得你的代碼易分享,而且在多個工程中易複用。你須要的是一種方法來打包你的靜態庫,將全部的頭文件放到一個單元中,這樣你就能夠馬上將其加入到你的項目中並使用。正則表達式

很是幸運,這正是本篇教程所要解決的問題。你將會學到製做並使用Framework,幫助你解決這個頭疼的問題。OS X完美地支持這一點,由於Xcode就提供了一個項目模板,包含着默認構建目標(target)和能夠容納相似於圖片、聲音、字體等資源的文件。你能夠爲iOS建立Framework,不過這是一個比較複雜的手工活,若是你跟着教程走,你將學到怎麼樣跨過路障,順利地完成Framework的建立。macos

什麼是Framework?

Framework是資源的集合,將靜態庫和其頭文件包含到一個結構中,讓Xcode能夠方便地把它歸入到你的項目中。xcode

在OS X上,可能會建立一個動態鏈接(Dynamically Linked)的framework。經過動態鏈接,framework能夠更新,不須要應用從新鏈接。在運行時,庫中代碼的一份拷貝被分享出來,整個工程均可以使用它,所以,這樣減小了內存消耗,提升了系統的性能。正如你看到的,這是一個功能強大的特性。bash

在iOS上,你不能用這種方式添加爲系統添加自定義的framework,所以僅有的動態連接的framework只能是Apple提供的那些。架構

建立一個靜態庫工程

打開Xcode,點擊File - New - Project,選擇iOS - Framework and Library - Cocoa Touch Static Library新建一個靜態庫工程.app

將工程命名爲你要建立的靜態庫名(這裏我用ZYResource命名),而後將工程保存到一個空目錄下iphone

一個靜態庫工程由頭文件和實現文件組成,這些文件將被編譯爲庫自己。ide

爲了方便其餘開發者使用你的庫和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用符號),

讓用戶本身選擇對本身的項目有利的部分使用。和以前同樣,使用搜索框,改變下述設置:

  • Dead Code Stripping設置爲NO

  • Strip Debug Symbol During Copy 所有設置爲NO(在我這個版本下,默認爲NO,只用確認一遍便可)

  • Strip Style設置爲Non-Global Symbols



編譯而後運行,到目前爲止沒什麼可看的,不過確保項目能夠成功構建,沒有錯誤和警報是很是好的。

選擇目標爲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也有幾點不一樣之處:

  1. 目錄結構。Framework有一個能被Xcode識別的特殊的目錄結構,你將會建立一個build task,由它來爲你建立這種結構。

  2. 片斷(Slice)。目前爲止,當你構建庫時,僅僅考慮到當前須要的結構(architecture)。例如,i38六、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腳本代碼

set -e  export FRAMEWORK_LOCN="${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.framework"  # Create the path to the real Headers diemkdir -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 - Current => A

  • Headers => Versions - Current - Headers

  • ZYResource => Versions - Current - ZYResource

最後,將公共頭文件從你以前定義的公共頭文件路徑拷貝到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架構下運行:

  • arm7: 在最老的支持iOS7的設備上使用

  • arm7s: 在iPhone5和5C上使用

  • arm64: 運行於iPhone5S的64位 ARM 處理器 上

  • i386: 32位模擬器上使用

  • x86_64: 64爲模擬器上使用

每一個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腳本代碼:

set -e  # If we're already inside this script then dieif [ -n "$RW_MULTIPLATFORM_BUILD_IN_PROGRESS" ]; then exit 0fiexport 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。

接下來,用腳本設置一些函數,這些函數一會項目就會用到,把下面的代碼加到腳本的底部。

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將兩個靜態庫合併爲一個,其參數爲兩個靜態庫和結果的輸出位置。從這裏瞭解更多關於lipo的知識。

爲了使用這兩個方法,接下來腳本將定義更多你要用到的變量,你須要知道其餘SDK是什麼,

例如,iphoneos7.0應該對應iphonesimulator7.0,反過來也同樣。你也須要找到該SDK對應的編譯目錄。

把下面的代碼添加到腳本的底部。

# 1 - Extract the platform (iphoneos/iphonesimulator) from the SDK nameif [[ "$SDK_NAME" =~ ([A-Za-z]+) ]]; then RW_SDK_PLATFORM=${BASH_REMATCH[1]}else echo "Could not find platform name from SDK_NAME: $SDK_NAME" exit 1fi # 2 - Extract the version from the SDKif [[ "$SDK_NAME" =~ ([0-9]+.*$) ]]; then RW_SDK_VERSION=${BASH_REMATCH[1]}else echo "Could not find sdk version from SDK_NAME: $SDK_NAME" exit 1fi # 3 - Determine the other platformif [ "$RW_SDK_PLATFORM" == "iphoneos" ]; then RW_OTHER_PLATFORM=iphonesimulatorelse RW_OTHER_PLATFORM=iphoneosfi # 4 - Find the build directoryif [[ "$BUILT_PRODUCTS_DIR" =~ (.*)$RW_SDK_PLATFORM$ ]]; then RW_OTHER_BUILT_PRODUCTS_DIR="${BASH_REMATCH[1]}${RW_OTHER_PLATFORM}"else echo "Could not find other platform build directory." exit 1fi複製代碼

上面四句聲明都很是類似,都是使用字符串比較和正則表達式來肯定RW_OTHER_PLATFORM和RW_OTHER_BUILT_PRODUCTS_DIR。

詳細解釋一下上面四句聲明:

  1. SDK_NAME將指代iphoneos7.0和iphonesimulator6.1,這個正則表達式取出字符串開頭不包含數字的那些字符,所以,其結果是iphoneos 或 iphonesimulator。

  2. 這個正則表達式取出SDK_NAME中表示版本用的數字,7.0或6.1等。

  3. 這裏用簡單的字符串比較來將iphonesimulator 轉換爲iphoneos,反過來也同樣。

  4. 從構建好的工程的目錄路徑的末尾找出平臺名稱,將其替換爲其餘平臺。這樣能夠確保爲其餘平臺構建的目錄能夠被找到。這是將兩個靜態庫合併的關鍵部分。

如今你能夠啓動腳本爲其餘平臺編譯,而後獲得合併兩靜態庫的結果。

在腳本最後添加下面的代碼:

# Build the other platform.build_static_library "${RW_OTHER_PLATFORM}${RW_SDK_VERSION}" # If we're currently building for iphonesimulator, then need to rebuild# to ensure that we get both i386 and x86_64if [ "$RW_SDK_PLATFORM" == "iphonesimulator" ]; then build_static_library "${SDK_NAME}"fi # Join the 2 static libs into 1 and push into the .frameworkmake_fat_library "${BUILT_PRODUCTS_DIR}/${RW_INPUT_STATIC_LIB}" \ "${RW_OTHER_BUILT_PRODUCTS_DIR}/${RW_INPUT_STATIC_LIB}" \ "${RW_FRAMEWORK_LOCATION}/Versions/A/${RW_FRAMEWORK_NAME}"複製代碼

  • 首先,調用你以前定義好的函數爲其餘平臺編譯

  • 若是你如今正在爲模擬器編譯,那麼Xcode會默認只在該系統對應的結構下編譯,例如i386 或 x86_64。爲了在這兩個結構下都進行編譯,這裏調用了build_static_library,基於iphonesimulator SDK從新編譯,確保這兩個結構都進行了編譯。

  • 最後調用make_fat_library將在當前編譯目錄下的靜態lib同在其餘目錄下地lib合併,依次實現支持多結構的FAT靜態庫。這個被放到了framework中。

腳本的最後是簡單的拷貝命令,將下面代碼添加到腳本最後:

# Ensure that the framework is present in both platform's build directoriescp -a "${RW_FRAMEWORK_LOCATION}/Versions/A/${RW_FRAMEWORK_NAME}" \ "${RW_OTHER_BUILT_PRODUCTS_DIR}/${RW_FRAMEWORK_NAME}.framework/Versions/A/${RW_FRAMEWORK_NAME}" # Copy the framework to the user's desktopditto "${RW_FRAMEWORK_LOCATION}" "${HOME}/Desktop/${RW_FRAMEWORK_NAME}.framework"複製代碼

  • 第一條命令確保framework在全部平臺的編譯目錄中都存在。

  • 第二條將完成的framework拷貝到用戶的桌面上,這一步是可選的,但我發現這樣作能夠很方便的存取framework。



選擇Framework集合方案(aggregate scheme),按下cmd+B編譯該framework。


這一步將構建並在你的桌面上存放一個ZYResource.framework

爲了檢查一下咱們的多平臺編譯真的成功了,啓動終端,導航到桌面上的framework,像下面同樣:

$ cd ~/Desktop/ZYResource.framework$ ZYResource.framework  xcrun lipo -info ZYResource複製代碼

第一條指令導航到framework中,第二行使用lipo指令從ZYResource靜態庫中獲得須要的信息,這將列出存在於該庫中的全部片斷。



如今,你就已經有一個屬於你本身的Framework了。

只須要在拖進工程,包含你的頭文件就能使用了!

至於圖片的處理,本人研究了挺久並無研究明白,這裏給出原文地址,你們能夠去看看,若是有研究明白的,還望告訴本人!

原文地址:www.cocoachina.com/ios/2015012…

注:本文只是本人作一個備忘,不喜勿噴!
相關文章
相關標籤/搜索