美文网首页
33.iOS底层学习之内存管理TaggedPointer

33.iOS底层学习之内存管理TaggedPointer

作者: 牛牛大王奥利给 | 来源:发表于2022-02-23 18:25 被阅读0次

本篇提纲
1、内存五大区
2、TaggedPointer
3、Retain和Release对于小对象类型的处理

1.内存五大区

  • 堆区(Heap)
    堆区在运行时分配,一般由程序员分配和释放,若程序员不释放,程序结束后可能由OS回收,分配方式类似于链表,存储方式是不连续的,它的效率不如栈。
    iOS中堆区的地址一般是以0x6开头。
    OC中,使用alloc或者new来创建对象,在ARC下由系统回收释放。
    C语言中,使用malloccallocrealloc开辟的空间,需要手动调用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专门用来存储小对象,例如:NSNumberNSDateNSString

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_obfuscatorptr进行异或得到,而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;
}

然后分别去解码NSStringNSNumberNSIndexPathNSDate

  • 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,表示该isaTagged Pointer类型。
第二到四位标记了TaggedPointer的指针类型,010,表示字符串类型,下面附上类型的定义。
解码的最后四位表示长度是100也就是长度为4,后面可以验证下,逐一加长看最后四位的变化。
那么剩下的就表示真实的内容。 TaggedPointer的指针类型
三位的范围是000 ~ 111也就是0~7,能表示对应的这八种类型。
接下来我们来解析下实质的内容把LuckTaggedPointer解出来。

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

ASCII码表
再来对应下相应的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这样的!
我们运行一下来验证下这个规律。

ABCDE结果

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

微微一笑
  • 解码NSNumber
    首先关闭数据混淆。


    模拟器

可以看到直接就是存的值1,末尾表示类型:
0 -> char
1 -> short
2 -> int
3 -> long
4 -> float
5-> double

而真机运行情况会有所变化:

真机情况
最后三位变成了类型标记小对象类型011对应的是3也就是NSNumber
第四位到第七位表示的是具体的类型同上000000010010001101000101对应也是0~5这几个类型。

3.Retain和Release对于小对象类型的处理

直接看源码关于RetainRelease对于小对象类型的处理。

  • Retain
    找到方法rootRetain

    处理小对象类型
    可以看到小对象类型不进行引用计数的内存管理操作。
  • Release


    小对象类型的Release

    同样对应的小对象类型不进行release的操作!

总结

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

相关文章

网友评论

      本文标题:33.iOS底层学习之内存管理TaggedPointer

      本文链接:https://www.haomeiwen.com/subject/oehygltx.html