Runtime-運行時

1 對象模型圖

對象模型.jpg
  1. NSObject 的類中定義了實例方法,例如 -(id)init 方法 和 - (void)dealloc 方法。
  2. NSObject 的元類中定義了類方法,例如 +(id)alloc 方法 和 + (void)load 、+ (void)initialize 方法。
  3. NSObject 的元類繼承自 NSObject 類,所以 NSObject 類是所有類的根,因此 NSObject 中定義的實例方法可以被所有對象調(diào)用,例如 - (id)init 方法 和 - (void)dealloc 方法。
  4. NSObject 的元類的 isa 指向自己。

2 實例對象在內(nèi)存中的結(jié)構(gòu)

實例對象內(nèi)存圖.jpg

實例的內(nèi)存結(jié)構(gòu)是由其類決定的,已經(jīng)存在的類,是無法動態(tài)加成員變量的。因為如果類加了成員變量,該類的所有實例,其內(nèi)存結(jié)構(gòu)必須做相應(yīng)的增加,試想一下如果是增加了NSObject類的成員變量,那內(nèi)存中所有的實例都得修改,成本太高。

3 消息發(fā)送和轉(zhuǎn)發(fā)

  1. 檢測這個 selector 是不是要忽略的。比如 Mac OS X 開發(fā),有了垃圾回收就不理會 retain, release 這些函數(shù)了。
  2. 檢測這個 target 是不是 nil 對象。ObjC 的特性是允許對一個 nil 對象執(zhí)行任何一個方法不會 Crash,因為會被忽略掉。
  3. 如果上面兩個都過了,那就開始查找這個類的 IMP,先從 cache 里面找,完了找得到就跳到對應(yīng)的函數(shù)去執(zhí)行。
  4. 如果 cache 找不到就找一下方法分發(fā)表。
  5. 如果分發(fā)表找不到就到超類的分發(fā)表去找,一直找,直到找到NSObject類為止。
  6. 如果還找不到就要開始進(jìn)入動態(tài)方法解析和消息轉(zhuǎn)發(fā)


    動態(tài)方法解析和消息轉(zhuǎn)發(fā).png

動態(tài)方法解析

  1. 重載resolveInstanceMethod:和resolveClassMethod:方法分別添加實例方法實現(xiàn)和類方法實現(xiàn)
  2. class_addMethod函數(shù)完成向特定類添加特定方法實現(xiàn)
  3. 如果返回YES,就會重新啟動一次消息發(fā)送過程
#import <Foundation/Foundation.h>
@interface Student : NSObject
+ (void)learnClass:(NSString *) string;
- (void)goToSchool:(NSString *) name;
@end

#import "Student.h"
#import <objc/runtime.h>

@implementation Student
+ (BOOL)resolveClassMethod:(SEL)sel {
    if (sel == @selector(learnClass:)) {
        class_addMethod(object_getClass(self), sel, class_getMethodImplementation(object_getClass(self), @selector(myClassMethod:)), "v@:");
        return YES;
    }
    return [class_getSuperclass(self) resolveClassMethod:sel];
}

+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
    if (aSEL == @selector(goToSchool:)) {
        class_addMethod([self class], aSEL, class_getMethodImplementation([self class], @selector(myInstanceMethod:)), "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:aSEL];
}

+ (void)myClassMethod:(NSString *)string {
    NSLog(@"myClassMethod = %@", string);
}

- (void)myInstanceMethod:(NSString *)string {
    NSLog(@"myInstanceMethod = %@", string);
}
@end

重定向

  • 通過重載- (id)forwardingTargetForSelector:(SEL)aSelector方法替換消息的接受者為其他對象
  • 替換類方法的接受者,需要覆寫 + (id)forwardingTargetForSelector:(SEL)aSelector 方法,并返回類對象
- (id)forwardingTargetForSelector:(SEL)aSelector
{
    if(aSelector == @selector(mysteriousMethod:)){
        return alternateObject;
    }
    return [super forwardingTargetForSelector:aSelector];
}

轉(zhuǎn)發(fā)

  1. 重寫methodSignatureForSelector:方法,否則會拋異常
  2. 重寫forwardInvocation:,forwardInvocation:方法就像一個不能識別的消息的分發(fā)中心,將這些消息轉(zhuǎn)發(fā)給不同接收對象。或者它也可以象一個運輸站將所有的消息都發(fā)送給同一個接收對象。它可以將一個消息翻譯成另外一個消息,或者簡單的”吃掉“某些消息,因此沒有響應(yīng)也沒有錯誤。
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    if ([someOtherObject respondsToSelector:
            [anInvocation selector]])
        [anInvocation invokeWithTarget:someOtherObject];
    else
        [super forwardInvocation:anInvocation];
}

