iOS底層學習 - 內存管理以內存管理方案

不一樣的系統版本對 App 運行時佔用內存的限制不一樣,超過限制時,App就會被強制殺死,因此對於內存的要求也就愈來愈高,因此本章來探索一下iOS中的內存管理方案c++

移動端的內存管理技術,主要有 GC(Garbage Collection,垃圾回收)的標記清除算法和蘋果公司使用的引用計數方法程序員

相比較於 GC 標記清除算法,引用計數法能夠及時地回收引用計數爲 0 的對象,減小查找次數。可是,引用計數會帶來循環引用的問題,好比當外部的變量強引用 Block 時,Block 也會強引用外部的變量,就會出現循環引用。咱們須要經過弱引用,來解除循環引用的問題。面試

另外,在 ARC(自動引用計數)以前,一直都是經過 MRC(手動引用計數)這種手寫大量內存管理代碼的方式來管理內存,所以蘋果公司開發了 ARC 技術,由編譯器來完成這部分代碼管理工做。可是,ARC 依然須要注意循環引用的問題。當 ARC 的內存管理代碼交由編譯器自動添加後,有些狀況下會比手動管理內存效率低,因此對於一些內存要求較高的場景,咱們仍是要經過 MRC 的方式來管理、優化內存的使用。算法

內存佈局

首先咱們再來回顧一下,OS/iOS系統的內存佈局c#

在iOS程序的內存中,從底地址開始,到高地址一次分爲:程序區域、數據區域、堆區、棧區。其中程序區域主要是代碼段,數據區域包括數據段和BSS段。咱們具體分析一下各個區域所表明的含義數組

  • 棧區(stack):編譯器自動分配,由系統管理,在不須要的時候自動清除。局部變量、函數參數存儲在這裏。棧區的內存地址通常是0x7開頭,從高地址到底地址分配內存空間
  • 堆區 (heap): 那些由newallocblockcopy建立的對象存儲在這裏,是由開發者管理的,須要告訴系統何時釋放內存。ARC下編譯器會自動在合適的時候釋放內存,而在MRC下須要開發者手動釋放。堆區的內存地址通常是0x6開頭,從底地址到高地址分配內存空間
  • _DATA區:主要分爲BSS(靜態區)和數據段(常量區)
    • BSS(靜態區):BSS段又稱靜態區,未初始化的全局變量,靜態變量存放在這裏。一旦初始化就會被回收,而且將數據轉存到數據段中。
    • 數據段(常量區):數據段又稱常量區,專門存放常量,直到程序結束的時候纔會被回收。
  • 代碼段:用於存放程序運行時的代碼,代碼會被編譯成二進制存進內存的程序代碼區。程序結束時系統會自動回收存儲在代碼段中的數據。

小問題:static靜態變量的做用域

首先先查看下面代碼,age的打印結果是多少呢?bash

static int age = 10;

@interface Person : NSObject
-(void)add;
+(void)reduce;
@end

@implementation Person

- (void)add {
    age++;
    NSLog(@"Person內部:%@-%p--%d", self, &age, age);
}

+ (void)reduce {
    age--;
    NSLog(@"Person內部:%@-%p--%d", self, &age, age);
}
@end


@implementation Person (WY)

- (void)wy_add {
    age++;
    NSLog(@"Person (wy)內部:%@-%p--%d", self, &age, age);
}

@end

- (void)viewDidLoad {
    [super viewDidLoad];

    NSLog(@"vc:%p--%d", &age, age);
    age = 40;
    NSLog(@"vc:%p--%d", &age, age);
    [[Person new] add];
    NSLog(@"vc:%p--%d", &age, age);
    [Person reduce];
    NSLog(@"vc:%p--%d", &age, age);
    [[Person new] wy_add];
}
複製代碼

經過打印結果,咱們能夠得出這麼一個結論:數據結構

靜態變量的做用域與對象、類、分類不要緊,只與文件有關係。架構

內存優化方案

