今天看啥  ›  专栏  ›  红酒牛排

OC源码分析之类的结构解读

红酒牛排  · 掘金  ·  · 2020-02-01 13:14
阅读 26

OC源码分析之类的结构解读

前言

想要成为一名iOS开发高手,免不了阅读源码。以下是笔者在OC源码探索中梳理的一个小系列——类与对象篇,欢迎大家阅读指正,同时也希望对大家有所帮助。

  1. OC源码分析之对象的创建
  2. OC源码分析之isa
  3. OC源码分析之类的结构解读
  4. 未完待续...

1. 类的结构

如果你使用过Objective-C(简称OC)这门语言开发过应用程序,你一定对NSObject不陌生。OC里面有两个NSObject,一个是我们熟知的NSObject类,另一个是NSObject协议。协议类似于其他面向对象语言(如JavaC++)的接口,NSObject协议里面定义了一些属性和方法,但本身并未实现,而NSObject类遵循了NSObject协议,所以NSObject类实现了这些方法。

我们跟NSObject类打了那么多交道,却不一定对它了如指掌,今天笔者将带大家对NSObject类的结构进行全方位的解读。

注意:

  • 本文笔者用的所有源码都是基于苹果开源的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类的底层定义

clang -rewrite-objc main.m -o main.cpp
复制代码

打开main.cpp文件,找到了NSObject

#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成员变量)。

此时,笔者特别好奇我们自己定义的类经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




原文地址:访问原文地址
快照地址: 访问文章快照