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

这是我参与更文挑战的第5天,活动详情查看: 更文挑战

isa分析元类

类的isa分析

我们在上一篇文章中对Person的对象做过这样的分析

我们可以根据pisa通过与掩码运算得到类Person

接下来我们继续打印Person的内存情况

Person和对象p一样,是存在内存结构的

我们接下来尝试继续进行运算,看看会是什么结果:

我们又得到了Person,但是前后两个Person的地址却完全不一样

0x00000001000081c80x00000001000081a0两个地址都是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,我们将其称为根元类

那么类NSObjectisa,又指向何处呢?

NSObjectisa指向了根元类

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) 
复制代码
  • 0x00000001000081d0Class ISA,8字节
  • 0x00007fff8089a008Class superclass,8字节

如果我们想要找到class_data_bits_t bits,那么我们就必须要知道cache_t cache的大小,接下来,我们来计算cache_t cache占用的内存空间

我们在前边的文章已经确定了,成员变量是影响内存的因素,static修饰的存在在全局区,也不占用结构体内存,那么最终影响cache_t占用内存大小的主要因素为:

接下来我们计算其占用内存大小:

  • uintptr_tunsigned 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里边有两个元素,那么我们查看一下:

getC++的获取数组的方法

我们已经找到了类PersonrealNameage两个属性

方法获取

同理,我们可以找到methods():

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

看不到,我们需要去研究一下methods()properties()的区别:

属性与方法获取的区别

我们研究发现:properties()是个property_array_t类型,而property_array_t继承自list_array_tt<property_t, property_list_t, RawPtr>,最终返回了一个property_list_t类型的数组,元素类型为property_tproperty_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: 类的原理分析(上)-一一网

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

接下来我们来验证一下:

一共5个方法,分别是runagesetAge:reaNamesetRealName:

那么到此结束了么?我们的成员变量nickName和类方法+ (void)eat呢?

未完待续,请听下回分解……

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