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

從這篇文章開始探索iOS的內存管理,主要涉及的內容有
1. 內存佈局;
2. 內存管理方案:Tagged Pointer、NONPOINTER_ISA、SiddeTables
3. ARC&MRC:retain和release以及retainCount
4. 自動釋放池:autoreleasepool
5. 弱引用weak的實現原理html

一、內存佈局

iOS中內存佈局區域大概分爲五個區域:棧區、堆區、BSS段、數據段、代碼段,他們在內存的分佈以下圖所示:c++

內存佈局

  • 棧區:編譯器自動分配,由系統管理,在不須要的時候自動清除。局部變量、函數參數存儲在這裏。棧區的內存地址通常是0x7開頭。
  • 堆區:那些由newallocblock copy建立的對象存儲在這裏,是由開發者管理的,須要告訴系統何時釋放內存。ARC下編譯器會自動在合適的時候釋放內存,而在MRC下須要開發者手動釋放。堆區的內存地址通常是0x6開頭。
  • BSS段:BSS段又稱靜態區,未初始化的全局變量,靜態變量存放在這裏。程序運行過程當中內存中的數據一直存在,程序結束後由系統釋放。
  • 數據段:數據段又稱常量區,專門存放常量,程序結束後由系統釋放。
  • 代碼段:用於存放程序運行時的代碼,代碼會被編譯成二進制存進內存的程序代碼區。

這裏有點值得一提的是靜態變量的做用域與對象、類、分類不要緊,只與文件有關係算法

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 (DS)

