詳解iOS中的Runtime

前言

導讀

本文較長,分爲如下幾個部分html

  • isa
  • class結構
  • Type Encoding
  • cache_t
  • 方法調用
  • 消息機制的三個階段
  • 消息發送
  • 動態解析
  • 消息轉發
  • 源碼分析

什麼是runtimec++

蘋果官方說法git

The Objective-C language defers as many decisions as it can from compile time and link time to runtime. (儘可能將決定放到運行的時候,而不是在編譯和連接過程)github

版本和平臺

runtime是有個兩個版本的: legacy 、 modern 在Objective-C 1.0使用的是legacy,在2.0使用的是modern。這裏簡單介紹下區別:數組

  • 在legacy runtime,若是你改變了實例變量的設計,須要從新編譯它的子類。支持 32bit的OS X 程序
  • 在modern runtime,若是你改變了實例變量的設計,不須要從新編譯它的子類。支持iphone程序和OS X10.5以後的64bit程序

如今通常來講runtime都是指modern緩存

isa詳解

共用體

要想學習Runtime,首先要了解它底層的一些經常使用數據結構,好比isa指針bash

在arm64架構以前,isa就是一個普通的指針,存儲着Class、Meta-Class對象的內存地址數據結構

從arm64架構開始,對isa進行了優化,變成了一個共用體(union)結構,還使用位域來存儲更多的信息。架構

查看runtime源碼能夠看到關於isa結構。官方的源碼是不能編譯的。我本身編譯了一份能夠運行的源碼在github上。app

union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };
};
複製代碼

在runtime723版本之前,直接把結構體放在isa裏面了。750版本以後,抽成宏了,展開宏ISA_BITFIELD__arm64__架構下 以下所示

下面的代碼對isa_t中的結構體進行了位域聲明,地址從nonpointer起到extra_rc結束,從低到高進行排列。位域也是對結構體內存佈局進行了一個聲明,經過下面的結構體成員變量能夠直接操做某個地址。位域總共佔8字節,全部的位域加在一塊兒正好是64位。

小提示:unionbits能夠操做整個內存區,而位域只能操做對應的位。

eg: 一個對象的地址是0x7faf1b580450 轉換成二進制11111111010111100011011010110000000010001010000,而後根據不一樣位置,去匹配不一樣的含義

define ISA_BITFIELD                                                      \
      uintptr_t nonpointer        : 1;   //指針是否優化過                                   \
      uintptr_t has_assoc         : 1;   //是否有設置過關聯對象,若是沒有,釋放時會更快                                   \
      uintptr_t has_cxx_dtor      : 1; 	 //是否有C++的析構函數(.cxx_destruct),若是沒有,釋放時會更快                                     \
      uintptr_t shiftcls          : 33; //存儲着Class、Meta-Class對象的內存地址信息 \
      uintptr_t magic             : 6;  //用於在調試時分辨對象是否未完成初始化                                     \
      uintptr_t weakly_referenced : 1;  //是否有被弱引用指向過,若是沒有,釋放時會更快                                     \
      uintptr_t deallocating      : 1;  //對象是否正在釋放                                     \
      uintptr_t has_sidetable_rc  : 1;  //引用計數器是否過大沒法存儲在isa中                                     \
      uintptr_t extra_rc          : 19 //裏面存儲的值是引用計數器減1
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
複製代碼

isa中不一樣的位域表明不一樣的含義。

  • nonpointer

    • 0,表明普通的指針,存儲着Class、Meta-Class對象的內存地址
    • 1,表明優化過,使用位域存儲更多的信息
  • has_assoc

    • 是否有設置過關聯對象,若是沒有,釋放時會更快
  • has_cxx_dtor

    • 是否有C++的析構函數(.cxx_destruct),若是沒有,釋放時會更快
  • shiftcls

    • 存儲着Class、Meta-Class對象的內存地址信息
  • magic

    • 用於在調試時分辨對象是否未完成初始化
  • weakly_referenced

    • 是否有被弱引用指向過,若是沒有,釋放時會更快
  • deallocating

    • 對象是否正在釋放
  • extra_rc

    • 裏面存儲的值是引用計數器減1
  • has_sidetable_rc

    • 引用計數器是否過大沒法存儲在isa中
    • 若是爲1,那麼引用計數會存儲在一個叫SideTable的類的屬性中

eg: 查看objc_runtime-new.mm文件中有以下代碼。

void *objc_destructInstance(id obj) 
{
    if (obj) {
        //是否有C++的析構函數
        bool cxx = obj->hasCxxDtor();
        //是否有設置過關聯對象
        bool assoc = obj->hasAssociatedObjects();
        //有C++的析構函數,就去銷燬
        if (cxx) object_cxxDestruct(obj);
         //有設置過關聯對象,就去移除管理對象
        if (assoc) _object_remove_assocations(obj);
        
        obj->clearDeallocating();
    }

    return obj;
}
複製代碼

能夠看出,釋放時候,會先判斷是否有設置過關聯對象,若是沒有,釋放時會更快。 是否有C++的析構函數(.cxx_destruct),若是沒有,釋放時會更快。其餘的弱引用,nonpointer等,讀者可自行看源碼。

