你是否是常常會好奇本身參與的這麼些項目,爲何有的編譯起來很快,有的卻很慢;編譯完成後,有的啓動得很快,有的卻很慢。其實,在理解了編譯和啓動時連接器所作的事兒以後,你就能夠從根兒上找到這些問題的答案了。git
那麼,使用編譯器和解釋器執行代碼的特色,咱們就能夠歸納以下github
如今蘋果公司使用的編譯器是 LLVM,相比於 Xcode 5 版本前使用的 GCC,編譯速度提升了 3 倍shell
LLVM 是編譯器工具鏈技術的一個集合。而其中的 lldb 項目,就是內置連接器。編程
編譯器會對每一個文件進行編譯,生成 Mach-O(可執行文件);連接器會將項目中的多個 Mach-O 文件合併成一個。swift
我先簡單爲你總結下編譯的幾個主要過程:xcode
Mach-O 文件裏面的內容,主要就是代碼和數據緩存
不論是代碼仍是數據,它們的實例都須要由符號將其關聯起來。bash
由於 Mach-O 文件裏的那些代碼,好比 if、for、while 生成的機器指令序列,要操做的數據會存儲在某個地方,變量符號就須要綁定到數據的存儲地址。你寫的代碼還會引用其餘的代碼,引用的函數符號也須要綁定到該函數的地址上網絡
而連接器的做用,就是完成變量、函數符號和其地址綁定這樣的任務。而這裏咱們所說的符號,就能夠理解爲變量名和函數名app
若是地址和符號不作綁定的話,要讓機器知道你在操做什麼內存地址,你就須要在寫代碼時給每一個指令設好內存地址。寫這樣的代碼的過程,就像你直接在和不一樣平臺的機器溝通,連編譯生成 AST 和 IR 的步驟都省掉了,甚至優化平臺相關的代碼都須要你本身編寫。
這件事兒看起來挺酷,但可讀性和可維護性都會不好,好比修改代碼後對地址的維護就會讓你崩潰。而這種「崩潰」的罪魁禍首就是代碼和內存地址綁定得太早
另外,綁定得太早除了可讀性和可維護性差以外,還會有更多的重複工做。由於,你須要針對不一樣的平臺寫多份代碼,而這些代碼本能夠經過高級語言一次編譯成多份。既然這樣,那咱們應該怎麼辦呢?
咱們首先想到的就是,用匯編語言來讓這種綁定滯後。隨着編程語言的進化,咱們很快就發現,採用任何一種高級編程語言,均可以解決代碼和內存綁定過早產生的問題,同時還能掃掉使用匯編寫程序的煩惱。
你確定不但願一個項目是在一個文件裏從頭寫到尾的吧。項目中文件之間的變量和接口函數都是相互依賴的,因此這時咱們就須要經過連接器將項目中生成的多個 Mach-O 文件的符號和地址綁定起來。
沒有這個綁定過程的話,單個文件生成的 Mach-O 文件是沒法正常運行起來的。由於,若是運行時碰到調用在其餘文件中實現的函數的狀況時,就會找不到這個調用函數的地址,從而沒法繼續執行。
連接器在連接多個目標文件的過程當中,會建立一個符號表,用於記錄全部已定義的和全部未定義的符號。連接時若是出現相同符號的狀況,就會出現「ld: dumplicate symbols」的錯誤信息;若是在其餘目標文件裏沒有找到符號,就會提示「Undefined symbols」的錯誤信息。
你在項目裏爲某項需求寫了一些功能函數,但隨着業務的發展,一些功能被下掉了或者被其餘負責的同事在另外一個文件裏用其餘函數更新了功能。那麼這時,你之前寫的那些函數就沒有用武之地了。日長月久,無用的函數愈來愈多,生成的 Mach-O 文件也就愈來愈大。
這時,連接器在整理函數的符號調用關係時,就能夠幫你理清有哪些函數是沒被調用的,並自動去除掉。那這是怎麼實現的呢?
連接器在整理函數的調用關係時,會以 main 函數爲源頭,跟隨每一個引用,並將其標記爲 live。跟隨完成後,那些未被標記 live 的函數,就是無用函數。而後,連接器能夠經過打開 Dead code stripping 開關,來開啓自動去除無用代碼的功能。而且,這個開關是默認開啓的。
在真實的 iOS 開發中,你會發現不少功能都是現成可用的,不光你可以用,其餘 App 也在用,好比 GUI 框架、I/O、網絡等。連接這些共享庫到你的 Mach-O 文件,也是經過連接器來完成的
連接的共用庫分爲靜態庫和動態庫
Mach-O 文件是編譯後的產物,而動態庫在運行時纔會被連接,並沒參與 Mach-O 文件的編譯和連接。
因此 Mach-O 文件中並無包含動態庫裏的符號定義。也就是說,這些符號會顯示爲「未定義」,但它們的名字和對應的庫的路徑會被記錄下來。運行時經過 dlopen 和 dlsym 導入動態庫時,先根據記錄的庫路徑找到對應的庫,再經過記錄的名字符號找到綁定的地址。
dlopen 會把共享庫載入運行進程的地址空間,載入的共享庫也會有未定義的符號,這樣會觸發更多的共享庫被載入。dlopen 也能夠選擇是馬上解析全部引用仍是滯後去作。dlopen 打開動態庫後返回的是引用的指針,dlsym 的做用就是經過 dlopen 返回的動態庫指針和函數符號,獲得函數的地址而後使用。
使用 dyld 加載動態庫,有兩種方式
爲了減小啓動時間,大部分動態庫使用的都是符號第一次被用到時再綁定的方式。
加載過程開始會修正地址偏移,iOS 會用 ASLR 來作地址偏移避免攻擊,肯定 Non-Lazy Pointer 地址進行符號地址綁定,加載全部類,最後執行 load 方法和 Clang Attribute 的 constructor 修飾函數。每一個函數、全局變量和類都是經過符號的形式定義和使用的,當把目標文件連接成一個 Mach-O 文件時,連接器在目標文件和動態庫之間對符號作解析處理
這裏系統上的動態連接器會使用共享緩存,共享緩存在 /var/db/dyld/。當加載 Mach-O 文件時,動態連接器會先檢查是否有共享緩存。每一個進程都會在本身的地址空間映射這些共享緩存,這樣作能夠起到優化 App 啓動速度的做用
簡單來講, dyld 作了這麼幾件事兒:
John Holdsworth 開發了一個叫做 Injection 的工具能夠動態地將 Swift 或 Objective-C 的代碼在已運行的程序中執行,以加快調試速度,同時保證程序不用重啓
Injection 是咱們須要用到個一個工具,不要由於要用一個工具而厭煩這個方案,它很簡單。 它是免費的,app store 搜索:InjectionIII,Icon是 一個針筒。 也是開源的,
打開InjectionIII工具,選擇Open Project,選擇你的代碼所在的路徑,而後點擊Select Project Directory保存
1.設置AppDelegate.m 打開你的源碼,在AppDelegate.m的didFinishLaunchingWithOptions方法添加一行代碼
#if DEBUG
// iOS
[[NSBundle bundleWithPath:@"/Applications/InjectionIII.app/Contents/Resources/iOSInjection.bundle"] load];
#endif
複製代碼
2.設置ViewController 在須要修改界面的ViewController添加方法- (void)injected,或者給ViewController類擴展添加方法- (void)injected。 全部修改控件的代碼都寫在這裏面
- (void)injected
{
[self viewDidLoad];
}
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
UIView *red = [[UIView alloc]initWithFrame:CGRectMake(100, 100, 100, 100)];
red.backgroundColor = [UIColor redColor];
[self.view addSubview:red];
}
複製代碼
在Xcode Command+R運行項目 ,看到Injection connected 提示即表示配置成功。
💉 Injection connected 👍
💉 Have you remembered to add "-Xlinker -interposable" to your project's "Other Linker Flags"? 💉 Watching /Users/yunna/Desktop/a/** 複製代碼
在須要修改的頁面,修改控件UI,而後Command+S保存一下代碼,馬上就在模擬器上顯示修改的信息了。
用runtime 給每一個VC加個方法class_addMethod 依託InjectionIII的iOS熱部署配置文件,無侵害,導入即用。
@implementation InjectionIIIHelper
#if DEBUG
/**
InjectionIII 熱部署會調用的一個方法,
runtime給VC綁定上以後,每次部署完就從新viewDidLoad
*/
void injected (id self, SEL _cmd) {
//從新加載view
[self loadView];
[self viewDidLoad];
[self viewWillLayoutSubviews];
[self viewWillAppear:NO];
}
+ (void)load
{
//註冊項目啓動監聽
__block id observer =
[[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidFinishLaunchingNotification object:nil queue:nil usingBlock:^(NSNotification * _Nonnull note) {
//更改bundlePath
[[NSBundle bundleWithPath:@"/Applications/InjectionIII.app/Contents/Resources/iOSInjection.bundle"] load];
//[[NSBundle bundleWithPath:@"/Applications/InjectionIII.app/Contents/Resources/iOSInjection.bundle"] load];
[[NSNotificationCenter defaultCenter] removeObserver:observer];
}];
//給UIViewController 註冊injected 方法
class_addMethod([UIViewController class], NSSelectorFromString(@"injected"), (IMP)injected, "v@:");
}
#endif
@end
複製代碼
Injection 會監聽源代碼文件的變化,若是文件被改動了,Injection Server 就會執行 rebuildClass 從新進行編譯、打包成動態庫,也就是 .dylib 文件。編譯、打包成動態庫後使用 writeSting 方法經過 Socket 通知運行的 App
- (BOOL)writeString:(NSString *)string {
const char *utf8 = string.UTF8String;
uint32_t length = (uint32_t)strlen(utf8);
if (write(clientSocket, &length, sizeof length) != sizeof length ||
write(clientSocket, utf8, length) != length)
return FALSE;
return TRUE;
}
複製代碼
Server 會在後臺發送和監聽 Socket 消息,實現邏輯在 InjectionServer.mm 的 runInBackground 方法裏。Client 也會開啓一個後臺去發送和監聽 Socket 消息,實現邏輯在 InjectionClient.mm裏的 runInBackground 方法裏。
Client 接收到消息後會調用 inject(tmpfile: String) 方法,運行時進行類的動態替換。inject(tmpfile: String) 方法的具體實現代碼,你能夠點擊這個連接查看。
inject(tmpfile: String) 方法的代碼大部分都是作新類動態替換舊類。inject(tmpfile: String) 的入參 tmpfile 是動態庫的文件路徑,那麼這個動態庫是如何加載到可執行文件裏的呢?具體的實如今 inject(tmpfile: String) 方法開始裏,以下:
let newClasses = try SwiftEval.instance.loadAndInject(tmpfile: tmpfile)
複製代碼
你先看下 SwiftEval.instance.loadAndInject(tmpfile: tmpfile) 這個方法的代碼實現:
@objc func loadAndInject(tmpfile: String, oldClass: AnyClass? = nil) throws -> [AnyClass] {
print("???? Loading .dylib - Ignore any duplicate class warning...")
// load patched .dylib into process with new version of class
guard let dl = dlopen("\(tmpfile).dylib", RTLD_NOW) else {
throw evalError("dlopen() error: \(String(cString: dlerror()))")
}
print("???? Loaded .dylib - Ignore any duplicate class warning...")
if oldClass != nil {
// find patched version of class using symbol for existing
var info = Dl_info()
guard dladdr(unsafeBitCast(oldClass, to: UnsafeRawPointer.self), &info) != 0 else {
throw evalError("Could not locate class symbol")
}
debug(String(cString: info.dli_sname))
guard let newSymbol = dlsym(dl, info.dli_sname) else {
throw evalError("Could not locate newly loaded class symbol")
}
return [unsafeBitCast(newSymbol, to: AnyClass.self)]
}
else {
// grep out symbols for classes being injected from object file
try injectGenerics(tmpfile: tmpfile, handle: dl)
guard shell(command: """ \(xcodeDev)/Toolchains/XcodeDefault.xctoolchain/usr/bin/nm \(tmpfile).o | grep -E ' S _OBJC_CLASS_\\$_| _(_T0|\\$S).*CN$' | awk '{print $3}' >\(tmpfile).classes """) else {
throw evalError("Could not list class symbols")
}
guard var symbols = (try? String(contentsOfFile: "\(tmpfile).classes"))?.components(separatedBy: "\n") else {
throw evalError("Could not load class symbol list")
}
symbols.removeLast()
return Set(symbols.flatMap { dlsym(dl, String($0.dropFirst())) }).map { unsafeBitCast($0, to: AnyClass.self) }
複製代碼
在這段代碼中,你是否是看到你所熟悉的動態庫加載函數 dlopen 了呢?
guard let dl = dlopen("\(tmpfile).dylib", RTLD_NOW) else {
throw evalError("dlopen() error: \(String(cString: dlerror()))")
}
複製代碼
如上代碼所示,dlopen 會把 tmpfile 動態庫文件載入運行的 App 裏,返回指針 dl。接下來,dlsym 會獲得 tmpfile 動態庫的符號地址,而後就能夠處理類的替換工做了。dlsym 調用對應代碼以下:
guard let newSymbol = dlsym(dl, info.dli_sname) else {
throw evalError("Could not locate newly loaded class symbol")
}
複製代碼
當類的方法都被替換後,咱們就能夠開始從新繪製界面了。整個過程無需從新編譯和重啓 App,至此使用動態庫方式極速調試的目的就達成了。
我把 Injection 的工做原理用一張圖表示了出來,以下所示:
文章轉載自: