iOS 性能优化(内存布局&内存管理策略)

1,097 阅读15分钟

内存布局

内存布局中有七个区,分别是内核区堆区栈区未初始化数据(静态区)已初始化数据(常量区)代码段保留区

  • 栈区:创建临时变量时由编译器自动分配,在不需要的时候自动清除的变量的存储区。里面的变量通常是局部变量、函数参数等。在一个进程中,位于用户虚拟地址空间顶部的是用户栈,编译器用它来实现函数的调用。和堆一样,用户栈在程序执行期间可以动态地扩展和收缩。
  • 堆区:那些由 new alloc 创建的对象所分配的内存块,它们的释放系统不会主动去管,由我们的开发者去告诉系统什么时候释放这块内存(一个对象引用计数为0是系统就会回销毁该内存区域对象)。一般一个 new 就要对应一个release。在ARC下编译器会自动在合适位置为OC对象添加release操作。会在当前线程Runloop退出或休眠时销毁这些对象,MRC则需程序员手动释放。堆可以动态地扩展和收缩。
  • 未初始化数据(静态区):程序运行过程内存的数据一直存在,程序结束后由系统释放
  • 已初始化数据(常量区):专门用于存放常量,程序结束后由系统释放
  • 代码段:用于存放程序运行时的代码,代码会被编译成二进制存进内存的程序代码区
  • 内核区:用于加载内核代码,预留1GB
  • 保留区:内存有4MB保留,地址从低到高递增

下面用代码来探索一下日常使用的数据存放在哪个区

  • 栈区指针,以及一些简单的基本数据类型存储在栈区。一般来说,地址为0x7开头的一般都是在栈区
#import "ViewController.h"
@interface ViewController ()
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"************栈区************");
    // 栈区
    int a = 10;
    int b = 20;
    NSObject *object = [NSObject new];
    NSLog(@"a == \t%p",&a);
    NSLog(@"b == \t%p",&b);
    NSLog(@"object == \t%p",&object);
    NSLog(@"%lu",sizeof(&object));
    NSLog(@"%lu",sizeof(a));
}
@end

打印:

2020-02-13 16:33:11.605082+0800 001---五大区Demo[1545:519291] ************栈区************
2020-02-13 16:33:11.605169+0800 001---五大区Demo[1545:519291] a == 	0x7ffee0cd87fc
2020-02-13 16:33:11.605236+0800 001---五大区Demo[1545:519291] b == 	0x7ffee0cd87f8
2020-02-13 16:33:11.605298+0800 001---五大区Demo[1545:519291] object == 	0x7ffee0cd87f0
2020-02-13 16:33:11.605357+0800 001---五大区Demo[1545:519291] 8
2020-02-13 16:33:11.605411+0800 001---五大区Demo[1545:519291] 4
  • 堆区堆区一般用来储存new出来的对象,一般来说,堆区的地址一般为0x6开头
#import "ViewController.h"
@interface ViewController ()
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"************堆区************");
    // 堆区
    NSObject *object1 = [NSObject new];
    NSObject *object2 = [NSObject new];
    NSObject *object3 = [NSObject new];
    NSObject *object4 = [NSObject new];
    NSObject *object5 = [NSObject new];
    NSObject *object6 = [NSObject new];
    NSObject *object7 = [NSObject new];
    NSObject *object8 = [NSObject new];
    NSObject *object9 = [NSObject new];
    NSLog(@"object1 = %@",object1);
    NSLog(@"object2 = %@",object2);
    NSLog(@"object3 = %@",object3);
    NSLog(@"object4 = %@",object4);
    NSLog(@"object5 = %@",object5);
    NSLog(@"object6 = %@",object6);
    NSLog(@"object7 = %@",object7);
    NSLog(@"object8 = %@",object8);
    NSLog(@"object9 = %@",object9);
}
@end

打印:

