iOS性能优化(内存分布与TaggedPointer)

915 阅读5分钟

iOS中的五大内存区域

iOS中的内存区域从低地址到高地址分别为 .text段(代码区)、.data段(已初始化的全局变量、静态变量)、.bss段(未初始化的全局变量、静态变量)、堆区、栈区。 image.png

保留段:用于给系统提供一些必要的空间; 内核区:由系统使用;

这里说明一点:栈区从上往下走,堆区会从下往上走,当两个相遇的时候,则会发生堆栈溢出。

    // 一般0x1开头的是 常量 静态  0x7开头的在栈   0x6开头的在堆

    NSLog(@"%d - %p",bssA,&bssA); // 0x10efae020 .bss段
    
    NSLog(@"%d - %p",bssB,&bssB); // 0x10efadf50 .data段
    
    int a = 10;
    NSLog(@"%p",&a); // 栈 -- 0x7ffee0c51a6c 栈
    
    NSObject *obj = [NSObject new]; // 对象 --
    NSLog(@"%@ - %p",obj,&obj); // 0x60000270c170> - 0x7ffee0c51a60 对象在堆上 指针在栈上
    
    NSArray *array = [[NSArray alloc] init];
    NSLog(@"%@-%p",array,&array);   //()-0x7ffee0c51a58 

普通对象查找过程: 先从栈中找到指针,然后去堆中寻找指针对应的内存空间,进而读取到值。在64位机器上,苹果引进了TaggedPointer的概念。

#TaggedPointer 为什么要使用taggedPointer 假设要存储一个NSNumber对象,其值是一个整数。正常情况下,如果这个整数只是一个NSInteger的普通变量,在64位CPU下是占8个字节的。1个字节有8位,如果我们存储一个很小的值,会出现很多位都是0的情况,这样就造成了内存浪费,苹果为了解决这个问题,引入了taggedPointer的概念。

计算机位运算

image.png

    //用位运算交换两个数的值
    int a = 2;
    int b = 3;
    exchange(a, b);
}

void exchange(int a,int b ) {   //a = 0000 0010  b = 0000 0011
    a = a^b;                    //a = 0000 0001
    b = a^b;                    //b = 0000 0010   可以看到 a^b^b = a
    a = a^b;                    //a = 0000 0011
    printf("a=%d b=%d",a,b);
}

有了上面的基础,我们来看一下taggedPointer的源码

static inline void * _Nonnull
_objc_makeTaggedPointer(objc_tag_index_t tag, uintptr_t value)
{
    if (tag <= OBJC_TAG_Last60BitPayload) {
        uintptr_t result =
            (_OBJC_TAG_MASK | 
             ((uintptr_t)tag << _OBJC_TAG_INDEX_SHIFT) | 
             ((value << _OBJC_TAG_PAYLOAD_RSHIFT) >> _OBJC_TAG_PAYLOAD_LSHIFT));
        return _objc_encodeTaggedPointer(result);
    } else {
        uintptr_t result =
            (_OBJC_TAG_EXT_MASK |
             ((uintptr_t)(tag - OBJC_TAG_First52BitPayload) << _OBJC_TAG_EXT_INDEX_SHIFT) |
             ((value << _OBJC_TAG_EXT_PAYLOAD_RSHIFT) >> _OBJC_TAG_EXT_PAYLOAD_LSHIFT));
        return _objc_encodeTaggedPointer(result);
    }
}
static inline void * _Nonnull
_objc_encodeTaggedPointer(uintptr_t ptr)
{
    return (void *)(objc_debug_taggedpointer_obfuscator ^ ptr);
}

static inline uintptr_t
_objc_decodeTaggedPointer(const void * _Nullable ptr)
{
    return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
}

可以看到,系统对taggedPointer进行了 _objc_encodeTaggedPointer 编码,该编码的实现就是对value进行了 objc_debug_taggedpointer_obfuscator 的异或操作,而在读取taggedPointer的时候,通过 _objc_decodeTaggedPointer 进行解码,还是进行了objc_debug_taggedpointer_obfuscator的异或操作,这样进行了两次异或操作就还原了初始值。

下面我们通过代码来看一下:

    NSNumber * num1 = [NSNumber numberWithInt:100];
    NSNumber * num2 = [NSNumber numberWithInt:200];
    
    NSLog(@"num1 = %@ - %p",num1,&num1);        //num1 = 100 - 0x7ffee0bf5a68
    NSLog(@"num2 = %@ - %p",num2,&num2);        //num2 = 200 - 0x7ffee0bf5a60