- (void)ds_add {
    age++;
    NSLog(@"Person (DS)內部:%@-%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] ds_add];
}

複製代碼

打印結果:
2020-03-23 16:53:35.671470+0800 ThreadDemo[40300:1619888] vc:0x103688cc0--10
2020-03-23 16:53:35.671611+0800 ThreadDemo[40300:1619888] vc:0x103688cc0--40
2020-03-23 16:53:35.671809+0800 ThreadDemo[40300:1619888] Person內部:<Person: 0x60000239c640>-0x103688d88--11
2020-03-23 16:53:35.671926+0800 ThreadDemo[40300:1619888] vc:0x103688cc0--40
2020-03-23 16:53:35.672071+0800 ThreadDemo[40300:1619888] Person內部:Person-0x103688d88--10
2020-03-23 16:53:35.672183+0800 ThreadDemo[40300:1619888] vc:0x103688cc0--40
2020-03-23 16:53:35.672332+0800 ThreadDemo[40300:1619888] Person (DS)內部:<Person: 0x6000023a7820>-0x103688cc4--11編程

從上面運行結果能夠知道,在Person類、Person分類、Controller中針對靜態變量age的操做,其值並不相互影響。c#

二、內存管理方案

OC中對內存優化管理的方案有以下幾種形式:Tagged Ponter、NONPOINTER_ISA 、SideTable。下面對着三種方案逐一解釋。數組

2.一、Tagged Ponter

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

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

那麼Tagged Ponter對於內存優化的點在哪裏呢?數據結構

2.1.一、Tagged Ponter內存優化

對於一個NSNumber對象,其值是一個整數。正常狀況下,若是這個整數只是一個 NSInteger的普通變量,那麼它在32位CPU下佔 4 個字節,在 64 位CPU下佔 8 個字節的。而NSNumber對象還有一個isa指針,它在32位CPU下爲4個字節,在 64 位 CPU 下也是 8 個字節。因此從32位機器遷移到64位機器中後,雖然邏輯沒有任何變化,但這種 NSNumber、NSDate 一類的對象所佔用的內存會翻倍。以下圖所示(圖片摘自唐巧博客):架構

而實際上一個NSNumber、NSDate這一類的變量的值須要的內存空間經常不須要8個字節,那麼如上述來進行數據的存儲,內存空間的浪費是很大的。Tagged Ponter偏偏解決了這一塊的問題。 Tagged Ponter將一個對象的指針拆成兩部分,一部分直接保存數據,另外一部分做爲特殊標記,表示這是一個特別的指針,不指向任何一個地址。因此,引入了Tagged Pointer對象以後,64位CPU下NSNumber 的內存圖變成了如下這樣:app

image.png

2.1.二、Tagged Ponter的底層探索

先來看一下關於Tagged Ponter的底層源碼。

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對象指針的值再也不是地址了,⽽是真正的值,那咱們須要知道的是Tagged Pointer的值的存儲方式。看以下代碼:

#define _OBJC_TAG_MASK (1UL << 63)
NSMutableString *mutableStr = [NSMutableString string];
NSString *immutable = nil;
char c = 'a';
do {
    [mutableStr appendFormat:@"%c", c++];
    immutable = [mutableStr copy];
    NSLog(@"0x%lx %@ %@", _objc_decodeTaggedPointer_(immutable), immutable, immutable.class);
} while (((uintptr_t)immutable & _OBJC_TAG_MASK) == _OBJC_TAG_MASK);
複製代碼

打印結果:
2020-03-25 17:05:22.784213+0800 taggedPointer[76305:3706620] 0xa000000000000611 a NSTaggedPointerString
2020-03-25 17:05:22.784368+0800 taggedPointer[76305:3706620] 0xa000000000062612 ab NSTaggedPointerString
2020-03-25 17:05:22.784481+0800 taggedPointer[76305:3706620] 0xa000000006362613 abc NSTaggedPointerString
2020-03-25 17:05:22.784594+0800 taggedPointer[76305:3706620] 0xa000000646362614 abcd NSTaggedPointerString
2020-03-25 17:05:22.784698+0800 taggedPointer[76305:3706620] 0xa000065646362615 abcde NSTaggedPointerString
2020-03-25 17:05:22.784791+0800 taggedPointer[76305:3706620] 0xa006665646362616 abcdef NSTaggedPointerString
2020-03-25 17:05:22.784874+0800 taggedPointer[76305:3706620] 0xa676665646362617 abcdefg NSTaggedPointerString
2020-03-25 17:05:22.784955+0800 taggedPointer[76305:3706620] 0xa0022038a0116958 abcdefgh NSTaggedPointerString
2020-03-25 17:05:22.785044+0800 taggedPointer[76305:3706620] 0xa0880e28045a5419 abcdefghi NSTaggedPointerString
2020-03-25 17:05:22.785173+0800 taggedPointer[76305:3706620] 0x409bac70e6d7a14b abcdefghij __NSCFString

從上面這段代碼的運行結果能夠看出當字符串的長度增長到10時,字符串的類型輸出是__NSCFString,當長度小於10時,字符串類型輸出是NSTaggedPointerString,並且其地址都是0xa開頭。回過頭來看上述的代碼,while中循環條件是爲了判斷在64位數據中最高是不是1,以此來判斷當前的對象是不是一個Tagged Pointer對象。咱們將0xa轉換爲二進制1010,其中最高位1表示是對象是一個Tagged Pointer對象,餘下010(十進制2)表示的是對象是一個NSString類型。那麼對象的值存在哪裏呢,拿0xa000000000000611來講,其中的61就是對應的ASII碼中的a。其餘的能夠照此類推。

以下是系統提供的各類標誌位的定義。

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
};
複製代碼

系統提供了判斷是不是Tagged Pointer的方法

# define _OBJC_TAG_MASK (1UL<<63)
static inline bool 
_objc_isTaggedPointer(const void * _Nullable ptr) 
{
    return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}
複製代碼

更加詳細的資料請參閱Tagged Pointers

2.二、NONPOINTER_ISA

NONPOINTER_ISA一樣是蘋果公司對於內存優化的一種方案。用 64 bit 存儲一個內存地址顯然是種浪費,畢竟不多有那麼大內存的設備。因而能夠優化存儲方案,用一部分額外空間存儲其餘內容isa 指針第一位爲 1 即表示使用優化的 isa 指針,這裏列出在__x86_64__架構下的 64 位環境中 isa 指針結構,__arm64__的架構會有所差異。

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
};
複製代碼
  • nonpointer:示是否對isa開啓指針優化。0表明是純isa指針,1表明除了地址外,還包含了類的一些信息、對象的引用計數等
  • has_assoc:關聯對象標誌位,0沒有,1存在。
  • has_cxx_dtor:該對象是否有C++或Objc的析構器,若是有析構函數,則須要作一些析構的邏輯處理,若是沒有,則能夠更快的釋放對象。
  • shiftcls:存在類指針的值,開啓指針優化的狀況下,arm64位中有33位來存儲類的指針
  • magic:判斷當前對象是真的對象仍是一段沒有初始化的空間
  • weakly_referenced:是否被指向或者曾經指向一個ARC的弱變量,沒有弱引用的對象釋放的更快。
  • deallocating:標誌是否正在釋放內存。
  • has_sidetable_rc:是否有輔助的引用計數散列表。當對象引⽤技術⼤於 10 時,則須要借⽤該變量存儲進位。
  • extra_rc:表示該對象的引⽤計數值,其實是引⽤計數值減 1,例如,若是對象的引⽤計數爲 10,那麼 extra_rc 爲 9。若是引⽤計數⼤於 10,則須要使⽤到下⾯的 has_sidetable_rc。

