iOS底层原理_04:类的原理分析(上)

第四节课 类的原理分析(上)

isa分析到元类

p/x p  //拿到当前指针地址

04-拿地址.png

我们通过x p指令可以验证下,看到打印出的地址与我们拿到的地址一模一样。

接下来我们
x/4gx 0x000000010292f480

04-isa.png

0x001d800100008365 就是我们的对象的isa

p/x 0x001d800100008365 & 0x00007ffffffffff8 po 0x0000000100008360 //就是我们当前的类

04-当前的类.png

其实最后还是这么个地址,有那么一丢丢好奇,再次x/4g会得到什么,干就完了~

04-x:4g类.png

可以看到,打印出来的结果里跟上面的指针对象拥有同样的内存结构,那第一个地址是啥?还是isa嘛?
04-po.png

又一次得到LGPerson,0x0000000100008360LGPerson0x0000000100008338还是LGPerson,小朋友,你是否有很多问号??
猜想:类会和我们的对象无限开辟,内存不止有一个类
接下来就来验证下我们的猜想

    Class class1 = [LGPerson class];
    Class class2 = [LGPerson alloc].class;
    Class class3 = object_getClass([LGPerson alloc]);
    Class class4 = [LGPerson alloc].class;
    NSLog(@"\n%p \n%p \n%p \n%p",class1,class2,class3,class4);
    
    <----输出结果---->
0x100008360 
0x100008360 
0x100008360 
0x100008360

我们可以看到最终输出的类都是0x100008360,那就证明之前的0x0000000100008338它不是类,那么它是个什么?接下来我们就需要使用烂苹果来分析了

04-烂苹果.png

在这里我们看到了熟悉的东西,但是并没有看到0x0000000100008338这个东西

当我们看到符号表的时候看到了一个奇奇怪怪的东西


04-METACLASS.png

这个METACLASS实际上并不是我们通过代码创建的,这个是系统或者编译器帮我们生成好的,这个东西就叫做元类
我们得到了一个大概的流程:对象isa -> 类isa -> 元类

isa走位图和继承链

isa走位图

上面我们通过isa分析出了一个元类,那我们就想,元类之后会不会还有东西?
继续用上面元类的isa

04-根元类.png

po打印输出的是NSObject,&上我们的掩码,又是他本身,这个东西就是我们的根元类
对象isa -> 类isa -> 元类 -> 根元类
我们看到了NSObject有点奇怪,那我们就跟直接打印的对比下
04-NSObject.png

我们发现,通过NSObject.class获取到的地址与刚才po的地址并不相同,但是我们继续往下x/4g的时候发现isa的位置又与po的地址相同了。就相当于是NSObject.class的isa -> NSObject的元类,只有两层
根类 isa -> 根元类 isa
04-isa流程图.png

根据上面我们分析的过程可以看到isa的链路如上图虚线部分。同时我们也可以使用代码来进行验证一下

    NSObject *object1 = [NSObject alloc];
    // NSObject类
    Class class = object_getClass(object1);
    // NSObject元类
    Class metaClass = object_getClass(class);
    // NSObject根元类
    Class rootMetaClass = object_getClass(metaClass);
    // NSObject根根元类
    Class rootRootMetaClass = object_getClass(rootMetaClass);
    NSLog(@"\n%p 实例对象\n%p 类\n%p 元类\n%p 根元类\n%p 根根元类",object1,class,metaClass,rootMetaClass,rootRootMetaClass);
    
    // LGPerson元类
    Class pMetaClass = object_getClass(LGPerson.class);
    Class psuperClass = class_getSuperclass(pMetaClass);
    NSLog(@"%@ - %p",psuperClass,psuperClass);
    
    
<----输出结果---->
0x100547790 实例对象
0x7fff9103a118 类
0x7fff9103a0f0 元类
0x7fff9103a0f0 根元类
0x7fff9103a0f0 根根元类

NSObject - 0x7fff9103a0f0

继承链

根据上面的打印结果,我们发现任何对象,元类的父类都是根元类,有点烧脑了吧?
接下来我们写了一个LGTeacher继承自LGPerson,再次通过上面代码进行打印

    // LGTeacher -> LGPerson -> NSObject
    // 元类也有一条继承链
    Class tMetaClass = object_getClass(LGTeacher.class);
    Class tsuperClass = class_getSuperclass(tMetaClass);
    NSLog(@"%@ - %p",tsuperClass,tsuperClass);
    <----输出结果---->
    LGPerson - 0x100008338

元类的继承链如下图实线部分
[图片上传失败...(image-743b09-1624451069459)]

