iOS優美的內存管理

前言

相關代碼工程Demo

內存管理這篇文章主要會從內存佈局,內存管理方案,ARC&MRC ,引用計數,弱引用,以及自動釋放池這幾個方面來進行詳細的介紹;

內存佈局,五大區

程序加載到內存中會被分開爲幾個區:內存地址從高到低分別是:內核區

 

  • 內核區:系統內核處理大小一般爲1G 
  • stack(棧區):存儲函數, 方法,常量等, 地址從高到低;(0x7,大概地址)
  • heap(堆區):通過alloc,New創建的對象和blockcopy都放在這裏, 地址從低到高;( 0x6 大概地址)
  • bss:未初始化的全局變量,靜態變量等;一般爲:0x1開頭
  • data:初始化的全局變量,靜態變量;
  • text:程序中的代碼段加載到內存中放在這裏;

這些內存佈局中,很多人會說stack 區訪問速度比heap區速度要快,這是什麼原因呢?

如圖所示,我們在工程中創建一個對象objc ,我們可以看出po objc 可以看到對象是存儲在堆區,但是objc 地址是在棧區,也就是說,我們一般在訪問對象的時候是由寄存區-->訪問棧區-->然後找到對象地址--->再訪問堆區,才能獲取到對象, 所以一般情況,棧區比堆區訪問速度要快一些。

       面試題:全局變量和局部變量在內存中是否有區別?如果有,是什麼區別?

答:有區別。全局變量保存在內存的全局存儲區中,佔用靜態的存儲單元局部變量保存在棧中,只有在所在函數被調用時動態地爲變量分配存儲單元;

       Block 是否可以直接修改全局變量 ?

//全局變量
static NSString *xzname = @"XZ";

 [UIView animateWithDuration:1 animations:^{
        xzname = @"XZ";
    }];
    
 NSLog(@"xzname = %@",xzname);

答:可以改變全局變量,因爲全局變量作用域比較大,作用在全局;

看下面代碼中定義的變量分佈的區域:

@interface ViewController ()

@end

@implementation ViewController

int clA;
int clB = 10;

static int bssA;
static NSString *bssStr1;

static int bssB = 10;
static NSString *bssStr2 = @"Alan";
static NSString *xzname = @"XZ";

- (void)viewDidLoad {
    [super viewDidLoad];

    [self testStack];
    [self testHeap];
    [self testConst];

    // Do any additional setup after loading the view.
}
- (void)testStack{
    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));
}

- (void)testHeap{
    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);
    // 訪問---通過對象->堆區地址->存在棧區的指針
}

- (void)testConst{
    
    // 面試:全局變量和局部變量在內存中是否有區別?如果有,是什麼區別?
   
    
//     面試:Block 是否可以直接修改全局變量 ?
//    [UIView animateWithDuration:1 animations:^{
//        xzname = @"XZ";
//    }];
//
//    NSLog(@"xzname = %@",xzname);
    
    
    NSLog(@"************靜態區************");
    NSLog(@"clA == \t%p",&clA);
    NSLog(@"bssA == \t%p",&bssA);
    NSLog(@"bssStr1 == \t%p",&bssStr1);
    
    NSLog(@"************常量區************");
    NSLog(@"clB == \t%p",&clB);
    NSLog(@"bssB == \t%p",&bssB);
    NSLog(@"bssStr2 == \t%p",&bssStr2);
    
}

看下打印結果:

下面我們來看一道常見的靜態區安全測試面試題:

看下面代碼,說出執行結果:

在XZPerson中代碼爲:聲明瞭一個全局靜態變量,並在run 和eat方法中打印self,personNum地址和值,都進行++ 操作

#import <Foundation/Foundation.h>

static int personNum = 100;

NS_ASSUME_NONNULL_BEGIN

@interface XZPerson : NSObject
- (void)run;
+ (void)eat;

@end

NS_ASSUME_NONNULL_END
#import "XZPerson.h"

@implementation XZPerson
- (void)run{
    personNum ++;
    NSLog(@"XZPerson內部:%@-%p--%d",self,&personNum,personNum);
}

