【iOS】源码阅读(五)——类&类的结构分析

发布于:2025-05-29 ⋅ 阅读:(24) ⋅ 点赞:(0)

前言

  本篇博客主要是笔者在学习类&类的结构的底层探索和分析时所作的笔记,主要涉及实例对象的类以及类的结构。Objective-C的类结构是其动态性和面向对象特性的核心,理解类的内存布局和内部机制,对开发、调试和性能优化至关重要。

类的分析

类的本质

  在 OC 中,类(Class)本身是一个对象(objc_class 结构体),在探索类的本质之前,我们先在main文件里自定义一个继承自NSobjetc的TCJPerson类:

#import <Foundation/Foundation.h>
#import <objc/runtime.h>

@interface TCJPerson : NSObject

@end

@implementation TCJPerson

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        TCJPerson *person = [TCJPerson alloc];
        Class cls = object_getClass(person);
    }
    return 0;
}

然后利用clang工具将oc语言的main文件输出为cpp文件,命令行如下:

clang -rewrite-objc main.m -o main.cpp

将文件拖到我们的objc源码中打开,方便我们调试和查找,可以发现,这段代码在cpp文件文件中如下:

//类型定义部分
#ifndef _REWRITER_typedef_TCJPerson
#define _REWRITER_typedef_TCJPerson
typedef struct objc_object TCJPerson; //将 TCJPerson 定义为 objc_object 结构体
typedef struct {} _objc_exc_TCJPerson; //空结构体,用于异常处理占位(实际未使用)
#endif

//类的实现结构
struct TCJPerson_IMPL {
	struct NSObject_IMPL NSObject_IVARS; //继承自 NSObject 的实例变量
};

/* @end */

// @implementation TCJPerson
// @end
//main函数的底层转换
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        TCJPerson *person = ((TCJPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("TCJPerson"), sel_registerName("alloc"));
        //objc_getClass("TCJPerson"):获取 TCJPerson 的类对象(Class)
        //sel_registerName("alloc"):注册方法名 alloc,返回 SEL 类型的选择子
        //objc_msgSend:发送 alloc 消息给 TCJPerson 类,创建实例
        //强制类型转换 (TCJPerson *(*)(id, SEL))(void *) 是为了匹配 objc_msgSend 的函数指针签名
        
        Class cls = object_getClass(person);//获取 person 实例的类(即 TCJPerson 的类对象)
    }
    return 0;
}
  • TCJPerson 被定义为 objc_object,说明在底层,Objective-C 类实例的本质就是 objc_object 结构体。
  • 因为 TCJPerson 继承自 NSObject,所以它的底层结构会包含父类的实例变量。其中,NSObject_IVARS 就是 NSObject 的实例变量,通常就是 isa 指针(指向类的元数据)。

然后我们发现类在底层是用class接收的。
在这里插入图片描述

typedef struct objc_class *Class;

在左侧搜索栏查找objc_class,我们可以发现objc_class继承自objc_object。

请添加图片描述

点击进入objc_object的源码实现中:

在这里插入图片描述
小结

  • Objective-C 对象的本质:
    类实例(如 person)本质是 objc_object 结构体,包含 isa 指针(来自 NSObject_IVARS)。类(如 TCJPerson)本质是 objc_class 结构体,继承自 objc_object。所以满足万物皆对象。
  • 方法调用的本质:[TCJPerson alloc] 被编译为 objc_msgSend 的调用,动态查找并执行方法。
  • 内存布局:TCJPerson_IMPL 只包含 NSObject 的 isa,因为没有自定义实例变量。

objc_class 、objc_object和NSObject

objc_object:所有对象的基类型

底层源码:

struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY; //必须为非空的类指针(即指向 objc_class 结构体的指针)
};

这证明objc_object是所有 Objective-C 对象的最底层结构,包括实例对象、类对象、元类对象。其通过 isa 指针实现对象与类的关联(即“对象是什么类”)。

特点:

实例对象的 isa 指向它的类(如 TCJPerson 实例的 isa 指向 TCJPerson 类)。
类对象的 isa 指向元类(Meta Class)。
元类的 isa 指向根元类(Root Meta Class)。

