Objective-C記憶體管理

sunshinfight發表於2019-03-16

釋放掉不用的記憶體,保證還可能被使用的記憶體不會被回收。這是記憶體管理要做的的事情,OC是通過引用計數來管理的,MRC和ARC的區分只是:引用計數是由程式設計師還是編譯器和語言來負責管理。

為啥要使用引用計數

在c中堆中的物件是由程式設計師負責的:

// malloc必須和free成對出現
char *str = (char*)malloc(sizeof(char)*10);

// do something

// 如果忘了free就洩漏了10個位元組的記憶體,如果多次free,那將導致崩潰
free(str);

// 如果此時又使用了str,這塊記憶體可能已經分配做其他用途了,所以行為是不確定的
// do not use str

複製程式碼

看起來好像很簡單,但是在do somethind的過程中可能經歷裡很多的,所以說保證這塊記憶體的正確使用完全依靠程式設計師,有很大的風險,而且除錯指標異常也是很麻煩的。

OC中的引用計數

// NSObject 提供的引用計數的功能
- (instancetype)retain OBJC_ARC_UNAVAILABLE;    // rc++
- (oneway void)release OBJC_ARC_UNAVAILABLE;    // rc--,若rc==0,則釋放該例項物件
- (instancetype)autorelease OBJC_ARC_UNAVAILABLE;   // 延遲釋放
複製程式碼

蘋果記憶體管理文件給出了使用引用計數的原則:

  1. 你擁有你建立的任何物件,呼叫命名上為“alloc”, “new”, “copy”, or “mutableCopy”的函式,將獲得一個有所有權的例項物件(You own any object you create)
// 這個NSObject的例項物件屬於obj,也就是obj擁有NSObject的例項物件
NSObject *obj = [[NSObject alloc] init];
複製程式碼
  1. 可以使用retain獲取物件的所有權
// 這個NSObject的例項物件屬於obj,也就是obj擁有NSObject的例項物件
NSObject *obj = [[NSObject alloc] init];
// 通過retain方法,這個物件也屬於obj1了
NSObject *obj1 = [obj retain];
複製程式碼
  1. 當不再需要它時,必須放棄你擁有的物件的所有權
// 這個NSObject的例項物件屬於obj,也就是obj擁有NSObject的例項物件
NSObject *obj = [[NSObject alloc] init];
// 通過retain方法,這個物件也屬於obj1了
NSObject *obj1 = [obj retain];
// 通過release方法,放棄objc1對這個例項物件的所有權
[objc1 release];
複製程式碼
  1. 不能放棄沒有擁有權物件的所有權
// 根據1中的命名規則,str沒有這個NSString例項物件的所有權
NSString *str = [NSString stringWithFormat:@"hello NSString"];

// 所以如下,釋放一個沒有所有權例項物件的所有權是錯誤的
[str release];
複製程式碼

關於autorelease(延時釋放)

//
- (id)myMethod {
    // obj 擁有 MyObject例項物件的所有權,引用計數為1
    MyObject *obj = [[MyObject alloc] init];
    // 如果按照上述蘋果的記憶體管理原則3,你需要在return前release obj,但是如果release了就被回收了
    // 這就用到了autorelease 將物件延時釋放
    return [obj autorelease];
}
複製程式碼

autorelease的實現是要配合autoreleasepool來完成的,AutoreleasePoolPage是由一個雙向連結串列實現的棧,每個節點一個虛頁的大小。autorelease訊息實際是將obj push到棧中,並在pop時進行release。

MRC與ARC

MRC:手動引用計數,需要我們手動控制擁有權,也就是負責傳送retain release autorelease訊息。 ARC:通過分析編譯生成的語法樹,編譯器幫我們自動插入引用計數相關的程式碼。

ARC存在的問題和解決方法

考慮如下程式碼:

// ARC環境下
@interface Person : NSObject <NSMutableCopying, NSCopying>
@property (nonatomic, copy)NSString *name;
@property (nonatomic)Person *partner;
+(instancetype) personWithName: (NSString*)name;
@end

Person *xiaohong = [Person personWithName: @"xiaohong"];
Person *xiaoming = [Person personWithName: @"xiaoming"];
xiaohong.partner = xiaoming;
xiaoming.partner = xiaohong;
複製程式碼

在上述程式碼中,xiaohongxiaoming如果沒有其他的引用,那麼最終他們的引用計數都為1,所以他們都不能被釋放,又因為不能被釋放的原因是xiaohong引用xiaoming導致xiaoming不能被釋放,xiaoming引用xiaohong導致xiaohong不能被釋放,如下圖①,所以稱為迴圈引用

迴圈引用示意圖
迴圈引用示意圖.png

如何解決迴圈引用呢,首先選擇一方放棄對另一方的引用,例如xiaohong.partner = nil;,如上圖②所示,此時沒有物件引用xiaoming,所以釋放,在xiaoming呼叫dealloc時將會放棄對xiaohong的引用,所以xiaohong也能得以正確釋放。

使用weak來解決迴圈引用

在使用weak關鍵字:

// ARC環境下
@interface Person : NSObject <NSMutableCopying, NSCopying>
@property (nonatomic, copy)NSString *name;
@property (nonatomic, weak)Person *partner;
+(instancetype) personWithName: (NSString*)name;
@end

Person *xiaohong = [Person personWithName: @"xiaohong"];
Person *xiaoming = [Person personWithName: @"xiaoming"];
xiaohong.partner = xiaoming;
xiaoming.partner = xiaohong;
複製程式碼

此時的情況如上圖③所示,weak有並不會增加物件的引用計數,所以在xiaohongxiaoming可以順利的被釋放。

其實解決迴圈引用只需要要打破這個引用環就可以,即如圖②所示, 圖③所示只是一種充分非必要條件。

Core Foundation和ARC

ARC是針對於NSObject的一套方法,所以在Core Framework是不適用的,OC中的一些物件與Core Frameword又是toll-bridge(就是可以進行簡單的相互轉換)的,雖然結構可以相互轉換,但是還需要考慮所有權的問題,即誰來負責release的工作。

// 即使是兩種結構是toll-bridge的,我們也不能簡單的類似於(NSString*)的指標強轉,因為還有所有權問題(引用計數)
// cf與NSObject結構相互轉化的關鍵字
__bridge    // NSObject <-> cf 不改變所有權
__bridge_retained   // NSObject -> cf   將所有權從ARC交給CF的引用計數   
__bridge_transfer   // cf -> NSObject   將所有權從CF的引用計數交給ARC

複製程式碼

在實際使用的過程中,無非兩種情況,記憶體管理最終交由誰處理呢?

1. 最終由ARC負責記憶體管理

// case 1 這應該是最常用的方式
    NSString *str = [[NSString alloc]initWithFormat:@"test"];  
    CFStringRef cfStr = (__bridge CFStringRef)str; // 不改變所有權,類似於(CFStringRef)str
    
// case 2 
    CFStringRef cfName = ABRecordCopyValue(person, kABPersonFirstNameProperty);
    NSString *name = (__bridge_transfer NSString*)cfName;   // 將所有權從CF的引用計數交給ARC
    
// case 3   下面實際是發生了所有權變化,但經過步驟1,2最終又將所有權交給了ARC
    NSString *str = [[NSString alloc]initWithFormat:@"test"];  
    CFStringRef cfStr = (__bridge_retained CFStringRef)str; // 1. 將所有權從ARC交給CF的引用計數
    str = (__bridge_transfer NSString*)cfstr;  // 2. 將所有權從CF的引用計數交給ARC
複製程式碼

2. 最終由CF的引用計數負責

    NSString *str = [[NSString alloc]initWithFormat:@"test"];  
    CFStringRef cfStr = (__bridge_retained CFStringRef)str; // 將所有權從ARC交給CF的引用計數
    CFRelease(cfStr); // 由CF的記憶體管理負責回收
複製程式碼

由objc associated object 引發的迴圈引用

在category增加property時我們很有可能會使用associated object

// 獲取與self associal 的object
objc_getAssociatedObject(self, &key);

// 設定與self associal 的object
objc_setAssociatedObject(self, &key, retainAssocialValueInCategory, OBJC_ASSOCIATION_RETAIN_NONATOMIC);


// associal的object與self的四種關係
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
    OBJC_ASSOCIATION_ASSIGN = 0,           /**< Specifies a weak reference to the associated object. */
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object. 
                                            *   The association is not made atomically. */
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,   /**< Specifies that the associated object is copied. 
                                            *   The association is not made atomically. */
    OBJC_ASSOCIATION_RETAIN = 01401,       /**< Specifies a strong reference to the associated object.
                                            *   The association is made atomically. */
    OBJC_ASSOCIATION_COPY = 01403          /**< Specifies that the associated object is copied.
                                            *   The association is made atomically. */
};

複製程式碼

如果有如下程式碼:

@interface Person: NSObject 

@property NSString *name;
@property Person *partener;

@end

@implementation Person
static char partenerAssocialKey;
- (void)setPartener: (Person*)partener {
    objc_setAssociatedObject(self, &partenerAssocialKey, partener, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (Person*)partener {
    return objc_getAssociatedObject(self, &partenerAssocialKey);
}

@end

Person *xiaoming = [[Person alloc] init];
Person *xiaohong = [[Person alloc] init];
xiaoming.name = @"xiaoming";
xiaohong.name = @"xiaohong";

// 引用迴圈,由associal object 引起
xiaoming.partener = xiaohong;
xiaohong.partener = xiaoming;
複製程式碼

解決方案也比較簡單

// 加一層包裝
@interface CD_weakAssociatedObject : NSObject
@property (weak) id value;
@end

@implementation CD_weakAssociatedObject

@end

// 實現非原子的set associate方法
void cd_setWeakAssociatedObject(id _Nonnull object, id _Nullable value, const void* _Nonnull key) {
    CD_weakAssociatedObject *obj = objc_getAssociatedObject(object, key);
    if (!obj) {
        obj = [[CD_weakAssociatedObject alloc] init];
        objc_setAssociatedObject(object, key, obj, OBJC_ASSOCIATION_RETAIN);
    }
    obj.value = value;
}

// get
id _Nonnull cd_getWeakAssociatedObject(id _Nonnull object, const void* _Nonnull key) {
    CD_weakAssociatedObject *obj = objc_getAssociatedObject(object, key);
    if (obj) {
        return obj.value;
    }
    return nil;
}
複製程式碼

通過新增對associated object的一層包裝,實現指向associated object的弱引用

相關文章