+ (void)eat{
    personNum ++;
    NSLog(@"XZPerson內部:%@-%p--%d",self,&personNum,personNum);
}

- (NSString *)description{
    return @"";
}
@end

XZPerson 分類中代碼爲:定義了一個方法,打印self,PersoNum 值和地址

#import "XZPerson.h"

NS_ASSUME_NONNULL_BEGIN

@interface XZPerson (XZ)
- (void)cate_method;

@end

NS_ASSUME_NONNULL_END
#import "XZPerson+XZ.h"



@implementation XZPerson (XZ)
- (void)cate_method{
    NSLog(@"XZPerson內部:%@-%p--%d",self,&personNum,personNum);
}

@end

然後在ViewController內寫一個測試代碼;

//personNum 默認值爲100
- (void)testConstSafety
{
     NSLog(@"************靜態區安全測試************");
     NSLog(@"vc:%p--%d",&personNum,personNum); 
     personNum = 10000;
     NSLog(@"vc:%p--%d",&personNum,personNum); 
     [[XZPerson new] run]; 
     NSLog(@"vc:%p--%d",&personNum,personNum); 
     [XZPerson eat]; 
     NSLog(@"vc:%p--%d",&personNum,personNum); 
    
     [[XZPerson alloc] cate_method];

}

看到博文的兄弟們,可以自己先考慮一下這個的輸出結果,接下來我們來看一下輸出結果:

可以看出在ViewController控制器中的personNum同一個地址,而且變化是相同的,XZPerson類中是同一個地址XZPerson分類中又是另一個地址,這些地址和別的類中地址都不相同這些值都不跟隨別的文件中的值變化而變化,唯一一個就是這個值在沒有賦值的情況下,默認值都是100;

這就說明:靜態全局變量是全局可以使用,但是每個文件如果使用都會開闢出一個單獨的空間進行賦值改變,而且別的文件中的值都不會改變,只針對文件有效。

內存管理方案

isa ,在之前的文章中有講過.

TaggedPointer,我們先來看一到面試題:請問下面代碼在運行中,點擊屏幕會不會有問題?

- (void)viewDidLoad {
    [super viewDidLoad];
    [self taggedPointerDemo];

}
//MARK: - taggedPointer 面試題
- (void)taggedPointerDemo {
    
    self.queue = dispatch_queue_create("com.cooci.cn", DISPATCH_QUEUE_CONCURRENT);
    
    for (int i = 0; i<10000; i++) {
        dispatch_async(self.queue, ^{
            self.nameStr = [NSString stringWithFormat:@"alan"];
            NSLog(@"%@",self.nameStr);
        });
    }
    
 }
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    
    NSLog(@"來了");
    for (int i = 0; i<10000; i++) {
        dispatch_async(self.queue, ^{
            self.nameStr = [NSString stringWithFormat:@"內存管理--內存管理方案"];
            NSLog(@"%@",self.nameStr);
        });
    }

}

查看下運行結果:

運行程序可以看到確實崩潰了,而且是崩潰到touchBegan ,多次嘗試,都崩潰到這個地方,我們就需要分析了:

   多線程+ setter getter 就會導致崩潰;

打開Debug->DebugWorkflow->Always Show Disassembly, 查看堆棧

可以看出是在objc_release 方法導致崩潰的

我們可以看一下setter 方法的源碼:

static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
    if (offset == 0) {
        object_setClass(self, newValue);
        return;
    }

    id oldValue;
    id *slot = (id*) ((char*)self + offset);

    if (copy) {
        newValue = [newValue copyWithZone:nil];
    } else if (mutableCopy) {
        newValue = [newValue mutableCopyWithZone:nil];
    } else {
        if (*slot == newValue) return;
        newValue = objc_retain(newValue);
    }

    if (!atomic) {
        oldValue = *slot;
        *slot = newValue;
    } else {
        spinlock_t& slotlock = PropertyLocks[slot];
        slotlock.lock();
        oldValue = *slot;
        *slot = newValue;        
        slotlock.unlock();
    }

    objc_release(oldValue);
}