關Tagged Pointer技術,深刻研究的話,能夠參考唐巧博客深刻理解Tagged Pointer

class結構

用一幅圖來表示

objc_class

查看源碼(只保留了主要代碼)

struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;    //方法緩存
    class_data_bits_t bits;    // 用於獲取具體的類的信息
}
複製代碼

也就是說結構體objc_class裏面

struct objc_class : objc_object {
    Class isa;
    Class superclass;
    cache_t cache;    //方法緩存
    class_data_bits_t bits;    // 用於獲取具體的類的信息}
複製代碼

class_rw_t

根據bits能夠獲得class_rw_tclass_rw_t裏面的methods、properties、protocols是二維數組,是可讀可寫的,包含了類的初始內容、分類的內容

eg:方法列表methods中存放着不少一維數組method_list_t,而每個method_list_t中存放這method_t,method_t中是對應方法的imp指針,名字。類型等方法信息,在詳解iOS中分類Cateogry一文中,咱們知道,每一個分類編譯完成以後都會生成一個_category_t,對應着method_list_t

#define FAST_DATA_MASK 0x00007ffffffffff8UL

class_rw_t *data() { 
   return bits.data();
}

class_rw_t* data() {
   return (class_rw_t *)(bits & FAST_DATA_MASK);
}

複製代碼

由代碼可知 bits & FAST_DATA_MASK可得到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;

    method_array_t methods; //方法列表
    property_array_t properties; //屬性列表
    protocol_array_t protocols; // 協議列表

    Class firstSubclass;
    Class nextSiblingClass;

    char *demangledName;
}
複製代碼

結構體method_array_t

class method_array_t : 
    public list_array_tt<method_t, method_list_t> 
{
    typedef list_array_tt<method_t, method_list_t> Super;

 public:
    method_list_t **beginCategoryMethodLists() {
        return beginLists();
    }
    
    method_list_t **endCategoryMethodLists(Class cls);

    method_array_t duplicate() {
        return Super::duplicate<method_array_t>();
    }
};

複製代碼

method_t

method_t是對方法、函數的封裝

  • IMP
    • IMP表明函數的具體實現
// IMP表明函數的具體實現
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...); 
複製代碼
  • SEL

    • SEL表明方法、函數名,通常叫作選擇器,底層結構跟char *相似
    • 能夠經過@selector()sel_registerName()得到
    • 能夠經過sel_getName()NSStringFromSelector()轉成字符串
    • 不一樣類中相同名字的方法,所對應的方法選擇器是相同的
typedef struct objc_selector *SEL;
複製代碼
  • types包含了函數返回值、參數編碼的字符串

    • 返回值 參數1 參數2 ...... 參數n
    • eg: v16@0:8表明,返回值void類型,第一個參數是id類型,第二個參數是SEL類型。後面會詳細說明。
struct method_t {
    SEL name; //函數名
    const char *types; //編碼(返回值類型,參數類型)
    MethodListIMP imp; //指向函數的指針(函數地址)

    struct SortBySELAddress :
        public std::binary_function<const method_t&,
                                    const method_t&, bool>
    {
        bool operator() (const method_t& lhs,
                         const method_t& rhs)
        { return lhs.name < rhs.name; }
    };
};
複製代碼
  • 建立兩個不一樣的類,並定義兩個相同的方法,經過@selector()獲取SEL並打印。能夠發現SEL都是同一個對象,地址都是相同的。由此證實,不一樣類的相同SEL是同一個對象。
@interface TestObject : NSObject
- (void)testMethod;
@end

@interface TestObject2 : NSObject
- (void)testMethod;
@end


// TestObject實現文件
@implementation TestObject
- (void)testMethod {
    NSLog(@"TestObject testMethod %p", @selector(testMethod));
}
@end


// TestObject2實現文件也同樣
@implementation TestObject
- (void)testMethod {
    NSLog(@"TestObject testMethod %p", @selector(testMethod));
}

// 結果:
TestObject testMethod 0x100000f81
TestObject2 testMethod 0x100000f81
複製代碼

class_ro_t

class_ro_t裏面的baseMethodList、baseProtocols、ivars、baseProperties是一維數組,是隻讀的,包含了類的初始內容

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;//instance對象佔用的內存空間
#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;
    }
};
複製代碼

Type Encoding

前面說了,v16@0:8表明,返回值void類型,第一個參數是id類型,第二個參數是SEL類型。這裏詳細說明

iOS中提供了一個叫作@encode的指令,能夠將具體的類型表示成字符串編碼,連接爲 Type Encodings

eg: 咱們有以下函數

void objc_msgSend(id receiver, SEL selector)
{
    if (receiver == nil) return;
    
    // 查找緩存
    ...
}
複製代碼

就能夠用v16@0:8表示

  • 其中返回值void類型,第一個參數是id類型,第二個參數是SEL類型。
  • 另外第一個數字16表明總共16個字節,0表明第一個參數從第0個字節開始,8表明第二個參數從第8個字節開始。
  • 其實也能夠簡寫爲v@:,這在後面講到消息轉發的時候會用到。

