这是我参与更文挑战的第5天,活动详情查看: 更文挑战
isa分析元类
类的isa分析
我们在上一篇文章中对Person的对象做过这样的分析

我们可以根据
p的isa通过与掩码的与运算得到类Person
接下来我们继续打印Person的内存情况

类
Person和对象p一样,是存在内存结构的
我们接下来尝试继续进行与运算,看看会是什么结果:

我们又得到了Person,但是前后两个Person的地址却完全不一样
0x00000001000081c8和0x00000001000081a0两个地址都是Person,name他们两个有什么区别呢?会不会类和对象一样,在内存中可以无限开辟,在内存中不只有一个Person类存在呢?
关于类的思考
针对上文中的疑问,我们用多种方式,创建Person类,看看它在内存中的地址情况:

可以发现,多种方式创建的Person类,在内存中地址都一样0x1000081e0,那么也就是上图中第一次得到的地址为0x00000001000081e0的才是我们的Person类

我们通过对象的
isa找到了类Person,之后又通过类的isa找到了另个地址,经过打印发现也是Person,那么它究竟是什么呢?
我们将可执行文件拖入MachOView来查看一下符号表:

我们可以根据地址与ASLR偏移之后,找到_OBJC_METACLASS_$_Person,我们将它称为Person的元类,它是由系统生成和编译的

isa继承链
根元类
书接上文,我们通过对象的isa找到了类,我们又根据类的isa找到了元类,那么我们进行大胆的假设,是不是能够根据元类的isa还能找到什么东西呢?

我们重新运行项目,进行验证:

最终我们通过
元类的isa找到了NSObject,那么他是不是就是我们的类NSObject呢

通过验证发现我们通过元类的isa找到的不是我们的类NSObject,我们将其称为根元类

那么类NSObject的isa,又指向何处呢?

类
NSObject的isa指向了根元类
isa走位图
那么,根据以上结论,我们可以得出一张isa的走位图

元类的继承链
我们接下来看一组代码的打印结果
// NSObject实例对象
NSObject *object = [NSObject alloc];
// NSObject类
Class class = object_getClass(object);
// NSObject元类
Class metaClass = object_getClass(class);
// NSObject根元类
Class rootMetaClass = object_getClass(metaClass);
// NSObject根根元类
Class rootRootMetaClass = object_getClass(rootMetaClass);
NSLog(@"NSObject实例对象-->%p", object);
NSLog(@"NSObject类------->%p", class);
NSLog(@"NSObject元类------>%p", metaClass);
NSLog(@"NSObject根元类---->%p", rootMetaClass);
NSLog(@"NSObject根根元类--->%p", rootRootMetaClass);
// Person元类
Class pMetaClass = object_getClass(Person.class);
Class pSuperClass = class_getSuperclass(pMetaClass);
NSLog(@"%@----%p", pSuperClass, pSuperClass);
复制代码
打印结果
NSObject实例对象-->0x100610370
NSObject类------->0x7fff8089a008
NSObject元类------>0x7fff80899fe0
NSObject根元类---->0x7fff80899fe0
NSObject根根元类--->0x7fff80899fe0
NSObject----0x7fff80899fe0
复制代码
我们发现,Person的元类的父类是NSObject的元类/根元类
任何对象,它的
元类的父类就是当前的根元类
那么上边的isa走位图可以进一步完善为:

我们继续修改代码,看打印结果:
// NSObject实例对象
NSObject *object = [NSObject alloc];
// NSObject类
Class class = object_getClass(object);
// NSObject元类
Class metaClass = object_getClass(class);
// NSObject根元类
Class rootMetaClass = object_getClass(metaClass);
// NSObject根根元类
Class rootRootMetaClass = object_getClass(rootMetaClass);
NSLog(@"NSObject实例对象-->%p", object);
NSLog(@"NSObject类------->%p", class);
NSLog(@"NSObject元类------>%p", metaClass);
NSLog(@"NSObject根元类---->%p", rootMetaClass);
NSLog(@"NSObject根根元类--->%p", rootRootMetaClass);
// Person元类
Class pMetaClass = object_getClass(Person.class);
Class pSuperClass = class_getSuperclass(pMetaClass);
NSLog(@"%@----%p", pSuperClass, pSuperClass);
// Teacher继承自Person Person继承自NSObject
Class tMetaClass = object_getClass(Teacher.class);
Class tSuperClass = class_getSuperclass(tMetaClass);
NSLog(@"%@----%p", tSuperClass, tSuperClass);
复制代码
打印结果:
NSObject实例对象-->0x103804ac0
NSObject类------->0x7fff8089a008
NSObject元类------>0x7fff80899fe0
NSObject根元类---->0x7fff80899fe0
NSObject根根元类--->0x7fff80899fe0
NSObject----0x7fff80899fe0
Person----0x1000081c8
复制代码
通过打印我们发现,元类也存在一条继承链:

我们在查看一下NSObject的父类及根元类的父类:

结论:
NSObject的父类是null,根元类的父类是这个类本身,万物皆来自于NSObject
那么,元类的继承链最终完善为:

我们可以将isa走位图和元类的继承图合二为一

类的结构分析
我们在上文说到,类也是存在内存结构的;我们知道对象的内存空间放的是一个个的成员变量,那么类的内存结构里存放的是什么东西呢?接下来我们来研究一下;我们在之前的文章中已经确定了,类在底层一个objc_class的结构体,那么我们在objc的源码中搜索objc_class的定义发现有两处:

由于我们当前使用的是
Objective-C 2.0,所以次处我们可以忽略(不支持OBJC2),着重研究第二处即可;

