runtime的底層原理和使用

weixin_34365417發表於2018-08-15

先來了解一下isa的組成

我們去這個網站(https://opensource.apple.com/tarballs/objc4/)搜尋objc4,然後下載最新的壓縮檔案,這個就是蘋果開源的部分的底層程式碼(所以我們不能說蘋果是完全閉元的),如圖所示:

7935076-98d749d8503f7be4.png
image.png

解壓 開啟工程搜尋isa_t 如圖:
7935076-3583aafea964c69a.png
image.png

可以看到如下的結構,程式碼為:

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

    Class cls;
    uintptr_t bits;
#if SUPPORT_PACKED_ISA
# if __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL
#   define ISA_MAGIC_MASK  0x000003f000000001ULL
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL
    struct {
        uintptr_t nonpointer        : 1;
        uintptr_t has_assoc         : 1;
        uintptr_t has_cxx_dtor      : 1;
        uintptr_t shiftcls          : 33; // MACH_VM_MAX_ADDRESS 0x1000000000
        uintptr_t magic             : 6;
        uintptr_t weakly_referenced : 1;
        uintptr_t deallocating      : 1;
        uintptr_t has_sidetable_rc  : 1;
        uintptr_t extra_rc          : 19;
#       define RC_ONE   (1ULL<<45)
#       define RC_HALF  (1ULL<<18)
    };
};

其中我只分析arm64的 x86_64的是模擬器 或者mac的 我們不分析了 ,其中共用體裡面有一個結構體,可以看到 結構體中加起來正好是64 也就說 我們的一個物件的isa中存放這些東西以及他們的記憶體分配情況,我們知道這個結構體是用來更高好的做解讀說明的,也就是isa中存放這些東西 ,那麼他的每一個東西都是幹什麼用的呢 我會具體的解釋每一個的作用

        uintptr_t nonpointer        : 1;// 儲存著class meta-class的物件的記憶體地址,0 :代表普通 1:代表優化過的。
        uintptr_t has_assoc         : 1;// 是否有沒有設定過關聯物件 ,如果沒有設定過關聯物件 釋放的就會更快。(0:代表沒有1:代表有)
        uintptr_t has_cxx_dtor      : 1;// 是否有c++的解構函式,如果沒有釋放的更快。(0:代表沒有1:代表有)
        uintptr_t shiftcls          : 33; // 這33為 儲存的是物件的記憶體地址資訊。
        uintptr_t magic             : 6; // 這6為用於除錯時是否為完成初始化。 
        uintptr_t weakly_referenced : 1;// 是否是為被弱引用指向過
        uintptr_t deallocating      : 1; // 物件是否正在釋放
        uintptr_t has_sidetable_rc  : 1;// 裡面引用計數是否過大無法儲存在isa中,如果為1 則證明過大,那麼引用計數會存在SideTable的類中
        uintptr_t extra_rc          : 19;// 儲存的是引用計數器減1

為了驗證我的結論,我拿出一個例子來進行驗證
程式碼1為:

 DGPerson *person = [[DGPerson alloc] init];
  NSLog(@"-------------");

列印person的記憶體地址


7935076-e7d3e5bbeff86aff.png
image.png

將我們的記憶體地址放到我們的系統自帶的計算器中 可以看到


7935076-16d6010c586b7583.png
image.png

沒有設定關聯物件的這個位置是0,接下來設定一下關聯物件
程式碼2為:
DGPerson *person = [[DGPerson alloc] init];
    objc_setAssociatedObject(person, @"nameKey", @"asdasdasdasd", OBJC_ASSOCIATION_COPY_NONATOMIC);

可以看到 第二位為1了


7935076-3e8b00e3e95979be.png
image.png

其他的不一一驗證了

瞭解一下class的組成以及每一部分的作用

  • 大家我們上面所說的底層的程式碼。搜尋objc_class,如圖所示,找到這個檔案objc-runtime-new.h檔案(執行時的檔案)


    7935076-c1f1510e61f1a785.png
    image.png

    經過精簡:

struct objc_class : objc_object {
    // Class ISA;
    Class superclass;  // 用於指向父類的指標
    cache_t cache;             // 快取方法,為了下次快速查詢
    class_data_bits_t bits; // 用於獲取具體的類資訊
}

其中superclass 是如果在當前的類物件中找不到就通過superclass到父類中去查詢。
cache_t的結構可以看到為:

struct cache_t {
    struct bucket_t *_buckets;
    mask_t _mask;
    mask_t _occupied;
}
struct bucket_t {
    cache_key_t _key;
    IMP _imp;
}

其中_buckets儲存的是一個個的bucket_t,而_mask是雜湊表的長度-1,(比如雜湊表的長度是10,那麼他就是9)_occupied是已經快取的方法的個數。cache_t 是通過雜湊表(雜湊表)的方式進行快取的,這樣的做的目的是更加快速找到找到我們曾經快取的方法。bucket_t中存在一個key和imp,其中SEL作為key,而imp為方法的地址 。比如我們下面的程式碼:

   DGPerson *person = [[DGPerson alloc] init];
   [person test];
   
   [person test];
   [person test];
   [person test];

這樣的方法 在第一次person 第一次執行test方法的時候是按部就班的執行,先去當前類中查詢,如果找不到就到父類中查詢,以此類推。但是第二次在呼叫的時候就會就直接從快取中查詢了 那樣的話查詢的速度就更加的快了。

  • class_data_bits_t這個&上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;
}

對其中重點的東西進行說明:methods 就是方法列表,properties屬性列表,protocols協議列表。其中methods是一個二維的陣列,methods存放的是method_list_t, method_list_t中存放的是method_t ,結構如圖所示:


7935076-52ee01e2d740abd4.png
image.png

我們重點研究一下ro(只讀)。可以看到class_ro_t的結構如下:

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
#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;
    }
};

其中他也有一個baseMethodList,他的baseMethodList存放的是method_t,其中class_ro_t中的methodlist 和class_rw_t中的methodlist有什麼區別呢?可以說class_ro_t中的methodlist 是隻讀的,是類原始的方法等等,而class_rw_t中的methodlist是整個的比如後面分類中增加的方法都加入到這裡來了,可以說class_rw_t中的methodlist大於等於class_ro_t中的methodlist的方法。

  • method_t的結構以及解釋:
struct method_t {
    SEL name; // 函式名字
    const char *types; // 編碼(返回值型別、引數型別)
    IMP imp;// 指向函式的指標(函式的地址)
};

解釋:
1.其中imp是指向函式的指標,代表著具體函式的實現。
2.SEL是函式的方法選擇器
可以通過一下幾種方法生成

    SEL method1 = @selector(name);
    SEL method2 = sel_registerName("name");
    NSLog(@"method1 : %p -- method2:%p",method1,method2);

而且不同類中只要方法的名字相同生成的方法選擇器是相同的,比如以上的列印:


7935076-e0285c5cc28d30a7.png
image.png

當然你可以試試不同的類 我這裡不試了 因為確實是這樣的。
還可以通過以下的或者相應的字串

    SEL method1 = @selector(name);
    SEL method2 = sel_registerName("name");
    NSString *methodName1 = NSStringFromSelector(method1);
    const char *methodName2 = sel_getName(method2);
    NSLog(@"methodName1 --- %@ //// methodName2 ---- %s",methodName1,methodName2);

可以看到列印的結果為:


7935076-24e2899eb017a8b0.png
image.png
  • types編碼他的格式為:


    7935076-466951aea4225929.png
    image.png

    下面我們通過程式碼看下person中的types的型別

    DGPerson *person = [[DGPerson alloc] init];
    DG_objc_class *personStruct = (__bridge DG_objc_class *)[DGPerson class];
    class_rw_t *rwStruct = personStruct->data();
      NSLog(@"---------");
其中person中的方法為:
      - (void)test;

其中DG_objc_class是c++的一個檔案,相關的修改內容如下(就是一個.h檔案,使用需要將你的controller改為controller.mm)

//
//  DGPersonInfo.h
//  verification_Isa
//
//  Created by apple on 2018/8/1.
//  Copyright © 2018年 apple. All rights reserved.
//
#import <Foundation/Foundation.h>

#ifndef DGPersonInfo_h
#define DGPersonInfo_h


# if __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL
# elif __x86_64__
#   define ISA_MASK        0x00007ffffffffff8ULL
# endif

#if __LP64__
typedef uint32_t mask_t;
#else
typedef uint16_t mask_t;
#endif
typedef uintptr_t cache_key_t;

#if __arm__  ||  __x86_64__  ||  __i386__
// objc_msgSend has few registers available.
// Cache scan increments and wraps at special end-marking bucket.
#define CACHE_END_MARKER 1
static inline mask_t cache_next(mask_t i, mask_t mask) {
    return (i+1) & mask;
}

#elif __arm64__
// objc_msgSend has lots of registers available.
// Cache scan decrements. No end marker needed.
#define CACHE_END_MARKER 0
static inline mask_t cache_next(mask_t i, mask_t mask) {
    return i ? i-1 : mask;
}

#else
#error unknown architecture
#endif

struct bucket_t {
    cache_key_t _key;
    IMP _imp;
};

struct cache_t {
    bucket_t *_buckets;
    mask_t _mask;
    mask_t _occupied;
    
    IMP imp(SEL selector)
    {
        mask_t begin = _mask & (long long)selector;
        mask_t i = begin;
        do {
            if (_buckets[i]._key == 0  ||  _buckets[i]._key == (long long)selector) {
                return _buckets[i]._imp;
            }
        } while ((i = cache_next(i, _mask)) != begin);
        return NULL;
    }
};

struct entsize_list_tt {
    uint32_t entsizeAndFlags;
    uint32_t count;
};

struct method_t {
    SEL name;
    const char *types;
    IMP imp;
};

struct method_list_t : entsize_list_tt {
    method_t first;
};

struct ivar_t {
    int32_t *offset;
    const char *name;
    const char *type;
    uint32_t alignment_raw;
    uint32_t size;
};

struct ivar_list_t : entsize_list_tt {
    ivar_t first;
};

struct property_t {
    const char *name;
    const char *attributes;
};

struct property_list_t : entsize_list_tt {
    property_t first;
};

struct chained_property_list {
    chained_property_list *next;
    uint32_t count;
    property_t list[0];
};

typedef uintptr_t protocol_ref_t;
struct protocol_list_t {
    uintptr_t count;
    protocol_ref_t list[0];
};

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;
};

struct class_rw_t {
    uint32_t flags;
    uint32_t version;
    const class_ro_t *ro;
    method_list_t * methods;    // 方法列表
    property_list_t *properties;    // 屬性列表
    const protocol_list_t * protocols;  // 協議列表
    Class firstSubclass;
    Class nextSiblingClass;
    char *demangledName;
};

#define FAST_DATA_MASK          0x00007ffffffffff8UL
struct class_data_bits_t {
    uintptr_t bits;
public:
    class_rw_t* data() {
        return (class_rw_t *)(bits & FAST_DATA_MASK);
    }
};

/* OC物件 */
struct DG_objc_object {
    void *isa;
};

