【OC 底层】对象与类

275 阅读7分钟

Preparation

源码

文中可能有很多展示runtime中结构体的代码,基本上都没有展示完整代码,而是只放了对理解和解释有必要的部分。有兴趣的可以去下载源码看看哈~

探索工具clang

clang是一个C语言、C++、OC语言的,基于LLVM的轻量级编译器。

# 把目标文件编译成C++文件
clang -rewrite-objc main.m -o main.cpp
# xcrun命令在clang的基础上进行了一些封装
## 模拟器
xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main.cpp
## 真机
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp

struct&union的内存对齐

  1. 数据成员对齐规则:结构体struct/联合union的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小/该成员的子成员大小(只要该成员有子成员,如数组,结构体等)的整数倍开始。e.g. int为4字节,则要从4的整数倍地址开始存储。
  2. 结构体作为成员:如果一个结构中有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储。e.g. struct A中存有struct B,B中有char,int,double等元素,那么B应该从8的整数倍开始存储
  3. 结构体的总大小(sizeof),必须是其内部最大成员的整数倍,不足的要补齐。

探索流程

首先我们有这样一个类

@interface YKPerson : NSObject
@property (nonatomic,strong) NSString *name;
- (void)testFunc;
@end

@implementation YKPerson
- (void)testFunc {
    NSLog(@"testFunc");
}
@end

通过以下clang指令可以生成的iphoneos26.0架构的cpp文件

clang -x objective-c \
-arch arm64 \
-isysroot $(xcrun --sdk iphoneos26.0 --show-sdk-path) \
-rewrite-objc YKPerson.m \
-o YKPerson.cpp

# 若报错xcrun: error: SDK "iphoneos" cannot be located,可通过xcodebuild -showsdks查看已有sdk

cpp文件如下

// YKPerson
typedef struct objc_object YKPerson;

extern "C" unsigned long OBJC_IVAR_$_YKPerson$_name;
struct YKPerson_IMPL {
	struct NSObject_IMPL NSObject_IVARS; // Class isa;
	NSString *_name;
};

// NSObject
typedef struct objc_object NSObject;
struct NSObject_IMPL {
	Class isa;
};

从上述代码我们可以看出

  1. 对象在底层的本质就是objc_object类型的结构体
  2. NSObject为ROOT_CLASS (插句话:在著名的isa走位图中ROOT_Class即为NSObject,且它的元类的isa指向的正是NSObject类)
  3. 由于YKPerson继承自NSObject,所以除了自身的name属性外,也继承了NSObjectClass isa属性。

在objc4源码中,objc_object长这个样子。

struct objc_object {
    Class _Nonnull isa;
};

// Class
typedef struct objc_class *Class;

// 再看objc-runtime-new.h中的objc_class
struct objc_class : objc_object {
    // Class isa(继承自objc_object)
    Class superclass;
    cache_t cache;          
    class_data_bits_t bits;
    ···
}

Tips:源码中也可以看到这样一句typedef struct objc_object *id;这也就解释了为什么我们平时用id修饰对象时不用加*

Class superclass

我们验证一下类的内存结构,运行程序并断点

image.png 在此处进行lldb调试,x/6gx YKPerson.class输出结果 image.png 根据上面objc_class的内存结构模型,我们知道前两个成员变量都为Class类型,也就是都为8字节,那么可以推断出第二个地址存储的是其父类,那么来执行po 0x00007fff80030660可以看到!

image.png

cache_t cache

// 以下仅摘取cache_t成员变量
struct cache_t {
private:
    // 1
    // typedef unsigned long uintptr_t;8字节
    explicit_atomic<uintptr_t> _bucketsAndMaybeMask;
    
    // 2
    union {
        struct {
        // typedef uint32_t mask_t;4字节
            explicit_atomic<mask_t>    _maybeMask;
#if __LP64__
            uint16_t                   _flags; // 2字节
#endif
            uint16_t                   _occupied; // 2字节
        };
        // 8字节
        explicit_atomic<preopt_cache_t *> _originalPreoptCache;
    };

因为static变量存储在全局区,方法存储在方法区,均不占用结构体内存,所以一个对象内部的cache_t的大小就取决于_bucketsAndMaybeMask的大小加上联合体的大小。也就是16字节。

class_data_bits_t bits

首先我们通过上述分析知道了class_data_bits_t的地址,就在YKPerson的类地址偏移8+8+16=32个字节的位置。那么执行po 0x103d2fe20+0x20获得地址4359126592,再执行p/x 4359126592

image.png

// 仅摘取部分成员变量和重要方法
struct class_data_bits_t {
    friend objc_class;
    