关于类 元类 根元类

  1. ​​类(Class)​​
    ​​作用​​:定义对象的​​实例变量​​和​​实例方法​​(如 -init、-description)。
    ​​内存结构​​:每个类是一个 objc_class 结构体实例,包含方法列表、父类指针、缓存等。
    ​​对象关系​​:实例对象(如 MyObject *obj)的 isa 指针指向其类对象。
  2. ​​元类(Meta-class)​​
    ​​作用​​:定义类的​​类方法​​(如 +alloc、+new)。
    元类本身也是一个类,它的实例是类对象。
    ​​内存结构​​:元类也是一个 objc_class 结构体,但其方法列表存储类方法。
    ​​对象关系​​:类对象(如 MyObject.class)的 isa 指针指向其元类。
  3. ​​根元类(Root Meta-class)​​
    ​​作用​​:所有元类的最终基类,通常是 NSObject 的元类。
    根元类的类方法(如 +alloc)会被所有类的元类继承。
    根元类的 isa 指针指向自身,形成闭环。

在这里插入图片描述

​​Instance (MyObject)​​: 一个对象实例
​​Class (MyObject class)​​: 定义该实例的类
​​Meta-class (MyObject meta)​​: 定义该类的元类

isa 指针:形成继承链的核心,每个实例、类和元类都有一个指向其类型的指针
实例通过 isa 指向类,类通过 isa 指向元类,元类通过 isa 指向根元类
super_class 指针:形成继承体系
类通过 super_class 指向父类,元类通过 super_class 指向父元类

​​Root Meta-class (NSObject meta)​​: 所有元类的根元类
根元类的 isa 指向自己(self),形成闭环
根元类的 super_class 指向 NSObject 类

运行时​​方法查找路径​​:
实例方法首先在实例所属的类中查找
如果没找到,则沿着 super_class 链向上查找
类方法则在元类及其父元类中查找
​​
继承机制​​:实例继承自类,类继承自元类,元类继承自父元类,最终根元类继承自NSObject类

objc_class:类的底层结构

底层代码:

struct objc_class : objc_object { //继承自objc_object
  objc_class(const objc_class&) = delete;
  objc_class(objc_class&&) = delete;
  void operator=(const objc_class&) = delete;
  void operator=(objc_class&&) = delete;
    // 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  //类的方法、属性、协议等数据

    Class getSuperclass() const {
#if __has_feature(ptrauth_calls)
#   if ISA_SIGNING_AUTH_MODE == ISA_SIGNING_AUTH
        if (superclass == Nil)
            return Nil;
          
          ......
}

这说明objc_class是 objc_object 的子类,说明类本身也是对象(即“类对象”)。
存储类的元数据:方法列表、属性列表、协议列表、父类指针等。
因为其继承自 objc_object,所以其类对象也有 isa 指针(指向元类)。
其中,Class 是指向 objc_class 的指针(typedef struct objc_class *Class)。

NSObject:面向用户的根类

底层代码:

@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
    Class isa  OBJC_ISA_AVAILABILITY; //指向类对象的指针
#pragma clang diagnostic pop
}

NSObject 是 Objective-C 中所有类的根类(除了 NSProxy),提供面向开发者的基础方法(如 alloc、init、description)。

与 objc_object 的关系:
NSObject 的实例对象在底层就是 objc_object。
NSObject 的类对象在底层是 objc_class。
编译器会将 NSObject 的代码转换为对 objc_object 和 objc_class 的操作。

小结

通过对objc_class、objc_object和NSObject底层的大致了解,我们就可以明白三者存在以下金字塔关系:
请添加图片描述
objc_object:所有对象的终极基类(C结构体)
objc_class:继承objc_object,说明"类也是对象"
NSObject:继承链最顶端的公开类,封装了objc_class的面向对象接口

小结

所有的对象 + 类 + 元类 都有isa属性
所有的对象都是由objc_object继承来的
简单概括就是万物皆对象,万物皆来源于objc_object,有以下两点结论:
所有以 objc_object为模板创建的对象,都有isa属性
所有以 objc_class为模板创建的类,都有isa属性

在结构层面可以通俗的理解为上层OC 与 底层的对接:

下层是通过 结构体 定义的 模板,例如objc_class、objc_object
上层是通过底层的模板创建的一些类型,例如TCJPerson

objc_class、objc_object、isa、object、NSObject等的整体的关系如下图:
请添加图片描述

