[iOS研習記]——談談靜態庫與動態庫

[iOS研習記]——談談靜態庫與動態庫

在iOS項目開發中,靜態庫和動態庫我們時刻都在使用,離開了庫的支持,我們將會舉步維艱。比如,你要畫界面,總離不開UIKit這個庫吧,你要使用的各種基礎數據結構,如NSString,NSArray等,也離不開Foundation這個基礎庫。除了官方的庫外,開發中我們也會從Github等開源社區下載第三方的開源庫進行使用。一般我們使用的第三方庫或自己開發的庫都採用靜態庫的方式使用,而系統提供的庫大多是動態庫,方便多進程共享。雖然我們天天在用庫,但你對靜態庫和動態庫真的瞭解麼?靜態庫和動態庫的結構是怎樣的?靜態庫和動態庫有什麼區別?它們又是怎麼應用的?本節博客,我們就來聊一聊這些問題。

1. 引言

靜態庫與動態庫有很多相似之處,當然也有很多差異。

從後綴名來說,.a爲後綴名的庫文件是靜態庫,.dylib爲後綴名的庫文件是動態庫。在iOS開發中,更多時候我們使用的庫是以.framework爲後綴的。framework可以是靜態庫,也可以是動態庫,framework本身是一種打包方式。我們知道,我們在編寫代碼時,編寫的都是“源碼”,而要讓計算機理解這些源碼,就需要編譯器對源碼進行編譯,將其編譯成計算機可理解的“機器碼”,我們每編寫的一個源碼文件都會被編譯成一個二進制的.o文件,無論靜態庫還是動態庫,都是.o文件的合集。僅僅只有.o文件集合而成的庫文件,對於開發者來說是不夠的,在開發時我們不可能在沒有頭文件的情況下方便的調用庫中的方法,因此還需要有頭文件將庫中提供的接口暴露出來,還有時候,可能還需要一些其他資源,比如和頁面相關的庫會有內置一些圖片資源等,framework的功能就是將庫文件,頭文件,資源文件打包在一起,方便我們進行使用。下圖描述了framework文件與庫文件的關係:

2. 創建一個靜態庫

更深入的瞭解靜態庫之前,我們可以先創建一個靜態庫體驗下,首先使用Xcode創建一個新的工程,選擇Framework,如下圖所示:

創建好的framework工程模板,會生成一個和工程名相同的頭文件,以及一個Resources資源文件夾,我們可以創建新的功能類文件,例如可以新建一個命名爲MyLog的類和一個MyTool的類,代碼如下:

MyLog.h

// MyLog.h
#import <Foundation/Foundation.h>

@interface MyLog : NSObject

+ (void)log:(NSString *)str;

@end

MyLog.m

#import "MyLog.h"

@implementation MyLog

+ (void)log:(NSString *)str {
    NSLog(@"MyLog:%@",str);
}

@end

MyTool.h

#import <Foundation/Foundation.h>

@interface MyTool : NSObject

+ (NSInteger)add:(NSInteger)a another:(NSInteger)b;

@end

MyTool.m

#import "MyTool.h"

@implementation MyTool

+ (NSInteger)add:(NSInteger)a another:(NSInteger)b {
    return a + b;
}

@end

在默認生成的庫頭文件中,引入這兩個功能頭文件,如下:

#import <Foundation/Foundation.h>

//! Project version number for MyStatic.
FOUNDATION_EXPORT double MyStaticVersionNumber;

//! Project version string for MyStatic.
FOUNDATION_EXPORT const unsigned char MyStaticVersionString[];

#import "MyLog.h"
#import "MyTool.h"

在構建framewrok前,我們可以設置此framework構建成動動態庫還是靜態庫,我們先將其構建成靜態庫,設置編譯選項的Mach-o Type爲Static Library,如下:

之後,可以讓Xcode進行Build,之後在對應的Products文件夾中可以找到生成的framework文件,如下圖所示:

如果你查看此framework文件的包內容,會發現其中有5類文件,如下:

其中,_CodeSignature中存放的是framework的簽名文件。

Headers中存放的是頭文件,需要注意,在編譯framework工程時,要將需要暴露的頭文件設置爲public。

Info.plist文件是當前framework的配置文件。

Modules中的modulemap文件用來管理LLVM的module map,定義組件結構。

下面,我們可以嘗試使用下此靜態庫,使用Xcode新建一個名爲LibDemo的iOS工程,將前面構建的MyStatic.framework文件直接拖入此工程中,在工程的編譯選項中,找到Framework Search Paths和Header Search Paths中分別將此framework的路徑與頭文件的路徑進行配置,如下圖所示:

修改測試項目的ViewController.m文件如下:

#import "ViewController.h"
#import "MyStatic.framework/Headers/MyStatic.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    NSInteger a = 100;
    NSInteger b = 200;
    NSInteger c = [MyTool add:a another:b];
    [MyLog log:[NSString stringWithFormat:@"%ld", c]];
}


@end