其結構以下圖所示:

2.三、SideTable

SideTable在OC中扮演這一個很重要的角色。在runtime中,經過SideTable來管理對象的引用計數以及weak引用。同時,系統中維護了一個全局的SideTables,這是一個SideTable的集合。

來看看SideTable的定義:

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

SideTable的定義很清晰,有三個成員:

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

關於更多的SideTable的內容請移步我以前的文章iOS底層原理:weak的實現原理,在這篇文章中詳細介紹了SideTable。

三、引用計數

3.一、什麼是引用計數

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

當一個對象建立並在堆區申請內存時,對象的引用計數爲1;當其餘的對象須要持有這個對象時,就須要將這個對象的引用計數加1;當其餘的對象再也不須要持有這個對象時,須要將對象的引用計數減1;當對象的引用計數爲0時,對象的內存就會當即釋放,對象銷燬。

  • 調用alloc、new、copy、mutableCopy名稱開頭的方法建立的對象,該對象的引用計數加1。
  • 調用retain方法時,該對象的引用計數加1。
  • 調用release方法時,該對象的引用計數減1。
  • autorelease方法不改變該對象的引用計數器的值,只是將對象添加到自動釋放池中。
  • retainCount方法返回該對象的引用計數值。

3.二、對象持有規則

對象的持有規則以下:

  1. 本身生成的對象,本身持有。
  2. 非本身生成的對象,本身也能持有。
  3. 再也不須要本身持有的對象時釋放。
  4. 非本身持有的對象沒法釋放。

對象的持有標準在於對象的引用計數的值,那麼結合對象建立方式,對象的引用計數加減,對象的銷燬大體以下的關係:

對象操做 Objective-C方法
生成並持有對象 alloc/new/copy/mutableCopy等方法
持有對象 retain方法
釋放對象 release方法
廢棄對象 dealloc方法

3.2.一、本身生成的對象,本身持有

使用如下名稱開頭的方法名意味着本身生成的對象只有本身持有:alloc、new、copy、mutableCopy。 在OC中對象的建立能夠經過allocnew這兩種方式來建立一個對象。

NSObject *obj = [NSObject alloc];
NSObject *obj1 = [NSObject new];//等價於 NSObject *obj1 = [[NSObject alloc]init];
複製代碼

關於alloc和new的相關知識請移步以前的文章IOS底層原理之alloc、init和new,在這裏就很少加描述。這裏着重講解一下copymutableCopy,它們意味着對象的拷貝。對象的拷貝須要遵循NSCopying協議和NSMutableCopying協議。

@interface Person : NSObject<NSCopying,NSMutableCopying>

@end

@implementation Person


- (nonnull id)copyWithZone:(nullable NSZone *)zone { 
    Person *person = [[self class] allocWithZone:zone];
    return person;
}

- (id)mutableCopyWithZone:(NSZone *)zone{
    Person *person = [[self class] allocWithZone:zone];
    return person;
}

@end

- (void)viewDidLoad {
    [super viewDidLoad];
    Person *person = [[Person alloc]init];
    Person *person1 = [person copy];
    Person *person2 = [person mutableCopy];
    NSLog(@"person:%p--person1:%p--person2:%p",person,person1,person2);
}
複製代碼

打印結果:
2020-03-26 15:56:26.666859+0800 taggedPointer[89806:4395707] person:0x6000038342c0 retainCount:1
2020-03-26 15:56:26.667011+0800 taggedPointer[89806:4395707] person1:0x6000038342f0 retainCount:1
2020-03-26 15:56:26.667113+0800 taggedPointer[89806:4395707] person2:0x600003834300 retainCount:1