/* 類物件 */
struct DG_objc_class : DG_objc_object {
    Class superclass;
    cache_t cache;
    class_data_bits_t bits;
public:
    class_rw_t* data() {
        return bits.data();
    }
    
    DG_objc_class* metaClass() {
        return (DG_objc_class *)((long long)isa & ISA_MASK);
    }
};
#endif /* DGPersonInfo_h */

通過列印可以看到types為:v16@0:8
解釋,其實test的真正為-(void)test:(id)self cmd:(SEL)cmd.
1.v:返回值為(void)
2.整個物件佔用16個位元組的空間
3.@:id
4.從第0位開始(id )我們知道佔用8位
5.:返回值是SEL
6.8從第八位開始,佔用8個位元組。
假如我們修改test方法改為- (int)test:(NSString *)name,那麼我們知道其實他的真正為:- (int)test:(id)self cmd:(SEL)cmd name:(NSString *)name,其中前兩個引數是系統預設給我們加上去的。
那麼我們看一下types是什麼?


7935076-3f6dc32c49ee4450.png
image.png

可以看到types為“i24@0:8@16”
解釋:
1.i:返回的是int型別
2.24 共佔用24個位元組
3.@引數id型別
4.0:從第0個位元組開始
5.:SEL引數型別
6.8從第8個位元組開始(因為self是一個物件,所以佔用8個位元組)
7.@引數nsstring型別
8.16從第16個位元組開始,那麼他佔用的是8個位元組(24-16)
我在網上找了一篇文章,找到了type encoding的對應表


7935076-928188183554c843.png
image.png
  • 重點研究一下cache_t 快取的方法和雜湊表的演算法原理。
    1.雜湊表的演算法的大致說明:
    首先我們將一個方法新增進去快取的陣列,但是他怎麼放的呢,也就說這個方法所在的陣列的index是多少呢 ,它是@selector(方法名字)&MASK = index,開始的時候他會預設開啟一個陣列的空間,然後讓第一次呼叫的方法快取進去我們的陣列中,依次類推。比如我們的開始陣列的個數是10,但是我們快取的方法是3個那麼還有7個位置是空的 那麼他就會將那些位置置為null,他下一個在一次呼叫同樣的方法時候,他就會通過@selector(方法名字)&MASK = index 這樣直接去拿陣列的index,這樣的操作雖然犧牲了一些空間 但是確實提高了很大的效率 不用便利查詢了。但是存在
    問題1.有可能我們生成的下標是重複的。
    問題2.如果有一天我們的快取的方法變成了20 那麼我們之前的陣列空間不夠了怎麼辦。
    問題1解決:他每一次拿出來的東西會判斷是否等於我們的方法名字,如果發現發現等於直接取出,如果不等於直接去前一個以此類推,如果發現第0個還不是 那就取陣列最後一個 然後在往前找一次類推。當然儲存的時候他會判斷當前設定的下標所對應的物件是否有值,有值的話就會往前儲存依次類推,如果發現第0個都有值 那麼就設定設定最後一個值 依次類推。
    問題2解決:當有一天他發現陣列的空間小於要快取的方法的個數,那麼他會重新計算分配陣列空間 以及重新快取方法。
  • 我們具體檢視一下快取的方法 以及驗證我以上說的結論
    先說名cache_t的結構如下:
struct cache_t {
    struct bucket_t *_buckets; // 方法的key和imp
    mask_t _mask;// mask做&操作
    mask_t _occupied;//已經快取的方法的個數
}
struct bucket_t {
    cache_key_t _key;
    IMP _imp;
}

程式碼中全部都是繼承關係,我們看下具體執行的結果

        DGChinesePerson *chinesePerson = [[DGChinesePerson alloc] init];
        DG_objc_class *chinesePersonStruct = (__bridge DG_objc_class *)[DGChinesePerson class];
        
        [chinesePerson DGChinesePersonTest];
        [chinesePerson DGYellowPersonTest];
        [chinesePerson personTest];
        NSLog(@"---------------");

我們分別在每一個方法執行的地方打上一個斷點,檢視效果


7935076-555651772805125c.png
image.png

7935076-ca72290750e0ca39.png
image.png

7935076-f1edc430a2ca5acf.png
image.png

7935076-82263c88fa7db810.png
image.png

可以看到 開始分配的陣列的個數是4(mask+1),mask是3 當到第一個方法的時候occupied為1,因為此時執行了alloc方法,到最後一個方法執行的時候陣列重新分配變成了8((mash= k)+1),可以驗證我以上的結論了。
下面我們來看下具體的快取的方法,我迴圈便利列印出來
列印的程式碼為:

        DGChinesePerson *chinesePerson = [[DGChinesePerson alloc] init];
        DG_objc_class *chinesePersonStruct = (__bridge DG_objc_class *)[DGChinesePerson class];
        cache_t cacheMethods = chinesePersonStruct->cache;
        
        [chinesePerson DGChinesePersonTest];
        [chinesePerson DGYellowPersonTest];
        [chinesePerson personTest];
        NSLog(@"---------------");
        bucket_t *bucketLists = cacheMethods._buckets;
        for (int index = 0; index <= cacheMethods._mask; index++) {
            bucket_t bucket = bucketLists[index];
            NSLog(@"_key : %s --- _imp : %p",bucket._key,bucket._imp);
        }
7935076-482d6406426edefd.png
image.png

可以看到我們快取中並沒有看到persontest這個方法 其實這不是結論錯誤是因為我們獲取的地方不對,假如程式碼這樣修改為:


7935076-383fafc2d0f4d2f9.png
image.png

