类的结构探究分析

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

对象的本质文章中,通过.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中)。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343

推荐阅读更多精彩内容