    uintptr_t bits;
public:
    // 获取class_rw_t
    class_rw_t* data() const {···}
    // 获取class_ro_t
    const class_ro_t *safe_ro() const {···}
}

可以看到通过bits可以获取到两个关键结构体class_rw_tclass_ro_t,我们来看下这两个结构体中都有些什么

class_rw_t

首先根据名字,可以判断出这个结构体中的内容是read-write的,看下objc4源码中的定义

// 仅摘取class_rw_t的成员变量和部分核心方法
struct class_rw_t {
    uint32_t flags;
    uint16_t witness;
#if SUPPORT_INDEXED_ISA
    uint16_t index;
#endif
    explicit_atomic<uintptr_t> ro_or_rw_ext;
    
    // 所有的类都会链接成一个树状结构,通过下面这两个指针实现
    Class firstSubclass;
    Class nextSiblingClass;
    
    const method_array_t methods() const {···}
    const property_array_t properties() const {···}
    const protocol_array_t protocols() const {···}
}

可以看到,类信息的class_data_bits_t bits->class_rw_t结构体中存储了该类的方法、属性、实现协议的信息。

感兴趣的话我们也可看下这三个结构体中都存着什么信息

method_array_t

存储方法结构体method_t的列表。每一条数据都是一个指针。根据下面的结构可以看出,每个method_t占用24个字节。

struct method_t {
    ···
    struct big {
        SEL name; // 方法名指针/选择器,具有唯一性,所以可以使用指针相等来比较
        const char *types; // 类型编码。表示参数和返回类型的字符串【不是用来发送消息的,但是是运行时introspection和message forwarding所必需的东西】
        MethodListIMP imp; // 指向方法实现的指针
    };
}
property_array_t

存储属性结构体property_t的列表

struct property_t {
    const char *name; // 属性名
    const char *attributes; // 属性的属性(类型等)
};
protocol_array_t

存储协议结构体protocol_t的指针protocol_ref_t的列表

typedef uintptr_t protocol_ref_t// protocol_t *, but unremapped

// 以下仅摘取【部分】protocol_t的成员变量
struct protocol_t : objc_object {
    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) 内存大小
    property_list_t *_classProperties; // 类属性
    ···
}

class_ro_t

// 以下仅摘取【部分】class_ro_t的成员变量
struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart; // 实例内存地址起点
    uint32_t instanceSize; // 实例的内存大小
#ifdef __LP64__
    uint32_t reserved;
#endif
    union {
        const uint8_t * ivarLayout;
        Class nonMetaclass;
    };
    explicit_atomic<const char *> name;
    void *baseMethodList;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;
    property_list_t *baseProperties;
}
class_ro_t补充说明
  1. Swift类和Objective-C类,共享这一数据结构。
  2. 当类第一次从磁盘加载到内存中时,一开始也是这样的,但是一经使用,就会发生变化

clean memory与dirty memory

clean memory:加载后不会发生更改的内存。如class_ro_t,因为它是只读的。 dirty memory:在进程运行时会发生更改的内存。类结构一经使用就会变成dirty memory,因为运行时会向它写入新的数据(E.g. 创建一个新的方法缓存,并从类中指向它;分类扩展方法等)如class_rw_t

  1. dirty memory比clean memory要昂贵的多,只要进程在运行,它就必须一直存在。
  2. clean memory可以进行移除,从而节省更多的内存空间。
  3. 为什么class_ro_tclass_rw_t都存有方法列表呢?因为主类中的方法存在class_ro_t中,而当category被加载时,可以向类中添加新的方法,即添加到class_rw_t

class_rw_ext_t

class_rw_t中分解出来,用于存储用不到的部分。

struct class_rw_ext_t {
    DECLARE_AUTHED_PTR_TEMPLATE(class_ro_t)
    class_ro_t_authed_ptr<const class_ro_t> ro;
    method_array_t methods;
    property_array_t properties;
    protocol_array_t protocols;
    char *demangledName;
    uint32_t version;
};

  • 待完善
  • 待修正
  • 欢迎指出错误和没解释清楚的地方!!!