再如:

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    Method method = class_getClassMethod([self class], @selector(test));
    const char *str = method_getTypeEncoding(method);
    
    Method method2 = class_getClassMethod([self class], @selector(testWithNum:));
    const char *str2 = method_getTypeEncoding(method2);
    
    NSLog(@"test的類型 = %s ",str);
    NSLog(@"testWithNum: = %s ",str2);
}

// v16@0:8 
+(void)test{
    
}

// i20@0:8i16
+(int)testWithNum:(int)num{
    return num;
}
複製代碼

輸出結果爲:

RuntimeDemo[28247:303205] test的類型 = v16@0:8 RuntimeDemo[28247:303205] testWithNum: = i20@0:8i16

對於方法testWithNum來講

  • i表示返回值是int類型,20是參數總共20字節
  • @表示第一個參數是id類型,0表示第一個參數從第0個字節開始
  • :表示第二個參數是SEL類型。8表示第二個參數從第8個字節開始。
  • i表示第三個參數是int類型,16表示第三個參數從第16個字節開始
  • 第三個參數從第16個字節開始,是Int類型,佔用4字節。總共20字節

@encode

方法緩存cache_t

前面講了Class內部結構,其中有個方法緩存cache_t,用散列表(哈希表)來緩存曾經調用過的方法,能夠提升方法的查找速度

struct cache_t {
    struct bucket_t *_buckets; //散列表
    mask_t _mask; //散列表的長度 -1
    mask_t _occupied; //已經緩存的方法數量
}
複製代碼

散列表數組_buckets中存放着bucket_t,bucket_t的結構以下

struct bucket_t {
     MethodCacheIMP _imp; //函數的內存地址
    cache_key_t _key;   //SEL做爲Key
}
複製代碼

散列表cache_t查找原理

cache_t中如何查找方法,其實對於其餘散列表也是通用的。

在文件objc-cache.mm中找到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);
}


複製代碼

其中,根據key和散列表長度減1 mask 計算出下標 key & mask,取出的值若是key和當初傳進來的Key相同,就說明找到了。不然,就不是本身要找的方法,就有了hash衝突,把i的值加1,繼續計算。以下代碼

// 計算下標
static inline mask_t cache_hash(cache_key_t key, mask_t mask) 
{
    return (mask_t)(key & mask);
}


//hash衝突的時候
static inline mask_t cache_next(mask_t i, mask_t mask) {
    return (i+1) & mask;
}
複製代碼

cache_t的擴容

當方法緩存太多的時候,超過了容量的3/4s時候,就須要擴容了。擴容是,把原來的容量增長爲2倍

static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)
{
			...
    if (cache->isConstantEmptyCache()) {
        // Cache is read-only. Replace it.
        cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);
    }
    else if (newOccupied <= capacity / 4 * 3) {
        // Cache is less than 3/4 full. Use it as-is.
    }
    else {
        // 來到這裏說明,超過了3/4,須要擴容
        cache->expand();
    }

  		 ...
}

複製代碼

具體擴容代碼爲

enum {
    INIT_CACHE_SIZE_LOG2 = 2,
    INIT_CACHE_SIZE      = (1 << INIT_CACHE_SIZE_LOG2)
};