整體流程圖

消息發(fā)送與轉(zhuǎn)發(fā)路徑流程圖.jpg

相關(guān)問題

編譯器如何編譯成objc_msgSend

objc_msgSend是匯編語言,其實在 [objc-msg-x86_64.s] 中包含了多個版本的 objc_msgSend方法,它們是根據(jù)返回值的類型和調(diào)用者的類型分別處理的:

  • objc_msgSendSuper:向父類發(fā)消息,返回值類型為 id
  • objc_msgSend_fpret:返回值類型為 floating-point,其中包含 objc_msgSend_fp2ret入口處理返回值類型為 long double的情況
  • objc_msgSend_stret:返回值為結(jié)構(gòu)體
  • objc_msgSendSuper_stret:向父類發(fā)消息,返回值類型為結(jié)構(gòu)體

這也是為什么 objc_msgSend要用匯編語言而不是 OC、C 或 C++ 語言來實現(xiàn),因為單獨一個方法定義滿足不了多種類型返回值,有的方法返回 id,有的返回 int
。考慮到不同類型參數(shù)返回值排列組合映射不同方法簽名(method signature)的問題,那 switch 語句得老長了。。。這些原因可以總結(jié)為 [Calling Convention],也就是說函數(shù)調(diào)用者與被調(diào)用者必須約定好參數(shù)與返回值在不同架構(gòu)處理器上的存取規(guī)則,比如參數(shù)是以何種順序存儲在棧上,或是存儲在哪些寄存器上。除此之外還有其他原因,比如其可變參數(shù)用匯編處理起來最方便,因為找到 IMP 地址后參數(shù)都在棧上。要是用 C++ 傳遞可變參數(shù)那就悲劇了,prologue 機(jī)制會弄亂地址(比如 i386 上為了存儲 ebp
向后移位 4byte),最后還要用 epilogue 打掃戰(zhàn)場。而且匯編程序執(zhí)行效率高,在 Objective-C Runtime 中調(diào)用頻率較高的函數(shù)好多都用匯編寫的。

消息緩存
struct objc_cache {
    uintptr_t mask;            /* total = mask + 1 */
    uintptr_t occupied;       
    cache_entry *buckets[1];
};

嗯,objc_cache的定義看起來很簡單,它包含了下面三個變量:
1)、mask:可以認(rèn)為是當(dāng)前能達(dá)到的最大index(從0開始的),所以緩存的size(total)是mask+1
2)、occupied:被占用的槽位,因為緩存是以散列表的形式存在的,所以會有空槽,而occupied表示當(dāng)前被占用的數(shù)目
3)、buckets:用數(shù)組表示的hash表,cache_entry類型,每一個cache_entry代表一個方法緩存
(buckets定義在objc_cache的最后,說明這是一個可變長度的數(shù)組)

消息是如何緩存起來的
// Scan for the first unused slot and insert there.
// There is guaranteed to be an empty slot because the 
// minimum size is 4 and we resized at 3/4 full.
buckets = (cache_entry **)cache->buckets;
for (index = CACHE_HASH(sel, cache->mask); 
     buckets[index] != NULL; 
     index = (index+1) & cache->mask)
{
    // empty
}
buckets[index] = entry;

//hash的方式
#define CACHE_HASH(sel, mask) (((uintptr_t)(sel)>>2) & (mask))
方法緩存存在什么地方?
struct _class_t {
  struct _class_t *isa;
  struct _class_t *superclass;
  void *cache;
  void *vtable;
  struct _class_ro_t *ro;
  };