根据打印出来的信息,我们并不能分析出有什么特殊的地方,我们来到taggetpointer的init方法

static void
initializeTaggedPointerObfuscator(void)
{
    if (sdkIsOlderThan(10_14, 12_0, 12_0, 5_0, 3_0) ||
        // Set the obfuscator to zero for apps linked against older SDKs,
        // in case they're relying on the tagged pointer representation.
        DisableTaggedPointerObfuscation) {
        objc_debug_taggedpointer_obfuscator = 0;
    } else {
        // 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;
    }
}

可以看到,在Mac10.14和iOS12之前,对taggedpointer做异或的objc_debug_taggedpointer_obfuscator值为0,之后为 objc_debug_taggedpointer_obfuscator &= ~_OBJC_TAG_MASK这么一步操作,那么我们如何知道系统做了什么呢? 我们来自己实现taggetpointer的decode,来查看系统在decode之后的数据是怎样的: image.png 然后写一段测试代码:

    int     num1 = 15;
    float   num2 = 11;
    double  num3 = 10;
    long    num4 = 8;

    NSNumber * number1 = @(num1);
    NSNumber * number2 = @(num2);
    NSNumber * number3 = @(num3);
    NSNumber * number4 = @(num4);
    
    NSLog(@"number1 = %@ - %@ - %p - 0x%lx",object_getClass(number1),number1,&number1,_objc_decodeTaggedPointer((number1)));
    NSLog(@"number2 = %@ - %@ - %p - 0x%lx",object_getClass(number2),number2,&number2,_objc_decodeTaggedPointer((number2)));
    NSLog(@"number3 = %@ - %@ - %p - 0x%lx",object_getClass(number3),number3,&number3,_objc_decodeTaggedPointer((number3)));
    NSLog(@"number4 = %@ - %@ - %p - 0x%lx",object_getClass(number4),number4,&number4,_objc_decodeTaggedPointer((number4)));

    number1 = __NSCFNumber - 15 - 0x7ffee3683a50 - 0xb0000000000000f2
    number2 = __NSCFNumber - 11 - 0x7ffee3683a48 - 0xb0000000000000b4
    number3 = __NSCFNumber - 10 - 0x7ffee3683a40 - 0xb0000000000000a5
    number4 = __NSCFNumber - 8 - 0x7ffee3683a38 - 0xb000000000000083

可以看到,解码之后的真实的数据并不单单是表示值,以number1为例,0xb0000000000000f2,他的真实的值其实是第二位的f(f转10进制就是15),同理,number2,number3,number4都是如此,最后一位 2、4、5、3分别代表int long float double类型

再来看看string

    NSString * str1 = [NSString stringWithFormat:@"a"];
    NSString * str2 = [NSString stringWithFormat:@"bb"];
    NSString * str3 = [NSString stringWithFormat:@"ccc"];
    NSString * str4 = [NSString stringWithFormat:@"dddd"];
    NSLog(@"str1 = %@ - %p - 0x%lx",object_getClass(str1),str1,_objc_decodeTaggedPointer(str1));
    NSLog(@"str2 = %@ - %p - 0x%lx",object_getClass(str2),str2,_objc_decodeTaggedPointer(str2));
    NSLog(@"str3 = %@ - %p - 0x%lx",object_getClass(str3),str3,_objc_decodeTaggedPointer(str3));
    NSLog(@"str4 = %@ - %p - 0x%lx",object_getClass(str4),str4,_objc_decodeTaggedPointer(str4));

    str1 = NSTaggedPointerString - 0xf5ec647546bfddec - 0xa000000000000611
    str2 = NSTaggedPointerString - 0xf5ec647546b9fddf - 0xa000000000062622
    str3 = NSTaggedPointerString - 0xf5ec64754089edce - 0xa000000006363633
    str4 = NSTaggedPointerString - 0xf5ec647300f99db9 - 0xa000000646464644

最后一位表示长度,61、6262、636363、64646464分别对应ASCII的a,bb,ccc,dddd。

TaggedPointer极大的提高了内存利用率和简化了查询步骤。它不单单是一个指针,还包括了其值+类型,节省了对象的查询流程。