上一节我们了解了isa的内部结构,了解了结构和类的关系。
- 现在,我们用代码来探究下
isa的指针指向与类的关系
1. 类与isa指针的关系
在objc4源码中,加入测试代码。在NSLog打印出加上断点
int main(int argc, const char * argv[]) {
@autoreleasepool {
HTPerson * person = [[HTPerson alloc]init];
NSLog(@"%@", person);
}
return 0;
}
- 准备好
isa位运算的MASK(遮罩)。
image.png
p person打印,x/4gx打印,p/x&与上ISA_MASK、打印获取到的isa中的shiftcls地址。成功找到HTPerson类

如果看不到,需要回看上一节。了解
isa内部构造和获取类地址的方法。
- 我们猜想,既然拿到了
HTPerson类的isa地址,那HTPerson类的isa指针指向哪里呢?
image.png
我们发现,0x0000000100002630和0x0000000100002608都打印出来是HTPerosn的。2个地址不一样为什么打印出来结果一样?
类地址不应该是唯一的吗?
这个问题我们保留。下面再一起回答。
现在,我想继续顺着这根藤(isa指针方向),看可以摸到哪个类去。

我们发现,一直顺着摸,摸到NSObject后,内存地址不再发生变化。
为了解答上面2个不同地址都是打印了HTPerson的问题。我们需要先了解一个新东西: 👇
2. 元类(Meta)
元类的定义和创建都由系统控制,由编译器自动完成,不受我们管理
对象的isa来自于类,类也是对象。那类的isa指向哪里呢?
- 答案: 元类 。类的归属来自于元类。
类既然是对象,就需要管理方法、属性的存储和归属。而这个管理者,就是元类(Meta)
上面2个
不同地址都打印HTPerson的问题,实际上打印路径是: HTPerosn -> HTPerson元类 -> NSObject
问题: 既然你说打印到了元类, 那HTPerson元类到NSObject之后,为什么就结束了? 不应该再打印一次NSObject元类吗?
- 我们顺着上面代码。打印一次
p/x [NSObject class]:

发现[NSObject class]打印的地址与之前打印的地址不一致!
- 因为
[NSObject class]打印的是NSObject本类,而HTPerson元类的父类是NSObject元类。所以地址不一样。 - 我们验证一下。

果然,NSObject的isa指针指向了NSObject元类。 地址和HTPerson元类的isa指针地址一致。
那么,类在内存中会存在多份(多地址)吗?
#import <objc/runtime.h>
#import "HTPerson.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
Class class1 = [HTPerson class];
Class class2 = [HTPerson alloc].class;
Class class3 = object_getClass([HTPerson alloc]);
Class class4 = [HTPerson alloc].class;
NSLog(@"%p", class1);
NSLog(@"%p", class2);
NSLog(@"%p", class3);
NSLog(@"%p", class4);
}
return 0;
}

我们多种方式读取类,通过打印可以发现,所有地址都一样。
- 类的信息在
内存中永远只存在一份
问题: 不直接继承NSObject类,Isa是如何指向的呢?
@interface HTMan : HTPerson
@end
@implementation HTMan
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
HTMan * man = [[HTMan alloc]init];
NSLog(@"%p", man);
}
return 0;
}
代码中HTMan继承自HTPerson,而HTPerson继承自NSObject

但是isa的指向,却是HTMan->HTMan元类->NSObject元类
总结
- 类的信息在
内存中永远只存在一份 - 类的
isa指针首先指向自己的元类,再直接指向NSObject元类
(自己类->自己元类->NSObject元类) -
NSObject元类的isa也指向NSObject元类 -
NSObject元类是所有元类的始祖。所以也叫根元类

误区:
- 上述是isa的
指针指向,并非类的继承关系。类有继承关系,实例对象无继承关系。NSObject没有父类(父类为Null)。
所以我们类的继承,溯源只需要找到NSObject。
OC语言中:NSObject是对象的始祖。万物皆对象。- NSObject
根元类的isa指针是直接指向NSObject根元类
3. OC对象的本质
首先了解2个结构体: objc_object (根对象)和 objc_class(根类)
我们打开objc4源码,搜索struct objc_object

搜索struct objc_class :

源码搜索时,注意看结构体
尾部的声明。UNAVAILABLE已废弃的不要耗费精力了。
image.png
我们发现,objc_class继承自objc_object。
object_object拥有isa属性,所以objc_class也拥有isa。万物皆对象(object)。
使用方法:
-
struct objc_object * Object以objc_object为模板,定义一个对象 -
struct objc_class * Class以objc_class为模板,定义一个类
3. 类的结构分析
老规矩,先从案例下手,看懂了再总结
#import <Foundation/Foundation.h>
#import "HTPerson.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
HTPerson * person = [[HTPerson alloc]init];
NSLog(@"%p", person);
}
return 0;
}
-
p/x打印HTPerson类地址。 ->x/4gx 打印内存信息

请问:
1. 为什么第一个地址是
HTPeroson?第二个是NSObject?
objc_class第一个地址存放的是isa地址,第二个存放的是superclass类地址
(参考上面objc_object部分代码和objc_class部分代码图。)2. 第二个地址打印的是
NSObject类还是元类?
image.png
NSObject.class打印的地址与第二个地址打印的一致。 与接着打印的NSObject元类地址不一致。 说明第二个地址打印的是NSObjetc自身类。
3. 内存偏移
老规矩,先从案例开始,在main.m文件中加入测试代码:
int main(int argc, const char * argv[]) {
@autoreleasepool {
int f[4] = {10,20,30,40};
NSLog(@"%p", &f);
NSLog(@"%p", &f[0]);
NSLog(@"%p", &f[1]);
NSLog(@"%p", &f[2]);
NSLog(@"%p", &f[3]);
}
return 0;
}
在打印的尾部加入断点

