iOS runtime——看這一篇就夠了
theme: channing-cyan
本文篇幅比較長,創作的目的為了自己日後溫習知識所用,希望這篇文章能對你有所幫助。 如發現任何有誤之處,肯請留言糾正,謝謝。
一、深入程式碼理解 instance、class object、metaclass
1、instance物件例項
我們經常使用id來宣告一個物件,那id的本質又是什麼呢?檢視objc/objc.h檔案
``` /// An opaque type that represents an Objective-C class. typedef struct objc_class *Class;
/// Represents an instance of a class. struct objc_object { Class _Nonnull isa OBJC_ISA_AVAILABILITY; };
/// A pointer to an instance of a class. typedef struct objc_object *id;
```
我們建立的一個物件或例項其實就是一個struct objc_object結構體,而我們常用的id也就是這個結構體的指標。
這個結構體只有一個成員變數,這是一個Class型別的變數isa,也是一個結構體指標,isa指標就指向物件所屬的類。
一個 NSObject 物件佔用多少記憶體空間? 一個NSObject例項物件只有一個isa指標,所以一個isa指標的大小,他在64位的環境下佔8個位元組,在32位環境上佔4個位元組。
``` NSObject *obj = [[NSObject alloc] init]; NSLog(@"class_getInstanceSize--%zd", class_getInstanceSize([NSObject class]));
```
輸出結果:
``` class_getInstanceSize--8
```
2、class object(類物件)/metaclass(元類)
看結構體objc_class的定義
``` struct objc_class { Class isa OBJC_ISA_AVAILABILITY;
if !OBJC2
Class super_class OBJC2_UNAVAILABLE;
const char *name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_cache *cache OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
endif
} OBJC2_UNAVAILABLE;
/ Use Class
instead of struct objc_class *
/
```
- Class superclass;——用於獲取父類,也就是元類物件,它也是一個Class型別
- cache_t cache;——是方法快取
- class_data_bits_t bits;——用於獲取類的具體資訊,看到bits
- class_rw_t data()函式,該函式的作用就是獲取該類的可讀寫資訊,通過class_data_bits_t的bits.data()方法獲得,class_rw_t後面會介紹*
``` class_rw_t data() { return (class_rw_t )(bits & FAST_DATA_MASK); }
```
該結構體的第一個成員變數也是isa指標,這就說明了Class本身其實也是一個物件,我們稱之為類物件。類物件中的元資料儲存的都是如何建立一個例項的相關資訊,那麼類物件和類方法應該從哪裡建立呢?就是從isa指標指向的結構體建立,類物件的isa指標指向的我們稱之為元類(metaclass),元類中儲存了建立類物件以及類方法所需的所有資訊。
3、isa指標與superclass相關邏輯圖
4、總結 + 程式碼校驗
- 物件 的類(Superclass)是 類(物件) ;
- 類(物件) 的類(Superclass)是 元類,和類同名;
- 元類 的類(Superclass)是 根元類 NSObject;
- 根元類 的類(Superclass)是 自己 ,還是NSObject;
- 物件的isa指標指向類(物件) ;
- 類物件的isa指標指向元類,和類同名;
- 元類的isa指標指向跟根元類 NSObject;
- 根元類 NSObject的isa指標指向自己。
isa驗證
``` NSString *string = @"字串"; Class class1 = object_getClass(string);//NSString類物件 Class metaClass = object_getClass(class1);//NSString元類 Class rootMetaClass = object_getClass(metaClass);//根元類 Class rootRootMetaClass = object_getClass(rootMetaClass);//根元類 NSLog(@"%p 例項物件 ",string); NSLog(@"%p 類 %@",class1,NSStringFromClass(class1)); NSLog(@"%p 元類 %@",metaClass,NSStringFromClass(metaClass)); NSLog(@"%p 根元類 %@",rootMetaClass,NSStringFromClass(rootMetaClass)); NSLog(@"%p 根根元類 %@",rootRootMetaClass,NSStringFromClass(rootRootMetaClass));
Class rootMetaClass_superclass = rootMetaClass.superclass;//根元類的superclass
NSLog(@"根根元類的superclass:%@",NSStringFromClass(rootMetaClass_superclass));
```
輸出結果:
``` 0x102d48078 例項物件 0x1d80e3d10 類 __NSCFConstantString 0x1d80e3cc0 元類 __NSCFConstantString 0x1d80c66c0 根元類 NSObject 0x1d80c66c0 根根元類 NSObject 根根元類的superclass:NSObject
```
superclass驗證
``` NSString *string = @"字串"; Class class1 = object_getClass(string);//NSString類物件 Class class2 = class1.superclass; NSLog(@"%@ 的superclass是 %@",NSStringFromClass(class1),NSStringFromClass(class2)); Class class3 = class2.superclass; NSLog(@"%@ 的superclass是 %@",NSStringFromClass(class2),NSStringFromClass(class3)); Class class4 = class3.superclass; NSLog(@"%@ 的superclass是 %@",NSStringFromClass(class3),NSStringFromClass(class4)); Class class5 = class4.superclass; NSLog(@"%@ 的superclass是 %@",NSStringFromClass(class4),NSStringFromClass(class5)); Class class6 = class5.superclass; NSLog(@"%@ 的superclass是 %@",NSStringFromClass(class5),NSStringFromClass(class6));
```
輸出結果:
``` __NSCFConstantString 的superclass是 __NSCFString __NSCFString 的superclass是 NSMutableString NSMutableString 的superclass是 NSString NSString 的superclass是 NSObject NSObject 的superclass是 (null)
```
二、class_rw_t 與 class_ro_t
1、class_ro_t 一"碼"當先:
``` struct class_ro_t { uint32_t flags; uint32_t instanceStart; uint32_t instanceSize;
ifdef LP64
uint32_t reserved;
endif
const uint8_t * ivarLayout;
const char * name;
method_list_t * baseMethodList;
protocol_list_t * baseProtocols;
const ivar_list_t * ivars;
const uint8_t * weakIvarLayout;
property_list_t *baseProperties;
method_list_t *baseMethods() const {
return baseMethodList;
}
};
```
- uint32_t instanceSize;——instance物件佔用的記憶體空間
- const char * name;——類名
- const ivar_list_t * ivars;——類的成員變數列表
class_ro_t儲存了當前類在編譯期就已經確定的屬性、方法以及遵循的協議,裡面是沒有分類的方法的。那些執行時新增的方法將會儲存在執行時生成的class_rw_t中。 ro即表示read only,是無法進行修改的。
2、class_rw_t 一"碼"當先:
``` // 可讀可寫 struct class_rw_t { // Be warned that Symbolication knows the layout of this structure. uint32_t flags; uint32_t version;
const class_ro_t *ro; // 指向只讀的結構體,存放類初始資訊
/*
這三個都是二位陣列,是可讀可寫的,包含了類的初始內容、分類的內容。
這三個二位陣列中的資料有一部分是從class_ro_t中合併過來的。
*/
method_array_t methods; // 方法列表(類物件存放物件方法,元類物件存放類方法)
property_array_t properties; // 屬性列表
protocol_array_t protocols; //協議列表
Class firstSubclass;
Class nextSiblingClass;
//...
}
```
3、class_rw_t生成時機
class_rw_t生成在執行時,在編譯期間,class_ro_t結構體就已經確定,objc_class中的bits的data部分存放著該結構體的地址。在runtime執行之後,具體說來是在執行runtime的realizeClass 方法時,會生成class_rw_t結構體,該結構體包含了class_ro_t,並且更新data部分,換成class_rw_t結構體的地址。
類的realizeClass執行之前:
然後在載入 ObjC 執行時的過程中在 realizeClass 方法中:
- 從 class_data_bits_t 呼叫 data 方法,將結果從 class_rw_t 強制轉換為 class_ro_t 指標
- 初始化一個 class_rw_t 結構體
- 設定結構體 ro 的值以及 flag
- 最後設定正確的 data。
``` const class_ro_t ro = (const class_ro_t )cls->data(); class_rw_t rw = (class_rw_t )calloc(sizeof(class_rw_t), 1); rw->ro = ro; rw->flags = RW_REALIZED|RW_REALIZING; cls->setData(rw);
```
但是,在這段程式碼執行之後 class_rw_t 中的方法,屬性以及協議列表均為空。這時需要 realizeClass 呼叫 methodizeClass 方法來將類自己實現的方法(包括分類)、屬性和遵循的協議載入到 methods、 properties 和 protocols 列表中。
realizeClass 方法執行過後的類所佔用記憶體的佈局:
細看兩個結構體的成員變數會發現很多相同的地方,他們都存放著當前類的屬性、例項變數、方法、協議等等。區別在於:class_ro_t存放的是編譯期間就確定的;而class_rw_t是在runtime時才確定,它會先將class_ro_t的內容拷貝過去,然後再將當前類的分類的這些屬性、方法等拷貝到其中。所以可以說class_rw_t是class_ro_t的超集,當然實際訪問類的方法、屬性等也都是訪問的class_rw_t中的內容。
4、method_t
上面我們剖析了class_rw_t、class_ro_t這兩個重要部分的結構,並且主要關注了其中的方法列表部分,而從上面的分析,可發現裡面最基本也是重要的單位是method_t,這個結構體包含了描述一個方法所需要的各種資訊。
``` struct method_t { SEL name; const char *types; IMP imp; };
```
變數介紹可以參考之前文章:iOS 程式碼注入—— hook 實踐
三、Runtime 初始化函式
1、一"碼"當先
``` /********** * _objc_init * Bootstrap initialization. Registers our image notifier with dyld. * Called by libSystem BEFORE library initialization time ***********/
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);
}
```
_dyld_objc_notify_register(&map_images, load_images, unmap_image)。這個函式裡面的三個引數分別是另外三個函式:
- map_images -- Process the given images which are being mapped in by dyld.(處理那些正在被dyld對映的映象檔案)
- load_images -- Process +load in the given images which are being mapped in by dyld.(處理那些正在被dyld對映的映象檔案中的+load方法)
- unmap_image -- Process the given image which is about to be unmapped by dyld.(處理那些將要被dyld進行去對映操作的映象檔案)
我們檢視一下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); }
```
四、分類底層原理
根據map_images函式,繼續點進去看,可以看到如下程式碼:
``` // Discover categories. for (EACH_HEADER) { category_t **catlist = _getObjc2CategoryList(hi, &count); bool hasClassProperties = hi->info()->hasCategoryClassProperties();
for (i = 0; i < count; i++) {
category_t *cat = catlist[i];
Class cls = remapClass(cat->cls);
if (!cls) {
// Category's target class is missing (probably weak-linked).
// Disavow any knowledge of this category.
catlist[i] = nil;
if (PrintConnecting) {
_objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with "
"missing weak-linked target class",
cat->name, cat);
}
continue;
}
// Process this category.
// First, register the category with its target class.
// Then, rebuild the class's method lists (etc) if
// the class is realized.
bool classExists = NO;
if (cat->instanceMethods || cat->protocols
|| cat->instanceProperties)
{
addUnattachedCategoryForClass(cat, cls, hi);
if (cls->isRealized()) {
remethodizeClass(cls);
classExists = YES;
}
if (PrintConnecting) {
_objc_inform("CLASS: found category -%s(%s) %s",
cls->nameForLogging(), cat->name,
classExists ? "on existing class" : "");
}
}
if (cat->classMethods || cat->protocols
|| (hasClassProperties && cat->_classProperties))
{
addUnattachedCategoryForClass(cat, cls->ISA(), hi);
if (cls->ISA()->isRealized()) {
remethodizeClass(cls->ISA());
}
if (PrintConnecting) {
_objc_inform("CLASS: found category +%s(%s)",
cls->nameForLogging(), cat->name);
}
}
}
}
```
根據程式碼:
``` category_t *cat = catlist[i];
```
一開始的那個catlist是一個二維陣列,裡面的成員也是一個一個的陣列,也就是程式碼裡面的cat所指向的陣列,它的型別是category_t *,說明cat數組裡面裝的就是category_t,一個cat裡面裝的就是某個class所對應的所有category。
那麼什麼決定了這些category_t在cat陣列中的順序呢? 答案是category檔案的編譯順序決定的。先參與編譯的,就放在陣列的前面,後參與編譯的,就放在陣列後面。我們可以在xcode-->target-->Build Phases-->Compile Sources列表檢視和調整category檔案的編譯順序
載入分類的最後,執行方法:remethodizeClass(cls->ISA());
``` static void remethodizeClass(Class cls) { category_list *cats; bool isMeta;
runtimeLock.assertLocked();
isMeta = cls->isMetaClass();
// Re-methodizing: check for more categories
if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) {
if (PrintConnecting) {
_objc_inform("CLASS: attaching categories to class '%s' %s",
cls->nameForLogging(), isMeta ? "(meta)" : "");
}
attachCategories(cls, cats, true /*flush caches*/);
free(cats);
}
}
```
然後在這裡面找到一個方法attachCategories,看名字就知道,附著分類,也就是把分類的內容新增/合併到class裡面,感興趣的可以自己檢視一下這個方法,這個理就不做解釋了。
五、方法快取
1、資料結構
它的底層是通過散列表(雜湊表)的資料結構來實現的,用於快取曾經呼叫過的方法,可以提高方法的查詢速度。 首先,回顧一下正常情況下方法呼叫的流程。假設我們呼叫一個例項方法[obj XXXX];
- obj -> isa -> obj的Class物件 -> method_array_t methods -> 對該表進行遍歷查詢,找到就呼叫,沒找到繼續往下走
- obj -> superclass -> obj的父類 -> isa -> method_array_t methods -> 對父類的方法列表進行遍歷查詢,找到就呼叫,沒找到就重複本步驟
- 直到NSObject -> isa -> NSObject的Class物件 -> method_array_t,如果還是沒有找到就會crash
如果XXXX方法在程式內會被頻繁的呼叫,那麼這種逐層便利查詢的方式肯定是效率低下的,因此蘋果設計了cache_t cache,當XXXX第一次被呼叫的時候,會按照常規流程查詢,找到之後,就會被加入到cache_t cache中,當再次被呼叫的時候,系統就會直接現到cache_t cache來查詢,找到就直接呼叫,這樣便大大提升了查詢的效率。
``` struct cache_t { struct bucket_t *_buckets; mask_t _mask; mask_t _occupied; }
```
- struct bucket_t *_buckets; —— 用來快取方法的雜湊/雜湊表
- mask_t _mask; —— 這個值 = 散列表長度 - 1
- mask_t _occupied; —— 表示已經快取的方法的數量
_buckets散列表裡面的儲存單元是bucket_t,
``` struct bucket_t { private: cache_key_t _key; IMP _imp; }
```
- cache_key_t _key; —— 這個key實際上就是方法的SEL,也就是方法名
- IMP _imp; —— 這個就是方法對應的函式的記憶體地址
2、快取邏輯
- (1) 當一個物件接收到訊息時[obj message];,首先根據obj的isa指標進入它的類物件class裡面。
- (2) 在obj的class裡面,首先到快取cache_t裡面查詢方法message的函式實現,如果找到,就直接呼叫該函式。
- (3) 如果上一步沒有找到對應函式,在對該class的方法列表進行二分/遍歷查詢,如果找到了對應函式,首先會將該方法快取到obj的類物件class的cache_t裡面,然後對函式進行呼叫。
- (4) 在每次進行快取操作之前,首先需要檢查快取容量,如果快取內的方法數量超過規定的臨界值(設定容量的3/4),需要先對快取進行2倍擴容,原先快取過的方法全部丟棄,然後將當前方法存入擴容後的新快取內。
- (5) 如果在obj的class物件裡面,發現快取和方法列表都找不到mssage方法,則通過class的superclass指標進入它的父類物件father_class裡面
- (6) 進入father_class後,首先在它的cache_t裡面查詢mssage,如果找到了該方法,那麼會首先將方法快取到訊息接受者obj的類物件class的cache_t裡面,然後呼叫方法對應的函式。
- (7) 如果上一步沒有找到方法,將會對father_class的方法列表進行遍歷二分/遍歷查詢,如果找到了mssage方法,那麼同樣,會首先將方法快取到訊息接受者obj的類物件class的cache_t裡面,然後呼叫方法對應的函式。需要注意的是,這裡並不會將方法快取到當前父類物件father_class的cache_t裡面。
- (8) 如果還沒找到方法,則會通過father_class的superclass進入更上層的父類物件裡面,按照(6)->(7)->(8)步驟流程重複。如果此時已經到了基類物件NSObject,仍沒有找到mssage,則進入步驟(9)
六、訊息轉發
第一步:Method resolution 方法解析處理階段 如果呼叫了物件方法首先會進行+(BOOL)resolveInstanceMethod:(SEL)sel判斷 如果呼叫了類方法 首先會進行 +(BOOL)resolveClassMethod:(SEL)sel判斷 兩個方法都為類方法;
``` + (BOOL)resolveClassMethod:(SEL)sel { ///這裡動態新增方法 return YES; } + (BOOL)resolveInstanceMethod:(SEL)sel { ///這裡動態新增方法 return YES; }
```
_class_resolveInstanceMethod原始碼解析
``` /********** * _class_resolveInstanceMethod * Call +resolveInstanceMethod, looking for a method to be added to class cls. * cls may be a metaclass or a non-meta class. * Does not check if the method already exists. ***********/ static void _class_resolveInstanceMethod(id inst, SEL sel, Class cls) { SEL resolve_sel = @selector(resolveInstanceMethod:);
if (! lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA())) {
// Resolver not implemented.
return;
}
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(cls, resolve_sel, sel);
// Cache the result (good or bad) so the resolver doesn't fire next time.
// +resolveInstanceMethod adds to self a.k.a. cls
IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);
if (resolved && PrintResolving) {
if (imp) {
_objc_inform("RESOLVE: method %c[%s %s] "
"dynamically resolved to %p",
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel), imp);
}
else {
// Method resolver didn't add anything?
_objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES"
", but no new implementation of %c[%s %s] was found",
cls->nameForLogging(), sel_getName(sel),
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel));
}
}
}
```
從runtime的原始碼,resolveInstanceMethod的返回值對於訊息轉發流程沒有任何意義,這個返回值只和debug的資訊相關。 這兩個方法是最先走到的方法,可以在這兩個方法中動態的新增方法,進行訊息轉發。這裡有一個需要特別注意的地方,類方法需要新增到元類裡面,原因這裡就不贅述了。
第二步:Fast forwarding 快速轉發階段
``` - (id)forwardingTargetForSelector:(SEL)aSelector { return [xxx new]; }
```
這個裡可以快速重定向成其他物件,已經讓備用的物件去響應了該物件本身無法響應的一個SEL
第三步:Normal forwarding 常規轉發階段
``` //返回方法簽名 -(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{ if ([NSStringFromSelector(aSelector) isEqualToString:@"xxx"]) { return [[xxx new] methodSignatureForSelector:aSelector]; } return [super methodSignatureForSelector:aSelector]; }
//處理返回的方法簽名 -(void)forwardInvocation:(NSInvocation *)anInvocation{ if ([NSStringFromSelector(anInvocation.selector) isEqualToString:@"xxx"]) { [anInvocation invokeWithTarget:[xxx new]]; }else{ [super forwardInvocation:anInvocation]; } }
```
自動簽名
``` -(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{ //如果返回為nil則進行自動簽名 if ([super methodSignatureForSelector:aSelector]==nil) { NSMethodSignature * sign = [NSMethodSignature signatureWithObjCTypes:"[email protected]:"]; return sign; } return [super methodSignatureForSelector:aSelector]; }
-(void)forwardInvocation:(NSInvocation *)anInvocation{ //建立備用物件 xxx * backUp = [xxx new]; SEL sel = anInvocation.selector; //判斷備用物件是否可以響應傳遞進來等待響應的SEL if ([backUp respondsToSelector:sel]) { [anInvocation invokeWithTarget:backUp]; }else{ // 如果備用物件不能響應 則丟擲異常 [self doesNotRecognizeSelector:sel]; } }
////觸發崩潰 - (void)doesNotRecognizeSelector:(SEL)aSelector {
}
```
七、super的本質
1、定義
- super—— 是一個指向結構體指標struct objc_super *,它裡面的內容是{訊息接受者 recv, 訊息接受者的父類類物件 [[recv superclass] class]},objc_msgSendSuper會將訊息接受者的父類類物件作為訊息查詢的起點。
2、流程 [obj message] -> 在obj的類物件cls查詢方法 -> 在cls的父類物件[cls superclass]查詢方法 -> 在更上層的父類物件查詢方法 -> ... -> 在根類類物件 NSObject裡查詢方法
[super message] -> ~~在obj的類物件cls查詢方法~~(跳過此步驟) -> (直接從這一步開始)在cls的父類物件[cls superclass]查詢方法 -> 在更上層的父類物件查詢方法 -> ... -> 在根類類物件 NSObject裡查詢方法
3、例項
``` NSLog(@"[self class] = %@",[self class]);
```
- 接受者 當前class例項物件
- 最終呼叫的方法:基類NSObject的-(Class)class方法
``` NSLog(@"[super class] = %@",[super class]);
```
- 接受者 當前class例項物件
- 最終呼叫的方法:基類NSObject的-(Class)class方法
``` NSLog(@"[self superclass] = %@",[self superclass]);
```
- 接受者 當前class例項物件
- 最終呼叫的方法:基類NSObject的-(Class) superclass方法
``` NSLog(@"[super superclass] = %@",[super superclass]);
```
- 接受者 當前class例項物件
- 最終呼叫的方法:基類NSObject的-(Class) superclass方法
因此 [self class] [super class] [self superclass] [super superclass] 的值都相等
至此,runtime相關的知識點全部總結完畢,該文章將會持續更新迭代!! 看到就是緣分😁,如發現任何有誤之處,肯請留言糾正,謝謝。
- iOS runtime——看這一篇就夠了
- 適用於 iOS 的 CreateML 教程:使用樣式轉移建立自定義影象過濾器
- Flutter 與 Swift - 在建立 iOS 應用程式時應該押注什麼技術?
- iOS開發·背景模式教程:入門
- 適用於 iOS 的主螢幕快速操作
- iOS 的 AsyncSequence 和 AsyncStream 教程
- iOS開發 Charles 代理教程
- iOS開發中的佈局入門教程
- Swift 與 Objective-C:您應該為下一個 iOS 移動應用選擇哪個語言?
- 適用於 iOS 的 HEIC 影象壓縮
- 5 個使用 Swift 高階函式簡化的複雜演算法
- iOS開發之視覺框架中的人員與背景分割
- iOS視覺框架教程:輪廓檢測
- Google Maps iOS SDK 教程:入門
- iOS 中的模型-檢視-控制器 (MVC)
- iOS視覺教程:身體和手部姿勢檢測
- 適用於 iOS 的 AR 人臉追蹤入門教程
- iOS開發—建立一個 iOS 圖書開啟動畫:第 1 部分
- iOS開發—建立一個 iOS 圖書開啟動畫:第 2 部分
- iOS 動畫教程:自定義檢視控制器演示轉換