美文网首页
类的结构探究分析

类的结构探究分析

作者: 囤囤fc | 来源:发表于2021-08-13 19:09 被阅读0次

之前我们研究了对象创建流程对象内存对齐算法解析对象的本质,今天我们开始对类的本质进行探究。

对象的本质文章中,通过.cpp文件其实已经对一个类所包含的内容有了初步了解。现在创建一个类,包含属性,成员变量,实例方法和类方法,我今天将带大家通过源码和lldb的方式来找到他们在类中储存的位置,并尝试取出来!

image.png

类的本质

既然研究的是类,那么类 - Class的本质是什么,是什么结构,包含什么内容呢?之前我也介绍过NSObject对应底层就是objc_object, Class对应底层为objc_class。直接在源码中搜索objc_class找到他的定义可以得知:

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
}
  • Class/objc_class 是结构体;
  • objc_class继承自objc_object(万物皆对象),并继承了隐藏属性isa;
  • 成员变量包括:类的isa,指向父类的指针superclass, cache和bits;

通过objc_class中成员变量的类型名称和注释,我们可以得出cache_t类型就是类的缓存数据,、class_data_bits_t是什么?注释中的class_rw_t又是什么?objc_class定义向下找到了class_rw_t:

class_rw_t *data() const {
        return bits.data();
    }

进入class_rw_t查看定义,发现他是一个结构体,成员变量翻到下面看到了我们熟悉的内容:

  const method_array_t methods() const {
        auto v = get_ro_or_rwe();
        if (v.is<class_rw_ext_t *>()) {
            return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->methods;
        } else {
            return method_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseMethods()};
        }
    }

    const property_array_t properties() const {
        auto v = get_ro_or_rwe();
        if (v.is<class_rw_ext_t *>()) {
            return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->properties;
        } else {
            return property_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseProperties};
        }
    }

    const protocol_array_t protocols() const {
        auto v = get_ro_or_rwe();
        if (v.is<class_rw_ext_t *>()) {
            return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->protocols;
        } else {
            return protocol_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseProtocols};
        }
    }

得出结论:类包含的属性变量,方法存在class_data_bits_t bits中。

获取类中的bits

有了目标后,我们就要先想办法把bits取出来才能具体研究bits中的data。工程里创建一个FCPerson对象,打上断点,跑起来,然后开始使用lldb:

  • 使用x/4gx FCPerson.class打印出FCPerson的内容:
    image.png
    可以知道输出的结果就是objc_class的成员变量,但是对照着objc_class的定义,如何取出bits呢?
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
}

目前我们已有的是FCPerson的首地址,尝试通过内存平移的方法找到bits。已知的是objc_class中isa所占字节为8,Class superclass所占字节为8,cache_t所占多少字节呢�?进去看cache_t的源码,去掉无用代码后如下:

struct cache_t {
private:
    explicit_atomic<uintptr_t> _bucketsAndMaybeMask;
    union {
        struct {
            explicit_atomic<mask_t>    _maybeMask;
#if __LP64__
            uint16_t                   _flags;
#endif
            uint16_t                   _occupied;
        };
        explicit_atomic<preopt_cache_t *> _originalPreoptCache;
    };
}

可以看到cache_t为结构体,第一个成员变量explicit_atomic<uintptr_t> _bucketsAndMaybeMask大小为8字节(typedef unsigned long uintptr_t;),第二个成员变量是一个联合体,explicit_atomic<mask_t> _maybeMask占4字节(typedef uint32_t mask_t),_flags_occupied分别占2字节,explicit_atomic<preopt_cache_t *> _originalPreoptCache;占8字节(preopt_cache_t *是指针类型)。

得出结论:cache_t所占大小为16字节。

接下来,我们通过类首地址0x00000001000042a8进行平移8(isa) + 8(superClass) + 16(cache)得到:

image.png
0x00000001000042f0是不是bits呢?我们先把他强转一下:
image.png
查看class_data_bits_t定义,可以看到他包含着关于class_rw_t* data()的方法:
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;
    }

我们尝试调用一下:

image.png
调用成功,得到了class_rw_t类型的结果!并且还提示了我们调用方法要使用->,强迫症的我就重新来一遍:
image.png
至此,成功取出了bits并且得到了class_rw_t类型的数据集合!

class_rw_t中找出属性

重新拿出刚刚在class_rw_t定义中看到的内容:

const method_array_t methods() const {
        auto v = get_ro_or_rwe();
        if (v.is<class_rw_ext_t *>()) {
            return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->methods;
        } else {
            return method_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseMethods()};
        }
    }

    const property_array_t properties() const {
        auto v = get_ro_or_rwe();
        if (v.is<class_rw_ext_t *>()) {
            return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->properties;
        } else {
            return property_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseProperties};
        }
    }

先从属性列表properties来测试:

image.png
可以看到property_array_tlist_array_tt类型,其中的property_t定义为:
struct property_t {
    const char *name;
    const char *attributes;
};

property_t就是我们最终要获取到的属性,先取出list:

image.png
(同样的命令p &17.list居然时好时坏-。-)

取出list中的ptr得到一个property_list_t *const数组:

image.png

使用*输出property_list_t的内容(隔了一天,19变成了23 -。-):

image.png
得到了新类型的数据,并且还看到entsize_list_ttcount = 2
进入entsize_list_tt的定义看到他是一个结构体,其中包含了一个get方法:
Element& get(uint32_t i) const { 
        ASSERT(i < count);
        return getOrEnd(i);
    }
