iOS Objective-C底層 part3:live^ARC

1. 概念

首先,ARC和GC是兩碼事,ARC是編譯時(shí)編譯器“幫我們”插入了原本需要我們自己手寫的內(nèi)存管理代碼,而非像GC一樣運(yùn)行時(shí)的垃圾回收系統(tǒng).
ARC并不是在OC內(nèi)建立一個(gè)垃圾回收系統(tǒng),內(nèi)存仍然由代碼進(jìn)行管理,
只是由MRC的人工手動(dòng)添加內(nèi)存管理代碼,變?yōu)榫幾g器在編譯時(shí)自動(dòng)分析插入.

2. 方法名約定

對(duì)于類的不同方法,編譯器會(huì)對(duì)調(diào)用方法內(nèi)外插入不同的內(nèi)存管理代碼.那么這就需要對(duì)方法名進(jìn)行規(guī)定,以方便編譯器對(duì)調(diào)用方法內(nèi)外插入不同的內(nèi)存管理代碼.

+ (instancetype)sarkWithMark:(NSString *)mark //NS_RETURNS_NOT_RETAINED;
- (instancetype)initWithMark:(NSString *)mark //NS_RETURNS_RETAINED;
- (__strong const char *)UTF8String; //NS_RETURNS_INNER_POINTER;
#define NS_RETURNS_RETAINED __attribute__((ns_returns_retained))
#define NS_RETURNS_NOT_RETAINED __attribute__((ns_returns_not_retained))
#define NS_RETURNS_INNER_POINTER __attribute__((objc_returns_inner_pointer))
  • NS_RETURNS_RETAINED

initinitWithMark都屬于init的家族方法.
對(duì)于以alloc,init,copy,mutableCopy,new開頭的家族的方法,后面默認(rèn)加NS_RETURNS_RETAINED標(biāo)識(shí).ARC在會(huì)在調(diào)用方法外圍要加上內(nèi)存管理代碼:retain+release

  • NS_RETURNS_NOT_RETAINED

sarkWithMark方法,則是不帶alloc,init,copy,mutableCopy,new開頭的方法,默認(rèn)添加NS_RETURNS_NOT_RETAINED標(biāo)識(shí).標(biāo)識(shí)返回的對(duì)象已經(jīng)在方法內(nèi)部做過autorelease了.

在本篇文章的最后一部分會(huì)說autorelease的優(yōu)化,這里可以暫且理解方法內(nèi)部將對(duì)象做了autorelease

  • NS_RETURNS_INNER_POINTER

這個(gè)只是做返回純C語(yǔ)言的指針變量,ARC外圍不必做內(nèi)存管理的操作.

  • 違背約定范例:
@property (nonatomic, copy) NSString *newString; //編譯器不允許

這屬性的get方法會(huì)被當(dāng)做new的家族方法,
ARC在外圍添加內(nèi)存管理代碼的時(shí)候會(huì)加上retain+release,從而導(dǎo)致內(nèi)存管理錯(cuò)誤

3. 方法調(diào)用

正常書寫OC代碼時(shí),我們采取[target action]的方式調(diào)用方法,但在消息轉(zhuǎn)發(fā)或者其他runtime參與的方法調(diào)用時(shí),我們會(huì)用其他的書寫方式來實(shí)現(xiàn)[target action]的一樣的功能.姑且稱[target action]明文調(diào)用,其他的書寫方式為非明文調(diào)用.(NSInvocation的使用就是最好的非明文調(diào)用,接下來的例子也會(huì)用NSInvocation來示范)

3.1 錯(cuò)誤范例

ARC有效的前提就是明文調(diào)用.以下是幾個(gè)錯(cuò)誤示范的例子

  • 非明文調(diào)用范例1:非明文調(diào)用工廠方法
- (void)nomalFunc{
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[NSMethodSignature signatureWithObjCTypes:"@@:"]];
    invocation.target = self;
    invocation.selector = @selector(createDic);
    [invocation invoke];
    
    //error handle 崩潰
    Son * son;
    [invocation getReturnValue:&son];
    
    //right handle
