Runtime - 方法傳送機制土味講解
面試驅動技術合集(初中級iOS開發),關注倉庫,及時獲取更新 Interview-series
Class 結構詳解
struct objc_class : objc_object { Class isa; Class superclass; cache_t cache;--> 方法快取 class_data_bits_t bits; }
struct cache_t { struct bucket_t *_buckets;//散列表 mask_t _mask;//散列表長度-1 mask_t _occupied;//已經快取的方法數量 }
struct bucket_t { cache_key_t _key;//@selecter(xxx) 作為key MethodCacheIMP _imp;//函式的執行地址 }
buckets 散列表,是一個數組,數組裡面的每一個元素就是一個bucket_t,bucket_t裡面存放兩個
-
_key SEL作為key
-
_imp 函式的記憶體地址
_mask 散列表的長度
_occupied已經快取的方法數量
函式呼叫底層走的是objc_msgSend
正常的流程:
-
物件通過isa,找到函式所在的類物件
-
這時候先做快取查詢,如果快取的函式列表中沒找到該方法
-
就去類的class_rw中的methods中找,如果找到了,呼叫並快取該方法
-
如果類的class_rw中沒找到該方法,通過superclass到父類中,走的邏輯還是先查快取,快取沒有查類裡面的方法。
-
最終如果在父類中呼叫到了,會將方法快取到當前類的方法快取列表中
方法快取
如何進行快取查詢->使用散列表(散列表 - 空間換時間)
MNGirl *girl = [[MNGirl alloc]init]; mj_objc_class *girlClass = (__bridge mj_objc_class *)[MNGirl class]; [girl beauty]; [girl rich]; //遍歷快取(散列表長度 = mask + 1) cache_t cache = girlClass->cache; bucket_t *buckets = cache._buckets; for (int i = 0; i < cache._mask + 1; i++) { bucket_t bucket = buckets[i]; NSLog(@"%s %p", bucket,bucket._imp); } ---------------------------------------- 2019-03-13 22:11:42.911494+0800 rich 0x100000be0 2019-03-13 22:11:42.912946+0800 beauty 0x100000c10 2019-03-13 22:11:42.912970+0800 (null) 0x0 2019-03-13 22:11:42.913002+0800 init 0x7fff4f98ff4d
發現快取中已經有三個方法了,分別是初始化呼叫的init,第一次呼叫的beauty和第二次呼叫的rich
散列表取方法
[girl beauty]; [girl rich]; //遍歷快取(散列表長度 = mask + 1) cache_t cache = girlClass->cache; bucket_t *buckets = cache._buckets; bucket_t bucket = buckets[(long long)@selector(beauty) & cache._mask]; NSLog(@"%s %p", bucket,bucket._imp); ----------------------------------------- 2019-03-13 22:15:00 beauty 0x100000c60
確實是取方法的時候,不用遍歷,通過@selector( ) & mask = index索引,陣列同index就
注意,不一定每次都能準確的index索引,算出來的index取出來的內容不一定是想要的,但是經常是比較接近,最差的情況下,也只是一邊的迴圈遍歷
索引散列表效率遠高於陣列!
方法查詢的原始碼: bucket_t * cache_t::find(cache_key_t k, id receiver)
bucket_t * cache_t::find(cache_key_t k, id receiver) { assert(k != 0); bucket_t *b = buckets(); mask_t m = mask(); mask_t begin = cache_hash(k, m); mask_t i = begin; do { if (b[i].key() == 0||b[i].key() == k) { return &b[i]; } } while ((i = cache_next(i, m)) != begin); // hack Class cls = (Class)((uintptr_t)this - offsetof(objc_class, cache)); cache_t::bad_cache(receiver, (SEL)k, cls); }
索引值 Index 的計算
static inline mask_t cache_hash(cache_key_t key, mask_t mask) { return (mask_t)(key & mask); } mask_t begin = cache_hash(k, m);
走的是 key & mask的方法, A & B 一定是小於 A的
1111 0010 &0011 1111 ---------- 0011 0010 <= 原來的值
雜湊表的演算法也有用求餘的,和&類似
實現如下:
(i = cache_next(i, m)) != begin
查詢流程梳理: 比如起始下標是4, 總長度是6,目標不在列表中
-
取出index = 4的值,發現不是想要的,i - - 變成3
-
3 依次 - - 到0,然後mask長度開始 = 6繼續
-
當6 又 - - 到起始index = 4的時候,說明已經遍歷一圈了,還是沒找到,方法快取查詢結束
OC的訊息機制
三個階段
-
訊息傳送
-
動態方法解析
-
訊息轉發
訊息傳送
當前類查詢順序
-
排序好的列表,採用二分查詢演算法查詢對應的執行函式
-
未排序的列表,採用一般遍歷的方法查詢物件執行函式
父類逐級查詢
動態方法解析
@interface IOSer : NSObject - (void)interview; @end @implementation IOSer - (void)test{ NSLog(@"%s",__func__); } + (BOOL)resolveInstanceMethod:(SEL)sel{ if (sel == @selector(interview)) { Method method = class_getInstanceMethod(self, @selector(test)); //動態新增interview方法 class_addMethod(self, sel, method_getImplementation(method), method_getTypeEncoding(method)); return YES; } return [super resolveInstanceMethod:sel]; } @end ---------------------------------------------- //呼叫 IOSer *ios = [[IOSer alloc]init]; [ios interview]; --------------------------------------------- 結果,不會crash,進入了動態新增的方法了 2019-03-17 21:33:51.475717+0800 Runtime-TriedResolverDemo[11419:9277997] -[IOSer test]
訊息轉發流程
-
訊息轉發流程1:forwardingTargetForSelector
@implementation IOSer - (void)interview{ NSLog(@"%s",__func__); } @end @interface Forwarding : NSObject - (void)interview; @end @implementation Forwarding - (id)forwardingTargetForSelector:(SEL)aSelector{ if (aSelector == @selector(interview)) { //objc_msgSend([[IOSer alloc]init],aSelector) //由IOSer作為訊息轉發的接收者 return [[IOSer alloc]init]; } return [super forwardingTargetForSelector:aSelector]; } @end --------------------------------------------------------------- 呼叫 Forwarding *obj = [[Forwarding alloc]init]; [obj interview]; --------------------------------------------- 結果,不會crash,進入了動態新增的方法了 2019-03-17 22:57:45.130805+0800 Runtime-TriedResolverDemo[13776:9355195] -[IOSer interview]
-
訊息轉發流程2:forwardingTargetForSelector
@implementation Forwarding //返回方法簽名 - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{ if (aSelector == @selector(interview)) { //v16@0:8 = void xxx (self,_cmd) return [NSMethodSignature signatureWithObjCTypes:"v16@0:8"]; } return [super methodSignatureForSelector:aSelector]; } //NSInvocation - 方法呼叫 - (void)forwardInvocation:(NSInvocation *)anInvocation{ //設定方法呼叫者 [anInvocation invokeWithTarget:[[IOSer alloc]init]]; } @end
NSInvocation 其實封裝了一個方法呼叫,包括:
-
方法名 - anInvocation.selector
-
方法呼叫 - anInvocation.target
-
方法引數 - anInvocation getArgument: atIndex:
冷門知識補充
//類方法的訊息轉發 [Forwarding test];
類方法也可以實現訊息轉發,但是用的是+ (id)forwardingTargetForSelector:(SEL)aSelector函式
因為__forwarding底層,是用receiver去傳送 forwardingTargetForSelector訊息,如果是類方法,receiver是類物件,所以要呼叫的是 “+” 方法
小tips:預設是沒有+ (id)forwardingTargetForSelector:(SEL)aSelector方法,可以先打- (id)forwardingTargetForSelector:(SEL)aSelector,“-” 替換成“+”,完成~
友情演出: 小馬哥MJ
參考資料:
Objective-C-Message-Sending-and-Forwarding
作者:小蠢驢打程式碼
連結:https://www.jianshu.com/p/228c7258ce0c