我们将通过以下步骤实现几个NSString并观察底层实现:
1. 创建一个NSString实例str1,其值为@"";
2. 创建一个NSString实例str1,其值为__NSConstantStringImpl__var_folders_zr_p1cnc5b14b7bfgk7vs7_x6fh0000gp_T_main_35ff71_mi_0的地址;
3. 创建一个NSString实例str2,使用stringWithFormat:方法格式化字符串。
```objective-c
// 实现几个string看看底层实现
NSString *str1 = @""; // 通过直接赋值实现
NSString *str2 = (NSString *)&__NSConstantStringImpl__var_folders_zr_p1cnc5b14b7bfgk7vs7_x6fh0000gp_T_main_35ff71_mi_0; // 通过地址实现
NSString *str3 = [NSString stringWithFormat:@"%@", @""]; // 通过stringWithFormat:方法实现
```
在这段代码中,我们可以看到有4组字符串的创建过程。首先,我们需要将这些代码重构为一个函数,如下所示:
```objc
NSString *createString(int index) {
NSString *str = ((NSString * _Nonnull (*)(id, SEL, NSString * _Nonnull, ...))(void *)objc_msgSend)((id)objc_getClass("NSString"), sel_registerName("stringWithFormat:"), (NSString *)&__NSConstantStringImpl__var_folders_zr_p1cnc5b14b7bfgk7vs7_x6fh0000gp_T_main_35ff71_mi + index, 1);
return str;
}
```
然后,我们可以使用这个函数来创建字符串:
```objc
NSString *str1 = createString(1);
NSString *str2 = createString(2);
NSString *str3 = createString(3);
NSString *str4 = createString(4);
```
通过对比这4组字符串的创建过程,我们可以发现在底层,字符串会被构造成以`__NSConstantStringImpl__var_folders_zr_p1cnc5b14b7bfgk7vs7_x6fh0000gp_T_main_35ff71_mi`开头的结构体。其中,`__NSConstantStringImpl__var_folders_zr_p1cnc5b14b7bfgk7vs7_x6fh0000gp_T_main_35ff71_mi_n`表示字符串在内存中的偏移量。
_NSConstantStringImpl__var_folders_zr_p1cnc5b14b7bfgk7vs7_x6fh0000gp_T_main_35ff71_mi_1和__NSConstantStringImpl__var_folders_zr_p1cnc5b14b7bfgk7vs7_x6fh0000gp_T_main_35ff71_mi_3是标识占位符字符串,它们对应的占位符分别是:
- _mi_1:@"%@"
- _mi_3:@"%d"
由于_mi_1和_mi_4是一样的,所以我们只研究_mi_1和_mi_3。下面是它们的定义和实现:
```objc
// 定义 __NSConstantStringImpl 结构体
struct __NSConstantStringImpl {
int *isa; // isa 指针
int flags; // 标志位
char *str; // str 字符串指针
#if _WIN64 // 如果是 _WIN64 平台
long long length; // length 为长整型,表示长度
#else // 否则,为长整型
long length; // length 表示长度
#endif // endif
};
// __NSConstantStringImpl__var_folders_zr_p1cnc5b14b7bfgk7vs7_x6fh0000gp_T_main_35ff71_mi_1 常量字符串的定义
static __NSConstantStringImpl __NSConstantStringImpl__var_folders_zr_p1cnc5b14b7bfgk7vs7_x6fh0000gp_T_main_35ff71_mi_1 __attribute__((section("__DATA, __cfstring"))) = {
__CFConstantStringClassReference, // isa 为 __CFConstantStringClassReference
0x000007c8, // flags 为固定值 0x000007c8
"%@", // str 为 "%@" 占位符的字符串指针
2 // length 为固定值 2,表示占位符字符串的长度
};
// __NSConstantStringImpl__var_folders_zr_p1cnc5b14b7bfgk7vs7_x6fh0000gp_T_main_35ff71_mi_3 常量字符串的定义
static __NSConstantStringImpl __NSConstantStringImpl__var_folders_zr_p1cnc5b14b7bfgk7vs7_x6fh0000gp_T_main_35ff71_mi_3 __attribute__((section("__DATA, __cfstring"))) = {
__CFConstantStringClassReference, // isa 为 __CFConstantStringClassReference
0x000007c8, // flags 为固定值 0x000007c8
"%d", // str 为 "%d" 占位符的字符串指针
2 // length 为固定值 2,表示占位符字符串的长度
};
```
从这个结构体的结构可以看出,_mi_1和_mi_3就是赋值后的__NSConstantStringImpl结构体。占位符放在了str字段中。不过这样我们还是看不出来占位符的原理是怎样的。占位符被放在__NSConstantStringImpl通过objc_msgSend函数发送进了NSString类中。
我们知道%@的占位符一般都是用来对OC对象进行占位,在获取OC对象的具体值时我们一般都是通过引用来操作,而引用的本质是指针。
我们实现下面的代码并运行。
```objective-c
NSString *str = [NSString stringWithFormat:@"%@",1];
```
这句代码摘编译过程中只会报警告,但是可以通过编译,但是在运行时却会崩溃,崩溃信息是Thread 1: EXC_BAD_ACCESS (code=1, address=0x1)。
这个崩溃信息很常见,是野指针造成的崩溃。
我们现在可以做个猜想,%@占位时会把传入的变量当做指针,去指针对应的位置获取实际对象,这里传入了1而存储空间中地址为1的位置中可能是没有对象的,此时就会造成野指针崩溃。
为了验证这个猜想我们再实现下面的代码。
```objective-c
NSString *str1 = @"1"; NSInteger pointer = (NSInteger)str1; NSString *str2 = [NSString stringWithFormat:@"%@",pointer]; NSLog(@"pointrt:%ld",pointer); NSLog(@"str2:%@",str2);
```
打印结果
```
2020-08-07 11:04:44.496693+0800 BlockPrinciple[24761:111933] pointrt:4476567608 2020-08-07 11:04:44.498160+0800 BlockPrinciple[24761:111933] str2:1
```
在这段代码中,我们把str1的地址强转成了NSInteger类型的变量pointer,在构造str2时将pointer和%@占位符结合使用,最后取到了str1的值“1”,这说明我们之前的猜想应该没有问题。
总结
尽管我们目前还不能确定NSString内部如何区分使用%@和%ld作为占位符的差异(我们猜测是基于字符串匹配),但是,可以肯定的是,在%@占位符的使用情况下,stringWithFormat方法会将对应的参数当作地址,并去这个地址获取相应的对象。如果你对此话题感兴趣,欢迎关注我们的公众号,并留言进行讨论。