iOS 使用動態庫


蘋果的開放態度

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代碼鏈接並打進同一個二進制包中不同,動態庫可以在運行時手動加載,這樣就可以做很多事情,比如:

  • 共享可執行文件

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

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

2014-6-23修正:

@唐巧_boy提醒,sandbox會驗證動態庫的簽名,所以如果是動態從服務器更新的動態庫,是簽名不了的,因此應用插件化、軟件版本實時模塊升級等功能在iOS上無法實現。

創建動態庫

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上,需要的朋友請自取。

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

參考文檔:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM