Runtime - 方法發送機制土味講解

面試驅動技術合集(初中級iOS開發),關注倉庫,及時獲取更新 Interview-series

image

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已經緩存的方法數量
image
  • 函數調用底層走的是objc_msgSend
image-20190313222359416

正常的流程:

  1. 對象通過isa,找到函數所在的類對象
  2. 這時候先做緩存查找,如果緩存的函數列表中沒找到該方法
  3. 就去類的class_rw中的methods中找,如果找到了,調用并緩存該方法
  4. 如果類的class_rw中沒找到該方法,通過superclass到父類中,走的邏輯還是先查緩存,緩存沒有查類里面的方法。
  5. 最終如果在父類中調用到了,會將方法緩存到當前類的方法緩存列表中

方法緩存

如何進行緩存查找->使用散列表(散列表 - 空間換時間)

image-20190317205913318
image-20190313220800705
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取出來的內容不一定是想要的,但是經常是比較接近,最差的情況下,也只是一邊的循環遍歷

索引散列表效率遠高于數組!

image-20190313223112407

方法查找的源碼: 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 <= 原來的值

哈希表的算法也有用求余的,和&類似

實現如下:

image-20190313223858753
(i = cache_next(i, m)) != begin

查找流程梳理: 比如起始下標是4, 總長度是6,目標不在列表中

  1. 取出index = 4的值,發現不是想要的,i - - 變成3
  2. 3 依次 - - 到0,然后mask長度開始 = 6繼續
  3. 當6 又 - - 到起始index = 4的時候,說明已經遍歷一圈了,還是沒找到,方法緩存查找結束

OC的消息機制

三個階段

  • 消息發送

  • 動態方法解析

  • 消息轉發

消息發送

當前類查找順序

  • 排序好的列表,采用二分查找算法查找對應的執行函數
  • 未排序的列表,采用一般遍歷的方法查找對象執行函數

父類逐級查找

image
image

動態方法解析

@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]
image-20190317214712857

消息轉發流程

  • 消息轉發流程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:
image


冷門知識補充

//類方法的消息轉發
[Forwarding test];

類方法也可以實現消息轉發,但是用的是+ (id)forwardingTargetForSelector:(SEL)aSelector函數

因為__forwarding底層,是用receiver去發送 forwardingTargetForSelector消息,如果是類方法,receiver是類對象,所以要調用的是 “+” 方法

小tips:默認是沒有+ (id)forwardingTargetForSelector:(SEL)aSelector方法,可以先打- (id)forwardingTargetForSelector:(SEL)aSelector,“-” 替換成“+”,完成~



友情演出:小馬哥MJ

參考資料:

objc-msgsend

gun

libmalloc

objc4

Objective-C-Message-Sending-and-Forwarding

Type Encodings

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。