可以看到結果為:


7935076-5705487e82184d8c.png
image.png

由此可見以上所說的結論是毫無問題的
下面我們執行這段程式碼:
       DGChinesePerson *chinesePerson = [[DGChinesePerson alloc] init];
        DG_objc_class *chinesePersonStruct = (__bridge DG_objc_class *)[DGChinesePerson class];
        cache_t cacheMethods = chinesePersonStruct->cache;
        
        [chinesePerson DGChinesePersonTest];
        [chinesePerson DGYellowPersonTest];
        NSLog(@"---------------");
        bucket_t *bucketLists = cacheMethods._buckets;
        bucket_t testBucket = bucketLists[(long long)@selector(DGChinesePersonTest) & cacheMethods._mask];
        NSLog(@"_key : %s --- _imp : %p",testBucket._key,testBucket._imp);

列印結果是:


7935076-568f846d959de80d.png
image.png

可以看到真是我們的的方法名字(但是這樣列印是不準確的,應該明白只不過是趕巧而已)
我們可以將以上程式修改為,這樣列印的一定是準確的

        DGChinesePerson *chinesePerson = [[DGChinesePerson alloc] init];
        DG_objc_class *chinesePersonStruct = (__bridge DG_objc_class *)[DGChinesePerson class];
        cache_t cacheMethods = chinesePersonStruct->cache;
        [chinesePerson DGChinesePersonTest];
        [chinesePerson DGYellowPersonTest];
        NSLog(@"---------------");
        IMP methodImp = cacheMethods.imp(@selector(DGChinesePersonTest));
        NSLog(@"methodImp -- %p",methodImp);
        NSLog(@"+++++++++++++");
7935076-c90585ca368d90bf.png
image.png

為什麼是準確的,因為我的標頭檔案中這樣書寫


7935076-f201c46517f0db79.png
image.png

相當於按照雜湊表的演算法進行書寫,這樣拿到的一定不為null

進入正題 訊息機制

  • ios的訊息機制分為三個主要的步驟:
    1.訊息傳送
    2.訊息方法的動態解析
    3.訊息的轉發
  • 訊息傳送:
    他的大致過程為:首先他會判斷receiver是否為空,如果為空則直接返回。如果不為空到當前類的方法的快取中去查詢,如果找到了直接返回方法,如果沒有找到到當前類的方法中繼續查詢,如果找到了返回方法並且儲存在當前類的快取方法中。如果沒有找到通過superclass指標到當前類的父類中中的快取方法中去查詢,如果找到了方法那麼返回方法並且快取進當前類物件的快取方法中。如果沒有找到找當前類物件的class_rw_t的方法列表中查詢,找到了快取進方法列表中並且返回方法。如果沒有找到繼續通過superclass的指標到其父類的父類中查詢,依次類推。
    用一張圖形象的表示為:
    7935076-f33ab1fc049c60ea.png
    image.png

    其中如果找class_rw_t的方法列表中存在的方法是有順序的採用的是二分查詢方法。如果不是有序的那麼採用的是普通的便利查詢
  • 如果以上都找不到方法,就會進入動態的方法的解析階段,我們可以在此階段動態的新增方法的實現。比如
    程式碼1:
person中新增如下方法,並沒有新增實現
- (void)test;
我在person中新增另一個方法的實現比如
- (void)otherTest{
    
    NSLog(@"------------");
    
}
我想動態的新增方法的實現可以如下操作:
+(BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == @selector(test)) {
        Method method = class_getInstanceMethod(self, @selector(otherTest));
        class_addMethod(self, sel, method_getImplementation(method), method_getTypeEncoding(method));
        return YES;
    }
   return [super resolveInstanceMethod:sel];
}

其中resolveInstanceMethod這個方法是在訊息傳送的過程中找不到當前方法的實現才會呼叫這個方法,來動態的找方法的實現。
程式碼二:也可以通過這種方法來實現:

void otherTest(id self,SEL _cmd){
    
    NSLog(@"------------");
    
}

