OC源碼分析之類的結構解讀

前言

想要成爲一名iOS開發高手,免不了閱讀源碼。如下是筆者在OC源碼探索中梳理的一個小系列——類與對象篇,歡迎你們閱讀指正,同時也但願對你們有所幫助。git

  1. OC源碼分析之對象的建立
  2. OC源碼分析之isa
  3. OC源碼分析之類的結構解讀
  4. 未完待續...

1. 類的結構

若是你使用過Objective-C(簡稱OC)這門語言開發過應用程序,你必定對NSObject不陌生。OC裏面有兩個NSObject,一個是咱們熟知的NSObject類,另外一個是NSObject協議。協議相似於其餘面嚮對象語言(如JavaC++)的接口,NSObject協議裏面定義了一些屬性和方法,但自己並未實現,而NSObject類遵循了NSObject協議,因此NSObject類實現了這些方法。github

咱們跟NSObject類打了那麼多交道,卻不必定對它瞭如指掌,今天筆者將帶你們對NSObject類的結構進行全方位的解讀。swift

注意:api

  • 本文筆者用的全部源碼都是基於蘋果開源的objc4-756.2源碼,文末會附上github的地址。
  • 本文采用的是x86_64CPU架構,跟arm64差異不大,有區別的地方會註明的。

1.1 解讀類的本質

NSObject類的定義開始緩存

OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0)
OBJC_ROOT_CLASS
OBJC_EXPORT
@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
    Class isa  OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}
複製代碼

能夠看到NSObject有個Class類型的isa成員變量,這裏你們留意一下。架構

接下來用Clang編譯main.m,輸出.cpp文件,看一下NSObject類的底層定義app

clang -rewrite-objc main.m -o main.cpp
複製代碼

打開main.cpp文件,找到了NSObjectless

#ifndef _REWRITER_typedef_NSObject
#define _REWRITER_typedef_NSObject
typedef struct objc_object NSObject;
typedef struct {} _objc_exc_NSObject;
#endif

struct NSObject_IMPL {
	Class isa;
};
複製代碼

發現NSObject類本質上是objc_object結構體,同時有定義一個NSObject_IMPL結構體(IMPLimplementation的縮寫),裏面有NSObject類isa成員變量(對應於OC時的NSObject類定義中的isa成員變量)。ide

此時,筆者特別好奇咱們本身定義的類經Clang編譯後是什麼樣子,索性看一下吧函數

@interface Person : NSObject

@property (nonatomic) NSInteger age;

- (void)run;

@end

@implementation Person

- (void)run {
    NSLog(@"I am running.");
}

@end
複製代碼

一個簡單的Person類,有個age屬性和run方法,編譯後就是

#ifndef _REWRITER_typedef_Person
#define _REWRITER_typedef_Person
typedef struct objc_object Person;
typedef struct {} _objc_exc_Person;
#endif

extern "C" unsigned long OBJC_IVAR_$_Person$_age;
struct Person_IMPL {
	struct NSObject_IMPL NSObject_IVARS;
	NSInteger _age;
};

static void _I_Person_run(Person * self, SEL _cmd) {
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_mc_9fhhprrj4k92vxzqm3g127z40000gn_T_main_09fc70_mi_0);
}

static NSInteger _I_Person_age(Person * self, SEL _cmd) { return (*(NSInteger *)((char *)self + OBJC_IVAR_$_Person$_age)); }
static void _I_Person_setAge_(Person * self, SEL _cmd, NSInteger age) { (*(NSInteger *)((char *)self + OBJC_IVAR_$_Person$_age)) = age; }
複製代碼

可見,Person類本質一樣是objc_object結構體類型,惟一能體現與NSObject類之間的繼承關係的就是,Person_IMPL結構體內部多了個struct NSObject_IMPL類型的NSObject_IVARS成員變量——即以NSObject爲根類的繼承體系裏的全部類,都有個Class類型的isa成員變量。

1.2 objc_object結構

若是你對isa有所瞭解,或者有讀過筆者的 OC源碼分析之isa 這篇文章,相信必定對objc_object有印象。

這裏就直接上源碼

struct objc_object {
private:
    isa_t isa;
    
