在前一篇文章中,我们已经探讨了iOS底层原理--isa与类关联的原理,isa包含了Class类,从而将isa与Class类进行了关联。
那么,我们现在来看下类的结构分析。
首先我们还是看一段代码
//
// main.m
// KCObjc
//
// Created by Cooci on 2020/7/24.
//
#import <Foundation/Foundation.h>
#import "LGPerson.h"
#import <objc/runtime.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
// objc_alloc VS alloc
// NSObject LGPerson
// NSObject 根类
// sel 处理 llvm
// define ISA_MASK 0x00007ffffffffff8ULL
NSObject *objc1 = [NSObject alloc];
LGPerson *objc2 = [LGPerson alloc];
// LGPerson *objc3 = [LGPerson alloc];
NSLog(@"Hello, World! %@",objc2);
}
return 0;
}
- 我们将断点打到
objc2处,然后查看objc2的内存情况。
image.png
x/4gx表示查看objc2的内存情况
- 通过上面的输出,我们看到了内存,然后我们知道,内存中第一位代表了对象的
isa指针的地址,所以0x001d800100002205这个表示isa指针的地址。 - 通过iOS底层原理--isa与类关联的原理这篇文章,我们将
isa于ISA_MASK进行与运算可以得的isa指针指向的类的地址,我们进行验证。
ISA_MASK在x86_64中的地址0x00007ffffffffff8ULL

通过上面的图片我们可以看到,经过与运算后,我们得出了最后得到的地址中存放了
LGPerson,至此我们可以得到结论。对象的isa指针指向了类。-
然后我们再来一步,将类的
isa拿出来和ISA_MASK进行与运算,得到什么呢?我们验证一下。
LGPerson.png 看到输出结果,显示还是
LGPerson,为什么呢?这就是所谓的元类。
元类(Meta Class)
我们都知道
对象的isa是指向类,类的其实也是一个对象,可以称为类对象,其isa的位域指向苹果定义的元类元类是系统给的,其定义和创建都是由编译器自动完成,在这个过程中,类的归属来自于元类元类
是类对象的类,每个类都有一个独一无二的元类用来存储 类方法的相关信息。元类本身是没有名称的,由于与类相关联,所以使用了同类名一样的名称
上面这段话摘自Style_月月的文章iOS-底层原理 08:类 & 类结构分析,大家也可以去看看她的其他文章,都写的非常详细。
那么我们接着按照上面的流程进行探索
-
首先我们得到
LGPerson的元类
元类 -
此事可以看到,在
LGPerson的元类中,我们仍然可以拿到isa指针。那么我们用isa于ISA_MASK进行与运算,得到什么呢?请往下看。
NSObject.png
可以看到,经过一系列的操作,LGPerson的元类最终竟然指向了NSObject。
而且NSObject的首地址和isa指针的地址相同,所以也可以说明NSObject指向的是自己。
- 那我们看看
NSObject的情况,

可以看到,这次打印的地址跟我们之前得到的NSObject地址不一样,难道NSObject在内存中有多份吗?那我们再打印一下完整的地址。

再和之前的地址对比一下,可以看到一样。
所以得出结论,LGPerson元类的isa指针并不是指向NSObject,而是指向NSObject的元类,即根元类,而NSObject也是指向根元类,而根元类最终指向自己。

上面就是isa和继承关系的走势图,总结如下:
isa
- 实例对象
isa指向类对象 - 类对象
isa指向元类 - 元类
isa指根(NSObject)元类 - 根(NSObject)元类指向自己
superclass(继承关系)
- 子类继承自父类
- 类才有继承关系,而实例对象不存在继承关系
- 所有类都继承
NSObject,而NSObject继承自nil -
NSObject的元类也继承自NSObject
objc_object & objc_class
在我们之前的分析中,通过clang进行解析
#ifndef _REWRITER_typedef_LGPerson
#define _REWRITER_typedef_LGPerson
typedef struct objc_object LGPerson;
typedef struct {} _objc_exc_LGPerson;
#endif
extern "C" unsigned long OBJC_IVAR_$_LGPerson$_name;
struct LGPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS; //isa
NSString *_name;
};
可以看到LGPerson是objc_object类型的 ,LGPerson的底层编译LGPerson_IMPL中又包含了NSObject_IMPL,NSObject_IMPL是一个结构体,包含了Class isa
struct NSObject_IMPL {
Class isa;
};
Class又是一个objc_class类型的结构提体。
typedef struct objc_class *Class;
所以,objc_object就是一个根类,而objc_class定义了一个Class,这证明了每个对象都有isa指针。
而从我们的源码,可以看出来objc_class继承自objc_object