+(BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == @selector(test)) {
        class_addMethod(self, sel, (IMP)otherTest, "v16@0:8");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

當然如果是類物件,那麼就需要實現這個方法

void otherTest(id self,SEL _cmd){
    
    NSLog(@"------------");
    
}
+(BOOL)resolveClassMethod:(SEL)sel{
    if (sel == @selector(test)) {
        class_addMethod(object_getClass(self), sel, (IMP)(otherTest), "v@:");
    }
    return [super resolveClassMethod:sel];
    
}

需要注意兩點:
1.例項方法實現的是例項方法,類方法實現的是類方法。
2.還有其中引數傳遞的時候是object_getClass(self) 也就是他的原類的物件。

訊息的轉發

當訊息的傳送和訊息的動態的方法的實現都找不到方法的時候也就是進入到了訊息的轉發的階段
他會實現這個方法:

-(id)forwardingTargetForSelector:(SEL)aSelector{
    if (aSelector == @selector(test)) {
        return [[DGStudent alloc] init];
    }
    return [super forwardingTargetForSelector:aSelector];
}
其中DGStudent的內部實現了test方法
@interface DGStudent : NSObject
- (void)test;
@end
@implementation DGStudent
- (void)test{
    NSLog(@"我的列印是 --- %s",__func__);
}
@end

假如這個方法不實現,他會呼叫哪個方法呢?他會實現這個方法,就是

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;

這個方法是返回一個方法的types,要想實現該方法需要結合下面的方法一起實現
所以結合起來的是這樣實現的:

-(void)forwardInvocation:(NSInvocation *)anInvocation{
    [anInvocation invokeWithTarget:[[DGStudent alloc] init]];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    if (aSelector == @selector(test)) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
   return [super methodSignatureForSelector:aSelector];
}
總結:小的轉發的流程是首先進入這個方法:forwardingTargetForSelector:(SEL)aSelector如果這個方法返回了那麼就去返回方法的實現,如果返回為nil那麼就去找這個方法- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector當然需要-(void)forwardInvocation:(NSInvocation *)anInvocation這個方法的配合實現。如果這個方法- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector也沒有返回那麼就去找這個方法doesNotRecognizeSelector的這個方法。也就是報錯(方法找不到實現的)。

一個簡單的流程圖是這樣的


7935076-d0aa388f32b50f8f.png
image.png

也就是說訊息的一個傳送過程就是以上的步驟。

  • 訊息轉發的用處:
    我們知道當報方法找不到這個錯誤的話就會顯示錯誤,也就是程式就會crash,為了降低crash的出錯率 ,我簡單寫一個小的程式,比如person類,當出現了幾個方法找不到的時候我們怎樣能找到他
    程式碼如下:
#import "DGPerson.h"
#import <objc/runtime.h>
@implementation DGPerson

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

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    if ([self respondsToSelector:aSelector]) {
        
        return [NSMethodSignature methodSignatureForSelector:aSelector];
    }
    return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
-(void)forwardInvocation:(NSInvocation *)anInvocation{
    NSLog(@"%@ -- 哪個方法 %@ 沒有實現",anInvocation.target,NSStringFromSelector(anInvocation.selector));
    
}
@end

#import <Foundation/Foundation.h>

@interface DGPerson : NSObject
-(void)test;
-(void)run;
-(void)eat;
@end

當我們呼叫這三個方法的時候,他會列印如下的方法


7935076-06afb4d9bdebef5e.png
image.png

super的理解

看看下面的列印,

#import "DGStudent.h"

@implementation DGStudent

-(void)handleAction{
    [super handleAction];
}
-(instancetype)init{
    if (self = [super init]) {
        
        NSLog(@"[self class] = %@",[self class]);
        NSLog(@"[self superclass] = %@",[self superclass]);
        
        NSLog(@"-------------------------");
        
        NSLog(@"[super class] = %@",[super class]);
        NSLog(@"[super superclass] = %@",[super superclass]);
    }
    return self;
}
@end
其中的繼承的關係為 student繼承person,person繼承object

列印的結果為:


7935076-269736c2d0a51ab5.png
image.png

其中第一和第二都能很簡單的知道為什麼,但是第三和第四是為什麼
按照我的個人的理解應該是person 和object但是為什麼student 和person呢
先解釋一下super,其中實現這樣的方法

#import "DGStudent.h"
@implementation DGStudent
-(void)handleAction{
   [super handleAction];
}
end

看一下他的底層的程式碼的實現

static void _I_DGStudent_handleAction(DGStudent * self, SEL _cmd)
 {
    ((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super)
{      
        (id)self,
        (id)class_getSuperclass(objc_getClass("DGStudent"))
},
        sel_registerName("handleAction"));
}

相當於是一個結構體,我們知道student 那麼就相當於這樣寫

static void _I_DGStudent_handleAction(DGStudent * self, SEL _cmd)
 {
    ((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super)
{      
        (id)self,
        DGPerson
},
        handleAction方法
}

我們看下__rw_objc_super這個結構體裡面是怎樣組成的(到我們的底層程式碼庫中查詢發現)


7935076-33f67d8852d7fd22.png
image.png

整理得到:

struct objc_super {
   __unsafe_unretained _Nonnull id receiver;
   __unsafe_unretained _Nonnull Class super_class;
}

在看一下我們的handleAction的底層實現,發現他的receiver我們傳遞的是self而super_class我們傳遞的是DGPerson,在看一下底層的這方法的objc_msgSendSuper的實現原理


7935076-02fd4016659bac89.png
image.png

通過註釋中可以發現,他的super的實現是從當前類中的父類開始查詢方法的實現,但是物件傳遞的的接受者還是傳遞的當前的物件。
下面回到當前的問題 為什麼[super class] 列印的是student 按照剛才的分析不應該是person嗎 ?但是我們還是忽略了class的這個方法,因為class的這個方法是nsobject的方法 他的虛擬碼大致為:

-(Class)class{
    return object_getClass(self);
}

那就是說明返回物件本身
而superclass的虛擬碼的大致實現為:

-(Class)superClass {
   return class_getSuperclass(object_getClass(self));
}

那麼他應該返回的就是person。

  • 繼續對super進行深入的探究,剛剛我們探究得到super執行的是objc_msgSendSuper這個方法,但是我們看到我們的彙編執行的程式碼是


    7935076-406b21fd440d956c.png
    image.png

    首先我們看下如下的程式碼:

- (void)viewDidLoad {
    [super viewDidLoad];
    id cls = [DGPerson class];
    void *obj = &cls;
    [(__bridge id)obj print];
    NSLog(@"---------------");
}

我們列印可以看出


7935076-91ac68c4db7a5dc3.png
image.png

可以看到我們按照8個位元組往上找,我們找打了super viewdidload的底層實現,我們知道他的大概實現為:

 struct ss = {
        id self,
        [ViewController class]
    };
    objc_MegSendSuper(ss,sel_registerName("viewDidLoad"));
7935076-6d7e3661c17fc4d7.png
image.png

可以看到列印,但是為什麼第三個就是呢?我大致畫一個圖分析下


7935076-5f1f50d68c9f4a9c.png
image.png

所以我們獲取第三個就是viewcontroller 那麼說明super底層呼叫的就是super2的方法。(lldb中命令 x/4g的意思是16進位制,列印4個8個位元組的)

  • 瞭解的內容(llvm)
    我們oc的語言實際上是先轉換成中間的語言(llvm)然後在轉換成底層的組合語言/機器語言,下面我們將我們寫的程式碼轉化成llvm的語言
    我們的程式碼為:
void test(int a){
    NSLog(@"%d",a);
}
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int a = 10;
        int b = 20;
        int c = a + b;
        test(c);
    }
    return 0;
}

