Runtime-執行時

weixin_34393428發表於2017-04-17

1 物件模型圖

143264-b59014312d9d2684.jpg
物件模型.jpg
  1. NSObject 的類中定義了例項方法,例如 -(id)init 方法 和 - (void)dealloc 方法。
  2. NSObject 的元類中定義了類方法,例如 +(id)alloc 方法 和 + (void)load 、+ (void)initialize 方法。
  3. NSObject 的元類繼承自 NSObject 類,所以 NSObject 類是所有類的根,因此 NSObject 中定義的例項方法可以被所有物件呼叫,例如 - (id)init 方法 和 - (void)dealloc 方法。
  4. NSObject 的元類的 isa 指向自己。

2 例項物件在記憶體中的結構

143264-4147d48f78cae62b.jpg
例項物件記憶體圖.jpg

例項的記憶體結構是由其類決定的,已經存在的類,是無法動態加成員變數的。因為如果類加了成員變數,該類的所有例項,其記憶體結構必須做相應的增加,試想一下如果是增加了NSObject類的成員變數,那記憶體中所有的例項都得修改,成本太高。

3 訊息傳送和轉發

  1. 檢測這個 selector 是不是要忽略的。比如 Mac OS X 開發,有了垃圾回收就不理會 retain, release 這些函式了。
  2. 檢測這個 target 是不是 nil 物件。ObjC 的特性是允許對一個 nil 物件執行任何一個方法不會 Crash,因為會被忽略掉。
  3. 如果上面兩個都過了,那就開始查詢這個類的 IMP,先從 cache 裡面找,完了找得到就跳到對應的函式去執行。
  4. 如果 cache 找不到就找一下方法分發表。
  5. 如果分發表找不到就到超類的分發表去找,一直找,直到找到NSObject類為止。
  6. 如果還找不到就要開始進入動態方法解析和訊息轉發


    143264-f9790765567edaac.png
    動態方法解析和訊息轉發.png

動態方法解析

  1. 過載resolveInstanceMethod:和resolveClassMethod:方法分別新增例項方法實現和類方法實現
  2. class_addMethod函式完成向特定類新增特定方法實現
  3. 如果返回YES,就會重新啟動一次訊息傳送過程
#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];
}

轉發

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

整體流程圖

143264-33ba6aa68c028c80.jpg
訊息傳送與轉發路徑流程圖.jpg

相關問題

編譯器如何編譯成objc_msgSend

objc_msgSend是組合語言,其實在 [objc-msg-x86_64.s] 中包含了多個版本的 objc_msgSend方法,它們是根據返回值的型別和呼叫者的型別分別處理的:

  • objc_msgSendSuper:向父類發訊息,返回值型別為 id
  • objc_msgSend_fpret:返回值型別為 floating-point,其中包含 objc_msgSend_fp2ret入口處理返回值型別為 long double的情況
  • objc_msgSend_stret:返回值為結構體
  • objc_msgSendSuper_stret:向父類發訊息,返回值型別為結構體

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

訊息快取
struct objc_cache {
    uintptr_t mask;            /* total = mask + 1 */
    uintptr_t occupied;       
    cache_entry *buckets[1];
};

嗯,objc_cache的定義看起來很簡單,它包含了下面三個變數:
1)、mask:可以認為是當前能達到的最大index(從0開始的),所以快取的size(total)是mask+1
2)、occupied:被佔用的槽位,因為快取是以雜湊表的形式存在的,所以會有空槽,而occupied表示當前被佔用的數目
3)、buckets:用陣列表示的hash表,cache_entry型別,每一個cache_entry代表一個方法快取
(buckets定義在objc_cache的最後,說明這是一個可變長度的陣列)

訊息是如何快取起來的
// 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;

其實不用再看進一步的程式碼片段,僅從註釋我們就可以看到問題的答案。註釋中說明,當_class_slow_grow是非0值的時候,只有當方法快取第奇數次滿(使用的槽位超過3/4)的時候,方法快取的大小才會增長(會清空快取,否則hash值就不對了);當第偶數次滿的時候,方法快取會被清空並重新利用。 如果_class_slow_grow值為0,那麼每一次方法快取滿的時候,其大小都會增長。
所以單就問題而言,答案是沒有限制,雖然這個值被設定為1,方法快取的大小增速會慢一點,但是確實是沒有上限的。

其他問題:編譯器如何編譯成objc_msgSend,訊息Cache機制,訊息轉發機制,objc_msgSend的各個版本,objc_msgSend的實現,跳板機制

Method Swizzling

  • class_replaceMethod 替換類方法的定義,當類中沒有想替換的原方法時,該方法會呼叫class_addMethod來為該類增加一個新方法,也因為如此,class_replaceMethod在呼叫時需要傳入types引數,而method_exchangeImplementations和method_setImplementation卻不需要
  • method_exchangeImplementations 交換 2 個方法的實現,其內部實現相當於呼叫了 2 次method_setImplementation方法
  • method_setImplementation 設定 1 個方法的實現
#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

相關文章