我們看到在類的定義里就有cache字段,沒錯,類的所有緩存都存在metaclass上,所以每個類都只有一份方法緩存,而不是每一個類的object都保存一份。

類的方法緩存大小有沒有限制?
 /* When _class_slow_grow is non-zero, any given cache is actually grown
   * only on the odd-numbered times it becomes full; on the even-numbered
   * times, it is simply emptied and re-used.  When this flag is zero,
   * caches are grown every time. */
  static const int _class_slow_grow = 1;

其實不用再看進(jìn)一步的代碼片段,僅從注釋我們就可以看到問題的答案。注釋中說明,當(dāng)_class_slow_grow是非0值的時候,只有當(dāng)方法緩存第奇數(shù)次滿(使用的槽位超過3/4)的時候,方法緩存的大小才會增長(會清空緩存,否則hash值就不對了);當(dāng)?shù)谂紨?shù)次滿的時候,方法緩存會被清空并重新利用。 如果_class_slow_grow值為0,那么每一次方法緩存滿的時候,其大小都會增長。
所以單就問題而言,答案是沒有限制,雖然這個值被設(shè)置為1,方法緩存的大小增速會慢一點,但是確實是沒有上限的。

其他問題:編譯器如何編譯成objc_msgSend,消息Cache機(jī)制,消息轉(zhuǎn)發(fā)機(jī)制,objc_msgSend的各個版本,objc_msgSend的實現(xiàn),跳板機(jī)制

Method Swizzling

  • class_replaceMethod 替換類方法的定義,當(dāng)類中沒有想替換的原方法時,該方法會調(diào)用class_addMethod來為該類增加一個新方法,也因為如此,class_replaceMethod在調(diào)用時需要傳入types參數(shù),而method_exchangeImplementations和method_setImplementation卻不需要
  • method_exchangeImplementations 交換 2 個方法的實現(xiàn),其內(nèi)部實現(xiàn)相當(dāng)于調(diào)用了 2 次method_setImplementation方法
  • method_setImplementation 設(shè)置 1 個方法的實現(xiàn)
#import <objc/runtime.h> 
 
@implementation UIViewController (Tracking) 
 
+ (void)load { 
    static dispatch_once_t onceToken; 
    dispatch_once(&onceToken, ^{ 
        Class aClass = [self class]; 
 
        SEL originalSelector = @selector(viewWillAppear:); 
        SEL swizzledSelector = @selector(xxx_viewWillAppear:); 
 
        Method originalMethod = class_getInstanceMethod(aClass, originalSelector); 
        Method swizzledMethod = class_getInstanceMethod(aClass, swizzledSelector); 
        
        // When swizzling a class method, use the following:
        // Class aClass = object_getClass((id)self);
        // ...
        // Method originalMethod = class_getClassMethod(aClass, originalSelector);
        // Method swizzledMethod = class_getClassMethod(aClass, swizzledSelector);
 
        BOOL didAddMethod = 
            class_addMethod(aClass, 
                originalSelector, 
                method_getImplementation(swizzledMethod), 
                method_getTypeEncoding(swizzledMethod)); 
 
        if (didAddMethod) { 
            class_replaceMethod(aClass, 
                swizzledSelector, 
                method_getImplementation(originalMethod), 
                method_getTypeEncoding(originalMethod)); 
        } else { 
            method_exchangeImplementations(originalMethod, swizzledMethod); 
        } 
    }); 
} 
 
#pragma mark - Method Swizzling 
 
- (void)xxx_viewWillAppear:(BOOL)animated { 
    [self xxx_viewWillAppear:animated]; 
    NSLog(@"viewWillAppear: %@", self); 
} 
 
@end

參考文章

http://yulingtianxia.com/blog/2014/11/05/objective-c-runtime/
http://blog.devtang.com/2013/10/15/objective-c-object-model/
http://tech.meituan.com/DiveIntoMethodCache.html

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容