這篇是對(duì) iOS 應(yīng)用啟動(dòng)時(shí)箩祥,main 函數(shù)執(zhí)行前發(fā)生的事的一點(diǎn)總結(jié),限于水平,如有錯(cuò)誤請(qǐng)指正~
FAT 二進(jìn)制
FAT 二進(jìn)制文件,將多種架構(gòu)的 Mach-O 文件合并而成县匠。它通過(guò) Fat Header 來(lái)記錄不同架構(gòu)在文件中的偏移量,F(xiàn)at Header 占一頁(yè)(64位16kb撒轮,32位4kb)的空間乞旦。
按分頁(yè)來(lái)存儲(chǔ)這些 segement 和 header 會(huì)浪費(fèi)空間,但這有利于虛擬內(nèi)存的實(shí)現(xiàn)题山。
Mach-O 文件
Mach-O
為 Mach Object 文件格式的縮寫(xiě)兰粉,它是一種用于可執(zhí)行文件,目標(biāo)代碼顶瞳,動(dòng)態(tài)庫(kù)玖姑,內(nèi)核轉(zhuǎn)儲(chǔ)的文件格式愕秫。
在 Mac OS X 系統(tǒng)中使用 Mach-O 作為其可執(zhí)行文件類(lèi)型。
它的組成結(jié)構(gòu)如下圖所示:
每個(gè) Mach-O 文件包括一個(gè) Mach-O Header焰络,然后是一系列的載入命令 load commands戴甩,再是一個(gè)或多個(gè)段(segment),每個(gè)段包括0到255個(gè)節(jié)(section)闪彼。Mach-O使用REL再定位格式控制對(duì)符號(hào)的引用甜孤。Mach-O在兩級(jí)命名空間中將每個(gè)符號(hào)編碼成“對(duì)象-符號(hào)名”對(duì),在查找符號(hào)時(shí)則采用線性搜索法畏腕。
Mach-O包含了幾個(gè) segment缴川,每個(gè) segment 又包含幾個(gè) section。segment的名字都是大寫(xiě)的郊尝,例如__DATA;section的名字都是小寫(xiě)的, 例如 __text二跋。在 Mach-O 的類(lèi)型不為MH_OBJECT
時(shí),空間大小為頁(yè)的整數(shù)倍流昏。頁(yè)的大小跟硬件有關(guān)扎即,在 arm64 架構(gòu)一頁(yè)是16kb,其余為4kb况凉。
section 雖然沒(méi)有整數(shù)倍頁(yè)大小的限制谚鄙,但是 section 之間不會(huì)有重疊。
Mach-O Header
推薦使用MachOView
這個(gè)軟件查看 Mach-O 的文件結(jié)構(gòu)刁绒。注意需要手動(dòng)關(guān)閉 processing
闷营,不然會(huì)閃退。下面是用 MachOView 查看自己的應(yīng)用結(jié)構(gòu):
東西有點(diǎn)多就沒(méi)有截取全部知市。我們查看一下Mach-O Header
部分
下面是64位架構(gòu)下header
的數(shù)據(jù)結(jié)構(gòu):
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 */
};
Mach-O 全部的 filetype 和 flags在loader.h
中找到傻盟。
除了MH_OBJECT
以外的所有類(lèi)型,段(Segment)的空間大小均為頁(yè)的整數(shù)倍嫂丙。頁(yè)的大小跟硬件有關(guān)系娘赴,在 arm64 架構(gòu)下一頁(yè)為16kb,其它為4kb跟啤。
Load commands
Load commands
緊跟在頭部之后, 當(dāng)加載過(guò) header 之后诽表,會(huì)通過(guò)解析Load commands
來(lái)加載剩下的數(shù)據(jù),確定其內(nèi)存的分布隅肥。
下面是 load commands 的結(jié)構(gòu)定義:
struct load_command {
uint32_t cmd; /* 載入命令類(lèi)型 */
uint32_t cmdsize; /* total size of command in bytes */
};
所有load commands
的大小即為 Header->sizeofcmds, 共有 Header->ncmds 條load command
竿奏。
load command 以LC
開(kāi)頭,不同的加載命令有不同的專(zhuān)有的結(jié)構(gòu)體腥放,cmd 和 cmdsize 是都有的泛啸,分別為命令類(lèi)型(即命令名稱(chēng)),這條命令的長(zhǎng)度捉片。這些加載命令告訴系統(tǒng)應(yīng)該如何處理后面的二進(jìn)制數(shù)據(jù)平痰,對(duì)系統(tǒng)內(nèi)核加載器和動(dòng)態(tài)鏈接器起指導(dǎo)作用汞舱。如果當(dāng)前 LC_SEGMENT 包含 section伍纫,那么 section 的結(jié)構(gòu)體緊跟在 LC_SEGMENT 的結(jié)構(gòu)體之后宗雇,所占字節(jié)數(shù)由 SEGMENT 的 cmdsize 字段給出。
cmd(命令名稱(chēng)) | 作用 |
---|---|
LC_SEGMENT_64 | 將對(duì)應(yīng)的段中的數(shù)據(jù)加載并映射到進(jìn)程的內(nèi)存空間去 |
LC_SYMTAB | 符號(hào)表信息 |
LC_DYSYMTAB | 動(dòng)態(tài)符號(hào)表信息 |
LC_LOAD_DYLINKER | 啟動(dòng)動(dòng)態(tài)加載連接器/usr/lib/dyld程序 |
LC_UUID | 唯一的 UUID莹规,標(biāo)示該二進(jìn)制文件赔蒲,128bit |
LC_VERSION_MIN_IPHONEOS/MACOSX | 要求的最低系統(tǒng)版本(Xcode中的Deployment Target) |
LC_MAIN | 設(shè)置程序主線程的入口地址和棧大小 |
LC_ENCRYPTION_INFO | 加密信息 |
LC_LOAD_DYLIB | 加載的動(dòng)態(tài)庫(kù),包括動(dòng)態(tài)庫(kù)地址良漱、名稱(chēng)舞虱、版本號(hào)等 |
LC_FUNCTION_STARTS | 函數(shù)地址起始表 |
LC_CODE_SIGNATURE | 代碼簽名信息 |
注意:不同類(lèi)型的 segment 會(huì)使用不同的函數(shù)來(lái)加載
Segment
Mach-O 文件中由許多個(gè)段(Segment),每個(gè)段都有不同的功能母市,每個(gè)段包含了許多個(gè)小的Section矾兜。LC_SEGMENT
意味著這部分文件需要映射到進(jìn)程的地址空間去,幾乎所有 Mach-O 都包含這三個(gè)段:
- __TEXT:包含了執(zhí)行代碼和其它只讀數(shù)據(jù)(如C 字符串)患久。權(quán)限:只讀(VM_PROT_READ)椅寺,可執(zhí)行(VM_PROT_EXECUTE)
- __DATA:程序數(shù)據(jù),包含全局變量蒋失,靜態(tài)變量等返帕。權(quán)限:可讀寫(xiě)(VM_PROT_WRITE/READ) 可執(zhí)行(VM_PROT_EXECUTE)
- __LINKEDIT:包含了加載程序的"元數(shù)據(jù)",比如函數(shù)的名稱(chēng)和地址篙挽。權(quán)限:只讀(VM_PROT_READ)
除了上面三個(gè)荆萤,還有一個(gè)常見(jiàn)的 segment:
- __PAGEZERO:空指針陷阱段,映射到虛擬內(nèi)存空間的第一頁(yè)铣卡,用于捕捉對(duì) NULL 指針的引用
LC_SEGMENT_64
的結(jié)構(gòu)定義為:
struct segment_command_64 { /* for 64-bit architectures */
uint32_t cmd; /* LC_SEGMENT_64 */
uint32_t cmdsize; /* includes sizeof section_64 structs */
char segname[16]; /* segment name */
uint64_t vmaddr; /* memory address of this segment */
uint64_t vmsize; /* memory size of this segment */
uint64_t fileoff; /* file offset of this segment */
uint64_t filesize; /* amount to map from the file */
vm_prot_t maxprot; /* maximum VM protection */
vm_prot_t initprot; /* initial VM protection */
uint32_t nsects; /* number of sections in segment */
uint32_t flags; /* flags */
};
可以看到這里大部分的成員變量都是幫助內(nèi)核將 segment 映射到虛擬內(nèi)存的链韭。nsects
即表明該段中包含多少個(gè) section,section是具體數(shù)據(jù)存放的地方煮落。cmdsize
表示當(dāng)前 segment 結(jié)構(gòu)體以及它所包含的所有 section 結(jié)構(gòu)體的總大小敞峭。
文件映射的起始位置由fileoff
給出,映射到地址空間的vmaddr
處州邢。
Section
section 的名字均為小寫(xiě)儡陨。section 是具體數(shù)據(jù)存放的地方,它的結(jié)構(gòu)體跟隨在 LC_SEGMENT 結(jié)構(gòu)體之后量淌。在64位環(huán)境中它的結(jié)構(gòu)定義為:
struct section_64 { /* for 64-bit architectures */
char sectname[16]; /* name of this section */
char segname[16]; /* segment this section goes in */
uint64_t addr; /* memory address of this section */
uint64_t size; /* size in bytes of this section */
uint32_t offset; /* file offset of this section */
uint32_t align; /* section alignment (power of 2) */
uint32_t reloff; /* file offset of relocation entries */
uint32_t nreloc; /* number of relocation entries */
uint32_t flags; /* flags (section type and attributes)*/
uint32_t reserved1; /* reserved (for offset or index) */
uint32_t reserved2; /* reserved (for count or sizeof) */
uint32_t reserved3; /* reserved */
};
其中flasg
字段儲(chǔ)存了兩個(gè)屬性的值:type 和 attributes骗村。type 只能有一個(gè)值,而 attributes 的值可以有多個(gè)呀枢。如果 segment 中任何一個(gè) section 擁有屬性 S_ATTR_DEBUG胚股,那么該段所有的 section 都會(huì)擁有這個(gè)屬性。屬性詳情可以參考loader.h
section name | 作用 |
---|---|
__text | 主程序代碼 |
__stub_helper | 用于動(dòng)態(tài)鏈接的存根 |
__symbolstub1 | 用于動(dòng)態(tài)鏈接的存根 |
__objc_methname | Objective-C 的方法名 |
__objc_classname | Objective-C 的類(lèi)名 |
__cstring | 硬編碼的字符串 |
__lazy_symbol | 懶加載裙秋,延遲加載節(jié)琅拌,通過(guò) dyld_stub_binder 輔助鏈接 |
_got | 存儲(chǔ)引用符號(hào)的實(shí)際地址缨伊,類(lèi)似于動(dòng)態(tài)符號(hào)表 |
__nl_symbol_ptr | 非延遲加載節(jié) |
__mod_init_func | 初始化的全局函數(shù)地址,在 main 之前被調(diào)用 |
__mod_term_func | 結(jié)束函數(shù)地址 |
__cfstring | Core Foundation 用到的字符串(OC字符串) |
__objc_clsslist | Objective-C 的類(lèi)列表 |
__objc_nlclslist | Objective-C 的 +load 函數(shù)列表进宝,比 __mod_init_func 更早執(zhí)行 |
__objc_const | Objective-C 的常量 |
__data | 初始化的可變的變量 |
__bss | 未初始化的靜態(tài)變量 |
虛擬內(nèi)存
在 segment 的結(jié)構(gòu)體中刻坊,我們可以看到vmaddr
和vmsize
兩個(gè)成員變量,它們分別代表 segment 在虛擬內(nèi)存中的地址以及大小党晋。
虛擬內(nèi)存
就是一層間接尋址(indirection)谭胚。軟件工程中有句格言就是任何問(wèn)題都能通過(guò)添加一個(gè)間接層來(lái)解決。虛擬內(nèi)存解決的是管理所有進(jìn)程使用物理 RAM 的問(wèn)題未玻。通過(guò)添加間接層來(lái)讓每個(gè)進(jìn)程使用邏輯地址空間灾而,它可以映射到 RAM 上的某個(gè)物理頁(yè)上。這種映射不是一對(duì)一的扳剿,邏輯地址可能映射不到 RAM 上旁趟,也可能有多個(gè)邏輯地址映射到同一個(gè)物理 RAM 上。針對(duì)第一種情況庇绽,當(dāng)進(jìn)程要存儲(chǔ)邏輯地址內(nèi)容時(shí)會(huì)觸發(fā) page fault锡搜;第二種情況就是多進(jìn)程共享內(nèi)存。
對(duì)于文件可以不用一次性讀入整個(gè)文件敛劝,可以使用分頁(yè)映射(mmap())
的方式讀取余爆。也就是把文件某個(gè)片段映射到進(jìn)程邏輯內(nèi)存的某個(gè)頁(yè)上。當(dāng)某個(gè)想要讀取的頁(yè)沒(méi)有在內(nèi)存中夸盟,就會(huì)觸發(fā) page fault蛾方,內(nèi)核只會(huì)讀入那一頁(yè),實(shí)現(xiàn)文件的懶加載上陕。
也就是說(shuō) Mach-O 文件中的__TEXT
段可以映射到多個(gè)進(jìn)程桩砰,并可以懶加載,且進(jìn)程之間共享內(nèi)存释簿。__DATA
段是可讀寫(xiě)的亚隅。這里使用到了Copy-On-Write
技術(shù),簡(jiǎn)稱(chēng) COW庶溶。也就是多個(gè)進(jìn)程共享一頁(yè)內(nèi)存空間時(shí)煮纵,一旦有進(jìn)程要做寫(xiě)操作,它會(huì)先將這頁(yè)內(nèi)存內(nèi)容復(fù)制一份出來(lái)偏螺,然后重新映射邏輯地址到新的 RAM 頁(yè)上行疏。也就是這個(gè)進(jìn)程自己擁有了那頁(yè)內(nèi)存的拷貝。這就涉及到了 clean/dirty page 的概念套像。dirty page 含有進(jìn)程自己的信息酿联,而 clean page 可以被內(nèi)核重新生成(重新讀磁盤(pán))。所以 dirty page 的代價(jià)大于 clean page。
在多個(gè)進(jìn)程加載 Mach-O 文件時(shí)__TEXT
和__LINKEDIT
因?yàn)橹蛔x贞让,都是可以共享內(nèi)存的周崭。而__DATA
因?yàn)榭勺x寫(xiě),就會(huì)產(chǎn)生 dirty page喳张。當(dāng) dyld 執(zhí)行結(jié)束后续镇,__LINKEDIT
就沒(méi)用了,對(duì)應(yīng)的內(nèi)存頁(yè)會(huì)被回收蹲姐。
dyld
dyld(the dynamic link editor)磨取,Apple 的動(dòng)態(tài)鏈接器人柿。在內(nèi)核完成映射進(jìn)程的工作后會(huì)啟動(dòng)dyld
柴墩,負(fù)責(zé)加載應(yīng)用依賴的所有動(dòng)態(tài)鏈接庫(kù),準(zhǔn)備好運(yùn)行所需的一切凫岖。
在 App 啟動(dòng)的時(shí)候江咳,首先會(huì)加載 App 的 mach-o 文件,從 load commands 中得到 dyld 的路徑哥放,并且運(yùn)行歼指。隨后 dyld 做的事情順序概括如下:
- 初始化運(yùn)行環(huán)境
- 加載主程序執(zhí)行文件 生成 image, 將image添加到一個(gè)全局容器中
- 加載共享緩存
- 根據(jù)依賴鏈遞歸加載動(dòng)態(tài)鏈接庫(kù) dylib,如果在緩存中有加載好的 image 則直接拿出來(lái)甥雕,否則生成一個(gè)新的 image踩身,將image添加到一個(gè)全局容器中
- link 主執(zhí)行文件
- link dylib
- 根據(jù)依賴鏈遞歸修正指針 Rebase
- 根據(jù)依賴鏈遞歸符號(hào)綁定 Bind
- 初始化 dylib(runtime 的初始化就在這個(gè)時(shí)候)
在加載完所有的 dylib 之后,它們處于互相獨(dú)立的狀態(tài)社露,所以還需要將它們綁定起來(lái)挟阻。代碼簽名讓我們不能修改指令,所以不能直接讓一個(gè) dylib 調(diào)用另一個(gè) dylib峭弟,這時(shí)需要很多間接層附鸽。
這個(gè)時(shí)候需要 dyld 來(lái)修正指針(rebasing)和綁定符號(hào)(binding)。
詳細(xì)可以查看 dyld 的源碼中的_main
函數(shù)瞒瘸。
下面會(huì)分析上述的其中幾個(gè)步驟坷备。
ImageLoader
ImageLoader
是一個(gè)將 mach-o 文件里面二進(jìn)制數(shù)據(jù)(編譯過(guò)的代碼、符號(hào)等)加載到內(nèi)存的基類(lèi)情臭,它負(fù)責(zé)將 mach-o 中的二進(jìn)制數(shù)據(jù)映射到內(nèi)存省撑,它的實(shí)例就是我們熟悉的 image。
每一個(gè) mach-o 文件都會(huì)有一個(gè)對(duì)應(yīng)的 image俯在,實(shí)例的類(lèi)型根據(jù) mach-o 格式的不同也會(huì)不同竟秫。
- ImageLoaderMachOClassic: 用于加載
__LINKEDIT
段為傳統(tǒng)格式的 mach-o 文件 - ImageLoaderMachOCompressed: 用于加載
__LINKEDIT
段為壓縮格式的 mach-o 文件
因?yàn)?code>dylib之間有依賴關(guān)系,所以系統(tǒng)會(huì)沿著依賴鏈遞歸加載 image朝巫。
Rebasing
dylib
的二進(jìn)制數(shù)據(jù)會(huì)隨機(jī)的映射到內(nèi)存的一個(gè)隨機(jī)地址ASLR(Address space layout randomization,)中鸿摇,這個(gè)隨機(jī)的地址跟代碼和數(shù)據(jù)指向的舊地址(preferred_address)會(huì)有一定的偏差,dyld
需要修正這個(gè)偏差(slide)劈猿,做法就是將dylib
內(nèi)部的指針地址都加上這個(gè)偏移值拙吉,偏移值的計(jì)算方法如下:
slide = actual_address - preferred_address
隨后就是不斷的將__DATA
段中需要修正的指針加上這個(gè)偏移值潮孽。
注意:每次程序啟動(dòng)后的地址都會(huì)變化,所以指針的地址都需要重新修正筷黔。
在 mach-o 的一個(gè)載入命令LC_DYLD_INFO_ONLY
可以查看到rebase
, bind
, week_bind
,lazy_bind
的偏移量和大小往史。
Binding
binding
處理那些指向dylib
外部的指針,它們實(shí)際上被符號(hào)名稱(chēng)(symbol)綁定佛舱,也就是個(gè)字符串椎例。比如我們 objc 代碼中需要使用到 NSObject, 即符號(hào)OBJC_CLASS$_NSObject,但是這個(gè)符號(hào)不存在當(dāng)前的 image 中请祖,而是在系統(tǒng)庫(kù) Foundation.framework中订歪,因此就需要binding這個(gè)操作將對(duì)應(yīng)關(guān)系綁定到一起。
Lazy Binding
lazyBinding
就是在加載動(dòng)態(tài)庫(kù)的時(shí)候不會(huì)立即 binding, 當(dāng)時(shí)當(dāng)?shù)谝淮握{(diào)用這個(gè)方法的時(shí)候再實(shí)施 binding肆捕。 做到的方法也很簡(jiǎn)單: 通過(guò)dyld_stub_binder
這個(gè)符號(hào)來(lái)做刷晋。lazy binding 的方法第一次會(huì)調(diào)用到 dyld_stub_binder, 然后 dyld_stub_binder負(fù)責(zé)找到真實(shí)的方法,并且將地址bind到樁上慎陵,下一次就不用再bind了眼虱。
多數(shù)符號(hào)都是 lazy binding 的
Runtime
每一個(gè)dylib
都有自己的初始化方法,當(dāng)相應(yīng)的 image 被加載到內(nèi)存后席纽,就會(huì)調(diào)用初始化方法捏悬。當(dāng)然這不是調(diào)用名為initialize
方法,而是C++靜態(tài)對(duì)象初始化構(gòu)造器润梯,__attribute__((constructor))
標(biāo)記的方法以及Initializer
方法过牙。你可以在程序中設(shè)置環(huán)境變量DYLD_PRINT_INITIALIZERS
來(lái)打印dylib
的初始化方法。
我們可以看到程序首先調(diào)用了libSystem
這個(gè)dylib的初始化方法仆救。libSystem
是很多系統(tǒng)的lib的集合抒和,包括 libdispatch(GCD), libsystem_c(c語(yǔ)言庫(kù)), libsystem_blocks(block)。
在libSystem
的源碼init.c中我們可以看到彤蔽,它的初始化方法libSystem_initializer
會(huì)調(diào)用libdispatch_init();
, 然后逐步調(diào)用到_objc_init
摧莽,也就是 objc 和 runtime 的入口。
添加一個(gè)符號(hào)斷點(diǎn)_objc_init
顿痪,下面是方法調(diào)用棧:
注意:runtime 和 objc 屬于libobjc
下面是_objc_init
的實(shí)現(xiàn):
void _objc_init(void)
{
// 省略...
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
}
上面的map_images
不是將 image 添加到內(nèi)存中的意思镊辕,在這個(gè)方法被調(diào)用的時(shí)候,已經(jīng)完成了 image 的映射以及指針修正蚁袭,綁定符號(hào)的工作了征懈。
這個(gè)函數(shù)實(shí)際上是將 image 中 OBJC 相關(guān)的信息進(jìn)行初始化,具體實(shí)現(xiàn)可以查看_read_image
的源碼揩悄,因?yàn)榇a太多所以這里就不貼出來(lái)了卖哎,下面是具體做的事情:
- 會(huì)將所有的 Class 存放在一張映射類(lèi)名與 Class 的全局表中
gdb_objc_realized_classes
- 隨后調(diào)用
readClass
函數(shù)將 每一個(gè) Class 添加到gdb_objc_realized_classes
表中。 - 確定 selector 是唯一的
- read protocols: 讀取protocol
- realizeClasses:這一步的意義就是動(dòng)態(tài)鏈接好class, 讓class處于可用狀態(tài),主要操作如下:
- 檢查ro是否已經(jīng)替換為rw,沒(méi)有就替換一下亏娜。
- 檢查類(lèi)的父類(lèi)和metaClass是否已經(jīng)realize,沒(méi)有就先把它們先realize
- 重新layout ivar. 因?yàn)橹挥屑虞d好了所有父類(lèi)焕窝,才能確定ivar layout
- 把一些flags從ro拷貝到rw
- 鏈接class的 nextSiblingClass 鏈表
- attach categories: 合并categories的method list、 properties维贺、protocols到 class_rw_t 里面
- read categories:讀取類(lèi)目
在map_images
結(jié)束會(huì)調(diào)用load_images
函數(shù)它掂。這一步做的事情比較少,就是調(diào)用我們熟悉的+(load)
函數(shù)溯泣。父類(lèi)會(huì)先調(diào)用虐秋,除了 Class,每個(gè)類(lèi)目的+(load)
方法也會(huì)被調(diào)用一次垃沦,但順序就不一定了客给。
總結(jié)
在這里對(duì) main 函數(shù)之前的操作做一個(gè)小總結(jié)吧:
- 將 App 的 mach-o header 讀取到內(nèi)存中
- 根據(jù) load commands 獲取 dyld 的路徑,運(yùn)行 dyld
- 初始化運(yùn)行環(huán)境栏尚,加載 dylib起愈,如果緩存中存在則從緩存中拿出加載過(guò)的 image,否則新建一個(gè) image译仗,加載到內(nèi)存中
- 修正指針(rebase),綁定符號(hào)(bind)
- 初始化 dylib官觅,運(yùn)行 runtime
- runtime 將 image 中有關(guān) OBJC 的數(shù)據(jù)進(jìn)行初始化
- 調(diào)用 +(load) 方法
- dyld 調(diào)用 main 函數(shù)
花了一周的時(shí)間用來(lái)研究這部分的內(nèi)容纵菌,終于填完坑了~很舒服
最大的感受就是學(xué)習(xí)完后,看 clang 編譯后的 C++ 代碼能看懂的更多了休涤。比如添加完一個(gè)類(lèi)目之后咱圆,會(huì)將這個(gè)這個(gè)類(lèi)目添加到__DATA的section __objc_catlist
中,以前不知道啥意思現(xiàn)在就明白了功氨。也明白 xcode 的許多設(shè)置是用來(lái)干嘛的序苏,總之好處多多~
學(xué)習(xí)也是一個(gè)遞歸的過(guò)程,總之捷凄,也多加油吧忱详!
引用
iOS 程序 main 函數(shù)之前發(fā)生了什么
優(yōu)化 App 的啟動(dòng)時(shí)間
dyld源碼分析-動(dòng)態(tài)加載load
dyld與ObjC