Objective-C基礎之五(Runtime之Class結構解析)

isa

在以前的學習中,咱們瞭解到isa指針在runtime機制中起到了很是大的做用,經過實例對象的isa指針,咱們能夠找到類對象,經過類對象的isa指針咱們能夠找到元類對象,在經過查看objc4的源碼,咱們能夠看到isa指針是一個union isa_t類型的共用體。數組

其實在arm64以前,isa只是單純的一個指針,裏面存放了類對象(class)、元類對象(mata-class)的地址值。可是在arm64以後,isa指針被優化爲共用體的結構,而且使用位域的技術來使得isa中能夠存儲更多的信息。緩存

此處objc4的源碼版本我選擇的是objc4-756.2bash

位運算

在深刻了解isa內部結構以前,咱們先來簡單瞭解一下什麼是位運算。計算機內存中存儲的數據都是以二進制的形式存儲的,也就是0或者1,而位運算就是直接對內存中的二進制位進行操做,因此它的運算效率很是高。經常使用位運算有如下幾種,此處以C語言爲例:數據結構

運算符 用法
按位與(&) a & b
按位或(|) a | b
按位異或(^) a ^ b
按位取反(~) ~a
左移(<<) a << b
右移(>>) a >> b

按位與(&)

運算規則:相同位的兩個數字都爲1,則爲1,若其中有一個不爲1,則爲1架構

舉例:有兩個數12和20進行按位與運算,20的二進制爲0b00001011,12的二進制格式爲0b00010100app

//12和22進行按位與運算
   0000 1011  (20)
 & 0001 1100  (12)
 ------------
   0000 1000
複製代碼

按位或(|)

運算規則:相同位的兩個數字只要有一個爲1,則爲1less

舉例:有兩個數12和20進行按位或運算,20的二進制爲0b00001011,12的二進制格式爲0b00010100ide

//12和22進行按位或運算
   0000 1011  (20)
 | 0001 1100  (12)
 ------------
   0001 1111
複製代碼

按位異或(^)

運算規則:相同位置的兩個數字相同則爲0,不一樣則爲1函數

舉例:有兩個數12和20進行按位異或運算,20的二進制爲0b00001011,12的二進制格式爲0b00010100工具

//12和22進行按位異或運算
   0000 1011  (20)
 ^ 0001 1100  (12)
 ------------
   0001 0111
複製代碼

按位取反(~)

運算規則:將二進制的每一位變成相反的數,1->0或者0->1

舉例:對0b0100 1110進行取反操做

~0100 1110
 ------------
   1011 0001
複製代碼

左移(<<)

運算規則:將一個運算對象的各個二進制位所有左移若干位(注意:最左側的二進制位丟棄,右側二進制位補0)

舉例:(0b0100 1110)<<一、(0b1100 1110)<<1

0100 1110<<1    1100 1110<<1
 ---------------  --------------
   1001 1100       1001 1100
複製代碼

若是左側最高位不爲1,那麼左移至關於將原有數乘以2,此處0b0100 1110的十進制數爲78,左移一位獲得的十進制數爲156

右移(<<)

運算規則:將一個運算對象的各個二進制位所有右移若干位(注意:正數左側補0,負數左側補1,右側二進制位丟棄)

舉例:(0b1100 1110)>>1

1100 1110>>1
 ---------------
   0110 0111
複製代碼

操做的數每右移一位至關於改數除以2,此處0b1100 1110的十進制數爲206,右移一位獲得十進制數爲103

共用體

在C語言中,共用體其實就是將不一樣類型的變量存放到同一段內存單元中,使用覆蓋技術,幾個變量共同佔用同一段內存結構,相互覆蓋。

//建立一個共用體size
union {
    int height; //寬度
    int width;  //高度
}size;

//對共用體進行操做
size.height = 10;
//若是將height的值修改成10,這時去打印width的值結果顯示爲10
NSLog(@"%d",size.width);

//若是將width的值修改成20,這時去打印height的值結果顯示爲20
size.width = 20;
NSLog(@"%d",size.height);

複製代碼

咱們定義一個共用體size,其中有兩個int類型的成員變量height和width,各佔用4個字節,可是在共用體中,這兩個成員變量共用4個字節的內存空間,一旦修改其中一個成員變量的值,另外一個成員變量的值也會跟着修改。

位域

位域(又叫作位短)實際上是一種數據結構,它能夠將數據以二進制位的形式來存儲,而且容許對此結構的位進行運算。有些信息在存儲的時候,並不須要佔用一個完整字節,有時候只須要佔用一個或幾個二進制位,好比存放一個BOOL類型的變量時,只須要保存0或1兩種狀態,此時只須要1個二進制位就能存儲。所以,位域就是運用在這種場景下的一種數據結構,使用位域能夠有效的節省存儲空間。

位域能夠把一個字節中的二進制位劃分爲幾個不一樣的區域,而且制定每一個區域佔用的位數,每一個域能夠設置一個域名,能夠根據域名對指定的位進行操做。

可是位域也有明顯的缺點,就是它的內存分配和內存對齊的方式依賴於具體的機器和操做系統,不一樣的平臺可能會有不一樣的結果。

位域的結構和結構體相似,它的形式爲

struct 位域結構名稱{
    類型說明符 位域名 : 位域長度;
    類型說明符 位域名 : 位域長度;
    類型說明符 位域名 : 位域長度;
    ......
}

//具體事例
struct size{
    unsigned int width  : 4;
    unsigned int height : 4;
    unsigned int area   : 8;
};

複製代碼

此處須要注意的是:位域成員必須聲明爲int、unsigned int或signed int類型(short char long)

經過sizeof(struct size)能夠獲得位域所佔用內存大小爲4個字節,其實,若是不使用位域的話,整個size結構體佔用的內存大小爲12個字節(int佔用4個字節),可是使用位域以後,size總共只佔用了4個字節,由於其中的width佔用一個字節中的4位,height佔4位,area佔8位,共佔16位,共2個字節,可是因爲內存對齊的原則,這個size共佔用4個字節的內存。所以,經過位域就能夠大量節省內存消耗。若是想了解更多關於位域和內存對齊的知識能夠自行查詢資料,此處只是簡單的作一下介紹。

位域和共用體結合,配合位運算給XLPerson增長BOOL屬性