我们继续分析NSObject特殊情况

    // NSObject 根类特殊情况
    Class nsuperClass = class_getSuperclass(NSObject.class);
    NSLog(@"%@ - %p",nsuperClass,nsuperClass);
    // 根元类 -> NSObject
    Class rnsuperClass = class_getSuperclass(metaClass);
    NSLog(@"%@ - %p",rnsuperClass,rnsuperClass);
    <----输出结果---->
(null) - 0x0
 NSObject - 0x7fff9103a118

根据上述结果,我们推出了图中RootClass(class) -> nil的这一步
以及RootClass(meta) -> RootClass(class)这一步

小结:

  1. NSOject对象的元类根元类同一个
  2. 元类间存在着继承的关系,跟类是一样的
  3. 根元类的父类指向了NSObject
  4. NSObject的父类是(null),地址为0x0,即NSObject没有父类

指针和内存平移

如果要想获取对象内存中的变量,底层实现方式是对象的首地址+偏移值。下面探究下内存偏移
我们平时遇到的指针分为三种:普通指针、对象指针、数组指针

普通指针

int a = 10; 
int b = 10; 
NSLog(@"%d -- %p",a,&a);
NSLog(@"%d -- %p",b,&b);
<----输出结果---->
10 -- 0x7ffeefbff41c
10 -- 0x7ffeefbff418
  • 我们发现,值是一样的,但是指针地址却是不同的,这就是我们所说的copy:值拷贝
  • a的地址是0x7ffeefbff41cb的地址是0x7ffeefbff418,相差4字节,主要取决于a的类型

对象指针

HZMPerson *p1 = [HZMPerson alloc];
HZMPerson *p2 = [HZMPerson alloc];
NSLog(@"%@ -- %p",p1,&p1);
NSLog(@"%@ -- %p",p2,&p2);
<----输出结果---->
<HZMPerson: 0x1007049f0> -- 0x7ffeefbff410
<HZMPerson: 0x100704650> -- 0x7ffeefbff408

我们发现,对象的地址不同,地址指向的空间也不同

数组指针

int c[4] = {1,2,3,4};
int *d   = c;
NSLog(@"%p - %p - %p",&c,&c[0],&c[1]);
NSLog(@"%p - %p - %p",d,d+1,d+2);
<----输出结果---->
0x7ffeefbff430 - 0x7ffeefbff430 - 0x7ffeefbff434
0x7ffeefbff430 - 0x7ffeefbff434 - 0x7ffeefbff438

  • 数组的地址就是数组元素中的首地址,即&c&c[0]都是首地址
  • 数组中每个元素之间的地址间隔,由当前元素的数据类型决定
  • 数组的元素地址可以通过首地址+n*类型大小方式,这种方式是数组中的元素类型必须相同
  • 数组元素不相同用首地址+偏移量方式,根据当前变量的偏移值(需要前面类型大小相加)

小结:

  • 内存地址就是内存元素的首地址
  • 内存偏移可以根据首地址+ 偏移值方法获取相对应变量的地址

源码分析类的结构

在之前的探索时我们发现isaClass类型的。Class类型是objc_class *objc_class是一个结构体。 所有的Class底层实现都是objc_class。又来到了我们的源码分析部分,在 objc4-818.2 中全局搜索objc_class 代码如下

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 flags

    Class getSuperclass() const {
#if __has_feature(ptrauth_calls)
#   if ISA_SIGNING_AUTH_MODE == ISA_SIGNING_AUTH
        if (superclass == Nil)
            return Nil;
            
//省略部分源码
}

objc_class这个结构体内部存储了几个成员变量,怎么去探究它们呢?类的地址是知道的,那么就根据上面探究过的首地址+偏移值来获取里面的成员变量的地址,然后获取值。但是偏移值需要知道当前变量之前的所有成员变量的大小

  • Class ISA://结构体指针占8字节 继承自objc_object
  • Class superclass:结构体指针占8字节
  • cache_t cache:??母鸡
  • class_data_bits_t bits;

计算 cache 类的内存大小

接下来我们就先看cache的内存大小,查看cache_t

struct cache_t {
private:
    explicit_atomic<uintptr_t> _bucketsAndMaybeMask; // 8
    union {
        struct {
            explicit_atomic<mask_t>    _maybeMask;   // 4
#if __LP64__
            uint16_t                   _flags;       // 2
#endif
            uint16_t                   _occupied;    // 2
        };
        explicit_atomic<preopt_cache_t *> _originalPreoptCache; //8
    };
    
    //下面是一些方法省略
};