運行代碼,從控制檯可以看到,我們的靜態庫已經可以正常工作了。你可能會覺得上面的頭文件引入方式非常的醜陋,你完全可以在工程中新建一個文件夾,將framework包內的頭文件拷貝過來,如下圖:

這樣你就可以像引用工程內的頭文件一樣的使用framework中的功能了:

#import "ViewController.h"
#import "MyStatic.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    NSInteger a = 100;
    NSInteger b = 200;
    NSInteger c = [MyTool add:a another:b];
    [MyLog log:[NSString stringWithFormat:@"%ld", c]];
}

@end

3.試試動態庫吧

靜態庫的構建和使用過程看上去非常容易,動態庫應該也與其類似。我們現在就來試試吧,使用Xcode新建一個命名爲MyDylib的framework工程,將編譯選項中的Mach-O Type 改爲Dynamic Library,創建一些簡單的測試類如下:

MyObjectOne.h

#import <Foundation/Foundation.h>

@interface MyObjectOne : NSObject

@property(copy) NSString *name;

@end

MyObjectOne.m

#import "MyObjectOne.h"

@implementation MyObjectOne

@end

MyObjectTwo.h

#import <Foundation/Foundation.h>

@interface MyObjectTwo : NSObject

@property(copy) NSString *title;

@end

MyObjectTwo.m

#import "MyObjectTwo.h"

@implementation MyObjectTwo

@end

按照同樣的方式,將構建好的framework文件拖入到測試工程中,配置頭文件路徑,添加測試代碼如下:

#import "ViewController.h"
#import "MyStatic.h"
#import "MyDylib.framework/Headers/MyDylib.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    NSInteger a = 100;
    NSInteger b = 200;
    NSInteger c = [MyTool add:a another:b];
    [MyLog log:[NSString stringWithFormat:@"%ld", c]];
    
    MyObjectOne *one = [[MyObjectOne alloc] init];
    one.name = @"Hello";
    [MyLog log:one.name];
}

試下編譯運行,目前爲止,看上去一切正常,但是當程序運行起來後會崩潰,控制檯會輸出如下信息:

dyld[72035]: Library not loaded: @rpath/MyDylib.framework/MyDylib

產生這個異常的原因是沒有找到動態庫文件,靜態庫的動態庫的區別出現了,怎麼解決這個問題呢,其實很簡單,我們找到當前測試工程編譯的產出可執行文件,點擊顯示包內容,在其中新建一個Frameworks的文件夾,將MyDylib.framework文件拷貝進入,如下圖所示:

現在再運行工程,你會發現程序已經可以正常執行了。但是手動拷貝動態庫到可執行文件的操作非常不優雅,如果真的要在項目中使用動態庫,我們更多時候會通過自動化的腳本來實現複製庫文件這一步操作。

通過這些實踐,我們好像能感覺到靜態庫和動態庫之間有些什麼不同,但究竟哪裏不同呢?我們帶着疑問繼續探索。

4.靜態庫和動態庫的不同之處

Ⅰ. 載入的方式

出現前面動態庫無法找到的原因其實是動態庫與靜態庫的載入方式不同。

靜態庫:靜態庫在鏈接時,會被完整的複製到可執行文件中,如果有多個應用使用了相同的靜態庫,每個應用的二進制文件中都會有一份完整的靜態庫代碼。

動態庫:程序在鏈接時,動態庫並不會被複制進二進制文件,而是在程序運行時由系統動態加載到內存供程序進行調用。由於這種特性,動態庫系統可以只加載一次,應用程序共享。

對於靜態庫動態庫載入方式來說,我們可以再深一步。首先,靜態庫會被完整複製進可執行文件中,這裏的完整其實是不精準的,我們在引入第三方庫時,往往需要在工程的Other Linker Flags中配置-Objc選項,這一項的作用是對鏈接優化做設置。

默認情況下,靜態庫在鏈接的時候,並不會把所有代碼都複製到可執行文件,其只會複製使用到的代碼,這樣可以減少最終應用包的體積,但是OC語言的動態性決定了並非代碼直接引用纔算使用,這種連接方式經常會產生運行時的問題。

設置-Objc選項後,鏈接器不管代碼中有沒有使用,都會將OC類和其對應的Category全部加載進來。

設置-all_load選項後,鏈接器會把所有目標文件都加載進來,不止侷限與OC文件。

設置-force_load參數可以指定強制加載某個靜態庫的所有目標文件,對這個靜態庫來說,作用與-all_load一樣。

對於動態庫來說,鏈接器就沒有辦法做這樣的優化動作了,因爲動態庫是運行時加載的,鏈接器不知道哪些代碼會被用到,因此從這一個方面來說,靜態庫對包大小的優化貌似會比動態庫更加優異,但是真的是這樣麼?我們先留下個伏筆,後面再分析。

Ⅱ.文件結構不同

靜態庫和動態庫的本質區別還是在於構建出的文件結構完全不同。可以使用MachOView工具來查看庫文件。

我們先說靜態庫,MachOView打開的靜態庫結構如下:

