0. 基础准备
0.1 大小端模式的内存存储和读取规则
arm64
采用的是小端模式
存储:数据的低位保存在内存的低地址中,而数据的高位保存在内存的高地址中
读取:从高位地址到地位地址开始读取
大端模式与之相反
字节转化
1byte(字节) = 8bit(位)二进制
1位十六进制 = 4位二进制
2位十六进制 = 8位二进制 = 1byte(字节)
苹果开源代码:https://opensource.apple.com/tarballs/objc4/
0.2 将OC
的文件转化为C++
文件
这种方式没有指定架构
clang -rewrite-objc main.m -o main.cpp
我们可以指定架构模式的命令行,使用Xcode
工具 xcrun
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
0.3 lldb
操作内存
读取内存:
memory read 0x10074c450
// 简写形式:x 0x10074c450
// 增加读取条件:
x/4xw 0x10074c450
// 或者memory read/4xw 0x10074c450
/** 后面的读取条件参数 “4xw” 解读:
1. 4则表示读取4次内存
2. x表示以16进制的方式读取数据
x是16进制,f是浮点,d是10进制
3. w表示每次读取4字节
b:byte 1字节,h:half word 2字节,w:word 4字节,g:giant word 8字节
*/
往内存写入的数据:
memory write 0x100400c68 6
通过打断点查看内存数据:
Debug Workflow -> viewMemory address -> 输入内存地址
1. 系统对象的底层实现
我们平时编写的Objective-C
代码,底层实现其实都是C\C++
代码。
graph LR
Objective-C --> C/C++
C/C++ --> 汇编语言
汇编语言 --> 机器语言
OC的代码:
int main(int argc, char * argv[]) {
@autoreleasepool {
NSObject *obj = [[NSObject alloc] init];
}
return 0;
}
我们通过xcrun
命令行将O
C的main.m
文件转化成C++
的文件main-arm64.cpp
然后在main-arm64.cpp
文件中搜索NSObjcet
,可以找到NSObjcet_IMPL
(IMPL
代表 implementation
实现)
// NSObject的实现
struct NSObject_IMPL {
Class isa;
};
/** 查看Class的本质
我们发现Class其实就是一个结构体指针,对象底层实现其实就是这个样子。
*/
typedef struct objc_class *Class;
通过查看底层的C++
代码,我们发现NSObject
对象在底层就是一个结构体指针
那么这个结构体占多大的内存空间呢,我们发现这个结构体只有一个成员,那就是isa
指针,而指针在64位架构中占8个字节。也就是说一个NSObjec
对象所占用的内存是8个字节。
假设isa
的地址为0x100400110
,那么上述代码分配存储空间给NSObject
对象,然后将存储空间的地址赋值给obj
指针。obj
存储的就是isa
的地址。obj
指向内存中NSObject
对象地址,即指向内存中的结构体的地址,也就是isa
的位置。
graph LR
objc --> isa
1.1 NSObject
对象的内存布局
通过函数获取NSObject
对象内存大小:
NSObject *obj = [[NSObject alloc]init];
// 获得NSObject类的实例对象的成员变量占用的大小 >> 8
NSLog(@"%zd", class_getInstanceSize([NSObject class]));
// 获得obj指针所指向内存的大小 >> 16
NSLog(@"%zd", malloc_size((__bridge const void*)obj));
我们发现两种方式获取的大小是不一致的,我们通过底层源码来查看为什么会不一样。
class_getInstanceSize
:
size_t class_getInstanceSize(Class cls)
{
if (!cls) return 0;
return cls->alignedInstanceSize();
}
// 返回成员变量占用大小
// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() const {
return word_align(unalignedInstanceSize());
}
alloc
方法其实是调用了allocWithZone
方法
- (NSUInteger)retainCount {
return _objc_rootRetainCount(self);
}
+ (id)alloc {
return _objc_rootAlloc(self);
}
// Replaced by ObjectAlloc
+ (id)allocWithZone:(struct _NSZone *)zone {
return _objc_rootAllocWithZone(self, (malloc_zone_t *)zone);
}
// Replaced by CF (throws an NSException)
+ (id)init {
return (id)self;
}
- (id)init {
return _objc_rootInit(self);
}
↓↓↓
id
_objc_rootAlloc(Class cls)
{
return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
↓↓↓
// 调用 [cls alloc]或者 [cls allocWithZone:nil]
// Call [cls alloc] or [cls allocWithZone:nil], with appropriate
// shortcutting optimizations.
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
#if __OBJC2__
if (slowpath(checkNil && !cls)) return nil;
if (fastpath(!cls->ISA()->hasCustomAWZ())) {
return _objc_rootAllocWithZone(cls, nil);
}
#endif
// No shortcuts available.
if (allocWithZone) {
return ((id(*)(id, SEL, struct _NSZone *))objc_msgSend)(cls, @selector(allocWithZone:), nil);
}
return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));
}
↓↓↓
id
_objc_rootAllocWithZone(Class cls, malloc_zone_t *zone __unused)
{
// allocWithZone under __OBJC2__ ignores the zone parameter
return _class_createInstanceFromZone(cls, 0, nil,
OBJECT_CONSTRUCT_CALL_BADALLOC);
}
allocWithZone
:
id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone)
{
void *bytes;
size_t size;
// Can't create something for nothing
if (!cls) return nil;
// 分配内存,此处获得的是实例对象占用的大小,为8
// Allocate and initialize
size = cls->alignedInstanceSize() + extraBytes;
// 在这里做判断,CoreFoundation要求所有对象至少为16字节
// CF requires all objects be at least 16 bytes.
if (size < 16) size = 16;
if (zone) {
bytes = malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
} else {
// 对应上面的size
bytes = calloc(1, size);
}
return objc_constructInstance(cls, bytes);
}
通过lldb
查看内存分配:
(lldb) x 0x600003ba00a0
0x600003ba00a0: 00 1d be 89 ff 7f 00 00 00 00 00 00 00 00 00 00 ................
0x600003ba00b0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
我们发现只用了前8字节00 1d be 89 ff 7f 00 00
,后8字节都是000 00 00 00 00 00 00 00
一个NSObject
对象占用多少内存?
系统分配了16个字节给NSObject
对象(通过malloc_size
函数获得)
,但NSObject
对象内部只使用了8个字节的空间(64bit环境下,可以通过class_getInstanceSize
函数获得)
2. 自定义对象的底层实现
#import <Foundation/Foundation.h>
// Student继承自NSObject
@interface Student: NSObject {
@public
int _no;
int _age;
}
@end
@implementation Student
@end
生成C++
文件,并且查找Student_IMPL
:
struct Student_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int _no;
int _age;
};
我们发现结构体的第一个成员是其父类NSObject
的底层实现 NSObject_IMPL
结构体。而通过上面的验证我们知道NSObject_IMPL
内部其实就是Class isa
,所以struct NSObject_IMPL NSObject_IVARS
等价于Class isa
。
上面的实现可以转化为:
struct Student_IMPL {
Class *isa;
int _no;
int _age;
};
因此此Student_IMPL
结构体占用多少存储空间,对象就占用多少存储空间。
结构体占用的存储空间为:isa
指针(8字节) + int
类型_no
(4字节) + int
类型_age
(4字节),共16字节。
上述代码实际上在内存中的体现为:创建Student
对象首先会分配16字节,存储3个东西:
- 8字节的
isa
指针 - 4字节的
_no
成员变量 - 4字节的
_age
成员变量
2.1 Student
对象的内存布局
#import <Foundation/Foundation.h>
struct Student_IMPL {
Class isa;
int _no;
int _age;
};
@interface Student: NSObject {
@public
int _no;
int _age;
}
@end
@implementation Student
int main(int argc, char * argv[]) {
@autoreleasepool {
Student *stu = [[Student alloc] init];
stu -> _no = 4;
stu -> _age = 5;
NSLog(@"%@", stu); // 0x600002994000
// 将oc的对象指针stu强制转化为一个结构体指针,成员变量会一一对应
struct Student_IMPL *stuImpl = (__bridge struct Student_IMPL *)(stu);
NSLog(@"_no = %d, _age = %d", stuImpl->_no, stuImpl->_age);
// 打印结果:_no = 4, _age = 5
}
return 0;
}
@end
上述代码将OC
对象强转成Student_IMPL
的结构体。也就是说把一个指向OC
对象的指针,指向这种结构体,如果可以转化成功,说明我们的猜想正确。由此说明stu
这个对象指向的内存确实是一个结构体指针,准确的说是指向结构体的第一个成员Class isa
的地址
通过函数来获取内存大小:
NSLog(@"%zd", class_getInstanceSize([Student class])); // 16
NSLog(@"%zd", malloc_size((__bridge const void *)stu)); // 16
实时查看内存数据:
从上图中,我们可以发现stu
对象的内存读取数据从高位数据开始读,查看前16字节,每四个字节读出的数据为
16进制地址:0x0000004
(4字节,_no
),0x0000005
(4字节, _age
),00D1081000001119
(8字节, isa
)
通过lldb
:
2020-01-08 11:37:23.581510+0800 OC对象的本质[7208:132335] <Student: 0x6000033701a0>
2020-01-08 11:37:23.582071+0800 OC对象的本质[7208:132335] _no = 4, _age = 5
(lldb) memory read 0x6000033701a0
0x6000033701a0: 78 ae d4 06 01 00 00 00 04 00 00 00 05 00 00 00 x...............
0x6000033701b0: b0 01 82 92 60 9c 00 00 fb 07 6c 6f 67 64 00 00 ....`.....logd..
(lldb) x/4xw 0x6000033701a0 // 16进制,每4字节(8位16进制地址)读取一次,读取16字节的内容
0x6000033701a0: 0x06d4ae78 0x00000001 0x00000004 0x00000005
(lldb) x/4dw 0x6000033701a0 // 10进制读取
0x6000033701a0: 114601592
0x6000033701a4: 1
0x6000033701a8: 4
0x6000033701ac: 5
(lldb) memory write 0x6000033701a8 6 // 修改_no的值
(lldb) po stu
<Student: 0x6000033701a0>
(lldb) po stu -> _no
6
(lldb)
3. 继承关系底层实现
// Father
@interface Father : NSObject {
int _age;
}
@end
@implementation Father
@end
// Son
@interface Son : Father {
int _no;
}
@end
@implementation Son
@end
Father *father = [[Father alloc] init];
Son *son = [[Son alloc] init];
NSLog(@"%zd",class_getInstanceSize([Father class])); // 16
NSLog(@"%zd",malloc_size((__bridge const void *)father));// 16
NSLog(@"%zd",class_getInstanceSize([Son class])); // 16
NSLog(@"%zd",malloc_size((__bridge const void *)son)); // 16
依据上面的分析与发现,对象实质上是以结构体的形式存储在内存中
struct NSObject_IMPL {
Class isa;
};
struct Father_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int _age;
};
// father的最终实现
struct Father_IMPL {
Class isa; // 8字节
int _age; // 4字节
};// 16字节,内存对齐
struct Son_IMPL {
struct Father_IMPL Father_IVARS;// 12字节
int _no; // 4字节
};
// son的最终实现
struct Son_IMPL {
Class isa; // 8字节
int _age; // 4字节
int _no; // 4字节
};// 16字节,内存对齐
我们发现只要是继承自NSObject
的对象,那么底层结构体内一定有一个isa
指针。那么他们所占的内存空间是多少呢?单纯的将指针和成员变量所占的内存相加即可吗?
上述代码实际打印的内容都是16,也就是说son
对象和father
对象所占用的内存空间都为16个字节。
而且我们发现通函数class_getInstanceSize
打印出是16,但是father
对象的结构体只占12字节,这是因为内存对齐的原因,class_getInstanceSize
函数打印出的是内存对齐之后的大小。
3.1 内存对齐
编译器在给结构体开辟空间时,首先找到结构体中最宽的基本数据类型,然后寻找内存地址能是该基本数据类型的整倍的位置,作为结构体的首地址。将这个最宽的基本数据类型的大小作为对齐模数。
为结构体的一个成员开辟空间之前,编译器首先检查预开辟空间的首地址相对于结构体首地址的偏移是否是本成员的整数倍,若是,则存放本成员,反之,则在本成员和上一个成员之间填充一定的字节,以达到整数倍的要求,也就是将预开辟空间的首地址后移几个字节。
我们可以总结内存对齐为两个原则:
- 前面成员的地址必须是后面成员的地址整数倍,不是就补齐。
- 整个
Struct
结构体的地址必须是占用最大字节成员的整数倍。
通过上述内存对齐的原则我们来看,father
对象的第一个地址要存放isa
指针需要8个字节,第二个地址要存放_age
成员变量需要4个字节,根据原则1,8是4的整数倍,符合原则一,不需要补齐。然后检查原则2,目前father
对象共占据12个字节的内存,不是最大字节数8个字节的整数倍,所以需要补齐4个字节,因此father
对象就占用16个字节空间。
而对于son
对象,我们知道son
对象中,包含father
对象的结构体实现,和一个int
类型的_no
成员变量,同样isa
指针8个字节,_age
成员变量4个字节,_no
成员变量4个字节,刚好满足原则1和原则2,所以student
对象占据的内存空间也是16个字节。
3.2 OC对象的属性
@interface Father : NSObject {
@public
int _age;
}
@property (nonatomic, assign) int height; // 属性会自动生成成员变量_height
/**
属性还会生成set方法和get方法
但是方法不可能放在实例对象里面
不同的实例对象拥有自己的成员变量,拥有不同的内存但是方法是公共的,每个对象调用的方法都是一样的
每必要每次都存储实例对象里面
*/
// 底层实现
struct Father_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int _age;
int _height // 属性生成成员变量
};
3.3 alloc
的size
分析
@interface HJRPerson : NSObject
{
int _age;
int _height;
int _no;
}
@end
@implementation HJRPerson
@end
// 底层实现
struct Student_IMPL {
Class isa; // 父类NSObject只有一个isa,8字节
int _age; // 4字节
int _height; // 4字节
int _no; // 4字节
}; // 内存对齐之后:24字节
使用函数打印内存占用的大小
HJRPerson *person = [[HJRPerson alloc]init];
NSLog(@"%zd", class_getInstanceSize([HJRPerson class])); // 24
NSLog(@"%zd", malloc_size((__bridge const void*)person)); // 32
我发现实例对象的成员变量占用的内存确实是24字节,但是person
对象是32字节,为什么会多分配内存?我查看源码:
// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() const {
return word_align(unalignedInstanceSize());
}
id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone)
{
void *bytes;
size_t size;
// Can't create something for nothing
if (!cls) return nil;
// 调用类的alignedInstanceSize
size = cls->alignedInstanceSize() + extraBytes;
// CF requires all objects be at least 16 bytes.
if (size < 16) size = 16;
if (zone) {
bytes = malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
} else {
// 调用calloc分类内存,size是内存对齐字后的字节,在上面的代码中就是24
// calloc函数分配了32字节,所以需要查看calloc函数的实现
bytes = calloc(1, size);
}
return objc_constructInstance(cls, bytes);
}
3.4 libmalloc
源码:
源码:https://opensource.apple.com/tarballs/libmalloc/
我们发现源码里面有好多的内存实现C文件:
frozen_malloc.c
legacy_malloc.c
magazine_malloc.c
malloc.c
nano_malloc.c
nanov2_malloc.c
purgeable_malloc.c
Apple
会采用不同的方式分配内存。
我们在malloc.c
发现了alloc(size_t num_items, size_t size)
函数:
void *
calloc(size_t num_items, size_t size)```
{
void *retval;
retval = malloc_zone_calloc(default_zone, num_items, size);
if (retval == NULL) {
errno = ENOMEM;
}
return retval;
}
底层的操作系统在分配内存的时候也需要内存对齐,
#define NANO_MAX_SIZE 256 /* Buckets sized {16, 32, 48, ..., 256} */
主要是为了提高CPU的访问速度,在此为OC对象分配内存都是16的倍数,即使calloc(size_t num_items, size_t size)
函数参数传的是24字节,但是操作系统为了提高CPU的访问速度,所以需要内存对齐,在这儿分配了32字节。
所以我们可以认为class_getInstanceSize
函数返回的是实例对象至少需要多少内存,是返回成员变量的内存字节,而malloc_size
返回的是系统实际返回多少内存,
sizeof()
返回的是类型占用的字节数,sizeof()
不是个函数,而是个运算符,在编译期间值已经就是确定的,即使参数传的是对象和变量,在编译期间也会转成计算类型的大小
int a = 10;
sizeof(a); // 4
NSObject *objc = [[NSObject alloc]init];
sizeof(objc); // 8
4. OC对象的种类
OC
中的对象,主要可以分为3种:instance
对象(实例对象)、class
对象(类对象)、meta-class
对象(元类对象)。
4.1 instance
对象(实例对象)
instance
对象就是通过类alloc
出来的对象,每次调用alloc
都会产生新的instance
对象
instance
对象在内存中存储的信息包括:
-
isa
指针 - 其他成员变量
Father *father1 = [[Father alloc] init];
father1->_age = 10;
father1
对象指向isa
指针,isa
指针在底层的结构体当中总是最前面的。
struct Father_IMPL {
Class isa
_age;
}
即使是继承关系也遵循这条原则。
在实例对象中根本没有看到方法,那么实例对象的方法的代码放在什么地方呢?那么类的方法的信息,协议的信息,属性的信息都存放在什么地方呢?
4.2 class
对象(类对象)
NSObject *object1 = [[NSObject alloc] init];
NSObject *object2 = [[NSObject alloc] init];
// 调用的对象的class方法获取类对象
Class objectClass1 = [object1 class];
Class objectClass2 = [object2 class];
// 调用类的class方法获取类对象
Class objectClass3 = [NSObject class];
// 调用runtime方法object_getClass获取类对象
Class objectClass4 = object_getClass(object1);
Class objectClass5 = object_getClass(object2);
NSLog(@"%p %p %p %p %p", objectClass1, objectClass2, objectClass3, objectClass4, objectClass5);
// 调用class方法获取到的一直都是class对象
它们是同一个对象。每个类在内存中有且只有一个class
对象。所以它里面存储一些固定的相关信息(类型、名称等)
class
对象在内存中存储的信息主要包括:
-
isa
指针 -
superclass
指针 - 类的属性信息(
@property
)、类的对象方法信息(instance method
) - 类的协议信息(
protocol
)、类的成员变量信息(ivar
)
这里的类的成员变量信息(ivar
)不是指成员变量的值,而是一些描述信息(类型、名字等),不要和实例对象存储成员变量的值搞混。
成员变量的值是存储在实例对象中的,因为只有当我们创建实例对象的时候才为成员变赋值。但是成员变量叫什么名字,是什么类型,只需要有一份就可以了。所以存储在class对象中。
那么类方法放在哪里?
4.3 meta-class
对象(元类对象)
// runtime方法中传入类对象获得的就是元类对象
Class objectMetaClass = object_getClass([NSObject class]);
// 而调用类对象的class方法时得到还是类对象,无论调用多少次都是类对象
Class cls = [[NSObject class] class];
// 判断该对象是否为元类对象
class_isMetaClass(objectMetaClass);
每个类在内存中有且只有一个meta-class
对象。
meta-class
对象和class
对象的内存结构是一样的(都是class
对象),但是用途不一样,所以meta-class
中也有类的属性信息,类的对象方法信息等成员变量,但是其中的值可能是空的,在内存中存储的信息主要包括:
-
isa
指针 -
superclass
指针 - 类的类方法信息(
class method
)
4.4 objc_getClass
和object_getClass
区别
源码:
Class objc_getClass(const char *aClassName)
{
if (!aClassName) return Nil;
// 根据类的名字,返回类对象
// NO unconnected, YES class handler
return look_up_class(aClassName, NO, YES);
}
Class object_getClass(id obj)
{
// obj如果是instance对象,返回class对象
// obj如果是class对象,返回meta-class对象
// obj如果是meta-class对象,返回NSObject的meta-class对象
if (obj) return obj->getIsa();
else return Nil;
}
// class方法返回的就是类对象
5. isa
和super_class
问题
- 对象的
isa
指针指向哪里? - OC的类信息存放在哪里?
5.1 instance
实例对象的isa
// 实例对象调用实例方法
[father1 fatherInstanceFunc];
当对象调用实例方法的时候,我们上面讲到,实例方法信息是存储在class
类对象中的,那么要想找到实例方法,就必须找到class
类对象,那么此时isa
的作用就来了。
instance
实力对象的isa
指向class
类对象,当调用对象方法时,通过instance
的isa
找到class
类对象,最后找到对象方法的实现进行调用。
5.2 class
类对象的isa
// 类对象调用类方法
[Father fatherClassFunc];
当类对象调用类方法的时候,同上,类方法是存储在meta-class
元类对象中的。那么要找到类方法,就需要找到meta-class
元类对象,而class
类对象的isa
指针就指向元类对象。
class
类对象的isa
指向meta-class
元类对象
,当调用类方法时,通过class
的isa
找到meta-class
类对象,最后找到类方法的实现进行调用。
实例对象、类对象、元类对象通过各自的isa
指针关联了起来。
当对象调用其父类对象方法的时候,又是怎么找到父类对象方法的呢?,此时就需要使用到class
类对象的superclass
指针。
5.3 super_class
指针
类对象的super_class
[son fatherInstanceFunc];
当子类Son
的实例对象要调用父类Father
的实例方法时,会先通过自己Son
的实例对象的isa
找到自己Son
的类对象,然后通过自己Son
的类对象的superclass
找到父类Father
的class
类对象,最后找到实例方法的实现进行调用,同样如果父类Father
发现自己没有响应的实例方法,又会通过Father
类对象的superclass
指针找到其父类NSObject
的class
类对象,去寻找响应的方法。
继承关系的方法调用通过各自的superclass
指针关联起来。
元类对象的superclass
当类对象调用父类的类方法时,就需要先通过自己的isa
指针找到自己的meta-class
元类对象,然后通过superclass
去寻找响应的方法
[Son load];
[Son fatherClassFunc];
当子类Son
的class
类对象要调用父类Father
的类方法时,会先通过自己的类对象的isa
找到自己Son
的meta-class
元类对象,然后通过superclass
找到Father
的meta-class
元类对象,最后在元类对象找到类方法的实现进行调用。
官方isa
和superclass
指向图:
基类的元类对象的superclass
指向类对象
HJRPerson
:
interface HJRPerson : NSObject
+ (void)test;
@end
@implementation HJRPerson
//+ (void)test {
// NSLog(@"+[HJRPerson test], %p", self);
//}
@end
NSObject+Test
:
@interface NSObject (Test)
//- (void)test;
+ (void)test;
@end
@implementation NSObject (Test)
- (void)test {
NSLog(@"-[NSObject test], %p", self);
}
//+ (void)test {
// NSLog(@"+[NSObject test], %p", self);
//}
@end
调用:
HJRPerson *person = [[HJRPerson alloc] init];
NSObject *objc = [[NSObject alloc] init];
NSLog(@"HJRPerson: %p, NSObject: %p", [HJRPerson class], [NSObject class]);
[HJRPerson test];
[NSObject test];
输出:
2020-04-06 21:30:49.124546+0800 OC对象的本质[79409:6378199] HJRPerson: 0x10709b328, NSObject: 0x7fff89be1d00
2020-04-06 21:30:49.125176+0800 OC对象的本质[79409:6378199] -[NSObject test], 0x10709b328
2020-04-06 21:30:49.125258+0800 OC对象的本质[79409:6378199] -[NSObject test], 0x7fff89be1d00
我们发现一个类对象可以成功的调用一个实例方法,具体分析一下:
HJRPerson
通过实例对象的isa
找到自己的类对象,再通过类对象的isa
找到元类对象,发现元类对象里面没有实现test
方法,所以通过元类对象的superclass
找到NSObject
的的元类,还没有找到test
方法,所以再通过NSObject
的元类的superclass
找到了NSObject
的类对象,在类对象中找到了test
方法,所以最后调用成功了test
实例方法
NSObject
类对象为什么也会调用成功实例方法test
,上面已经说明了。
5.4 总结
-
instance
实例对象的isa
指向class
类对象 -
class
类对象的isa
指向meta-class
元类对象 -
meta-class
元类对象的isa
指向基类的meta-class
元类对象,基类的isa
指向自己 -
class
类对象的superclass
指向父类的````class类对象,如果没有父类,
superclass```指针为nil -
meta-class
元类对象的superclass
指向父类的meta-class
元类对象,基类的meta-class
的superclass
指向基类的class
类对象 - 实例对象调用对象方法的轨迹,通过
isa
找到自己的类对象,如果方法不存在,就通过superclass
找父类 - 类对象调用类方法的轨迹,通过
isa
找到元类对象,如果方法不存在,就通过superclass
找父类
6. 验证isa
和super_class
指针指向
6.1 isa
指向
我们通过如下代码证明:
// 实例对象
NSObject *instanceObjc = [[HJRPerson alloc] init];
// 类对象
Class classObjc = [HJRPerson class];
// 元类对象
Class metaClassObjc = object_getClass([HJRPerson class]);
打断点并通过控制台打印相应对象的isa
指针
(lldb) p/x (long)instanceObjc->isa
(long) $0 = 0x000000010d3b5320
(lldb) p/x classObjc
(Class) $1 = 0x000000010d3b5320 HJRPerson
(lldb)
发现实例对象的isa
确实是指向类对象。
再进行验证类对象的isa
指向:
(lldb) p/x classObjc->isa
error: <user expression 3>:1:10: member reference base type 'Class' is not a structure or union
classObjc->isa
~~~~~~~~~^ ~~~
(lldb)
发现类对象classObjc
中并没有isa
指针,我们来到Class
内部看一下数据结构:
typedef struct objc_class *Class;
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */
objc_class
结构体内确实是有一个isa
和super_class
指针,为了拿到isa
指针的地址,我们自己创建一个同样的结构体并通过强制转化拿到isa
指针:
// isa指针结构
struct hjr_objc_class{
Class isa;
};
// 类对象的isa指向自己的元类对象
struct hjr_objc_class *classObjc_isa = (__bridge struct hjr_objc_class *)(classObjc);
此时我们重新验证一下:
(lldb) p/x classObjc_isa->isa
(Class) $2 = 0x000000010d3b52f8
(lldb) p/x metaClassObjc
(Class) $3 = 0x000000010d3b52f8
(lldb)
类对象的isa
指针的地址是元类对象的地址。
6.2 super_class
指向
再来看看super_class
指向:
// 拿到super_class指针
struct hjr_objc_class{
Class isa;
Class super_class;
const char *name;
};
struct hjr_objc_class *superClsObjc = (__bridge struct hjr_objc_class *)[Father class];
struct hjr_objc_class *sonClsObjc = (__bridge struct hjr_objc_class *)[Son class];
(lldb) p/x sonClsObjc->super_class
(Class) $0 = 0x000000010a9db388 Father
(lldb) p/x superClsObjc
(hjr_objc_class *) $1 = 0x000000010a9db388
子类类对象的super_class
指针确实是指向父类的类对象。
Tips
有的版本的Xcode
我们发现isa
与对象的地址不同
,isa
需要进行一次位&
运算,才能计算出真实地址,而位&
运算的值我们可以通过objc
源代码找到:
# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
# elif __x86_64__
# define ISA_MASK 0x00007ffffffffff8ULL
# else
# error unknown architecture for packed isa
# endif