我們通過這個命令進行轉化:(切換到我們的main. m所在的上層資料夾)

clang -emit-llvm -S main.m

可以看到如下程式碼:


7935076-5b7e116f356991eb.png
image.png

7935076-4d95e7b8b28d2c83.png
image.png

isMemberOfClass和isKindOfClass的區別(順便一講,已經熟悉的請濾過,本人只是為了做筆記)

  • 實力物件開始呼叫的時候
    比如我們看下這個例子:
        DGPerson *person = [[DGPerson alloc] init];
        NSLog(@"%d",[person isMemberOfClass:[DGPerson class]]);
        NSLog(@"%d",[person isMemberOfClass:[NSObject class]]);
        NSLog(@"%d",[person isKindOfClass:[DGPerson class]]);
        NSLog(@"%d",[person isKindOfClass:[NSObject class]]);

可以看到列印的結果


7935076-b77fbc3b7f2886dd.png
image.png

為了弄清除這個問題我們需要看下底層程式碼的實現:

+ (BOOL)isMemberOfClass:(Class)cls {
    return object_getClass((id)self) == cls;
}

- (BOOL)isMemberOfClass:(Class)cls {
    return [self class] == cls;
}

+ (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}

- (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}

因為我們現在呼叫的是例項物件,所以我們看下減號的方法,可以看到- (BOOL)isMemberOfClass:(Class)cls拿到的是[self class]進行比較,也就是我們實力物件呼叫isMemberOfClass比較的是當前物件的類物件,如果一致返回的就是yes否則是no,而isKindOfClass通過當前類物件以及當前類物件的父類物件進行比較發現是當前的類物件或者當前類物件的父類的類物件那麼就返回yes,否則返回no。
所以以上列印的結果是1 0 1 1
下面我將程式修改為:

        NSLog(@"%d", [[NSObject class] isKindOfClass:[NSObject class]]);
        NSLog(@"%d", [[NSObject class] isMemberOfClass:[NSObject class]]);
        NSLog(@"%d", [[DGPerson class] isKindOfClass:[DGPerson class]]);
        NSLog(@"%d", [[DGPerson class] isMemberOfClass:[DGPerson class]]);

可以看到執行的結果為:


7935076-ac1dfc069e079795.png
image.png

先看下我們的isKindOfClass的類物件的呼叫方法,可以看到他拿到的是當前物件的原類物件進行判斷的,判斷我們傳遞的物件的原類的物件,已經原類物件的父類的原類物件,如果發現相等就返回yes 否則返回no,而isMemberOfClass是判斷當前物件的原類物件是不是和我們傳遞進來的物件是否相等,由此可以分析出isMemberOfClass必定都是no,而isKindOfClass當我們呼叫這句程式碼的時候( [[DGPerson class] isKindOfClass:[DGPerson class]]);)應該也是返回no,因為我們知道他應該等於他的原類物件,但是我們判斷[[NSObject class] isKindOfClass:[NSObject class]]);為什麼就是yes了 因為isKindOfClass他會去父類中查詢一直找找到nsobject的原類的時候他就會去找類物件 類物件就是nsobject,我們以前說過這樣的一幅圖


7935076-c6c9bfd6547c7d01.png
image.png

可以看到以上所說,那麼現在也就是說任何繼承nsobject物件的類物件呼叫isKindOfClass如果我們傳遞的是[NSObject class]那麼都應該返回的是yes,比如
NSLog(@"%d", [[DGPerson class] isKindOfClass:[NSObject class]]);
7935076-7ecaac926dc2872e.png
image.png

runtime的運用(個人覺得很重要)

    1. 類的相關的api
      1.1 object_setClass :切換isa的指向
    DGPerson *person = [[DGPerson alloc] init];
    [person test];
    // 將person的isa指標指向DGCar
    object_setClass(person, [DGCar class]);
    [person test];
7935076-b58e35d4b7239525.png
image.png

1.2 object_getClass:獲取isa所指向的類
解釋:
如果傳遞的是例項物件那麼獲取的是類物件。
如果傳遞的是類物件那麼獲取的是原類物件。
1.3 object_isClass 判斷的是否是一個類物件:

    DGPerson *person = [[DGPerson alloc] init];
    NSLog(@"%d - %d - %d", object_isClass([DGPerson class]),object_isClass(object_getClass([DGPerson class])),object_isClass(person));
7935076-a216ec512153a6c5.png
image.png

解釋:值得說明的是原類物件也是特殊的類物件,所以第二個列印的是1
1.4 class_isMetaClass 判斷是不是原類的物件,不再舉例子 。
1.5 class_getSuperclass()獲取父類,太簡單不在舉例子。
1.6 動態建立一個類,以及註冊一個類 一般的情況下他們是組合一起使用的(其中也包括動態的新增方法和屬性等)
程式碼如下:

- (void)viewDidLoad {
    [super viewDidLoad];  
    //建立一個類
    Class animalClass = objc_allocateClassPair([NSObject class], "DGAnimal", 0);
    // 新增屬性(animalClass:類名 4:int佔的位元組數 1:記憶體對齊預設為1 "v@:":types)
    class_addIvar(animalClass, "_age", 4, 1, @encode(int));
    // 新增方法
    class_addMethod(animalClass, @selector(test), (IMP)otherTest, "v@:");
    // 註冊一個類 ,新增方法等等的 要在註冊類的前面執行最好
    objc_registerClassPair(animalClass);
    
    // 開始使用(必須還要alloc,否則失敗)
    id animal = [[animalClass alloc] init];
    [animal setValue:@10 forKey:@"_age"];
    
    NSLog(@"age = %@",[animal valueForKey:@"_age"]);
    [animal test];
    
}
void otherTest(){
    
    NSLog(@"老夫列印了哈");
}
// 值得非常注意的是,在這個類不用的時候需要釋放因為是c語言的 ,所以必須要釋放
// 不在用到這個類的時候需要釋放
    objc_disposeClassPair(animalClass);
7935076-1572cd5dbb7eb3bc.png
image.png
  • 2 成員變數相關的資訊
    2.1查詢成員變數的資訊(獲取當前類的成員變數)
// 獲取成員變數
    unsigned int count;
    Ivar *ivarList = class_copyIvarList([UITextField class], &count);
    for (int index = 0; index < count; index++) {
        Ivar ivar = ivarList[index];
        NSString *name = [NSString stringWithCString: ivar_getName(ivar) encoding:NSUTF8StringEncoding];
        NSLog(@"%@",name);
        
    }
    free(ivarList);
7935076-5e052c8b2380bd71.png
image.png

2.2 獲取一個成員變數

 // 獲取一個物件的一個成員變數
    Ivar ivar = class_getInstanceVariable([DGPerson class], "_name");
    NSString *nameStr = [NSString stringWithCString:ivar_getName(ivar) encoding:NSUTF8StringEncoding];
    NSString *typeStr = [NSString stringWithCString:ivar_getTypeEncoding(ivar) encoding:NSUTF8StringEncoding];
    NSLog(@"nameStr --- %@  type---%@",nameStr,typeStr);
7935076-921399f19a178aab.png
image.png

2.3設定和獲取成員變數的值

 // 設定一個ivar
    DGPerson *person = [[DGPerson alloc] init];
    Ivar nameIvar = class_getInstanceVariable([DGPerson class], "_name");
    Ivar ageIvar = class_getInstanceVariable([DGPerson class], "_age");
    object_setIvar(person, nameIvar, @"ahshdahshdahsd");
    object_setIvar(person, ageIvar, (__bridge id)(void *)10);
    NSLog(@"name = %@ --- age = %d",person.name,person.age);
7935076-3c97045013ac8c6f.png
image.png
  • 3 屬性相關的
    3.1 property_getName(獲得一個類的屬性)
    objc_property_t nameProperty = class_getProperty([DGPerson class], "name");
    NSString *nameStr = [NSString stringWithCString:property_getName(nameProperty)   encoding:NSUTF8StringEncoding];
    NSLog(@"nameStr -- %@",nameStr);
7935076-f15e3379611f5f48.png
image.png

3.2class_copyPropertyList(獲取屬性列表)
與獲取成員變數的方式一樣 不在贅述。
3.3 class_addProperty(動態的新增成員變數)

//動態的新增屬性
    objc_property_attribute_t type = {"T",[[NSString stringWithFormat:@"@\"%@\"",NSStringFromClass([NSString class])] UTF8String] }; // type 我這裡是string型別
    objc_property_attribute_t copyShip = { "C",""}; // C = copy
    objc_property_attribute_t nonatomicAttr = {"N",""}; // N = nonatomic
    objc_property_attribute_t nameIvar = { "V",[[NSString stringWithFormat:@"_%@",@"hand"] UTF8String]};
    objc_property_attribute_t attrs[] = {type,copyShip,nonatomicAttr,nameIvar};
    class_addProperty([DGPerson class], [@"hand" UTF8String], attrs, 4);
    
    // 動態的獲取屬性
    unsigned int outCount, i;
    objc_property_t *properties = class_copyPropertyList([DGPerson class], &outCount);
    for (i = 0; i < outCount; i++) {
        objc_property_t property = properties[i];
        NSLog(@"屬性 %s ======= 特徵 %s\n", property_getName(property), property_getAttributes(property));
    }

