WWDC2014之iOS使用動態庫 framework


from:http://www.cocoachina.com/industry/20140613/8810.html

JUN 12TH, 2014

蘋果的開放態度

WWDC2014上發佈的Xcode6 beta版有了不少更新,其中令我驚訝的一個是蘋果在iOS上開放了動態庫,在Xcode6 Beta版的更新文檔中是這樣描述的:

Frameworks for iOS. iOS developers can now create dynamic frameworks. Frameworks are a collection of code and resources to encapsulate functionality that is valuable across multiple projects. Frameworks work perfectly with extensions, sharing logic that can be used by both the main application, and the bundled extensions.

詳情見官方文檔New Features in Xcode 6 Beta

framework是Cocoa/Cocoa Touch程序中使用的一種資源打包方式,可以將將代碼文件、頭文件、資源文件、說明文檔等集中在一起,方便開發者使用,作爲一名Cocoa/Cocoa Touch程序員每天都要跟各種各樣的Framework打交道。Cocoa/Cocoa Touch開發框架本身提供了大量的Framework,比如Foundation.framework/UIKit.framework/AppKit.framework等。需要注意的是,這些framework無一例外都是動態庫。

但殘忍的是,Cocoa Touch上並不允許我們使用自己創建的framework。不過由於framework是一種優秀的資源打包方式,擁有無窮智慧的程序員們便想出了以framework的形式打包靜態庫的招數,因此我們平時看到的第三方發佈的framework無一例外都是靜態庫,真正的動態庫是上不了AppStore的。

WWDC2014給我的一個很大感觸是蘋果對iOS的開放態度:允許使用動態庫、允許第三方鍵盤、App Extension等等,這些在之前是想都不敢想的事。

iOS上動態庫可以做什麼

和靜態庫在編譯時和app代碼鏈接並打進同一個二進制包中不同,動態庫可以在運行時手動加載,這樣就可以做很多事情,比如:

  • 應用插件化

目前很多應用功能越做越多,軟件顯得越來越臃腫。因此插件化就成了很多軟件發展的必經之路,比如支付寶這種平臺級別的軟件:

首頁上密密麻麻的功能,而且還在增多,照這個趨勢發展下去,軟件包的大小將會不可想象。目前常用的解決方案是使用web頁面,但用戶體驗和Native界面是沒法比的。

設想,如果每一個功能點都是一個動態庫,在用戶想使用某個功能的時候讓其從網絡下載,然後手動加載動態庫,實現功能的的插件化,就再也不用擔心功能點的無限增多了,這該是件多麼美好的事!

  • 軟件版本實時模塊升級

還在忍受蘋果動輒一週,甚至更長的審覈週期嗎?有了動態庫媽媽就再也不用擔心你的軟件升級了!

如果軟件中的某個功能點出現了嚴重的bug,或者想在其中新增功能,你的這個功能點又是通過動態庫實現的,這時候你只需要在適當的時候從服務器上將新版本的動態庫文件下載到本地,然後在用戶重啓應用的時候即可實現新功能的展現。

  • 共享可執行文件

在其它大部分平臺上,動態庫都可以用於不同應用間共享,這就大大節省了內存。從目前來看,iOS仍然不允許進程間共享動態庫,即iOS上的動態庫只能是私有的,因爲我們仍然不能將動態庫文件放置在除了自身沙盒以外的其它任何地方。

不過iOS8上開放了App Extension功能,可以爲一個應用創建插件,這樣主app和插件之間共享動態庫還是可行的。

PS: 上述關於動態庫在iOS平臺的使用,在技術上都是可行的,但本人並沒有真正嘗試過做出一個上線AppStore的應用,因此並不保證按照上述方式使用動態庫一定能通過蘋果審覈!

2014-6-23修正:

@唐巧_boy提醒,sandbox會驗證動態庫的簽名,所以如果是動態從服務器更新的動態庫,應該是簽名不了的,因此應用插件化、軟件版本實時模塊升級估計很難實現,一切都只能等到iOS8正式版發佈以後再驗證了。感謝@唐巧_boy

創建動態庫

1、創建動態庫

  • 創建工程文件

在下圖所示界面能夠找到Cocoa Touch動態庫的創建入口:

framework

跟隨指引一步步操作即可創建一個新的動態庫工程,我的工程名字叫Dylib,Xcode會同時創建一個和工程target同名的.h文件,比如我的就是Dylib.h。

  • 向工程中添加文件

