說到動態庫,就不得不提靜態庫。靜態庫能夠看作是一個具備特定功能的代碼塊,若是app中引用了靜態庫,則在編譯時會將靜態庫直接複製到app的可執行文件(也就是mach-o)中。 使用靜態庫會致使mach-o文件過大,而mach-o文件直接影響app的啓動時間和執行時佔用的內存大小。c++
爲了減小mach-o文件的大小,須要用到動態庫。當app中引用了動態庫時,動態庫並不會被複制到app的mach-o文件中,只有當動態庫真正被用到時,纔會去加載(加載到內存中)和連接(動態庫可能引用了其餘庫)動態庫,多是在app啓動時或者是運行時。git
靜態庫的後綴名是以.a
結尾,動態庫的後綴名能夠是.dylib
或.framework
結尾,全部的系統庫都屬於動態庫,在iOS中通常使用framework做爲動態庫。github
下面是apple官方的兩張圖,表示app啓動後內存的使用狀況,很形象的說明了靜態庫和動態庫的區別bash
使用靜態庫的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的進程共用的,因此在內存中只會存在一份;若是是靜態庫,因爲每一個app的mach-o文件中都會存在一份,則會存在多份。相對靜態庫,使用動態庫能夠減小app佔用的內存大小。iphone
另外,使用動態庫能夠縮短app的啓動時間。緣由是,使用動態庫時,app的mach-o文件都會比較小;app依賴的動態庫可能已經存在於內存中了(其餘已啓動的app也依賴了這個動態庫),因此不須要重複加載。ide
上文提到過,動態庫通常有兩種,分別以.framework
和.dylib
後綴結尾,一般把它們叫作Framework和Shared Library。Framework本質上是由Shared Library加上頭文件header和其餘資源文件打包得來的。函數
下面以建立LibPersonFramework
爲例ui
建立一個新工程,選擇iOS -> Cocoa Touch Framework
實現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的架構模式,這裏選擇了Generic iOS Device
機型,而後build一下,就會建立一個通用mach-o文件,包含了arm64和arm_v7兩種架構。若是選擇了模擬器,會建立一個x86_64架構的mach-o文件。
須要注意的是,App和它依賴的framework的架構必須兼容,也就是說,在建立可執行文件時,要麼都是真機,要麼都是模擬器。固然,也能夠分別在真機和模擬器兩種模式下建立framwork,而後使用lipo
命令來將兩個framework內部的同名mach-o文件合併成一個通用mach-o文件,這樣,無論App是什麼架構模式,都能正確使用這個framework了。
使用動態庫有兩種方式,一種是將動態庫添加爲依賴庫,這樣會在工程啓動時加載動態庫,一種是使用dlopen
在運行時加載動態庫,這兩種方式的區別在於加載動態庫的時機。
在iOS中通常使用第一種方法,第二種方式通常在mac開發中使用,若是在iOS中使用了這種方式,是不能上架到App Store的。
建立一個新的工程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!
複製代碼
在運行時加載動態庫,是指不須要在工程中引入動態庫,做爲替代,在代碼中使用dlopen()
這個函數來加載動態庫,在調用完成以後,須要調用相同次數的dlclose()
函數來關閉動態庫。 除了dlopen()
和dlclose()
之外,另外還有一個dlsym()
函數來根據傳入的symbol獲取對應數據或函數的地址。在本例中,會使用runtime機制來代替dlsym()
函數。(dlsym()
通常是在c或c++中使用)
1. 建立新工程DylibDemo-Runtime,添加被調用庫的頭文件LibPerson.h
(這裏不須要添加LibPersonFramework.framework)
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_LAZY
和RTLD_NOW
表示動態庫中的symbol何時被加載,RTLD_GLOBAL
和RTLD_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"
複製代碼
到這裏就作完了,運行一下,應該是成功的!
注入動態庫是指,給一個現有的mach-o添加一個動態庫,這樣能夠在一個現有的app中執行動態庫的代碼。在給現有app注入動態庫時,這個動態庫只能做爲一個依賴庫被注入,這是由於在注入以前,不能在現有app中執行代碼,因此也就不能使用dlopen()
函數來加載動態庫了。
首先,觀察一下,當一個app添加了一個依賴庫以後,會有哪些變化。在上文中,DylibDemo添加了一個依賴庫LibPersonFramework.framework,下面就以這個項目做爲例子。
項目生成的app包中增長了Frameworks文件,若是是系統動態庫,則不會被添加到app包中。
mach-o文件中增長了一條Load Commands
數據,這條記錄表示了app對指定的動態庫的依賴。
使用MachOView打開app包中的mach-o文件
在app啓動時,會自動根據Load Commands
指定的路徑去加載動態庫,因此必須保證路徑下存在對應的動態庫。
新建一個動態庫LibInjectFramework,下面會將這個動態庫注入到一個現有app中,若是注入成功,則圖中的+[load]
方法會被執行。
新建一個項目DylibDemo-Inject,這個項目什麼代碼都沒有,只是一個空項目,下面須要將動態庫LibInjectFramework注入到這個項目中。
將動態庫LibInjectFramework複製到這個項目的app包中
添加動態庫依賴
這一步須要修改被注入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文件,而後每次運行前替換。
在使用yololib去添加動態庫依賴時,會修改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
的總數目和總大小。
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_Command
和Section
之間還預留了一部分空間,因此直接覆蓋不會影響Section
的數據。