一、内存布局
1.1 内存分区
- 栈区:存储函数、方法、指针、局部变量、参数等(访问速度快,通过寄存器访问),当局部变量的作用域被执行完毕之后,这个局部变量就会被系统立即回收。内存地址一般以
0x7
开头。 - 堆区:开辟内存空间(
new
、alloc
、copy
、malloc
、calloc
、realloc
),一般放对象。内存地址一般以0x6
开头。 - 全局区:分为 BSS段 与 DATA段。内存地址一般以
0x1
开头。- BSS段:存储未初始化的全局变量,静态变量。一旦初始化就回收,并转存到数据段中。
- DATA段:存放已初始化的全局变量,静态变量。直到程序结束才会被回收。
- 代码段(text):存储程序代码,加载到内存中。直到程序结束才会被回收。
- 内核区:系统内核使用(
1GB
),比如GCD
开辟线程所用的空间。0xc0000000
也就是对应3GB
。 - 保留:
0x00400000
(4MB
)。
栈区的内存是通过SP寄存器
去定位的。堆区是通过寄存器中地址去定位的。
1.1.1 验证
栈区验证
// 栈区
int a = 10;
int b = 20;
//栈区分配指针,指向堆区开辟的内存空间。
NSObject *obj = [NSObject new];
NSObject *obj2 = obj;
NSLog(@"a address:%p",&a);
NSLog(@"b address:%p",&b);
NSLog(@"obj pointer address:%p",&obj);
NSLog(@"obj address:%p",obj);
NSLog(@"obj2 pointer address:%p",&obj2);
NSLog(@"obj2 address:%p",obj2);
输出:
a address:0x7ffee63e90bc
b address:0x7ffee63e90b8
obj pointer address:0x7ffee63e90b0
obj address:0x600002860060
obj2 pointer address:0x7ffee63e90a8
obj2 address:0x600002860060
a
与b
都是临时变量,分配在栈区以0x7
开头。
obj
以及obj2
分配了两个指针在栈区,指向堆区的同一块内存。
堆区验证
NSObject *object1 = [NSObject new];
NSObject *object2 = [NSObject new];
NSObject *object3 = [NSObject new];
NSObject *object4 = [NSObject new];
NSLog(@"object1 = %@",object1);
NSLog(@"object2 = %@",object2);
NSLog(@"object3 = %@",object3);
NSLog(@"object4 = %@",object4);
输出:
object1 =
object2 =
object3 =
object4 =
栈区内存地址以0x6
开头。寄存器通过分配的栈区指针访问指向堆区的地址。
全局区验证
int A;
int B = 10;
static int bssA;
static NSString *bssStr1;
static int dataB = 10;
static NSString *dataStr2 = @"HP";
static NSString *dataStr3 = @"Cat";
- (void)testConst {
//bss
NSLog(@"A: %p",&A);
NSLog(@"bssA: %p",&bssA);
NSLog(@"bssStr1: %p",&bssStr1);
//data
NSLog(@"B: %p",&B);
NSLog(@"dataB: %p",&dataB);
NSLog(@"dataStr2: %p",&dataStr2);
NSLog(@"dataStr3: %p",&dataStr3);
}
输出:
A: 0x10ebb36b0
bssA: 0x10ebb36b8
bssStr1: 0x10ebb36c0
B: 0x10ebb3510
dataB: 0x10ebb3528
dataStr2: 0x10ebb3518
dataStr3: 0x10ebb3520
全局区内存以0x1
开始,已初始化地址小于未初始化地址。
全局区安全性问题
有如下代码:
HPObject
static int num = 100;
@interface HPObject : NSObject
- (void)test;
+ (void)test2;
@end
@implementation HPObject
- (void)test {
num++;
NSLog(@"HPObject 内部 test num:%@-%p--%d",self,&num,num);
}
+ (void)test2 {
num++;
NSLog(@"HPObject 内部 test2 num:%@-%p--%d",self,&num,num);
}
@end
HPObject + HP
分类
@implementation HPObject (HP)
- (void)test3 {
num++;
NSLog(@"HPObject 分类内部 test3 num:%@-%p--%d",self,&num,num);
}
@end
调用(ViewController.m
):
NSLog(@"vc:%p--%d",&num,num); // 100
num = 10000;
NSLog(@"vc:%p--%d",&num,num); // 10000
[[HPObject new] test]; // 100 + 1 = 101
NSLog(@"vc:%p--%d",&num,num); // 10000
[HPObject test2]; // 102
NSLog(@"vc:%p--%d",&num,num); // 10000
[[HPObject alloc] test3];
NSLog(@"vc:%p--%d",&num,num); // 10000
输出:
vc:0x104e68688--100
vc:0x104e68688--10000
HPObject 内部 test num:-0x104e68690--101
vc:0x104e68688--10000
HPObject 内部 test2 num:HPObject-0x104e68690--102
vc:0x104e68688--10000
HPObject 分类内部 test3 num:-0x104e68758--101
vc:0x104e68688--10000
可以看到num
虽然可以修改但是是以文件为单位的,只是文件内有效,不同的文件对应的num
地址不同。
静态全局变量针对文件有效。
将num
的定义修为:
//.h
extern int num;
//.m
int num = 100;
输出:
vc:0x10965e688--100
vc:0x10965e688--10000
HPObject 内部 test num:-0x10965e688--10001
vc:0x10965e688--10001
HPObject 内部 test2 num:HPObject-0x10965e688--10002
vc:0x10965e688--10002
HPObject 分类内部 test3 num:-0x10965e688--10003
vc:0x10965e688--10003
这个时候修改全局有效,num
所有文件中是同一个地址。extern全局有效。
1.2 内存管理方案
-
MRC
&ARC
。 - 垃圾回收(
Garbage Collection
):目前已经不支持了。 -
TaggedPointer
:小对象(NSNumber
、NSDate
)。 -
NONPOINTER_ISA
:非指针型isa
。 - 散列表:引用计数表、弱引用表。
静态全局变量针对文件有效,extern全局有效。
二、TaggedPointer
Tagged Pointer
是一种特殊标记的对象,通过在其最后一个 bit
位设置为特殊标记位,并将数据直接保存在指针自身中。
在 64
位系统中,有 64
位空间可以表示一个对象指针。由于内存对齐,通常没有真正使用到所有这些位。对象必须位于指针大小倍数的地址中,低位和高位均被 0
填充,因此只用到了中间部分的位,出现了大量的内存浪费。
x86
:
arm64
:
-
Tagged Pointer 标记
:x86
最后一位是标记位,arm64
最高位是标记位。1
表示是Tagged Pointer
对象,0
表示是普通对象。 -
Tag
:对象类型标记。x86
为1~3
位,arm64
为0~2
。7
表示有扩展信息。 -
Extended
:x86
为4~11
位,arm64
为54~62
。用来扩展更多类型。 -
payload
:有效负载。存储真正的数据(除了标记位、tag
以及extended
),不过为了安全苹果做了编码。
小结:
-
Tagged Pointer
(指针标记)专⻔用来存储小的对象,例如NSNumber
和NSDate
(针对64
位指针)。 -
Tagged Pointer
指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,是一个披着对象皮的普通变量而已。内存并不存储在堆中,也不需要malloc
和free
。 - 在内存读取上有着
3
倍的效率,创建与释放比以前快106
倍。
arm64
使用高位存储标记位,为了objc_msgSend
的优化。在最高位能一次排查Tagged Pointer
指针和nil
两种类型,节省了一个case
的逻辑。
2.1 TaggedPointer 结构
NSString *str = [NSString stringWithFormat:@"hp"];
NSLog(@"%p - %@ - %@",str,str,str.class);
NSString *str1 = @"hp";
NSLog(@"%p - %@ - %@",str1,str1,str1.class);
NSString *str2 = [[NSString alloc] initWithString:@"hp"];
NSLog(@"%p - %@ - %@",str2,str2,str2.class);
NSString *str3 = [str copy];
NSLog(@"%p - %@ - %@",str3,str3,str3.class);
NSString *str4 = [str1 copy];
NSLog(@"%p - %@ - %@",str4,str4,str4.class);
NSString *str5 = [[NSString alloc] initWithString:str];
NSLog(@"%p - %@ - %@",str5,str5,str5.class);
NSString *str6 = [[NSString alloc] initWithFormat:@"hp"];
NSLog(@"%p - %@ - %@",str6,str6,str6.class);
NSString *str7 = [[NSString alloc] initWithCString:"hp" encoding:NSASCIIStringEncoding];
NSLog(@"%p - %@ - %@",str7,str7,str7.class);
NSString *str8 = [[NSString alloc] initWithUTF8String:"hp"];
NSLog(@"%p - %@ - %@",str8,str8,str8.class);
输出:
0xf67b106ca2ef7750 - hp - NSTaggedPointerString
0x10b1e4020 - hp - __NSCFConstantString
0x10b1e4020 - hp - __NSCFConstantString
0xf67b106ca2ef7750 - hp - NSTaggedPointerString
0x10b1e4020 - hp - __NSCFConstantString
0xf67b106ca2ef7750 - hp - NSTaggedPointerString
0xf67b106ca2ef7750 - hp - NSTaggedPointerString
0xf67b106ca2ef7750 - hp - NSTaggedPointerString
0xf67b106ca2ef7750 - hp - NSTaggedPointerString
-
stringWithFormat
以及char * 字符串
创建的字符串是NSTaggedPointerString
类型。 -
copy
创建的字符串类型与它拷贝的目标字符串有关。 - 字面量创建的字符串为
__NSCFConstantString
类型,其它创建字符串与传递的字符串类型有关。
很明显NSTaggedPointerString
类型的字符串地址是0xf67b106ca2ef7750
与前面分析的内存地址完全不同。它是一个TaggedPointer
类型的指针。
在objc
源码中taggedpointer
有如下注释:
可以看到要得到
payload
需要解码后进行位移操作。
decode
与encode
对应如下:
static inline uintptr_t
_objc_decodeTaggedPointer(const void * _Nullable ptr)
{
uintptr_t value = _objc_decodeTaggedPointer_noPermute(ptr);
#if OBJC_SPLIT_TAGGED_POINTERS
uintptr_t basicTag = (value >> _OBJC_TAG_INDEX_SHIFT) & _OBJC_TAG_INDEX_MASK;
value &= ~(_OBJC_TAG_INDEX_MASK << _OBJC_TAG_INDEX_SHIFT);
value |= _objc_obfuscatedTagToBasicTag(basicTag) << _OBJC_TAG_INDEX_SHIFT;
#endif
return value;
}
static inline void * _Nonnull
_objc_encodeTaggedPointer(uintptr_t ptr)
{
uintptr_t value = (objc_debug_taggedpointer_obfuscator ^ ptr);
#if OBJC_SPLIT_TAGGED_POINTERS
if ((value & _OBJC_TAG_NO_OBFUSCATION_MASK) == _OBJC_TAG_NO_OBFUSCATION_MASK)
return (void *)ptr;
uintptr_t basicTag = (value >> _OBJC_TAG_INDEX_SHIFT) & _OBJC_TAG_INDEX_MASK;
uintptr_t permutedTag = _objc_basicTagToObfuscatedTag(basicTag);
value &= ~(_OBJC_TAG_INDEX_MASK << _OBJC_TAG_INDEX_SHIFT);
value |= permutedTag << _OBJC_TAG_INDEX_SHIFT;
#endif
return (void *)value;
}
_objc_decodeTaggedPointer
调用了_objc_decodeTaggedPointer_noPermute
:
static inline uintptr_t
_objc_decodeTaggedPointer_noPermute(const void * _Nullable ptr)
{
uintptr_t value = (uintptr_t)ptr;
#if OBJC_SPLIT_TAGGED_POINTERS
//没有混淆
if ((value & _OBJC_TAG_NO_OBFUSCATION_MASK) == _OBJC_TAG_NO_OBFUSCATION_MASK)
return value;
#endif
//有混淆
return value ^ objc_debug_taggedpointer_obfuscator;
}
objc_debug_taggedpointer_obfuscator
定义如下:
extern uintptr_t objc_debug_taggedpointer_obfuscator;
它是在initializeTaggedPointerObfuscator
中设置的:
static void
initializeTaggedPointerObfuscator(void)
{
// if (!DisableTaggedPointerObfuscation && dyld_program_sdk_at_least(dyld_fall_2018_os_versions))
//允许混淆
if (!DisableTaggedPointerObfuscation) {
// Pull random data into the variable, then shift away all non-payload bits.
//随机数
arc4random_buf(&objc_debug_taggedpointer_obfuscator,
sizeof(objc_debug_taggedpointer_obfuscator));
//获取tag。
objc_debug_taggedpointer_obfuscator &= ~_OBJC_TAG_MASK;
#if OBJC_SPLIT_TAGGED_POINTERS
// The obfuscator doesn't apply to any of the extended tag mask or the no-obfuscation bit.
objc_debug_taggedpointer_obfuscator &= ~(_OBJC_TAG_EXT_MASK | _OBJC_TAG_NO_OBFUSCATION_MASK);
// Shuffle the first seven entries of the tag permutator.
int max = 7;
for (int i = max - 1; i >= 0; i--) {
int target = arc4random_uniform(i + 1);
swap(objc_debug_tag60_permutations[i],
objc_debug_tag60_permutations[target]);
}
#endif
} else {//不允许混淆
// Set the obfuscator to zero for apps linked against older SDKs,
// in case they're relying on the tagged pointer representation.
objc_debug_taggedpointer_obfuscator = 0;
}
}
-
objc_debug_taggedpointer_obfuscator
是一个随机数。 -
DisableTaggedPointerObfuscation
可以通过配置OBJC_DISABLE_TAG_OBFUSCATION
来控制是否开启。 -
initializeTaggedPointerObfuscator
是在类的加载_read_images
的时候调用的。
那么可以模仿decode
的逻辑,实现自己的解码函数(这里不是真机,真机还有额外处理):
//声明
extern uintptr_t objc_debug_taggedpointer_obfuscator;
//模仿解码
uintptr_t hp_objc_decodeTaggedpointer(id ptr) {
return (uintptr_t)ptr^objc_debug_taggedpointer_obfuscator;
}
由于objc_debug_taggedpointer_obfuscator
是全局静态变量,所以可以直接extern
声明就好了。
当然更简单的方法是配置
OBJC_DISABLE_TAG_OBFUSCATION
为YES
关闭混淆。
payload
获取_objc_getTaggedPointerValue
如下:
//获取payload
static inline uintptr_t
_objc_getTaggedPointerValue(const void * _Nullable ptr)
{
// ASSERT(_objc_isTaggedPointer(ptr));
//解码
uintptr_t value = _objc_decodeTaggedPointer_noPermute(ptr);
//_OBJC_TAG_INDEX_SHIFT 为 0/1/60,_OBJC_TAG_INDEX_MASK 为 7
// (value >> 0/60) & 0x7。arm64 为 0/60 iOS14后为0,之前为60 ,x86为1
uintptr_t basicTag = (value >> _OBJC_TAG_INDEX_SHIFT) & _OBJC_TAG_INDEX_MASK;
// == 7,也就是有extend
if (basicTag == _OBJC_TAG_INDEX_MASK) {
//(value << 0/9/12) >> 12。 arm64 iOS14 9,之前为12,x86 为 0。由于都左移了,所以还原需要 >> 12(加起来是12)
return (value << _OBJC_TAG_EXT_PAYLOAD_LSHIFT) >> _OBJC_TAG_EXT_PAYLOAD_RSHIFT;
} else {
// (value << 0/1/4) >> 4。arm64 iOS14 1,之前 4。 x86 为 0。需要右移 >> 4还原。
return (value << _OBJC_TAG_PAYLOAD_LSHIFT) >> _OBJC_TAG_PAYLOAD_RSHIFT;
}
}
通过位移实现解码后的数据还原,为了方便只模拟iOS14
和mac
的逻辑:
static inline uintptr_t hp_objc_getTaggedPointerValue(id ptr) {
uintptr_t value = hp_objc_decodeTaggedpointer(ptr);
uintptr_t basicTag = 0x0;
#if TARGET_OS_IPHONE
basicTag = value & 0x7;
#else
basicTag = (value >> 1) & 0x7;
#endif
//有extend
if (basicTag == 0x7) {
#if TARGET_OS_IPHONE
return (value << 9) >> 12;
#else
return value >> 12;
#endif
} else {
#if TARGET_OS_IPHONE
return (value << 1) >> 4;
#else
return value >> 4;
#endif
}
}
2.1.1 x86 TaggedPointer 结构 (Mac)
NSString *str = [NSString stringWithFormat:@"hp"];
NSLog(@"%p - %@ - %@ - decode value:0x%lx, payload:0x%lx",str,str,str.class,hp_objc_decodeTaggedpointer(str),hp_objc_getTaggedPointerValue(str));
输出:
0x495f9cbcde9767b - hp - NSTaggedPointerString - decode value:0x706825, payload:0x70682
解码后的值为0x706825
,payload
为0x70682
。
payload
解析如下:
同样的对于其它类型:
NSNumber *number = @6;
NSLog(@"%p - %@ - %@ - decode value:0x%lx, payload:0x%lx",number,number,number.class,hp_objc_decodeTaggedpointer(number),hp_objc_getTaggedPointerValue(number));
NSDate *date = [NSDate date];
NSLog(@"%p - %@ - %@ - decode value:0x%lx, payload:0x%lx",date,date,date.class,hp_objc_decodeTaggedpointer(date),hp_objc_getTaggedPointerValue(date));
NSIndexPath *indexPath = [NSIndexPath indexPathWithIndex:3];
NSLog(@"%p - %@ - %@ - decode value:0x%lx, payload:0x%lx",indexPath,indexPath,indexPath.class,hp_objc_decodeTaggedpointer(indexPath),hp_objc_getTaggedPointerValue(indexPath));
输出:
0x9da3a6c50fc90087 - 6 - __NSCFNumber - decode value:0x627, payload:0x62
0xb094e947cc26e7fd - Thu Sep 9 15:33:44 2021 - __NSTaggedDate - decode value:0x2d374f82c3efe15d, payload:0x2d374f82c3efe15
0x9da3a6c50fc93649 - {length = 1, path = 3} - NSIndexPath - decode value:0x30e9, payload:0x30e
2.1.2 arm64 TaggedPointer 结构(真机)
由于真机上面解码有一些额外操作,直接配置OBJC_DISABLE_TAG_OBFUSCATION
更方便。
NSString *str = [NSString stringWithFormat:@"hp"];
NSLog(@"%p - %@ - %@ - decode value:0x%lx, payload:0x%lx",str,str,str.class,hp_objc_decodeTaggedpointer(str),hp_objc_getTaggedPointerValue(str));
输出:
0x967c561218f83f2f - hp - NSTaggedPointerString - decode value:0xa000000000070682, payload:0x40000000000e0d0
解码后的值为0xa000000000070682
,payload
为0x40000000000e0d0
。
payload
解析如下:
那么除了存储
hp
外还存储了额外信息。
同样的对于其它类型:
NSNumber *number = @6;
NSLog(@"%p - %@ - %@ - decode value:0x%lx, payload:0x%lx",number,number,number.class,hp_objc_decodeTaggedpointer(number),hp_objc_getTaggedPointerValue(number));
NSDate *date = [NSDate date];
NSLog(@"%p - %@ - %@ - decode value:0x%lx, payload:0x%lx",date,date,date.class,hp_objc_decodeTaggedpointer(date),hp_objc_getTaggedPointerValue(date));
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:1 inSection:2];
NSLog(@"%p - %@ - %@ - decode value:0x%lx, payload:0x%lx",indexPath,indexPath,indexPath.class,hp_objc_decodeTaggedpointer(indexPath),hp_objc_getTaggedPointerValue(indexPath));
输出:
0x8dbe59f3baa78095 - 6 - __NSCFNumber - decode value:0x8000000000000315, payload:0x62
0x9b25fe4764c5d200 - Thu Sep 9 15:20:23 2021 - __NSTaggedDate - decode value:0x969ba7b4de625180, payload:0x2d374f69bcc4a30
0x8dbe59f3bba79332 - {length = 2, path = 2 - 1} - NSIndexPath - decode value:0x80000000010010b2, payload:0x200216
2.1.3 存储额外信息解析
上面分析payload
的时候除了存储存储内容本身外,还有额外信息。
对于字符串:
NSString *str = [NSString stringWithFormat:@"h"];
NSLog(@"%p - %@ - %@ - decode value:0x%lx, payload:0x%lx",str,str,str.class,hp_objc_decodeTaggedpointer(str),hp_objc_getTaggedPointerValue(str));
NSString *str1 = [NSString stringWithFormat:@"hp"];
NSLog(@"%p - %@ - %@ - decode value:0x%lx, payload:0x%lx",str1,str1,str1.class,hp_objc_decodeTaggedpointer(str1),hp_objc_getTaggedPointerValue(str1));
NSString *str2 = [NSString stringWithFormat:@"HotpotCat"];
NSLog(@"%p - %@ - %@ - decode value:0x%lx, payload:0x%lx",str2,str2,str2.class,hp_objc_decodeTaggedpointer(str2),hp_objc_getTaggedPointerValue(str2));
字符串除了存储字符串本身外,还存储了字符串长度,占用
4
位。
NSTaggedPointerString
的存储有三种编码方式:ASCII码
,六位编码
,五位编码
。
-
ASCII
码:除去第一位和最后一位,用8
位的ascll
码的话最多可以存储7
个字符。字符串数目0~7
之间。 - 六位编码: 六位二进制编码,
(144)/6=9.333…
;最多存储9
位字符。字符数目在8~9
使用。 - 五位编码:五位二进制编码,
(144)/5 = 11.2
;最多存储11
位字符。字符数目在10~11
使用。
当然编码也与字符类型有关,具体对应关系如下:
对于NSNumber:
NSNumber *number = @6;
NSLog(@"%p - %@ - %@ - decode value:0x%lx, payload:0x%lx",number,number,number.class,hp_objc_decodeTaggedpointer(number),hp_objc_getTaggedPointerValue(number));
NSNumber *number1 = @(6.0);
NSLog(@"%p - %@ - %@ - decode value:0x%lx, payload:0x%lx",number1,number1,number1.class,hp_objc_decodeTaggedpointer(number1),hp_objc_getTaggedPointerValue(number1));
0
代表char
,1
代表short
,2
代表int
,3
代表long
,4
代表float
,5
代表double
。
2.2 案例
@property (nonatomic, strong) NSString *nameStr;
- (void)taggedPointerTest {
dispatch_queue_t queue = dispatch_queue_create("com.hotpot.cn", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i < 10000; i++) {
dispatch_async(queue, ^{
self.nameStr = [NSString stringWithFormat:@"hotpot"];
NSLog(@"%@",self.nameStr);
});
}
}
- (void)taggedPointerTest1 {
dispatch_queue_t queue = dispatch_queue_create("com.hotpot1.cn", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i < 10000; i++) {
dispatch_async(queue, ^{
//多线程的读和写,对新值的retain,旧值的release。有可能在这个过程中访问了release的值,操作了野指针。
self.nameStr = [NSString stringWithFormat:@"hotpothotpothotpothotpothotpothotpot"];
NSLog(@"%@",self.nameStr);
});
}
}
taggedPointerTest
的调用不会发生crash
,taggedPointerTest1
有可能发生crash
。
taggedPointerTest1
发生crash
的原因是发生了多线程读和写,在setter
过程中有对新值的retain
,旧值的release
。在这个过程中getter
有可能访问了release
的值,操作了野指针。
那么taggedPointerTest
为什么不会发生crash
呢?
因为在taggedPointerTest
的过程中self.nameStr
是NSTaggedPointerString
类型并不会发生retain
和release
(直接返回)。
TaggedPointer 总结:
TaggedPointer
专⻔用来存储小的对象,指针的值不再是地址了而是真正的值。实际上不再是一个对象,是一个披着对象皮的普通变量。- 内存并不存储在堆中,也不需要
malloc
和free
(retain
与release
直接返回)。 - 在内存读取上有着
3
倍的效率,创建与释放比以前快106
倍。 - 为了安全进行了混淆,值存储和读取需要相应的编码和解码。
arm64
真机上面TaggedPointer
标记在最高位63
,tag
在0~2
,extended
在54~62
位,其余位置存储真正的值payload
。x86_64
TaggedPointer
标记在最低位0
,tag
在1~3
,extended
在4~11
,其余位置存储真正的值payload
。-
payload
并不仅仅只存储真正的值,还存储额外信息(4
位)。对于字符串存储了字符串长度。对于NSNumber
存储了类型。0
:char
、1
:short
、1
:int
、3
:long
、4
:float
、5
:double
。- 对于字符串通过
stringWithFormat
以及char *
(包含拷贝以及传值)创建的字符串是NSTaggedPointerString
类型。字面量方式创建的不是。 - 是否是
NSTaggedPointerString
也与长度和编码方式有关。