接下來就可以在工程中隨意添加文件了。我在其中新建了一個名爲Person的測試類,提供的接口如下:

1
2
3
4
5
@interface Person : NSObject

- (void)run;

@end

對應的實現部分:

1
2
3
4
5
6
7
8
9
10
11
@implementation Person

- (void)run
{
    NSLog(@"let's run.");

    UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"The Second Alert" message:nil delegate:nil cancelButtonTitle:nil otherButtonTitles:@"done", nil];
    [alert show];
}

@end
  • 設置開放的頭文件

一個庫裏面可以後很多的代碼,但是我們需要設置能夠提供給外界使用的接口,可以通過Target—>Build Phases—>Headers來設置,如下圖所示:

header

我們只需將希望開放的頭文件放到Public列表中即可,比如我開放了Dylib.hPerson.h兩個頭文件,在生成的framework的Header目錄下就可以看到這兩個頭文件,如下圖所示:

public_header

一切完成,Run以後就能成功生成framework文件了。

2、通用動態庫

經過第一步我們只是創建了一個動態庫文件,但是和靜態庫類似,該動態庫並同時不支持真機和模擬器,可以通過以下步驟創建通用動態庫:

  • 創建Aggregate Target

按下圖所示,在動態庫工程中添加一個類型爲Aggregate的target:

aggregate

按提示一步步操作即可,我給Aggregate的Target的命名是CommonDylib

  • 設置Target Dependencies

按以下路徑設置CommonDylib對應的Target Dependencies:

1
TARGETS-->CommonDylib-->Build Phases-->Target Dependencies

將真正的動態庫Dylib Target添加到其中。

  • 添加Run Script

按以下路徑爲CommonDylib添加Run Script:

1
TARGETS-->CommonDylib-->Build Phases-->Run Script

添加的腳本爲:

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
# Sets the target folders and the final framework product.
FMK_NAME=${PROJECT_NAME}

# Install dir will be the final output to the framework.
# The following line create it in the root folder of the current project.
INSTALL_DIR=${SRCROOT}/Products/${FMK_NAME}.framework

# Working dir will be deleted after the framework creation.
WRK_DIR=build
DEVICE_DIR=${WRK_DIR}/Release-iphoneos/${FMK_NAME}.framework
SIMULATOR_DIR=${WRK_DIR}/Release-iphonesimulator/${FMK_NAME}.framework

# -configuration ${CONFIGURATION} 
# Clean and Building both architectures.
xcodebuild -configuration "Release" -target "${FMK_NAME}" -sdk iphoneos clean build
xcodebuild -configuration "Release" -target "${FMK_NAME}" -sdk iphonesimulator clean build

# Cleaning the oldest.
if [ -d "${INSTALL_DIR}" ]
then
rm -rf "${INSTALL_DIR}"
fi

mkdir -p "${INSTALL_DIR}"

cp -R "${DEVICE_DIR}/" "${INSTALL_DIR}/"

# Uses the Lipo Tool to merge both binary files (i386 + armv6/armv7) into one Universal final product.
lipo -create "${DEVICE_DIR}/${FMK_NAME}" "${SIMULATOR_DIR}/${FMK_NAME}" -output "${INSTALL_DIR}/${FMK_NAME}"

rm -r "${WRK_DIR}"

添加以後的效果如圖所示:

commonlib_setting

該腳本是我根據一篇文章中介紹的腳本改寫的,感謝原文作者

腳本的主要功能是:

1.分別編譯生成真機和模擬器使用的framework; 2.使用lipo命令將其合併成一個通用framework; 3.最後將生成的通用framework放置在工程根目錄下新建的Products目錄下。

如果一切順利,對CommonDylib target執行run操作以後就能生成一個如圖所示的通用framework文件了:

products

使用動態庫

添加動態庫到工程文件

經過以上步驟的努力,生成了最終需要的framework文件,爲了演示動態庫的使用,新建了一個名爲FrameworkDemo的工程。通過以下方式將剛生成的framework添加到工程中:

1
Targets-->Build Phases-->Link Binary With Libraries

同時設置將framework作爲資源文件拷貝到Bundle中:

1
Targets-->Build Phases-->Copy Bundle Resources

如圖所示:

framework_demo_setting

僅僅這樣做是不夠的,還需要爲動態庫添加鏈接依賴。