從上面的代碼運行的結果能夠看出使用copymutableCopy生成的person1和person2對象以及person對象,三者之間地址是不同的,說明建立了新的對象。並且它們的引用計數都爲1。copymutableCopy的區別在於,前者生成不可變動的對象,然後者生成可變動的對象。

須要說明的是alloc方法並無對retainCount進行操做,這裏的引用計數之因此爲1,那是由於retainCount方法的底層是默認+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();
}

複製代碼

3.2.1.一、淺拷貝和深拷貝

既然已經說到了copymutableCopy,那麼就來講說淺拷貝和深拷貝。

淺拷貝:對象的指針拷貝,不會開闢新的內存。
深拷貝:拷貝對象自己,會建立一個新的對象,指向不一樣的內存地址。

對於不可變對象(如NSString、NSArray、NSDictionary)和可變對象(如NSMutableString、NSMutableArray、NSMutableDictionary)用copy和mutableCopy會有一些差異,大體以下表所示:

對於集合類的可變對象來講,深拷貝並不是嚴格意義上的深複製,雖然新開闢了內存,可是對於存放在數組裏面的元素來講仍然是淺拷貝。

3.2.二、非本身生成的對象,本身也能持有

alloc、new、copy、mutableCopy以外的方法得到的對象,由於並不是本身生產持有,因此本身不是該對象的持有者。

//非本身生成的對象,暫時沒有持有
id obj = [NSMutableArray array];

//經過retain持有對象
[obj retain];
複製代碼

上述代碼中NSMutableArray經過類方法array生成了一個對象賦給變量obj,但變量obj本身並不持有該對象。使用retain方法能夠持有對象。

3.2.三、再也不須要本身持有的對象時釋放

本身持有的對象,一旦該對象再也不須要時,持有者有義務調用release方法釋放該對象。固然在ARC環境下並不須要開發者主動調用方法,系統會自動調用該方法,可是在MRC環境下須要開發者手動在合適的地方作對象的retain 方法和release方法的調用。

3.2.四、非本身持有的對象沒法釋放

對於用alloc、new、copy、mutableCopy方法生成並持有的對象,或是用retain方法持有的對象,因爲持有者是本身,因此在不須要該對象時須要將其釋放。而由此之外所獲得的對象絕對不能釋放。假若在程序中釋放了非本身所持有的對象就會形成崩潰。

// 本身生成並持有對象
id obj = [[NSObject alloc] init];

//釋放對象
[obj release];

//再次釋放已經非本身持有的對象,應用程序崩潰
[obj release];
複製代碼

釋放了非本身持有的對象,確定會致使應用崩潰。所以絕對不要去釋放非本身持有的對象。

3.三、alloc、retain、release、dealloc、autorelease實現

3.3.一、alloc實現

總結一句話就是alloc建立了對象而且申請了一塊很多於16字節的內存空間。關於alloc的實現請移步以前的文章IOS底層原理之alloc、init和new,在這裏就很少加描述。

3.3.二、retain實現

在前面的小節內容中,講到在isabits中的extra_rc字段和SideTable結構中的RefcountMap refcnts都有存儲引用計數,那麼在這二者之間會有什麼聯繫呢?下面經過retain的源碼來分析引用計數的存儲。

id objc_retain(id obj)
{
    if (!obj) return obj;
    if (obj->isTaggedPointer()) return obj;
    return obj->retain();
}
複製代碼

首先是objc_retain方法,在該方法內部會現有一個判斷當前對象是不是TaggedPointer,若是是則返回,不然調用retain方法。經過這裏咱們也能夠看到 TaggedPointer對象並不作引用計數處理。

inline id objc_object::retain()
{
    assert(!isTaggedPointer());

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

    return ((id(*)(objc_object *, SEL))objc_msgSend)(this, SEL_retain);
}
複製代碼

retain方法內部其實很簡單,就是一個判斷,而後調用rootRetain方法。其中fastpath是大機率發生的意思。

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中
        }
        // don't 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;
}

複製代碼

rootRetain方法是retain引用計數的核心方法。咱們能夠看到方法作了以下幾方面的工做:

  1. 判斷當前對象是否一個TaggedPointer,若是是則返回。
  2. 判斷isa是否通過NONPOINTER_ISA優化,若是未通過優化,則將引用計數存儲在SideTable中。64位的設備不會進入到這個分支。
  3. 判斷當前的設備是否正在析構。
  4. isabits中的extra_rc進行加1操做。
  5. 若是在extra_rc中已經存儲滿了,則調用sidetable_addExtraRC_nolock方法將一半的引用計數移存到SideTable中。

