iOS 性能優化(內存佈局&內存管理策略)

內存佈局

內存佈局中有七個區,分別是內核區堆區棧區未初始化數據(靜態區)已初始化數據(常量區)代碼段保留區c++

  • 棧區:建立臨時變量時由編譯器自動分配,在不須要的時候自動清除的變量的存儲區。裏面的變量一般是局部變量、函數參數等。在一個進程中,位於用戶虛擬地址空間頂部的是用戶棧,編譯器用它來實現函數的調用。和堆同樣,用戶棧在程序執行期間能夠動態地擴展和收縮。
  • 堆區:那些由 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
複製代碼

打印:bash

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,能夠看到倒數第二位是值,倒數第一位是類型,能夠得出最後一位 二、四、五、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
複製代碼

字符串類型解壓出來的值,最後一位表明的是字符串長度,而6一、6二、6三、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 的實現

相關文章
相關標籤/搜索