我们将通过以下步骤实现几个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方法会将对应的参数当作地址,并去这个地址获取相应的对象。如果你对此话题感兴趣,欢迎关注我们的公众号,并留言进行讨论。