我们发现:
-
&f和&f[0]内存地址一样。 证明对象的内存地址就是使用内部首元素的地址
-
-
f是int类型的数组,内部元素每位占用4字节。 所以每个元素内存偏移值为4。
-
是不是瞬间有个小想法:
我们是否可以根据数据类型,确定内存占用空间的大小,通过内存值偏移,就可定位到下一元素的指针地址。然后直接取出这个指针地址指向的值。
小拓展: 如何通过
地址取出对应的值:
知识点: 指针(地址)的指针
地址
强转为指定类型(int) -> 加*读取对象(指针)的指针-> 打印目标值image.png快速读取:
p *((int *)0x7ffeefbff5b0)image.png
我们通过首地址偏移,果然:

完美👍
但前提是我们必须知道偏移值是多少,也就是存储的是什么类型。
- 上面我们使用的是
地址偏移,所以必须加入偏移值。 - 我们也可以使用
指针偏移。直接在属性层在进行操作。

至此。我们已经掌握了地址偏移和指针偏移。
- 只有知道
属性类型,占用空间大小,才能使用地址偏移和指针偏移,读取类的所有信息。
现在,我们来分析类的结构
4. 分析类结构
在objc4源码中,搜索objc_class。
-
剔除不会占用类空间的const、void、static和函数。
struct objc_class : objc_object {
// Class ISA; 。 // 指针 - 占用8字节
Class superclass; // 指针 - 占用8字节
cache_t cache; // ?
class_data_bits_t bits;
};
如果我们要获取bits的首地址位置。只有cache_t类型不知道空间大小。 点进去看看:
-
剔除不会占用类空间的const、void、static和函数。
struct cache_t {
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
explicit_atomic<struct bucket_t *> _buckets;
explicit_atomic<mask_t> _mask;
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
explicit_atomic<uintptr_t> _maskAndBuckets;
mask_t _mask_unused;
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
explicit_atomic<uintptr_t> _maskAndBuckets;
mask_t _mask_unused;
#else
#error Unknown cache mask storage type.
#endif
#if __LP64__
uint16_t _flags;
#endif
uint16_t _occupied;
};
- 我们进入
explicit_atomic查看:

- 发现返回的类型就是传入的泛型
T。 所以explicit_atomic的类型只跟他传入的类型相关。

- 我们进入
bucket_t结构体:

这里的
_sel和_imp是不是很眼熟。 😃 后续我们会详细介绍
进入uintptr_t:

- 发现
uintptr_t实际就是unsigned long,64位操作操作系统下占用8个字节
所以我们_buckets实际大小就是8字节
接下来我们看mask_t。

-
点进去
image.png
发现它就是uint32_t类型。占用4字节
汇总一下:

现在系统都是64位系统。
所以
cache_t内存大小为: 12 + 2 + 2 =16 字节
回头继续分析object_class的内存大小
struct objc_class : objc_object {
// Class ISA; 。 // 指针 - 占用8字节
Class superclass; // 指针 - 占用8字节
cache_t cache; // 占用16字节
class_data_bits_t bits;
};
所以我们得出结论。
-
object_class内部的找到bits,需要偏移8 + 8 + 16 =
32位
现在,我们来读取bits
5. 读取bits
-
x/4gx HTPerson.class打印内存地址 -
p/x 0x0000000100002390 + 32获取bits地址 -
p (class_data_bits_t *)$12强转为class_data_bits_t类型

在objc_class结构中,我们发现bits.data()返回的是class_rw_t类型

我们打印bits->data()检验一下

成功拿到,打印HTPerson的class_rw_t信息:

- 我们想要寻找这个类的
方法和属性,但是发现只有一个ro_or_rw_ext。
我们进入class_rw_t内部, 折叠所有函数。
Xcode折叠所有函数:
command + shift + ←

6. 读取methods、properties和protocols
main.m加入测试代码:
@interface HTPerson : NSObject {
NSString * hobby;
}
@property(nonatomic, copy) NSString * name;
@property(nonatomic, copy) NSString * nickname;
+(void) ClassFunction;
-(void) objectFunction;
@end
@implementation HTPerson
+(void) ClassFunction {
NSLog(@"类方法");
}
-(void) objectFunction{
NSLog(@"对象方法");
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
HTPerson * person = [[HTPerson alloc]init];
NSLog(@"%p", person);
}
return 0;
}
NSLog处加入断点,运行代码。快速找到data()对象的位置:
p/x HTPerson.class- ->
p/x (class_data_bits_t *)(0x0000000100002258 + 32) - ->
p $1->data()

methods
利用$2对象打印p $2->methods():

我从图中看到了list。继续打印p *$3.list:

- 看到
count有6个值,我们使用get打印一下:

properties
利用$2对象打印p $2->properties():
接下来打印p * properties()属性方法。

protocols
利用$2对象打印p $2->protocols():

成员变量hobby去哪了?

打印ro

打印p *$23.ivars

打印每一个成员变量p $25.get(0)

- 我们发现,除了
hobby变量。所有property属性,都自动生成了带下划线的成员变量。
为什么案例中的ClassFunction类方法没出现在Methods中?
对象的isa指向自己的类,所以对象方法存放到了HTPerosn中,难道类方法存放在它的isa上一级?HTPerson元类中?
实践出真知:
-
p HTPerson.class->x/4gx $0->p/x (class_data_bits_t *)(0x0000000100002230 + 32)->p $1->data()->p $2->methods()->p $3.list->p *$4->p $5.get(0)
image.png
果然。 对象的方法存在类中,类的方法存在元类中
下一章







