上一节我们了解了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(遮罩)。
p person
打印,x/4gx
打印,p/x
&与上ISA_MASK
、打印获取到的isa中的shiftcls
地址。成功找到HTPerson
类
如果看不到,需要回看上一节。了解
isa内部构造
和获取类地址的方法。
- 我们猜想,既然拿到了
HTPerson
类的isa地址
,那HTPerson
类的isa
指针指向哪里呢?
我们发现,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
已废弃的不要耗费精力了。
我们发现,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
类还是元类?
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) -> 加*
读取对象(指针)的指针
-> 打印目标值
快速读取:
p *((int *)0x7ffeefbff5b0)
我们通过首地址偏移
,果然:
完美👍
但前提是我们必须知道偏移值
是多少,也就是存储
的是什么类型
。
- 上面我们使用的是
地址偏移
,所以必须加入偏移值
。 - 我们也可以使用
指针偏移
。直接在属性层在进行操作。
至此。我们已经掌握了地址偏移
和指针偏移
。
- 只有知道
属性类型
,占用空间大小
,才能使用地址偏移
和指针偏移
,读取类的所有信息。
现在,我们来分析类的结构
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
。
-
点进去
发现它就是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)
果然。 对象
的方法存在类
中,类
的方法存在元类
中
下一章