iOS動態庫的使用

wangzzzzz發表於2019-03-04

目錄

  1. 動態庫和靜態庫的區別
  2. 建立動態庫
  3. 使用動態庫 3.1. 新增為依賴庫-啟動時載入 3.2. 執行時載入
  4. 注入動態庫
  5. yololib

前言

說到動態庫,就不得不提靜態庫。靜態庫可以看做是一個具有特定功能的程式碼塊,如果app中引用了靜態庫,則在編譯時會將靜態庫直接複製到app的可執行檔案(也就是mach-o)中。 使用靜態庫會導致mach-o檔案過大,而mach-o檔案直接影響app的啟動時間和執行時佔用的記憶體大小。

為了減少mach-o檔案的大小,需要用到動態庫。當app中引用了動態庫時,動態庫並不會被複制到app的mach-o檔案中,只有當動態庫真正被用到時,才會去載入(載入到記憶體中)和連結(動態庫可能引用了其他庫)動態庫,可能是在app啟動時或者是執行時。

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

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

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

使用靜態庫的app

iOS動態庫的使用
使用動態庫的app
iOS動態庫的使用

在使用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

    iOS動態庫的使用

  2. 實現framework,並指定對外的標頭檔案

定義標頭檔案LibPerson.h

#import <Foundation/Foundation.h>

@interface LibPerson : NSObject

@property (nonatomic, copy) NSString *name ;

- (void)watch;

- (void)eat;

@end
複製程式碼

指定LibPersonFramework.hLibPerson.h為對外的標頭檔案

iOS動態庫的使用

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

iOS動態庫的使用

需要注意的是,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中的方法

iOS動態庫的使用

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

iOS動態庫的使用

最後執行一下,呼叫成功!

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)

iOS動態庫的使用

2. 在main.m檔案中載入和呼叫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的包檔案

iOS動態庫的使用

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

iOS動態庫的使用

找到app的包檔案,滑鼠右鍵點選顯示包內容,然後將這個LibPersonFramework檔案複製到這裡

iOS動態庫的使用

4. 給動態庫重簽名

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

新增一個指令碼

iOS動態庫的使用

/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包中。

    iOS動態庫的使用

  2. mach-o檔案中增加了一條Load Commands資料,這條記錄表示了app對指定的動態庫的依賴。

使用MachOView開啟app包中的mach-o檔案

iOS動態庫的使用

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

下面舉個例子

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

iOS動態庫的使用

新建一個專案DylibDemo-Inject,這個專案什麼程式碼都沒有,只是一個空專案,下面需要將動態庫LibInjectFramework注入到這個專案中。

  1. 將動態庫LibInjectFramework複製到這個專案的app包中

    iOS動態庫的使用

  2. 新增動態庫依賴

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

在專案中,新增兩個指令碼命令,分別用來重簽名動態庫和修改mach-o檔案

iOS動態庫的使用

/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的資料。

相關文章