3.3.三、release實現

在上一章節中咱們分析了引用計數的存儲在bitsSideTable中的存儲,那麼做爲釋放對象的release又是怎麼對引用計數進行減1操做的呢?

void 
objc_release(id obj)
{
    if (!obj) return;
    if (obj->isTaggedPointer()) return;
    return obj->release();
}
複製代碼

首先是objc_release方法,在該方法內部會現有一個判斷當前對象是不是TaggedPointer,若是是則返回,不然調用release方法。

inline void
objc_object::release()
{
    assert(!isTaggedPointer());

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

    ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_release);
}

複製代碼

release方法內部其實很簡單,就是一個判斷,而後調用rootRelease方法。其中fastpath是大機率發生的意思。

ALWAYS_INLINE bool 
objc_object::rootRelease(bool performDealloc, bool handleUnderflow)
{
    //判斷是不是TaggedPointer
    if (isTaggedPointer()) return false;

    bool sideTableLocked = false;

    isa_t oldisa;
    isa_t newisa;

 retry:
    do {
        oldisa = LoadExclusive(&isa.bits);
        newisa = oldisa;
        //若是isa是未通過NONPOINTER_ISA優化,則對SideTable中的引用計數進行清理
        if (slowpath(!newisa.nonpointer)) {
            ClearExclusive(&isa.bits);
            if (sideTableLocked) sidetable_unlock();
            return sidetable_release(performDealloc);
        }
        // don't check newisa.fast_rr; we already called any RR overrides
        uintptr_t carry;
        //isa的bits的extra_rc減1
        newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc--
        //extra_rc已經置空
        if (slowpath(carry)) {
            // don't ClearExclusive()
            goto underflow;
        }
    } while (slowpath(!StoreReleaseExclusive(&isa.bits, 
                                             oldisa.bits, newisa.bits)));

    if (slowpath(sideTableLocked)) sidetable_unlock();
    return false;

 underflow:
    // newisa.extra_rc-- underflowed: borrow from side table or deallocate

    // abandon newisa to undo the decrement
    newisa = oldisa;
    //isa的has_sidetable_rc表示是否有輔助的引用計數散列表
    if (slowpath(newisa.has_sidetable_rc)) {
        if (!handleUnderflow) {
            ClearExclusive(&isa.bits);
            return rootRelease_underflow(performDealloc);
        }

        // Transfer retain count from side table to inline storage.

        if (!sideTableLocked) {
            ClearExclusive(&isa.bits);
            sidetable_lock();
            sideTableLocked = true;
            // Need to start over to avoid a race against 
            // the nonpointer -> raw pointer transition.
            goto retry;
        }

        // Try to remove some retain counts from the side table.
        //
        size_t borrowed = sidetable_subExtraRC_nolock(RC_HALF);

        // To avoid races, has_sidetable_rc must remain set 
        // even if the side table count is now zero.

        if (borrowed > 0) {
            // Side table retain count decreased.
            // Try to add them to the inline count.
            newisa.extra_rc = borrowed - 1;  // redo the original decrement too
            bool stored = StoreReleaseExclusive(&isa.bits, 
                                                oldisa.bits, newisa.bits);
            if (!stored) {
                // Inline update failed. 
                // Try it again right now. This prevents livelock on LL/SC 
                // architectures where the side table access itself may have 
                // dropped the reservation.
                isa_t oldisa2 = LoadExclusive(&isa.bits);
                isa_t newisa2 = oldisa2;
                if (newisa2.nonpointer) {
                    uintptr_t overflow;
                    newisa2.bits = 
                        addc(newisa2.bits, RC_ONE * (borrowed-1), 0, &overflow);
                    if (!overflow) {
                        stored = StoreReleaseExclusive(&isa.bits, oldisa2.bits, 
                                                       newisa2.bits);
                    }
                }
            }

            if (!stored) {
                // Inline update failed.
                // Put the retains back in the side table.
                sidetable_addExtraRC_nolock(borrowed);
                goto retry;
            }

            // Decrement successful after borrowing from side table.
            // This decrement cannot be the deallocating decrement - the side 
            // table lock and has_sidetable_rc bit ensure that if everyone 
            // else tried to -release while we worked, the last one would block.
            sidetable_unlock();
            return false;
        }
        else {
            // Side table is empty after all. Fall-through to the dealloc path.
        }
    }

    // Really deallocate.

    if (slowpath(newisa.deallocating)) {
        ClearExclusive(&isa.bits);
        if (sideTableLocked) sidetable_unlock();
        return overrelease_error();
        // does not actually return
    }
    newisa.deallocating = true;
    if (!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)) goto retry;

    if (slowpath(sideTableLocked)) sidetable_unlock();

    __sync_synchronize();
    if (performDealloc) {
        ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
    }
    return true;
}
複製代碼