Class ISA来自于上层的objc_objectClass superclass是父类cache_t cache是什么我们不知道class_data_bits_t bits我们看到bits.data()返回了一个class_rw_t类型的数据,而进入class_tw_t
发现里边有methods()、properties()、protocols()等数据,那么我们来着重研究一下这个数据
既然我们知道类的地址,那么我们就能够通过内存地址的平移来获取数据,那么内存是怎么平移的呢?
指针和内存平移
我们通过几个代码案例来了解一下指针和内存平移
普通指针:
int a = 10;
int b = 10;
NSLog(@"%d--%p", a, &a);
NSLog(@"%d--%p", b, &b);
复制代码
打印结果:
10--0x7ffeefbff430
10--0x7ffeefbff434
复制代码
结论:两个地址不一样的指针指向了同一个数字10
对象指针:
Person *p1 = [Person alloc];
Person *p2 = [Person alloc];
NSLog(@"%@--%p", p1, &p1);
NSLog(@"%@--%p", p2, &p2);
复制代码
打印结果:
<Person: 0x100506810>--0x7ffeefbff410
<Person: 0x100506820>--0x7ffeefbff418
复制代码
结论:两个地址不一样的指针分别指向了不同的对象
数组指针:
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);
复制代码
打印结果:
0x7ffeefbff420--0x7ffeefbff420--0x7ffeefbff424
0x7ffeefbff420--0x7ffeefbff424--0x7ffeefbff428
复制代码
结论:**数组的首地址也就是第一个元素的地址,每个元素相差4个字节,int占4字节(步长) **
那么我们就可以通过以下方式打印数组元素:
for (int i = 0; i<4; i++) {
int value = *(d+i);
NSLog(@"%d\n", value);
}
复制代码
打印结果:
1
2
3
4
复制代码
总结:我们可以通过类的首地址平移一些大小的内存就可以得到数据
类的结构内存计算
打印Person的内存结构
(lldb) x/4gx Person.class
0x1000081f8: 0x00000001000081d0 0x00007fff8089a008
0x100008208: 0x000000010072e4a0 0x000780100000000f
(lldb)
复制代码
0x00000001000081d0为Class ISA,8字节0x00007fff8089a008为Class superclass,8字节
如果我们想要找到class_data_bits_t bits,那么我们就必须要知道cache_t cache的大小,接下来,我们来计算cache_t cache占用的内存空间
我们在前边的文章已经确定了,成员变量是影响内存的因素,static修饰的存在在全局区,也不占用结构体内存,那么最终影响cache_t占用内存大小的主要因素为:

接下来我们计算其占用内存大小:
uintptr_t为unsigned long类型,占用8字节union为联合体,内部成员变量占用同一块内存,所以我们需要计算较大的成员变量占用的内存大小
union {
struct {
explicit_atomic<mask_t> _maybeMask; // mask_t为uint32_t 占4字节
#if __LP64__
uint16_t _flags; // uint16_t 占2字节
#endif
uint16_t _occupied; // uint16_t 占2字节
};
explicit_atomic<preopt_cache_t *> _originalPreoptCache; // 占8字节
};
复制代码
最终可知,联合体占用了8字节,那么我们可以计算得出cache_t占用了16字节
结论:类的首地址偏移
8 + 8 + 16 = 32字节及偏移0x20即可找到bit
lldb分析类的结构及bit数据
接下来我们将Person类进行修改如下:
@interface Person : NSObject {
NSString *nickName;
}
@property (nonatomic, copy) NSString *realName;
@property (nonatomic, assign) NSInteger age;
- (void)run;
+ (void)eat;
@end
@implementation Person
- (void)run {
}
+ (void)eat {
}
@end
复制代码
我们在控制台,通过lldb来打印相关内存数据:

既然data()返回的变量是class_rw_t类型,那么我们去看看此类型都有什么方法,我们找到如下方法:
property_array_t properties()method_array_t methods()- ……
属性获取
我们先来看properties()方法:

properties()是property_array_t类型,继承自list_array_tt,里边有迭代器iterator操作,那么他极有可能是个数组

变量$8里边有两个元素,那么我们查看一下:

get是C++的获取数组的方法
我们已经找到了类Person的realName和age两个属性
方法获取
同理,我们可以找到methods():

竟然有5个元素,那我们来查看一下:

看不到,我们需要去研究一下methods()与properties()的区别:
属性与方法获取的区别
我们研究发现:properties()是个property_array_t类型,而property_array_t继承自list_array_tt<property_t, property_list_t, RawPtr>,最终返回了一个property_list_t类型的数组,元素类型为property_t,property_list_t在底层却没有具体实现:


接下来研究一下methods(): 是个method_array_t类型,继承自list_array_tt<method_t, method_list_t, method_list_t_authed_ptr>,最终返回了一个method_list_t类型的数组,元素类型为method_t,然而method_list_t在底层是有实现的:
![图片[1]-iOS底层原理05: 类的原理分析(上)-一一网](https://www.proyy.com/skycj/data/images/2021-06-22/2f9e2b3eb9c9fe83411d6eee18f3c202.jpg)

我们发现method_t中的结构体big与property_t结构体相似,那么我们是不是只要获取到big结构体就可以拿到数据了呢?获取big结构体,通过一下方法:

接下来我们来验证一下:

一共5个方法,分别是run、age、setAge:、reaName、setRealName:
那么到此结束了么?我们的成员变量nickName和类方法+ (void)eat呢?
未完待续,请听下回分解……
























![[桜井宁宁]COS和泉纱雾超可爱写真福利集-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/4d3cf227a85d7e79f5d6b4efb6bde3e8.jpg)

![[桜井宁宁] 爆乳奶牛少女cos写真-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/d40483e126fcf567894e89c65eaca655.jpg)