iOS 如何使用動態(tài)庫

sdk開發(fā)筆記基礎(chǔ):

說到動態(tài)庫,就不得不提靜態(tài)庫。靜態(tài)庫可以看做是一個具有特定功能的代碼塊,如果app中引用了靜態(tài)庫,則在編譯時會將靜態(tài)庫直接復(fù)制到app的可執(zhí)行文件(也就是mach-o)中。

使用靜態(tài)庫會導(dǎo)致mach-o文件過大,而mach-o文件直接影響app的啟動時間和執(zhí)行時占用的內(nèi)存大小。

為了減少mach-o文件的大小,需要用到動態(tài)庫。當(dāng)app中引用了動態(tài)庫時,動態(tài)庫并不會被復(fù)制到app的mach-o文件中,只有當(dāng)動態(tài)庫真正被用到時,才會去加載(加載到內(nèi)存中)和鏈接(動態(tài)庫可能引用了其他庫)動態(tài)庫,可能是在app啟動時或者是運行時。

目錄

動態(tài)庫和靜態(tài)庫的區(qū)別

創(chuàng)建動態(tài)庫

使用動態(tài)庫

3.1. 添加為依賴庫-啟動時加載

3.2. 運行時加載

注入動態(tài)庫

yololib

1. 動態(tài)庫和靜態(tài)庫的區(qū)別

靜態(tài)庫的后綴名是以.a結(jié)尾,動態(tài)庫的后綴名可以是.dylib或.framework結(jié)尾,所有的系統(tǒng)庫都屬于動態(tài)庫,在iOS中一般使用framework作為動態(tài)庫。

下面是apple官方的兩張圖,表示app啟動后內(nèi)存的使用情況,很形象的說明了靜態(tài)庫和動態(tài)庫的區(qū)別

使用靜態(tài)庫的app:


使用動態(tài)庫的app:


在使用static linker鏈接app時,靜態(tài)庫會被完整的加載到app的mach-o文件(上圖中的Application file)中,作為mach-o文件的一部分,而動態(tài)庫不會被添加到mach-o文件中,這可以有效減少mach-o文件的大小。

如果app將動態(tài)庫作為它的依賴庫,則在mach-o文件中會添加了一個動態(tài)庫的引用;如果app在運行時動態(tài)加載動態(tài)庫,則在mach-o文件中不會添加動態(tài)庫的引用。

在使用app時,靜態(tài)庫和動態(tài)庫都會被加載到內(nèi)存中。當(dāng)多個app使用同一個庫時,如果這個庫是動態(tài)庫,由于動態(tài)庫是可以被多個app的進(jìn)程共用的,所以在內(nèi)存中只會存在一份;如果是靜態(tài)庫,由于每個app的mach-o文件中都會存在一份,則會存在多份。相對靜態(tài)庫,使用動態(tài)庫可以減少app占用的內(nèi)存大小。

另外,使用動態(tài)庫可以縮短app的啟動時間。原因是,使用動態(tài)庫時,app的mach-o文件都會比較?。籥pp依賴的動態(tài)庫可能已經(jīng)存在于內(nèi)存中了(其他已啟動的app也依賴了這個動態(tài)庫),所以不需要重復(fù)加載。

2. 創(chuàng)建動態(tài)庫

上文提到過,動態(tài)庫一般有兩種,分別以.framework和.dylib后綴結(jié)尾,通常把它們叫做Framework和Shared Library。Framework本質(zhì)上是由Shared Library加上頭文件header和其他資源文件打包得來的。

下面以創(chuàng)建LibPersonFramework為例

1.創(chuàng)建一個新工程,選擇iOS -> Cocoa Touch Framework


2.實現(xiàn)framework,并指定對外的頭文件

定義頭文件LibPerson.h

#import <Foundation/Foundation.h>

@interface LibPerson : NSObject

@property (nonatomic, copy) NSString *name ;

- (void)watch;

- (void)eat;

@end

指定LibPersonFramework.h和LibPerson.h為對外的頭文件


指定framework的架構(gòu)模式,這里選擇了Generic iOS Device機型,然后build一下,就會創(chuàng)建一個通用mach-o文件,包含了arm64和arm_v7兩種架構(gòu)。如果選擇了模擬器,會創(chuàng)建一個x86_64架構(gòu)的mach-o文件。


需要注意的是,App和它依賴的framework的架構(gòu)必須兼容,也就是說,在創(chuàng)建可執(zhí)行文件時,要么都是真機,要么都是模擬器。當(dāng)然,也可以分別在真機和模擬器兩種模式下創(chuàng)建framwork,然后使用lipo命令來將兩個framework內(nèi)部的同名mach-o文件合并成一個通用mach-o文件,這樣,不管App是什么架構(gòu)模式,都能正確使用這個framework了。