iOS除了使用ARC來進行自動引用計數之外,還有一些其餘的內存優化方案,主要有Tagged Ponter,NONPOINTER_ISA,SideTable3種async

Tagged Ponter

概述

在 2013 年 9 月,蘋果推出了 iPhone5s,與此同時,iPhone5s 配備了首個採用 64 位架構的 A7 雙核處理器,爲了節省內存和提升執行效率,蘋果提出了Tagged Pointer的概念。

使用Tagged Pointer以前,若是聲明一個NSNumber *number = @10;變量,須要一個佔8字節的指針變量number,和一個佔16字節的NSNumber對象,指針變量number指向NSNumber對象的地址。這樣須要耗費24個字節內存空間。

而使用Tagged Pointer以後,NSNumber指針裏面存儲的數據變成了:Tag + Data,也就是將數據直接存儲在了指針中。直接將數據10保存在指針變量number中,這樣僅佔用8個字節。

可是當指針不夠存儲數據時,就會使用動態分配內存的方式來存儲數據。

經過下面面試題的打印結果能夠很好的反映出來

//第1段代碼
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    for (int i = 0; i < 1000; i++) {
        dispatch_async(queue, ^{
            self.name = [NSString stringWithFormat:@"asdasdefafdfa"];
        });
    }
    NSLog(@"end");
複製代碼
//第2段代碼
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    for (int i = 0; i < 1000; i++) {
        dispatch_async(queue, ^{
            self.name = [NSString stringWithFormat:@"abc"];
        });
    }
    NSLog(@"end");
複製代碼

上面兩段代碼的打印結果過是: 第1段會發生崩潰,而第2段不會。

按道理講,建立多個線程來對name進行操做時,name的值會不斷的retainrelease,此時就會出現資源競爭而崩潰,可是第二段卻不會崩潰,說明在Tagged Pointer下,較小的值,不會調用set和get等方法,因爲其值是直接存儲在指針變量中的,因此能夠直接修改。

經過源碼,咱們也能夠比較直觀的看出,Tagged Pointer類型對象是直接返回的。

總結一下使用Tagged Pointer的好處

  • Tagged Pointer是專⻔⽤來存儲⼩的對象,例如NSNumberNSDate等。
  • Tagged Pointer指針的值再也不是地址了,⽽是真正的值。因此,實際上它再也不是⼀個對象了,它只是⼀個披着對象⽪的普通變量⽽已。因此,它的內存並不存儲在堆中,也不須要mallocfree
  • 當指針不夠存儲數據時,就會使用動態分配內存的方式來存儲數據。
  • 在內存讀取上有着3倍的效率,建立時⽐之前快106倍。

源碼

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;
}

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);
    }
}

複製代碼

從上面的這代碼能夠看出來,系統調用了_objc_decodeTaggedPointer_objc_taggedPointersEnabled這兩個方法對於taggedPointer對象的指針進行了編碼和解編碼,這兩個方法都是將指針地址和objc_debug_taggedpointer_obfuscator進行異或操做

咱們都知道將a和b異或操做獲得c再和a進行異或操做即可以從新獲得a的值,一般可使用這個方式來實現不用中間變量實現兩個值的交換。Tagged Pointer正是使用了這種原理。經過這種解碼的方法,咱們能夠獲得對象真正的指針地址

下面是系統定義的各類Tagged Pointer標誌位。

enum objc_tag_index_t : uint16_t
{
    // 60-bit payloads
    OBJC_TAG_NSAtom            = 0, 
    OBJC_TAG_1                 = 1, 
    OBJC_TAG_NSString          = 2, 
    OBJC_TAG_NSNumber          = 3, 
    OBJC_TAG_NSIndexPath       = 4, 
    OBJC_TAG_NSManagedObjectID = 5, 
    OBJC_TAG_NSDate            = 6,

    // 60-bit reserved
    OBJC_TAG_RESERVED_7        = 7, 

    // 52-bit payloads
    OBJC_TAG_Photos_1          = 8,
    OBJC_TAG_Photos_2          = 9,
    OBJC_TAG_Photos_3          = 10,
    OBJC_TAG_Photos_4          = 11,
    OBJC_TAG_XPC_1             = 12,
    OBJC_TAG_XPC_2             = 13,
    OBJC_TAG_XPC_3             = 14,
    OBJC_TAG_XPC_4             = 15,

    OBJC_TAG_First60BitPayload = 0, 
    OBJC_TAG_Last60BitPayload  = 6, 
    OBJC_TAG_First52BitPayload = 8, 
    OBJC_TAG_Last52BitPayload  = 263, 

    OBJC_TAG_RESERVED_264      = 264
};

複製代碼

NONPOINTER_ISA

關於NONPOINTER_ISA咱們在以前的博客中有講過,這也是蘋果的一種內存優化的方案。用 64 bit 存儲一個內存地址顯然是種浪費,畢竟不多有那麼大內存的設備。因而能夠優化存儲方案,用一部分額外空間存儲其餘內容。isa 指針第一位爲 1 即表示使用優化的 isa 指針。能夠閱讀☞iOS底層學習 - OC對象前世此生

union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;
#if defined(ISA_BITFIELD)
    struct {
      uintptr_t nonpointer        : 1;                                         
      uintptr_t has_assoc         : 1;                                         
      uintptr_t has_cxx_dtor      : 1;                                         
      uintptr_t shiftcls          : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ 
      uintptr_t magic             : 6;                                         
      uintptr_t weakly_referenced : 1;                                         
      uintptr_t deallocating      : 1;                                         
      uintptr_t has_sidetable_rc  : 1;                                         
      uintptr_t extra_rc          : 8
    };
#endif
};

複製代碼

SideTable

NONPOINTER_ISA中有兩個成員變量has_sidetable_rcextra_rc,當extra_rc的19位內存不夠存儲引用計數時has_sidetable_rc的值就會變爲1,那麼此時引用計數會存儲在SideTable中。

SideTables能夠理解爲一個全局的hash數組,裏面存儲了SideTable類型的數據,其長度爲64,就是裏面有64個SideTable。

struct SideTable {
    spinlock_t slock;
    RefcountMap refcnts;
    weak_table_t weak_table;
}
複製代碼

查看SideTable的源碼,其3個成員變量,表明的意義以下:

  • spinlock_t:自旋鎖,用於上鎖/解鎖 SideTable。
  • RefcountMap:用來存儲OC對象的引用計數的 hash表(僅在未開啓isa優化或在isa優化狀況下isa_t的引用計數溢出時纔會用到)。
  • weak_table_t:存儲對象弱引用指針的hash表。是OC中weak功能實現的核心數據結構。

有關於弱引用表weak_table_t,能夠閱讀☞iOS底層學習 - 內存管理之weak原理探究

// RefcountMap disguises its pointers because we 
// do not want the table to act as a root for `leaks`.
typedef objc::DenseMap<DisguisedPtr<objc_object>,size_t,RefcountMapValuePurgeable> RefcountMap;
複製代碼

實質上是模板類型objc::DenseMap。模板的三個類型參數DisguisedPtr<objc_object>size_ttrue 分別表示DenseMap的 key類型value類型、是否須要在value == 0 的時候自動釋放掉響應的hash節點,這裏是true。

MRC與ARC

MRC與ARC概念

MRC

在MRC時代,程序員須要手動的去管理內存,建立一個對象時,須要在set方法和get方法內部添加釋放對象的代碼。而且在對象的dealloc裏面添加釋放的代碼。

@property (nonatomic, strong) Person *person;

- (void)setPerson:(Person *)person {
    if (_person != person) {
        [_person release];
        _person = [person retain];
    }
}

- (Person *) person {
    return _person;
}
複製代碼

ARC

在ARC環境中,咱們再也不像之前同樣本身手動管理內存,系統幫助咱們作了release或者autorelease等事情。 ARC是LLVM編譯器RunTime協做的結果。其中LLVM編譯器自動生成releasereatinautorelease的代碼,像weak弱引用這些則靠RunTime在運行時釋放。

引用計數

引用計數是一種內存管理技術,是指將資源(能夠是對象、內存或磁盤空間等等)的被引用次數保存起來,當被引用次數變爲零時就將其釋放的過程。使用引用計數技術能夠實現自動資源管理的目的。同時引用計數還能夠指使用引用計數技術回收未使用資源的垃圾回收算法。

在iOS中,對象執行reatin等操做後,該對象的引用計數就會+1,調用release等操做後,改對象的引用計數就會-1

引用計數規則

關於引用計數的規則,能夠總結以下:

  1. 本身生成的對象,本身持有。(alloc,new,copy,mutableCopy等)
  2. 非本身生成的對象,本身也能持有。(retain 等)
  3. 再也不須要本身持有的對象時釋放。(release,dealloc 等)
  4. 非本身持有的對象沒法釋放。

原理探索

alloc與retainCount原理

有關alloc的流程,能夠閱讀☞iOS底層學習 - OC對象前世此生來進行查看,就不作過多的贅述。

可是有一個細節須要注意,alloc自己是隻申請內存空間,不增長引用計數的。此時isaextra_rc爲0。

可是爲何咱們打印retainCount時,顯示的是1呢,咱們經過查看源碼能夠發現uintptr_t rc = 1 + bits.extra_rc;其自己前面是會加一個常量1的,用來標記本身生成的對象的引用計數。

inline uintptr_t 
objc_object::rootRetainCount()
{
    if (isTaggedPointer()) return (uintptr_t)this;

    sidetable_lock();
    isa_t bits = LoadExclusive(&isa.bits);
    ClearExclusive(&isa.bits);
    if (bits.nonpointer) {
        uintptr_t rc = 1 + bits.extra_rc;
        if (bits.has_sidetable_rc) {
            rc += sidetable_getExtraRC_nolock();
        }
        sidetable_unlock();
        return rc;
    }

    sidetable_unlock();
    return sidetable_retainCount();
}
複製代碼

reatin原理

經過如下源碼,咱們能夠知道:

  • TaggedPointer對象是不參與引用計數的
  • 小几率若有hasCustomRR,會走消息發送
inline id objc_object::retain()
{
    assert(!isTaggedPointer());

    if (fastpath(!ISA()->hasCustomRR())) {
        return rootRetain();
    }

    return ((id(*)(objc_object *, SEL))objc_msgSend)(this, SEL_retain);
}

複製代碼

接着咱們查看rootRetain方法,其主要處理以下:

  1. 判斷當前對象是否一個TaggedPointer,若是是則返回。
  2. 判斷isa是否通過NONPOINTER_ISA優化,若是未通過優化,則將引用計數存儲在SideTable中。64位的設備不會進入到這個分支。
  3. 判斷當前的設備是否正在析構。
  4. 將isa的bits中的extra_rc進行加1操做。
  5. 若是在extra_rc中已經存儲滿了,則調用sidetable_addExtraRC_nolock方法將一半的引用計數移存到SideTable中。
