iOS 类的原理分析

开篇

好好学习,不急不躁。每天进步一点点。

课程回顾补充

1、isa 字节大小

上一次,我们对isa进行了推导,而可能对 isa 的大小为 8 字节,可能会存在疑惑,那么我们在这里补充说明一下;

通过源码,我们直接进入NSObject内部

1.png

可以看到,isa是一个Class 类型;全局查找Class

截屏2021-06-17 下午10.24.21.png

通过上图可以看出,Class 是一个结构体指针,别名为Class,但真正对应底层llvm为objc_class * 类型;
之前了解过,一个结构体指针,他的大小为8字节;所以这里的isa也就是 8字节大小;

2、联合体内部分析

之前对 isa_t 这个联合体位域的内部结构,进行分析,可能有些不明白,在 __x86_64__ 下,shiftcls为什么会在是44位,而其他的为什么会是17位,和3位;

截屏2021-06-17 下午10.40.57.png

从上图,为们可以看到,前3位,占据3个字节, shiftcls占据 44 字节,后5 位占据 17字节。我们iOS是小端模式,所以是从右往左读取,也是从右往左存储;
如下图:

截屏2021-06-17 下午10.57.13.png

类信息分析

1、ISA

1、isa 走向流程

上一次,我们了解了对象的本质,以及 isa 的简单推导。今天我们来分析一下类的原理走向,以及深入了解经典的 isa 走向图;

走代码,查看类以及其底层关系;

LGPerson *p = [LGPerson alloc];
声明一个 LGPerson 类;
通过 p/x p,查询类信息;再通过x/4gx 获取地址内存信息;使用 isa 与 mask,我们将得到这个类
的地址;
复制代码

1.png

可能会好奇 为什么要 与 上 mask 呢?

与上mask,其实是 `首地址位运算`,之前我们讲过,类的信息,都是存储在 `isa_t` 的 
`shiftcls`中,而`shiftcls` 如上图,是位于 3 字节之后,所以为了能 取出 `shiftcls`,
我们需要进行位移运算,而mask是系统提供的,在 _x86_64系统下的掩码,可以快速运算;
复制代码

通过上面的操作,我们通过地址,可以得出类地址,那么这个类地址,他的更底层,又是什么信息呢?
来一波骚操作,我们使用这个类地址,看看这个地址,会有什么样的内存信息呢?

截屏2021-06-17 下午11.34.21.png

通过实践,LGPerson的类地址,还能继续打印出类信息,而且类地址打印的类信息,与上面打印的不一样;

继续实践,尝试使用这个内存结构的isa去与上mask

截屏2021-06-18 下午6.17.12.png

纳尼?????怎么会一样???就 alloc 一个 LGPerson,怎么会有两个地址?

121.jpg

验证一下,通过多种方式获取看看类对象在内存里,是不是存在多份。。。。。。

void lgTestClassNum(void){
    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);
}
复制代码

2.png

结果显示,只有一个地址,而且跟上面打印的第一个LGPerson地址一样,那么,上面的第二个地址,是什么呢?为什么会是LGPerson?

这就引出了,今天我们要讲的,类他爹 ---- > 元类

回顾一下,刚才的流程:
通过对象的isa,我们摸到了类,通过类的isa,我们摸到了元类

截屏2021-06-18 下午11.03.04.png

但是,在iOS中,从未有过元类这个定义;我们换另一种方式,查看一下元类是否真实存在;

将工程的可执行文件,拖入MachOView工具中,查看oc底层文件

machoview.png

在工程内,我们并没有对Metal Class 进行声明,由次可以,Metal Class 是系统底层自动声明的。也就不需要我们负责与维护。

那么元类,是否还能指向其他不为人知的类吗? 我们尝试继续打印,看看能不能得出元类的更底层;

nso.png

通过对LGPerson层层摸索,最终我们摸索到NSObject,那这个NSObject,与我们直接使用NSObject对象,有什么关系吗?

一致.png

2、isa 走向总结

我们通过这个流程,绘制一个简单的流程图,总结一下isa的走向;

截屏2021-06-19 下午4.53.10.png

通过对象的isa,我们探索到了,再通过类的isa,探索到元类,元类isa,继续探索到 根元类,也就是NSObject,再继续探索,就形成了闭环,回归NSObject。

并且我们发现,NSObject 作为根类,他的走向图,只进行了2次走向,就回归NSObject,所以NSObject的走向,可以总结为:根类 isa —> 根元类 isa —-> 根元类 isa

通过这些探索,我们统一思想,iOS里,任何对象,他的元类的父类,都是根元类,也就是NSObject,所以NSObject是一切类的基础。

2、类继承链

我们都知道,iOS 中,类是由继承关系的。但是最终的父类是谁,父类又源至谁,我们都比较模糊,接下来,我们就彻底理清一下这些继承关系;

截屏2021-06-19 下午5.55.14.png
LGTeacher继承LGPerson,但是在寻找底层元类的时候,他没有直接显示出,最底层的根元类,我们依照之前的思路,分析一下:

以LGTeacher为分析对象,他继承LGPerson,对象isa —> 类isa — > 元类,相当于说,LGTeacher指向元类后,被元类截胡了,不在由自身对象直接去指向根元类,而是由元类,再开一个分支,去指向根元类;

所以说,对象会有一条isa走向链,元类会有一条isa走向链,根元类也会有一条is a走向链;同理,继承链也是如此;

并且,在我们通过元类获取根元类,根元类获取父类时,得到的值,是null,也就是说,NSObject的父类是null

