本篇提纲
1、内存五大区
2、TaggedPointer
3、Retain和Release对于小对象类型的处理
1.内存五大区
堆区(Heap)
堆区在运行时分配,一般由程序员分配和释放,若程序员不释放,程序结束后可能由OS回收,分配方式类似于链表,存储方式是不连续的,它的效率不如栈。
iOS中堆区的地址一般是以0x6开头。
OC中,使用alloc或者new来创建对象,在ARC下由系统回收释放。
C语言中,使用malloc、calloc、realloc开辟的空间,需要手动调用free函数进行释放。
堆区的内存空间较大,可以存储整个结构体以及成员变量。栈区(Stack)
栈是系统数据结构,栈所对应的进程或者线程是唯一的。
栈的地址由高到底,是一块连续的内存区域,遵循先进后出原则。
在iOS中,一般以0x7开头,在寄存器sp中。
栈区由系统负责分配和销毁,效率高,速度快。
栈区中存储局部变量和方法/函数的参数
iOS主线程栈的大小为1MB,其他线程为512k。全局静态区( .bss和 .data)
全局区在编译时分配内存空间,在iOS中一般以0x1开头。
全局区的数据一直存在,程序结束后由系统释放
全局区存储全局变量和static修饰的静态变量,其中静态变量分为全局静态变量和静态局部变量。
未初始化的变量存储在BSS区(.bss),已经初始化的存储在数据区(.data)常量区(.rodata)
常量区在编译时分配。
常量区的数据会一直存在,程序结束后由系统释放。代码段(
.text)
代码区在编译时分配。
代码区存储程序运行时的代码,会被编译成二进制存储进内存。
2.TaggedPointer
2.1 TaggedPointer的引入
从2013年苹果推出了iPhone5s之后,iOS的内存寻址空间扩大到了64位。我们可以用63位表示一个数字。那么这个数字的范围是2^63,是一个很大的数字,一般情况我们用不到这么大的数字,所以使用63位来存储一个数字对于空间是非常浪费的。苹果为了优化这一个问题,引入了Tagged Pointer。
Tagged Pointer是一种特殊的指针,它特殊在于它存储的不是地址,而是真实的数据和一些附加的信息。
Tagged Pointer专门用来存储小对象,例如:NSNumber,NSDate,NSString。
Tagged Pointer指针的值不再是地址了,而是真正的值,所以不是一个对象,它的内存不是在堆中,不需要malloc和free。在内存读取上有着3倍的效率,创建时比以前快106倍。
2.2 TaggedPointer的例子

通过打印我们可以看到,打印出来三个类型的字符串:NSTaggedPointerString,__NSCFConstantString,__NSCFString。我查了下相关资料关于这三种字符串的区别。
NSTaggedPointerString
小对象类型,存储在栈区,通过方法NSStringFromClass创建的,字符串长度不超过10个的时候,字符串的类型为NSTaggedPointerString。__NSCFConstantString
常量字符串,存储在常量区,通过方法initWithString创建,或者直接创建,此时类型是__NSCFConstantString。__NSCFString
可变字符串,存储在堆区,通过方法[NSMutableString alloc]initWithString或者NSStringFromClass长度超过10的字符串为__NSCFString类型。
2.3 TaggedPointer源码相关
通过objc4_818.2的源码中可以看到关于TaggedPointer的一些方法。
- TaggedPointer的编码
通过查找TaggedPointer相关,可以找到关于操作TaggedPointer的一系列方法。
_objc_encodeTaggedPointer
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;
}
首先看到value是通过objc_debug_taggedpointer_obfuscator和ptr进行异或得到,而objc_debug_taggedpointer_obfuscator的值,在支持TaggedPointer的情况下是通过方法initializeTaggedPointerObfuscator进行初始化的。
Initialize objc_debug_taggedpointer_obfuscator with randomness.
/***********************************************************************
* initializeTaggedPointerObfuscator
* Initialize objc_debug_taggedpointer_obfuscator with randomness.
*
* The tagged pointer obfuscator is intended to make it more difficult
* for an attacker to construct a particular object as a tagged pointer,
* in the presence of a buffer overflow or other write control over some
* memory. The obfuscator is XORed with the tagged pointers when setting
* or retrieving payload values. They are filled with randomness on first
* use.
**********************************************************************/
static void
initializeTaggedPointerObfuscator(void)
{
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));
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;
}
}
这个方法通过DisableTaggedPointerObfuscation来判断是否开启了混淆。
开启了objc_debug_taggedpointer_obfuscator的值是一个随机数;未开启直接赋值为0。
所以,方法_objc_encodeTaggedPointer的编码就是当前的值和随机数进行一个异或操作。我们再来看看解码操作。
- TaggedPointer的解码
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;
}
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;
}
方法_objc_decodeTaggedPointer通过调用_objc_decodeTaggedPointer_noPermute方法,而_objc_decodeTaggedPointer_noPermute的实现就是,通过这个objc_debug_taggedpointer_obfuscator和当前的value进行再异或就完成了解码。这里我们以32位机器来复习下异或运算:
- A异或0xffff ffff = A的取反
- A异或0x0000 0000 = A
- A异或B再异或一次B = A
所以上面这个TaggedPointer类型的编码和解码,都是通过异或运算来完成的。编码时c = A异或objc_debug_taggedpointer_obfuscator;
解码时 A = c异或objc_debug_taggedpointer_obfuscator;
我们也可以通过他这个解码的规律在程序里进行解码操作试试解出TaggedPointer中存储的内容。
2.4 TaggedPointer解码实例操作
首先在代码中自定义一个解码的方法:
extern uintptr_t objc_debug_taggedpointer_obfuscator;
uintptr_t kc_objc_decodeTaggedPointer(id ptr)
{
return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
}
然后分别去解码NSString,NSNumber,NSIndexPath,NSDate。
- 2.4.1解码NSString
- (void)indexPathDemo{
NSString *str = [NSString stringWithFormat:@"Luck"];
NSLog(@"%p-%@-%@ - 0x%lx",str,str,str.class,kc_objc_decodeTaggedPointer(str));
}
输出结果:
0xa0000006b63754c4-Luck-NSTaggedPointerString - 0xa0000006b63754c4
使用模拟器运行,解码的结果为0xa0000006b63754c4我们通过lldb,将其转化为二进制:

首先
0b表示二进制。然后b后面的第一位为
1,表示该isa为Tagged Pointer类型。第二到四位标记了TaggedPointer的指针类型,
010,表示字符串类型,下面附上类型的定义。解码的最后四位表示长度是
100也就是长度为4,后面可以验证下,逐一加长看最后四位的变化。那么剩下的就表示真实的内容。

三位的范围是
000 ~ 111也就是0~7,能表示对应的这八种类型。接下来我们来解析下实质的内容把
Luck从TaggedPointer解出来。
内容解析再看二进制就比较费劲了,因为数字比较大,所以我们还是来看十六进制这串0xa0000006b63754c4。
前边已经说过第一位是表示是不是小对象类型,这个首位的a转成二进制是1010,也就对应了那个小对象类型和字符串,而最末尾的4表示长度,剩下的有效内容就是6b 63 75 4c转成十进制就是107 99 117 76

再来对应下相应的
ASCII码表,分别查出这几个对应的ASCII值,107->k,99->c,117->u,76->L。因为存储方式是小端模式,所以高位存到低地址,反读就是Luck,就拿到了我们存储的内容。我们按照这个方式再来验证一下。
例子:
如果我们这一次去存字符串ABCDE,那么我们通过上面的推导进行反推导打印出来的结果。
首先ABCDE对应的ASCII分别是65 66 67 68 69转成16进制为41 42 43 44 45;
小端模式存储反着存为 45 44 43 42 41;
长度是5,末尾加个5变成45 44 43 42 41 5;
类型是小对象字符串,所以前四位也是1010十六进制就是a;
所以最终打印出来的结果是0xa000045444342415这样的!
我们运行一下来验证下这个规律。

此处微微一笑!也想像KC一样问问还有谁(实在是太洗脑了这个台词😂😂😂)!!

-
解码NSNumber
首先关闭数据混淆。
模拟器
可以看到直接就是存的值1,末尾表示类型:
0 -> char
1 -> short
2 -> int
3 -> long
4 -> float
5-> double
而真机运行情况会有所变化:

最后三位变成了类型标记小对象类型
011对应的是3也就是NSNumber。第四位到第七位表示的是具体的类型同上
0000,0001,0010,0011,0100,0101对应也是0~5这几个类型。
3.Retain和Release对于小对象类型的处理
直接看源码关于Retain和Release对于小对象类型的处理。
-
Retain
找到方法rootRetain
处理小对象类型
可以看到小对象类型不进行引用计数的内存管理操作。 -
Release
小对象类型的Release
同样对应的小对象类型不进行release的操作!
总结
本篇文章主要着重介绍到了小对象类型Tagged Pointer的一些概念和相关的理解,它的作用,带来的优化等等。下一篇继续针对内存管理进行更加深入的相关知识SideTable的学习。


