前言
第三方庫是工程開發必不可少的部分,而第三方庫可以是.a和.framework的靜態庫,也可以是.framework的動態庫,其中靜態庫是最常用的方式。
靜態庫往往比較大,可在打包到可執行文件之后,對安裝包大小的增加遠遠小于靜態庫本身的Size。
那么,就產生兩個問題:
1、靜態庫里面存在什么內容?
2、靜態鏈接到可執行文件后為什么體積變小?
本文就以.framework的靜態庫來分析具體情況。
正文
1、framework靜態庫的打包
新建工程,選擇Cocoa Touch Framework,再到Build Settings
選擇Mach-O Type為Static Library,然后build出來目標文件就是framework靜態庫。
2、framework靜態庫的內容
按照上面的步驟打包出來的LYTestKit.framework,具體的內容如下:
- Headers為頭文件;
- plist文件和Modules文件夾是一些描述文件;
- LYTestKit為Mach-O binary文件,LYTestKit占了靜態庫99%的體積。
3、LYTestKit文件內容分析
打開terminal(終端),進入文件夾所在目錄,輸入指令file LYTestKit
。
LYTestKit: Mach-O universal binary with 4 architectures: [arm_v7:current ar archive] [arm64]
LYTestKit (for architecture armv7): current ar archive
LYTestKit (for architecture i386): current ar archive
LYTestKit (for architecture x86_64): current ar archive
LYTestKit (for architecture arm64): current ar archive
可以知道LYTestKit包括armv7
、i386
、x86_64
、arm64
四種CPU架構的指令。
接下來就是我們的大殺器Hopper Disassembler
登場。
軟件的鏈接地址,密碼:vbrqit。
用Hopper Disassembler
打開LYTestKit,選擇架構為arm64,再選擇一個原工程對應的.o文件,首先映入眼簾的是下面這段描述:(不知道.o文件是什么的,點這里復習下)
/*
--------------------------------------------------------------------------------
File: /Users/loyinglin/Documents/LYTestKit/Framework/LYTestKit.framework/LYTestKit
MachO file
CPU: aarch64
64 bits
--------------------------------------------------------------------------------
*/
接下來,是這個文件的描述:
; Segment
; Range: [0x0; 0x8a1b[ (35355 bytes)
; File offset : [3584; 38939[ (35355 bytes)
; Permissions: readable / writable / executable
整個文件的大小是35355 bytes,對應的區間為[3584, 38939]。
文件的最后一行為:
0000000000008a1a db 0x01 ; '.'
整個Segment的地址是0x8a1b結束,十六進制0x8a1b = 35355 十進制。
從這里可以看出,靜態庫中地址與大小一一對應。
可以這么理解,假如某條指令需要到地址0x0008a1a取值,即是相對應起始位置加上0x0008a1a偏移的地址。
而靜態庫中的偏移應該等于內存中的偏移,那么0x0008a1a的偏移地址在內存中的大小就是35355 bytes。
緊接著是下面這段描述:
; Section __text
; Range: [0x0; 0x2300[ (8960 bytes)
; File offset : [3584; 12544[ (8960 bytes)
; Flags: 0x80000400
; S_REGULAR
; S_ATTR_PURE_INSTRUCTIONS
; S_ATTR_SOME_INSTRUCTIONS
.Section __text
表示接下來是代碼段的內容,機器指令的大小為8960 bytes。
類似的,代碼段之后還有:
-
Section __objc_classrefs
類的引用,大小為 64 bytes; -
Section __objc_methname
自動生成和聲明的函數名, 大小為2140 bytes;("viewDidLoad"、"init"這樣的函數名常量) -
Section __objc_selrefs
selector的引用),大小為 520 bytes;(@selector(init)、@selector(setProperty:)) -
Section __objc_data
初始化的變量, 大小為80 bytes;(@property、ivar、 struct __objc_class這樣的變量) -
Section __objc_ivar
ivar的引用,大小為 84 bytes;(OBJC_IVAR$_SampleClass._property)
ivar是objc_ivar的指針,objc_ivar有變量名、變量類型等成員,如下:
typedef objc_ivar * Ivar;
struct objc_ivar {
char *ivar_name;
char *ivar_type;
int ivar_offset;
#ifdef __LP64__
int space;
#endif
}
-
Section __cstring
代碼中聲明的字符常量,大小為 1360 bytes;(@"Hello"、@"你好"這樣的字符常量) -
Section __cfstring
代碼中聲明的CFString字符常量,大小為 224 bytes; -
Section __objc_classname
Objective-C Class的類名,大小為 64 bytes; -
Section __objc_const
__objc_metaclass_xxx常量的引用和__objc_class_xxx structs 的聲明,大小為 3672 bytes;(OC的runtime用結構體比如struct __objc_method、 struct __objc_ivar 、struct __objc_property) -
Section __debug_str
調試用的字符信息,包括方法對應的字符串,大小為 5489 bytes; -
Section __debug_line
調試用的代碼行號信息,大小為 2891 bytes; - 以及各類其他的Section。
在大致了解一個.o文件的內部構成之后,我們來看看鏈接過程.o文件變化。
4、靜態鏈接的過程
靜態連接就是把靜態鏈接庫中的文件鏈接到可執行文件中,整個過程由鏈接器負責。
鏈接過程分為兩步:
- 1、空間和地址分配,掃描所有的目標文件,獲得各個段的長度、屬性、位置信息,并把所有的符號定義以及引用收集起來,放到全局的符號表中。通過所有段的長度,計算和合并后的長度和位置,并建立映射關系;
- 2、符號解析和重定位,使用上一步收集到的信息,讀取文件中段的數據和重定位信息,進行符號解析和重定位。
靜態鏈接的更詳細內容點這里。
那么可執行文件的內部是什么組成?
答案是:File Header
、.text secton
、.data secton
、.bss secton
四大部分。
File Header是文件頭,描述整個文件的文件屬性,包括是否可執行文件、目標操作系統、目標硬件等信息。文件頭還包括一個段表(Section Table)描述下面幾個段的偏移地址以及屬性。
.text section是代碼段,存放編譯生成的指令;
.data section是數據段,存放已初始化的靜態常量數據;
.bss section存放未初始化的靜態常量。
靜態庫鏈接過程體積變小的答案
framework靜態庫在鏈接之后,體積會急劇減少,原因有幾個:
1、用于鏈接的信息被剔除,比如說類引用、函數名等,字符信息中的函數名字等在鏈接時會放入鏈接表,用于查找地址,但不打入二進制文件;
2、調試用的信息比如符號串、代碼行號等不會打入二進制包,而是額外生成符號表;
3、Xcode默認在release下會用fastest的優化選項;
總結
經過此次思考,對靜態庫的內容有更清晰的認識,也對編譯與鏈接更加了解。
附錄
exploring Mach-O binaries
analysis of facebook app
classllvm MCSectionMachO
developer.apple Assemble asm_directives