Runtime(二)isa指针

一、基础

在开始探讨isa指针之前,我们要准备一些基础知识,包括位域、联合体以及内存分配的相关知识。

1.1 位域

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//定义位段
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 联合体

参考C语言共用体(Union)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#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;
}

运行结果:

1
2
3
4
5
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指针的探索:

image-20181123102401283

并且,我们通过实践得到通过isa如何得到类地址:

image-20181123102411439

现在,我们从objc的源码里,需要重新认识isa是什么了!

image-20181212194136472

2.2 isa里其他字段

通过上面我们看到联合体还包含了其他有用的字段,简单罗列如下:

image-20181212195446353

看这些字段,我们验证了如下代码,代码放在这里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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

其中一次验证的结果:

image-20181212200341187

三、一个晦涩的实例

现在我们对isa已经了解的七七八八了,可以来看一个有趣的例子,源自sunny的一道面试题。

我作了细微改动,代码如下,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@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

看到上面,有两个疑问?

  1. 这代码能不能跑起来?
  2. 跑起来,最终结果又是什么呢?

​ 答案是,可以跑起来,输出如下:

my name is <ViewController: 0x7fc1c6e15df0>

​ 那么疑问就来了,这name怎么来的。

​ 要解释这个问题,我们还是要了解OC/C的内存布局以及函数调用栈帧:

​ 看第一个问题:能不能跑起来?

3.1 能不能运行?

先看看平时是如何调用方法的:

1
2
BFPerson *person = [[BFPerson alloc] init];
[person print];

再看看,这个面试题中又是如何调用的:

1
2
3
4
5
6
7
- (void)viewDidLoad {
[super viewDidLoad];

id cls = [BFPerson class];
void *obj = &cls;
[(__bridge id)obj print];
}

现在,我们将两中调用的方式作了如下的对比:

image-20181213071551847

​ 从上图可以看到:

​ 两者在内存中的方式极其类似。我们再还原一下,一个实例对象是如何调用实例方法的过程:

  1. 根据实例对象,找到其isa指针;
  2. 根据isa找到类对象,类对象存储了实例方法,找到方法列表,找出实例方法;
  3. 调用实例方法;

​ 不管是[obj print]还是[person print],我们目的是找到类对象,此处,person是指向实例对象的指针,当然能找到。

​ 那么!

obj能找到吗?

​ 可以的,obj指向了cls,而cls直接指向了类对象,那么通过cls,这个相当于isa指针的指针,obj是能找到类对象的。

​ 那么,既然能找到类对象,调用其实例方法也就顺理成章。

我们的目标就是:找到cls后第一个指针变量。

3.2 运行结果的探讨

下面是我们最终的输出函数,要输出实例对象的name

1
2
3
4
- (void)print
{
NSLog(@"my name is %@", self.name);
}

根据上面的探讨,我们知道cls和实例对象person中的isa非常类似,在本质上,其实就是isa指针的变体

最终person是找到其name属性就是直接在isa指针后的第一个成员变量值,而且必须是指针变量,因为我们知道name是一个指针变量。

那么,我们只要找到cls后面第一个指针变量即可。

现在,重新回到函数:

1
2
3
4
5
6
7
- (void)viewDidLoad {
[super viewDidLoad];

id cls = [BFPerson class];
void *obj = &cls;
[(__bridge id)obj print];
}

viewDidLoad是一个函数,根据函数栈帧,局部变量分布在栈空间。

现在,将以上代码转为C++代码。

1
2
3
4
5
6
7
8
9
10
11
//[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转换为一个结构体,这个在后面会讲到:

1
2
3
4
struct objc_super {
__unsafe_unretained _Nonnull id receiver;
__unsafe_unretained _Nonnull Class class;
};

其他几行,我们非常熟悉。

现在来看一下该函数内部的局部变量有哪些:

  • objc_super临时变量
  • cls
  • obj

它们内存空间分布如下:

image-20181213074055539

现在我们找到了cls的第一个指针变量self,这也就是结果,在开始给出的输出:

my name is <ViewController: 0x7fc1c6e15df0>

我们还可以验证一下:

看是不是cls后的第一个变量就行:

1
2
3
4
NSString *test = @"good";
id cls = [BFPerson class];
void *obj = &cls;
[(__bridge id)obj print];

上面的就会直接输出test的值。

另外,我们再验证下,假如不是指针变量是不是可以。

1
2
3
4
5
NSString *test = @"good";
int age = 18;
id cls = [BFPerson class];
void *obj = &cls;
[(__bridge id)obj print];

最终结果如下:

image-20181213093129593

参考

链接

  1. C/C++位域结构深入解析
  2. C语言共用体(Union)
  3. objc4源码

示例代码

  1. 位操作
  2. isa指针
  3. isa有趣的实例