iOS——类与对象底层探索

类和对象的本质

当我们使用OC创建一个testClass类并在main函数创建它的实例对象的时候,OC的底层到底是什么样的呢?

首先,我们要了解OC对象的底层结构,那么我们就得知道:OC本质底层实现转化其实都是C/C++代码。

使用下面这行命令将main.m文件转化为c++文件:

clang -rewrite-objc main.m

然后打开c++文件,我们可以看见TestClass的定义实际是:

cpp 复制代码
#ifndef _REWRITER_typedef_TestClass
#define _REWRITER_typedef_TestClass
typedef struct objc_object TestClass;
typedef struct {} _objc_exc_TestClass;
#endif

struct TestClass_IMPL {
	struct NSObject_IMPL NSObject_IVARS;
};

可以看出:

  • OC中的TestClass类底层是struct TestClass_IMPL结构体。
  • OC中@interface TestClass : NSObject,TestClass继承NSObject底层是typedef struct objc_object TestClass;这样体现的。
  • 首先数入参TestClass * self,SEL _cmd,所有OC方法都有这两个隐藏参数,所以我们可以在OC的方法中使用self指针,但是这两个参数对外是不显示的。
  • 看到getter和setter里是通过首地址指针+对应成员变量的地址值指针的偏移量的方式取和存的,最终通过(*(NSString **)还原为string类型。取值的过程就是:先拿到当前成员变量的地址,再去取这个地址里面所存的值。

struct NSObject_IMPL的具体实现:

通过在代码中查找发现:

cpp 复制代码
#ifndef _REWRITER_typedef_NSObject
#define _REWRITER_typedef_NSObject
typedef struct objc_object NSObject;
typedef struct {} _objc_exc_NSObject;
#endif

struct NSObject_IMPL {
	Class isa;
};

该结构体中只有一个Class isa

我们进一步查看Class:

cpp 复制代码
typedef struct objc_class *Class;

发现Class实际上就是一个指针,指向了objc_class类型的结构体。关于objc_class,我们在下面的类结构中讲。

编译发现:

  • TestPerson这个类在底层编译成了TestPerson_IMP结构体。

  • 函数_I_TestPerson_name,实际上就是getter方法,包含两个默认参数self、_cmd。

  • 函数_I_TestPerson_setName_,实际上就是setter方法,包含两个默认参数self、_cmd,与一个形参name。

    根据编译的结构我们可以得出下面结论:

  • 对象的本质在底层就是一个objc_object结构体。

  • 属性与成员变量的区别,属性是由成员变量+getter方法+setter方法组成。

类结构

现在我们来看objc_class,在将obj4源码搜索后可以发现:

cpp 复制代码
struct objc_class : 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 flagsflags

在这段代码中可以看出:类是一个结构体,其中存放了superclass、cache和bits等。objc_class继承于objc_object

  • objc_class继承于objc_object,换句话说,类的本质也是个对象,其中isa指针式其继承自objc_object的,所以代码里才注释了// Class ISA;
    继续搜索objc_object,我们可以找到:
cpp 复制代码
struct objc_object {
private:
    isa_t isa;

public:
//...

会发现isa_t类型的属性isa。

isa

我们去搜一下isa_t,可以看见它的源码:

cpp 复制代码
#include "isa.h"

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

    uintptr_t bits;

private:
    // Accessing the class requires custom ptrauth operations, so
    // force clients to go through setClass/getClass by making this
    // private.
    Class cls;

public:
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };

#if ISA_HAS_INLINE_RC
    bool isDeallocating() const {
        return extra_rc == 0 && has_sidetable_rc == 0;
    }
    void setDeallocating() {
        extra_rc = 0;
        has_sidetable_rc = 0;
    }
#endif // ISA_HAS_INLINE_RC

#endif

    void setClass(Class cls, objc_object *obj);
    Class getClass(bool authenticated) const;
    Class getDecodedClass(bool authenticated) const;
};
cpp 复制代码
# if  __ARM_ARCH_7K__ >= 2  ||  (__arm64__ && !__LP64__)
    // armv7k or arm64_32

#   define ISA_BITFIELD                         \
      // 定义 ISA_BITFIELD 宏,用于设置 isa 位域结构
      uintptr_t nonpointer        : 1;
      uintptr_t has_assoc         : 1;
      uintptr_t indexcls          : 15;//indexcls位,用于存储类索引
      uintptr_t magic             : 4;//magic位,用于存储魔术值
      uintptr_t has_cxx_dtor      : 1;//has_cxx_dtor位,表示是否有 C++ 析构函数
      uintptr_t weakly_referenced : 1;//weakly_referenced位,表示是否有弱引用
      uintptr_t unused            : 1//unused位,未使用
      uintptr_t has_sidetable_rc  : 1;//has_sidetable_rc位,表示是否有附加引用计数
      uintptr_t extra_rc          : 7//extra_rc位,表示额外的引用计数

在这个定义中,isa_t 是一个联合体,包含了以下几个部分:

  1. bits:一个无符号整数类型,表示整个 isa 值。
  2. cls:一个类指针,指向对象的类。
  3. 位域结构:包含了多个字段,每个字段表示 isa 指针的一部分。

** 什么是isa**

isa指针保存着指向类对象的内存地址,类对象全局只有一个,因此每个类创建出来的对象都会默认有一个isa属性,保存类对象的地址,也就是class,通过class就可以查询到这个对象的属性和方法,协议等;

isa 的作用

  • 类指针:isa 指针的主要作用是指向对象的类。通过这个指针,运行时可以找到对象的类定义,并调用相应的类方法。
  • 标志位:在现代 Objective-C 运行时中,isa 指针还包含了一些额外的标志位,用来存储对象的元数据。例如,nonpointer 标志表示 isa 是否包含额外的元数据,has_assoc 标志表示对象是否有关联的对象,extra_rc 表示对象的引用计数等。

__arm64__之前,isa仅仅是一个指针,保存着对象或类对象内存地址,在__arm64__架构之后,apple对isa进行了优化,变成了一个共用体(union)结构,同时使用位域来存储更多的信息。
所以现在的isa是结构体,其中会存储类的地址,它已经并不是指针了,"isa指向"这句话严格来说是不正确的,但是方便理解

isa的初始化流程

bits

在objc_class中我们可以看见:

objectivec 复制代码
class_data_bits_t bits;

bits是class_data_bits_t类型的结构体,其源码:

objectivec 复制代码
public:

    class_rw_t* data() const {
        return (class_rw_t *)(bits & FAST_DATA_MASK);
    }
    void setData(class_rw_t *newData)
    {
        ASSERT(!data()  ||  (newData->flags & (RW_REALIZING | RW_FUTURE)));
        // Set during realization or construction only. No locking needed.
        // Use a store-release fence because there may be concurrent
        // readers of data and data's contents.
        uintptr_t newBits = (bits & ~FAST_DATA_MASK) | (uintptr_t)newData;
        atomic_thread_fence(memory_order_release);
        bits = newBits;
    }

    // Get the class's ro data, even in the presence of concurrent realization.
    // fixme this isn't really safe without a compiler barrier at least
    // and probably a memory barrier when realizeClass changes the data field
    const class_ro_t *safe_ro() const {
        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;
        }
    }

这段代码用于处理类对象的数据,有三个主要的方法:data(), setData(class_rw_t *newData), 和 safe_ro()

  1. data():这个方法返回类对象的数据。 bits & FAST_DATA_MASK 是一个位操作,用于获取存储在 bits 中的类数据。
  2. setData(class_rw_t *newData):这个方法用于设置类对象的数据。它首先检查新数据是否有效,然后使用原子操作更新 bits
  3. safe_ro():这个方法用于安全地获取类对象的只读数据。它首先获取类的数据,然后检查数据的状态。如果数据已经被实现(RW_REALIZED),那么它返回数据的只读版本。如果数据还没有被实现,那么它直接返回数据。
    在这之中比较重要的是class_rw_t class_ro_t
class_rw_t

该结构体用于存储类对象的可读写数据,包括类的实例方法列表、协议列表、属性列表等。当你向一个类添加方法或协议,或者修改属性时,这些信息会被存储在class_rw_t结构体中。

通过runtime动态修改类的方法时,其实是修改在class_rw_t区域中存储的方法列表。

class_ro_t

该结构体用于存储类对象的只读数据,包括类的名称、父类、实例大小等信息。这些信息在类对象创建时就已经确定,不会在运行时改变,所以被存储在class_ro_t结构体中。

chche_t

cache_t是一个结构体,用于存储类对象的方法缓存。每个类对象都有一个cache_t成员,这个成员会缓存类对象最近使用的方法。当你向一个类对象发送消息时,Runtime会首先在这个缓存中查找方法。如果找到了,就直接调用缓存的方法,这样可以大大提高方法调用的效率。如果没有找到,Runtime会在类的方法列表中查找,然后将找到的方法添加到缓存中。

指针内存平移

指针内存平移是指通过操作指针的值,移动指针指向的内存位置。在 Objective-C 运行时,这种技术被广泛用于访问对象的成员变量、方法列表等。

为什么category不能添加成员变量

因为category是运行时添加的,他只能操作class_rw_t结构,但是class_rw_t结构没有添加成员变量的入口。成员变量是存储在class_ro_t中的,是无法被修改的,所以category就不能添加成员变量。

对象、类、父类、元类的关系

  1. 对象(Instance)
  • 每个对象是某个类的实例,它包含该类的成员变量和方法。
  • 对象在运行时通过 isa 指针指向它所属的类。
  1. 类(Class)
  • 类是对象的蓝图,定义了对象的成员变量和方法。
  • 类本身也是一个对象,它是 Class 类型的实例。
  • 每个类有一个 isa 指针,指向它的元类(metaclass)。
  1. 父类(Superclass)
  • 类可以继承自另一个类,称为父类或超类。
  • 继承使得子类(subclass)可以使用父类的成员变量和方法,同时可以添加新的成员变量和方法,或重写父类的方法。
  • 每个类有一个 super_class 指针,指向它的父类。
  1. 元类(Metaclass)
  • 元类是类的类,定义了类对象的行为和方法。
  • 每个元类也是一个类,并且有自己的 isa 指针,指向根元类(root metaclass)。
  • 元类也有一个 super_class 指针,指向父类的元类。

元类的定义

其原理就是OC对象在发送消息时,运行时库会追寻着对象的isa指针得到对象所属的类。这个类包含了能应用于这个类的所有实例方法以及指向父类的指针,以便可以找到父类的实例方法。运行时库检查这个类和其父类的方法列表,找到与消息对应的方法。编译器会将消息转换为消息函数objc_msgSend进行调用(这里在消息传递和消息转发的博客里有说)

如图:每一个实例变量的isa都指向自己所属的类,每一个类的isa都指向自己所属的元类,同时,父类的元类也是子类的元类的父类,父类的元类也是根元类的子类。而父类的元类和子类的元类都属于根元类。根元类的isa指向自己,同时根元类的父类也是自己的下属类(NSObject元类的父类是NSObject类):

从消息机制的层面来讲:

  • 当你给对象发消息时,消息会寻找这个对象的类的方法列表
  • 当你给类发消息时,消息是在寻找这个类的元类的方法列表

类对象

Objective-C中所有对象可以分为3类:实例对象,类对象,元类对象。其中我们开发者常用的继承自NSObject都属于实例对象,实例对象通过isa指针指向的是类对象。类对象通过isa指向的是元类对象。类对象和元类对象拥有同样的结构,都是来自objc_class。

class方法

class方法是一种特殊的类方法。它的主要作用是返回调用该方法的类的类对象。类对象是一个特殊的对象,它包含了类的元数据,如类的名称,类的父类,类的实例变量(ivar)等信息。

class方法一些注意的点:

  • class ObjectClass = [[nsobject class]class]; 返回的还是class对象,并不是meta-class对象。- (Class)class, +(Class)class返回的就是类对象。

class ObjectClass = [[NSObject class] class];这行代码的含义是获取NSObject类对象的类对象。首先,[NSObject class]获取了NSObject的类对象,然后再次调用class方法获取该类对象的类对象,也就是元类对象,然后将这个元类对象赋值给ObjectClass变量。

然而,Objective-C在设计上对这一点进行了特殊处理。当我们调用一个类对象的class方法时,返回的仍然是类对象,而不是元类对象。这也就是为什么class ObjectClass = [[NSObject class] class];这行代码返回的还是类对象,而不是元类对象。

至于- (Class)class和+ (Class)class,它们分别是实例方法和类方法。- (Class)class是对象的实例方法,调用这个方法会返回该对象的类对象。+ (Class)class是类方法,调用这个方法会返回调用它的类的类对象。在Objective-C中,类方法和实例方法是可以有相同的名称的,它们被归类到不同的命名空间中。

  • 在Objective-C中,元类对象和类对象的内存结构是一样的,但它们的用途不同。

类对象包含了类的元数据信息,例如实例变量的布局信息、属性信息、对象方法列表等。当我们创建一个新的对象实例时,这些信息会被用到。

而元类对象则主要包含了类方法的信息。当我们调用一个类方法时,这些信息会被用到。除了类方法的信息,元类对象的其他部分,如实例变量的布局信息、属性信息等,通常都为空的。

objectivec 复制代码
//instance实例对象
        NSObject *object1 = [[NSObject alloc] init];
        NSObject *object2 = [[NSObject alloc] init];

        //class对象,类对象
        //objectClass1~5都是NSObject的类对象
        Class objectClass1 = [object1 class];
        Class objectClass2 = [object2 class];
        Class objectClass3 = [NSObject class];
        Class objectClass4 = object_getClass(object1);
        Class objectClass5 = object_getClass(object2);
        
        //元类对象(将类对象当作参数传入进去)
        Class objectMetaClass = object_getClass([NSObject class]);
        Class objectMetaClass2 = [[NSObject class] class];
        
        //判断是不是元类对象

        NSLog(@"instance - %p %p", object1, object2);
        NSLog(@"class - %p %p %p %p %p %d", objectClass1,objectClass2, objectClass3, objectClass4, objectClass5, class_isMetaClass(objectClass3 ));
        NSLog(@"mateClass - %p %p %d",objectMetaClass, objectMetaClass2, class_isMetaClass(objectMetaClass));

输出情况:
instance - 0x100511920 0x10050e840
class - 0x7fff91da2118 0x7fff91da2118 0x7fff91da2118 0x7fff91da2118 0x7fff91da2118 0
mateClass - 0x7fff91da20f0 0x7fff91da2118 1

无论多少次class方法得到的都还是类函数。
相关推荐
&岁月不待人&16 分钟前
Kotlin by lazy和lateinit的使用及区别
android·开发语言·kotlin
StayInLove19 分钟前
G1垃圾回收器日志详解
java·开发语言
无尽的大道27 分钟前
Java字符串深度解析:String的实现、常量池与性能优化
java·开发语言·性能优化
爱吃生蚝的于勒30 分钟前
深入学习指针(5)!!!!!!!!!!!!!!!
c语言·开发语言·数据结构·学习·计算机网络·算法
binishuaio40 分钟前
Java 第11天 (git版本控制器基础用法)
java·开发语言·git
zz.YE42 分钟前
【Java SE】StringBuffer
java·开发语言
就是有点傻1 小时前
WPF中的依赖属性
开发语言·wpf
洋2401 小时前
C语言常用标准库函数
c语言·开发语言
进击的六角龙1 小时前
Python中处理Excel的基本概念(如工作簿、工作表等)
开发语言·python·excel
wrx繁星点点1 小时前
状态模式(State Pattern)详解
java·开发语言·ui·设计模式·状态模式