可以看到核心就是: retian newvalue 和 realase oldvalue,

這個時候如果加入多線程後,就會導致可能會多次release 導致野指針,這就說明,多線程狀態下,set方法和get方法是不安全的;

但是爲什麼不會崩潰到上面呢,我們來看一下這兩個字符串:

可以看出在賦值爲alan 的時候字符串類型爲NSTaggedPointerString  類型 賦值爲@"內存管理--內存管理方案" 時字符串類型爲NSCFString 類型

我們來看一下release 源碼:

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

可以看出來,當對象是NSTaggedPointerString 類型時,不進行release ,這就解釋了,爲什麼不會在上面崩潰了;

經過測試當英文字符爲11個以內的時候字符串都會是NSTaggedPointerString 類型中文字符都不是這個類型

深入探索TaggedPointer

通常我們創建對象,對象存儲在堆中,對象的指針存儲在棧中,如果我們要找到這個對象,就需要先在棧中,找到指針地址,然後根據指針地址找到在堆中的對象。

這個過程比較繁瑣,當存儲的對象只是一個很小的東西,比如一個字符串,一個數字。去走這麼一個繁瑣的過程,無非是耗費性能的,所以蘋果就搞出了TaggedPointer這麼一個東西。

  1. TaggedPointer是蘋果爲了解決32位CPU到64位CPU的轉變帶來的內存佔用和效率問題,針對NSNumberNSDate以及部分NSString的內存優化方案。

  2. TaggedPointer指針的值不再是地址了,而是真正的值。所以,實際上它不再是一個對象了,它只是一個披着對象皮的普通變量而已。所以,它的內存並不存儲在堆中也不需要malloc和free

  3. TaggedPointer指針中包含了當前對象的地址、類型、具體數值。因此TaggedPointer指針在內存讀取上有着更高的效率,創建時比普通需要malloc跟free的類型快;

- (void)taggentPointer
{
    // 優化 - 編譯讀取的時候 更直接
    // retian release
    // 3倍
    // 創建 100
    // [8-10]
    
    NSString *str1 = [NSString stringWithFormat:@"a"];
    NSString *str2 = [NSString stringWithFormat:@"b"];
    
    NSLog(@"%@--%p-%@",object_getClass(str1),str1,str1);
    NSLog(@"%@--%p-%@",object_getClass(str2),str2,str2);
   
    NSNumber *number1 = @1;
    NSNumber *number2 = @1;
    NSNumber *number3 = @2.0;
    NSNumber *number4 = @3.2;
    NSLog(@"%@--%p---%@",object_getClass(number1),number1,number1);
    NSLog(@"%@--%p---%@",object_getClass(number2),number2,number2);
    NSLog(@"%@--%p---%@",object_getClass(number3),number3,number3);
    NSLog(@"%@--%p---%@",object_getClass(number4),number4,number4);
}

運行結果爲:



看下這個的初始化:

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

可以看到在iOS 10_14h後會有一個objc_debug_taggedpointer_obfuscator &= ~_OBJC_TAG_MASK;這個操作,一起看一下objc_debug_taggedpointer_obfuscator這個操作具體是幹了什麼?

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

static inline void * _Nonnull
_objc_encodeTaggedPointer(uintptr_t ptr)
{
    return (void *)(objc_debug_taggedpointer_obfuscator ^ ptr);
}

可以看出對地址進行了一次異或操作,可以看出,在編碼和解碼過程中都是進行了異或objc_debug_taggedpointer_obfuscator這個值,這個應該都知道,一個數異或同一個數兩次,結果還是那個數,常見的面試題,a,和b值進行變換,不加入第三方參數方式:

   a ^ a ^ b = b;
   a ^ b ^ a = b;
   b ^ a ^ a = b;

這種操作就可以將a,和b的值進行變換;

我們來對地址進行解碼操作:

//因爲外部沒有這個值,所以我們將這個加入extern外部引用
extern uintptr_t objc_debug_taggedpointer_obfuscator;
//添加方法
uintptr_t
_objc_decodeTaggedPointer_(id ptr)
{
    return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
}
- (void)taggentPointer
{
    // 優化 - 編譯讀取的時候 更直接
    // retian release
    // 3倍
    // 創建 100
    // [8-10]
    
    
    NSString *str1 = [NSString stringWithFormat:@"a"];
    NSString *str2 = [NSString stringWithFormat:@"ab"];
    NSString *str3 = [NSString stringWithFormat:@"abc"];
    NSString *str4 = [NSString stringWithFormat:@"abcd"];
    NSString *str5 = [NSString stringWithFormat:@"abcde"];
    NSString *str6 = [NSString stringWithFormat:@"abcdef"];

    NSLog(@"%@--%p-%@--0x%lx",object_getClass(str1),str1,str1,_objc_decodeTaggedPointer_(str1));
    NSLog(@"%@--%p-%@--0x%lx",object_getClass(str2),str2,str2,_objc_decodeTaggedPointer_(str2));
    NSLog(@"%@--%p-%@--0x%lx",object_getClass(str3),str3,str3,_objc_decodeTaggedPointer_(str3));
    NSLog(@"%@--%p-%@--0x%lx",object_getClass(str4),str4,str4,_objc_decodeTaggedPointer_(str4));
    NSLog(@"%@--%p-%@--0x%lx",object_getClass(str5),str5,str5,_objc_decodeTaggedPointer_(str5));
    NSLog(@"%@--%p-%@--0x%lx",object_getClass(str6),str6,str6,_objc_decodeTaggedPointer_(str6));
  
    NSNumber *number1 = @1;
    NSNumber *number2 = @1;
    NSNumber *number3 = @2.0;
    NSNumber *number4 = @3.2;
   
    NSLog(@"%@-%p-%@ - 0x%lx",object_getClass(number1),number1,number1,_objc_decodeTaggedPointer_(number1));
    NSLog(@"%@-%p-%@ - 0x%lx",object_getClass(number2),number2,number2,_objc_decodeTaggedPointer_(number2));
    NSLog(@"%@-%p-%@ - 0x%lx",object_getClass(number3),number3,number3,_objc_decodeTaggedPointer_(number3));
    NSLog(@"%@-%p-%@ - 0x%lx",object_getClass(number4),number4,number4,_objc_decodeTaggedPointer_(number4));   
}

看一下運行結果:


首先我們來看一下字符串str1的地址

0xa000000000000611    其中 首位的a 表示爲字符串   61  標示ASCII 中的a,最後面的1 標示字符串有幾位

看一下number1 的地址:

0xb000000000000012    其中b標示是一個數值int 類型,  後面的  1 表示值, 最後面的2  標示是int 型;

根據測試可以得出number 類型最後一位

0表示char類型,1表示short類型,2表示整形,3表示長整型,4表示單精度類型,5表示雙精度類型.

具體情況我們來看一下蘋果大大對tagged point的介紹吧


/***********************************************************************
* Tagged pointer objects.
*
* Tagged pointer objects store the class and the object value in the 
* object pointer; the "pointer" does not actually point to anything.
* 
* Tagged pointer objects currently use this representation:
* (LSB)
*  1 bit   set if tagged, clear if ordinary object pointer
*  3 bits  tag index
* 60 bits  payload
* (MSB)
* The tag index defines the object's class. 
* The payload format is defined by the object's class.
*
* If the tag index is 0b111, the tagged pointer object uses an 
* "extended" representation, allowing more classes but with smaller payloads:
* (LSB)
*  1 bit   set if tagged, clear if ordinary object pointer
*  3 bits  0b111
*  8 bits  extended tag index
* 52 bits  payload
* (MSB)
*
* Some architectures reverse the MSB and LSB in these representations.
*
* This representation is subject to change. Representation-agnostic SPI is:
* objc-internal.h for class implementers.
* objc-gdb.h for debuggers.
**********************************************************************/

從apple給出的聲明中,可以得到:

(1) 標籤指針對象存儲了類信息和對象實際的值,此時的指針不指向任何東西;