//    Son * son;
//    void *result;
//    [invocation getReturnValue:&result];
//    son = (__bridge id)result;
}

-(Son *)createDic{
    return [Son new];
}
  • 非明文調(diào)用范例2:非明文調(diào)用new家族方法
- (void)famliyFunc{
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[NSMethodSignature signatureWithObjCTypes:"@@:"]];
    invocation.target = self;
    invocation.selector = @selector(newDic);
    [invocation invoke];
    
    //error handle 不釋放
    Son * son;
    void *result;
    [invocation getReturnValue:&result];
    son = (__bridge id)result;
    
    //right handle
//    Son * son;
//    [invocation getReturnValue:&son];
}
-(Son *)newDic{
    return [Son new];
}

兩個(gè)例子對(duì)比起來看很奇怪吧!

Son * son;
[invocation getReturnValue:&son];

非明文調(diào)用new家族方法后,用以上代碼取出返回值==>完全沒有問題;
非明文調(diào)用工廠方法后,用以上代碼取出返回值==>崩潰;

3.2 why

如果是明文調(diào)用,編譯器會(huì)非常智能的為我們加上內(nèi)存管理的代碼.

  • 明文調(diào)用范例1
- (void)nomalFunc{
Son * son = [self createObj];
// [son retain];
...
// [son release];
}

反觀錯(cuò)誤非明文調(diào)用范例1中我們沒有對(duì)son進(jìn)行顯式的賦值,而是傳入getReturnValue:方法中去獲取返回值,這樣的賦值后 ARC 沒有自動(dòng)給這個(gè)變量插入retain語(yǔ)句,但退出作用域時(shí)還是自動(dòng)插入了release語(yǔ)句,導(dǎo)致這個(gè)變量多釋放了一次,導(dǎo)致crash.

  • 明文調(diào)用范例2
- (void)familyFunc{
Son * son = [self newObj];
...
// [son release];
}

反觀錯(cuò)誤非明文調(diào)用范例2中我們沒有對(duì)son進(jìn)行顯式的賦值,而是傳入getReturnValue:方法中去獲取返回值,這樣的賦值后 ARC 沒有自動(dòng)給這個(gè)變量插入retain語(yǔ)句,退出作用域時(shí)還是自動(dòng)插入了release語(yǔ)句,這流程正和明文調(diào)用范例2的內(nèi)存管理邏輯一模一樣,所以非明文調(diào)用new家族方法妥妥執(zhí)行了.

總結(jié):非明文調(diào)用方法時(shí),ARC添加內(nèi)存管理的代碼沒那么智能,都走一個(gè)流程[指向返回值不做retain,退出作用域調(diào)用release].
同樣的流程,不同的命運(yùn):
非明文調(diào)用new家族方法==>相安無事;

非明文調(diào)用工廠方法==>雞犬不寧;

3.3 bridge挽回一程

__bridge 源在哪端,哪端消除

(__bridge T) op:告訴編譯器在 bridge 的時(shí)候不要多做任何事情

// objc to cf
NSString *nsStr = [self createSomeNSString];
CFStringRef cfStr = (__bridge CFStringRef)nsStr;
CFUseCFString(cfStr);
// CFRelease(cfStr); 不需要
//源在Objc端 編譯器消除
// cf to objc
CFStringRef hello = CFStringCreateWithCString(kCFAllocatorDefault, "hello", kCFStringEncodingUTF8);
NSString *world = (__bridge NSString *)(hello);
CFRelease(hello); // 需要
[self useNSString:world];
//源在CF端 猿消除

我們?cè)倏创a:

- (void)nomalFunc{
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[NSMethodSignature signatureWithObjCTypes:"@@:"]];
    invocation.target = self;
    invocation.selector = @selector(createDic);
    [invocation invoke];
    
    //right handle
    Son * son;
    void *result;
    [invocation getReturnValue:&result];
    son = (__bridge id)result;
}

-(Son *)createDic{
    return [Son new];
}

源在Objc端,
先由非Objc指針做指向,
son = (__bridge id)result;完成對(duì)對(duì)象的retain
ARC在退出作用域時(shí)加上release.妥了

