一、基础
在开始探讨isa指针之前,我们要准备一些基础知识,包括位域、联合体以及内存分配的相关知识。
1.1 位域
//定义位段
struct mybitfields
{
unsigned short a : 4;
unsigned short b : 5;
unsigned short c : 7;
} test;
int main( void );
{
test.a = 2;
test.b = 31;
test.c = 0;
}
//最后如下显示
00000001 11110010
cccccccb bbbbaaaa
位域表示的是,在一个结构体中,用位来存储数据。
关于位域的内存分配,有几个值得注意的点:
- 位域的最小内存不能小于位域中最大的修饰符,上面实例都是
short,即最小不能小于short的大小; - 位域的最小内存不能小于所有位域字段加起来的大小,并且根据内存对齐,可能会分配多余。假设所有位域字段加起来为18位,那么就会分配32位,即4字节。
- 位域中如果有无名位域,那么无名位域会强制下一位域对齐到其下一type位域的边界。(C/C++位域结构深入解析)
1.2 联合体
#include <stdio.h>
union data{
int n;
char ch;
short m;
};
int main(){
union data a;
printf("%d, %d\n", sizeof(a), sizeof(union data) );
a.n = 0x40;
printf("%X, %c, %hX\n", a.n, a.ch, a.m);
a.ch = '9';
printf("%X, %c, %hX\n", a.n, a.ch, a.m);
a.m = 0x2059;
printf("%X, %c, %hX\n", a.n, a.ch, a.m);
a.n = 0x3E25AD54;
printf("%X, %c, %hX\n", a.n, a.ch, a.m);
return 0;
}
运行结果:
4, 4
40, @, 40
39, 9, 39
2059, Y, 2059
3E25AD54, T, AD54
关于位域和位操作,这里有一个Demo,原出于MJ。
二、isa
isa 在arm64架构前,是一个指针,指向类对象或元类对象。
但是在arm64之后,是个联合体,里面包含了更丰富的实例对象的信息,存储着更多运行期使用的信息。
需要提醒的是,本系列中讨论的isa,都只针对iOS,且只针对64位。
2.1 isa
我们先回顾一下,在Objective-C(八)对象的本质及分类,对isa指针的探索:
并且,我们通过实践得到通过isa如何得到类地址:
现在,我们从objc的源码里,需要重新认识isa是什么了!
2.2 isa里其他字段
通过上面我们看到联合体还包含了其他有用的字段,简单罗列如下:
看这些字段,我们验证了如下代码,代码放在这里:
NSLog(@"1----%p", [BFPerson class]);
//0:nonpointer, 是否指针优化过,优化过即1,表示存储更多信息
BFPerson *person1 = [[BFPerson alloc] init];
//此时person1->isa 0x000001a100d88e39
//has_assoc, 添加关联对象
objc_setAssociatedObject(person1, @"test", @"good", OBJC_ASSOCIATION_RETAIN);
//person1->isa 0x000001a100d88e3b
//shiftclass,isa地址
BFPerson *person2 = person1;
//person1->isa 0x000021a100d88e3b
// weakly_referenced,是否被弱引用
__weak BFPerson *person3 = person1;
//person1->isa 0x000025a100d88e3b
其中一次验证的结果:
三、一个晦涩的实例
现在我们对isa已经了解的七七八八了,可以来看一个有趣的例子,源自sunny的一道面试题。
我作了细微改动,代码如下,
@interface BFPerson : NSObject
@property (nonatomic, copy) NSString * name;
- (void)print;
@end
@implementation BFPerson
- (void)print
{
NSLog(@"my name is %@", self.name);
}
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
id cls = [BFPerson class];
void *obj = &cls;
[(__bridge id)obj print];
}
@end
看到上面,有两个疑问?
-
这代码能不能跑起来?
-
跑起来,最终结果又是什么呢?
答案是,可以跑起来,输出如下:
my name is <ViewController: 0x7fc1c6e15df0>
那么疑问就来了,这`name`怎么来的。
要解释这个问题,我们还是要了解`OC/C`的内存布局以及函数调用栈帧:
看第一个问题:能不能跑起来?
3.1 能不能运行?
先看看平时是如何调用方法的:
BFPerson *person = [[BFPerson alloc] init];
[person print];
再看看,这个面试题中又是如何调用的:
- (void)viewDidLoad {
[super viewDidLoad];
id cls = [BFPerson class];
void *obj = &cls;
[(__bridge id)obj print];
}
现在,我们将两中调用的方式作了如下的对比:
从上图可以看到:
两者在内存中的方式极其类似。我们再还原一下,一个实例对象是如何调用实例方法的过程:
- 根据实例对象,找到其
isa指针; - 根据
isa找到类对象,类对象存储了实例方法,找到方法列表,找出实例方法; - 调用实例方法;
不管是[obj print]还是[person print],我们目的是找到类对象,此处,person是指向实例对象的指针,当然能找到。
那么!
`obj`能找到吗?
可以的,`obj`指向了`cls`,而`cls`直接指向了类对象,那么通过`cls`,这个相当于`isa`指针的指针,`obj`是能找到类对象的。
那么,既然能找到类对象,调用其实例方法也就顺理成章。
我们的目标就是:找到cls后第一个指针变量。
3.2 运行结果的探讨
下面是我们最终的输出函数,要输出实例对象的name:
- (void)print
{
NSLog(@"my name is %@", self.name);
}
根据上面的探讨,我们知道cls和实例对象person中的isa非常类似,在本质上,其实就是isa指针的变体。
最终person是找到其name属性就是直接在isa指针后的第一个成员变量值,而且必须是指针变量,因为我们知道name是一个指针变量。
那么,我们只要找到cls后面第一个指针变量即可。
现在,重新回到函数:
- (void)viewDidLoad {
[super viewDidLoad];
id cls = [BFPerson class];
void *obj = &cls;
[(__bridge id)obj print];
}
viewDidLoad是一个函数,根据函数栈帧,局部变量分布在栈空间。
现在,将以上代码转为C++代码。
//[super viewDidLoad];
(
(__rw_objc_super){
self,
class_getSuperclass(objc_getClass("ViewController"))
}, sel_registerName("viewDidLoad")
);
id cls = (objc_msgSend)(objc_getClass("BFPerson"), sel_registerName("class"));
void *obj = &cls;
(objc_msgSend)(obj, sel_registerName("print"));
焦点放在第一行,在OC中,调用super方法,均会将super转换为一个结构体,这个在后面会讲到:
struct objc_super {
__unsafe_unretained _Nonnull id receiver;
__unsafe_unretained _Nonnull Class class;
};
其他几行,我们非常熟悉。
现在来看一下该函数内部的局部变量有哪些:
objc_super临时变量clsobj
它们内存空间分布如下:
现在我们找到了cls的第一个指针变量self,这也就是结果,在开始给出的输出:
my name is <ViewController: 0x7fc1c6e15df0>
我们还可以验证一下:
看是不是cls后的第一个变量就行:
NSString *test = @"good";
id cls = [BFPerson class];
void *obj = &cls;
[(__bridge id)obj print];
上面的就会直接输出test的值。
另外,我们再验证下,假如不是指针变量是不是可以。
NSString *test = @"good";
int age = 18;
id cls = [BFPerson class];
void *obj = &cls;
[(__bridge id)obj print];
最终结果如下: