• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

OC底层-类和对象深基

武飞扬头像
Clearlove_Dianfen
帮助1

前言

分析了对象,类及其父类元类的关系之后,类和对象的底层实现是需要我们更多的学习的,在之后的消息转发,包括源码的部分结构体,包括RunLoop等等或多或少都会涉及OC类和对象的底层。

我个人认为深究类和对象的深入底层的代码较为繁琐看不懂也记不住,对于类和对象 isa_t指针是重中之重,跟着这个指针向下一步一步探索,能看懂多少就看造化了。

1 类和对象

1.1 类

在OC中,类和对象的本质是基于C/C 的结构体,编译时期会以结构体的形式被编译到二进制里面。

  • 因为在源码直接看不到类的剧具体定义,直接跳转NSObject的实现。
    学新通
  • 通过查看NSObject的类定义,可以看到内部有一个Class isa的成员变量. 从Apple开放的objc源码来看,可以发现,Class类型是一个结构体指针
  • 继续看objc_class的实现 发现objc_class是继承于objc_object的结构体.而objc_object内部.只存放了isa变量.
    学新通
  • objc_class的 Class ISA
struct objc_class : objc_object {
    // Class ISA;
    Class superclass;          // 指向父类的Class结构体
    cache_t cache;             // formerly cache pointer and vtable 缓存结构体,用于加速方法查找。
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags 个字段包含了一些标志位和其他信息,用于描述该类对象的一些特性。

可以看到objc_class继承于objc_object,也就是类的本质也是对象(元类的学习讲到过)

那么类的学习也可以归为对象的学习的一部分

也就是都要深基isa结构体,对于类的isa :每个类的 isa 指针都指向它的父类的 Class 结构体,直到指向 NSObject 类为止。这就形成了一个类的继承结构,同时也支持多态和动态绑定等面向对象编程的特性

1.1.1 cache_t 和 class_data_bits_t

cache_t

  • 在 Objective-C 中,每个类都有一个方法缓存,用于加速方法查找。当程序需要调用一个对象的方法时,它首先在缓存中查找该方法,如果找到了就直接调用,否则就在类的方法列表中进行查找。这个过程会被重复执行,以便于提高方法查找的效率。
  • cache_t 是一个缓存结构体,它包含了一些关于方法缓存的信息,例如缓存的大小、已经缓存的方法数量、缓存的方法列表等等。在 Objective-C 中,缓存是在运行时创建并维护的,通过使用缓存可以显著提高方法调用的速度。
  • 因此,定义一个 cache_t 类型的变量 cache 可以用于对方法缓存进行操作,例如查询缓存中是否存在指定的方法、向缓存中添加方法等等。

class_data_bits_t

  • 这个字段包含了一些标志位和其他信息,用于描述该类对象的一些特性。

1.2 对象

对象的结构体在上一章说过了,在objc834可编译源码看看如何实现

学新通

这里是C 的面相对象思想 ,私有和公开。

抛开公开的方法部分,发现对象的结构体里面有一个名为isaisa_t类型的成员变量

struct objc_object {
private:
    isa_t isa;

public:
....省略方法部分

对象的本质除了其本身含有的方法列表,它有一个isa指针又来存储其所属类的信息。

在 Objective-C 中,每个对象都有一个 isa 指针,指向它的类对象。isa 指针实际上是一个指向一个 Class 结构体的指针,这个结构体包含了与类相关的一些信息,包括:

  • 类的名字;
  • 父类的指针;
  • 类的成员变量列表;
  • 类的属性列表;
  • 类的方法列表;
  • 类的协议列表。

2 isa指针(结构体)

在64位系统之前,isa是一个普通的指针,存放着类对象和元类对象的地址值,在64位系统里面,isa变成了一个共用体结构(union),同时使用位域来存储更多的信息。(这里涉及到共用内存以及位域的概念)网上查找补充的

isa结构体

union isa_t 
{
    Class cls;
    uintptr_t bits;
    struct {
         uintptr_t nonpointer        : 1;//->表示使用优化的isa指针
         uintptr_t has_assoc         : 1;//->是否包含关联对象
         uintptr_t has_cxx_dtor      : 1;//->是否设置了析构函数,如果没有,释放对象更快
         uintptr_t shiftcls          : 33; // MACH_VM_MAX_ADDRESS 0x1000000000 ->类的指针
         uintptr_t magic             : 6;//->固定值,用于判断是否完成初始化
         uintptr_t weakly_referenced : 1;//->对象是否被弱引用
         uintptr_t deallocating      : 1;//->对象是否正在销毁
         uintptr_t has_sidetable_rc  : 1;//1->在extra_rc存储引用计数将要溢出的时候,借助Sidetable(散列表)存储引用计数,has_sidetable_rc设置成1
         uintptr_t extra_rc          : 19;  //->存储引用计数
    };
};

学新通

复习-OC中的对象

要在这里讲一下isa指针的指向问题和方法的调用顺序,再次复习一下OC的对象。

实例对象(Instance)

实例对象,顾名思义就是类的实例对象.当一个类的实例对象在堆中alloc时.内存中存放的是实例对象的isa以及成员变量.至于方法.是通过isa找到对应类对象.再寻找对应的方法进行调用

类对象(Class)

OC的类对象,其实我们上面已经讲过了.类对象存放了isa.成员变量列表.属性列表.方法列表.协议列表.但是值得注意的一点是,类对象中存放的方法为实例方法

元类对象

元类对象.是类对象的元类对象.元类对象的结构与类对象基本一致.但是存放的是类的类方法

类对象存放的是实例方法.元类对象存放的是类方法

isa的指向

对象的isa ===> 类
类的isa ===> 元类(metaClass)

元类(metaClass)的isa ===>父类 ===> RootMetaClass(根元类)

RootMetaClass(根元类)的isa ===> RootMetaClass(根元类)

类对象的superClass ===> 父类 ===> rootClass(根类) ===> nil

元类的superClass ==> 父元类 ===> metaRootClass (根元类) ===> rootClass(根类) ===> nil
学新通

方法调用顺序(不涉及消息转发)

调用实例对象方法时

  • 通过isa找到对象的类,然后查找类中的方法列表,
  • 如果找不到,通过superClass找到父类
  • 最终到根类.如果都找不到.程序崩溃(unrecognize selector)

调用类方法时

  • 通过isa找到元类,查找类方法列表
  • 层层向上.到根元类.
  • 最终到根类.根元类isa指向根类.都找不到.崩溃

2.1 union简单了解

为了理解isa_t的实现过程,简单了解一下union

在 Objective-C 中,union 是一种数据类型,它可以将多个不同的数据类型共享同一个内存空间。换句话说,它允许程序员在同一个内存地址上存储不同类型的数据。

一个 union 可以包含多个成员变量,每个成员变量可以是不同的数据类型,但是 union 只会为其中一个成员变量分配内存空间,这个空间的大小是所有成员变量中最大的一个。

2.2 isa的初始化

通过isa_t的结构可以得出根据union的定义,cls和bits这两个成员变量是互斥的,这也决定了isa指针初始化的时候有两种初始化方式

  • 通过cls初始化,bits无默认值
  • 通过bits初始化,cls有默认值、

学新通

2.2.1 objc_object::initIsa

isa的初始化会调用两层函数

objc_object::initIsa->objc_object::initIsa(Class cls, bool nonpointer, UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor)

表层:objc_object::initIsa
学新通

跳转到 objc_object::initIsa(Class cls, bool nonpointer, UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor)

学新通

inline void 
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
    ASSERT(!cls->instancesRequireRawIsa());
    ASSERT(hasCxxDtor == cls->hasCxxDtor());

    initIsa(cls, true, hasCxxDtor);
}

#if !SUPPORT_INDEXED_ISA && !ISA_HAS_CXX_DTOR_BIT
#define UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT __attribute__((unused))
#else
#define UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT
#endif

inline void 
objc_object::initIsa(Class cls, bool nonpointer, UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor)
{ 
    ASSERT(!isTaggedPointer()); 
    
    isa_t newisa(0);

    if (!nonpointer) {
        newisa.setClass(cls, this);
    } else {
        ASSERT(!DisableNonpointerIsa);
        ASSERT(!cls->instancesRequireRawIsa());


#if SUPPORT_INDEXED_ISA
        ASSERT(cls->classArrayIndex() > 0);
        newisa.bits = ISA_INDEX_MAGIC_VALUE;
        // isa.magic is part of ISA_MAGIC_VALUE
        // isa.nonpointer is part of ISA_MAGIC_VALUE
        newisa.has_cxx_dtor = hasCxxDtor;
        newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else
        newisa.bits = ISA_MAGIC_VALUE;
        // isa.magic is part of ISA_MAGIC_VALUE
        // isa.nonpointer is part of ISA_MAGIC_VALUE
#   if ISA_HAS_CXX_DTOR_BIT
        newisa.has_cxx_dtor = hasCxxDtor;
#   endif
        newisa.setClass(cls, this);
#endif
        newisa.extra_rc = 1;
    }

    // This write must be performed in a single store in some cases
    // (for example when realizing a class because other threads
    // may simultaneously try to use the class).
    // fixme use atomics here to guarantee single-store and to
    // guarantee memory order w.r.t. the class index table
    // ...but not too atomic because we don't want to hurt instantiation
    isa = newisa;
}

学新通
  • 首先是assert函数:在 Objective-C 中,assert 函数用于进行断言检查,它是一个宏定义,通常用于验证程序中的条件是否满足。如果断言的条件为假,即不满足预期,assert 函数会触发一个断言失败的错误,并终止程序的执行。
void assert(int expression);

例如

int x = 5;
assert(x > 0);  // 检查 x 是否大于 0,如果为假,则触发断言失败

如果 x 的值不大于 0,assert 函数会触发断言失败,程序会中止执行,并在标准错误流中输出相应的错误信息。

  • 接着进行isa初始化,注释写到
    isa.magicisa_magic_VALUE的一部分
    isa.nonpointerisa_MAGIC_VALUE的一部分
 newisa.bits = ISA_INDEX_MAGIC_VALUE;
        // isa.magic is part of ISA_MAGIC_VALUE
        // isa.nonpointer is part of ISA_MAGIC_VALUE
        

也就是news.bits初始化时候只设置了nonpointer,magic两个部分。

  • 接着has_cxx_dtor的赋值(->是否设置了析构函数,如果没有,释放对象更快)
newisa.has_cxx_dtor = hasCxxDtor;
newisa.indexcls = (uintptr_t)cls->classArrayIndex();

2.3 Class

  • class ObjectClass = [[nsobject class]class]; 返回的还是class对象,并不是meta-class对象.
    使用Class metaClass = objc_getMetaClass("ClassName"); 获取元类对象
  • 类对象在内存中有且仅有一个对象 主要包括 isa指针 super Class指针 类的属性信息 类的对象方法信息 类的协议信息 类的成员变量信息
  • 元类对象和class内存结构是一样的 但是用途不一样 主要有类方法的类信息 其他为空的
//     instacne对象
    NSObject *obj1 = [[NSObject alloc] init];
    NSObject *obj2 = [[NSObject alloc] init];

    //类对象
    Class objClass1 = [obj1 class];
    Class objClass2 = [obj2 class];
    Class objClass3 = [NSObject class];
    Class objClass4 = object_getClass(obj1);
    Class objClass5 = object_getClass(obj2);
    
    // 元类对象
    Class objcMetaClass = object_getClass([NSObject class]);
    Class objcMetaClass2 = [[NSObject class] class];
    
    // 获取元类对象
    Class metaClass = objc_getMetaClass("NSObject");
    NSLog(@"instance - %p %p", obj1, obj2);
    NSLog(@"class - %p %p %p %p %p %d", objClass1, objClass2, objClass3, objClass4, objClass5, class_isMetaClass(objClass3));
    NSLog(@"mateClass - %p %p %d",objcMetaClass, objcMetaClass2, class_isMetaClass(objcMetaClass));
    NSLog(@"%@", metaClass);
}

// class方法得到的都还是类函数
学新通

2.4 objc_Class的源码部分

  • 关于objc_Class的isa指针的变量已经说过了
// 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
    ...

这里需要说的是 bits

bits里面存储了类的方法列表等等信息,是class_data_bits_t类型的结构体。

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;
        }
    }

学新通

需要知道 bits里面的class_rw_t, class_ro_t
(readwrite, readonly)

2.4.1 class_rw_t, class_ro_t

class_rw_tclass_ro_t 是用来描述类信息的两个结构体

  • class_rw_t 结构体表示类的可写信息,包含了类的实例变量、属性、方法等信息。
    学新通
  • class_ro_t 结构体表示类的只读信息,包含了类的名称、父类、实例变量、属性、方法等信息。
    学新通
区别和联系
  1. 可写信息和只读信息:
  • class_rw_t 结构体表示类的可写信息,包括类的方法列表、属性列表、协议列表等。这些信息可以在运行时进行修改。
  • class_ro_t 结构体表示类的只读信息,包括类的名称、父类、实例变量、基本方法列表、基本协议列表等。这些信息在类加载时被初始化,之后不可更改。
  1. 指针引用:
  • class_rw_t 结构体中的 ro 指针指向对应类的 class_ro_t 结构体,用于访问类的只读信息。
  • class_ro_t 结构体中的 baseMethodsbaseProtocolsivarsweakIvarLayoutbaseProperties 等指针分别指向对应的方法列表、协议列表、实例变量列表、弱引用实例变量布局、属性列表等。
  1. 功能和用途:
  • class_rw_t 结构体用于描述类的可写信息,可以用来修改类的方法、属性、协议等,以及添加新的方- 法。
  • class_ro_t 结构体用于描述类的只读信息,包括类的静态属性、方法列表、协议列表等,用于获取类的基本信息。
    通过runtime动态修改类的方法时,其实是修改在class_rw_t区域中存储的方法列表
    学新通
    ro编译阶段生成,rw运行的时候生成。从存储的内容角度来讲,ro中有方法、属性、协议和成员变量,而rw中并没有成员变量。rw中的方法属性协议的取值方法中,也是通过取ro或者rwe中的值来获得。ro中的方法、属性、协议都是base,也就是只有本类中的方法属性和协议
总结

编写代码运行后,开始类的方法,成员变量 属性 协议等信息都存放在 const class_ro_t
运行过程中,会将信息整合,动态创建 class_rw_t
然后会将 class_ro_t中的内容(类的原始信息:方法 属性 成员变量 协议信息) 和 分类的方法 属性 协议 成员变量的信息 存储到 class_rw_t中.并通过数组进行排序,分类方法放在数组的前端,原类信息放在数组的后端.
初始运行时 objc-class中的 bits是指向 class_ro_t的,bits中的data取值是从class_ro_t中获得,而后创建 class_rw_t,class_rw_t中的 class_ro_t从初始的 class_ro_t中取值(所以这两个不是一个对象),class_rw_t初始化完成后,修改 objc_class中的bits指针指向class_rw_t

2.4.2 cache_t
struct cache_t {
	    struct bucket_t *buckets() const;
	    mask_t mask() const;
	    mask_t occupied() const;
}

这里引入了bucket_t(散列表),是cache的内部哈希结构
bucket_t中存储的是SEL和IMP的键值对
学新通

2.4.5 property_t

只储存了其name和属性关键字

struct property_t {
    const char *name; // 类型的指针,表示属性的名称。
    const char *attributes; // 类型的指针,表示属性的特性,包括读写权限、属性类型、内存管理等信息。
};

  • 在真正访问的时候,访问的实例变量(该结构体只是属性信息的一个描述,不包含实际的属性值。在实际使用中,需要将属性信息与具体的对象实例关联起来,才能真正地使用属性。)

2.5 category不能添加成员变量

在之前关联对象的源码也学习过,因为因为category是运行时添加的,他只能操作class_rw_t结构,但是class_rw_t结构没有添加成员变量的入口。成员变量是存储在class_ro_t中的,是无法被修改的,所以category就不能添加成员变量。

2.6 类方法为什么在元类里面?

  • 单一职责设计原理。
    实例对象存储成员变量的值,类对象存放,实例方法、协议、成员变量、属性,元类对象存放类方法,各司其职,互不影响。
  • 复用msgSend消息发送机制。
    类方法、实例方法是在上层的定义,在底层并不区分类方法实例方法,但在runtime这一层,需要承接上层类方法和实例方法,对接到底层方法调用。使用了msgSend,如果msgSend的时候需要再区分类对象,实例对象,会在内部增加判读逻辑,从而降低了效率,有了元类的存在,问题迎刃而解。

OC类的信息存放在哪里

  • 对象方法、属性、成员变量、协议信息,存放在class对象中
  • 类方法存放在meta-class对象中(元类对象和class内存结构是一样的 但是用途不一样 主要有类方法的类信息 其他为空的)
  • 成员变量的具体值存放在instance对象中

最后的点

  • 对于类和对象 这篇学习从 类 父类 元类开始,到分析 对象的结构 类的结构学习到了isa指针 知道类的本质也是对象,接着了解到了union,知道了isa的初始化两种方法,接着学习objcet_class的源码 学习到了ClassISA的cache和bits,进一步了解了编译和运行时期的过程总结,通过类和元类的学习,知道了类调用方法和对象调用方法的顺序和区别,最后总结了一点小小的问题,至少对类和对象的内部结构有了更深的理解,在之后的源码学习过程和消息转发等机制中打了基础。

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhgcbbhi
系列文章
更多 icon
同类精品
更多 icon
继续加载