rootRelease方法是release引用計數的核心方法。咱們能夠看到方法作了以下幾方面的工做:

  1. 判斷當前對象是否一個TaggedPointer,若是是則返回。
  2. 判斷isa是否通過NONPOINTER_ISA優化,若是未通過優化,則清理在SideTable中的引用計數。64位的設備不會進入到這個分支。
  3. isabits中的extra_rc進行減1操做。
  4. 若是extra_rc已經置空,則清理SideTable中的引用計數。
  5. 嘗試將SideTable中的引用計數移存到isabit中。

3.3.四、autorelease實現

說到Objective-C內存管理,就不能不提autorelease。 顧名思義,autorelease就是自動釋放。這看上去很像ARC,但實際上它更相似於C語言中自動變量(局部變量)的特性。autorelease會像C語言的局部變量那樣來對待對象實例。當其超出做用域時,對象實例的release實例方法被調用。另外,同C語言的局部變量不一樣的是,編程人員能夠設置變量的做用域。 autorelease的具體使用方法以下:

  • 生成並持有NSAutoreleasePool對象。
  • 調用已分配對象的autorelease實例方法。
  • 廢棄NSAutoreleasePool對象。

來看autorelease的代碼實現。

id objc_autorelease(id obj)
{
    if (!obj) return obj;
    if (obj->isTaggedPointer()) return obj;
    return obj->autorelease();
}
複製代碼

首先是objc_autorelease方法,在該方法內部會現有一個判斷當前對象是不是TaggedPointer,若是是則返回,不然調用autorelease方法。

inline id 
objc_object::autorelease()
{
    if (isTaggedPointer()) return (id)this;
    if (fastpath(!ISA()->hasCustomRR())) return rootAutorelease();

    return ((id(*)(objc_object *, SEL))objc_msgSend)(this, SEL_autorelease);
}
複製代碼

autorelease方法內部會再次判斷當前對象是不是TaggedPointer,若是是則返回,不然調用rootAutorelease方法。其中fastpath是大機率發生的意思。

inline id objc_object::rootAutorelease()
{
    if (isTaggedPointer()) return (id)this;
    if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this;

    return rootAutorelease2();
}

id objc_object::rootAutorelease2()
{
    assert(!isTaggedPointer());
    return AutoreleasePoolPage::autorelease((id)this);
}
複製代碼

rootAutorelease的代碼核心就是將當前對象添加到AutoreleasePool自動釋放池中。

3.3.五、dealloc對象銷燬

當對象的引用計數爲0時,底層會調用_objc_rootDealloc方法對對象進行釋放,而在_objc_rootDealloc方法裏面會調用rootDealloc方法。以下是rootDealloc方法的代碼實現。

inline void
objc_object::rootDealloc()
{
    if (isTaggedPointer()) return;  // fixme necessary?

    if (fastpath(isa.nonpointer  &&  
                 !isa.weakly_referenced  &&  
                 !isa.has_assoc  &&  
                 !isa.has_cxx_dtor  &&  
                 !isa.has_sidetable_rc))
    {
        assert(!sidetable_present());
        free(this);
    } 
    else {
        object_dispose((id)this);
    }
}
複製代碼
  1. 首先判斷對象是不是Tagged Pointer,若是是則直接返回。
  2. 若是對象是採用了優化的isa計數方式,且同時知足對象沒有被weak引用!isa.weakly_referenced、沒有關聯對象!isa.has_assoc、沒有自定義的C++析構方法!isa.has_cxx_dtor、沒有用到SideTable來引用計數!isa.has_sidetable_rc則直接快速釋放。
  3. 若是不能知足2中的條件,則會調用object_dispose方法。

object_dispose方法很簡單,主要是內部調用了objc_destructInstance方法。