    ... // 一些函數
};

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
};
複製代碼
  • 關於isa_t的分析請戳 OC源碼分析之isa ,裏面已經很詳細了,這裏就很少做贅述。
  • objc_object結構體內部的方法有五十個左右,大體可分爲如下幾類
    • 一些關於isa的函數,如initIsa()getIsa()changeIsa()
    • 一些弱引用的函數,如isWeaklyReferenced()setWeaklyReferenced_nolock()
    • 一些內存管理函數,如retain()release()autorelease()
    • 兩個關聯對象函數,分別是hasAssociatedObjects()setHasAssociatedObjects

1.3 Class結構簡介

一樣先上源碼

typedef struct objc_class *Class;
typedef struct objc_object *id;

struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags

    ... // 一些函數
};
複製代碼

從源碼能夠看出,Classobjc_class結構體類型的指針變量,繼承自objc_object結構體。也就是說,Class有4個成員變量,且它們在內存存儲上是有序的,依次分別是:

  1. isa:類型是isa_t64位下長度爲8字節,因爲上篇博文已作過度析,這裏略過;
  2. superclass:類型是Class,表示繼承關係,指向當前類的父類,一樣8字節;
  3. cache:類型是cache_t,表示緩存,用於緩存指針和 vtable,加速方法的調用。其具體結構以下
struct cache_t {
    struct bucket_t *_buckets;  // 64位下是8字節
    mask_t _mask;               // 64位下是4字節
    mask_t _occupied;           // 64位下是4字節

    ... // 一些函數
};

#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 unsigned int uint32_t;
複製代碼

可見,cache這個成員變量長度是16字節。

cache比較重要,關於它的分析筆者將另起一篇博文,這裏暫時擱置。

  1. bits:類型是class_data_bits_t,用於存儲類的數據(類的方法、屬性、遵循的協議等信息),其結構以下
struct class_data_bits_t {

    // Values are the FAST_ flags above.
    uintptr_t bits;     // unsigned long

    ... // 一些函數
};
複製代碼

其長度也是8字節。根據bits成員變量在objc_object結構體中的描述,它實質上是class_rw_t *加上自定義rr/alloc標誌,也就是說,最重要的是class_rw_t——筆者接下來將重點介紹它。

2. class_rw_t & class_ro_t分析

OC類中的屬性、方法還有遵循的協議等信息都保存在class_rw_t,首先看看class_rw_t的結構:

struct class_rw_t {
    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;

#if SUPPORT_INDEXED_ISA
    uint32_t index;
#endif
    ...// 一些函數
};

#if __ARM_ARCH_7K__ >= 2 || (__arm64__ && !__LP64__)
# define SUPPORT_INDEXED_ISA 1
#else
# define SUPPORT_INDEXED_ISA 0
#endif
複製代碼

發現class_rw_t中還有一個被const修飾的指針變量 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;    // 屬性列表

    // This field exists only when RO_HAS_SWIFT_INITIALIZER is set.
    _objc_swiftMetadataInitializer __ptrauth_objc_method_list_imp _swiftMetadataInitializer_NEVER_USE[0];

    ... // 一些函數
};
複製代碼

說明:

其實,rwreadwrite的意思,而ro則是readonly

2.1 獲取class_rw_t

要想獲取class_rw_t指針地址,須要知道objc_classbits指針地址,經過對objc_class的結構分析得知,bits指針地址是objc_class首地址偏移32個字節(isa + superclass + cache = 32字節)

也能夠從源碼得知如何拿到class_rw_t指針

// objc_class結構體中
class_rw_t *data() { 
    return bits.data();     // bits是class_data_bits_t類型
}

// class_data_bits_t結構體中
class_rw_t* data() {
    return (class_rw_t *)(bits & FAST_DATA_MASK);
}

// 64位下
#define FAST_DATA_MASK 0x00007ffffffffff8UL
複製代碼

64位下class_rw_t指針地址是在[3, 46]數據段,因此也能夠用bits & FAST_DATA_MASK計算出class_rw_t指針地址。

接着筆者將經過一個例子來驗證class_rw_tclass_ro_t是否存儲了類的信息

2.2 準備工做

Person類添加屬性、方法和協議,代碼以下

@protocol PersonProtocol <NSObject>

- (void)walk;

@end

