建立 iOS 靜態函式庫

来源:互联网 发布:网络流行歌曲大全 对唱 编辑:程序博客网 时间:2024/06/05 12:01
Ernesto GarcíaErnesto García

如果你已經開發iOS一段時間了,你可能會有一些自己常用的類別或功能,在你其他專案中也會都會使用這些類別或功能。

最簡單的方法就是將這程式碼複製貼上。很快的這將會變成維護上的夢魘。每個app都各自擁有這些程式碼,每當bug修復時或更新時,很難同時將這些通用的程式碼同時更新。

這時就可透過靜態函式庫(static library)來解決這問題!靜態函式庫可以輕易的將類別,方法,定義和資源打包起來並且讓其他專案使用。

本教程你將會使用兩種方法建立自己的靜態函式庫。

開始本教程前,你需要熟悉Objective-C及iOS。Core Image的相關知識是非必需的,如果你對範例應用程式中的濾鏡效果程式碼的運作很有興趣,這個範例對你將有很大的幫助。

效率是減少、重複使用以及循環再造你的程式碼!

為什麼使用靜態函式庫

很多原因會讓你想建立靜態函式庫。舉例來說:

  • 你想綁定一些經常使用的類別讓你或你的同事能簡單的使用它。
  • 你想讓這些通用的程式碼集中管理,每當有錯誤修復或更新時能輕易的維護。
  • 你想讓大家使用這個函式庫,但你不想讓其他人看到你的程式碼。
  • 你想為這個函式庫加上版本的快照。

本教程,假設你已經看過Core Image的教程,並且提供這些原始碼供你使用方便。

你將會將這些程式碼加入到靜態函式庫,而使用靜態函式庫的應用程式不會有什麼改變,為一改變就是得到了上述的條列出的優點。

讓我們開始吧!

開始

開啟Xcode,選擇FileNewProject。如下圖,會看到樣板選擇對話視窗,選擇iOSFramework & LibraryCocoa Touch Static Library

NewLib

點選Next。如下圖,專案名稱輸入ImageFilters,勾選Use Automatic Reference CountingInclude Unit Tests不用勾選。

libname

點選Next。最後選擇檔案儲存的路徑並按下Create

Xcode已經幫你建立好靜態函式庫專案,甚至替你加上了ImageFilters類別。(Xcode是不是很棒呢?) 那裡存放的就是濾鏡效果的程式碼。

Note:你可以加入多個類別到靜態函式庫,或者刪除原本的類別。本教程中,將會將類別加入到ImageFilters類別內。

由於你的Xcode專案現在是空的,讓我們加入一些程式碼進去吧!

Image Filters

這個函式庫是使用UIKit並針對iOS設計,所以第一件事情是在檔案的最前面要引入UIKit。開啓ImageFIlters.h檔案加入下列的程式碼:

#import <UIKit/UIKit.h>

接著將下列的程式碼宣告貼到 @interface ImageFilters : NSObject下方。

@property (nonatomic,readonly) UIImage *originalImage; - (id)initWithImage:(UIImage *)image;- (UIImage *)grayScaleImage;- (UIImage *)oldImageWithIntensity:(CGFloat)level;

在header檔案宣告一個public interface(可稱為類別)。當其他開發者(包括自己)要使用這個函式庫,只要閱讀header檔案,就可以知道這個類別內有哪些靜態函式庫的方法可以使用。

現在加入實作。開啓ImageFilters.m並將下列程式碼加到#import "ImageFilters.h"下方:

@interface ImageFilters() @property (nonatomic,strong) CIContext  *context;@property (nonatomic,strong) CIImage    *beginImage; @end

上面的程式碼宣告了一些類別內部使用的屬性。這些屬性並不是公開的(public),所以其他應用程式使用並沒有函式庫存取的權限。

最後你要實作該類別的方法。接著將下面的程式碼加到@implementation ImageFilters下:

- (id)initWithImage:(UIImage *)image{    self = [super init];    if (self) {        _originalImage  = image;        _context        = [CIContext contextWithOptions:nil];        _beginImage     = [[CIImage alloc] initWithImage:_originalImage];    }    return self;} - (UIImage*)imageWithCIImage:(CIImage *)ciImage{    CGImageRef cgiImage = [self.context createCGImage:ciImage fromRect:ciImage.extent];    UIImage *image = [UIImage imageWithCGImage:cgiImage];    CGImageRelease(cgiImage)return image;} - (UIImage *)grayScaleImage{    if( !self.originalImage)        return nil;     CIImage *grayScaleFilter = [CIFilter filterWithName:@"CIColorControls" keysAndValues:kCIInputImageKey, self.beginImage, @"inputBrightness", [NSNumber numberWithFloat:0.0], @"inputContrast", [NSNumber numberWithFloat:1.1], @"inputSaturation", [NSNumber numberWithFloat:0.0], nil].outputImage;     CIImage *output = [CIFilter filterWithName:@"CIExposureAdjust" keysAndValues:kCIInputImageKey, grayScaleFilter, @"inputEV", [NSNumber numberWithFloat:0.7], nil].outputImage;     UIImage *filteredImage = [self imageWithCIImage:output];    return filteredImage;} - (UIImage *)oldImageWithIntensity:(CGFloat)intensity{    if( !self.originalImage )        return nil;     CIFilter *sepia = [CIFilter filterWithName:@"CISepiaTone"];    [sepia setValue:self.beginImage forKey:kCIInputImageKey];    [sepia setValue:@(intensity) forKey:@"inputIntensity"];     CIFilter *random = [CIFilter filterWithName:@"CIRandomGenerator"];     CIFilter *lighten = [CIFilter filterWithName:@"CIColorControls"];    [lighten setValue:random.outputImage forKey:kCIInputImageKey];    [lighten setValue:@(1 - intensity) forKey:@"inputBrightness"];    [lighten setValue:@0.0 forKey:@"inputSaturation"];     CIImage *croppedImage = [lighten.outputImage imageByCroppingToRect:[self.beginImage extent]];     CIFilter *composite = [CIFilter filterWithName:@"CIHardLightBlendMode"];    [composite setValue:sepia.outputImage forKey:kCIInputImageKey];    [composite setValue:croppedImage forKey:kCIInputBackgroundImageKey];     CIFilter *vignette = [CIFilter filterWithName:@"CIVignette"];    [vignette setValue:composite.outputImage forKey:kCIInputImageKey];    [vignette setValue:@(intensity * 2) forKey:@"inputIntensity"];    [vignette setValue:@(intensity * 30) forKey:@"inputRadius"];     UIImage *filteredImage = [self imageWithCIImage:vignette.outputImage]return filteredImage;}

這些程式碼實作了初始化(initialization)以及執行Core Image濾鏡的方法。Core Image已經超出本教程的範圍,你可以在Code Image Tutorial中得知更多Core Image濾鏡相關知識。

現在你有一個靜態函式庫,一個公開的ImageFilters類別,這個類別也提供三個方法:

  • initWithImage:初始化濾鏡類別
  • grayScaleImage:建立一個灰階的濾鏡
  • oldImageWithIntensity:建立一個古老效果的濾鏡

現在建構(build)及執行(Run)你的函式庫。你會發現Xcode的執行(Run)按鈕只完成一次的建構(build);你無法看到函式庫執行的結果,因為他背後沒有一個應用程式!

函式庫建構好的檔案副檔名並不是.app.ipa的,而是一個.a的副檔名檔案。如下圖,你可以在Products的資料夾下找到他。對著libImageFilters.a右鍵或按住control鍵並點擊它,接著選取Show in Finder

showinfinder

如下圖,Xcode會在Finder中開啟到該檔案的位置:

lib-structure

最終函式庫有兩個部分:

Header files: 在include資料夾內,你會發現這個公開的函式庫。這個案例中只有一個公開的類別,所以在這個資料夾內只有一個ImageFilters.h檔案。你的應用程式專案會需要使用到這個檔案,因此Xcode在編譯的時候才會知道該類別。

Binary Library: ImageFilters.a是由Xcode產生的靜態函式庫檔案。當你的應用程式要使用該函式庫,你需要連結到這個檔案。

這兩個很相似。你只需要將framework header和framework程式碼與你的app做好連結。

共享的函式庫隨時可以使用。預設的情況下,函式庫檔案會編譯成目前的架構建構。如果你針對模擬器建構,函式庫將包含i386的架構程式碼;以及如果你建構的對象是實體裝置,你將會得到ARM架構的程式碼。你需要建構兩個版本的函式庫,根據你選擇的目標是模擬器或是實體裝置,選擇使用不同的連結。