__bridge是Toll-Free Bridging的內(nèi)容,這里只說了一點(diǎn)點(diǎn),延伸閱讀請(qǐng)戳.

聲明:非明文調(diào)用在開發(fā)中真的不常用到,我對(duì)ARC在非明文調(diào)用中失效的認(rèn)識(shí)也是來自對(duì)'JSPatch的原理'的解讀.證明1->非明文調(diào)用真的不常用到,證明2->不常用到的,你知道+你會(huì)用+你用的好=牛

4. 運(yùn)行時(shí)優(yōu)化(Thread Local Storage)

在ARC與MRC的環(huán)境下都有autoreleasepool,主要作用就是延時(shí)銷毀.
對(duì)象調(diào)用autorelease就會(huì)被放入autoreleasepool.

//MRC
+ (instancetype)createObj{
id any = [[PGCustomClass alloc]init];
return [any autorelease];
}

PGCustomClass * obj = [PGCustomClass createObj];

類方法+createObj對(duì)返回的對(duì)象調(diào)用了autorelease,使對(duì)象可以保存在autoreleasepool內(nèi),從而順利返回,而不會(huì)在方法結(jié)束時(shí)被銷毀.

可以想見將對(duì)象放入autoreleasepool和從autoreleasepool內(nèi)移除對(duì)象也是有開銷的.

ARC中對(duì)autorelease進(jìn)行了優(yōu)化,代碼如下:

//ARC
+ (instancetype)createObj{
    id tmp = [[self alloc]init];
    return objc_autoreleaseReturnValue(tmp);
}

id tmp = objc_retainAutoreleasedReturnValue([PGCustomClass createObj]);
PGCustomClass * obj = tmp;
objc_storeStrong(&obj, nil);//就是release
id 
objc_autoreleaseReturnValue(id obj)
{
    if (prepareOptimizedReturn(ReturnAtPlus1)) return obj;

    return objc_autorelease(obj);
}
id
objc_retainAutoreleasedReturnValue(id obj)
{
    if (acceptOptimizedReturn() == ReturnAtPlus1) return obj;

    return objc_retain(obj);
}

工廠方法內(nèi)由objc_autoreleaseReturnValue將對(duì)象放入Thread Local Storage;
工廠方法內(nèi)由objc_retainAutoreleasedReturnValue將對(duì)象由Thread Local Storage取出.
簡(jiǎn)單的說就是中轉(zhuǎn)不走autoreleasepoolThread Local Storage代勞,這樣autoreleasepool對(duì)對(duì)象的存儲(chǔ)和清除的開銷就沒有了.

當(dāng)然走優(yōu)化路徑是有要求的:工廠方法的調(diào)用方與被調(diào)用方都支持ARC,因?yàn)橹挥羞@樣方法內(nèi)的objc_autoreleaseReturnValueobjc_retainAutoreleasedReturnValue才會(huì)配套使用.很多系統(tǒng)庫(kù)還可能是MRC實(shí)現(xiàn)的,這樣的系統(tǒng)類調(diào)用工廠方法生成的對(duì)象還是得進(jìn)autoreleasepool.

那也就是說ARC下只要調(diào)用方和被調(diào)方都用ARC編譯時(shí),所建立的對(duì)象都不加入autoreleasepool.更簡(jiǎn)單的說我們自己寫的類,調(diào)用工廠方法生成對(duì)象都不會(huì)放
autoreleasepool.(我的三觀有點(diǎn)被毀,所以著重強(qiáng)調(diào)一下)

《OC高級(jí)編程》+《Effective Objective-C 2.0》都有關(guān)于Thread Local Storage的內(nèi)容,但沒有強(qiáng)調(diào)調(diào)用方被調(diào)用方必須都是ARC的情況下Thread Local Storage才會(huì)起作用.外加天天看到的autoreleasepool這觀念和自己寫的類生成的對(duì)象沒有關(guān)系,我有點(diǎn)沒想通,所以反反復(fù)復(fù)的和sunny大神確認(rèn),在這也謝謝sunny大神.

與sunny大神在微博的上的問答記錄


文章參考:
objc arc的簡(jiǎn)單探索

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容