@interface Person : NSObject <PersonProtocol> {
    NSInteger _gender;
}

@property (nonatomic) NSString *name;
@property (nonatomic) NSInteger age;

+ (void)printMyClassName;
- (void)run;

@end

@implementation Person

+ (void)printMyClassName {
    NSLog(@"my class name is Person");
}

- (void)run {
    NSLog(@"I am running.");
}

- (void)walk {
    NSLog(@"I am walking.");
}

@end
複製代碼

而後在合適的位置打上斷點

好了,準備工做完成,下面開始驗證

2.3 class_rw_t驗證過程

  1. 打印Person
(lldb) x/5gx pcls
0x100002820: 0x001d8001000027f9 0x0000000100b39140
0x100002830: 0x00000001003dc250 0x0000000000000000
0x100002840: 0x0000000102237404
複製代碼

說明:

  • Person類首地址是0x100002820,所以,0x100002840是其bits地址(32字節就是0x200x100002840 = 0x100002820 + 0x20),bits內容是0x0000000102237404
  • 0x001d8001000027f9Person類的isa地址,指向Person元類
  • 0x0000000100b39140Person類的superclass地址,也就是NSObject類首地址
  • 0x00000001003dc250 0x0000000000000000則是Person類的cache
  1. 打印class_rw_t
// bits & FAST_DATA_MASK
(lldb) p (class_rw_t *)(0x0000000102237404 & 0x00007ffffffffff8)
(class_rw_t *) $1 = 0x0000000102237400
(lldb) p *$1
(class_rw_t) $2 = {
  flags = 2148139008
  version = 0
  ro = 0x0000000100002788
  methods = {
    list_array_tt<method_t, method_list_t> = {
       = {
        list = 0x0000000100002608
        arrayAndFlag = 4294977032
      }
    }
  }
  properties = {
    list_array_tt<property_t, property_list_t> = {
       = {
        list = 0x0000000100002720
        arrayAndFlag = 4294977312
      }
    }
  }
  protocols = {
    list_array_tt<unsigned long, protocol_list_t> = {
       = {
        list = 0x00000001000025a8
        arrayAndFlag = 4294976936
      }
    }
  }
  firstSubclass = nil
  nextSiblingClass = NSUUID
  demangledName = 0x0000000000000000
}
複製代碼

這裏請你們留意一下class_rw_t的幾個關鍵成員變量:

  • ro地址是0x0000000100002788
  • methodslist地址是0x0000000100002608
  • propertieslist地址是0x0000000100002720
  • protocolslist地址是0x0000000100002608
  1. 驗證methods

目前來看,Person類至少有6個實例方法,分別是runwalk以及nameagegettersetter,還有1個類方法,即printMyClassName,總計7個方法。

(lldb) p (method_list_t *)0x0000000100002608    // rw的methods的list地址
(method_list_t *) $7 = 0x0000000100002608
(lldb) p *$7
(method_list_t) $8 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 26
    count = 7
    first = {
      name = "walk"
      types = 0x0000000100001e96 "v16@0:8"
      imp = 0x0000000100001530 (CCTest`-[Person walk] at main.m:45)
    }
  }
}
複製代碼

正好是7個方法,讓咱們看看都是哪些(因爲method_list_t繼承自entsize_list_tt,能夠經過entsize_list_ttget()函數一一打印)

(lldb) p $8.get(0)
(method_t) $9 = {
  name = "walk"
  types = 0x0000000100001e96 "v16@0:8"
  imp = 0x0000000100001530 (CCTest`-[Person walk] at main.m:45)
}
(lldb) p $8.get(1)
(method_t) $10 = {
  name = ".cxx_destruct"
  types = 0x0000000100001e96 "v16@0:8"
  imp = 0x0000000100001600 (CCTest`-[Person .cxx_destruct] at main.m:35)
}
(lldb) p $8.get(2)
(method_t) $11 = {
  name = "name"
  types = 0x0000000100001eb1 "@16@0:8"
  imp = 0x0000000100001560 (CCTest`-[Person name] at main.m:27)
}
(lldb) p $8.get(3)
(method_t) $12 = {
  name = "setName:"
  types = 0x0000000100001f4b "v24@0:8@16"
  imp = 0x0000000100001580 (CCTest`-[Person setName:] at main.m:27)
}
(lldb) p $8.get(4)
(method_t) $13 = {
  name = "age"
  types = 0x0000000100001f56 "q16@0:8"
  imp = 0x00000001000015c0 (CCTest`-[Person age] at main.m:28)
}
(lldb) p $8.get(5)
(method_t) $14 = {
  name = "run"
  types = 0x0000000100001e96 "v16@0:8"
  imp = 0x0000000100001500 (CCTest`-[Person run] at main.m:41)
}
(lldb) p $8.get(6)
(method_t) $15 = {
  name = "setAge:"
  types = 0x0000000100001f5e "v24@0:8q16"
  imp = 0x00000001000015e0 (CCTest`-[Person setAge:] at main.m:28)
}
複製代碼

顯然,class_rw_tmethods確實包含了Person類的所有實例方法,只是多了個.cxx_destruct方法。.cxx_destruct方法本來是爲了C++對象析構的,ARC借用了這個方法插入代碼實現了自動內存釋放的工做,關於其原理這裏略過不提。

思考:類方法printMyClassName哪裏去了?

  1. 驗證properties

同理,Person類至少有nameage這兩個屬性,且看

(lldb) p (property_list_t *)0x0000000100002720  // rw的properties的list地址
(property_list_t *) $18 = 0x0000000100002720
(lldb) p *$18
(property_list_t) $19 = {
  entsize_list_tt<property_t, property_list_t, 0> = {
    entsizeAndFlags = 16
    count = 6
    first = (name = "name", attributes = "T@\"NSString\",&,N,V_name")
  }
}
(lldb) p $19.get(0)
(property_t) $20 = (name = "name", attributes = "T@\"NSString\",&,N,V_name")
(lldb) p $19.get(1)
(property_t) $21 = (name = "age", attributes = "Tq,N,V_age")
(lldb) p $19.get(2)
(property_t) $22 = (name = "hash", attributes = "TQ,R")
(lldb) p $19.get(3)
(property_t) $23 = (name = "superclass", attributes = "T#,R")
(lldb) p $19.get(4)
(property_t) $24 = (name = "description", attributes = "T@\"NSString\",R,C")
(lldb) p $19.get(5)
(property_t) $25 = (name = "debugDescription", attributes = "T@\"NSString\",R,C")
複製代碼

顯然nameage存儲在properties中。

多餘的屬性也不做贅述。

  1. 驗證protocols

在驗證以前,先分析一下protocol_list_t,這個結構體並非繼承自entsize_list_tt的,其結構以下

struct protocol_list_t {
    // count is 64-bit by accident. 
    uintptr_t count;
    protocol_ref_t list[0]; // variable-size

    ... // 一些函數
}
複製代碼

注意到variable-size這個註釋部分(可變大小),彷彿看到了但願

typedef uintptr_t protocol_ref_t;  // protocol_t *, but unremapped

struct protocol_t : objc_object {
    const char *mangledName;
    struct protocol_list_t *protocols;
    method_list_t *instanceMethods;
    method_list_t *classMethods;
    method_list_t *optionalInstanceMethods;
    method_list_t *optionalClassMethods;
    property_list_t *instanceProperties;
    uint32_t size;   // sizeof(protocol_t)
    uint32_t flags;
    // Fields below this point are not always present on disk.
    const char **_extendedMethodTypes;
    const char *_demangledName;
    property_list_t *_classProperties;

    const char *demangledName();

    ... // 一些函數
};
複製代碼

protocol_ref_t雖然未映射成protocol_t *,不過應該能夠考慮一下強轉,實驗一下吧(此次是找到PersonProtocol協議)

(lldb) p (protocol_list_t *)0x00000001000025a8  // rw的protocols的list地址
(protocol_list_t *) $26 = 0x00000001000025a8
(lldb) p *$26
(protocol_list_t) $27 = (count = 1, list = protocol_ref_t [] @ 0x00007fb5decb30f8)

(lldb) p (protocol_t *)$26->list[0]
(protocol_t *) $32 = 0x00000001000028a8
(lldb) p *$32
(protocol_t) $33 = {
  objc_object = {
    isa = {
      cls = Protocol
      bits = 4306735304
       = {
        nonpointer = 0
        has_assoc = 0
        has_cxx_dtor = 0
        shiftcls = 538341913
        magic = 0
        weakly_referenced = 0
        deallocating = 0
        has_sidetable_rc = 0
        extra_rc = 0
      }
    }
  }
  mangledName = 0x0000000100001d16 "PersonProtocol" // 出現了!!!
  protocols = 0x0000000100002568
  instanceMethods = 0x0000000100002580
  classMethods = 0x0000000000000000
  optionalInstanceMethods = 0x0000000000000000
  optionalClassMethods = 0x0000000000000000
  instanceProperties = 0x0000000000000000
  size = 96
  flags = 0
  _extendedMethodTypes = 0x00000001000025a0
  _demangledName = 0x0000000000000000
  _classProperties = 0x0000000000000000
}
複製代碼

功夫不負有心人,最終驗證了class_rw_tprotocols中含有Person類所遵循的PersonProtocol協議。

到了這裏,class_rw_t確實存儲了類的實例方法、屬性和遵循的協議了。

2.4 class_ro_t驗證過程

如今就剩下ro

  1. 打印class_ro_t
(lldb) p $1->ro
(const class_ro_t *) $38 = 0x0000000100002788
(lldb) p *$38
(const class_ro_t) $39 = {
  flags = 388
  instanceStart = 8
  instanceSize = 32
  reserved = 0
  ivarLayout = 0x0000000100001d2e "\x11"
  name = 0x0000000100001d0f "Person"
  baseMethodList = 0x0000000100002608
  baseProtocols = 0x00000001000025a8
  ivars = 0x00000001000026b8
  weakIvarLayout = 0x0000000000000000
  baseProperties = 0x0000000100002720
  _swiftMetadataInitializer_NEVER_USE = {}
}
複製代碼

有沒有發現什麼!class_ro_t的方法、屬性和協議的地址都與class_rw_t的一致,既然指向的是同一塊內存空間,顯然class_ro_t也存儲了Person類的實例方法、屬性和協議

class_rw_t不一樣的是,class_ro_t多了一個ivars列表,裏面存放的應該是Person類的成員變量。

  1. 驗證ivars

Person類的成員變量有:_gender_name_age

所幸ivar_list_t是繼承自entsize_list_tt的,get()函數又能夠用了。

(lldb) p $39.ivars
(const ivar_list_t *const) $40 = 0x00000001000026b8
(lldb) p *$40
(const ivar_list_t) $41 = {
  entsize_list_tt<ivar_t, ivar_list_t, 0> = {
    entsizeAndFlags = 32
    count = 3
    first = {
      offset = 0x00000001000027e0
      name = 0x0000000100001e83 "_gender"
      type = 0x0000000100001f69 "q"
      alignment_raw = 3
      size = 8
    }
  }
}
(lldb) p $41.get(0)
(ivar_t) $42 = {
  offset = 0x00000001000027e0
  name = 0x0000000100001e83 "_gender"
  type = 0x0000000100001f69 "q"
  alignment_raw = 3
  size = 8
}
(lldb) p $41.get(1)
(ivar_t) $43 = {
  offset = 0x00000001000027e8
  name = 0x0000000100001e8b "_name"
  type = 0x0000000100001f6b "@\"NSString\""
  alignment_raw = 3
  size = 8
}
(lldb) p $41.get(2)
(ivar_t) $44 = {
  offset = 0x00000001000027f0
  name = 0x0000000100001e91 "_age"
  type = 0x0000000100001f69 "q"
  alignment_raw = 3
  size = 8
}
複製代碼

徹底符合預期,class_ro_t確實存儲了Person類的成員變量。

2.5 rwro的聯繫

爲何class_rw_tclass_ro_t的方法、屬性和協議的地址一致?筆者在class_data_bits_t結構體中的safe_ro()函數中發現了端倪

const class_ro_t *safe_ro() {
    class_rw_t *maybe_rw = data();
    if (maybe_rw->flags & RW_REALIZED) {
        // maybe_rw is rw
        return maybe_rw->ro;
    } else {
        // maybe_rw is actually ro
        return (class_ro_t *)maybe_rw;
    }
}
複製代碼