自動鏈接動態庫

添加完動態庫後,如果希望動態庫在軟件啓動時自動鏈接,可以通過以下方式設置動態庫依賴路徑:

1
Targets-->Build Setting-->Linking-->Runpath Search Paths

由於向工程中添加動態庫時,將動態庫設置了Copy Bundle Resources,因此就可以將Runpath Search Paths路徑依賴設置爲main bundle,即沙盒中的FrameworkDemo.app目錄,向Runpath Search Paths中添加下述內容:

1
@executable_path/

如圖所示:

run_search_path

其中的@executable_path/表示可執行文件所在路徑,即沙盒中的.app目錄,注意不要漏掉最後的/

如果你將動態庫放到了沙盒中的其他目錄,只需要添加對應路徑的依賴就可以了。

需要的時候鏈接動態庫

動態庫的另一個重要特性就是即插即用性,我們可以選擇在需要的時候再加載動態庫。

  • 更改設置

如果不希望在軟件一啓動就加載動態庫,需要將

1
Targets-->Build Phases-->Link Binary With Libraries

Dylib.framework對應的Status由默認的Required改成Optional;或者更乾脆的,將Dylib.frameworkLink Binary With Libraries列表中刪除即可。

  • 使用dlopen加載動態庫

Dylib.framework爲例,動態庫中真正的可執行代碼爲Dylib.framework/Dylib文件,因此使用dlopen時如果僅僅指定加載動態庫的路徑爲Dylib.framework是沒法成功加載的。

示例代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (IBAction)onDlopenLoadAtPathAction1:(id)sender
{
    NSString *documentsPath = [NSString stringWithFormat:@"%@/Documents/Dylib.framework/Dylib",NSHomeDirectory()];
    [self dlopenLoadDylibWithPath:documentsPath];
}

- (void)dlopenLoadDylibWithPath:(NSString *)path
{
    libHandle = NULL;
    libHandle = dlopen([path cStringUsingEncoding:NSUTF8StringEncoding], RTLD_NOW);
    if (libHandle == NULL) {
        char *error = dlerror();
        NSLog(@"dlopen error: %s", error);
    } else {
        NSLog(@"dlopen load framework success.");
    }
}

以dlopen方式使用動態庫不知道是否能通過蘋果審覈。

  • 使用NSBundle加載動態庫

也可以使用NSBundle來加載動態庫,實現代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (IBAction)onBundleLoadAtPathAction1:(id)sender
{
    NSString *documentsPath = [NSString stringWithFormat:@"%@/Documents/Dylib.framework",NSHomeDirectory()];
    [self bundleLoadDylibWithPath:documentsPath];
}

- (void)bundleLoadDylibWithPath:(NSString *)path
{
    _libPath = path;
    NSError *err = nil;
    NSBundle *bundle = [NSBundle bundleWithPath:path];
    if ([bundle loadAndReturnError:&err]) {
        NSLog(@"bundle load framework success.");
    } else {
        NSLog(@"bundle load framework err:%@",err);
    }
}

使用動態庫中代碼

通過上述任一一種方式加載的動態庫後,就可以使用動態庫中的代碼文件了,以Dylib.framework中的Person類的使用爲例:

1
2
3
4
5
6
7
8
- (IBAction)onTriggerButtonAction:(id)sender
{
    Class rootClass = NSClassFromString(@"Person");
    if (rootClass) {
        id object = [[rootClass alloc] init];
        [(Person *)object run];
    }
}

注意,如果直接通過下屬方式初始化Person類是不成功的:

1
2
3
4
5
6
7
- (IBAction)onTriggerButtonAction:(id)sender
{
    Person *object = [[Person alloc] init];
    if (object) {
       [object run];
    }
}

監測動態庫的加載和移除

我們可以通過下述方式,爲動態庫的加載和移除添加監聽回調:

1
2
3
4
5
+ (void)load
{
  _dyld_register_func_for_add_image(&image_added);
  _dyld_register_func_for_remove_image(&image_removed);
}

github上有一個完整的示例代碼

從這裏看出,原來就算空白工程軟件啓動的時候也會加載多達一百二十多個動態庫,如果這些都是靜態庫,那該有多可怕!!

Demo

本文使用的例子已經上傳到github上,需要的朋友請自取。

另外,本文對某些東西可能有理解錯誤的地方,還請指出。

參考文檔:

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