ALWAYS_INLINE id 
objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
    ✅//若是是TaggedPointer 直接返回
    if (isTaggedPointer()) return (id)this;

    bool sideTableLocked = false;
    bool transcribeToSideTable = false;

    isa_t oldisa;
    isa_t newisa;

    do {
        transcribeToSideTable =false;
        oldisa = LoadExclusive(&isa.bits);
        newisa = oldisa;
        ✅// 若是isa未通過NONPOINTER_ISA優化
        if (slowpath(!newisa.nonpointer)) {
            ClearExclusive(&isa.bits);
            if (!tryRetain && sideTableLocked) sidetable_unlock();
            if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
            else return sidetable_retain();//引用計數存儲於SideTable中
        }
        // donot check newisa.fast_rr; we already called any RR overrides
        ✅//檢查對象是都正在析構
        if (slowpath(tryRetain && newisa.deallocating)) {
            ClearExclusive(&isa.bits);
            if (!tryRetain && sideTableLocked) sidetable_unlock();
            return nil;
        }
        uintptr_t carry;
        ✅//isa的bits中的extra_rc進行加1
        newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc++
        ✅//若是bits的extra_rc已經存滿了,則將其中的一半存儲到sidetable中
        if (slowpath(carry)) {
            // newisa.extra_rc++ overflowed
            if (!handleOverflow) {
                ClearExclusive(&isa.bits);
                return rootRetain_overflow(tryRetain);
            }
            // Leave half of the retain counts inline and 
            // prepare to copy the other half to the side table.
            if (!tryRetain && !sideTableLocked) sidetable_lock();
            sideTableLocked = true;
            transcribeToSideTable = true;
            newisa.extra_rc = RC_HALF;//extra_rc置空一半的數值
            newisa.has_sidetable_rc = true;
        }
    } while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)));

    if (slowpath(transcribeToSideTable)) {
        // Copy the other half of the retain counts to the side table.
        ✅//將另外的一半引用計數存儲到sidetable中
        sidetable_addExtraRC_nolock(RC_HALF);
    }

    if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock();
    return (id)this;
}
複製代碼

其餘

關於release的相關原理,和retain的原理相反,你們能夠本身去探究一下;

關於dealloc的相關原理,在iOS底層學習 - 內存管理之weak原理探究中也有解析

總結

  • 內存佈局:
    • 內存地址由高到低分爲棧區(自動處理內存)、堆區(開發者管理,ARC下會自動釋放)、靜態區(全局變量,靜態變量)、數據段(常量)、代碼段(編寫的二進制代碼區)
    • static靜態變量的做用域與對象、類、分類不要緊,只與文件有關係。
  • 內存優化方案:
    • Tagged Pointer
      • Tagged Pointer是專⻔⽤來存儲⼩的對象,例如NSNumber,NSDate等。
      • Tagged Pointer指針的值再也不是地址了,⽽是真正的值。因此,實際上它再也不是⼀個對象了,它只是⼀個披着對象⽪的普通變量⽽已。因此,它的內存並不存儲在堆中,也不須要malloc和free。
      • 當指針不夠存儲數據時,就會使用動態分配內存的方式來存儲數據。
      • 在內存讀取上有着3倍的效率,建立時⽐之前快106倍。
    • NONPOINTER_ISA
      • 經過nonpointer標誌是否開啓指針優化
      • 經過extra_rc來存儲引用計數,根據系統架構不一樣,長度不一樣
      • 經過has_sidetable_rc來判斷超出extra_rc後,是否有全局SideTable存儲引用計數
    • SideTable
      • SideTables是一個全局的哈希數組,裏面存儲了SideTable類型數據
      • SideTablespinlock_t(自旋鎖,用於上/解鎖)、RefcountMap(存儲extra_rc溢出或者未開啓優化的引用計數)、weak_table_t(存儲弱引用表)
  • 引用計數規則:
    • 本身生成的對象,本身持有。(alloc,new,copy,mutableCopy等)
    • 非本身生成的對象,本身也能持有。(retain 等)
    • 再也不須要本身持有的對象時釋放。(release,dealloc 等)
    • 非本身持有的對象沒法釋放。
  • alloc自己是隻申請內存空間,不增長引用計數的。此時isa中extra_rc爲0。只是調用rootRetainCount其自己前面是會加一個常量1的,用來標記本身生成的對象的引用計數。
  • reatin原理:
    • 判斷當前對象是否一個TaggedPointer,若是是則返回。
    • 判斷isa是否通過NONPOINTER_ISA優化,若是未通過優化,則將引用計數存儲在SideTable中。64位的設備不會進入到這個分支。
    • 判斷當前的設備是否正在析構。
    • 將isa的bits中的extra_rc進行加1操做。
    • 若是在extra_rc中已經存儲滿了,則調用sidetable_addExtraRC_nolock方法將一半的引用計數移存到SideTable中。

參考

內存管理(一)

iOS內存管理一:Tagged Pointer&引用計數

相關文章
相關標籤/搜索