void *objc_destructInstance(id obj) {
    if (obj) {
        // Read all of the flags at once for performance.
        bool cxx = obj->hasCxxDtor();
        bool assoc = obj->hasAssociatedObjects();

        // This order is important.
        if (cxx) object_cxxDestruct(obj);
        if (assoc) _object_remove_assocations(obj);
        obj->clearDeallocating();
    }

    return obj;
}
複製代碼

上面這一段代碼很清晰,若是有自定義的C++析構方法,則調用C++析構函數。若是有關聯對象,則移除關聯對象並將其自身從AssociationManager的map中移除。調用clearDeallocating方法清除對象的相關引用。

inline void 
objc_object::clearDeallocating()
{
    if (slowpath(!isa.nonpointer)) {
        // Slow path for raw pointer isa.
        sidetable_clearDeallocating();
    }
    else if (slowpath(isa.weakly_referenced  ||  isa.has_sidetable_rc)) {
        // Slow path for non-pointer isa with weak refs and/or side table data.
        clearDeallocating_slow();
    }

    assert(!sidetable_present());
}
複製代碼

clearDeallocating中有兩個分支,先是判斷對象是否採用了優化isa引用計數,若是沒有的話則須要清理對象存儲在SideTable中的引用計數數據。若是對象採用了優化isa引用計數,則判斷是都有使用SideTable的輔助引用計數(isa.has_sidetable_rc)或者有weak引用(isa.weakly_referenced),符合這兩種狀況中一種的,調用clearDeallocating_slow方法。

NEVER_INLINE void
objc_object::clearDeallocating_slow()
{
    assert(isa.nonpointer  &&  (isa.weakly_referenced || isa.has_sidetable_rc));

    SideTable& table = SideTables()[this]; // 在全局的SideTables中,以this指針爲key,找到對應的SideTable
    table.lock();
    if (isa.weakly_referenced) { // 若是obj被弱引用
        weak_clear_no_lock(&table.weak_table, (id)this); // 在SideTable的weak_table中對this進行清理工做
    }
    if (isa.has_sidetable_rc) { // 若是採用了SideTable作引用計數
        table.refcnts.erase(this); // 在SideTable的引用計數中移除this
    }
    table.unlock();
}
複製代碼

clearDeallocating_slow方法中有兩個分支,一是若是對象被弱引用,則調用weak_clear_no_lock方法在SideTableweak_table中對this進行清理工做。二是若是採用了SideTable作引用計數,則在 SideTable的引用計數中移除this。

3.四、ARC下的規則

  1. 不能顯式的調用retain、release、retainCount、autorelease。
  2. 不能使用NSAllocateObject和NSDeallocateObject。
  3. 必須遵照內存管理的命名規則。
  4. 不要顯式的調用dealloc。
  5. 使用@autoreleasepool代替NSAutoreleasePool。
  6. 不能使用區域NSZone。
  7. 對象變量不能做爲C語言結構體的成員。
  8. 顯式轉換"id"和"void *"。

四、總結

  1. IOS中內存佈局區域大概分爲五個區域:棧區、堆區、BSS段、數據段、代碼段
  2. OC中對內存優化管理的方案有以下幾種形式:Tagged Ponter、NONPOINTER_ISA 、SideTable
  3. Tagged Pointer是專⻔⽤來存儲⼩的對象,例如NSNumber,NSDate等。其指針的值再也不是地址,而是真正的值。因此,它的內存並不存儲在堆中,也不須要mallocfree。在內存讀取上有着3倍的效率,建立時⽐之前快106倍。
  4. NONPOINTER_ISA就是用一部分額外空間存儲其餘內容,這樣提升了內存的利用率。
  5. SideTable是一個hash表結構,主要是針對引用計數和弱引用表進行相關操做。
  6. 對象的持有規則:本身生成的對象,本身持有;非本身生成的對象,本身也能持有;再也不須要本身持有的對象時釋放;非本身持有的對象沒法釋放
  7. alloc/new/copy/mutableCopy等方法生成並持有對象,retain方法引用計數加1,release方法引用計數減1,dealloc方法銷燬對象。
  8. autorelease方法不改變該對象的引用計數器的值,只是將對象添加到自動釋放池中。
  9. ARC下不能顯式的調用retain、release、retainCount、autorelease

參考資料

相關文章
相關標籤/搜索