iOS 靜態庫製作(Xcode9.0,Framework製作,Bundle製作


基本知識:

1. 庫類別

  • 靜態庫(.a 和.framework)
  • 動態庫(.liby和.framework)

2.靜態庫和動態庫的區別

2.1靜態庫

  • 平時我們用的第三方SDK基本上都是靜態庫。
  • 靜態庫在項目編譯時完整地拷貝至可執行文件中,被多次使用就有多份冗餘拷貝。
  • 靜態庫很大的一個優點是減少耦合性,因爲靜態庫中是不可以包含其他靜態庫的,使用的時候要另外導入它的依賴庫,最大限度的保證了每一個靜態庫都是獨立的,不會重複引用。
  • 靜態庫有.a 和 .framework兩種形式。

2.2動態庫

  • iOS平時使用的系統庫基本是動態庫,比如使用頻率最高的UIKit.framework和Fundation.framework。
  • 動態庫在程序運行時由系統動態加載到內存,供程序調用,系統只加載一次,多個程序共用,節省內存。
  • 動態庫在製作的時候可以直接包含靜態庫,也能自動link所需要的依賴庫。
  • 動態庫有.dylib/.tbd 、.framework兩種形式。
  • 蘋果禁止iOS開發中使用動態庫

3.版本

  • 真機-Debug版本
  • 真機-Release版本
  • 模擬器-Debug版本
  • 模擬器-Release版本

3.1 Debug

  • 含完整的符號信息,以方便調試
  • 不會對代碼進行優化

3.2 Release

  • 不會包含完整的符號信息
  • 的執行代碼是進行過優化的
  • 的大小會比Debug版本的略小
  • 在執行速度方面,Release版本會更快些(但不意味着會有顯著的提升)

基本操作:.a靜態庫製作



1. 新建靜態庫工程

Create a New Xcode project -> Framework&Library -> Cocoa Touch Static Library 
這裏寫圖片描述

文件結構如下:


2. 配置工程環境

2.1配置最低支持版本


2.2設置適配所有模擬器架構重要

project -> buildSeting -> Build Active Architecture Only 設爲NO


3. 新建公開的文件

3.1新建一個名爲“testTool 文件” 公開一個方法“testLog”



3.2添加公開文件

3.2.1 點擊“+” 新增”Header Phase”

3.2.2 添加文件到 project

3.2.3 拖拽.h到 public

最終結果如下:


4. 生成.a文件

此處注意需要生成4個

4.1修改環境


 

這裏可以選擇 Debug 和 Release 環境

這裏寫圖片描述

4.2選擇模擬器+Debug 環境+“cmd+R”生成.a


此處注意需要生成4個 
如此類推打出四種.a

  • 真機-Debug版本
  • 真機-Release版本
  • 模擬器-Debug版本
  • 模擬器-Release版本 

最終生成結果 
這裏寫圖片描述

4.3合併 debug 兩個包和 release 兩個包

注意:這裏的合併指的是 1.debug 下真機+模擬器合併 2.release 下真機+模擬器合併 

4.3.1將4個.a文件拷貝到單獨文件夾

這裏寫圖片描述

4.3.2 合併

打開終端 
命令規則

  • 1
  • 2
  • 3
  • 4

這裏寫圖片描述

標註出是自己起名

同理 release 也是如此 最後生成兩個文件

這裏寫圖片描述

5.使用方法

5.1新建一個新工程 
**5.2引入文件 
5.2.1引入libStaticWork-Debug.a和libStaticWork-Release.a

5.2.2引入 include 文件 
在之前的靜態庫文件找到任意的生成文件 引入 include 文件

這裏寫圖片描述

最後引入結果如下

5.3調用以及結果

這裏寫圖片描述

基本操作:.framework靜態庫製作

1.新建 framework 工程

這裏寫圖片描述

2.新建文件類

這裏寫圖片描述

3.修改工程文件配置

build setting ->搜索 mach -> 修改 mach -O Type ->static Library

這裏寫圖片描述

這裏寫圖片描述

4.引入其他第三方庫(如果有的話)

⚠️注意:導入第三方靜態庫的時候不要選擇添加到target中

這裏寫圖片描述

5.暴露頭文件

拖拽文件到這裏

這裏寫圖片描述

6.生成 .framework

6.1修改環境

這裏寫圖片描述

這裏可以選擇 Debug 和 Release 環境

這裏寫圖片描述

6.2選擇模擬器+Debug 環境+“cmd+R”生成.a

這裏寫圖片描述

此處注意需要生成4個 
如此類推打出四種.a

  • 真機-Debug版本
  • 真機-Release版本
  • 模擬器-Debug版本
  • 模擬器-Release版本 

最終生成結果

這裏寫圖片描述

7.合併 debug 兩個包和 release 兩個包

注意:這裏的合併指的是 1.debug 下真機+模擬器合併 2.release 下真機+模擬器合併 

7.1將4個.a文件拷貝到單獨文件夾

這裏寫圖片描述

7.2 合併

打開終端 
命令規則

  • 1
  • 2
  • 3
  • 4

這裏寫圖片描述

標註出是自己起名

同理 release 也是如此 最後生成兩個文件

這裏寫圖片描述

**7.3替換文件 
7.3.1尋找之前生成的 任意framework 
**7.3.2替換二進制文件

替換前

這裏寫圖片描述

替換後

這裏寫圖片描述

8.使用方法 
8.1新建工程 
8.2應用 framework 
8.3結果如下

這裏寫圖片描述

9.注意的地方

9.1警告 
如果我們什麼都不配置的話 應用 framework 裏面的文件 會產生如下的警告 
missing submodule ‘StaticFramework.framewokrTool”

這裏寫圖片描述

解決方案: 
在 framework 的頭文件也就是創建 framework 時同名的.h裏面引入我們的頭文件

這裏寫圖片描述

就可以了

9.2資源文件 .bundle

靜態庫中有使用到圖片、音視頻等資源文件,可以將這些文件打包成.bundle文件供靜態庫使用。

最簡單的方法是,新建一個文件夾,將圖片、音視頻等資源拖到文件夾中,將文件夾後綴名改爲.bundle. 
靜態庫想要使用裏面的資源的話需要先獲取到該.bundle文件。

  • 1

靜態庫中使用.bundle文件裏面的圖片的方法是:

  • 1
  • 2

⚠️注意:.bundle文件無法封裝到framework裏,需要將.framework,.bundle同時導入項目中才能正常使用

9.3使用category

在製作framework的時候,如果使用了category,則使用該framework的項目運行時會crash,此時需要在該工程中 other linker flags添加一個參數 -ObjC

這裏寫圖片描述

參數說明

  • ObjC:加了這個參數後,鏈接器就會把靜態庫中所有的Objective-C類和分類都加載到最後的可執行文件中

  • all_load:會讓鏈接器把所有找到的目標文件都加載到可執行文件中,但是千萬不要隨便使用這個參數!假如你使用了不止一個靜態庫文件,然後又使用了這個參數,那麼你很有可能會遇到ld: duplicate symbol錯誤,因爲不同的庫文件裏面可能會有相同的目標文件,所以建議在遇到-ObjC失效的情況下使用-force_load參數。

  • force_load:所做的事情跟-all_load其實是一樣的,但是-force_load需要指定要進行全部加載的庫文件的路徑,這樣的話,你就只是完全加載了一個庫文件,不影響其餘庫文件的按需加載


    1.什麼是庫?

    庫是程序代碼的集合,是共享程序代碼的一種方式

    2.根據源代碼的公開情況,庫可以分爲2種類型

    a.開源庫

    公開源代碼,能看到具體實現 ,比如SDWebImage、AFNetworking

    b.閉源庫

    不公開源代碼,是經過編譯後的二進制文件,看不到具體實現。主要分爲:靜態庫、動態庫

    3.靜態庫和動態庫的存在形式

    靜態庫:以.a 和 .framework爲文件後綴名。
    動態庫:以.tbd(之前叫.dylib) 和 .framework 爲文件後綴名。

    .a是純二進制文件,.a文件不能單獨使用,至少要有.h文件配合,而.framework除了二進制文件外,還包含一些資源文件(頭文件,plist等),由於自身包含了頭文件,所以.framework可以單獨使用。

    .a和.framework兩種靜態庫,通常都是把需要用的到圖片或者xib文件存放在一個bundle文件中,而該bundle文件的名字和.a或.framework的名字相同。

    ios 8 以後蘋果放開了動態庫。

    4.靜態庫和動態庫在使用上的區別

    靜態庫:鏈接時會被完整的複製到可執行文件中,被多次使用就有多份拷貝。
    動態庫:鏈接時不復制,程序運行時由系統動態加載到內存,系統只加載一次,多個程序共用(如系統的UIKit.framework等),節省內存。

    5.創建靜態庫可能出於以下幾個理由:

    1.你想將工具類代碼或者第三方插件快捷的分享給其他人而無需拷貝大量文件。

    2.你想讓一些通用代碼處於自己的掌控之下,以便於修復和升級。

    3.你想將庫共享給其他人,但不想讓他們看到你的源代碼。

    4.比如很老的框架使用的是MRC環境下面的框架,那麼我們只需要將其打包成靜態庫就可以了,這樣就不用擔心是不是ARC環境了。

    6、iOS設備架構

    模擬器:
    iPhone4s-iPnone5:i386
    iPhone5s-iPhone7 Plus:x86_64

    真機:
    iPhone3gs-iPhone4s:     armv7
    iPhone5-iPhone5c:        armv7s
    iPhone5s-iPhone7 Plus: arm64

    支持armv7的靜態庫可以在armv7s上正常運行。

    Xcode創建靜態庫詳解(Cocoa Touch Static Library)(本人使用的是Version 6.3)

    一、創建靜態庫文件(.a 文件)

    打開Xcode, 選擇File ----> New ---> Project。 

    選擇iOS ----> Framework & Library ---> Cocoa Touch Static Library。

    點擊Next。按照流程一步一步的創建工程。

     

     

     注意靜態庫文件的版本(4種)

    1.真機-Debug版本

    2.真機-Release版本

    3.模擬器-Debug版本

    4.模擬器-Release版本

    就是在build configuration 裏面進行調整一下debug和release,然後分別在真機和模擬器上面進行編譯。

    結果如下圖:

     

    調試版本(Debug版本) VS 發佈版本(Release版本)

    --------------------------------------------------------------------------------

    - 調試版本會包含完整的符號信息,以方便調試

    - 調試版本不會對代碼進行優化

     

    - 發佈版本不會包含完整的符號信息

    - 發佈版本的執行代碼是進行過優化的

    - 發佈版本的大小會比調試版本的略小

    - 在執行速度方面,調試版本會更快些,但不意味着會有顯著的提升

     所以我們建議在產品即將上線的時候要進行如下圖的調整:

    二、應用靜態庫文件(.a 文件)

    1.想讓靜態庫文件給別人使用,需要將頭文件暴露給別人。按着下面的步驟將頭文件添加進來。

    再點擊libstatic.a右擊show In Finder 就可以查看如下圖:

    2.然後將靜態庫拖進項目中。就能利用頭文件了

     

    3.因爲無論是模擬器還是真機都有不同的架構。所以經常會出現如下找不到某個架構的錯誤

    爲了解決各個機型的模擬器都能用可以有2種方法:

    1.合併各個靜態庫(在終端中執行如下操作)

    cd /Users/XQ/Library/Developer/Xcode/DerivedData/static-bdsgvzfqigaspiaeivnhmmygrhgj/Build/Products/Release-iphonesimulator5s 

    lipo -info libstatic.a 

    Architectures in the fat file: libstatic.a are: i386 x86_64 

    $ cd /Users/XQ/Library/Developer/Xcode/DerivedData/static-bdsgvzfqigaspiaeivnhmmygrhgj/Build/Products/Release-iphonesimulator6 

    $ lipo -info libstatic.a 

    Architectures in the fat file: libstatic.a are: i386 x86_64

     1》.我們可以cd到.a文件所在文件夾的當前目錄

     2》.再執行 lipo -info 靜態庫.a文件

    這樣就可以查詢該靜態庫支持的架構是什麼。

    回到.a文件所在文件夾所在的文件夾目錄:cd ..

    $ cd ..

    $ pwd

    /Users/XQ/Library/Developer/Xcode/DerivedData/static-bdsgvzfqigaspiaeivnhmmygrhgj/Build/Products

     

    合併2個靜態庫

    cd ..

    pwd

    /Users/XQ/Library/Developer/Xcode/DerivedData/static-bdsgvzfqigaspiaeivnhmmygrhgj/Build/Products

    $ lipo -create Release-iphonesimulator5s/libstatic.a  Release-iphonesimulator6/libstatic.a -output lib.a

    fatal error: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/lipo: Release-iphonesimulator5s/libstatic.a and Release-iphonesimulator6/libstatic.a have the same architectures (i386) and can't be in the same fat output file

    由於2個靜態庫都含有相同的架構,所以出現錯誤,因爲我們合併的是Release-iphonesimulator6和Release-iphonesimulator5s之間的版本,都是模擬器的。其實我們在製作靜態庫的時候,無論是在模擬器還是真機的時候,設置Build Active Architecture Only爲no的話,一次打包,如果是模擬器就會適用所有機型的模擬器,如果是真機就會適用所有機型的真機。實際上我們通常合併的是模擬器和真機的靜態庫。模擬器和真機版本合併完後,我們姑且稱之爲“合併版本”吧,那麼分別會有debug合併版本和release合併版本,通常我們會在上線前將debug合併版本換成relaese合併版本(當然release合併版本會支持模擬器和真機)。目前還沒找到release合併版本和debug合併版本合併的方法(估計有,但是不會是這麼簡單的合併)。

    合併模擬器和真機的靜態庫,步驟如下:

    合併前:

    cd /Users/XQ/Library/Developer/Xcode/DerivedData/static-bdsgvzfqigaspiaeivnhmmygrhgj/Build/Products/Debug-iphonesimulator 

    lipo -info libstatic.a 

    input file libstatic.a is not a fat file

    Non-fat file: libstatic.a is architecture: x86_64

    cd /Users/XQ/Library/Developer/Xcode/DerivedData/static-bdsgvzfqigaspiaeivnhmmygrhgj/Build/Products/Debug-iphoneos

    lipo -info libstatic.a 

    Architectures in the fat file: libstatic.a are: armv7 arm64 

    cd ..

    $ pwd

    /Users/XQ/Library/Developer/Xcode/DerivedData/static-bdsgvzfqigaspiaeivnhmmygrhgj/Build/Products

    lipo -create Debug-iphonesimulator/libstatic.a Debug-iphoneos/libstatic.a -output lib.a

     合併後:

    合併後的lib.a同時支持:armv7 x86_64 arm64 架構。(可以看出在製作模擬器的靜態庫的時候並沒有設置Build Active Architecture Only爲no)

    cd /Users/XQ/Library/Developer/Xcode/DerivedData/static-bdsgvzfqigaspiaeivnhmmygrhgj/Build/Products 

    $ pwd

    /Users/XQ/Library/Developer/Xcode/DerivedData/static-bdsgvzfqigaspiaeivnhmmygrhgj/Build/Products

    lipo -info lib.a 

    Architectures in the fat file: lib.a are: armv7 x86_64 arm64 

     2.可以通過配置

     

    小結一下:

     1.編譯靜態庫:項目->Build Phases->Copy File->添加頭文件

     2.模擬器編譯時,挑選高版本(向下兼容,低版本不能在高版本運行)

     3.靜態庫分真機版本和模擬器版本(必須在對應的版本運行)

     4.合併真機版本和模擬器版本(常用)

     lipo -create 真機.a 模擬器.a -output 結果.a

     合併版本更大,開始時使用方便(所以可以開發時使用合併版本,發佈時,使用真機版本)

     5.release版本和debug版本:

     debug版本:調試版本,沒有任何優化,也就是說各種錯誤信息,都將拋出和檢測,相對來說性能低一點,但是方便調試

     release版本:發佈版本,進行了優化,執行效率更高

     提醒:實際開發當中,項目完成後,在debug版本上沒有問題了,一定要去release版本上調試一下,否則也許可能發生一些bug。

    三、調試靜態庫文件(.a 文件)

    因爲靜態庫也是需要不斷的開發,調試,最終才能完美,所以我們應該是不斷的開發,不斷的調試,那麼像上面的那種方式直接建立一個靜態庫項目就很麻煩,所以我們應該是在某一個項目中添加一個靜態庫文件,那麼就可以做到不斷開發,不斷調試。

     1.添加靜態庫target:項目->General->左下角+->添加靜態庫(StaticLib)

     2.在StaticLib文件夾內就可以就行開發靜態庫

     3.項目引入靜態庫:項目->General->Linked Frameworks and Libraries->添加靜態庫

     4.導出靜態庫:選擇左上角房子->同之前導出方式(也就是分別在模擬器和真機上面進行編譯)。

     

    (注意:如果是動態庫的話,上面的Embedded Binaries 也要導入相應的庫 )

    總體思路是我們在項目中新添加靜態庫target,然後在項目中導入靜態庫文件,這個時候scheme選中原本項目(注意是項目),進行編譯看是否通過。所以我們可以通過這樣的方法進行調試靜態庫,如果真的調試到沒有錯誤的時候,可以將scheme處選擇成小房子(靜態庫),然後編譯就可以生成相應的靜態庫。這樣我們就可以邊開發邊經行靜態庫的調試。

    2017.1.24增加腳本打包方法如下:

    建立一個Aggregate target  (使用Xcode Version 8.2.1 (8C1002))

    從上面可以看出來,因爲.a庫是會生成模擬器和真機兩個不同的包的,下面嘗試利用腳本來合成。

    1、我們先按照上面的步驟流程建立一個.a靜態庫(單獨的靜態庫)yooweiSDKTest

    2、建立一個Aggregate target,在Aggregate裏面執行腳本。

    3、新建一個運行腳本

    4、接下來我們就可以在Run Script裏寫我們的腳本了

    把下面的腳本複製到Run Script裏面:

    if [ "${ACTION}" = "build" ]
    then
    
    #要build的target名
    target_Name=${PROJECT_NAME}
        echo "target_Name=${target_Name}"
    
    #build之後的文件夾路徑
    build_DIR=${SRCROOT}/build
        echo "build_DIR=${build_DIR}"
    
    #真機build生成的頭文件的文件夾路徑
    DEVICE_DIR_INCLUDE=${build_DIR}/Release-iphoneos/include/${PROJECT_NAME}
        echo "DEVICE_DIR_INCLUDE=${DEVICE_DIR_INCLUDE}"
    
    #真機build生成的.a文件路徑
    DEVICE_DIR_A=${build_DIR}/Release-iphoneos/lib${PROJECT_NAME}.a
        echo "DEVICE_DIR_A=${DEVICE_DIR_A}"
    
    #模擬器build生成的.a文件路徑
    SIMULATOR_DIR_A=${build_DIR}/Release-iphonesimulator/lib${PROJECT_NAME}.a
        echo "SIMULATOR_DIR_A=${SIMULATOR_DIR_A}"
    
    #目標文件夾路徑
    INSTALL_DIR=${SRCROOT}/Products/${PROJECT_NAME}
        echo "INSTALL_DIR=${INSTALL_DIR}"
    
    #目標頭文件文件夾路徑
    INSTALL_DIR_Headers=${SRCROOT}/Products/${PROJECT_NAME}/Headers
        echo "INSTALL_DIR_Headers=${INSTALL_DIR_Headers}"
    
    #目標.a路徑
    INSTALL_DIR_A=${SRCROOT}/Products/${PROJECT_NAME}/lib${PROJECT_NAME}.a
        echo "INSTALL_DIR_A=${INSTALL_DIR_A}"
    
    #判斷build文件夾是否存在,存在則刪除
    if [ -d "${build_DIR}" ]
    then
    rm -rf "${build_DIR}"
    fi
    
    #判斷目標文件夾是否存在,存在則刪除該文件夾
    if [ -d "${INSTALL_DIR}" ]
    then
    rm -rf "${INSTALL_DIR}"
    fi
    #創建目標文件夾
    mkdir -p "${INSTALL_DIR}"
    
    #build之前clean一下
    xcodebuild -target ${target_Name} clean
    
    #模擬器build
    xcodebuild -target ${target_Name} -configuration Release -sdk iphonesimulator
    
    #真機build
    xcodebuild -target ${target_Name} -configuration Release -sdk iphoneos
    
    #複製頭文件到目標文件夾
    cp -R "${DEVICE_DIR_INCLUDE}" "${INSTALL_DIR_Headers}"
    
    #合成模擬器和真機.a包
    lipo -create "${DEVICE_DIR_A}" "${SIMULATOR_DIR_A}" -output "${INSTALL_DIR_A}"
    
    #打開目標文件夾
    open "${INSTALL_DIR}"
    
    fi

    5、運行程序

    注意scheme選擇yooweiShell,之後正常的run就行了。編譯成功以後,如下:

    我們這個腳本,將合成後的.a庫和頭文件都按照我們想要的方式放好了。其他工程要用這個SDK的話,既可以直接拉MySDK文件夾過去就可以了。如果你想要其他的放法,比如不要Headers文件夾,那就,改一下腳本就可以了。

    6、驗證

    在終端使用命令 “lipo -info .a文件路徑”查看

    $ lipo -info /Users/galahad/Downloads/yooweiSDKTest/Products/yooweiSDKTest/libyooweiSDKTest.a

    Architectures in the fat file: /Users/galahad/Downloads/yooweiSDKTest/Products/yooweiSDKTest/libyooweiSDKTest.a are: armv7 i386 x86_64 arm64

    說明成功了

    7、腳本解釋

    本代碼中用到的核心命令:xcodebuild  ,主要用來編譯Xcode的工程。可以在終端中輸入xcodebuild -h來查看命令的詳情

    介紹一下本腳本中用到的幾個參數:

      • clean
        clean一下工程
      • -configuration Release
        使用Release方式編譯,還可以使用Debug
      • -sdk iphoneos
        真機編譯,還可以使用-sdk iphonesimulator模擬器編譯
      • cp "源文件路徑" "目標文件路徑"
        複製"源文件路徑"的文件到 "目標文件路徑"
      • lipo -create "模擬器.a文件路徑" "真機.a文件路徑" -output "目標.a文件路徑"
        將模擬器和真機的.a包合成。

    用到的一些shell腳本基礎命令:

    (1)echo "想要查看的日誌或重要東西" ,echo相當於OC中的NSLog。運行腳本後,可以在這裏找到log

    (2)變量名=變量值  

    賦值命令。比如將"CrazyStone"賦值給MyName變量

    MyName=CrazyStone

     (3)${變量名}  

    取出變量名的內容。比如:取出變量MyName中的內容

    ${MyName}

     (4)判斷語句

    if [ 條件語句 ]then
    ...
    fi

    條件語句爲真就執行then後面的語句,不成立就結束判斷語句
    本腳本中用到的判斷語句:
    [ -d "文件夾路徑" ] :判斷是否爲文件夾

     

    腳本結構解釋

    看完上面,我想你再看一下代碼應該就能理解腳本,然後可以做一些簡單的改動了。下面再介紹一下腳本的結構。

    if [ "${ACTION}" = "build" ]
    then
    #我們的大部分腳本代碼
    fi

    執行腳本的時候做個判斷,在Xcode裏面build這個工程的時候就執行then後面的腳本

    #要build的target名
    target_Name=${PROJECT_NAME} 
        echo "target_Name=${target_Name}"

    變量target_Name是我們要編譯的target的名字,在這裏指的是工程的名字${PROJECT_NAME},也就是yooweiSDKTest
    順便說一下,ACTIONPROJECT_NAME都是Xcode裏面定義的,這是在Xcode裏面寫腳本的一個好處。

    #build之後的文件夾路徑
    build_DIR=${SRCROOT}/build
        echo "build_DIR=${build_DIR}"
    
    #真機build生成的頭文件的文件夾路徑
    DEVICE_DIR_INCLUDE=${build_DIR}/Release-iphoneos/include/${PROJECT_NAME}
        echo "DEVICE_DIR_INCLUDE=${DEVICE_DIR_INCLUDE}"
    
    #真機build生成的.a文件路徑
    DEVICE_DIR_A=${build_DIR}/Release-iphoneos/lib${PROJECT_NAME}.a
        echo "DEVICE_DIR_A=${DEVICE_DIR_A}"
    
    #模擬器build生成的.a文件路徑
    SIMULATOR_DIR_A=${build_DIR}/Release-iphonesimulator/lib${PROJECT_NAME}.a
        echo "SIMULATOR_DIR_A=${SIMULATOR_DIR_A}"

    這裏是定義的build之後各個文件的路徑。我們執行了xcodebuild命令之後,會在工程目錄生成一個build文件夾,裏面有build之後生成的文件。打開Finder看看就知道各個文件的路徑了。


    build目錄的位置
    #目標文件夾路徑
    INSTALL_DIR=${SRCROOT}/Products/${PROJECT_NAME}
        echo "INSTALL_DIR=${INSTALL_DIR}"
    
    #目標頭文件文件夾路徑
    INSTALL_DIR_Headers=${SRCROOT}/Products/${PROJECT_NAME}/Headers
        echo "INSTALL_DIR_Headers=${INSTALL_DIR_Headers}"
    
    #目標.a路徑
    INSTALL_DIR_A=${SRCROOT}/Products/${PROJECT_NAME}/lib${PROJECT_NAME}.a
        echo "INSTALL_DIR_A=${INSTALL_DIR_A}"

    這裏就是定義目標變量的路徑了。你想把文件放在哪裏?在這裏定義咯。${SRCROOT}表示工程的根目錄。用了這麼久的Xcode,這個有用過吧(全局頭文件配置過吧?)?

    #判斷build文件夾是否存在,存在則刪除
    if [ -d "${build_DIR}" ]
    then
    rm -rf "${build_DIR}"
    fi
    
    #判斷目標文件夾是否存在,存在則刪除該文件夾
    if [ -d "${INSTALL_DIR}" ]
    then
    rm -rf "${INSTALL_DIR}"
    fi
    #創建目標文件夾
    mkdir -p "${INSTALL_DIR}"

    這裏就是文件的操作了。如果有這兩個文件夾,就刪除掉。爲什麼?爲了保證我們工程的純淨啊。

    #build之前clean一下
    xcodebuild -target ${target_Name} clean
    
    #模擬器build
    xcodebuild -target ${target_Name} -configuration Release -sdk iphonesimulator
    
    #真機build
    xcodebuild -target ${target_Name} -configuration Release -sdk iphoneos

    這裏就跟平常操作一樣了。先clean一下工程,然後模擬器編譯一次,真機編譯一次。

    #複製頭文件到目標文件夾
    cp -R "${DEVICE_DIR_INCLUDE}" "${INSTALL_DIR_Headers}"
    
    #合成模擬器和真機.a包
    lipo -create "${DEVICE_DIR_A}" "${SIMULATOR_DIR_A}" -output "${INSTALL_DIR_A}"

    關鍵代碼。拷貝頭文件到我們的目標位置去。合成.a包。大功告成。

    #打開目標文件夾
    open "${INSTALL_DIR}"

    最後,打開文件夾。檢查一下文件是否真正生成了。


如果你想將你開發的控件與別人分享,一種方法是直接提供源代碼文件。然而,這種方法並不是很優雅。它會暴露所有的實現細節,而這些實現你可能並不想開源出來。此外,開發者也可能並不想看到你的所有代碼,因爲他們可能僅僅希望將你的這份漂亮代碼的一部分植入自己的應用中。

另一種方法是將你的代碼編譯成靜態庫(library),讓其他開發者添加到自己的項目中。然而,這需要你一併公佈所有的公開的頭文件,實在是非常不方便。

你需要一種簡單的方法來編譯你的代碼,這種方法應該使得你的代碼易分享,並且在多個工程中易複用。你需要的是一種方法來打包你的靜態庫,將所有的頭文件放到一個單元中,這樣你就可以立刻將其加入到你的項目中並使用。

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

當你跟着這篇教程走完後,你將能夠:

  • 使用Xcode構建一個基本的靜態庫工程。

  • 依賴於該靜態庫工程構建一款應用。

  • 掌握如何將靜態庫工程轉換爲完整的、合格的Framework。

  • 最終,你將看到如何將一個圖像文件同Framework一起打包到resource bundle下。

開始

這篇教程的主要目的是解釋怎麼樣在你的iOS工程中創建並使用一個Framework。所以,不像其他網站上的教程,這篇教程將只使用一小部分Objective-C代碼,並且這一小部分主要是爲了說明我們將會遇到的一些概念。

這裏下載可用的資源文件RWKnobControl。如果你在Creating a Static Library Project 這篇文章中完成了創建第一個項目的過程,這裏你將會看到怎麼樣使用去它們。

在創建本工程時,你將要創建的所有的代碼和項目文件都可以在Github上找到。對於本篇教程中每個創建階段都有不同的commit。

什麼是Framework?

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

在OS X上,可能會創建一個動態連接(Dynamically Linked)的framework。通過動態連接,framework可以更新,不需要應用重新連接。在運行時,庫中代碼的一份拷貝被分享出來,整個工程都可以使用它,因此,這樣減少了內存消耗,提高了系統的性能。正如你看到的,這是一個功能強大的特性。

在iOS上,你不能用這種方式添加爲系統添加自定義的framework,因此僅有的動態鏈接的framework只能是Apple提供的那些。(編者注:在iOS 8中已加入此特性,開發者可以使用第三方的動態框架

然而,這並不意味着framework對於iOS而言是無關緊要的,靜態連接的framework依然可以打包代碼,使其在不同的應用中複用。

由於framework本質上是靜態庫的“一站式採購點”,因此在本篇教程中你所做的第一件事就是創建並使用靜態庫。當跟着教程走到如何創建framework時,你就能明白你所做的一切了,整體思路也不會那麼煙霧繚繞了。

創建一個靜態庫工程

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

ios_framework_creating_static_lib-700x482.png

將工程命名爲RWUIControls,然後將工程保存到一個空目錄下。

ios_framework_options_for_static_lib-700x476.png

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

爲了方便其他開發者使用你的庫和framework,你將進行一些操作,讓他們僅需要導入一個頭文件便可以訪問所有你想公開的類。

當創建靜態庫工程時,Xcode會自動添加RWUIControls.h和RWUIControls.m。你不需要實現文件,因此右鍵單擊RWUIControls.m選擇delete,將它刪除到廢紙簍中。

打開RWUIControls.h,將所有內容替換爲:

1
#import < UIKit/UIKit.h>

導入UIKit的頭文件,這是創建一個庫所需要的。當你在創建不同的組成類時,你將會將它們添加到這個文件中,確保它們能夠被庫的使用者獲取到。

你所構建的項目依賴於UIKit,然而Xcode的靜態庫工程不會自動連接到UIKit。要解決這個問題,就要將UIKit作爲依賴庫添加到工程中。在工程導航欄中選擇工程名,然後在中央面板中選擇RWUIControls目標。

點擊BuildPhases,展開Link Binary with Libraries這一部分,點擊+添加一個新的framework,找到UIKit.framework,點擊add添加進來。

ios_framework_add_uikit_dependency.gif

如果不結合頭文件,靜態庫是沒有用的,靜態庫編譯一組文件,在這些文件中類和方法都以二進制數據的形式存在。在你創建的庫中,有些類將能夠被公開訪問到,有些類只能由庫內部訪問並使用。

接下來,你需要在build欄中添加新的phase,來包含所有頭文件,並將它們放到編譯器可以獲取到的某個地方。然後,你將會拷貝這些到你的framework中。

依然是在Xcode的Build Phases界面,選擇Editor\Add Build Phase\Add Copy Headers Build Phase。

Note:如果你發現按上面找到的菜單項是灰色的(不可點擊的),點擊下方Build Phases界面的白色區域來獲取Xcode的應用焦點,然後重新試一下。

ios_framework_add_copy_headers_build_phase.gif

把RWUIControls.h從項目導航欄中拖到中央面板的Copy Headers下的Public部分。這一步確保任何使用你的庫的用戶均可以獲取該頭文件。

ios_framework_add_header_to_public.gif

Note:顯然,所有包含在你的公共頭文件中的頭文件必須是對外公開的,這一點非常重要。否則,開發者在使用你的庫時會得到編譯錯誤。如果Xcode在讀取公共頭文件時不能讀到你忘記設爲public的頭文件,這實在是太令人沮喪了。

創建一個UI控件

既然你已經設置好你的工程了,是時候爲你的庫添加一些功能了。由於本篇教程的關鍵在於教你怎麼樣創建一個framework,而不是怎麼樣構建一個UI控件,這裏你將使用上一篇教程中創建好的控件。在你之前下載好的壓縮包文件中找到RWKnobControl目錄,從Finder中拖到Xcode下RWUIControls目錄下。

ios_framework_drop_rwuiknobcontrol_from_finder-700x466.png

選擇Copy items into destination group’s folder,點擊下方的選擇框,確保RWUIControls靜態庫目標被選中。

ios_framework_import_settings_for_rwknobcontrol-700x475.png

這一步默認把實現文件添加到編譯列表,把頭文件添加到Project組。這意味着它們目前是私有的。

ios_framework_default_header_membership-700x327.png

Note:在你弄清楚之前,這三個組的名稱可能會讓你迷惑,Public是你期望的,Private下的頭文件依然是可以暴露出來的,因此名字可能有些誤導。諷刺的是,在Project下的頭文件對你的工程來說纔是“私有”的,因此,你將會更多地希望你的頭文件或者在Public下,或者在Project下。

現在,你需要將控件的頭文件RWKnobControl.h分享出來,有幾種方式可以實現這一點,首先是在Copy Headers面板中將這個頭文件從Project欄拖到Public欄。

ios_framework_drag_header_to_public.gif

或者,你可能會發現,更簡單的方法是,編輯文件,改變Target Membership面板下的membership。這個選項更方便一些,可以讓你不斷添加文件,擴充你的庫。

ios_framework_header_membership-407x320.png

Note:如果你不斷往庫中添加新的類,記得及時更新這些類的關係(membership),使儘可能少的類成爲public,並確保其他非public的頭文件都在Project下。

對你的控件的頭文件需要做的另一件事是將其添加到庫的主頭文件RWControls.h中。在這個主頭文件的幫助下,開發者使用你的庫僅僅需要導入一個頭文件,如下面的代碼一樣,而不是自己去選擇自己需要的一塊導入。

1
#import < RWUIControls/RWUIControls.h>

因此,在RWUIControls.h中添加下面的代碼:

1
2
// Knob Control
#import

配置Build Settings

現在距離構建這個項目、創建靜態庫已經非常接近了。不過,這裏要先進行一些配置,讓我們的庫對於用戶來說更友好。

首先,你需要提供一個目錄名,表示你將把拷貝的公共頭文件存放到哪裏。這樣確保當你使用靜態庫的時候可以定位到相關頭文件的位置。

在項目導航欄中點擊項目名,然後選擇RWUIControls靜態庫目標,選擇Build Setting欄,然後搜索public header,雙擊Public Headers Folder Path,在彈出視圖中鍵入如圖所示內容:

ios_framework_public_headers_path-700x174.png

一會你就會看到這個目錄了。

現在你需要改變一些其他的設置,尤其是那些在二進制庫中遺留下的設置,編譯器提供給你一個選項,來消除無效代碼:永遠不會被執行的代碼。當然你也可以移除掉一些debug用符號,例如某些函數名稱或者其他跟debug相關的細節。

因爲你正在創建framework供他人使用,最好禁掉這些功能(無效代碼和debug用符號),讓用戶自己選擇對自己的項目有利的部分使用。和之前一樣,使用搜索框,改變下述設置:

  • Dead Code Stripping設置爲NO

  • Strip Debug Symbol During Copy 全部設置爲NO

  • Strip Style設置爲Non-Global Symbols

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

選擇目標爲iOS Device,按下command + B進行編譯,一旦成功,工程導航欄中Product目錄下libRWUIControls.a文件將從紅色變爲黑色,表明現在該文件已經存在了。右鍵單擊libRWUIControls.a,選擇Show in Finder。

ios_framework_successful_first_build-700x454.png

再此目錄下,你將看到靜態庫,libRWUIControls.a,以及其他你爲頭文件指定的目錄。注意到,正如你所期望的,那些定爲public的頭文件可以在此看到。

創建一個依賴開發(Dependent Development)工程

在無法看到真實效果的情況下爲iOS開發一個UI控件庫是極其困難的,而這是我們現在面臨的問題。

沒有人期望你閉着眼睛開發出一個UI控件,因此在這一部分你將創建一個新的Xcode工程,該工程依賴於你剛剛創建好的庫。這意味着允許你使用示例app創建一個framework。當然,這部分代碼將和庫本身完全分離,結構會非常清晰。

選擇File\Close Project關閉之前的靜態庫工程,使用File\New\Project創建一個新的工程,選擇iOS\Application\Single View Application,將新工程命名爲UIControlDevApp,將類前綴命名爲RW,選擇該工程只支持iPhone,最後將項目保存到和之前的RWUIControls相同的目錄下。

添加RWUIControls依賴庫,將RWUIControls.xcodeproj從Finder中拖到Xcode中UIControlDevApp組下。

ios_framework_import_library_into_dev_app-700x357.png

現在你可以在你的工程中導航到庫工程了,這樣做非常好,因爲這樣意味着你可以在庫中編輯代碼,並且運行示例工程來測試你做的改變。

Note:你無法將同一工程在兩個Xcode窗口中同時打開,如果你發現你無法在你的工程中導航到庫工程的話,檢查一下是否庫工程在其他Xcode窗口中打開了。

這裏你可以拷貝代碼,而不是和上一個教程似的重新創建代碼。首先,選擇Main.storyboard, RWViewController.h 和 RWViewController.m,然後右鍵單擊,選擇Delete,將它們刪除到廢紙簍中。然後,將你之前下載的壓縮文件中DevApp文件夾拷貝到Xcode的UIControlDevApp組下。

ios_framework_adding_files_to_dev_app.gif

現在,你將添加靜態庫作爲實例項目的依賴庫:

  • 在項目導航欄中選擇UIControlDevApp。

  • 導航到UIControlDevApp目標下Build Phases面板下。

  • 打開Target Dependencies面板,點擊+按鈕調出選擇器。

  • 找到RWUIControls靜態庫,選擇並點擊Add。這一步表明當構建dev應用時,Xcode會檢查是否靜態庫需要重新構建。

爲了連接到靜態庫本身,展開Link Binary With Libraries面板,再次點擊+按鈕,從Workspace組中選擇libRWUIControls.a然後點擊Add。

這一步確保Xcode可以連接到靜態庫,就像連接到系統framework(例如UIKit)一樣。

ios_framework_add_dependencies_to_dev_app.gif

編譯並運行,如果你按照之前的教程創建了一個旋鈕控件,在你眼前展示的將是與之相同的應用。

ios_framework_dev_app_buildrun1-333x500.png

像這樣使用嵌套工程的好處是你可以對庫本身做出修改,而不用離開示例工程,即使你同時改變兩個地方的代碼也一樣。每次你編譯工程,你都要檢查是否將頭文件的public/project關係設置正確。如果實例工程中缺失了任何需要的頭文件,它都不能被編譯。

創建一個Framework

到現在,你可能迫不及待地點着腳趾頭,想着什麼時候framework可以出來。可以理解,因爲到現在爲止你已經做了許多工作,然而卻沒有看到過framework的身影。

現在該有所改變了,你之所以到現在都沒有創建一個framework,是因爲framework本身就是靜態庫加上一組頭文件——實際上正是你已經創建好的東西。

當然,framework也有幾點不同之處:

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

  2. 片段(Slice)。目前爲止,當你構建庫時,僅僅考慮到當前需要的結構(architecture)。例如,i386、arm7等,爲了讓一個framework更有用,對於每一個運行framework的結構,該framework都需要構建這種結構。一會你就會創建一個新的工程,構建所有需要的結構,並將它們包含到framework中。

這一部分非常神奇,不過我們會慢慢地來。實際上它並不像看起來那樣複雜。

Framework結構

正如之前提到的,一個framework有一個特殊的目錄結構,看起來像是這樣的:

ios_framework_directory_structure-449x320.png

現在你需要在靜態庫構建過程中添加腳本來創建這種結構,在項目導航欄中選擇RWUIControls,然後選擇RWUIControls靜態庫目標,選擇Build Phases欄,然後選擇Editor/Add Build Phase/Add Run Script Build Phase來添加一個新的腳本。

ios_framework_framework_add_run_script_build_phase-700x271.png

這一步在build phases部分添加了一個新的面板,這允許你在構建時運行一個Bash腳本。你希望讓腳本在build的過程中何時執行,就把這個面板拖動到列表中相對應的那一位置。對於該framework工程來說,腳本最後執行,因此你可以讓它保留在默認的位置即可。

ios_framework_new_run_script_build_phase-700x299.png

雙擊面板標題欄Run Script,重命名爲Build Framework。

ios_framework_rename_script-700x131.png

在腳本文本框中粘貼下面的Bash腳本代碼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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"

這個腳本首先創建了RWUIControls.framework/Versions/A/Headers目錄,然後創建了一個framework所需要的三個連接符號(symbolic links)。

  • Versions/Current => A

  • Headers => Versions/Current/Headers

  • RWUIControls => Versions/Current/RWUIControls

最後,將公共頭文件從你之前定義的公共頭文件路徑拷貝到Versions/A/Headers目錄下,-a參數確保修飾次數作爲拷貝的一部分不會改變,防止不必要的重新編譯。

現在,選擇RWUIControls靜態庫scheme,然後選擇iOS Device構建目標,然後使用cmd+B構建。

ios_framework_build_target_static_lib.png

在RWUIControls工程裏Products目錄下右鍵單擊libRWUIControls.a靜態庫,然後再一次選擇Show in Finder。

ios_framework_static_lib_view_in_finder-480x295.png

在這次構建目錄中你可以看到RWUIControls.framework,可以確定一下這裏展示了正確的目錄結構:

ios_framework_created_framework_directory_structure-480x251.png

這算是在完成你的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:這裏實際上強調了創建依賴靜態庫的示例項目的另一個原因:庫僅僅在示例項目運行所需要的架構下編譯,只有當有變化的時候才重新編譯,爲什麼這一點會讓人激動?因爲開發週期會儘可能地縮短。

這裏將使用在RWUIControls工程中的一個新的目標來構建framework,在項目導航欄中選擇RWUIControls,然後點擊已經存在的目標下面的Add Target按鈕。

ios_framework_add_target_button-471x320.png

找到iOS/Other/Aggregate,點擊Next,將目標命名爲Framework。

Note:爲什麼使用集合(Aggregate)目標來創建一個framework呢?爲什麼這麼不直接?因爲OS X對庫的支持更好一些,事實上,Xcode直接爲每一個OS X工程提供一個Cocoa Framework編譯目標。基於此,你將使用集合編譯目標,作爲Bash腳本的連接串來創建神奇的framework目錄結構。你是不是開始覺得這裏的方法有些愚蠢了?

爲了確保每當這個新的framework目標被創建時,靜態鏈接庫都會被編譯,你需要往靜態庫目標中添加依賴(Dependency)。在庫工程中選擇Framework目標,在Build Phases中添加一個依賴。展開Target Dependencies面板,點擊 + 按鈕選擇RWUIControls靜態庫。

ios_framework_add_dependency_to_framework_target.gif

這個目標的主要編譯部分是多平臺編譯,你將使用一個腳本來做到這一點。和你之前做的一樣,在Framework目標下,選擇Build Phases欄,點擊Editor/Add Build Phase/Add Run Script Build Phase,創建一個新的Run Script Build Phase。

ios_framework_framework_add_run_script_build_phase-700x271.png

雙擊Run Script,重命名腳本的名字。這次命名爲MultiPlatform Build。

在腳本文本框中粘貼下面的Bash腳本代碼:

1
2
3
4
5
6
7
8
9
10
11
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。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 1 - Extract the platform (iphoneos/iphonesimulator) from the SDK name
if [[ "$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 1
fi
  
# 2 - Extract the version from the SDK
if [[ "$SDK_NAME" =~ ([0-9]+.*$) ]]; then
  RW_SDK_VERSION=${BASH_REMATCH[1]}
else
  echo "Could not find sdk version from SDK_NAME: $SDK_NAME"
  exit 1
fi
  
# 3 - Determine the other platform
if "$RW_SDK_PLATFORM" == "iphoneos" ]; then
  RW_OTHER_PLATFORM=iphonesimulator
else
  RW_OTHER_PLATFORM=iphoneos
fi
  
# 4 - Find the build directory
if [[ "$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 1
fi

上面四句聲明都非常相似,都是使用字符串比較和正則表達式來確定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. 從構建好的工程的目錄路徑的末尾找出平臺名稱,將其替換爲其他平臺。這樣可以確保爲其他平臺構建的目錄可以被找到。這是將兩個靜態庫合併的關鍵部分。

現在你可以啓動腳本爲其他平臺編譯,然後得到合併兩靜態庫的結果。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
# 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_64
if "$RW_SDK_PLATFORM" == "iphonesimulator" ]; then
    build_static_library "${SDK_NAME}"
fi
  
# Join the 2 static libs into 1 and push into the .framework
make_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中。

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

1
2
3
4
5
6
# Ensure that the framework is present in both platform's build directories
cp -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 desktop
ditto "${RW_FRAMEWORK_LOCATION}" "${HOME}/Desktop/${RW_FRAMEWORK_NAME}.framework"
  • 第一條命令確保framework在所有平臺的編譯目錄中都存在。

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

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

ios_framework_select_framework_aggregate_scheme-480x135.png

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

ios_framework_built_framework_on_desktop-700x319.png

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

1
2
$ cd ~/Desktop/RWUIControls.framework
$ RWUIControls.framework  xcrun lipo -info RWUIControls

第一條指令導航到framework中,第二行使用lipo指令從RWUIControls靜態庫中得到需要的信息,這將列出存在於該庫中的所有片段。

ios_framework_architectures_in_fat_library-700x257.png

這裏你可以看到,一共有五種片段:i386, x86_64, arm7, arm7s 和 arm64,正如你在編譯時設定的那樣。如果你之前使用lipo –info指令,你可以看到這些片段的一個分組。

如何使用Framework?

OK,你已經有了framework,你也有了庫。它們可以提供一種優雅的方法來解決你迄今爲止還沒有遇到過的問題,但是做這些的意義是什麼呢?

使用framework的其中一個主要的優點是簡化使用,現在你將創建一個簡單的iOS應用,並使用你剛剛創建好的RWUIControls.framework。

使用Xcode創建一個新工程,選擇File/New/Project,然後選擇iOS/Application/Single View Application,將新工程命名爲ImageViewer,設置爲僅僅用於iPhone,將其保存到與之前兩個工程同樣的目錄下。這個應用將展示一張圖片,允許用戶使用RWKnobControl旋轉圖片。

在你之前下載的壓縮文件中找到ImageViewer目錄,這裏面只有一個圖片文件,把這個圖片文件sampleImage.jpg從Finder中拖到Xcode的ImageViewer組中。

ios_framework_drag_sample_image_into_xcode-480x299.png

選中Copy items into destination group’s folder,點擊Finish完成導入操作。

導入一個framework的步驟幾乎相同,將RWUIControls.framework從桌面拖到Xcode中的Frameworks組下。同樣,確保選中了Copy items into destination group’s folder。

ios_framework_import_framework.gif

打開RWViewController.m,將裏面的代碼替換爲下面的代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#import "RWViewController.h"
#import < RWUIControls/RWUIControls.h>
  
@interface RWViewController ()
@property (nonatomic, strong) UIImageView *imageView;
@property (nonatomic, strong) RWKnobControl *rotationKnob;
@end
  
@implementation RWViewController
  
- (void)viewDidLoad
{
    [super viewDidLoad];
    // Create UIImageView
    CGRect frame = self.view.bounds;
    frame.size.height *= 2/3.0;
    self.imageView = [[UIImageView alloc] initWithFrame:CGRectInset(frame, 0, 20)];
    self.imageView.image = [UIImage imageNamed:@"sampleImage.jpg"];
    self.imageView.contentMode = UIViewContentModeScaleAspectFit;
    [self.view addSubview:self.imageView];
  
    // Create RWKnobControl
    frame.origin.y += frame.size.height;
    frame.size.height /= 2;
    frame.size.width  = frame.size.height;
    self.rotationKnob = [[RWKnobControl alloc] initWithFrame:CGRectInset(frame, 10, 10)];
    CGPoint center = self.rotationKnob.center;
    center.x = CGRectGetMidX(self.view.bounds);
    self.rotationKnob.center = center;
    [self.view addSubview:self.rotationKnob];
  
    // Set up config on RWKnobControl
    self.rotationKnob.minimumValue = -M_PI_4;
    self.rotationKnob.maximumValue = M_PI_4;
    [self.rotationKnob addTarget:self
                          action:@selector(rotationAngleChanged:)
                forControlEvents:UIControlEventValueChanged];
}
  
- (void)rotationAngleChanged:(id)sender
{
    self.imageView.transform = CGAffineTransformMakeRotation(self.rotationKnob.value);
}
  
- (NSUInteger)supportedInterfaceOrientations
{
    return UIInterfaceOrientationMaskPortrait;
}
  
@end

這就是一個簡單的視圖控制器,它做了以下幾件事:

  • 使用#import導入框架的頭文件

  • 設置了一組私有屬性來持有UIImageView和RWKnobControl。

  • 創建一個UIImageView,將其放到合適的位置。

  • 爲Knob control設置一些屬性,包括添加值改變的事件監聽器。相應方法爲rotationAngleChanged:方法。

  • rotationAngleChanged:方法簡單更新了UIImageView的transform屬性,讓圖片隨着knob control的移動而旋轉。

具體怎麼樣使用RWKnobControl,可以看一下上一篇教程,那裏解釋了怎麼樣去創建它。

編譯並運行,你就能看到一款簡單的應用,當你改變knob control的值時圖片就會旋轉。

ios_framework_image_viewer_rotating.gif

打包(Bundle)資源

你有沒有注意到RWUIControls的framework只包含了代碼和頭文件,其他的文件卻沒有被包含。例如,你沒有使用其他任何資源,比如圖片。這是iOS的一個限制,framework只能包含頭文件和靜態庫。

現在準備好,這篇教程要開始進階了。這一部分你將學到怎麼樣通過使用bundle整合資源,讓其可以隨着framework一起發佈,進而突破這一限制。

你將創建一個新的UI控件——絲帶控件,作爲RWUIControls庫的一部分。這個控件將在一個UIView的右上方展示一個絲帶圖片。

創建一個Bundle

資源都會被添加到bundle中,這將是RWUIControls工程上的另一個目標。

打開UIControlDevApp工程,選擇RWUIControls子工程,點擊Add Target按鈕,導航到OS X/Framework and Library/Bundle。將新的Bundle命名爲RWUIControlsResources,然後從framework選擇框中選擇Core Foundation。

ios_framework_import_framework.gif

這裏需要配置幾個編譯設置,因爲你正在創建一個在iOS上使用的bundle,這與默認的OS X不同。選擇RWUIControlsResources目標,然後點擊Build Settings欄,搜索base sdk,選擇Base SDK這一行,按下delete鍵,這一步將OS X切換爲iOS。

ios_framework_bundle_set_base_sdk-700x161.png

同時你需要將工程名稱改爲RWUIControls。搜索product name,雙擊進入編輯模式,將${TARGET_NAME}替換爲RWUIControls。

ios_framework_bundle_set_product_name-700x206.png

默認情況下,有兩種resolutions的圖片可以產生一些有趣的現象。例如,當你導入一個retina @2x版本的圖片時,普通版的和Retina版的將會合併成一個多resolution的TIFF(標籤圖像文件格式,Tagged Image File Format)。這不是一件好事。搜索hidpi將COMBINE_HIDPI_IMAGES設置爲NO。

ios_framework_bundle_hidpi_images-700x234.png

現在,你將確保當你編譯framework時,bundle也能被編譯並將framework作爲依賴添加到集體目標中。選中Framework目標,選擇Build Phases欄,展開Target Dependencies面板,點擊 + 按鈕,選擇RWUIControlsResources目標將其添加爲依賴。

ios_framework_add_bundle_as_framework_dependency.gif

現在,在Framework目標的Build Phases中,打開MultiPlatform Build面板,在腳本的最後添加下述代碼:

1
2
3
# Copy the resources bundle to the user's desktop
ditto "${BUILT_PRODUCTS_DIR}/${RW_FRAMEWORK_NAME}.bundle" \
      "${HOME}/Desktop/${RW_FRAMEWORK_NAME}.bundle"

這條指令將拷貝構建好的bundle到用戶的桌面上。現在,編譯framework scheme,你會發現bundle在桌面上出現。

ios_framework_bundle_on_desktop-198x320.png

導入Bundle

爲了用這個新的bundle開發,你需要在示例項目中使用它,這意味着你必須既把它作爲依賴添加到工程中,同時作爲一個對象拷貝到項目中。

在項目導航欄中,選擇UIControlDevApp工程,點擊UIControlDevApp目標,展開RWUIControls工程的Product組,把RWUIControls.bundle拖到Copy Bundle Resources面板中的 Build Phases欄。

在Target Dependencies面板中,點擊+按鈕,添加新的依賴,然後選擇RWUIControlsResources。

ios_framework_add_bundle_for_dev_project.gif

創建一個絲帶視圖(Ribbon View

上面的就是所有必需的配置工作了,從你之前下載的壓縮文件中將RWRibbon文件夾拖入到RWUIControls工程下RWUIControls組中。

ios_framework_drag_rwribbon-480x309.png

選中Copy the items into the destination group’s folder,在對應的選擇框中打勾,確保它被添加到RWUIControls靜態lib目標中。

ios_framework_rwribbon_membership-700x472.png

代碼中一個很重要的部分是你怎樣引用一張圖片。如果你看一下RWRibbonView.m文件中的addRibbonView方法,你將會看到相關的這一行代碼:

1
UIImage *image = [UIImage imageNamed:@"RWUIControls.bundle/RWRibbon"];

Bundle就像一個文件目錄,所以引用bundle中的一張圖片是非常簡單的。

將圖片添加到bundle中,選擇這張圖片,在右邊的面板中,通過選擇來表示它應該屬於RWUIControlsResources目標。

ios_framework_rwribbon_image_membership-700x208.png

還記得我們說過要確保framework可以被訪問嗎?現在,你需要導出頭文件RWRibbon.h,在Target Membership面板中選擇該文件,然後從彈出視圖中選擇Public。

ios_framework_rwribbon_header_membership-480x262.png

最後,你需要將頭文件引用添加到framework的頭文件中。打開RWUIControls.h添加下面這兩行:

1
2
// RWRibbon
#import < RWUIControls/RWRibbonView.h>

將絲帶添加到示例工程中

在UIControlDevApp項目中打開RWViewController.m文件,在@interface後的大括號中添加下面的實例變量聲明。

1
RWRibbonView  *_ribbonView;

在viewDidLoad:的末尾添加下面的代碼來創建一個絲帶視圖:

1
2
3
4
5
6
7
// Creates a sample ribbon view
_ribbonView = [[RWRibbonView alloc] initWithFrame:self.ribbonViewContainer.bounds];
[self.ribbonViewContainer addSubview:_ribbonView];
// Need to check that it actually works :)
UIView *sampleView = [[UIView alloc] initWithFrame:_ribbonView.bounds];
sampleView.backgroundColor = [UIColor lightGrayColor];
[_ribbonView addSubview:sampleView];

編譯並運行UIControlDevApp scheme。你將看到新的絲帶控件出現在應用的下方。

ios_framework_dev_app_with_ribbon-333x500.png

在ImageViewer中使用Bundle

我要向你分享的最後一件事是怎麼樣在其他應用中使用這個新的bundle,例如,你之前創建的ImageViewer應用。

開始之前,確保你的bundle和framework都是最新版本的,選擇Framework scheme然後按下cmd+B編譯。

打開ImageViewer工程,找到Frameworks組中的RWUIControls.framework項目,然後將其刪除,選擇Move to Trash。然後將RWUIControls.framework從你的桌面上拖到Frameworks組中。這是必須的,因爲此時的framework已經與你第一次導入時的framework大不相同了。

ios_framework_delete_framework-700x283.png

Note:如果Xcode拒絕讓你添加framework,這可能是因爲你並沒有真正將之前版本的framework刪除到廢紙簍。如果是因爲這樣的話,從Finder中ImageViewer目錄下刪除framework然後重新嘗試。

導入bundle,簡單將其從桌面上拖到ImageViewer組中。選中Copy items into destination group’s folder,選中對應的選擇框,確保它被添加到ImageViewer目標中。

ios_framework_import_bundle-700x474.png

接下來你要將絲帶添加到可以旋轉的圖片上。因此,在RWViewController.m文件中代碼要有一些簡單的變動。

打開該文件,將屬性imageView的類型從UIImageView變爲RWRibbonView:

1
@property (nonatomic, strong) RWRibbonView *imageView;

將viewDidLoad方法中第一部分,負責創建並配置UIImageView的代碼,替換爲下面的代碼:

1
2
3
4
5
6
7
8
9
10
[super viewDidLoad];
// Create UIImageView
CGRect frame = self.view.bounds;
frame.size.height *= 2/3.0;
self.imageView = [[RWRibbonView alloc] initWithFrame:CGRectInset(frame, 0, 20)];
UIImageView *iv = [[UIImageView alloc] initWithFrame:self.imageView.bounds];
iv.image = [UIImage imageNamed:@"sampleImage.jpg"];
iv.contentMode = UIViewContentModeScaleAspectFit;
[self.imageView addSubview:iv];
[self.view addSubview:self.imageView];

編譯並運行該項目,現在該項目中你同時使用了RWUIControls framework下的RWKnobControl和RWRibbonView。

ios_framework_image_viewer_with_ribbon.gif




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章