可見,rw不必定是rw,也多是ro。實際上,在編譯期間,類的class_data_bits_t *bits指針指向的是class_ro_t *,而後在OC運行時調用了realizeClassWithoutSwift()(蘋果開源的objc4-756.2源碼realizeClassWithoutSwift(),在此以前的版本是realizeClass()方法),這個方法主要作的就是利用編譯期肯定的ro來初始化rw

ro = (const class_ro_t *)cls->data();
if (ro->flags & RO_FUTURE) {
    // This was a future class. rw data is already allocated.
    rw = cls->data();
    ro = cls->data()->ro;
    cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);
} else {
    // 通常走這裏
    // Normal class. Allocate writeable class data.
    rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);   // 給rw申請內存
    rw->ro = ro;    // 設置rw的ro
    rw->flags = RW_REALIZED|RW_REALIZING;   // 設置flags
    cls->setData(rw);   // 給cls設置正確的rw
}

... // 初始化 rw 的其餘字段,更新superclass、meta class

// Attach categories
methodizeClass(cls);
複製代碼

在代碼的最後,還調用了methodizeClass(),其源碼以下

static void methodizeClass(Class cls) {
    runtimeLock.assertLocked();

    bool isMeta = cls->isMetaClass();
    auto rw = cls->data();
    auto ro = rw->ro;

    ... // 打印信息

    // Install methods and properties that the class implements itself.
    method_list_t *list = ro->baseMethods();
    if (list) {
        prepareMethodLists(cls, &list, 1, YES, isBundleClass(cls));
        rw->methods.attachLists(&list, 1);
    }

    property_list_t *proplist = ro->baseProperties;
    if (proplist) {
        rw->properties.attachLists(&proplist, 1);
    }

    protocol_list_t *protolist = ro->baseProtocols;
    if (protolist) {
        rw->protocols.attachLists(&protolist, 1);
    }

    if (cls->isRootMetaclass()) {
        addMethod(cls, SEL_initialize, (IMP)&objc_noop_imp, "", NO);
    }

    category_list *cats = unattachedCategoriesForClass(cls, true /*realizing*/);
    attachCategories(cls, cats, false /*don't flush caches*/);

    ... // 打印信息
    if (cats) free(cats);
    ... // 打印信息
}
複製代碼

在這個方法裏,將類本身實現的方法(包括分類)、屬性和遵循的協議加載到 methodspropertiesprotocols 列表中。

這就完美解釋了爲何運行時rwro的方法、屬性和協議相同。

2.6 rwro在運行時的不一樣之處

目前爲止的驗證都是基於Person類的現有結構,也就是在編譯期就肯定的,突出不了class_rw_tclass_ro_t的差別性。接下來筆者會用runtimeapi在運行時爲Person動態添加一個屬性fly()方法,再來一試。

  1. 添加方法

具體代碼以下:

void fly(id obj, SEL sel) {
    NSLog(@"I am flying");
}

class_addMethod([Person class], NSSelectorFromString(@"fly"), (IMP)fly, "v@:");
複製代碼

再加一個打印方法,用於打印類的methods

void printMethods(Class cls) {
    if (cls == nil) {
        return ;
    }
    CCNSLog(@"------------ print %@ methods ------------", NSStringFromClass(cls));
    uint32_t count;
    Method *methods = class_copyMethodList(cls, &count);
    for (uint32_t i = 0; i < count; i++) {
        Method method = methods[i];
        CCNSLog(@"名字:%@ -- 類型:%s", NSStringFromSelector(method_getName(method)), method_getTypeEncoding(method));
    }
}
複製代碼

運行一下看效果,發現添加成功,如圖

  1. 驗證過程

先打印class_rw_t,即

還有class_ro_t

對比後發現,二者的屬性、協議指針地址未發生變化,可是方法的指針地址不同了。 因爲class_rw_t是運行時才初始化的,而class_ro_t在編譯期間就肯定了,所以能夠猜想新增的fly方法存儲在class_rw_tmethods指針上,class_ro_tbaseMethodList指針從編譯期以後就未發生改變。

下面繼續驗證,首先看class_ro_t的方法列表

OK,編譯期就肯定的方法都在,而且沒有fly方法,也就是說class_ro_t的方法列表在運行時基本沒變。

class_ro_t的屬性列表、成員變量列表、協議在運行時都沒有發生改變。感興趣的同窗能夠本身嘗試驗證一下。

