動態庫的使用

前言

說到動態庫,就不得不提靜態庫。靜態庫可以看做是一個具有特定功能的代碼塊,如果app中引用了靜態庫,則在編譯時會將靜態庫直接復制到app的可執行文件(也就是mach-o)中。
使用靜態庫會導致mach-o文件過大,而mach-o文件直接影響app的啟動時間和執行時占用的內存大小。

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

目錄
  1. 動態庫和靜態庫的區別
  2. 創建動態庫
  3. 使用動態庫
    3.1. 添加為依賴庫-啟動時加載
    3.2. 運行時加載
  4. 注入動態庫
  5. yololib

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

靜態庫的后綴名是以.a結尾,動態庫的后綴名可以是.dylib.framework結尾,所有的系統庫都屬于動態庫,在iOS中一般使用framework作為動態庫。

下面是apple官方的兩張圖,表示app啟動后內存的使用情況,很形象的說明了靜態庫和動態庫的區別

使用靜態庫的app



使用動態庫的app


在使用static linker鏈接app時,靜態庫會被完整的加載到app的mach-o文件(上圖中的Application file)中,作為mach-o文件的一部分,而動態庫不會被添加到mach-o文件中,這可以有效減少mach-o文件的大小。
如果app將動態庫作為它的依賴庫,則在mach-o文件中會添加了一個動態庫的引用;如果app在運行時動態加載動態庫,則在mach-o文件中不會添加動態庫的引用。

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

另外,使用動態庫可以縮短app的啟動時間。原因是,使用動態庫時,app的mach-o文件都會比較小;app依賴的動態庫可能已經存在于內存中了(其他已啟動的app也依賴了這個動態庫),所以不需要重復加載。

2. 創建動態庫

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

下面以創建LibPersonFramework為例

  1. 創建一個新工程,選擇iOS -> Cocoa Touch Framework

  2. 實現framework,并指定對外的頭文件

定義頭文件LibPerson.h

#import <Foundation/Foundation.h>

@interface LibPerson : NSObject

@property (nonatomic, copy) NSString *name ;

- (void)watch;

- (void)eat;

@end

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

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

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

3. 使用動態庫

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

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

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

創建一個新的工程DylibDemo,并引入LibPersonFramework.framework,在main.m文件中調用這個framework中的方法

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

最后運行一下,調用成功!

2018-06-04 16:32:09.076551+0800 DylibDemo[1790:700462] wang is watching TV!
2018-06-04 16:32:09.078597+0800 DylibDemo[1790:700462] wang is eating!
3.2. 運行時加載

在運行時加載動態庫,是指不需要在工程中引入動態庫,作為替代,在代碼中使用dlopen()這個函數來加載動態庫,在調用完成之后,需要調用相同次數的dlclose()函數來關閉動態庫。
除了dlopen()dlclose()以外,另外還有一個dlsym()函數來根據傳入的symbol獲取對應數據或函數的地址。在本例中,會使用runtime機制來代替dlsym()函數。(dlsym()一般是在c或c++中使用)

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

  2. 在main.h文件中加載和調用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()函數需要傳入兩個參數path和mode,path表示動態庫的mach-o文件的路徑,mode中可以包含多個標識符,比如RTLD_LAZYRTLD_NOW表示動態庫中的symbol什么時候被加載,RTLD_GLOBALRTLD_LOCAL表示symbol的可見性。(詳情可通過終端命令man dlopen查看)

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

另外,在上述代碼中還有一點需要注意的,在創建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

這是因為在編譯時,如果調用了[LibPerson new],編譯器會去驗證app的mach-o文件以及它依賴的動態庫的mach-o文件中是否有這個類的定義。
由于在編譯時,程序還沒有加載動態庫LibPersonFramework,而程序只包含了LIbPerson類的頭文件,并沒有它對應的.m文件(編譯器只會將.m文件編譯到最終的mach-o文件中),所以編譯器在app的mach-o文件以及它依賴的動態庫中找不到LibPerson類的定義,然后編譯器就報錯了。

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

  Class class_person = objc_getClass("LibPerson");
  LibPerson *person = [class_person new];
3. 添加動態庫LibPersonFramework文件

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


這個時候可能會報編譯錯誤,說找不到LibPersonFramework,所以接下來就需要添加LibPersonFramework。
在之前創建的LibPersonFramework.framework中,找到動態庫LibPersonFramework


找到app的包文件,鼠標右鍵點擊顯示包內容,然后將這個LibPersonFramework文件復制到這里


4. 給動態庫重簽名

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

添加一個腳本


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

到這里就做完了,運行一下,應該是成功的!

4. 注入動態庫

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

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

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


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

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

在app啟動時,會自動根據Load Commands指定的路徑去加載動態庫,所以必須保證路徑下存在對應的動態庫。

下面舉個例子

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

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

  1. 將動態庫LibInjectFramework復制到這個項目的app包中


  2. 添加動態庫依賴

這一步需要修改被注入app的mach-o文件,這里使用yololib來完成。將yololib下載后,然后編譯,將生產的命令復制到/usr/local/bin$PATH中的其他路徑,這樣就可以在終端使用這個命令了。
yololib需要兩個參數,第一個參數指定被修改的mach-o文件的路徑,第二個參數指定動態庫的路徑。

在項目中,添加兩個腳本命令,分別用來重簽名動態庫和修改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"

執行,控制臺應該會輸出下面這句

Inject success????????????????????

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

5. yololib

在使用yololib去添加動態庫依賴時,會修改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,所以需要修改的是ncmdssizeofcmds這兩個字段,它們分別表示Load Command的總數目和總大小。

  1. 添加一個dylib_command結構體

動態庫的信息是以dylib_command結構體的形式被存儲,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*/
};

創建一個dylib_command結構體,并添加到所有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結構體的大小加上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,添加動態庫的path字符串。

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

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,763評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,238評論 3 428
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,823評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,604評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,339評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,713評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,712評論 3 445
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,893評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,448評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,201評論 3 357
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,397評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,944評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,631評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,033評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,321評論 1 293
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,128評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,347評論 2 377

推薦閱讀更多精彩內容