可以看到,靜態庫的結構其實是比較簡單的,除了庫本身的一些描述文件,符號表外,基本就是其他可執行文件的集合了,在圖中可以看到,每個可執行文件都會有一些頭數據,這些頭數據記錄了可執行未見的名字,大小等信息。可以點開任意一個可執行文件,其中就是我們熟悉的各種代碼段,數據段等數據了:

我們再來看動態庫,其結構如下:

可以看到動態庫本身就是一個可執行文件,其並不是將內部的所有.o文件做簡單的集合,而是一個最終鏈接完成的鏡像文件。由於動態庫是運行時進行鏈接的,其無法做編譯時的優化,看上去可能會增加應用包的大小,但是實際應用中,我們大多會採用-Objc參數來強制靜態庫鏈接所有OC文件,並且靜態庫中每一個.o文件都會有一個頭信息,而動態庫則省略了這部分信息,因此最終對影響應用包大小這一方面來說,並不一定靜態庫更優。但是有一點是確定了,靜態庫是編譯時鏈接,會節省應用啓動時間。往往在做優化類的項目時,沒有固定的方案,我們要根據實際情況,選擇最合適自己的方案。

5.動態庫與運行時

Ⅰ. 動態庫的加載

只要說到運行時,對開發者來說就大有可爲之處。首先,我們先思考下,前面的測試工程,如果我們不拷貝動態庫文件到IPA包內的時候,爲什麼程序運行會找不到這個庫文件?又爲什麼我們需要將動態庫拷貝進IPA包的Frameworks文件夾纔行?別的文件夾不行麼?

要解釋上面的問題,我們還是要從動態庫的加載原理上來看,可以用MachOView打開測試應用包的可執行文件,找到其中的Load Commands段,如下圖所示:

可以看到,其中有一些動態庫的加載指令,Foundation,UIKit等都是系統的動態庫,我們可以在其詳情中看到詳細的加載路徑,如下:

對於我們自己的MyDylib庫,其加載路徑如下:

可以看到,這個動態庫是從@rpath/MyDylib.framework/MyDylib這個路徑來加載的,這個加載路徑的設置在動態庫編譯時就已經確定,我們可以看下MyDylib這個工程,在Xcode的編譯配置選項中,找到Dynamic Library Install Name選項,如下所示:

這裏的@rpath實際上是一個環境變量,在應用工程中可以配置@rpath的值,在LibDemo工程的編譯選項中搜rpath,可以看到這個環境變量的配置:

現在我們清楚了,其實動態庫文件不一定要放入Frameworks文件夾下,修改@rpath變量的路徑即可修改動態庫的加載路徑。

對於動態庫的這種加載方式,原則上,我們可以修改此二進制文件的加載路徑,也可以直接替換包內的動態庫文件,實現一些逆向注入的功能,非常酷。

Ⅱ. 代碼載入動態庫

動態庫是在運行時被加載的,我們也可以在運行時使用代碼動態的控制動態庫的載入。可以將測試工程中引用MyDylib的地方全部刪掉,將配置的頭文件路徑也去掉,我們將這個動態庫拷貝進工程的Bundle中,如下:

修改ViewController類的代碼如下:

#import "ViewController.h"
#import "MyStatic.h"
#import <dlfcn.h>

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    NSInteger a = 100;
    NSInteger b = 200;
    NSInteger c = [MyTool add:a another:b];
    [MyLog log:[NSString stringWithFormat:@"%ld", c]];
    
    NSString *path = [[[NSBundle mainBundle] pathForResource:@"MyDylib" ofType:@"framework"] stringByAppendingString:@"/MyDylib"];
    // 載入動態庫
    void * p = dlopen([path cStringUsingEncoding:NSUTF8StringEncoding], RTLD_LAZY);
    if (p) {
        // 加載動態庫成功 直接使用
        Class cls = NSClassFromString(@"MyObjectOne");
        NSObject *obj = [[cls alloc] init];
        [obj performSelector:@selector(setName:) withObject:@"Hello"];
        [MyLog log:[obj performSelector:@selector(name)]];
    }
}

@end

此時,再次編譯運行此工程,如果你觀察測試項目的二進制文件,裏面的加載命令中已經沒有了MyDylib的加載,但是程序依然可以正常的執行,dlopen函數的作用就是在運行時載入動態鏈接庫,載入成功後,我們可以藉助OC的運行時方法,直接調用到動態庫中的代碼。通過這種方式,我們實際上可以實現插件動態下載與使用,使得應用有非常高的熱更新能力,但是需要注意,動態下載動態庫的方式並不允許在AppStore上架,我們只能在測試的App或企業的App中使用。

再進一步說,其實動態庫的讀取並不一定是從本地沙盒中,在本地調試時,你可以從任何位置讀取動態庫文件進行加載,這可以在本地實現很多非常酷的功能,比如Injection工具,它通過一個服務監聽代碼文件的變化,之後將其打包成動態庫注入到程序中,再通過運行時替換類和方法,從而實現本地開發iOS項目的熱更新效果,非常好用。

專注技術,懂的熱愛,願意分享,做個朋友

QQ:316045346

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章