2020-02-13 16:33:11.605468+0800 001---五大区Demo[1545:519291] ************堆区************
2020-02-13 16:33:11.605531+0800 001---五大区Demo[1545:519291] object1 = <NSObject: 0x600003854440>
2020-02-13 16:33:11.605601+0800 001---五大区Demo[1545:519291] object2 = <NSObject: 0x600003854460>
2020-02-13 16:33:11.605655+0800 001---五大区Demo[1545:519291] object3 = <NSObject: 0x600003854450>
2020-02-13 16:33:11.605720+0800 001---五大区Demo[1545:519291] object4 = <NSObject: 0x600003854470>
2020-02-13 16:33:11.605776+0800 001---五大区Demo[1545:519291] object5 = <NSObject: 0x600003854480>
2020-02-13 16:33:11.605840+0800 001---五大区Demo[1545:519291] object6 = <NSObject: 0x600003854490>
2020-02-13 16:33:11.605902+0800 001---五大区Demo[1545:519291] object7 = <NSObject: 0x6000038544a0>
2020-02-13 16:33:11.605965+0800 001---五大区Demo[1545:519291] object8 = <NSObject: 0x6000038544b0>
2020-02-13 16:33:11.606028+0800 001---五大区Demo[1545:519291] object9 = <NSObject: 0x6000038544c0>
  • 静态区:这里会存储一些定义出来但是未初始化的对象,一般来说,0x1开头的数据一般为常量区静态区
#import "ViewController.h"
@interface ViewController ()

@end

@implementation ViewController

int clA;
static int bssA;
static NSString *bssStr1;

- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"************静态区************");
    NSLog(@"clA == \t%p",&clA);
    NSLog(@"bssA == \t%p",&bssA);
    NSLog(@"bssStr1 == \t%p",&bssStr1);
   
}
@end

打印:

2020-02-13 16:33:11.623869+0800 001---五大区Demo[1545:519291] ************静态区************
2020-02-13 16:33:11.623950+0800 001---五大区Demo[1545:519291] clA == 	0x10ef2729c
2020-02-13 16:33:11.624016+0800 001---五大区Demo[1545:519291] bssA == 	0x10ef272a0
2020-02-13 16:33:11.624067+0800 001---五大区Demo[1545:519291] bssStr1 == 	0x10ef272a8
  • 常量区:会存储一些定义出来且已经初始化的数据,一般来说,0x1开头的数据一般为常量区静态区
#import "ViewController.h"

@interface ViewController ()

@end

@implementation ViewController

int clB = 10;
static int bssB = 10;
static NSString *bssStr2 = @"noah";

- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"************常量区************");
    NSLog(@"clB == \t%p",&clB);
    NSLog(@"bssB == \t%p",&bssB);
    NSLog(@"bssStr2 == \t%p",&bssStr2);
   
}
@end

打印:

2020-02-13 16:33:11.624130+0800 001---五大区Demo[1545:519291] ************常量区************
2020-02-13 16:33:11.624192+0800 001---五大区Demo[1545:519291] clB == 	0x10ef271c0
2020-02-13 16:33:11.624244+0800 001---五大区Demo[1545:519291] bssB == 	0x10ef271d0
2020-02-13 16:33:11.624300+0800 001---五大区Demo[1545:519291] bssStr2 == 	0x10ef271c8

内存管理策略

TaggedPointer

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

  • Tagged Pointer是苹果为了解决32位CPU到64位CPU的转变带来的内存占用和效率问题,针对NSNumber、NSDate以及部分NSString的内存优化方案。
  • Tagged Pointer指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要malloc和free
  • Tagged Pointer指针中包含了当前对象的地址、类型、具体数值。因此Tagged Pointer指针在内存读取上有着3倍的效率,创建时比普通需要mallocfree的类型快106倍
  • 如果想深入了解TaggedPointer请点击这里

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的异或操作,这样进行了两次异或操作就还原了初始值。

使用代码来验证taggedpointer类型

#import "ViewController.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    int     num1 = 10;
    float   num2 = 12;
    double  num3 = 14;
    long    num4 = 5;
    
    NSNumber * number1 = @(num1);
    NSNumber * number2 = @(num2);
    NSNumber * number3 = @(num3);
    NSNumber * number4 = @(num4);
    
    NSLog(@"number1 = %@ - %p - 0x%lx",number1,&number1,_objc_decodeTaggedPointer((__bridge const void * _Nullable)(number1)));
    NSLog(@"number2 = %@ - %p - 0x%lx",number2,&number2,_objc_decodeTaggedPointer((__bridge const void * _Nullable)(number2)));
    NSLog(@"number3 = %@ - %p - 0x%lx",number3,&number3,_objc_decodeTaggedPointer((__bridge const void * _Nullable)(number3)));
    NSLog(@"number4 = %@ - %p - 0x%lx",number4,&number4,_objc_decodeTaggedPointer((__bridge const void * _Nullable)(number4)));
}

extern uintptr_t objc_debug_taggedpointer_obfuscator;

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

@end

打印

2020-02-13 19:35:33.310386+0800 004-taggedPointer[3472:589831] number1 = 10 - 0x7ffee4bcc7e0 - 0xb0000000000000a2
2020-02-13 19:35:33.310471+0800 004-taggedPointer[3472:589831] number2 = 12 - 0x7ffee4bcc7d8 - 0xb0000000000000c4
2020-02-13 19:35:33.310529+0800 004-taggedPointer[3472:589831] number3 = 14 - 0x7ffee4bcc7d0 - 0xb0000000000000e5
2020-02-13 19:35:33.310578+0800 004-taggedPointer[3472:589831] number4 = 5 - 0x7ffee4bcc7c8 - 0xb000000000000053

以number1为例,经过_objc_decodeTaggedPointer解码出来的值是0xb0000000000000a2,可以看到倒数第二位是值,倒数第一位是类型,可以得出最后一位 2、4、5、3分别代表int long float double类型

再来看看字符串类型

//
//  ViewController.m
//  004-taggedPointer
//
//  Created by cooci on 2019/4/8.
//  Copyright © 2019 cooci. All rights reserved.
//

#import "ViewController.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    NSString * str1 = [NSString stringWithFormat:@"a"];
    NSString * str2 = [NSString stringWithFormat:@"ab"];
    NSString * str3 = [NSString stringWithFormat:@"abc"];
    NSString * str4 = [NSString stringWithFormat:@"abcd"];
    
    NSLog(@"str1 = %@ - %p - 0x%lx",str1,&str1,_objc_decodeTaggedPointer((__bridge const void * _Nullable)(str1)));
    NSLog(@"str1 = %@ - %p - 0x%lx",str2,&str2,_objc_decodeTaggedPointer((__bridge const void * _Nullable)(str2)));
    NSLog(@"str1 = %@ - %p - 0x%lx",str3,&str3,_objc_decodeTaggedPointer((__bridge const void * _Nullable)(str3)));
    NSLog(@"str1 = %@ - %p - 0x%lx",str4,&str4,_objc_decodeTaggedPointer((__bridge const void * _Nullable)(str4)));
}

extern uintptr_t objc_debug_taggedpointer_obfuscator;

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

@end

打印:

2020-02-13 19:42:22.152170+0800 004-taggedPointer[3539:593334] str1 = a - 0x7ffee3bf27c0 - 0xa000000000000611
2020-02-13 19:42:22.152257+0800 004-taggedPointer[3539:593334] str1 = ab - 0x7ffee3bf27b8 - 0xa000000000062612
2020-02-13 19:42:22.152322+0800 004-taggedPointer[3539:593334] str1 = abc - 0x7ffee3bf27b0 - 0xa000000006362613
2020-02-13 19:42:22.152386+0800 004-taggedPointer[3539:593334] str1 = abcd - 0x7ffee3bf27a8 - 0xa000000646362614

字符串类型解压出来的值,最后一位代表的是字符串长度,而61、62、63、64对应的是ASCII的a、b、c、d

NONPOINTER_ISA

苹果将 isa 设计成了联合体,在 isa 中存储了与该对象相关的一些内存的信息,原因也如上面所说,并不需要 64 个二进制位全部都用来存储指针。来看一下 isa 的结构:

// x86_64 架构
struct {
    uintptr_t nonpointer        : 1;  // 0:普通指针,1:优化过,使用位域存储更多信息
    uintptr_t has_assoc         : 1;  // 对象是否含有或曾经含有关联引用
    uintptr_t has_cxx_dtor      : 1;  // 表示是否有C++析构函数或OC的dealloc
    uintptr_t shiftcls          : 44; // 存放着 Class、Meta-Class 对象的内存地址信息
    uintptr_t magic             : 6;  // 用于在调试时分辨对象是否未完成初始化
    uintptr_t weakly_referenced : 1;  // 是否被弱引用指向
    uintptr_t deallocating      : 1;  // 对象是否正在释放
    uintptr_t has_sidetable_rc  : 1;  // 是否需要使用 sidetable 来存储引用计数
    uintptr_t extra_rc          : 8;  // 引用计数能够用 8 个二进制位存储时,直接存储在这里
};

// arm64 架构
struct {
    uintptr_t nonpointer        : 1;  // 0:普通指针,1:优化过,使用位域存储更多信息
    uintptr_t has_assoc         : 1;  // 对象是否含有或曾经含有关联引用
    uintptr_t has_cxx_dtor      : 1;  // 表示是否有C++析构函数或OC的dealloc
    uintptr_t shiftcls          : 33; // 存放着 Class、Meta-Class 对象的内存地址信息
    uintptr_t magic             : 6;  // 用于在调试时分辨对象是否未完成初始化
    uintptr_t weakly_referenced : 1;  // 是否被弱引用指向
    uintptr_t deallocating      : 1;  // 对象是否正在释放
    uintptr_t has_sidetable_rc  : 1;  // 是否需要使用 sidetable 来存储引用计数
    uintptr_t extra_rc          : 19;  // 引用计数能够用 19 个二进制位存储时,直接存储在这里
};

注意这里的 has_sidetable_rcextra_rchas_sidetable_rc 表明该指针是否引用了 sidetable 散列表,之所以有这个选项,是因为少量的引用计数是不会直接存放在 SideTables 表中的,对象的引用计数会先存放在 extra_rc 中,当其被存满时,才会存入相应的 SideTables 散列表中,SideTables 中有很多张 SideTable,每个 SideTable 也都是一个散列表,而引用计数表就包含在 SideTable 之中。

SideTables 散列表

散列表(Hash table,也叫哈希表),是根据建(Key)而直接访问在内存存储位置的数据结构。也就是说,它通过一个关于键值得函数,将所需查询的数据映射到表中一个位置来访问记录,这加快了查找速度。这个映射函数称作散列函数,存放记录的数组称作散列表。

来看一下 NSObject.mm 中它们对应的源码:

// SideTables
static StripedMap<SideTable>& SideTables() {
    return *reinterpret_cast<StripedMap<SideTable>*>(SideTableBuf);
}

// SideTable
struct SideTable {
    spinlock_t slock;           // 自旋锁
    RefcountMap refcnts;        // 引用计数表
    weak_table_t weak_table;    // 弱引用表
    
    // other code ...
};

它们的关系如下:

看下获取一个sidetable的源码:

// 获取一个sidetable
table = &SideTables()[obj];

// SideTables
static StripedMap<SideTable>& SideTables() {
    return *reinterpret_cast<StripedMap<SideTable>*>(SideTableBuf);
}
// StripedMap
template<typename T>
// 这里的模板规范了一个sidetable的样式
class StripedMap {
#if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
    //如果是真机,StripeCount=8
    enum { StripeCount = 8 };
#else
    //模拟器就是StripeCount=64
    enum { StripeCount = 64 };
#endif

    struct PaddedT {
        T value alignas(CacheLineSize);
    };