(2) 使用最低位作爲標記位,如果是標籤指針對象就標記爲1,如果是普通對象類型就標記爲0;

(3) 緊接着三位是標籤索引位;

(4) 剩餘的60位位有效的負載位,標籤索引位定義了標籤對象代表的對象的真實類型,負載的格式由實際的類定義;

(5) 如果標籤位是0b111,表示該對象使用了是被擴展的標籤對象,這種擴展的方式可以運訓更多的類使用標籤對象來表示,同時負載的有效位數變小。這時:

      5.1 最低位是標記位;

      5.2 緊接着三位位0b111;

      5.3 緊接着八位位擴展的標記位;

      5.4 剩餘的52位纔是真正的有效的負載位。

(6) 並不是所有的架構中都使用低位做標記位.在指令集框架中,除了64-bit的Mac操作系統之外,其餘全是使用MSB。比如iOS就是使用高位作爲標誌位,而在接下來的討論中,僅以iOS使用高位作爲標記位的實現策略來討論標籤指針。

 在iOS中,使用最高位作爲標籤指針標誌位,接下來的三位來作爲類標記位,且當這三位爲111的時候標籤指針就變成了擴展的標籤指針,所以普通的標籤指針最多能表示7(000-110)種對象類型,那這些對象類型都是那些呢?

enum
#endif
{
    // 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
};

從上邊的定義中,我們可以看到最開頭的七種枚舉就是最原始的標籤指針對象所能代表的六個類原型。而我們比較常見的就是2(NSString), 3(NSNumber), 4(NSIndexPath)和6(NSDate)這四種類型;

因爲TaggedPointer 有一部分代碼並沒有開源,所以不能在進行更深入的探索

MRC&ARC

ARC 爲系統自動管理,但是我們還是需要了解MRC 中的一些東西的,MRC 中主要就是alloc  retain ,release , retainCount  autorelease  ,dealloc 主要就是這幾個方面;

alloc

 我之前有一篇文章講述了alloc 的流程iOS底層探索二(OC 中 alloc 方法 初探),有興趣的可以去看看,這裏需要注意的是,在alloc的過程中並沒有對retainCount 進行處理

retain

我們直接看retain 源碼

id 
objc_retain(id obj)
{
    if (!obj) return obj;
    if (obj->isTaggedPointer()) return obj;
    return obj->retain();
}

判讀是不是TaggedPointer類型,是直接retain,前面文章也說了是TaggedPointer類型就不盡興retain處理,然後直接進入retain方法;

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

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

    return ((id(*)(objc_object *, SEL))objc_msgSend)(this, SEL_retain);
}
ALWAYS_INLINE id 
objc_object::rootRetain()
{
    return rootRetain(false, false);
}

判斷是不是快速,一般情況都是快速並調用rootRetain 方法:

ALWAYS_INLINE id 
objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
//    容錯處理
    if (isTaggedPointer()) return (id)this;

    bool sideTableLocked = false;
    bool transcribeToSideTable = false;

    isa_t oldisa;
    isa_t newisa;

//    處理引用計數
//    ISA 中有講iretaincount 是存在ISA 中
    do {
        transcribeToSideTable = false;
        oldisa = LoadExclusive(&isa.bits);
        newisa = oldisa;
//        散列表引用計數進行處理 這裏對非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();
        }
        // don't check newisa.fast_rr; we already called any RR overrides
//        判斷是否正在進行析構m,是的話就不處理了
        if (slowpath(tryRetain && newisa.deallocating)) {
            ClearExclusive(&isa.bits);
            if (!tryRetain && sideTableLocked) sidetable_unlock();
            return nil;
        }
        uintptr_t carry;
/*
 pointer isa 呢 處理extra_rc引用計數RC_ONE這個值模擬器和真機不一樣
 uintptr_t extra_rc          : 8   模擬器8位
 uintptr_t extra_rc          : 7   真機7位
 extra_rc  當表示該對象的引用計數值,實際上是引用計數值減 1, 例如,如果對象的引用計數爲 10,那麼 extra_rc 爲 9。如果引用計數大於 10, 則需要使用到下面的 has_sidetable_rc。
 */
        newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc++
