美文网首页
006--iOS底层 - 类的结构(属性、成员变量、方法的探索)

006--iOS底层 - 类的结构(属性、成员变量、方法的探索)

作者: Mr_wick | 来源:发表于2021-06-28 16:55 被阅读0次

引言

上一篇讲到了内存偏移的知识和操作,接下来内存偏移将在本文用到具体的示例。我们对对象的探究已经了解了对象的底层结构,isa的走向和对象的继承链。本文将还原探究类内部结构的过程。

类的探索

一、寻找objc_class

001-OC对象原理探究 - alloc这篇文章中,我们用到了objc源码环境调试。本文我们也在此基础上,探究类的结构。我们在对象的底层结构探索的时候,发现了类Class的底层为typedef struct objc_class *Class;也就是说,Class是一个结构体指针的别名。
具体寻找步骤:
1、objc源码调试环境工程,搜索Class {,在结果中我们得到runtime.h中的class结构:

image.png
2、这似乎就是我们要找的Class底层,但是看到#if !__OBJC2__以及OBJC2_UNAVAILABLE,才知道,整个结构体struct objc_class并不适用于objc2中(本文调试的环境的是objc4_818_2
3、那么我们怎么找到正确的Class呢?请看框起来的注释/* UseClassinstead ofstruct objc_class **/
源码如下:
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;
/// Represents an instance of a class.
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};
/// A pointer to an instance of a class.
typedef struct objc_object *id;

在此源码中,我们还得到一个信息:OC中id类型的底层竟然是typedef struct objc_object *id;,这就是为什么我们在定义id类型的变量时,不加*号的原因。
探索源码真的能学到很多!!!
4、搜索框输入struct objc_class,其中objc_runtime-new.h中的结果就是我们想要的,结果如下:

image.png

二、bits

上图的objc_class内部可知bitsClass superclasscache_t cache之后。我们调试得到bits,需要上篇文章提到的内存偏移来得到。因此,我们需要知道偏移了多少字节,接下来我们开始探索Class superclasscache_t cache的内存大小。
1、superclassClass指针类型,因此superclass8字节
2、cache_t为结构体类型,其内部结构如下:(由于我们需要知道结构体内存的大小,只需要知道其成员变量的大小即可,cache_t内部的static和方法函数均不影响结构体内存大小,因此以下源码为简化后的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;
    };
}

3、分析
a)、_bucketsAndMaybeMaskexplicit_atomic的泛型变量,因此实际大小为泛型的大小,即uintptr_t的大小,uintptr_t的源码为:typedef unsigned long uintptr_t;因此占8字节
b)、union为共用体,内存大小为最大的成员的大小。
1)struct中,_maybeMaskmask_t,源码为typedef uint32_t mask_t;4字节uint16_t大小为2字节。结构体最大占用内存4 + 2 + 2 = 8字节
2)_originalPreoptCachepreopt_cache_t *,结构体指针类型,我们知道指针类型的大小为8字节
3)其实换个角度来看,union中,我们只需要看_originalPreoptCache的大小即可知道union占用大小为8字节
4、cache_t所占内存大小为:16字节
到此为止,我们只需要或许到类的首地址后,将其平移isa:8 + superclass:8 + cache_t:16 = 32字节才能得到bits。简化后的objc_class源码如下:


struct objc_class : objc_object {
    Class ISA; //8字节
    Class superclass;// 8字节
    cache_t cache;             // 16字节
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
}

下面我们开始获取bits
1、QLPerson设计如下:

@interface QLPerson : NSObject{
    NSString *fullName;
}
@property (nonatomic,copy) NSString *nickName;
@property (nonatomic,assign) NSInteger age;
- (void)test1;
+ (void)test1;
@end

2、对QLPerson类进行lldb调试
3、p *$2->data()objc_class中的方法

class_rw_t *data() const {
   return bits.data();
}
image.png
但是似乎未能得到我们想要的东西。换种思路继续
4、我们继续objc_class向下翻找,治世之尊没有找到能看到类的属性和方法的关键词。后来看到bits后面的注释,我们要的东西是否在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};
        }
    }

methods()properties()protocols()这正是我们日思夜想的东西嘛。lldb调试如下:

image.png 由图可知:我们的@property声明的属性,均存在property_list_t中,用上图的lldb调试可以找到属性的值。但是问题来了,我们的方法成员变量均未得到,它们放在哪儿呢?
补充:properties()返回类型为property_array_t,继承自list_array_tt,源码如下:
struct property_list_t : entsize_list_tt<property_t, property_list_t, 0> {
};

说明:关于二维数组容器list_array_tt的知识请移步这篇文章
5、properties()探索结束,未能达到我们的目的,我们接着探索methods()

image.png
说明:我们用探索properties的方式来探索methods最终得到了该类的实例方法getter setter方法,达到部分目的,因为,我们的+(void)test2还未出现。
6、methods()探索结束我们仍未找到类方法成员变量ivar的存储位置,我们接着往下探索,在class_rw_t内找到一个ro()方法,源码如下:
const class_ro_t *ro() const {
        auto v = get_ro_or_rwe();
        if (slowpath(v.is<class_rw_ext_t *>())) {
            return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->ro;
        }
        return v.get<const class_ro_t *>(&ro_or_rw_ext);
    }

class_ro_t内部部分源码为:

struct class_ro_t {
    void *baseMethodList;
    const ivar_list_t * ivars;

这个ivars操作如下:

image.png
到此为止,我们拿到了成员变量的存储位置已经搞清楚。

7、类方法探索,换种思路,实例方法也叫做对象方法类方法似乎与对象无关,那么它是否在元类里呢?按照这个思路,我们对QLPerson的元类进行上面的查找操作:

image.png
由图可得到类方法存储在元类中

总结

1、探索类的结构是一个漫长而复杂的过程,有些地方卡在那里,如果不转换思路,将进入死胡同。对类的探索,应该多借鉴前辈的肩膀,偶尔用上帝视角去解决遇到的难题。
2、存储位置:
类的首地址+0x20得到bits
bits->data()得到class_rw_t
a)获取类的属性(@property标记):bits中的class_rw_t中的properties()
b)获取成员变量(类大括号内的声明):bits中的class_rw_t中的ro()
c)获取实例方法(也叫对象方法-()):bits中的class_rw_t中的methods(),每一项需要加.big()来打印
d)获取类方法(+()):元类中的bits->class_rw_t->methods(),每一项需要加.big()来打印

3、以method_array_t为例,结构图如下:

image.png

相关文章

网友评论

      本文标题:006--iOS底层 - 类的结构(属性、成员变量、方法的探索)

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