怎麼辦?

很幸運,有一個更好的方法可以支援多個平台,這樣就不用建立多個版本了。你可以建立一個universal binary,它包含了兩個架構的程式碼。

Universal Binaries

universal binary是個特別的binary檔案,它包含了多個架構的程式碼。在Mac電腦上,你可能熟悉PowerPC(PPC)到Intel(i386)的universal binary。轉換時,Mac應用程式會附帶一個universal binary檔案,所以應用程式可以同時支援Intel與PowerPC。

支援ARM與i386的概念並沒有太大的不同。這個範例中,靜態函式庫包含的程式碼將會支援iOS裝置(ARM)與模擬器(i386)的架構。Xcode會將函式庫視為一個通用的(universal),當你每次建構你的應用程式時,將會根據建構的目標選擇適當的架構。

為了建立一個universal binary函式庫,你需要使用一個名為lipo

lipo-cat

別擔心,這不是那liop! :]

lipo是一個命令列工具,它允許你在universal檔案執行操作。(例如建立universal binary,條列universal檔案內容等)。在本教程中你將會使用lipo將兩個不同架構的binary檔案整合成一個binary檔案。你可以直接在命令列工具使用lipo,但在本教程中,你將會使用Xcode幫你執行命令列腳本建立universal library。

Aggregate Target在Xcode會一次建構多個目標,包括命令列腳本。在Xcode目錄點選到File/New/Target。選擇iOS/Other,並點選Aggregate,如下圖:

aggregate-target

這個目標稱為UniversalLib,並且確定專案選擇到ImageFilters,如下圖:

aggregate-universal

在專案導覽列點選ImageFilters專案,然後選擇到UniversalLib target。切換到Build Phases分頁;你設定的動作會在target建立時執行。

如下圖,現在點選Add Build Phase按鈕,在彈出的視窗選擇Add Run Script

aggregate-phase

現在你需要設定腳本。展開Run Script模組,並且貼上下列的腳本:

# define output folder environment variableUNIVERSAL_OUTPUTFOLDER=${BUILD_DIR}/${CONFIGURATION}-universal # Step 1. Build Device and Simulator versionsxcodebuild -target ImageFilters ONLY_ACTIVE_ARCH=NO -configuration ${CONFIGURATION} -sdk iphoneos  BUILD_DIR="${BUILD_DIR}" BUILD_ROOT="${BUILD_ROOT}"xcodebuild -target ImageFilters -configuration ${CONFIGURATION} -sdk iphonesimulator -arch i386 BUILD_DIR="${BUILD_DIR}" BUILD_ROOT="${BUILD_ROOT}" # make sure the output directory existsmkdir -p "${UNIVERSAL_OUTPUTFOLDER}" # Step 2. Create universal binary file using lipolipo -create -output "${UNIVERSAL_OUTPUTFOLDER}/lib${PROJECT_NAME}.a" "${BUILD_DIR}/${CONFIGURATION}-iphoneos/lib${PROJECT_NAME}.a" "${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/lib${PROJECT_NAME}.a" # Last touch. copy the header files. Just for conveniencecp -R "${BUILD_DIR}/${CONFIGURATION}-iphoneos/include" "${UNIVERSAL_OUTPUTFOLDER}/"

這邊的腳本並不複雜,下面會一行一行的解釋:

  • UNIVERSAL_OUTPUTFOLDER資料夾名稱,這裡會得到的資料夾名稱為”Debug-universal”
  • Step 1. 第二行調用xcodebuild並指示它建構ARM架構binary(你會在這邊看到 -sdk iphoneos參數)
  • 下一行會再次呼叫xcodebuild,並且建構一個Intel架構的binary檔案到iPhone Simulator資料夾內,關鍵的參數為-sdk iphonesimulator -arch i386。(如果你有興趣,在這[連結]你可以學到很多關於xcodebuild)
  • Step 2.現在你已經有兩個不同架構的.a檔案,現在你只需要使用lipo-create並設置它來建構universal binary。
  • 最後一行將複製header file到universal build資料夾(使用cp指令)

如下圖,執行腳本的視窗:

aggregate-script

現在準備建構static library通用的版本。如下圖,在Scheme Selection下拉選單中選擇UniversalLib(下圖顯示的是”iOS Device”,如果你有連接實體裝置,將會顯示該裝置的名稱):

aggregate-scheme