指针内存偏移

  在分析类的结构之前,我们需要先来学习一下指针内存偏移作为前缀知识。

普通指针----值拷贝

        //普通指针
        //值拷贝
        int a = 10;
        int b = 10;
        NSLog(@"&a:%d--%p", a, &a);
        NSLog(@"&b:%d--%p", b, &b);

请添加图片描述

通过代码及其运行结果可以看出来,变量a和b虽然都是被赋值为10,但是变量a和b的内存地址是不一样的,我们称这种方式为值拷贝。

对象----指针拷贝或引用拷贝

        //对象
        NSObject *obj1 = [[NSObject alloc] init];
        NSObject *obj2 = [[NSObject alloc] init];
        NSLog(@"%@--%p", obj1, &obj1);
        NSLog(@"%@--%p", obj2, &obj2);

在这里插入图片描述
通过运行结果,我们可以看到obj1和obj2对象不仅自身内存地址不一样,其指向的对象的内存地址也不一样,这被称为指针拷贝或引用拷贝。

用数组指针引出----内存偏移

        //数组指针
        int arr[4] = {1, 2, 3, 4};
        int *c = arr;
        NSLog(@"&arr:%p--%p--%p", arr, &arr[0], &arr[1]);
        NSLog(@"&c:%p--%p--%p", c, c + 1, c + 2);
        for (int i = 0; i < 4; i++) {
            int value = *(c + i);
            NSLog(@"value:%d", value);
        }

请添加图片描述

通过运行结果可以看到:

  • &a和&a[0]的地址是相同的——即首地址就代表数组的第一个元素的地址。
  • 第一个元素地址0x16fdff2f8和第二个元素地址0x16fdff2fc相差4个字节,也就是int的所占的4字节,因为他们的数据类型相同。
  • d、d+1、d+2这个地方的指针相加就是偏移地址。地址加1就是偏移,偏移一个位数所在元素的大小。
  • 可以通过地址,取出对应地址的值。

小结
在这里插入图片描述

类的结构

从objc_class的定义可以得出,类有4个属性:isa、superclass、cache、bits。

Class ISA

不但实例对象中有isa指针,类对象中也有isa指针关联着元类。
Class本身就是一个指针,占用8字节。

Class surperclass

顾名思义就是类的父类(一般为NSObject)superclass是Class类型,所以占用8字节。

cache_t cache

进入cache_t的实现源码,我们能看到(笔者对部分进行了注释,方便理解):

struct cache_t {
private:
    explicit_atomic<uintptr_t> _bucketsAndMaybeMask; 
    //原子存储缓存桶指针或掩码(根据配置不同复用同一内存)
    //原子性​​:保证并发访问时的线程安全(如 objc_msgSend 高频调用场景)

    union {
        // Note: _flags on ARM64 needs to line up with the unused bits of
        // _originalPreoptCache because we access some flags (specifically
        // FAST_CACHE_HAS_DEFAULT_CORE and FAST_CACHE_HAS_DEFAULT_AWZ) on
        // unrealized classes with the assumption that they will start out
        // as 0.
        struct {
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED && !__LP64__
            // Outlined cache mask storage, 32-bit, we have mask and occupied.
            explicit_atomic<mask_t>    _mask; //缓存掩码
            uint16_t                   _occupied; //已占用槽位数
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED && __LP64__
            // Outlined cache mask storage, 64-bit, we have mask, occupied, flags.
            explicit_atomic<mask_t>    _mask;
            uint16_t                   _occupied;
            uint16_t                   _flags; //状态标志(FAST_CACHE_HAS_DEFAULT_CORE: 是否有默认核心实现 FAST_CACHE_HAS_CUSTOM_DEALLOC_INITIATION: 是否自定义释放逻辑)
#   define CACHE_T_HAS_FLAGS 1
#elif __LP64__
            // Inline cache mask storage, 64-bit, we have occupied, flags, and
            // empty space to line up flags with originalPreoptCache.
            //
            // Note: the assembly code for objc_release_xN knows about the
            // location of _flags and the
            // FAST_CACHE_HAS_CUSTOM_DEALLOC_INITIATION flag within. Any changes
            // must be applied there as well.
            uint32_t                   _unused;
            uint16_t                   _occupied;
            uint16_t                   _flags;
#   define CACHE_T_HAS_FLAGS 1
#else
            // Inline cache mask storage, 32-bit, we have occupied, flags.
            uint16_t                   _occupied;
            uint16_t                   _flags;
#   define CACHE_T_HAS_FLAGS 1
#endif

cache在英文中的意思是缓存。
cache_t是一个结构体,内存长度由所有元素决定:

_bucketsAndMaybeMask是long类型,它是一个指针,占用8字节;
mask_t是个uint32_t类型,_mask占用4字节;
_occupied和_flags都是uint16_t类型,uint16_t是 unsigned short 的别名,所以_occupied占用2字节;
_flags占用2字节;
所以,cache_t共占用16字节。

这里对cache简单了解一下,后面还会呢详细学习。

class_data_bits_t bits

class_data_bits_t实现源码如下:

struct class_data_bits_t {
    friend objc_class;