由此,我们引出了一张,非常经典的,继承关系链走位图

isa流程图的副本.png

类分析

1、类的内部结构分析

分析完ISA和继承链,接下来,我们对类的内部结构,进行分析;

我们都知道,一个对象,内部存储有 isa,shiftcls, 成员变量等数据;那么类呢,类的内部会是什么样的存储结构呢?

我们通过 x/4gx LGPerson.class,可以直接打印出LGPerson的内部信息;
类的底层,其实就是objc_object,所以我们需要通过源码,去分析类内部信息;

通过源码,搜索 objc_class ,查找他的声明,其中在runtime.h中,我们发现一处声明比较特殊,在此记录一下:

无用.png

objc_runtime_new.h 文件中,我们找到了现在使用的 objc_class的声明方法,里面声明了:

struct objc_class : objc_object {
    ......
    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
        ......
    }
    class_rw_t *data() const {
        return bits.data();
    }
    void setData(class_rw_t *newData) {
        bits.setData(newData);
    }

...... 
后面还有不少代码,可自行查看源码    
复制代码

Class superclass :存储当前的父类;

cache_t cache;: 用于缓存指针和 vtable,加速方法的调用;

__has_feature:判断编译器,对()内的条件,是否支持;
ptrauth_calls:针对 arm64 架构,进行指针验证;使用Apple A12或更高版本A系列处理器的设备(如iPhone XS、iPhone XS Max和iPhone XR或更新的设备)支持arm64e架构,具体可参考苹果原文;
developer.apple.com

class_data_bits_t bits;:相当于 class_rw_t 指针加上 rr/alloc 的标志,bits 用于存储类的方法、属性、遵循的协议等信息的地方;
class_data_bits_t 内部,包装有写入bits的方法 data(),最终返回class_rw_t的结构体

class_rw_t* data() const {
    return (class_rw_t *)(bits & FAST_DATA_MASK);
}
复制代码

class_rw_t,内部,可以看到里面有method_array_t,property_array_t
,protocol_array_t等方法,接下来,我们就提出去这里的方法;

我们之前可以获取LGPerson类的地址,类的地址,其实就是当前内存结构里的首地址,我们可以按照内存平移的步骤,获取类里的bits;

2、获取类的内存结构

直接通过x/4gx QLYPerson.class,查看类的内存结构信息;

内第二.png

我们需要读取bits内部的,所以可以依靠内存平移的方式来读取,但问题是,我们不清楚,需要平移多少字节;我们需要分析内部结构,来确定,需要平移多少字节;

位移数.png

cache_t内存大小

当我们看到 cache_t内部结构的时候,啊~~~~,疯了。里面的代码太多了。这 TM 怎么计算大小?

豆.jpg

在cache_t内部,有方法,全局变量;我们知道,方法是不占用结构体内存大小的,所以,哈~~,把方法干翻了,老子不看它。 全局变量也不占用我们的内存结构,它存在全局区,所以,干了它;

截屏2021-06-19 下午10.47.33.png

最后,我们得到平移位置为: 8+8+8+8 = 32 ; 然后进行地址平移

data() 信息读取

截屏2021-06-19 下午11.25.53.png

我们通过一些列大战,终于拿到了data(),可是data()内,并没有看到类的成员变量,还得继续查找,看看成员变量存放在哪里?

通过查看class_rw_t内部代码,它里面有method_array_tproperty_array_tprotocol_array_t等方法;如此,我们便可以通过这些方法,读取需要的数据;

p $4.properties.png

我们通过打印 properties() 函数,能获取更深层的数据,但这并不是我们的需求,我们需要的是打印出 name 这个属性,但我们也因此知道,name 是存储在list_array_tt这个内部结构里;

唯有继续深究,查咯,看list_array_tt里,如何才能读取数据;

12.png

既然可以读取内部的元素,那么我们就可以通过方法,来读取数组里的数据;
上面打印properties()的数据时,我们看到内部有一个list,那么直接打出list看看;

name.png

至此,我们就很完美的打印出属性啦;

增加难度,我们试试,在QLYPerson内增加方法,和成员变量,看看是否也能打印出来

截屏2021-06-20 上午12.42.10.png

截屏2021-06-20 上午12.51.02.png

接下来,我们尝试获取方法

空方法.png

查看源码,看看 property 和 method 内部底层,有什么变化;

在property内部,我们获取的数据,来源于prpperty_list_t这个结构体,而这个结构体,不会对数据进行任何处理,直接存储进去;
pro.png

实际上,我们读取出来的数据,就是 property_t这个值
截屏2021-06-20 上午1.08.59.png
可以看到,property_t这个结构体,内部是没有任何业务处理的,直接就是成员变量的方式进行读取;
截屏2021-06-20 上午1.17.53.png
而查看 method_list_t,method_t,我们发现,系统会对存入的数据,进行一番改造

method_t 内部,并没有直接的成员变量,所以我们无法直接通过get()去读取;

⚠️⚠️⚠️注意:但是我们可以看到,在struct big内部,有相应的成员变量;所以,我们可以直接获取这里的big,可以看到,他里面提供了,获取big的方法。

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 { // ⚠️⚠️⚠️注意:但是我们可以看到,在struct big内部,有相应的成员变量;
        SEL name;
        const char *types;
        MethodListIMP imp;
    };
    
........

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

复制代码

结果输出有问题,kc大神,有时间帮看一下

错误.png

知识补充–内存平移

截屏2021-06-19 下午8.44.15.png

截屏2021-06-19 下午9.43.50.png

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享