这次具体分析以下代码打印结果:
id cls = [Person class];
void *obj = &cls;
[(__bridge id)obj print];
相关类如下:
Person类
@interface Person : NSObject
@property (copy, nonatomic) NSString *name;
- (void)print;
@end
#import "Person.h"
@implementation Person
- (void)print
{
NSLog(@"my name is %@", self->_name);
}
@end
ViewController类
#import "ViewController.h"
#import "Person.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
id cls = [Person class];
void *obj = &cls;
[(__bridge id)obj print];
}
@end
通过上边的代码有以下几个问题
1. print
能不能调用成功?为什么?
2. 如果成功print
打印什么内容
想要弄清楚以上几个问题需要了解如标题所示的内部原理,只有掌握了这些才能回答好上边的问题。
一. 指针关系
-
正常情况下person的isa指向如图:
正常的personisa指向.png
-
cls、obj指向如图:
cls指向.png
-
对比如图:
对比图.png
通过上边几张图对比可以发现通过[(__bridge id)obj print];
调用方法和通过Person
对象调用方法非常相似。
正常情况下Person
对象调用方法通过该对象的isa
指针找到对应的类对象,然后开始方法查找,此处cls
指向的地址存放的就是Person
类对象,将cls
的地址赋值给obj
就相当于objc
就是Person
对象,cls
就是该对象的isa
指针,所以方调用会成功。
二. 内存布局
1. super具体作用
这里需要再次深入解析[super viewDidLoad];
具体是什么?
使用命令进行转换xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc ViewController.m -o ViewController.cpp
这样获取到的代码只是个大概实现,具体实现还需要根据LLVM具体版本来定,但是这个实现几乎就是运行时的实现。
[super viewDidLoad];
转换后如下:
((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("ViewController"))}, sel_registerName("viewDidLoad"));
删除转义代码
objc_msgSendSuper)({
self, class_getSuperclass(objc_getClass("ViewController"))},
sel_registerName("viewDidLoad")
);
可以看到第一个参数是一个结构体如下:
{
self,
class_getSuperclass(objc_getClass("ViewController"))
}
该结构体第一个参数是一个OC对象且该对象只有一个isa
指针所以占用8个字节,至于第二个参数是ViewController还是它的父类需要使用运行时或者直接查看中间代码来确定。
可以使用以下命令行指令生成中间代码
clang -emit-llvm -S main.m
不管是使用运行时查看Debug Workflow还是使用生成中间件的方式都可以看到实际使用的不是objc_msgSendSuper
而是objc_msgSendSuper2
。然后通过在objc源码里边查找objc_msgSendSuper2
可以在objc-msg-arm64中看到如下代码:
// no _objc_msgLookupSuper
ENTRY _objc_msgSendSuper2
UNWIND _objc_msgSendSuper2, NoFrame
MESSENGER_START
ldp x0, x16, [x0] // x0 = real receiver, x16 = class
ldr x16, [x16, #SUPERCLASS] // x16 = class->superclass
CacheLookup NORMAL
END_ENTRY _objc_msgSendSuper2
这段代码可以看出objc_msgSendSuper2
的两个参数分别为消息接收者和该接收者对应的类对象,然后通过class->superclass
拿到父类的类对象。
2. 局部变量内存布局
- (void)viewDidLoad
方法内的几个变量内存分布如下:

由于是在函数内部属于局部变量,所以内存分配在栈空间,栈空间从高地址向低地址分配,所以首先分配[super viewDidLoad];
所占空间,由于[super viewDidLoad];
在运行时会转换为结构体,所以以结构体的形式分配空间,第一个参数self
在低地址,然后[ViewController class]
在高地址,如上图所以,再然后分配cls
地址,接着分配obj
地址,整个内存分布图如上图所示。
在Person内部,内存分布为首先是isa
占用8个字节,然后是name
属性占用8个字节。isa
在低地址,name
紧挨着isa
在高地址。
结合最上边的几张图,所以在上图中cls
就相当于Person对象中的isa
,self
就相当于name
属性,所以打印结果为*my name is <ViewController: 0x7fd0aa505030>。
如果修改成这样:
#import "ViewController.h"
#import "Person.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSString *test = @"aaaaa";
id cls = [Person class];
void *obj = &cls;
[(__bridge id)obj print];
}
@end
内存分布图为:

打印结果为my name is aaaaa
以上两种情况都可以成功打印是因为self
和test
都是8个字节,如果把NSString *test = @"aaaaa";
换成int a = 1;
就会出错,因为在读取内存的时候实际是按照name
8个字节读取的,读取的时候int只有4个字节,剩余的4个字节是其他的部分,会导致取值失败,所以崩溃。
网友评论