3. 使用動態(tài)庫

使用動態(tài)庫有兩種方式,一種是將動態(tài)庫添加為依賴庫,這樣會在工程啟動時加載動態(tài)庫,一種是使用dlopen在運行時加載動態(tài)庫,這兩種方式的區(qū)別在于加載動態(tài)庫的時機。

在iOS中一般使用第一種方法,第二種方式一般在mac開發(fā)中使用,如果在iOS中使用了這種方式,是不能上架到App Store的。

3.1. 添加為依賴庫-啟動時加載

創(chuàng)建一個新的工程DylibDemo,并引入LibPersonFramework.framework,在main.m文件中調(diào)用這個framework中的方法


這個時候,app工程已經(jīng)對LibPersonFramework.framework產(chǎn)生了依賴,對于系統(tǒng)framework,到這一步就可以了,因為系統(tǒng)framework已經(jīng)被預(yù)先安裝在iphone上了。對于自定義的framework,還需要通過下面一步來將framework復(fù)制到app的安裝包中。


最后運行一下,調(diào)用成功!

2018-06-16 16:32:09.076551+0800 DylibDemo[1790:700462] wang is watching TV!

2018-06-16 16:32:09.078597+0800 DylibDemo[1790:700462] wang is eating!

3.2. 運行時加載

在運行時加載動態(tài)庫,是指不需要在工程中引入動態(tài)庫,作為替代,在代碼中使用dlopen()這個函數(shù)來加載動態(tài)庫,在調(diào)用完成之后,需要調(diào)用相同次數(shù)的dlclose()函數(shù)來關(guān)閉動態(tài)庫。

除了dlopen()和dlclose()以外,另外還有一個dlsym()函數(shù)來根據(jù)傳入的symbol獲取對應(yīng)數(shù)據(jù)或函數(shù)的地址。在本例中,會使用runtime機制來代替dlsym()函數(shù)。(dlsym()一般是在c或c++中使用)

1.創(chuàng)建新工程DylibDemo-Runtime,添加被調(diào)用庫的頭文件LibPerson.h(這里不需要添加LibPersonFramework.framework)


2.在main.h文件中加載和調(diào)用LibPersonFramework.framework

void loadWhenRunTime(){

? ? // Open the library.

? ? NSString *bundlePath = [[NSBundle mainBundle]pathForResource:@"LibPersonFramework" ofType:nil];

? ? void* lib_handle = dlopen([bundlePath UTF8String], RTLD_LOCAL);

? ? if (!lib_handle) {

? ? ? ? NSLog(@"[%s] main: Unable to open library: %s\n",

? ? ? ? ? ? ? __FILE__, dlerror());

? ? ? ? exit(EXIT_FAILURE);

? ? }

? ? Class class_person = objc_getClass("LibPerson");

? ? LibPerson *person = [class_person new];

? ? person.name = @"wang";

? ? [person watch];

? ? [person eat];

? ? // Close the library.

? ? if (dlclose(lib_handle) != 0) {

? ? ? ? NSLog(@"[%s] Unable to close library: %s\n",

? ? ? ? ? ? ? __FILE__, dlerror());

? ? ? ? exit(EXIT_FAILURE);

? ? }

}

dlopen()函數(shù)需要傳入兩個參數(shù)path和mode,path表示動態(tài)庫的mach-o文件的路徑,mode中可以包含多個標(biāo)識符,比如RTLD_LAZY和RTLD_NOW表示動態(tài)庫中的symbol什么時候被加載,RTLD_GLOBAL和RTLD_LOCAL表示symbol的可見性。(詳情可通過終端命令man dlopen查看)

上述代碼中,path指定動態(tài)庫是在生成的app包中,文件名為LibPersonFramework;mode的值是RTLD_LOCAL,表示在使用dlsym()函數(shù)時,只能通過dlopen()函數(shù)返回的handle來獲取傳入的symbol的地址,由于在例中并不會使用dlsym()函數(shù),所以大可不必關(guān)注這個值。

另外,在上述代碼中還有一點需要注意的,在創(chuàng)建LibPerson類的對象時,不能直接使用LibPerson *person = [LibPerson new],如果這樣做,程序會報如下編譯錯誤:

Undefined symbols for architecture arm64:

? "_OBJC_CLASS_$_LibPerson", referenced from:

? ? ? objc-class-ref in main.o

ld: symbol(s) not found for architecture arm64


這是因為在編譯時,如果調(diào)用了[LibPerson new],編譯器會去驗證app的mach-o文件以及它依賴的動態(tài)庫的mach-o文件中是否有這個類的定義。

