iOS 底層探索系列數組
- iOS 底層探索 - alloc & init
- iOS 底層探索 - calloc 和 isa
- iOS 底層探索 - 類
- iOS 底層探索 - cache_t
- iOS 底層探索 - 方法
- iOS 底層探索 - 消息查找
- iOS 底層探索 - 消息轉發
- iOS 底層探索 - 應用加載
- iOS 底層探索 - 類的加載
- iOS 底層探索 - 分類的加載
- iOS 底層探索 - 類拓展和關聯對象
- iOS 底層探索 - KVC
- iOS 底層探索 - KVO
iOS 查漏補缺系列緩存
上一章咱們對應用的加載有了初步的認識,咱們知道了markdown
exec()
會咱們的應用映射到新的地址空間dyld
進行加載、連接、初始化主程序和主程序所依賴的各類動態庫initializeMainExecutable
方法中通過一系列初始化調用 notifySingle
函數,該函數會執行一個 load_images
的回調doModinitFuntions
函數內部會調用 __attribute__((constructor))
的 c
函數dyld
返回主程序的入口函數,開始進入主程序的 main
函數在 main
函數執行執行,其實 dyld
還會在流程中初始化 libSystem
,而 libSystem
又會去初始化 libDispatch
,在 libDispatch
初始化方法裏面又會有一步 _os_object_init
,在 _os_object_init
內部就會調起 _objc_init
。而對於 _objc_init
咱們還須要繼續探索,由於這裏面會進行類的加載等一系列重要的工做。數據結構
_objc_init
首先來到 libObjc
源碼的 _objc_init
方法處,你能夠直接添加一個符號斷點 _objc_init
或者全局搜索關鍵字來到這裏:app
void _objc_init(void) { static bool initialized = false; if (initialized) return; initialized = true; // fixme defer initialization until an objc-using image is found? environ_init(); tls_init(); static_init(); lock_init(); exception_init(); _dyld_objc_notify_register(&map_images, load_images, unmap_image); } 複製代碼
咱們接着進行分析:函數
接着來到 environ_init
方法內部:工具
咱們能夠看到,這裏主要是讀取影響 Runtime
的一些環境變量,若是須要,還能夠打印環境變量幫助提示。oop
咱們能夠在終端上測試一下,直接輸入 export OBJC_HELP=1
:post
能夠看到不一樣的環境變量對應的內容都被打印出來了。學習
接着來到 tls_init
方法內部:
void tls_init(void) { #if SUPPORT_DIRECT_THREAD_KEYS _objc_pthread_key = TLS_DIRECT_KEY; pthread_key_init_np(TLS_DIRECT_KEY, &_objc_pthread_destroyspecific); #else _objc_pthread_key = tls_create(&_objc_pthread_destroyspecific); #endif } 複製代碼
這裏執行的是關於線程 key
的綁定,好比每線程數據的析構函數。
接着來到 static_init
方法內部:
/*********************************************************************** * static_init * Run C++ static constructor functions. * libc calls _objc_init() before dyld would call our static constructors, * so we have to do it ourselves. **********************************************************************/ static void static_init() { size_t count; auto inits = getLibobjcInitializers(&_mh_dylib_header, &count); for (size_t i = 0; i < count; i++) { inits[i](); } } 複製代碼
這裏會運行 C++
的靜態構造函數,在 dyld
調用咱們的靜態構造函數以前,libc
會調用 _objc_init
,因此這裏咱們必須本身來作,而且這裏只會初始化系統內置的 C++
靜態構造函數,咱們本身代碼裏面寫的並不會在這裏初始化。
接着來到 lock_init
方法內部:
void lock_init(void) { } 複製代碼
咱們能夠看到,這是一個空的實現。也就是說 objc
的鎖是徹底採用的 C++
那一套的鎖邏輯。
接着來到 exception_init
方法內部:
/*********************************************************************** * exception_init * Initialize libobjc's exception handling system. * Called by map_images(). **********************************************************************/ void exception_init(void) { old_terminate = std::set_terminate(&_objc_terminate); } 複製代碼
這裏是初始化 libobjc
的異常處理系統,咱們程序觸發的異常都會來到:
咱們能夠看到 _objc_terminate
是未處理異常的回調函數,其內部邏輯以下:
OC
拋出的異常OC
拋出的異常,調用 uncaught_handeler
回調函數指針OC
拋出的異常,則繼續 C++
終止操做接下來使咱們今天探索的重點了: _dyld_objc_notify_register
,咱們先看下它的定義:
注意:僅供
objc
運行時使用 當objc
鏡像被映射(mapped)、**卸載(unmapped)和初始化(initialized)**的時候,註冊的回調函數就會被調用。 這個方法是dlyd
中聲明的,一旦調用該方法,調用結果會做爲該函數的參數回傳回來。好比,當全部的images
以及section
爲objc-image-info
被加載以後會回調mapped
方法。load
方法也將在這個方法中被調用。
_dyld_objc_notify_register
方法的三個參數 map_images
、 load_images
、 unmap_image
其實都是函數指針:
這三個函數指針是在 dyld
中回調的,咱們打開 dyld
的源碼便可一探究竟,咱們直接搜索 _dyld_objc_notify_register
:
接着來到 dyld
的 registerObjCNotifiers
方法內部:
經過上面兩張截圖的內容說明在 registerObjCNotifiers
內部, libObjc
傳過來的這三個函數指針被 dyld
保存在了本地靜態變量中。換句話來講,最終函數指針是否能被調用,取決於這三個靜態變量:
sNotifyObjCMapped
sNotifyObjCInit
sNotifyObjCUnmapped
咱們注意到 registerObjCNotifiers
的 try-catch
語句中的 try
分支註釋以下:
call 'mapped' function with all images mapped so far 調用
mapped
函數來映射全部的鏡像
那麼也就是說 notifyBatchPartial
裏面會進行真正的函數指針的調用,咱們進入這個方法內部:
咱們能夠看到,在 notifyBatchPartial
方法內部,這裏的註釋:
tell objc about new images 告訴
objc
鏡像已經映射完成了
而圖中箭頭所指的地方正是 sNotifyObjCMapped
函數指針真正調用的地方。
弄清楚了三個函數指針是怎麼調用的還不夠,接下來咱們要深刻各個函數的內部看裏面究竟作了什麼樣的事情。
首先是 map_images
,咱們來到它的實現:
/*********************************************************************** * map_images * Process the given images which are being mapped in by dyld. * Calls ABI-agnostic code after taking ABI-specific locks. * * Locking: write-locks runtimeLock **********************************************************************/ void map_images(unsigned count, const char * const paths[], const struct mach_header * const mhdrs[]) { mutex_locker_t lock(runtimeLock); return map_images_nolock(count, paths, mhdrs); }C 複製代碼
Process the given images which are being mapped in by dyld. Calls ABI-agnostic code after taking ABI-specific locks.
處理由
dyld
映射的給定鏡像 取得特定於ABI
的鎖後,調用與ABI
無關的代碼。
這裏會繼續往下走到 map_images_nolock
map_images_nolock
內部代碼十分冗長,咱們通過分析以後,前面的工做基本上都是進行鏡像文件信息的提取與統計,因此能夠定位到最後的 _read_images
:
這裏進入
_read_images
的條件是hCount
大於 0,hCount
表示的是Mach-O
中header
的數量
OK,咱們的主角登場了, _read_images
和 lookupImpOrForward
能夠說是咱們學習 Runtime
和 iOS
底層裏面很是重要的兩個概念了, lookUpImpOrForward
已經探索過了,剩下的 _read_images
咱們也不能落下。
Perform initial processing of the headers in the linked list beginning with headerList. 從
headerList
開始,對已經連接了的Mach-O
鏡像表中的頭部進行初始化處理
咱們能夠看到,整個 _read_images
有接近 400 行代碼。咱們不妨摺疊一下里面的分支代碼,而後總覽一下:
經過摺疊代碼,以及日誌打印提示信息,咱們大體能夠將 _read_images
分爲下面幾個流程:
doneOnce 流程
**
咱們從第一個分支 doneOnce
開始,這個名詞顧名思義,只會執行一次:
SUPPORT_NONPOINTER_ISA
判斷當前是否支持開啓內存優化的 isa
SUPPORT_INDEXED_ISA
判斷當前是不是將類存儲在 isa
做爲類表索引
Mach-O
的頭部,而且判斷若是是 Swift 3.0
以前的代碼,就須要禁用對 isa
的內存優化TARGET_OS_OSX
判斷是不是 macOS
執行環境macOS
的系統版本,若是小於 10.11
則說明 app
太陳舊了,須要禁用掉 non-pointer isa
Mach-O
的頭部,判斷若是有 __DATA__,__objc_rawisa
段的存在,則禁用掉 non-pointer isa
,由於不少新的 app
加載老的擴展的時候會須要這樣的判斷操做。預先優化過的類不會加入到
gdb_objc_realized_classes
這個哈希表中來,gdb_objc_realized_classes
哈希表的裝載因子爲 0.75,這是一個通過驗證的效率很高的擴容臨界值。
gdb_objc_realized_classes
表中來咱們查看這個表的定義:
// This is a misnomer: gdb_objc_realized_classes is actually a list of // named classes not in the dyld shared cache, whether realized or not.
這是一個誤稱:gdb_objc_realized_classes 表實際上存儲的是不在
dyld
共享緩存裏面的命名類,不管這些類是否實現
除了 gdb_objc_realized_classes
表以外,還有一張表 allocatedClasses
:
objc_allocateClassPair
開闢以後的類和元類存儲的表(也就是說須要 alloc
)其實 gdb_objc_realized_classes
對 allocatedClasses
是一種包含的關係,一張是類的總表,一張是已經開闢了內存的類表,
Discover classes 流程
Discover classes. Fix up unresolved future classes. Mark bundle classes. 發現類。修正未解析的
future
類,標記bundle
類。
_getObjc2ClassList
來獲取到全部的類,咱們能夠經過 MachOView
來驗證:Mach-O
的 header
部分,而後經過 mustReadClasses
來判斷哪些條件能夠跳過讀取類這一步驟header
是不是 Bundle
header
是否開啓了 預優化_getObjc2ClassList
取出的全部的類
readClass
來讀取類信息readClass
結果不爲空,則須要從新爲類開闢內存Fix up remapped classes 流程
修復 重映射類 類表和非懶加載類表沒有被重映射 (也就是 _objc_classlist) 因爲消息轉發,類引用和父類引用會被重映射 (也就是 _objc_classrefs)
**
noClassesRemapped
方法判斷是否有類引用(_objc_classrefs)須要進行重映射
EACH_HEADER
_getObjc2ClassRefs
和 _getObjc2SuperRefs
取出當前遍歷到的 Mach-O
的類引用和父類引用,而後調用 remapClassRef
進行重映射Fix up @selector references 流程
修正
SEL
引用
selLock
鎖EACH_HEADER
Mach-O
_getObjc2SelectorRefs
拿到全部的 SEL
引用SEL
引用調用 sel_registerNameNoLock
進行註冊也就是說這一流程最主要的目的就是註冊 SEL
,咱們註冊真正發生的地方: __sel_registerName
,這個函數若是你們常常玩 Runtime
確定不會陌生:
咱們簡單分析一下 __sel_registerName
方法的流程:
sel
爲空,則返回一個空的 SEL
builtins
中搜索,看是否已經註冊過,若是找到,直接返回結果namedSelectors
哈希表中查詢,找到了就返回結果namedSelectors
未初始化,則建立一下這個哈希表sel_alloc
來建立一下 SEL
,而後把新建立的 SEL
插入哈希表中進行緩存的填充Fix up old objc_msgSend_fixup call sites 流程
修正舊的
objc_msgSend_fixup
調用
**
這個流程的執行前提是 FIXUP
被開啓。
EACH_HEADER
_getObjc2MessageRefs
方法來獲取當前遍歷到的 Mach-O
鏡像的全部消息引用fixupMessageRef
進行修正Discover protocols 流程
發現協議,並修正協議引用
**
Fix up @protocol references 流程
對全部的協議作重映射
**
Realize non-lazy classes 流程
初始化非懶加載類(
**+load**
方法和靜態實例)
Realize newly-resolved future classes 流程
初始化新解析出來的
future
類
**
Discover categories 流程
處理全部的分類,包括類和元類
**
到這裏, _read_images
的流程就分析完畢,咱們能夠新建一個文件來去掉一些干擾的信息,只保留核心的邏輯,這樣從宏觀的角度來分析更直觀:
Q & A 環節 Q:
dyld
主要邏輯是加載庫,也就是鏡像文件,可是加載完是怎麼讀取的呢? A:_read_images
是真正讀取的地方Q:
SEL
方法編號什麼時候加載? A:_read_images
咱們探索了 _read_images
方法的流程,接下來讓咱們把目光放到本文的主題 - 類的加載
既然是類的加載,那麼咱們在前面所探索的類的結構中出現的內容都會一一重現。
因此咱們不妨直接進行斷點調試,讓咱們略過其它干擾信息,聚焦於類的加載。
doneOnce
流程中會建立兩個哈希表,並無涉及到類的加載,因此咱們跳過
咱們在下圖所示的位置處打上斷點:
classList
中取出的
cls
只是一個內存地址,咱們嘗試經過
LLDB
打印
cls
的
clas_rw_t
:
能夠看到 cls
的屬性、方法、協議以及類名都爲空,說明這裏類並無被真正加載完成,咱們接着聚焦到 read_class
函數上面,咱們進入其內部實現,咱們大體瀏覽以後會定位到以下圖所示的代碼:
看起來類的信息在這裏完成了加載,那麼爲了驗證咱們的猜測,直接斷點調試一下但發現斷點根本走不進來,緣由在於這裏的判斷語句
if (Class newCls = popFutureNamedClass(mangledName)) 複製代碼
判斷當前傳入的類的類名是否有 future
類的實現,可是咱們剛纔已經打印了,類名是空的,因此確定不會執行這裏。咱們接着往下走:
cls
插入到 gdb_objc_realized_classes
表cls
插入到 allocatedClasses
表分析完 read_class
,咱們回到 _read_images
方法
咱們能夠看到 read_class
返回的 newCls
會進行一個判斷,判斷與傳入 read_class
以前的 cls
是否相等,而在 read_class
內部只有一個地方對類的內容進行了改動,可是咱們剛纔測試了是進不去的,因此這個 if
裏面的內容咱們能夠略過,也就是說 resolvedFutureClasses
的內容咱們均可以暫時略過。
總結一下 readClass
:
data()
類設置 ro/rw
經過分析 read_class
,咱們能夠得知,類已經被註冊到兩個哈希表中去了,那麼如今一切時機都已經成熟了。可是咱們仍是要略過像 Fix up remapped classes
、 Fix up @selector references
、 fix up old objc_msgSend_fixup call sites
、 Discover protocols. Fix up protocol refs
、 Fix up @protocol references
,由於咱們的重點是類的加載,咱們最終來到了 Realize non-lazy classes (for +load methods and static instances)
,略去無關信息以後,咱們能夠看到咱們的
主角 realizeClassWithoutSwift
閃亮登場了:
從方法的名稱以及方法註釋咱們能夠知道, realizeClassWithoutSwift
是進行類的第一次初始化操做,包括分配讀寫數據也就是咱們常說的 rw
,可是並不會進行任何的 Swift
端初始化。咱們直接聚焦下面的代碼:
calloc
開闢內存空間,返回一個新的 rw
cls
取出來的 ro
賦值給這個 rw
rw
設置到 cls
身上那麼是否是說在這裏 rw
就有值了呢,咱們 LLDB
打印大法走起:
能夠清楚地看到,此時 rw
仍是爲空,說明這裏只是對 rw
進行了初始化,可是方法、屬性、協議這些都沒有被添加上。
咱們接着往下走:
這裏能夠看到父類和元類都會遞歸調用 realizeClassWithoutSwift
來初始化各自的 rw
。爲何在類的加載操做裏面要去加載類和元類呢?回憶一下類的結構,答案很簡單,要保證 superclass
和 isa
的完整性,也就是保證類的完整性,
上面的截圖就是最好的證實,初始化完畢的父類和元類被賦值到了類的 superclass
和 isa
上面。
接着往下走能夠看到,不光要把父類關聯到類上面,還要讓父類知道子類的存在。
最後一行代碼是 methodizeClass(cls)
,註釋顯示的是 attach categories
,附加分類到類?咱們進入其內部實現一探究竟。
在探索 methodizeClass
前,咱們先總結一下 realizeClassWithoutSwift
:
class
的 data()
ro/rw
賦值對類的方法列表、協議列表和屬性列表進行修正 附加
category
到類上面來
咱們直接往下面走:
// Install methods and properties that the class implements itself. method_list_t *list = ro->baseMethods(); if (list) { prepareMethodLists(cls, &list, 1, YES, isBundleClass(cls)); rw->methods.attachLists(&list, 1); } 複製代碼
ro
中取出方法列表附加到 rw
上property_list_t *proplist = ro->baseProperties; if (proplist) { rw->properties.attachLists(&proplist, 1); } 複製代碼
ro
中取出屬性列表附加到 rw
上protocol_list_t *protolist = ro->baseProtocols; if (protolist) { rw->protocols.attachLists(&protolist, 1); } 複製代碼
ro
中取出協議列表附加到 rw
上category_list *cats = unattachedCategoriesForClass(cls, true /*realizing*/); attachCategories(cls, cats, false /*don't flush caches*/); 複製代碼
cls
中取出未附加的分類進行附加操做咱們能夠看到,這裏有一個操做叫 attachLists
,爲何方法、屬性、協議都能調用這個方法呢?
咱們能夠看到,方法、屬性、協議的數據結構都是一個二維數組,咱們深刻 attachLists
方法內部實現:
void attachLists(List* const * addedLists, uint32_t addedCount) { if (addedCount == 0) return; if (hasArray()) { // many lists -> many lists uint32_t oldCount = array()->count;//10 uint32_t newCount = oldCount + addedCount;//4 setArray((array_t *)realloc(array(), array_t::byteSize(newCount))); array()->count = newCount;// 10+4 memmove(array()->lists + addedCount, array()->lists, oldCount * sizeof(array()->lists[0])); memcpy(array()->lists, addedLists, addedCount * sizeof(array()->lists[0])); } else if (!list && addedCount == 1) { // 0 lists -> 1 list list = addedLists[0]; } else { // 1 list -> many lists List* oldList = list; uint32_t oldCount = oldList ? 1 : 0; uint32_t newCount = oldCount + addedCount; setArray((array_t *)malloc(array_t::byteSize(newCount))); array()->count = newCount; if (oldList) array()->lists[addedCount] = oldList; memcpy(array()->lists, addedLists, addedCount * sizeof(array()->lists[0])); } } 複製代碼
attachLists
的 list_array_tt
二維數組有多個一維數組
realloc
對容器進行從新分配,大小爲原來的大小加上新增的大小memmove
把原來的數據移動到容器的末尾attachLists
的 list_array_tt
二維數組爲空且新增大小數目爲 1,則直接取 addedList
的第一個 list
返回attachLists
的 list_array_tt
二維數組只有一個一維數組
咱們接着探索 _dyld_objc_notify_register
的第二個參數 load_images
,這個函數指針是在何時調用的呢,一樣的,咱們接着在 dyld
源碼中搜索對應的函數指針 sNotifyObjCInit
:
能夠看到,在 notifySingle
方法內部, sNotifyObjCInit
函數指針被調用了。根據咱們上一篇文章探索 dyld
底層能夠知道, _load_images
應該是對於每個加載進來的 Mach-O
鏡像都會遞歸調用一次。
咱們來到 libObjc
源碼中 load_images
的定義處:
處理由
dyld
映射的給定鏡像中的+load
方法
load
方法,若是沒有,直接返回load
方法,具體實現經過 prepare_load_methods
load
方法,具體實現經過 call_load_methods
從這個方法名稱,咱們猜想這裏應該作的是 load
方法的一些預處理工做,讓咱們來到源碼進行分析:
void prepare_load_methods(const headerType *mhdr) { size_t count, i; runtimeLock.assertLocked(); classref_t *classlist = _getObjc2NonlazyClassList(mhdr, &count); for (i = 0; i < count; i++) { schedule_class_load(remapClass(classlist[i])); } category_t **categorylist = _getObjc2NonlazyCategoryList(mhdr, &count); for (i = 0; i < count; i++) { category_t *cat = categorylist[i]; Class cls = remapClass(cat->cls); if (!cls) continue; // category for ignored weak-linked class if (cls->isSwiftStable()) { _objc_fatal("Swift class extensions and categories on Swift " "classes are not allowed to have +load methods"); } realizeClassWithoutSwift(cls); assert(cls->ISA()->isRealized()); add_category_to_loadable_list(cat); } } /*********************************************************************** * prepare_load_methods * Schedule +load for classes in this image, any un-+load-ed * superclasses in other images, and any categories in this image. **********************************************************************/ // Recursively schedule +load for cls and any un-+load-ed superclasses. // cls must already be connected. static void schedule_class_load(Class cls) { if (!cls) return; assert(cls->isRealized()); // _read_images should realize if (cls->data()->flags & RW_LOADED) return; // Ensure superclass-first ordering schedule_class_load(cls->superclass); add_class_to_loadable_list(cls); cls->setInfo(RW_LOADED); } /*********************************************************************** * add_class_to_loadable_list * Class cls has just become connected. Schedule it for +load if * it implements a +load method. **********************************************************************/ void add_class_to_loadable_list(Class cls) { IMP method; loadMethodLock.assertLocked(); method = cls->getLoadMethod(); if (!method) return; // Don't bother if cls has no +load method if (PrintLoading) { _objc_inform("LOAD: class '%s' scheduled for +load", cls->nameForLogging()); } if (loadable_classes_used == loadable_classes_allocated) { loadable_classes_allocated = loadable_classes_allocated*2 + 16; loadable_classes = (struct loadable_class *) realloc(loadable_classes, loadable_classes_allocated * sizeof(struct loadable_class)); } loadable_classes[loadable_classes_used].cls = cls; loadable_classes[loadable_classes_used].method = method; loadable_classes_used++; } 複製代碼
_getObjc2NonlazyClassList
獲取全部已經加載進去的類列表schedule_class_load
遍歷這些類
load
方法,確保父類的 load
方法順序排在子類的前面add_class_to_loadable_list
, 把類的 load
方法存在 loadable_classes
裏面schedule_class_load
以後,經過 _getObjc2NonlazyCategoryList
取出全部分類數據realizeClassWithoutSwift
來防止類沒有初始化,若是已經初始化了則不影響add_category_to_loadable_list
,加載分類中的 load
方法到 loadable_categories
裏面
經過名稱咱們能夠知道 call_load_methods
應該就是 load
方法被調用的地方了。咱們直接看源碼:
void call_load_methods(void) { static bool loading = NO; bool more_categories; loadMethodLock.assertLocked(); // Re-entrant calls do nothing; the outermost call will finish the job. if (loading) return; loading = YES; void *pool = objc_autoreleasePoolPush(); do { // 1. Repeatedly call class +loads until there aren't any more while (loadable_classes_used > 0) { call_class_loads(); } // 2. Call category +loads ONCE more_categories = call_category_loads(); // 3. Run more +loads if there are classes OR more untried categories } while (loadable_classes_used > 0 || more_categories); objc_autoreleasePoolPop(pool); loading = NO; } 複製代碼
call_load_methods 調用類和類別中全部未決的
+load
方法 類裏面+load
方法是父類優先調用的 而在父類的+load
以後纔會調用分類的+load
方法
objc_autoreleasePoolPush
壓棧一個自動釋放池do-while
循環開始
+load
方法直到找不到爲止+load
方法objc_autoreleasePoolPop
出棧一個自動釋放池至此, _objc_init
和 _dyld_objc_notify_register
咱們就分析完了,咱們對類的加載有了更細緻的認知。 iOS
底層有時候探索起來確實很枯燥,可是若是能找到高效的方法以及明確本身的所探索的方向,會讓本身從宏觀上從新審視這門技術。是的,技術只是工具,咱們不能被技術所綁架,咱們要作到有的放矢的去探索,這樣才能事半功倍。