    // Values are the FAST_ flags above.
    uintptr_t bits;
private:
    bool getBit(uintptr_t bit) const
    {
        return bits & bit;
    }

    // Atomically set the bits in `set` and clear the bits in `clear`.
    // set and clear must not overlap.  If the existing bits field is zero,
    // this function will mark it as using the RW signing scheme.
    void setAndClearBits(uintptr_t set, uintptr_t clear)
    {
        ASSERT((set & clear) == 0);
        uintptr_t newBits, oldBits = LoadExclusive(&bits);
        do {
            uintptr_t authBits
                = (oldBits
                   ? (uintptr_t)ptrauth_auth_data((class_rw_t *)oldBits,
                                                  CLASS_DATA_BITS_RW_SIGNING_KEY,
                                                  ptrauth_blend_discriminator(&bits,
                                                                              CLASS_DATA_BITS_RW_DISCRIMINATOR))
                   : FAST_IS_RW_POINTER);
            newBits = (authBits | set) & ~clear;
            newBits = (uintptr_t)ptrauth_sign_unauthenticated((class_rw_t *)newBits,
                                                              CLASS_DATA_BITS_RW_SIGNING_KEY,
                                                              ptrauth_blend_discriminator(&bits,
                                                                                          CLASS_DATA_BITS_RW_DISCRIMINATOR));
        } while (slowpath(!StoreReleaseExclusive(&bits, &oldBits, newBits)));
    }

    void setBits(uintptr_t set) {
        setAndClearBits(set, 0);
    }

    void clearBits(uintptr_t clear) {
        setAndClearBits(0, clear);
    }

public:

    void copyRWFrom(const class_data_bits_t &other) {
        bits = (uintptr_t)ptrauth_auth_and_resign((class_rw_t *)other.bits,
                                                  CLASS_DATA_BITS_RW_SIGNING_KEY,
                                                  ptrauth_blend_discriminator(&other.bits,
                                                                              CLASS_DATA_BITS_RW_DISCRIMINATOR),
                                                  CLASS_DATA_BITS_RW_SIGNING_KEY,
                                                  ptrauth_blend_discriminator(&bits,
                                                                              CLASS_DATA_BITS_RW_DISCRIMINATOR));
    }

    void copyROFrom(const class_data_bits_t &other, bool authenticate) {
        ASSERT((flags() & RO_REALIZED) == 0);
        if (authenticate) {
            bits = (uintptr_t)ptrauth_auth_and_resign((class_ro_t *)other.bits,
                                                      CLASS_DATA_BITS_RO_SIGNING_KEY,
                                                      ptrauth_blend_discriminator(&other.bits,
                                                                                  CLASS_DATA_BITS_RO_DISCRIMINATOR),
                                                      CLASS_DATA_BITS_RO_SIGNING_KEY,
                                                      ptrauth_blend_discriminator(&bits,
                                                                                  CLASS_DATA_BITS_RO_DISCRIMINATOR));
        } else {
            bits = other.bits;
        }
    }

虽然我们不是很能看懂,但至少能看出来这里是oc运行时用来存储数据的。
根据上面的分析,class指针各为8字节,cache_t cache为16字节,所以想要获取bits的中的内容,只需通过类的首地址平移32字节即可。

总结

  在学习过程中,笔者关于LLDB调试出了比较多的问题,至今还没解决,所以这篇笔记还有待完善,等笔者解决完问题后,会再次进行编撰,LLDB在源码学习中还是很不错的工具。


网站公告

今日签到

点亮在社区的每一天
去签到