// cache_t的擴容
void cache_t::expand()
{
    cacheUpdateLock.assertLocked();
    
    uint32_t oldCapacity = capacity();
    // 擴容爲原來的2倍
    uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE;

    if ((uint32_t)(mask_t)newCapacity != newCapacity) {
        // mask overflow - can't grow further // fixme this wastes one bit of mask newCapacity = oldCapacity; } reallocate(oldCapacity, newCapacity); } 複製代碼

方法調用

咱們常常寫的OC方法調用,到底是怎麼個調用流程,怎麼找到方法的呢?

咱們以下代碼

Person *per = [[Person alloc]init];
[per test];
複製代碼

執行指令

clang -rewrite-objc main.m -o main.cpp

生成cpp文件,對應上面的代碼爲

((void (*)(id, SEL))(void *)objc_msgSend)((id)per, sel_registerName("test"));
複製代碼

簡化爲

objc_msgSend)(per, sel_registerName("test"));
複製代碼

其中,per稱爲消息接收者(receiver), test稱爲消息名稱,也就是說,OC中方法的調用其實都是轉換爲objc_msgSend函數的調用

消息機制

三大階段

OC中的方法調用,其實都是轉換爲objc_msgSend函數的調用

objc_msgSend的執行流程能夠分爲3大階段

  • 消息發送

  • 動態方法解析

  • 消息轉發

運行時期,調用方法流程爲

實例對象中存放 isa 指針以及實例變量,有 isa 指針能夠找到實例對象所屬的類對象 (類也是對象,面向對象中一切都是對象),類中存放着實例方法列表,在這個方法列表中 SEL 做爲 key,IMP 做爲 value。 在編譯時期,根據方法名字會生成一個惟一標識,這個標識就是 SEL。IMP 其實就是函數指針 指向了最終的函數實現。整個 Runtime 的核心就是 objc_msgSend 函數,經過給類發送 SEL 以傳遞消息,找到匹配的 IMP 再獲取最終的實現

類中的 super_class 指針能夠追溯整個繼承鏈。向一個對象發送消息時,Runtime 會根據實例對象的 isa 指針找到其所屬的類,並自底向上直至根類(NSObject)中 去尋找 SEL 所對應的方法,找到後就運行整個方法。

用一張經典的圖來表示就是

類中的 super_class 指針能夠追溯整個繼承鏈。向一個對象發送消息時,Runtime 會根據實例對象的 isa 指針找到其所屬的類,並自底向上直至根類(NSObject)中 去尋找 SEL 所對應的方法,找到後就運行整個方法。

metaClass是元類,也有 isa 指針、super_class 指針。其中保存了類方法列表。

跟讀源碼順序

objc-msg-arm64.s裏面都是彙編

objc-msg-arm64.s
ENTRY _objc_msgSend
b.le	LNilOrTagged
CacheLookup NORMAL
.macro CacheLookup
.macro CheckMiss
STATIC_ENTRY __objc_msgSend_uncached
.macro MethodTableLookup
__class_lookupMethodAndLoadCache3
複製代碼

objc-runtime-new.mm

objc-runtime-new.mm
_class_lookupMethodAndLoadCache3
lookUpImpOrForward
getMethodNoSuper_nolock、search_method_list、log_and_fill_cache
cache_getImp、log_and_fill_cache、getMethodNoSuper_nolock、log_and_fill_cache
_class_resolveInstanceMethod
_objc_msgForward_impcache
複製代碼

一直跟到 __forwarding__的時候,已經不開源的了。

objc-msg-arm64.s
STATIC_ENTRY __objc_msgForward_impcache
ENTRY __objc_msgForward

Core Foundation
__forwarding__(不開源
複製代碼

_objc_msgSend

先來看 objc-msg-arm64.s

主要代碼爲

//1.進入objcmsgSend
	ENTRY _objc_msgSend
	UNWIND _objc_msgSend, NoFrame
    // x0 recevier
    // 消息接收者  消息名稱
	cmp	p0, #0 // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
    //2.isa 優化
	b.le	LNilOrTagged		//  (MSB tagged pointer looks negative)
#else
	b.eq	LReturnZero
#endif
	ldr	p13, [x0]		// p13 = isa
	GetClassFromIsa_p16 p13		// p16 = class
LGetIsaDone: // 3.isa優化完成
	CacheLookup NORMAL		//4.執行 CacheLookup NORMAL // calls imp or objc_msgSend_uncached

#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
	b.eq	LReturnZero		// nil check

...省略不少代碼

.macro MethodTableLookup
	
	// push frame
	SignLR
	stp	fp, lr, [sp, #-16]!
	mov	fp, sp

	...省略不少代碼
	
	// receiver and selector already in x0 and x1
	mov	x2, x16
	bl	__class_lookupMethodAndLoadCache3 //方法爲_class_lookupMethodAndLoadCache3調用的彙編語言

	...省略不少代碼
.endmacro

	STATIC_ENTRY __objc_msgSend_uncached
	UNWIND __objc_msgSend_uncached, FrameWithNoSaves

	// THIS IS NOT A CALLABLE C FUNCTION
	// Out-of-band p16 is the class to search
	
	MethodTableLookup //查找IMP
	TailCallFunctionPointer x17

	END_ENTRY __objc_msgSend_uncached


	STATIC_ENTRY __objc_msgLookup_uncached
	UNWIND __objc_msgLookup_uncached, FrameWithNoSaves

	// THIS IS NOT A CALLABLE C FUNCTION
	// Out-of-band p16 is the class to search
	
	MethodTableLookup
	ret

	END_ENTRY __objc_msgLookup_uncached


	STATIC_ENTRY _cache_getImp

	GetClassFromIsa_p16 p0
	CacheLookup GETIMP
	...省略不少代碼
	
複製代碼

從上面的代碼能夠看出方法查找 IMP 的工做交給了 OC 中的 _class_lookupMethodAndLoadCache3 函數,並將 IMP 返回(從 r11 挪到 rax)。最後在 objc_msgSend 中調用 IMP。

彙編代碼比較晦澀難懂,所以這裏將函數的實現反彙編成C語言的僞代碼:

/*
objc_msgSend的C語言版本僞代碼實現.
receiver: 是調用方法的對象
op: 是要調用的方法名稱字符串
*/
id  objc_msgSend(id receiver, SEL op, ...)
{

    //1............................ 對象空值判斷。
    //若是傳入的對象是nil則直接返回nil
    if (receiver == nil)
        return nil;
    
   //2............................ 獲取或者構造對象的isa數據。
    void *isa = NULL;
    //若是對象的地址最高位爲0則代表是普通的OC對象,不然就是Tagged Pointer類型的對象
    if ((receiver & 0x8000000000000000) == 0) {
        struct objc_object  *ocobj = (struct objc_object*) receiver;
        isa = ocobj->isa;
    }
    else { //Tagged Pointer類型的對象中沒有直接保存isa數據,因此須要特殊處理來查找對應的isa數據。
        
        //若是對象地址的最高4位爲0xF, 那麼表示是一個用戶自定義擴展的Tagged Pointer類型對象
        if (((NSUInteger) receiver) >= 0xf000000000000000) {
            
            //自定義擴展的Tagged Pointer類型對象中的52-59位保存的是一個全局擴展Tagged Pointer類數組的索引值。
            int  classidx = (receiver & 0xFF0000000000000) >> 52
            isa =  objc_debug_taggedpointer_ext_classes[classidx];
        }
        else {
            
            //系統自帶的Tagged Pointer類型對象中的60-63位保存的是一個全局Tagged Pointer類數組的索引值。
            int classidx = ((NSUInteger) receiver) >> 60;
            isa  =  objc_debug_taggedpointer_classes[classidx];
        }
    }
    
   //由於內存地址對齊的緣由和虛擬內存空間的約束緣由,
   //以及isa定義的緣由須要將isa與上0xffffffff8才能獲得對象所屬的Class對象。
    struct objc_class  *cls = (struct objc_class *)(isa & 0xffffffff8);
    
   //3............................ 遍歷緩存哈希桶並查找緩存中的方法實現。
    IMP  imp = NULL;
    //cmd與cache中的mask進行與計算獲得哈希桶中的索引,來查找方法是否已經放入緩存cache哈希桶中。
    int index =  cls->cache.mask & op;
    while (true) {
        
        //若是緩存哈希桶中命中了對應的方法實現,則保存到imp中並退出循環。
        if (cls->cache.buckets[index].key == op) {
              imp = cls->cache.buckets[index].imp;
              break;
        }
        
        //方法實現並無被緩存,而且對應的桶的數據是空的就退出循環
        if (cls->cache.buckets[index].key == NULL) {
             break;
        }
        
        //若是哈希桶中對應的項已經被佔用可是又不是要執行的方法,則經過開地址法來繼續尋找緩存該方法的桶。
        if (index == 0) {
            index = cls->cache.mask;  //從尾部尋找
        }
        else {
            index--;   //索引減1繼續尋找。
        }
    } /*end while*/

   //4............................ 執行方法實現或方法未命中緩存處理函數
    if (imp != NULL)
         return imp(receiver, op,  ...); //這裏的... 是指傳遞給objc_msgSend的OC方法中的參數。
    else
         return objc_msgSend_uncached(receiver, op, cls, ...);
}

/*
  方法未命中緩存處理函數:objc_msgSend_uncached的C語言版本僞代碼實現,這個函數也是用匯編語言編寫。
*/
id objc_msgSend_uncached(id receiver, SEL op, struct objc_class *cls)
{
   //這個函數很簡單就是直接調用了_class_lookupMethodAndLoadCache3 來查找方法並緩存到struct objc_class中的cache中,最後再返回IMP類型。
  IMP  imp =   _class_lookupMethodAndLoadCache3(receiver, op, cls);
  return imp(receiver, op, ....);
}

複製代碼

消息發送階段

前面跟到_class_lookupMethodAndLoadCache3以後,後面就不是彙編了,是C語言的實現

  • runtime的消息發送階段,首先判斷receiver是否爲空,若是爲空就直接返回
  • 若是不爲空,從receiverClass的緩存中,查找方法,若是找到了,就調用方法
  • 若是沒找到,就從receiverClass的class_rw_t中查找方法(分爲二分查找和線性查找,兩種),若是找到了,就結束查找,緩存一份到本身緩存中,調用方法
  • 若是沒找到,就去父類的緩存中查找,若是找到了,就就結束查找,緩存一份到本身緩存中,調用方法
  • 若是沒找到,就從父類的class_rw_t中查找方法,若是找到了,就結束查找,緩存一份到本身緩存中,調用方法
  • 若是沒找到,就看是否還有父類,若是有,就繼續查父類的緩存,方法列表
  • 若是沒有父類,說明消息發送階段結束,那麼就進入第二階段,動態方法解析階段。

IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
    
    return lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
複製代碼

關鍵代碼在lookUpImpOrForward裏面,下面的代碼,增長了註釋

IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
    IMP imp = nil;
    bool triedResolver = NO; //是否動態解析過的標記
    runtimeLock.assertUnlocked();
    if (cache) {
        imp = cache_getImp(cls, sel);
        if (imp) return imp;
    }
    runtimeLock.lock();
    checkIsKnownClass(cls);

    if (!cls->isRealized()) {
        realizeClass(cls);
    }

    if (initialize  &&  !cls->isInitialized()) {
        runtimeLock.unlock();
        _class_initialize (_class_getNonMetaClass(cls, inst));
        runtimeLock.lock();
    }

    
 retry:    
    runtimeLock.assertLocked();

    // 這裏先查緩存,雖然前面彙編裏面已經查過了。可是有可能動態添加,致使緩存有更新
    imp = cache_getImp(cls, sel);
    //若是查到了,就直接跳轉到最後
    if (imp) goto done;
    //來到這裏,說明緩存沒有
    // Try this class's method lists. { // 查找方法列表 Method meth = getMethodNoSuper_nolock(cls, sel); if (meth) { log_and_fill_cache(cls, meth->imp, sel, inst, cls); imp = meth->imp; //若是查到了,就直接跳轉到最後 goto done; } } // Try superclass caches and method lists. { //查找父類的緩存和方法列表 unsigned attempts = unreasonableClassCount(); // for循環層層向上找 for (Class curClass = cls->superclass; curClass != nil; curClass = curClass->superclass) { // Halt if there is a cycle in the superclass chain. if (--attempts == 0) { _objc_fatal("Memory corruption in class list."); } // Superclass cache. // 父類緩存 imp = cache_getImp(curClass, sel); if (imp) { if (imp != (IMP)_objc_msgForward_impcache) { // 若是父類緩存有,也要緩存一份到本身的緩存中 log_and_fill_cache(cls, imp, sel, inst, curClass); //跳轉到最後 goto done; } else { break; } } // Superclass method list. 查找父類方法列表 Method meth = getMethodNoSuper_nolock(curClass, sel); if (meth) { /// 若是父類方法列表有,也要緩存一份到本身的緩存中 log_and_fill_cache(cls, meth->imp, sel, inst, curClass); imp = meth->imp; //若是查到了,就跳轉到最後 goto done; } } } // 來到這裏,進入第二階段,動態方法解析階段,並且要求沒有動態解析過 if (resolver && !triedResolver) { runtimeLock.unlock(); _class_resolveMethod(cls, sel, inst); runtimeLock.lock(); // Don't cache the result; we don't hold the lock so it may have // changed already. Re-do the search from scratch instead. triedResolver = YES; //動態解析過,標記設爲YES // 回到查找緩存的地方開始查找,緩存中沒有加過,此次去查找,能夠再方法列表中查到 goto retry; } // No implementation found, and method resolver didn't help. 
    // Use forwarding.
    
    // 來到這裏,說明進入第三階段,消息轉發階段
    imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp, inst);

 done:
    runtimeLock.unlock();
    // 返回方法地址
    return imp;
}       
複製代碼

動態方法解析

前面的消息發送階段,沒有找到,就來到動態方法解析階段

頭文件中定義兩個方法

- (void)test;
- (void)run;
複製代碼

只實現test

-(void)test{
    
    NSLog(@"%s",__func__);

}
複製代碼

調用的是時候

Person *per = [[Person alloc]init];
[per run];
複製代碼

由前面的消息發送階段知道,去查緩存,查方法列表,查父類等等,這些操做以後,都沒有找到這個方法的實現,若是後面不作處理,必然拋出異常

報錯方法找不到

Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Person run]: unrecognized selector sent to instance 0x100f436c0'