3.4 class_replaceProperty(動態的替換成員變數)
與3.3基本類似,不在贅述。
3.5. 獲取屬性的資訊(property_getName(<#objc_property_t _Nonnull property#>) property_getAttributes(<#objc_property_t _Nonnull property#>))
在3.3使用過,不在贅述。
4 方法的相關的操作
4.1class_getClassMethod( 獲取一個類方法)

 // 獲取一個例項的方法
    Method testMethod = class_getClassMethod([DGPerson class], @selector(eat));
    NSString *methodName = NSStringFromSelector(method_getName(testMethod));
    NSLog(@"methodName -- %@",methodName);
7935076-1a6e9d8bffd6a88a.png
image.png

4.2 class_getInstanceMethod (獲取一個例項的物件)
與4.1類似
4.3class_getMethodImplementation(獲取一個方法的實現)

 IMP eatImp = class_getMethodImplementation([DGPerson class], @selector(eat));

返回是IMP
4.4 method_setImplementation(設定一個方法的實現)

    // 設定一個方法的實現
    IMP carTestImp = class_getMethodImplementation([DGCar class], @selector(test));
    Method personTestMethod = class_getInstanceMethod([DGPerson class], @selector(test));
    method_setImplementation(personTestMethod, carTestImp);
    DGPerson *person = [[DGPerson alloc] init];
    [person test];
7935076-837738bbd7239b8c.png
image.png

4.5 替換方法的實現,這個很重要我們經常使用。

  // 替換方法的實現
    
    Method personTestMethod = class_getInstanceMethod([DGPerson class], @selector(test));
    Method carTestMethod = class_getInstanceMethod([DGCar class], @selector(test));
    method_exchangeImplementations(personTestMethod, carTestMethod);
    
    DGPerson *person = [[DGPerson alloc] init];
    [person test];
7935076-b9fff86e60b562bf.png
image.png

4.6 class_copyMethodList (copy方法列表)
她的實現和我們獲取成員變數的形式類似
4.7 class_addMethod(動態的新增方法 )和class_replaceMethod(動態的替換方法)
其中class_addMethod 我們在訊息動態方法的實現,我們用過,class_replaceMethod與他差不多 只不過一個是新增 一個是替換
其中replace還可以這樣用:

class_replaceMethod([DGPerson class], @selector(test), imp_implementationWithBlock(^{
        NSLog(@"123455666");
        
    }), "v@:");
    DGPerson *person = [[DGPerson alloc] init];
    [person test];
7935076-3ca70c8539f5b00c.png
image.png

4.8 一些方法的相關的屬性


7935076-123fcfbc276d08a6.png
image.png

4.9 瑣碎內容 瞭解即可


7935076-e608c7d74c2f59c8.png
image.png

5 幾個例子 (工作中用到的runtime的)
  • 1.字典轉化模型(我們給nsobject新增一個分類)
    // 簡單的實現,要做好了 其實要考慮的型別應該還有很多(參考MJExtension)
+ (instancetype)modelWithJson:(NSDictionary *)dic{
    id obj = [[self alloc] init];
    unsigned int count;
    Ivar *ivarList = class_copyIvarList([self class], &count);
    
     // 開始便利
    for (int index = 0; index < count; index ++) {
        Ivar ivar = ivarList[index];
        NSMutableString *nameStr = [[NSMutableString alloc] initWithString:[NSString stringWithCString:ivar_getName(ivar) encoding:NSUTF8StringEncoding]];
        [nameStr deleteCharactersInRange:NSMakeRange(0, 1)];
        if ([dic.allKeys containsObject:nameStr]) {
            
            if (dic[nameStr]) { // 不能設定空的值
             [obj setValue:dic[nameStr] forKey:nameStr];
            }
        }
    }
    // 釋放
    free(ivarList);    
    return obj;
}
    1. 方法的替換,比如我們陣列中新增nil的值就會crash
      我們可以新增給陣列新增一個分類,防止插入空的值出現crash
      例如如下的程式碼:
    id obj = nil;
    NSMutableArray *array = [[NSMutableArray alloc] init];
    [array addObject:obj];
7935076-117efd5a52ef3dd7.png
image.png

如果我們這樣寫一個分類

+(void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        
        Class cla = NSClassFromString(@"__NSArrayM"); // 這個要寫類簇
        Method method1 = class_getInstanceMethod(cla, @selector(insertObject:atIndex:));
        Method method2 = class_getInstanceMethod(cla, @selector(DG_insertObject:atIndex:));
        method_exchangeImplementations(method1, method2);
        
    });
    
}
-(void)DG_insertObject:(id)anObject atIndex:(NSUInteger)index{
    
    if (!anObject) return;
    [self DG_insertObject:anObject atIndex:index];

}

看下執行的結果


7935076-4117e03b6e924406.png
image.png

可以看到不crash了 說明我們的方法替換成功了
其中有一個塊值得說明


7935076-8d955b0382478fdd.png
image.png

給人的感覺是死迴圈了 其實不是,我們知道method 其實就是我們的底層的method_t 他的結構包括
7935076-6f545ae8a45805f7.png
image.png

也就是他把imp這個實現給變化了 那麼現在的方法指向為:


7935076-43d985621d4c9a23.png
image.png

所以現在我們在呼叫-(void)DG_insertObject:(id)anObject atIndex:(NSUInteger)index正好呼叫的是系統的方法,所以不會出現死迴圈 反之呼叫系統的方法才會出現死迴圈。
  • 3.我們可以設定關聯物件 objc_setAssociatedObject
    舉個例子比如我們我們一個陣列中包含很多的button,然後我們想做的事情是一個button對應一個model,其實有三個辦法 ,第一我們打一個tag,第二寫一個父類的button,給button增加一個屬性,第三就是就是運用執行時動態的設定關聯屬性。
通過btn傳遞兩個例項物件  firstObject和secondObject
UIButton *btn = // create the button
objc_setAssociatedObject(btn, "firstObject", someObject, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
objc_setAssociatedObject(btn, "secondObject", otherObject, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
[btn addTarget:self action:@selector(click:) forControlEvents:UIControlEventTouchUpInside];
- (void)click:(UIButton *)sender
{
    id first = objc_getAssociatedObject(btn, "firstObject");
    id second = objc_setAssociatedObject(btn, "secondObject");
    // etc.
}

  • 4.獲取所有成員變數或者屬性,這樣的話我們就可以通過kvc來改變一些屬性,比如我們uitextfield的placeholder的顏色,我們就可以通過執行時的方式進行修改。
  • 5 看看那些方法沒有實現,(我們都知道如果方法沒有實現那就crash),所以我們可以在訊息轉發的時候進行攔截,列印出來到時候上報我們的伺服器
    比如我在上面說到的訊息轉發的用處。

總結:

我目前瞭解的runtime就這些,希望對大家有幫助。

相關文章