//     如果超值了,就進行散列表中添加
        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;
//            一半在散列表,一半在isa中的extra_rc
            newisa.extra_rc = RC_HALF;
            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_addExtraRC_nolock(RC_HALF);
    }

    if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock();
    return (id)this;
}

代碼中加了詳細的註釋,這裏大概總結一下retain:

    1. nopointer  會對 isa 進行引用計數表處理

    2. 然後對散列表處理,先lock  然後對引用計數表和弱引用表處理

    3. 非nopointer的isa 會對isa中的extraRC 進行處理addc

        3.1 如果 extraRC滿了(carry)  將滿數量的一半存入extraRC  (優先操作ISA ,因爲快)

        3.2 另一半存入 引用計數表中 (操作這個表會慢一點)

release

先看下源碼:

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

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

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

    ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_release);
}
ALWAYS_INLINE bool 
objc_object::rootRelease()
{
    return rootRelease(true, false);
}

可以看出前面和retain基本一樣;

ALWAYS_INLINE bool 
objc_object::rootRelease(bool performDealloc, bool handleUnderflow)
{
    if (isTaggedPointer()) return false;

    bool sideTableLocked = false;

    isa_t oldisa;
    isa_t newisa;

 retry:
    do {
        oldisa = LoadExclusive(&isa.bits);
        newisa = oldisa;
//nonpointer  -->散列表進行處理
        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;
//        進行減操作
        newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc--
//        如果isa 中的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;
//  對三列表進行操作
    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;
}

可以看出release 和 retain操作方式基本一樣,這裏在進行引用計數變化的時候,優先會對isa 中的進行減操作,減完了之後纔會查詢散列表中有沒有,

還有就是在release 最後會使用objc_msgSend 調用delloc 方法

retainCount :ios 中的對象引用計數

首先我們來看一下下面代碼:

 NSObject *objc = [NSObject alloc];
 NSLog(@"objc------%ld",CFGetRetainCount((__bridge CFTypeRef)objc));

 運行結果的retaincount 是什麼呢?


這就很奇怪了,對於alloc 的過程我們是很熟悉的:alloc的流程iOS底層探索二(OC 中 alloc 方法 初探),在這裏面並沒有對於retainCount 進行處理,但是這裏爲什麼輸出結果是1呢?

我們就需要看一下CFGetRetainCount 的源碼了

uintptr_t
_objc_rootRetainCount(id obj)
{
    assert(obj);

    return obj->rootRetainCount();
}
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) {
//bits.extra_rc;  isa中存儲retainCount
        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();
}

可以看到,因爲我們知道 alloc 後isa中沒有進行retainCount 處理所以 rc = 1+ bits.extra_rc (這個值爲0)所以結果爲0;所以默認給了一個值爲1,

可以看到運行程序可以看到這個值確實爲 空;

delloc

根據上面release 源碼,我們可看出,在最後會調用delloc 方法,來看下delloc 源碼:

void
_objc_rootDealloc(id obj)
{
    ASSERT(obj);

    obj->rootDealloc();
}
inline void
objc_object::rootDealloc()
{
    if (isTaggedPointer()) return;  // fixme necessary?
 //判斷是否是nonpointer isa 直接釋放
    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);
    }
}
id 
object_dispose(id obj)
{
    if (!obj) return nil;
//   釋放這個對象的關聯對象和weak 處理,CXX 處理,散列表清空等
    objc_destructInstance(obj);    
    free(obj);

    return nil;
}

這個底層還有對散列表,weak表,以及引用計數表等進行清空,下面是delloc的流程圖

  總結

這篇文章主要講述了內存管理的一部分知識,內存的分區,內存佈局,內存管理方案,ARC&MRC ,引用計數下篇文章在堆弱引用,和自動釋放池來進行詳細介紹。

歡迎大家點贊,關注我的CSDN,我會定期做一些技術分享!未完待續。。。 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章