美文网首页
iOS底层探索 --- 类的结构探索(上)

iOS底层探索 --- 类的结构探索(上)

作者: Jax_YD | 来源:发表于2021-06-25 15:03 被阅读0次
    image

    今天我们将进行类的结构体的探索,其中有些内容我们在iOS底层探索 ---Runtime(一)--- 基础知识有做过探索。不过没关系,今天我们再来回顾一下。

    本章我们将探索一下内容:

    1、类的结构(类,元类,根类之间的关系)

    2、objc_object & objc_class

    3、类中信息的探索


    1、类的结构(类,元类,根类之间的关系)

    1.1 类(Class)

    我们都知道,我们创建的基本上都继承自NSObject,而NSObject里面有一个默认参数isa;也就是说,继承自NSObject里面都会有一个isa。如下:

    image

    假设我们现在有一个Person类,那么Person对象的首地址就是isa地址,同样的isa也代表当前的类:

    image

    1.2 元类(Meta Class)

    这个时候,我们如果继续查看类地址,会打印什么呢?下面我们就来尝试一下:

    image
    可以看到,继续打印类地址,输出的是类的内存分布
    我们还可以通过x Person.class来查看类的内存分布 image
    • 此时有一点大家注意了,我们在上面通过isa打印出来了Person。对应的类地址是0x00000001000080e8
    • 同时我们还打印出了类的内存分布信息。如果我们继续打印类的内存分布信息的首地址,会发生什么呢?打印结果如下:
    image

    大家可以看到,此时我们同样打印出了Person。这就奇怪了!!!

    • 第一点:首先我们通过isa打印出Person,这一点相信大家没有什么问题。(有疑问的同学可以参考这一篇文章iOS底层探索 --- OC对象原理(下)

    • 第二点:为什么我们通过0x1000080e8也能都打印出Person呢?明明是两个不同的地址呀!!!这里就要引出元类(Meta Class)这个知识点了

    1、Meta Class(元类)就是类对象所属的。一个对象所属的类叫做类对象,而一个类对象所属的类就叫做元类

    2、Meta Class(元类)的定义和创建,都是由编译器自动完成。

    3、所有的类方法,都存储在Meta Class(元类)中。


    1.3 根类

    在上面我们已经找到了Meta Class (元类),那我们继续往下去寻找又会有什么发现呢?

    image

    我们会发现,居然指向了NSObject

    此时我们我们如果执行p/x NSObject.class这句指令,打印出来的NSObject地址会是什么呢?

    image

    此时有没有发现问题,NSObject的地址不一样呀。这是怎么回事呢?

    这是因为p/x NSObject.class中打印的NSObject并不是根类,而是根元类。怎么证明呢?我们只需要打印一下NSObject.class的内存信息,就会看到起isa上面打印的NSObject的地址是一样的。

    换句话说,就是元类isa 指向了根元类

    (注:严谨一点是要& ISA_MASK,这里由于isa中只有shifcls信息,可以直接看出来)。执行结果如下:

    image
    • 此时我们已经跟踪到了根类元类,如果我们在根类元类的基础上,继续追踪,又会出现什么情况呢?那我们就继续试一下:
      image
      大家有没有发现,根类元类isa指向了自己。

    此时就回到我们在iOS底层探索 ---Runtime(一)--- 基础知识中探索过的那幅图:

    [图片上传失败...(image-caaf2f-1624499873237)]


    1.4 类对象在内存中的个数

    类对象在内存中,有且仅有一份!!!
    这也是一道经常被问到的面试题,下面我们来证明一下:
    我们创建多个类对象,然后打印,看一下地址是否相同

    image

    通过打印可以发现,虽然创建了多个类对象,但是地址都是一样的。因此我们可以说:类对象在内存中,有且仅有一份。


    1.5 类的继承关系的坑点

    • 假设有一个类Man,继承自类Person;同时Man有一个对象mPerson有一个对象p
      那么问:mp有什么关系?
      答案:没有关系。(注意:一定不要说是继承关系)

    • 我们都知道子类继承自父类父类继承自根类
      那么问:根类继承自什么?
      答案:根类的继承关系来自于nil。(注意:根类不是继承自自己)


    2 objc_object & objc_class

    大家还记不记得,我们在iOS底层探索 --- OC对象原理(下)中,在我们转换mian.cpp文件中有这样一段代码:

    image

    当时我们只是对objc_classobjc_object简单的讲解了一下。那么它们两个具体是什么样子的,又有什么样的关系呢?今天我们来详细的探索一下。

    2.1 源码寻找

    首先我们要在源码中去寻找objc_objectobjc_class到底长什么样子。

    • objc_class
      我们在源码中搜索objc_class的时候,会出现两个不同的定义,其中有一个是已经废弃的。

    • 废弃的objc_class:

      image
    • 新的objc_class(可以看到新的objc_class继承自objc_object):

    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_rw_t *data() const {
           return bits.data();
       }
       void setData(class_rw_t *newData) {
           bits.setData(newData);
       }
    
       ......
       ......
       ......
    }
    
    • objc_object
      同样的,在源码中搜索objc_object的时候,也搜索到两个
    • 位于objc.h文件里面的:
    image
    • 位于objc-privat.h文件里面的:
    image

    通过main.cpp文件,我们可以确定,使用的是objc.h里面的objc_object(看注释,这个是给类的实例对象用的)。


    2.2 objc_classobjc_object之间的关系

    1. 结构体objc_class继承自结构体objc_object

    2. objc_object有一个isa属性,所以objc_class也有一个isa。(在新的objc_class中有一段注释// Class ISA;,也从侧面表现了这一点。)

    3. NSObject也拥有isa。因为NSObject在底层的表现形式就是objc_object。或者说,所有的NSObject对象,都拥有isa

    大家在捋这层关系的时候,要注意objc_object在源码中有两个定义,使用的范围也不一样。不要搞混了,不然会陷入死循环。

    注意:objc_class继承的是objc-privat.h里面的objc_object


    3 类中信息的探索

    在日常开发过程中,我们定义一个类的时候,不仅仅是只有isa,还会有成员变量方法等等一些信息。下面我们我们就来看一下这些信息在哪里。

    3.1 内存平移,获取bits信息

    在探索类里面信息之前,我们要先达成一个共识,内存的查找是内存平移来获得的。也就是说objc_class里面的变量,是通过内存平移来获得的。举个例子如下:

    image image

    objc_class中,isa8字节superclass也为8字节;我们不知道的是cache的内存大小。那么关键的就是获取cache的内存大小。

    下面我们就来探索一下cache的大小,首先进入cache_t:

    image
    发现代码有很长,这个时候要怎么确定cache的大小呢?

    注意:大家不要看到cache_t是一个结构体,就觉得cache8字节

    结构体指针是8字节

    结构体的大小要根据其内部的属性来计算。

    计算的时候只计算属性,方法不算,静态的属性也不计算

    通过上面我们就可以确定,cache内存大小的计算,只需要计算其中四个属性的大小(因为有if判断,大家可以仔细阅读以下这段源码。):

    image
    image
    • _buckets的类型为struct bucket_t *,是一个结构体指针;所以为8字节

    • _mask的类型为mask_tmask_tunsigned int的别名;所以为4字节

      image
      image
    • _maskAndBuckets的类型为uintptr_tmask_tunsigned long的别名;所以为8字节

      image
    • uint16_tunsigned short的别名;所以为2字节

    整体算下来之后,cache的内存大小为8 + 4 + 2 + 2 = 16

    那么bits的内存地址,就是首地址平移32个字节8 + 8 + 16

    接下来我们就可以通过控制台打印一下bits信息了:

    image

    到这里,我们已经拿到了bits里面的信息了;但是我们并没有看到我们想要的成员变量方法等等的信息。

    不要灰心,下面我们继续探索。


    3.2 继续探索类信息

    3.2.1 method(方法)

    上面我们知道bits的类型是class_data_bits_t,那么我们就跟进去找一下data

    image

    继续跟进class_rw_t(这个里有个小技巧,我们可以先找method相关的内容,其他看不懂的内容可以忽略),我们发现了这个:

    image

    这就是我们要找的内容呀,方法是存放在方法列表里面的,而这里正好是一个array

    • method_array_t
      image

    这个时候,我们继续在控制台操作:

    image
    我们给Person添加几个方法,再来查看一下: image

    )

    重复上面的步骤,此时可以看到methods里面有信息了:

    image

    接下来就是去找我们的Method的了,这个控制台流程如下:

    image

    这里面有一个big()方法,是因为在objc4-818.2的版本中,method_list_t里面的元素是method_t,而method_t被重写了。

    新的method_t长这个样子,我们要拿到方法名,就要通过big

    image
    method_t里面正好有对应的big()方法:
    image
    3.2.2 property(属性)
    image

    这个跟探索method前期是一样的操作,只不过后面获取property的时候略有不同;因为property_t直接就有name相关的属性:

    image

    整个控制台的操作如下:


    image
    3.2.3 会遇到的一些问题
    • 我们上面命名定义了属性jaxNum之外,还定义了一个成员变量NSString *jaxName,但是只能打印出一个jaxNum(打印第二个的时候,提示数组越界),因此我们可以说property_list里面没有成员变量
    image

    既然属性列表里面没有成员变量的信息,那我们就需要继续寻找了。

    同样的bits->data()的返回类型是class_rw_t(注:上面已知);那我们就在class_rw_t里面找。(正常来讲,优先查找methods()附近的方法)

    查找过之后,我们会发现,成员变量ivar在这里:

    image
    image

    下面就看我们能不将成员变量打印出来了,打印流程如下:

    image

    通过上图我们发现,最后怎么没办法执行get方法了呢?
    没关系,$7没法用get,那我们就使用$6:

    image

    最后成功打印成员变量jaxName

    • 在方法列表里面,找不到类方法
      我们都知道,类方法存储在元类中,因此在当前类method_lit_t中是找不到对应的类方法的。

    这个时候,我们既要去元类中寻找;同样的根据我们查找当前类method_lidt_t的经验,我们在元类中也这样查找。查找流程如下:

    image

    相关文章

      网友评论

          本文标题:iOS底层探索 --- 类的结构探索(上)

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