接着看class_rw_t的方法列表

class_rw_tmethods裏面數據竟然都沒有了? 沒辦法,這裏暫時留個坑吧,筆者也不知道緣由。

3. 總結

3.1 類的結構總結

關於類的結構,咱們瞭解到:

  • 類本質上是objc_object結構體,也就是類也是對象,即萬物是對象。
  • 類都包含一個Class類型的成員變量isaClassobjc_class結構體類型的指針變量,內部有4個成員變量,即
    • isa:類型是isa_t,詳細請戳 OC源碼分析之isa
    • superclass:類型是Class,表示繼承關係,指向類的父類
    • cache:類型是cache_t,表示緩存,用於緩存指針和 vtable,加速方法的調用
    • bits:類型是class_data_bits_t,用於存儲類的數據(類的方法、屬性、遵循的協議等信息),其長度在64位CPU下爲8字節,是個指針,指向class_rw_t *

3.2 class_rw_tclass_ro_t總結

  • class_ro_t存儲了類在編譯期肯定的方法(包括其分類的)、成員變量、屬性以及遵循的協議等信息,在運行時不會發生變化。編譯期,類的bits指針指向的是class_ro_t指針(即此時的class_rw_t *其實是class_ro_t *)。
    • 實例方法存儲在類中
    • 類方法存儲在元類中(【4.1】將給出證實)
  • realizeClassWithoutSwift()執行以後,class_rw_t纔會被初始化,同時存儲類的方法、屬性以及遵循的協議,實際上,class_rw_tclass_ro_t二者的方法列表(或屬性列表、協議列表)的指針是相同的。
  • 運行時向類動態添加屬性、方法時,會修改class_rw_t的屬性列表、方法列表指針,但class_ro_t對應的屬性列表、方法列表不會變。

一個待解決的坑:經過運行時添加方法(或屬性、協議)改變了 class_rw_t 對應的方法列表(或屬性列表、協議列表)的指針後,不知道爲何竟然在 class_rw_t 的方法列表(或屬性列表、協議列表)上找不到新增的方法(或屬性、協議)了。這個問題困擾筆者很久了,在這裏很是歡迎同窗在評論區留言討論。

4. 補充

4.1 類方法的存儲位置

(Person類的類方法printMyClassName()

// 1. 獲取 Person元類
(lldb) x/4gx pcls
0x100002820: 0x001d8001000027f9 0x0000000100b39140
0x100002830: 0x00000001003dc250 0x0000000000000000
(lldb) p/x 0x001d8001000027f9 & 0x00007ffffffffff8
(long) $50 = 0x00000001000027f8
(lldb) po 0x00000001000027f8
Person  // Person元類

// 2. 獲取 Person元類 的 bits
(lldb) x/5gx 0x00000001000027f8
0x1000027f8: 0x001d800100b390f1 0x0000000100b390f0
0x100002808: 0x0000000102237440 0x0000000100000003
0x100002818: 0x00000001022373a0 // Person元類 的 bits

// 3. 獲取 Person元類 的 class_rw_t
(lldb) p (class_rw_t *)(0x00000001022373a0 & 0x00007ffffffffff8)
(class_rw_t *) $52 = 0x00000001022373a0

// 4. 驗證 Person元類 的 methods
(lldb) p $52->methods
(method_array_t) $55 = {
  list_array_tt<method_t, method_list_t> = {
     = {
      list = 0x0000000100002270
      arrayAndFlag = 4294976112
    }
  }
}
(lldb) p (method_list_t *)0x0000000100002270
(method_list_t *) $56 = 0x0000000100002270
(lldb) p *$56
(method_list_t) $57 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 26
    count = 1
    first = {
      name = "printMyClassName" // 成功找到 Person類的類方法
      types = 0x0000000100001e96 "v16@0:8"
      imp = 0x00000001000014d0 (CCTest`+[Person printMyClassName] at main.m:37)
    }
  }
}
複製代碼

結論:類方法 存儲 在類的元類上,且位於元類的class_ro_tbaseMethodList指針上(或在class_rw_tmethods指針上)

參考資料

深刻解析 ObjC 中方法的結構(by Draveness)

PS

相關文章
相關標籤/搜索