cache_t结构体类型,有两个成员变量_bucketsAndMaybeMask和一个联合体

_bucketsAndMaybeMaskuintptr_t无符长整型占8字节

联合体里面有两个成员变量结构体_originalPreoptCache,联合体的内存大小由成员变量中的最大变量类型决定
_originalPreoptCache 是结构体指针占8字节
结构体中有_maybeMask_flags_occupied_maybeMaskuint32_t4字节_flags_occupieduint16_t 各占2字节结构体大小是8字节

cache_t的内存大小是 8+8 或者是8+4+2+2都是16字节,所以cache就是16字节

*isa的内存地址是首地址,前面已经探究了

  • superclass的内存地址是首地址+0x8
  • cache的内存地址是首地址+0x10
  • bits的内存地址是首地址+0x20

具体cache里面的内容我们后续再进行讲解,我们先看看bits这个东西

获取bits

所以有上述计算可知,想要获取bits的中的内容,只需通过首地址平移32字节即可

以下是通过lldb命令调试的过程


04-bits.png

ps:获取类的首地址有两种方式

  1. 通过p/x LGPerson.class直接获取首地址
  2. 通过x/4gx LGPerson.class,打印内存信息获取

$2指针的打印结果中可以看出bits中存储的信息,其类型是class_rw_t,也是一个结构体类型。但我们还是没有看到属性列表、方法列表等,需要继续往下探索

探索 属性列表,即 property_list

通过查看class_rw_t定义的源码发现,结构体中有提供相应的方法去获取 属性列表、方法列表等,如下所示

04-class_rw_t.png

在获取bits并打印bits信息的基础上,通过class_rw_t提供的方法,继续探索 bits中的属性列表,以下是lldb探索的过程图示

04-获取属性列表2.png

  • p $3.properties()命令中的propertoes方法是由class_rw_t提供的,方法中返回的实际类型为property_array_t

  • 由于list的类型是property_list_t,是一个指针,所以通过 p *$6获取内存中的信息,同时也证明bits中存储了 property_list,即属性列表

  • p $7.get(2),想要获取LGPerson中的下一个成员变量,发现提示数组越界了,说明 property_list 中只有两个属性

目前我们的LGPerson中只有两个属性,那么我们添加一些其他的东西后,property_list会有什么变化呢?

04-新增成员变量.png

我们新增了一个成员变量,重复上面步骤结果发现,还是只能取出两个属性,由此可得出property_list 中只有属性,没有成员变量与方法。那成员变量哪去了?

通过查看objc_classbits属性中存储数据的类class_rw_t的定义发现,除了methods、properties、protocols方法,还有一个ro方法,其返回类型是class_ro_t,通过查看其定义,发现其中有一个ivars属性,我们大胆猜测:是否成员变量就存储在这个ivar_list_t类型的ivars属性中呢?下图是lldb命令的调试流程

04-成员变量.png

通过{}定义的成员变量,会存储在类的bits属性中,通过bits -> data() ->ro() -> ivars获取成员变量列表,除了包括成员变量,还包括属性定义的成员变量
通过@property定义的属性,也会存储在bits属性中,通过bits -> data() -> properties() -> list获取属性列表,其中只包含属性

探索methods_list

上面我们新增了成员变量,现在我们添加两个方法来看看


04-新增方法.png

继续上面的lldb调试


04-methodslist.png

但是我们发现直到我们取到越界,打印出来的方法都是空的方法,这是为啥?
这个其实是因为
property_t的底层是存在两个成员变量的

struct property_t {
    const char *name;
    const char *attributes;
};

而我们的method_t则没有

struct method_t {
    static const uint32_t smallMethodListFlag = 0x80000000;

    method_t(const method_t &other) = delete;

    // The representation of a "big" method. This is the traditional
    // representation of three pointers storing the selector, types
    // and implementation.
    struct big {
        SEL name;
        const char *types;
        MethodListIMP imp;
    }; 

但是我们可以看见 有一个结构体big,里面则存在着成员变量name,我们取一下试试

04-big.png

可以取出来了,但是发现并没有类方法。在之前,我们曾提及了元类,类对象isa指向就是元类元类是用来存储类的相关信息的,所以我们大胆的猜一下:是否类方法存储在元类的bits中呢?可以通过lldb命令来验证我们的猜测。下图是lldb命令的调试流程

04-类方法.png

通过打印,我们看到了之前的类方法,证明了我们的猜想成立

实例方法存储在类的bits属性中类方法存储在元类的bits属性中。通过bits/元类bits -> methods() -> list获取实例方法列表

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容