    PaddedT array[StripeCount];
    // 散列算法
    static unsigned int indexForPointer(const void *p) {
        uintptr_t addr = reinterpret_cast<uintptr_t>(p);
        // 这个算法保证算出来的值是小于StripeCount的,这样就不会出现数据越界的情况
        return ((addr >> 4) ^ (addr >> 9)) % StripeCount;
    }

 public:
    // 重写操作符
    T& operator[] (const void *p) { 
        // 这里返回一个sidetable
        return array[indexForPointer(p)].value; 
    }
    const T& operator[] (const void *p) const { 
        return const_cast<StripedMap<T>>(this)[p]; 
    }

    ...
};

分离锁

分离锁并不是一种锁,而是一种对锁的用法。各个元素分别加一把锁就是我们说的分离锁。

问:为什么不用SideTables 直接包含自旋锁,引用计数表和弱引用表呢?

答:
这是因为在众多线程同时访问这个 SideTable 表的时候,为了保证数据安全,需要给其加上自旋锁,如果只有一张 SideTable 的表,那么所有数据访问都会出一个进一个,单线程进行,非常影响效率,虽然自旋锁已经是效率非常高的锁,这会带来非常不好的用户体验。针对这种情况,将一张 SideTable 分为多张表的 SideTables,再各自加锁保证数据的安全,这样就增加了并发量,提高了数据访问的效率,这就是为什么一个 SideTables 下涵盖众多 SideTable 表的原因。

自旋锁:计算机科学用于多线程同步的一种锁,线程会反复检查锁变量是否可用。由于线程在这一过程中保持执行(没有进入休眠),因此是一种忙等。一旦获取了自旋锁,线程会一直保持该锁,直至显式释放自旋锁。

自旋锁适用于小型数据耗时很少的操作,速度很快

引用计数表(RefcountMap refcnts

SideTable中包含一个 c++ Map RefcountMap refcnts 用来对象存储额外的引用计数,一个结构体weak_table_t weak_table 用来存储对象的弱引用数据

RefcountMap refcnts 中通过一个size_t(64位系统中占用64位)来保存引用计数,其中1位用来存储固定标志位,在溢出的时候使用,一位表示正在释放中,一位表示是否有弱引用,其余位表示实际的引用计数

  • RefcountMap refcnts 是一个C++的对象,内部包含了一个迭代器
  • 其中以DisguisedPtr<objc_object> 对象指针为key,size_t 为value保存对象引用计数
  • 将key、value通过std::pair打包以后,放入迭代器中,所以取出值之后,.first代表key,.second代表value

弱引用表

全局弱引用表 weak_table_t

struct weak_table_t {
    weak_entry_t *weak_entries;
    size_t    num_entries;
    uintptr_t mask;
    uintptr_t max_hash_displacement;
};

弱引用表的内部结构

struct weak_entry_t {
    DisguisedPtr<objc_object> referent;
    union {
        struct {
            weak_referrer_t *referrers;
            uintptr_t        out_of_line_ness : 2;
            uintptr_t        num_refs : PTR_MINUS_2;
            uintptr_t        mask;
            uintptr_t        max_hash_displacement;
        };
        struct {
            // out_of_line_ness field is low bits of inline_referrers[1]
            weak_referrer_t  inline_referrers[WEAK_INLINE_COUNT];
        };
    };
};
  • 其中主要包含了两个属性DisguisedPtr<objc_object> referent对象指针,还有一个容器类保存所以只需这个对象的弱引用
  • 共用体中包含两种结构体,当弱引用数量少于4的时候,使用数据结构来存储,当超过4个的时候使用hash表进行存储,out_of_line_ness 默认为 ob00,当弱引用数量大于4的时候,设置为 REFERRERS_OUT_OF_LINE ob10,通过判断out_of_line_ness来决定用什么方式存储
  • weak_referrer_t *referrers 是一个二级指针实现的hash表

参考文章

iOS概念攻坚之路(三):内存管理
iOS weak 的实现