Element& getOrEnd(uint32_t i) const { 
        ASSERT(i <= count);
        return *PointerModifier::modify(*this, (Element *)((uint8_t *)this + sizeof(*this) + i*entsize()));
    }

尝试调用get方法:

image.png
得到了name属性!因为已知count = 2所以再次调用得到age:
image.png
再次调用get(2)会怎样?数组越界咯,大家自己去试~

至此属性的获取已经完成,接下来用同样的方法获取方法!

class_rw_t中找出方法:

image.png
采用同样的方法获取method发现问题:对entsize_list_tt使用get()方法时拿到的方法都是空的。
跟属性相同,我们要获取的目标为method_t,对比method_tproperty_t的源码会发现method的成员变量不在表层,截取核心代码如下:
struct method_t {
    method_t(const method_t &other) = delete;

    struct big {
        SEL name;
        const char *types;
        MethodListIMP imp;
    };

big &big() const {
        ASSERT(!isSmall());
        return *(struct big *)this;
    }
}

由源码可得在method_t中的big结构体才是我们需要的结果,我们来试一试:

image.png
至此,得到了属性的setter,getter方法,我们定义的instanceMethod方法,和一个.cxx_destruct方法,经查阅资料得知.cxx_destruct方法是在ARC模式下,将所有的成员变量变成nil相当于MRC模式下的dealloc,同时在init方法中也找到了.cxx_destruct的赋值:
void sel_init(size_t selrefCount)
{
#if SUPPORT_PREOPT
    if (PrintPreopt) {
        _objc_inform("PREOPTIMIZATION: using dyld selector opt");
    }
#endif

  namedSelectors.init((unsigned)selrefCount);

    // Register selectors used by libobjc

    mutex_locker_t lock(selLock);

    SEL_cxx_construct = sel_registerNameNoLock(".cxx_construct", NO);
    SEL_cxx_destruct = sel_registerNameNoLock(".cxx_destruct", NO);
}

获取成员变量和类方法

至此我们已经获取了属性和实例方法,但是属性中没有看到成员变量,方法列表里也没有类方法,我们来继续探究,先来成员变量:

抄近路:在我查看property和method区别时,看到了ivar_t的定义,这应该就是成员变量,我通过搜索ivar_t层层倒推:ivar_t->ivar_list_t->class_ro_t,最终在class_rw_ext_t中找到了class_ro_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;
};

知道了寻找路径就很容易啦~:

image.png
成功获取成员变量hobby~

最后还剩类方法,按照class_rw_ext_t的定义来看,类方法应该只可能在methods里面,我重新阅读了一遍methods的源码以及method_t的相关定义,都没有找到类方法。结合isa的走位图和之前看过的一句话:类方法是父类的实例方法,我就想利用获取methods的流程对FCPerson的父类NSObject走一遍,看看能不能找到,结果:

image.png

这个count很明显不对,看来在NSObject中找是不靠谱的。。后来我又想到,isa的走位,在走到根类之前不是应该先走元类吗,我就尝试着获取FCPerson的metaClass:

image.png
虽然metaClass打印出来仍然显示的是FCPerson,但是从首地址判断显然跟x/4gx FCPerson.class得到的首地址是不同的,所以我们拿到的就是FCPerson的metaClass。
接下来我们利用之前的步骤向下探索,最终得到了FCPerson的类方法
image.png

同样的命令,有时候不行,得多试几次,最后p *这一步失败过,差点以为方向出错:


image.png

至此,我们已经通过源码和lldb获取到了一个类的属性(properties中),成员变量(ro中),实例方法(methods中)以及类方法(metaClass的methods中)。

相关文章

  • 类的结构探究分析

    之前我们研究了对象创建流程[https://www.jianshu.com/p/6650f5d973dc],对象内...

  • iOS底层之类的结构分析

    从iOS底层之isa结构分析及关联类我们探究了类的实例对象的内存结构,对象指针的首地址存储了isa,也就是存储了类...

  • 类结构探究(三)-- cache分析

    之前我们已经探究了bits的结构,本文将对类的一个重要成员--cache,从源码objc4-7.8.1层面进行分析...

  • iOS底层探究-类的结构分析

    前言 上篇博客说完了对象的成员博客-isa结构分析,今天我们来研究一下对象的爸爸,他就是类,相信大家都知道对象是由...

  • iOS类的结构分析之cache

    前言 在类的结构探究分析[https://www.jianshu.com/p/338040da01cb]中,我们了...

  • 类结构探究(二)-- bits结构探究

    在上一篇文章中我们已经探究了isa和superclass的指向问题,本文将通过lldb调试,探索objc_clas...

  • iOS底层原理探究 - 类结构分析

    前言 等风来不如追风去,总有那么一个人在这风景正好的季节来到你的身边。在上一篇探究了对象的本质和isa指针的底层,...

  • OC底层探究(4)-- 类的结构分析

    类的结构 老规矩,还是从源码搞起。我们先在main.m中定义一个类ZPerson,继承自NSObject。 然后通...

  • objc_msgSend底层之快速查找流程

    在上一篇文章类结构探究(三)-- cache分析中已经了解到,方法会保存到类的cache中,那么缓存的方法是如何查...

  • 类的探究分析

    准备工作 内存偏移 普通指针代码分析: 好明显a和b虽然值是一样的,但是它们的地址却不一样。这就是我们常说的深拷贝...

网友评论

      本文标题:类的结构探究分析

      本文链接:https://www.haomeiwen.com/subject/ntkjbltx.html