理解了位運算,位域和共用體的知識,咱們如今就經過具體的實例來加深理解,首先建立一個XLPerson類,若是咱們要給XLPerson類增長屬性,可使用如下方式

@interface XLPerson : NSObject

@property(nonatomic, assign)int height;

@end
複製代碼

可是經過@property這種方式建立的屬性,內部會自動生成_height成員變量,所以,咱們須要本身來實現setter和getter方法,XLPerson.h以下

@interface XLPerson : NSObject

- (void)setHappy:(BOOL)happy;
- (void)setSad:(BOOL)sad;
- (void)setAlone:(BOOL)alone;

- (BOOL)happy;
- (BOOL)sad;
- (BOOL)alone;

@end
複製代碼

XLPerson.m完整代碼以下

#import "XLPerson.h"

#define XLPersonHappyMask (1 << 0)
#define XLPersonSadMask (1 << 1)
#define XLPersonAloneMask (1 << 2)

/** 使用一個字節來存儲多個BOOL屬性 */
@implementation XLPerson{
    union {
        char bits; //共用一個字節 0b0000 0000
        struct {
            char happy  : 1;    //happy佔一位
            char sad    : 1;    //sad佔一位
            char alone  : 1;    //alone佔一位
        };
    } _emotion;
    
}

- (void)setHappy:(BOOL)happy{
    if (happy) {
        //將0b0000 0000的最後一位設置爲1
        _emotion.bits |= XLPersonHappyMask;
    }else{
        _emotion.bits &= ~XLPersonHappyMask;
    }
}

- (void)setSad:(BOOL)sad{
    if (sad) {
        //將0b0000 0000的倒數第二位設置爲1
        _emotion.bits |= XLPersonSadMask;
    }else{
        _emotion.bits &= ~XLPersonSadMask;
    }
}

- (void)setAlone:(BOOL)alone{
    if (alone) {
        //將0b0000 0000的倒數第三位設置爲1
        _emotion.bits |= XLPersonAloneMask;
    }else{
        _emotion.bits &= ~XLPersonAloneMask;
    }
}

- (BOOL)happy{
    return !!(_emotion.bits & XLPersonHappyMask);
}

- (BOOL)sad{
    return !!(_emotion.bits & XLPersonSadMask);
}

- (BOOL)alone{
    return !!(_emotion.bits & XLPersonAloneMask);
}


@end

複製代碼
  • 首先,經過位域和共用體結合,咱們建立出了_emotion這個共用體,內部有一個char類型的成員變量bits,佔用1個字節,共用體內部有一個結構體,有三個成員變量,分別爲happy、sad和alone,各佔一位,共用bits這一個字節的存儲空間,將bits用二進制位來表示就是0b0000 0000,從低位到高位(從右往左)3位依次表明happy、sad和alone
union {
    char bits; //共用一個字節 0b0000 0000
    struct {
        char happy  : 1;    //happy佔一位
        char sad    : 1;    //sad佔一位
        char alone  : 1;    //alone佔一位
    };
} _emotion;
複製代碼
  • 以happy爲例,它佔用0b0000 0000的最後一位,所以要想改變happy的值,只要修改0b0000 0000最後一位爲1或者0便可。要想將0b0000 0000的最後一位設置爲1,只須要將0b0000 00000b0000 0001進行或運算。同理,要想將0b0000 0000的最後一位設置爲0,只須要將0b0000 00000b1111 1110進行按位與運算。
//將最後一位設置爲1
  0000 0000
 |0000 0001
 -----------
  0000 0001

//將最後一位設置爲0
  0000 0000
 &1111 1110
 -----------
  0000 0000
複製代碼

因此,在代碼中,咱們單獨爲happy屬性設置一個掩碼,爲XLPersonHappyMask,它的值爲1 << 0,轉換成二進制位就是0b0000 0001,而後經過此掩碼來進行位運算,以下

- (void)setHappy:(BOOL)happy{
    if (happy) {
        //將0b0000 0000的最後一位設置爲1
        _emotion.bits |= XLPersonHappyMask;
    }else{
        _emotion.bits &= ~XLPersonHappyMask;
    }
}
複製代碼
  • 外部在獲取happy值的時候,須要拿到最後一位,這時能夠經過bits和XLPersonHappyMask進行按位與操做,這個時候獲取到的值最後一位必定是1,其它位必定爲0,而後再對結果進行兩次取反操做,就能拿到happy的值。
- (BOOL)happy{
    return !!(_emotion.bits & XLPersonHappyMask);
}
複製代碼

注意:此處進行按位與操做所獲得的值可能爲任何數,可是有一點不變,就是獲取到的值要麼爲0,要麼爲任意數,所以,咱們只要對按位與的結果進行兩次取反,就能將最後的結果轉換成0或者1.

  • sad、alone屬性和happy屬性實現方式相同,自此咱們就使用共同體加位域實現了爲XLPerson添加BOOL屬性的功能。

isa_t類型詳解

在最新版本的runtime源碼中,NSObject類型最終會轉化成object_class類型的結構體,而object_class繼承自objc_object,在結構體objc_object中就含有isa_t類型的成員isa

struct objc_object {
private:
    isa_t isa;
public:
    ......
}
複製代碼

查看isa_t的源碼,其中有除了兩個構造函數外,有一個cls指針,還有一個uintptr_t類型的成員bits以及一個結構體:

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

    Class cls;
    uintptr_t bits;
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };
#endif
};
複製代碼

查看結構體的源碼能夠發現,在結構體中使用位域來存儲了不少信息,此處只展現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
};
複製代碼

所以,咱們能夠將共用體isa_t的結構簡化爲如下形式:

//uintptr_t其實就是unsigned long類型,佔8個字節
typedef unsigned long uintptr_t;
union isa_t {
    uintptr_t bits;
    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
    };
}
複製代碼

isa_t位域存放信息類型