内存偏移
在接着往下分析之前,我们来了解一个知识 -- 内存偏移。
普通指针
先看下面的案例,打印a、b的值和地址。
int a = 10;
int b = 10;
//打印地址和
NSLog(@"%d --- %p",a,&a);
NSLog(@"%d --- %p",b,&b);
如下图所示

a、b都指向10,但是a、b的地址不一样,这是一种拷贝,属于
值拷贝,也称为浅拷贝。a,b的地址之间相差 4 个字节,这取决于a、b的类型
对象指针
LGPerson *p1 = [LGPerson alloc];
LGPerson *p2 = [LGPerson alloc];
NSLog(@"%@ --- %p",p1,&p1);
NSLog(@"%@ --- %p",p2,&p2);
打印结果如下所示:

- p1、p2 是指针,p1 是 指向
[LGPerson alloc]创建的空间地址,即内存地址,p2 同理
&p1、&p2是指向 p1、p2对象指针的地址,这个指针 就是二级指针
数组指针
//数组指针
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);
打印结果如下:

-
&c和&c[0]都是取 首地址,即数组名等于首地址 -
&c与&c[1]相差4个字节,地址之间相差的字节数,主要取决于存储的数据类型 - 可以通过 首地址+偏移量取出数组中的其他元素,其中偏移量是数组的下标,内存中首地址实际移动的字节数 等于
偏移量 * 数据类型字节数
类结构
我们已经知道,类在底层最终会转化成objc_class,objc_class结构如下所示,

而
bits有我们需要的所有信息,那么要拿到bits,我们需要进行内存偏移。那么看看
bits前面的东西有多大,进行多少偏移。
-
isa属性:继承自objc_object的isa,占 8字节 -
superclass属性:Class类型,Class是由objc_object定义的,是一个指针,占8字节 -
cache,是一个cache_t类型的结构体,所以我们来分析下cache
cache_t
我们来看看cache_t的结构,进入cache类cache_t的定义(只贴出了结构体中非static修饰的属性,主要是因为static类型的属性 不存在结构体的内存中),有如下几个属性
struct cache_t {
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
explicit_atomic<struct bucket_t *> _buckets; // 是一个结构体指针类型,占8字节
explicit_atomic<mask_t> _mask; //是mask_t 类型,而 mask_t 是 unsigned int 的别名,占4字节
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
explicit_atomic<uintptr_t> _maskAndBuckets; //是指针,占8字节
mask_t _mask_unused; //是mask_t 类型,而 mask_t 是 uint32_t 类型定义的别名,占4字节
#if __LP64__
uint16_t _flags; //是uint16_t类型,uint16_t是 unsigned short 的别名,占 2个字节
#endif
uint16_t _occupied; //是uint16_t类型,uint16_t是 unsigned short 的别名,占 2个字节
-
【情况一】if流程
buckets类型是struct bucket_t *,是结构体指针类型,占8字节mask是mask_t类型,而mask_t是unsigned int的别名,占4字节
-
【情况二】elseif流程
_maskAndBuckets是uintptr_t类型,它是一个指针,占8字节_mask_unused是mask_t类型,而mask_t是uint32_t类型定义的别名,占4```字节_flags是uint16_t类型,uint16_t是unsigned short的别名,占2个字节_occupied是uint16_t类型,uint16_t是 unsigned short的别名,占2```个字节
总结:所以最后计算出cache类的内存大小 = 12 + 2 + 2 = 16字节
所以最终要拿到bits需要平移32位。
class_data_bits_t
- 首先打印出
LGPerson
LGPerson.png - 拿出首地址
0x100002200平移32位 得到0x100002220,得到bits
bits .png - 获取
data()
data() .png - 获取
class_rw_t
class_rw_t .png - 获取属性列表
properties()
properties .png - 我们可以看到里面是一个
list,获取其中的元素
image.png
可以看到list中的属性与我们LGPerson中的属性一致。









