本篇提纲
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的操作!
总结
本篇文章主要着重介绍到了小对象类型Tagged Pointer
的一些概念和相关的理解,它的作用,带来的优化等等。下一篇继续针对内存管理进行更加深入的相关知识SideTable
的学习。