isa_t做爲共用體,內部使用8個字節的內存空間,共64位二進制位,存放了如下信息

  • nonpointer表明是不是優化過的isa指針,佔用1位。
    • 1:表示新版本isa指針,使用位域來存儲信息
    • 0:舊版本普通的isa指針,直接存儲Class和Mata-Class的內存地址
  • has_assoc表明是否有關聯對象,佔用1位,一旦設置過關聯對象,則會置爲1。若是添加過關聯對象,在釋放時會檢測是否有關聯對象,因此釋放會更慢。
  • has_cxx_dtor表明是否實現了C++的析構函數(.cxx_destruct),若是沒有,釋放時的速度會更快。佔用1位
  • shiftcls中存放着類或者元類的內存地址,佔用33位。
  • magic是調試時用來判斷對象是否初始化文采,佔用6位
  • weakly_referenced表明是否被弱引用指向過,佔用1位,若是爲0,則釋放時速度會更快
  • deallocating用來表示對象是否正在釋放,佔用1位
  • extra_rc用來存儲引用計數的值,佔用19位,此處須要注意的時,它存儲的是引用計數的值-1。若是對象的引用計數爲1,則extra_rc中存儲的值爲0
  • has_sidetable_rc用來表示是否將引用計數存儲在SiteTable中,引用計數的值過大,在extra_rc沒法存儲,則會將引用計數存放到SiteTable當中。

在函數objc_destructInstance中,咱們能夠區分出在什麼狀況下對象釋放會更快

//釋放一個實例對象
void *objc_destructInstance(id obj) {
    if (obj) {
        //判斷是否有.cxx_destruct析構函數
        bool cxx = obj->hasCxxDtor();
        //判斷是否有關聯對象
        bool assoc = obj->hasAssociatedObjects();

        //g若是有.cxx_destruct析構函數,則調用此析構函數,佔用部分時間
        if (cxx) object_cxxDestruct(obj);
        //若是有關聯對象,則移除關聯對象,佔用部分時間
        if (assoc) _object_remove_assocations(obj);
        //釋放對象
        obj->clearDeallocating();
    }
    return obj;
}
複製代碼

整個isa_t的內存結構圖以下

經過Demo查看isa的具體內存結構

  • 建立Demo,建立NSObject的實例對象
- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSObject *obj = [[NSObject alloc] init];
    __weak typeof(obj) weakObj = obj;
    objc_setAssociatedObject(obj, @"person", @"Jack", OBJC_ASSOCIATION_COPY_NONATOMIC);
}
複製代碼
  • 添加斷點,經過LLDB的p指令,查看obj的isa指針的內存地址,此處須要使用真機來調試,由於咱們主要研究arm64架構下的isa指針

  • 將obj的isa指針的內存地址0x000005a1ce6a7eb3轉換成二進制能夠獲得以下結構

  • 由於當前是新版的isa指針類型,因此nonpointer的值爲1。同時設置了關聯對象,因此has_assoc值爲1。obj對象有弱指針引用,因此weakly_referenced值爲1。

Class底層結構分析

Objective-C基礎之一(深刻理解OC對象)中,咱們瞭解到,Class實際上是一個objc_class類型的結構體,而且它繼承自結構體objc_object,在結構體objc_object的內部則有一個isa_t類型的指針isa,用來存放類對象和元類對象內存地址等一系列信息,上文中有明確說明。接下來咱們再次經過閱讀源碼來深刻理解Class的底層結構。

  • objc_class因爲是繼承自結構體objc_object,因此它的結構咱們能夠簡化,以下:
struct objc_class{
    Class ISA;                  //isa指針,經過位域存放多個信息
    Class superclass;           //supperClass
    cache_t cache;             // 方法緩存
    class_data_bits_t bits;    // 用來獲取類的具體信息
}
複製代碼
  • objc_class中除了有isa指針外,還保存了父類的class,方法緩存以及當前類的一些基本信息。繼續查看class_data_bits_t的源碼以下,此處只展現主要方法
struct class_data_bits_t {
    uintptr_t bits;
    //經過data函數能夠獲取當前結構體中class_rw_t類型的結構體成員
    class_rw_t* data() {
        //經過按位與來獲取到class_rw_t的內存地址
        return (class_rw_t *)(bits & FAST_DATA_MASK);
    }
    
    //經過safe_ro函數獲取到class_ro_t類型結構體
    const class_ro_t *safe_ro() {
        //首先經過data函數獲取到class_rw_t
        class_rw_t *maybe_rw = data();
        //使用class_rw_t中的flags進行按位與運算,判斷當前data返回的是不是被實現的class_rw_t
        if (maybe_rw->flags & RW_REALIZED) {
            //當前是class_rw_t
            return maybe_rw->ro;
        } else {
            //當前是class_ro_t
            return (class_ro_t *)maybe_rw;
        }
    }
}
複製代碼

class_data_bits_t中經過bits & FAST_DATA_MASK來獲取到class_rw_t的內存地址,可是經過safe_ro函數能夠看出,其實一開始在在class_data_bits_t中是不存在class_rw_t的,而是存放的class_ro_tclass_rw_t則是在以後進行建立的,具體會在下文中說明。

查看了class_rw_t和class_ro_t發現二者結構並不相同,可是由於在class_rw_t和class_ro_t中都有flags,而且都是第一個成員變量,所以不論是class_rw_t和class_ro_t它們的內存地址其實就是flags的內存地址,因此二者能夠經過強制轉換來拿到結構體中的flags。

class_rw_t

查看class_rw_t的源碼,發如今class_rw_t存在一個成員變量class_ro_t,以及方法列表,屬性列表和協議列表

struct class_rw_t {
    uint32_t flags;             //用來存放類的一些基本信息
    uint32_t version;           //版本號

    const class_ro_t *ro;       //class_ro_t類型指針

    method_array_t methods;     //方法列表
    property_array_t properties;//屬性列表
    protocol_array_t protocols; //協議列表
}
複製代碼

class_rw_t中的方法列表、屬性列表和協議列表其實都是二維數組,以method_array_t結構爲例,能夠發如今方法列表中其實存放的是method_list_t,而在method_list_t中存放的則是method_tmethod_t中則存放了咱們所須要的方法的基本信息。

class_rw_t結構圖以下

class_ro_t

  • 查看class_ro_t的源碼,發如今class_ro_t中也有方法列表、屬性列表和協議列表
struct class_ro_t {
    uint32_t flags;                 //存放類的一些基本信息
    uint32_t instanceStart;
    uint32_t instanceSize;          //實例對象佔用內存
    const char * name;              //當前類名
    method_list_t * baseMethodList; //方法列表
    protocol_list_t * baseProtocols;//協議列表
    const ivar_list_t * ivars;      //成員變量列表
    property_list_t *baseProperties;//屬性列表
}
複製代碼

class_ro_t中,方法列表、屬性列表和協議列表都是一維數組,分別是method_list_tproperty_list_tprotocol_list_t

class_ro_t結構圖以下:

class_rw_t和class_ro_t的區別

根據上文中class_rw_tclass_ro_t的結構,咱們能夠獲得class_rw_t的完整結構圖以下

  • 首先,在class_rw_t中的二維數組methods、properties、protocols是可讀可寫的,它包含了類的初始內容,分類的內容。
  • class_ro_t中的一維數組baseMethodList、baseProtocols、ivars、baseProperties是隻讀的,它包含了類初始的內容,而且在編譯完成以後就決定了,在運行時是沒法進行修改的。
  • class_rw_t中的二維數組,包含了class_ro_t中一維數組的內容,以methons爲例,methods做爲二維數組,內部存放了不少的methon_list_t,而在methon_list_t中,則存放了具體的方法信息methon_t。可是無論methods中有多少methon_list_t,它的最後一個元素永遠保存的是class_ro_t中的baseMethodList,這一點會經過閱讀源碼來進行驗證。這也是爲何class_rw_t可讀可寫的緣由。並且在methods中,一樣也保存了全部的Category所包含的方法。每個Category都對應一個methon_list_t,並且Category的方法列表存放在數組的最前面。這一點在Objective-C基礎之三(深刻理解Category)中也有詳細說明。

源碼解析

上文提到,在類初始化的時候其實class中保存的是class_ro_t而不是class_rw_t,這一點能夠經過objc-runtime-new.mm中的realizeClassWithoutSwift函數能夠看出

static Class realizeClassWithoutSwift(Class cls){
    const class_ro_t *ro;
    class_rw_t *rw;
    Class supercls;
    Class metacls;
    bool isMeta;
    
    if (!cls) return nil;
    //若是class已經初始化,則直接返回當前class
    if (cls->isRealized()) return cls;
    assert(cls == remapClass(cls));
    //首先經過class的data()函數取到class中bits中存放的class_ro_t
    ro = (const class_ro_t *)cls->data();
    if (ro->flags & RO_FUTURE) {
        //若是當前的cls是future class,而且rw已經被建立,則直接拿到rw和rw中的ro
        rw = cls->data();
        ro = cls->data()->ro;
        cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);
    } else {
        //若是是普通的class,建立rw
        rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);
        //將ro賦值給rw中的ro
        rw->ro = ro;
        //設置rw的flags
        rw->flags = RW_REALIZED|RW_REALIZING;
        //將rw設置到cls中的bits中去
        cls->setData(rw);
    }
    
    ......
    
    //遞歸初始化父類
    supercls = realizeClassWithoutSwift(remapClass(cls->superclass));
    //遞歸初始化元類,經過isa指針來獲取到cls的元類
    metacls = realizeClassWithoutSwift(remapClass(cls->ISA()));
    
    ......
    
    //修改rw中的方法列表,屬性列表和協議列表,而且將分類中的方法列表,屬性列表和協議列表附加到rw中去
    methodizeClass(cls);
}

複製代碼

在類初始化時,cls經過data()函數獲取到的實際上是class_ro_t,內部存放了類初始的方法列表、屬性列表和協議列表。若是當前的cls是普通的class,則經過calloc函數建立rw(class_rw_t),而後將rw中的ro指針指向原始的ro(class_ro_t),以後重置rw中的flags,而且將rw的內存地址保存到cls中的bits中去。而且,realizeClassWithoutSwift中首先是經過遞歸來初始化當前父類以及元類。最後才初始化當前類的。

建立完rw(class_rw_t)以後,則會從新整理cls中的方法列表、屬性列表和協議列表。具體methodizeClass函數源碼以下:

static void methodizeClass(Class cls){
    bool isMeta = cls->isMetaClass();
    auto rw = cls->data();
    auto ro = rw->ro;
    // 從ro中拿到baseMethodList
    method_list_t *list = ro->baseMethods();
    if (list) {
        prepareMethodLists(cls, &list, 1, YES, isBundleClass(cls));
        //將baseMethodList附加到rw的methods中去
        rw->methods.attachLists(&list, 1);
    }
    // 從ro中拿到baseProperties
    property_list_t *proplist = ro->baseProperties;
    if (proplist) {
        //將baseProperties附加到rw的properties中去
        rw->properties.attachLists(&proplist, 1);
    }
    //從ro中拿到baseProtocols
    protocol_list_t *protolist = ro->baseProtocols;
    if (protolist) {
        //將baseProtocols附加到rw的protocols中去
        rw->protocols.attachLists(&protolist, 1);
    }

    //最後將全部Category的方法列表、屬性列表和協議列表附加到cls
    category_list *cats = unattachedCategoriesForClass(cls, true /*realizing*/);
    attachCategories(cls, cats, false /*don't flush caches*/); } 複製代碼

methodizeClass函數中首先會拿到ro中的方法列表,屬性列表和協議列表,而後將拿到的方法列表,屬性列表和協議列表經過對應的attachLists函數附加到rw中的二維數組中去。

void attachLists(List* const * addedLists, uint32_t addedCount) {
    if (addedCount == 0) return;
    //這裏以方法列表爲例
    //array()->lists表示原來類中的方法列表
    //addedLists表示全部Category中的方法列表
    if (hasArray()) {
        //獲取原來類中方法列表的長度
        uint32_t oldCount = array()->count;
        //獲得方法合併以後的新的數組長度
        uint32_t newCount = oldCount + addedCount;
        //給array從新分配長度爲newCount的內存空間
        setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
        array()->count = newCount;
        //將原來array()->lists中的數據移動到數組中oldCount的位置
        //也就是至關於將array()->lists的數據在內存中日後移動了addedCount個位置
        memmove(array()->lists + addedCount, array()->lists,
                oldCount * sizeof(array()->lists[0]));
        //將Category中的方法列表copy到array()->lists中
        //而且是從數組的起始地址開始存放
        memcpy(array()->lists, addedLists,
               addedCount * sizeof(array()->lists[0]));
    }
    else if (!list  &&  addedCount == 1) {
        // 0 lists -> 1 list
        list = addedLists[0];
    }
    else {
        // 1 list -> many lists
        List* oldList = list;
        uint32_t oldCount = oldList ? 1 : 0;
        uint32_t newCount = oldCount + addedCount;
        setArray((array_t *)malloc(array_t::byteSize(newCount)));
        array()->count = newCount;
        if (oldList) array()->lists[addedCount] = oldList;
        memcpy(array()->lists, addedLists,
               addedCount * sizeof(array()->lists[0]));
    }
複製代碼

安裝完類自己實現的方法、屬性和協議以後,會繼續經過attachCategories函數拿到class的全部Category中的方法、屬性和協議列表,而後調用attachLists函數附加到rw中的二維數組中去

//將方法列表、屬性列表、協議列表附加到類中去
//假設cats中的全部的類別都是按順序進行加載和排序的,最先裝載進內存的類別是第一個
static void 
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
    if (!cats) return;
    if (PrintReplacedMethods) printReplacements(cls, cats);
    //用來判斷是不是元類
    bool isMeta = cls->isMetaClass();

    //申請連續內存空間,建立一個二維數組,裏面存放着全部的method_list_t
    method_list_t **mlists = (method_list_t **)
        malloc(cats->count * sizeof(*mlists));
    //申請連續內存空間,建立一個二維數組,裏面存放着全部的property_list_t
    property_list_t **proplists = (property_list_t **)
        malloc(cats->count * sizeof(*proplists));
    //申請連續內存空間,建立一個二維數組,裏面存放着全部的protocol_list_t
    protocol_list_t **protolists = (protocol_list_t **)
        malloc(cats->count * sizeof(*protolists));

    // Count backwards through cats to get newest categories first
    int mcount = 0;
    int propcount = 0;
    int protocount = 0;
    int i = cats->count;
    bool fromBundle = NO;
    //獲取到category_list以後,經過逆序遍從來取出Category內部的方法、屬性和協議列表
    while (i--) {
        auto& entry = cats->list[i];
        //遍歷cls全部的category_t,將category_t中的method_list_t取出,存放到二維數組mlists中
        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        if (mlist) {
            mlists[mcount++] = mlist;
            fromBundle |= entry.hi->isBundle();
        }
        // 將category_t中的property_list_t取出,存放到二維數組proplists中
        property_list_t *proplist = 
            entry.cat->propertiesForMeta(isMeta, entry.hi);
        if (proplist) {
            proplists[propcount++] = proplist;
        }
        //將category_t中的protocol_list_t取出,存放到二維數組protolists中
        protocol_list_t *protolist = entry.cat->protocols;
        if (protolist) {
            protolists[protocount++] = protolist;
        }
    }
    //拿到類對象cls的class_rw_t類型的成員data,它是可讀可寫的
    auto rw = cls->data();

    prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
    //將方法列表合併到rw的方法列表中去,而且插入到表頭位置
    rw->methods.attachLists(mlists, mcount);
    free(mlists);
    if (flush_caches  &&  mcount > 0) flushCaches(cls);
    //將屬性列表合併到rw的屬性列表中去,而且插入到表頭位置
    rw->properties.attachLists(proplists, propcount);
    free(proplists);
    //將協議列表合併到rw的協議列表中去,而且插入到表頭位置
    rw->protocols.attachLists(protolists, protocount);
    free(protolists);
}
複製代碼

具體的Category的附加操做在Objective-C基礎之三(深刻理解Category)中有詳細的說明。

由於是先附加的類自己實現的方法、屬性和協議,以後才附加的Category的方法、屬性和協議,而且attachLists操做從數組的頭部開始進行附加,因此先執行附加操做的方法、屬性和協議會放在數組的後面,所以上文中類自己實現的方法、屬性和協議確定存放在rw二維數組中的最後一個元素。

objc_class中方法緩存cache的做用

method_t

在瞭解方法緩存做用以前,先要了解方法底層是如何進行存儲的。OC中方法都是以method_t的形式存儲

//IMP其實就是函數的具體實現
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...); 
using MethodListIMP = IMP;

struct method_t {
    SEL name;           //方法名稱
    const char *types;  //方法的返回值和參數類型
    MethodListIMP imp;  //函數地址(指向函數的指針)
};
複製代碼

SEL

SEL表明着方法的名稱,也叫做方法選擇器,和c語言的char *結構相似,具體的定義以下

typedef struct objc_selector *SEL;
複製代碼

能夠經過@selector()函數和sel_registerName()函數獲取到對應的SEL

SEL res = sel_registerName("test:");
SEL res1 = @selector(test:);
複製代碼

能夠經過sel_getName()函數和NSStringFromSelector()方法來說SEL轉換成對應的字符串

SEL res = sel_registerName("test:");
SEL res1 = @selector(test:);
NSLog(@"%s %@", sel_getName(res), NSStringFromSelector(res));
複製代碼

其實不一樣類中若是定義了相同的方法,那麼經過@selector()函數和sel_registerName()獲取到的方法選擇器是同一個,在內存中只存在一份。

SEL res = sel_registerName("test:");
SEL res1 = @selector(test:);
//獲取方法選擇器的內存地址
NSLog(@"%p %p", res, res1);
複製代碼

type Encoding

types表示方法的返回值類型和參數類型,也是一個char *類型的字符串,這裏建立XLStudent,在XLStudent建立test方法以下:

@interface XLStudent : NSObject

- (int)test:(int)age height:(int)height;

@end
複製代碼

在OC中,每一個方法其實都有兩個默認的參數,id類型的self和SEL類型的_cmd,因此test方法本質上是如下的結構

int test(id self, SEL _cmd, int age, int height);
複製代碼

而後經過runtime的函數能夠獲取當前test方法的Encoding

XLStudent *student = [[XLStudent alloc] init];
Method method = class_getInstanceMethod([student class], @selector(test:height:));
NSLog(@"%s", method_getTypeEncoding(method));
複製代碼

最後獲得對應的types就是i24@0:8i16i20。其中每一位的含義以下

code meaning
i 表明返回值類型爲int
24 表明全部參數所佔內存大小爲24個字節
@ 表明方法的第一個參數是id類型
0 表明第一個參數地址從0開始
: 表明第二個參數是一個方法選擇器(SEL)
8 表明第二個參數地址從8開始,佔8個字節
i 表明第三個參數是int類型
16 表明第三個參數地址從16開始,佔4個字節
i 表明第四個參數是int類型
20 表明第四個參數地址從20開始,佔4個字節

在iOS中提供了一個@encode指令來獲取具體的類型所對應的字符串編碼

NSLog(@"%s", @encode(int));//運行結果爲 i
NSLog(@"%s", @encode(char));//運行結果爲 c
NSLog(@"%s", @encode(id));//運行結果爲 @
複製代碼

完整的Type Encoding列表以下

code meaning
c char
i int
s short
l long
q long long
c unsigned char
I unsigned int
S unsigned short
L unsigned long
Q unsigned long long
f float
d double
B C++ bool or C99 _Bool
v void
* A character string(char *)
@ An object(whether statically typed or typed id)
# class object(Class)
: method selecter(SEL)
[array type] An Array
{name=type...} A structure
{name=type...} A union
bnum A bit field of num bits
^type A pointer to type
? An unknown type

方法緩存

cache_t

上文中提到,在Class內部有個方法緩存cache_t,它的內部結構以下

struct cache_t {
    struct bucket_t *_buckets;  //散列表
    mask_t _mask;               //散列表的長度 - 1
    mask_t _occupied;           //已經緩存的方法數量
public:
    mask_t mask();              //獲取當前_mask的值
    mask_t occupied();          //獲取_occupied的值
    mask_t capacity();          //獲取當前散列表的容量,也就是_mask + 1
    struct bucket_t * find(SEL sel, id receiver);   //以sel爲key到散列表中查找對應的bucket_t
}
複製代碼

cache_t內部主要有3個成員變量。

  • _buckets是一個散列表(哈希表),內部存儲了多個bucket_t,bucket_t的內部結構以下
struct bucket_t {
private:
    // IMP-first is better for arm64e ptrauth and no worse for arm64.
    // SEL-first is better for armv7* and i386 and x86_64.
#if __arm64__
    uintptr_t _imp; //存放了函數的內存地址  (在最新版本的源碼中,_imp不是直接存放函數內存地址)
    SEL _sel;       //方法選擇器SEL的地址做爲key
#else
    SEL _sel;
    uintptr_t _imp;
#endif
}
複製代碼
  • _mask是散列表的長度-1
  • _occupied則是表明已經緩存的方法數量,也就是哈希表中已經存放的方法個數

散列表(哈希表)

上文中所說的散列表其實就是相似一個數組,在散列表中存放着對應的bucket_t,具體的散列表的結構以下圖

有了散列表,接下來就是如何計算出索引,而後向指定的位置添加bucket_t,在cache_t中有個find函數,就是用來根據索引查找到指定的bucket_t的,以下

bucket_t * cache_t::find(SEL s, id receiver)
{
    assert(s != 0);
    //拿到整個散列表
    bucket_t *b = buckets();
    //拿到散列表的_mask
    mask_t m = mask();
    //根據SEL計算出開始的索引地址
    mask_t begin = cache_hash(s, m);
    mask_t i = begin;
    do {
        //若是經過begin索引找到的bucket_t中的SEL和參數中的SEL相等,則直接返回bucket_t的地址
        if (b[i].sel() == 0  ||  b[i].sel() == s) {
            return &b[i];
        }
        //若是begin位置的bucket_t不是咱們要找的,則將begin+1,繼續查找下一個索引地址,直到找到爲止
    } while ((i = cache_next(i, m)) != begin);

    // hack
    Class cls = (Class)((uintptr_t)this - offsetof(objc_class, cache));
    cache_t::bad_cache(receiver, (SEL)s, cls);
}
複製代碼
  • 首先,會調用cache_hash函數,經過sel和_mask進行按位與運算獲得初始的索引begin
static inline mask_t cache_hash(SEL sel, mask_t mask) 
{
    return (mask_t)(uintptr_t)sel & mask;
}
複製代碼
  • 而後拿到begin位置的bucket_t,經過sel()函數獲取到SEL,與參數中的sel進行比較,若是相同,則直接返回bucket_t的內存地址,若是不一樣,則將begin+1,繼續經過按位與算出下一個索引繼續進行比較,直到找到對應的bucket_t爲止。
static inline mask_t cache_next(mask_t i, mask_t mask) {
    return (i+1) & mask;
}
複製代碼
  • 插入操做其實和查詢操做相同,也是先計算出起始的索引地址begin,若是當前索引地址有值,則將begin+1,再次計算出下一個索引地址,而後繼續進行判斷,直到找到能夠插入的位置。散列表會有一個初始的長度,若是整個散列表元素大於散列表總長度的3/4的話,會自動進行擴容操做,擴容爲原來的2倍,於此同時會將整個散列表清空,而後修改_mask和_occupied的值。

有一點要注意的是:cache_t中的_mask爲何要存放散列表長度-1?是由於經過SEL & _mask運算獲得的值永遠會小於等於_mask,也就是說(SEL & _mask) <= _mask。所以只有_mask的值爲散列表的長度-1才能保證不會產生數組越界。

方法查找流程

模擬散列表

首先,結合方法緩存,再來梳理一下iOS的方法調用流程

  1. 調用對象方法時,首先經過實例對象的isa指針找到對應的類對象,而後在類對象的散列表cache中根據SEL查找對應的方法,若是找到方法,則執行。若是未找到,則執行第二步。
  2. 在類對象的方法列表中查找方法,若是找到,則執行該方法,而後將該方法保存到當前類對象的方法緩存中,以便下次調用同一方法時能從緩存中調用。若是未找到,則執行第三步。
  3. 經過類對象的superClass指針找到父類對象,而後到父類對象的方法緩存中去查找,若是找到,則執行該方法,而且將該方法存放到當前類對象的方法緩存中去(注意,此處是當前類而不是它的父類對象),若是未找到則執行第四步
  4. 在父類對象的方法列表中去查找方法,若是找到,則執行該方法,而後將該方法保存到當前類對象的方法緩存中(注意,此處是當前類而不是它的父類對象)。若是未找到則重複執行第三步,直到superClass爲nil。

具體流程圖以下

以上就是方法緩存的完整流程,下面咱們就經過Demo來驗證咱們的結論。要想查看Class的內部結構,就須要對咱們建立的對象進行強制轉換,轉換成對應的結構體,須要用到的轉換工具類以下。建立XLClass.h,而後將如下代碼複製到XLClass.h中去。

//XLClass.h

#ifndef XLClass_h
#define XLClass_h

#if __LP64__
typedef uint32_t mask_t;  // x86_64 & arm64 asm are less efficient with 16-bits
#else
typedef uint16_t mask_t;
#endif
typedef uintptr_t SEL;

struct xl_class_data_bits_t {
    // Values are the FAST_ flags above.
    uintptr_t bits;
};

struct xl_bucket_t {
#if __arm64__
    uintptr_t _imp;
    SEL _sel;
#else
    SEL _sel;
    uintptr_t _imp;
#endif
};

struct xl_cache_t {
    struct xl_bucket_t *_buckets;
    mask_t _mask;
    mask_t _occupied;
};

/* OC對象 */
struct xl_objc_object {
    void *isa;
};

/* 類對象 */
struct xl_objc_class : mj_objc_object {
    Class superclass;
    xl_cache_t cache;
    xl_class_data_bits_t bits;
};


#endif /* XLClass_h */
複製代碼

上述代碼實際上是將源碼中的部分函數和結構體的定義拿出來,從新封裝一下。以後經過強制轉換就可以查看對象的內部結構。此處還須要注意將main.m的後綴改爲main.mm,以便整個項目支持C++編譯。

以後建立XLPerson類,在類中增長以下方法

@interface XLPerson : NSObject

- (void)personMethond1;

@end

@implementation XLPerson

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

@end
複製代碼

而後建立XLPerson類的子類XLTeacher,在XLTeacher中增長如下方法

@interface XLTeacher : XLPerson

- (void)teacherMethond1;
- (void)teacherMethond2;
- (void)teacherMethond3;
- (void)teacherMethond4;
- (void)teacherMethond5;
- (void)teacherMethond6;
- (void)teacherMethond7;
- (void)teacherMethond8;

@end

@implementation XLTeacher

- (instancetype)init
{
    self = [super init];
    if (self) {
        NSLog(@"%s", __func__);
    }
    return self;
}

- (void)teacherMethond1{
    NSLog(@"%s", __func__);
}
- (void)teacherMethond2{
    NSLog(@"%s", __func__);
}
- (void)teacherMethond3{
    NSLog(@"%s", __func__);
}
- (void)teacherMethond4{
    NSLog(@"%s", __func__);
}
- (void)teacherMethond5{
    NSLog(@"%s", __func__);
}
- (void)teacherMethond6{
    NSLog(@"%s", __func__);
}
- (void)teacherMethond7{
    NSLog(@"%s", __func__);
}
- (void)teacherMethond8{
    NSLog(@"%s", __func__);
}

@end
複製代碼

在main函數中建立XLTeacher對象,而後轉換成對應的mj_objc_class結構體

int main(int argc, const char * argv[]) {
    @autoreleasepool {
  
        XLTeacher *teacher = [XLTeacher alloc];
        [teacher teacherMethond1];
//        [teacher personMethond1];
//        [teacher teacherMethond2];
//        [teacher teacherMethond3];
//        [teacher teacherMethond1];
//        [teacher teacherMethond1];
//        [teacher teacherMethond4];
//        [teacher teacherMethond5];
//        [teacher teacherMethond6];
//        [teacher teacherMethond7];
//        [teacher teacherMethond8];
        
        NSLog(@"-------------散列表------------");
        //將XLTeacher轉換成mj_objc_class
        xl_objc_class *teacherClass = (__bridge xl_objc_class *)[teacher class];
        //獲取緩存cache_t
        xl_cache_t cache = teacherClass->cache;
        //拿到緩存中的散列表
        xl_bucket_t *buckets = cache._buckets;
        //打印散列表的內容
        for (int i = 0; i < cache._mask + 1; i++) {
            xl_bucket_t bt = buckets[i];
            NSLog(@"index:%d --- sel:%p --- imp:%lu", i,bt._sel, bt._imp);
        }
        
        NSLog(@"111");
    }
    return 0;
}
複製代碼
  • 首先只執行方法teacherMethond1,散列表的內容以下
2020-01-08 14:28:25.583428+0800 Test[652:4619102] -[XLTeacher teacherMethond1]
2020-01-08 14:28:25.583919+0800 Test[652:4619102] -------------散列表------------
2020-01-08 14:28:25.583989+0800 Test[652:4619102] index:0 --- sel:0x0 --- imp:0
2020-01-08 14:28:25.584024+0800 Test[652:4619102] index:1 --- sel:0x100001e49 --- imp:0
2020-01-08 14:28:25.584049+0800 Test[652:4619102] index:2 --- sel:0x0 --- imp:11928
2020-01-08 14:28:25.584068+0800 Test[652:4619102] index:3 --- sel:0x1 --- imp:4301329584
複製代碼

其中0x100001e49表示@selector(teacherMethond1)選擇器的內存地址,經過如下方式打印出@selector(teacherMethond1)的內存地址,發現也是0x100001e49,所以就能夠判定teacherMethond1方法被存放到了散列表索引爲1的位置。

SEL method1 = @selector(teacherMethond1);
NSLog(@"%p",method1);
複製代碼
  • 修改代碼,同時執行teacherMethond1teacherMethond2,再次查看散列表,發現teacherMethond2方法被緩存到了散列表索引爲1的位置,@selector(teacherMethond2)地址爲0x100001e59
2020-01-08 14:54:23.713899+0800 Test[1383:4643955] -[XLTeacher teacherMethond1]
2020-01-08 14:54:23.714313+0800 Test[1383:4643955] -[XLTeacher teacherMethond2]
2020-01-08 14:54:23.714427+0800 Test[1383:4643955] -------------散列表------------
2020-01-08 14:54:23.714484+0800 Test[1383:4643955] index:0 --- sel:0x0 --- imp:0
2020-01-08 14:54:23.714525+0800 Test[1383:4643955] index:1 --- sel:0x100001e49 --- imp:11976
2020-01-08 14:54:23.714557+0800 Test[1383:4643955] index:2 --- sel:0x100001e59 --- imp:12024
2020-01-08 14:54:23.714587+0800 Test[1383:4643955] index:3 --- sel:0x1 --- imp:4345415488
複製代碼
  • 繼續修改代碼,同時執行方法teacherMethond1teacherMethond2teacherMethond3,這個時候發現,整個散列表進行了擴容,長度從4擴容到了8,而且散列表中只剩下了teacherMethond3,以前的teacherMethond1teacherMethond2都被清空了
2020-01-08 14:54:53.400791+0800 Test[1399:4644780] -[XLTeacher teacherMethond1]
2020-01-08 14:54:53.401206+0800 Test[1399:4644780] -[XLTeacher teacherMethond2]
2020-01-08 14:54:53.401433+0800 Test[1399:4644780] -[XLTeacher teacherMethond3]
2020-01-08 14:54:53.401527+0800 Test[1399:4644780] -------------散列表------------
2020-01-08 14:54:53.401579+0800 Test[1399:4644780] index:0 --- sel:0x0 --- imp:0
2020-01-08 14:54:53.401613+0800 Test[1399:4644780] index:1 --- sel:0x100001e69 --- imp:11952
2020-01-08 14:54:53.401679+0800 Test[1399:4644780] index:2 --- sel:0x0 --- imp:0
2020-01-08 14:54:53.401705+0800 Test[1399:4644780] index:3 --- sel:0x0 --- imp:0
2020-01-08 14:54:53.401736+0800 Test[1399:4644780] index:4 --- sel:0x0 --- imp:0
2020-01-08 14:54:53.401765+0800 Test[1399:4644780] index:5 --- sel:0x0 --- imp:0
2020-01-08 14:54:53.401792+0800 Test[1399:4644780] index:6 --- sel:0x0 --- imp:0
2020-01-08 14:54:53.401818+0800 Test[1399:4644780] index:7 --- sel:0x1 --- imp:4301259200
複製代碼
  • 繼續執行完teacherMethond1teacherMethond8等8個方法,發現當執行teacherMethond8時,散列表又進行了一次擴容,長度從8擴容到了16,而且散列表中只剩下了方法teacherMethond8,以前的方法都被清空,所以能夠得出結論:當散列表的容量超過3/4時,散列表會進行一次擴容,而且會清空整個散列表。這一點其實在cache_fill_nolock函數中也能找到對應的源碼
static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)
{
    cacheUpdateLock.assertLocked();
    if (!cls->isInitialized()) return;
    if (cache_getImp(cls, sel)) return;

    cache_t *cache = getCache(cls);

    // Use the cache as-is if it is less than 3/4 full
    mask_t newOccupied = cache->occupied() + 1;
    mask_t capacity = cache->capacity();
    if (cache->isConstantEmptyCache()) {
        // Cache is read-only. Replace it.
        cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);
    }
    else if (newOccupied <= capacity / 4 * 3) {
        // 若是散列表中,當前已緩存的方法數量+1小於等於總長度的3/4,則繼續使用當前散列表
        // Cache is less than 3/4 full. Use it as-is.
    }
    else {
        // 若是散列表中,當前已緩存的方法數量+1大於總長度的3/4,則對當前散列表進行擴容
        cache->expand();
    }

    //散列表的最小長度爲4
    bucket_t *bucket = cache->find(sel, receiver);
    if (bucket->sel() == 0) cache->incrementOccupied();
    bucket->set<Atomic>(sel, imp);
}
複製代碼

散列表的最小長度爲4,若是散列表中已緩存的方法數量+1大於散列表長度的3/4,則調用expand函數對散列表進行擴容,容量擴大爲原來容量的2倍

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) {
        newCapacity = oldCapacity;
    }
    //從新分配內存
    reallocate(oldCapacity, newCapacity);
}
複製代碼
  • 修改代碼,只調用teacherMethond1和父類方法personMethond1,發現父類的方法也在散列表中,由此就證實了以前的結論:若是當子類中沒有找到對應方法,會到父類中查找,若是找到,會將父類的方法緩存到子類的cache中去。
2020-01-08 14:53:04.754352+0800 Test[1353:4642077] -[XLTeacher teacherMethond1]
2020-01-08 14:53:04.754734+0800 Test[1353:4642077] -[XLPerson personMethond1]
2020-01-08 14:53:04.754823+0800 Test[1353:4642077] -------------散列表------------
2020-01-08 14:53:04.754885+0800 Test[1353:4642077] index:0 --- sel:0x0 --- imp:0
2020-01-08 14:53:04.754921+0800 Test[1353:4642077] index:1 --- sel:0x100001e49 --- imp:11976
2020-01-08 14:53:04.754951+0800 Test[1353:4642077] index:2 --- sel:0x100001e31 --- imp:11384
2020-01-08 14:53:04.754979+0800 Test[1353:4642077] index:3 --- sel:0x1 --- imp:4301540624
複製代碼
  • bucket_t的最新核心源碼以下,_sel是方法選擇器的地址,用來進行散列表的索引值的計算,而_imp則存放了方法的具體內存地址,可是直接拿到_imp的值是沒法拿到具體的方法地址的,還須要調用trauth_auth_and_resign_imp指針進行身份驗證,而且從新分配它,最終才能獲得真實的方法內存地址。這一點和舊版的Api有所區別。
struct bucket_t {
private:
#if __arm64__
    uintptr_t _imp;
    SEL _sel;
#else
    SEL _sel;
    uintptr_t _imp;
#endif

public:
    inline SEL sel() const { return _sel; }

    inline IMP imp() const {
        if (!_imp) return nil;
        return (IMP)
            ptrauth_auth_and_resign((const void *)_imp,
                                    ptrauth_key_process_dependent_code,
                                    modifierForSEL(_sel),
                                    ptrauth_key_function_pointer, 0);
    }
複製代碼

未解決的問題

  • 在上述Demo中,方法緩存cache_t中有一個成員_occupied,從源碼上看,_occupied存放的是散列表中已緩存方法的數量。可是在Demo中,_occupied的值打印出來一直和散列表已緩存方法數量不匹配。
  • 在散列表中,最後一個索引的元素一直存放的是當前bucket_t *_buckets的內存地址,至於爲何這樣作還有待考證。

以上兩個問題尚未找到具體的解釋,若是有知道的同窗,歡迎不吝賜教。

結束語

以上內容純屬我的理解,若是有什麼不對的地方歡迎留言指正。

一塊兒學習,一塊兒進步~~~

相關文章
相關標籤/搜索