若是要處理的話,消息發送階段處理不了。那麼就來到第二階段,動態解析階段。這個階段的處理,從前面的源碼可知

// 動態方法解析
void _class_resolveMethod(Class cls, SEL sel, id inst)
{
    if (! cls->isMetaClass()) { //若是不是元類對象
        // try [cls resolveInstanceMethod:sel]
        _class_resolveInstanceMethod(cls, sel, inst);
    } 
    else { // 是元類對象
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        _class_resolveClassMethod(cls, sel, inst);
        if (!lookUpImpOrNil(cls, sel, inst, 
                            NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
        {
            _class_resolveInstanceMethod(cls, sel, inst);
        }
    }
}
複製代碼

系統默認的resolveClassMethodresolveInstanceMethod默認返回NO

+ (BOOL)resolveClassMethod:(SEL)sel {
    return NO;
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    return NO;
}
複製代碼

咱們能夠在動態解析階段,重寫resolveInstanceMethod並添加方法的實現

+ (BOOL)resolveInstanceMethod:(SEL)sel
{
     if (sel == @selector(run)) {
             // 獲取其餘方法 實例方法,或者類方法均可以
            Method method = class_getInstanceMethod(self, @selector(test));

            // 動態添加test方法的實現
            class_addMethod(self, sel,
                            method_getImplementation(method),
                            method_getTypeEncoding(method));
			//等價於下面的
			// class_addMethod(self, sel,
                         method_getImplementation(method),
                         "v@:");
				
            // 返回YES表明有動態添加方法  其實這裏返回NO,也是能夠的,返回YES只是增長了一些打印
            return NO;
        }
        return [super resolveInstanceMethod:sel];
}

複製代碼

上面的代碼中,由於-(void)test無參無返回值,函數類型爲v@:,因此,上面的method_getTypeEncoding(method)能夠換成"v@:"也是沒問題的。

這樣的話,就至關於,調用run的時候,實際上調用的是test。由源碼可知,動態解析完以後,回到查找緩存的地方開始查找,緩存中沒有加過,此次去查找,能夠再方法列表中查到。這樣就能夠正確執行了。輸出結果爲

objc-test[6681:75992] -[Person test]

直接運行源碼,以下圖

消息轉發

若是前面消息發送和動態解析階段,對方法都沒有處理,咱們還有最後一個階段,消息轉發階段來處理。從源碼的imp = (IMP)_objc_msgForward_impcache;能夠看出,_objc_msgForward_impcache的代碼是在彙編裏面

STATIC_ENTRY __objc_msgForward_impcache

// No stret specialization.
b	__objc_msgForward

END_ENTRY __objc_msgForward_impcache

	
ENTRY __objc_msgForward

adrp	x17, __objc_forward_handler@PAGE
// 這裏進去以後,不開源了
ldr	p17, [x17, __objc_forward_handler@PAGEOFF]
TailCallFunctionPointer x17
... 還有不少代碼

 
複製代碼

跟到 ___forwarding___以後就不開源了

objc-test[15568:163497] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Person run]: unrecognized selector sent to instance 0x100f039f0'
*** First throw call stack:
(
	0   CoreFoundation                      0x00007fff307de063 __exceptionPreprocess + 250
	1   libobjc.A.dylib                     0x000000010038ec9f objc_exception_throw + 47
	2   CoreFoundation                      0x00007fff308671bd -[NSObject(NSObject) __retain_OA] + 0
	3   CoreFoundation                      0x00007fff307844b4 ___forwarding___ + 1427
	4   CoreFoundation                      0x00007fff30783e98 _CF_forwarding_prep_0 + 120
	5   objc-test                           0x0000000100000e11 main + 97
	6   libdyld.dylib                       0x00007fff672e93f9 start + 1
	7   ???                                 0x0000000000000001 0x0 + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException 
複製代碼

在上述調用棧中,發現了在 Core Foundation 中會調用___forwarding___。根據資料也能夠了解到,在 objc_setForwardHandler 時會傳入 __CF_forwarding_prep_0___forwarding_prep_1___ 兩個參數,而這兩個指針都會調用____forwarding___。這個函數中,也交代了消息轉發的邏輯

接下來怎麼辦呢?能夠經過彙編調試,或逆向來進一步分析後續的實現。

站在前人的代碼上,能看的更遠 ---魯迅.尼古拉斯

___forwarding___的實現

國外有大神復原了___forwarding___的實現,具體可參考Hmmm, What's that Selector?

須要注意的是,復原了___forwarding___的實現是僞代碼。具體代碼我已經放在了github上。

僞代碼
// 兩個參數:前者爲被轉發消息的棧指針 IMP ,後者爲是否返回結構體
int __forwarding__(void *frameStackPointer, int isStret) {
    id receiver = *(id *)frameStackPointer;
    SEL sel = *(SEL *)(frameStackPointer + 8);
    const char *selName = sel_getName(sel);
    Class receiverClass = object_getClass(receiver);
    
    // 調用 forwardingTargetForSelector:
    // 進入 備援接收 主要步驟
    if (class_respondsToSelector(receiverClass, @selector(forwardingTargetForSelector:))) {
        // 得到方法簽名
        id forwardingTarget = [receiver forwardingTargetForSelector:sel];
        // 判斷返回類型是否正確
        if (forwardingTarget && forwardingTarget != receiver) {
            if (isStret == 1) {
                int ret;
                objc_msgSend_stret(&ret,forwardingTarget, sel, ...);
                return ret;
            }
            return objc_msgSend(forwardingTarget, sel, ...);
        }
    }
    
    // 殭屍對象
    const char *className = class_getName(receiverClass);
    const char *zombiePrefix = "_NSZombie_";
    size_t prefixLen = strlen(zombiePrefix); // 0xa
    if (strncmp(className, zombiePrefix, prefixLen) == 0) {
        CFLog(kCFLogLevelError,
              @"*** -[%s %s]: message sent to deallocated instance %p",
              className + prefixLen,
              selName,
              receiver);
        <breakpoint-interrupt>
    }
    
    // 調用 methodSignatureForSelector 獲取方法簽名後再調用 forwardInvocation
    // 進入消息轉發系統
    if (class_respondsToSelector(receiverClass, @selector(methodSignatureForSelector:))) {
        NSMethodSignature *methodSignature = [receiver methodSignatureForSelector:sel];
        // 判斷返回類型是否正確
        if (methodSignature) {
            BOOL signatureIsStret = [methodSignature _frameDescriptor]->returnArgInfo.flags.isStruct;
            if (signatureIsStret != isStret) {
                CFLog(kCFLogLevelWarning ,
                      @"*** NSForwarding: warning: method signature and compiler disagree on struct-return-edness of '%s'. Signature thinks it does%s return a struct, and compiler thinks it does%s.",
                      selName,
                      signatureIsStret ? "" : not,
                      isStret ? "" : not);
            }
            if (class_respondsToSelector(receiverClass, @selector(forwardInvocation:))) {
                // 傳入消息的所有細節信息
                NSInvocation *invocation = [NSInvocation _invocationWithMethodSignature:methodSignature frame:frameStackPointer];
                
                [receiver forwardInvocation:invocation];
                
                void *returnValue = NULL;
                [invocation getReturnValue:&value];
                return returnValue;
            } else {
                CFLog(kCFLogLevelWarning ,
                      @"*** NSForwarding: warning: object %p of class '%s' does not implement forwardInvocation: -- dropping message",
                      receiver,
                      className);
                return 0;
            }
        }
    }
    
    SEL *registeredSel = sel_getUid(selName);
    
    // selector 是否已經在 Runtime 註冊過
    if (sel != registeredSel) {
        CFLog(kCFLogLevelWarning ,
              @"*** NSForwarding: warning: selector (%p) for message '%s' does not match selector known to Objective C runtime (%p)-- abort",
              sel,
              selName,
              registeredSel);
    }  // doesNotRecognizeSelector,主動拋出異常
    // 代表未能獲得處理
    else if (class_respondsToSelector(receiverClass,@selector(doesNotRecognizeSelector:))) {
        [receiver doesNotRecognizeSelector:sel];
    }
    else {
        CFLog(kCFLogLevelWarning ,
              @"*** NSForwarding: warning: object %p of class '%s' does not implement doesNotRecognizeSelector: -- abort",
              receiver,
              className);
    }
    
    // The point of no return.
    kill(getpid(), 9);
}
 
複製代碼

小結

  • 消息轉發階段,先判斷forwardingTargetForSelector的返回值,若是有值,就向這個返回值發送消息。也就是objc_msgSend(返回值, SEL)
  • 若是返回爲nil,就調用methodSignatureForSelector方法,若是有值,就調用forwardInvocation,其中的參數是一個 NSInvocation 對象,並將消息所有屬性記錄下來。 NSInvocation 對象包括了選擇子、target 以及其餘參數。其中的實現僅僅是改變了 target 指向,使消息保證可以調用。假若發現本類沒法處理,則繼續想父類進行查找。直至 NSObject 。
  • 若是methodSignatureForSelector方法返回nil,就調用doesNotRecognizeSelector:方法

上面都是源碼分析,那下面代碼驗證

在源碼中forwardingTargetForSelector系統默認返回nil 。

+ (id)forwardingTargetForSelector:(SEL)sel {
    return nil;
}

- (id)forwardingTargetForSelector:(SEL)sel {
    return nil;
}
複製代碼

消息轉發實例一

咱們有類Person只定義了方法- (void)run;可是沒有實現,另外有類Car,實現了方法- (void)run;

@interface Car : NSObject
- (void)run;
@end


#import "Car.h"

@implementation Car
- (void)run{
    NSLog(@"%s",__func__);
}
@end
複製代碼

在person中,重寫forwardingTargetForSelector讓返回Car對象

// 消息轉發
- (id)forwardingTargetForSelector:(SEL)aSelector{
    if (aSelector == @selector(run)) {
        return [[Car alloc] init];
    }
    return [super forwardingTargetForSelector:aSelector];
}
    
複製代碼

調用的時候

Person *per = [[Person alloc]init];
[per run]; 
 
複製代碼

輸出objc-test[16694:174917] -[Car run]

驗證了前面說的,forwardingTargetForSelector返回值不爲空的話,就向這個返回值發送消息,也就是 objc_msgSend(返回值, SEL)

消息轉發實例二

若是前面的forwardingTargetForSelector返回爲空, 就會調用 methodSignatureForSelector 獲取方法簽名後再調用 forwardInvocation

// 方法簽名:返回值類型、參數類型
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    if (aSelector == @selector(run)) {
       return [NSMethodSignature signatureWithObjCTypes:"v16@0:8"];
    }
     return [super methodSignatureForSelector:aSelector];
}
    