按下Play按鈕建構aggregate scheme的target。

對著libImageFilters.a再次右鍵Show in Finder可以看到建構的結果。將Finder的顯示方式轉成階層式,你會看到一個新的資料夾名為Debug-Universal(如果你建構的是Release版本,則會看到Release-Universal),該資料夾內包含了Universal版本的library,如下圖:

st-finder

通常你會發現header檔案和static library這兩個檔案同時存在,除非他同時連接模擬器與實體裝置。

這些學習是為了能讓妳能建立你自己的universal static library!

總而言之,static library庫非常像是一個應用程式。你可以有1到多個類別(class),建構會得到header file和.a檔案與程式碼。這個.a檔是static library,它可以被不同的應用程式連接使用。

使用Static Library在你的應用程式

在你的應用程式使用ImageFilters類別與你引入header file並沒有什麼不同!問題在於Xcode不知道header或library檔案的位置。

有兩種方法將static library引入你的專案中:

  • 方法1:依據header和library binary檔案(.a)的路徑
  • 方法2:引入library檔案到專案中,如同一個子專案。

兩種方法選擇其中一種,這取決於你個人偏好以及你是否擁有static library的原始碼。

這兩種方法在本教程終會單獨說明。你可以試試其中一種方法,最好的情況是兩種方法都做過。在開始前,你需要先到Core Image教程下載一個zip壓縮檔,這個專案使用新的ImageFilters類別。

由於本教程主要目的是教你如何使用static library,該版本的應用程式包含了所有原始碼。這樣你僅需專注於library的配置。

方法1:Header和Library Binary檔案

在這個教程你需要下載這個專案。將這個zip檔案解壓縮,你可以看到這個專案資料夾的結構如下:

lib-file-tree

為了方便,這個專案已經將universal library .a檔案和 header file複製到該專案,但是該專案並沒有設定使用。這將由你來完成。

Note:Unix慣例會包含一個include資料夾放header file以及一個lib資料夾放library檔案(.a)。這些資料夾只是一個慣例,並沒有強制性。你不需要遵從這樣的結構或是複製這些檔案到你的專案資料夾。在你的應用程式,你可以有一個header和library檔案,而檔案的位置任你選擇,只要在Xocde專案設定指定到正確的位置即可。

開啟專案,試著建構及執行你的應用程式。你會看到這樣的錯誤訊息:

lib-error-include

不出所料,應用程式找不到header檔案。為了解決這問題,你需要加入一個新的Header Search Path到專案內,這個是用來指向到header資料夾的路徑。使用static library通常第一步驟就是設定Header Search Path。

如下圖所示,(1)在專案導覽列,點選專案。(2)點選CoreImageFun target。(3)選擇Build Settings,接著在下方列表可以看到Header Search Path。(4)你可以在搜尋欄位輸入”header search”篩選出Header Search Path,如果你找不到的話。

lib-header-search-1

雙擊Header Search Path,接著會跳出一個視窗。點擊 + 按鈕,並且輸入下列的文字:

$SOURCE_ROOT/include

跳出的新視窗如下圖:

lib-header-search-2

$SOURCE_ROOT是Xcode的環境變數,意思是指向專案的根目錄。Xcode將會將實際的專案路徑取代掉這個環境變數,這意味著專案的路徑為因為你放置到不同的地方會跟著改變。

點擊視窗的外圍,接著可以看到Xcode自動將路徑轉成正確的路徑,如下圖:

lib-header-search-3

建構及執行你的應用程式,看看會如何。阿…看來有些連結的錯誤訊息出現:

lib-header-search-4

看起來不太好,但是給你了一些資訊。如果你仔細看的話,你會看到編譯錯誤和以被替換的連結錯誤。這表示Xcode找到header檔案,並使用它編譯應用程式,但是在連結的階段,無法找到ImageFilter class中的程式碼。為什麼?

這很簡單 一 你還沒告訴Xcode在哪裡可以找到library檔案,而library檔案包含了class的實作。(你看,這不是很嚴重的問題。)

如下圖,(1)回到建構設定(build setting)。(2)選到CoreImageFun target。(3)選擇Build Phases分頁。(4)並展開Link Binary With Libraries。(5)點擊+ 按鈕。

lib-link-1

接著會顯示下圖的畫面,點擊Add Other…,接著選到專案根目錄中lib子目錄下的libImageFilters.a檔案:

lib-link-2

如下圖,當你完成時,應該與下圖一樣:

lib-link-3

最後一步是加入-ObjC連結標誌。這個連結會試著有效率的引入需要的程式碼,有時可以排除static library程式碼。有了這個標誌,所有Objective-C class和library中的類別都可以正確加載。你可以在Apple技術問答學到更多Technical Q&A QA1490。

點選Build Setting分頁,接著可以看到Other linker Flags設定,如下圖:

OBJC-flag

在彈出的視窗,點擊 + 按鈕,並輸入-ObjC,如下圖:

OBJC-flag-2

最後建構並執行你的應用程式;你不會再得到錯誤訊息,然後應用程式就可以開啟了,如下圖:

app

拖拉slider改變濾鏡的效果,或者點擊GrayScale按鈕。執行圖片的濾鏡效果不是透過應用程式的程式碼,而是來自static library。

恭喜 一 你建構了你第一個使用static library的應用程式!你可以看到在這個方法使用了很多第三方的library,例如AdMob,TestFlight以及其他商業library,這些都不提供原始的程式碼。

方法2:子專案

在這個教程你需要下載這個專案。將這個zip檔案解壓縮。你可以看到這專案的資料夾結構,如下圖:

subproject-structure

如果你看過第一個方法,你會發現這個專案變得不一樣。這個專案沒有任何header和static library檔案 一 這就是為什麼不用這些檔案。反而你會將你一開始建立的ImageFilters library專案加到此專案。

在開始之前先建構執行你的應用程式。你會看到一些錯誤訊息,如下圖:

lib-error-include

如果你看過第一個方法,這時你可以知道該如何修正這個問題。同樣的專案,在ViewController類別使用ImageFilters類別,但你仍然要告訴Xcode header檔案的位置。Xcode試圖尋找ImageFilters.h檔案,但失敗了。

ImageFilters library專案加入成為一個子專案,將library專案檔案拖拉到專案目錄結構。如果已經使用Xcode開啟,Xcode並不會正確的加入為子專案。所以在開始本教程前,請確認ImageFilters library專案已經關閉。

在Finder找到名為ImageFilters.xcodeproj的專案檔案。拖拉這個檔案到專案導覽列的CoreImageFun專案,如下圖:

subproject-drag-1

完成之後,你的專案導覽列會變得如下圖:

subproject-drag-2

現在Xcode知道library子專案,你可以加入library到專案中。在編譯應用程式前,Xcode會確保library是最新的版本。

(1)點選專案根目錄,(2)選擇CoreImageFun target。(3)點選Build Phases分頁,(4)展開Target Dependencies,如下圖:

Dependencies

點選 + 按鈕,加入一個新的dependency。如下圖,請確認選到ImageFilters target(並非UniversalLib)。

Dependency-2

完成之後,得到的結果如下圖:

Dependency-3

最後一個步驟是設定專案與static library的連接。展開Link Binary With Library,點選 + 按鈕,如下圖:

subproject-link-1

選擇libImageFilters.a並點選Add

subproject-link-2

連接Link Binaray with Libraries後的結果如下圖:

subproject-link-3

最後一個步驟如同方法1,加入-ObjC連接。點選**Build Settings**分頁,看到**Other linker Flags**設定,如下圖:

OBJC-flag

在彈跳視窗點選 + 按鈕,並輸入-ObjC,如下圖:

OBJC-flag-2

建構及執行你的應用程式,不應該再有任何錯誤出現,而且應用程式開始運作:

app

拖拉slider或按GrayScale按鈕看看圖片的的變化效果。現在濾鏡的邏輯已經完全融入你的library。

如果你已經看過方法1的加入library方法(使用header和library檔案),你會發現與第二個方法有些不同。在第二個方法,你並不需要加入header檔案的路徑到專案設定。另一個不同的是你不需要使用Universal library。

為什麼有差別?當加入library為一個子專案,Xcode會幫你顧及所有事情。加入子專案後,Xcode知道header和binary的檔案位置,以及根據你的應用程式設定使用適當架構的library。這是相當方便的。

如果你使用自己的library,將它加入為子專案,你原有的專案就可以與這個子專案做連結。專案的整合如同專案互相依賴,你可以任意做你想做的事情,再也不用擔心其他問題。 Win-win!


0 0
原创粉丝点击