这是我参与更文挑战的第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_object
Class 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
在底层是有实现的:
我们发现method_t
中的结构体big
与property_t
结构体相似,那么我们是不是只要获取到big
结构体就可以拿到数据了呢?获取big
结构体,通过一下方法:
接下来我们来验证一下:
一共5
个方法,分别是run
、age
、setAge:
、reaName
、setRealName:
那么到此结束了么?我们的成员变量nickName
和类方法+ (void)eat
呢?
未完待续,请听下回分解……