// NSInvocation封裝了一個方法調用,包括:方法調用者、方法名、方法參數
//        anInvocation.target 方法調用者
//        anInvocation.selector 方法名
//        [anInvocation getArgument:NULL atIndex:0]
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
        
   [anInvocation invokeWithTarget:[[Car alloc] init]];
}
複製代碼

依然能夠調用到-[Car run]

注意點1

消息轉發的forwardingTargetForSelectormethodSignatureForSelector以及forwardInvocation不只支持實例方法,還支持類方法。不過系統沒有提示,須要寫成實例方法,而後把前面的-改爲+便可。

注意點2 只能向運行時動態建立的類添加ivars,不能向已經存在的類添加ivars

這是由於在編譯時只讀結構體class_ro_t就會被肯定,在運行時是不可更改的。ro結構體中有一個字段是instanceSize,表示當前類在建立對象時須要多少空間,後面的建立都根據這個size分配類的內存。 若是對一個已經存在的類增長一個參數,改變了ivars的結構,這樣在訪問改變以前建立的對象時,就會出現問題。

資料下載

資料下載

github

參考資料

runtime源碼

Hmmm, What's that Selector?

RuntimePDF

iOS底層原理

Objective-C Runtime

Objective-C 消息發送與轉發機制原理

objc_msgSend消息傳遞學習筆記

更多資料,歡迎關注我的公衆號,不定時分享各類技術文章。

相關文章
相關標籤/搜索