由于在編譯時,程序還沒有加載動態(tài)庫LibPersonFramework,而程序只包含了LIbPerson類的頭文件,并沒有它對應(yīng)的.m文件(編譯器只會將.m文件編譯到最終的mach-o文件中),所以編譯器在app的mach-o文件以及它依賴的動態(tài)庫中找不到LibPerson類的定義,然后編譯器就報錯了。

從上述代碼可以看出,在創(chuàng)建LibPerson類的對象時,程序中其實已經(jīng)加載了LibPersonFramework,也就是說,在那個時候程序中已經(jīng)有這個類的定義了。所以,上述代碼中使用了下列代碼來”欺騙“編譯器。

Class class_person = objc_getClass("LibPerson");

? LibPerson *person = [class_person new];


3. 添加動態(tài)庫LibPersonFramework文件

首先build一下,生成app的包文件


這個時候可能會報編譯錯誤,說找不到LibPersonFramework,所以接下來就需要添加LibPersonFramework。

在之前創(chuàng)建的LibPersonFramework.framework中,找到動態(tài)庫LibPersonFramework

找到app的包文件,鼠標(biāo)右鍵點擊顯示包內(nèi)容,然后將這個LibPersonFramework文件復(fù)制到這里


4. 給動態(tài)庫重簽名

這個時候運行一下,dlopen()函數(shù)會報錯,它不能加載LibPersonFramework,這個是簽名出錯了。雖然生成framework和運行app使用的是同一個證書,但是這里使用的并不是整個framework,所以這里需要使用codesign強制重簽名一下。

添加一個腳本


/usr/bin/codesign --force --sign "$EXPANDED_CODE_SIGN_IDENTITY" "$BUILT_PRODUCTS_DIR/$TARGET_NAME.app/LibPersonFramework"


到這里就做完了,運行一下,應(yīng)該是成功的!

4. 注入動態(tài)庫

注入動態(tài)庫是指,給一個現(xiàn)有的mach-o添加一個動態(tài)庫,這樣可以在一個現(xiàn)有的app中執(zhí)行動態(tài)庫的代碼。在給現(xiàn)有app注入動態(tài)庫時,這個動態(tài)庫只能作為一個依賴庫被注入,這是因為在注入之前,不能在現(xiàn)有app中執(zhí)行代碼,所以也就不能使用dlopen()函數(shù)來加載動態(tài)庫了。

首先,觀察一下,當(dāng)一個app添加了一個依賴庫之后,會有哪些變化。在上文中,DylibDemo添加了一個依賴庫LibPersonFramework.framework,下面就以這個項目作為例子。

1.項目生成的app包中增加了Frameworks文件,如果是系統(tǒng)動態(tài)庫,則不會被添加到app包中。


2.mach-o文件中增加了一條Load Commands數(shù)據(jù),這條記錄表示了app對指定的動態(tài)庫的依賴。

使用MachOView打開app包中的mach-o文件


在app啟動時,會自動根據(jù)Load Commands指定的路徑去加載動態(tài)庫,所以必須保證路徑下存在對應(yīng)的動態(tài)庫。

下面舉個例子

新建一個動態(tài)庫LibInjectFramework,下面會將這個動態(tài)庫注入到一個現(xiàn)有app中,如果注入成功,則圖中的+[load]方法會被執(zhí)行。


新建一個項目DylibDemo-Inject,這個項目什么代碼都沒有,只是一個空項目,下面需要將動態(tài)庫LibInjectFramework注入到這個項目中。

1.將動態(tài)庫LibInjectFramework復(fù)制到這個項目的app包中


2.添加動態(tài)庫依賴

這一步需要修改被注入app的mach-o文件,這里使用yololib來完成。將yololib下載后,然后編譯,將生產(chǎn)的命令復(fù)制到/usr/local/bin或$PATH中的其他路徑,這樣就可以在終端使用這個命令了。

yololib需要兩個參數(shù),第一個參數(shù)指定被修改的mach-o文件的路徑,第二個參數(shù)指定動態(tài)庫的路徑。

在項目中,添加兩個腳本命令,分別用來重簽名動態(tài)庫和修改mach-o文件


/usr/bin/codesign --force --sign "$EXPANDED_CODE_SIGN_IDENTITY" "$BUILT_PRODUCTS_DIR/$TARGET_NAME.app/Frameworks/LibInjectFramework"

yololib "$BUILT_PRODUCTS_DIR/$TARGET_NAME.app/$TARGET_NAME" "Frameworks/LibInjectFramework"


執(zhí)行,控制臺應(yīng)該會輸出下面這句

Inject success


需要注意的是,這個項目只有在第一次運行時會成功,因為多次運行,會在mach-o文件中增加多個相同的Load Command。解決方法是保存一個原始的mach-o文件,然后每次運行前替換。

5. yololib

在使用yololib去添加動態(tài)庫依賴時,會修改mach-o文件的兩個地方

1.修改mach-o文件的頭文件

mach header的定義

struct mach_header_64 {

? ? uint32_t? ? magic;? ? ? /* mach magic number identifier */

? ? cpu_type_t? cputype;? ? /* cpu specifier */

? ? cpu_subtype_t? cpusubtype; /* machine specifier */

? ? uint32_t? ? filetype;? /* type of file */

? ? uint32_t? ? ncmds;? ? ? /* number of load commands */

? ? uint32_t? ? sizeofcmds; /* the size of all the load commands */

? ? uint32_t? ? flags;? ? ? /* flags */

? ? uint32_t? ? reserved;? /* reserved */

};


由于增加了一條Load Command,所以需要修改的是ncmds和sizeofcmds這兩個字段,它們分別表示Load Command的總數(shù)目和總大小。

2.添加一個dylib_command結(jié)構(gòu)體

動態(tài)庫的信息是以dylib_command結(jié)構(gòu)體的形式被存儲,dylib_command的定義

struct dylib_command {

? ? uint32_t? ? cmd;? ? ? ? /* LC_ID_DYLIB, LC_LOAD_{,WEAK_}DYLIB,

? ? ? ? ? ? ? ? ? ? ? LC_REEXPORT_DYLIB */

? ? uint32_t? ? cmdsize;? ? /* includes pathname string */

? ? struct dylib? ? dylib;? ? ? /* the library identification */

};

struct dylib {

? ? union lc_str? name;? ? ? ? /* library's path name */

? ? uint32_t timestamp;? ? ? ? /* library's build time stamp */

? ? uint32_t current_version;? ? ? /* library's current version number */

? ? uint32_t compatibility_version; /* library's compatibility vers number*/

};

創(chuàng)建一個dylib_command結(jié)構(gòu)體,并添加到所有Load Command之后,

fseek(newFile, sizeofcmds, SEEK_CUR);

? ? ? ? struct dylib_command dyld;

? ? ? ? fread(&dyld, sizeof(struct dylib_command), 1, newFile);

? ? ? ? NSLog(@"Attaching dylib..\n\n");

? ? ? ? dyld.cmd = LC_LOAD_DYLIB;

? ? ? ? //cmd的大小是dylib_command結(jié)構(gòu)體的大小加上path的大小。

? ? ? ? dyld.cmdsize = (uint32_t) dylib_size;

? ? ? ? dyld.dylib.compatibility_version = DYLIB_COMPATIBILITY_VERSION;

? ? ? ? dyld.dylib.current_version = DYLIB_CURRENT_VER;

? ? ? ? dyld.dylib.timestamp = 2;

? ? ? ? //指定從哪里開始是name

? ? ? ? dyld.dylib.name.offset = sizeof(struct dylib_command);

? ? ? ? fseek(newFile, -sizeof(struct dylib_command), SEEK_CUR);

? ? ? ? fwrite(&dyld, sizeof(struct dylib_command), 1, newFile);


緊跟著被添加的Load_Command,添加動態(tài)庫的path字符串。

fwrite([data bytes], [data length], 1, newFile);


在添加新的Load_Command時,是直接使用新數(shù)據(jù)來覆蓋就數(shù)據(jù)的,因為Load_Command和Section之間還預(yù)留了一部分空間,所以直接覆蓋不會影響Section的數(shù)據(jù)。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 前言 說到動態(tài)庫,就不得不提靜態(tài)庫。靜態(tài)庫可以看做是一個具有特定功能的代碼塊,如果app中引用了靜態(tài)庫,則在編譯時...
    wangzzzzz閱讀 5,374評論 6 13
  • 僅以方便自己查閱記錄前言1.靜態(tài)庫和動態(tài)庫有什么異同?靜態(tài)庫:鏈接時完整地拷貝至可執(zhí)行文件中,被多次使用就有多份冗...
    190CM閱讀 4,264評論 0 4
  • 前言 1.靜態(tài)庫和動態(tài)庫有什么異同? 靜態(tài)庫:鏈接時完整地拷貝至可執(zhí)行文件中,被多次使用就有多份冗余拷貝。利用靜態(tài)...
    Ly夢k閱讀 8,640評論 3 18
  • 介紹 動態(tài)庫形式:.dylib和.framework 靜態(tài)庫形式:.a和.framework 動態(tài)庫和靜態(tài)庫的區(qū)別...
    齊滇大圣閱讀 46,322評論 18 251
  • 13. Hook原理介紹 13.1 Objective-C消息傳遞(Messaging) 對于C/C++這類靜態(tài)語...
    Flonger閱讀 1,440評論 0 3