iOS 內(nèi)存管理、查找循環(huán)引用

內(nèi)存管理

1、ARC 下的內(nèi)存管理

循環(huán)引用

如圖,就是循環(huán)引用的情況,A、B 互相引用無(wú)法釋放。


循環(huán)引用內(nèi)存圖

造成循環(huán)引用的情況

1、代理

代理都是用弱指針,以避免循環(huán)引用。

2、block

block 可以捕獲外接的變量,控制器中的 block 引用 self ,block 的結(jié)構(gòu)體將有一個(gè)強(qiáng)指針指向 self(控制器)(如下面代碼),如果 block 是個(gè)局部變量,block 會(huì)很快釋放不會(huì)產(chǎn)生循環(huán)引用,如果 block 是控制器的一個(gè)屬性,則會(huì)出現(xiàn)循環(huán)引用。

struct __TestBlockStructViewController__testBlockCaptureStackVariable_block_impl_0 {
  struct __block_impl impl;
  struct __TestBlockStructViewController__testBlockCaptureStackVariable_block_desc_0* Desc;
  TestBlockStructViewController *const __strong self; // 強(qiáng)指針
    
  __TestBlockStructViewController__testBlockCaptureStackVariable_block_impl_0
    (void *fp,
     struct __TestBlockStructViewController__testBlockCaptureStackVariable_block_desc_0 *desc,
     TestBlockStructViewController *const __strong _self,
     int flags=0) : self(_self) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

有一個(gè)常見(jiàn)的循環(huán)引用場(chǎng)景是,Controller 發(fā)送網(wǎng)絡(luò)請(qǐng)求,網(wǎng)絡(luò)回調(diào) block 強(qiáng)引用 當(dāng)前 Controller。在網(wǎng)速比較慢的情況下,網(wǎng)絡(luò)請(qǐng)求可能持續(xù)幾十秒(建議設(shè)置網(wǎng)絡(luò)超時(shí)不超過(guò) 10s),在這段時(shí)間內(nèi)將有循環(huán)引用。
例如:

[NetReq getBookInfoWithShopId:@"3772" bookId:@"526377307" success:^(NSDictionary *data) {
    self.arrayModel = .... // 網(wǎng)絡(luò)返回前,一直強(qiáng)引用 self
} fail:^(NSError *error) {
}];

解決block 的循環(huán)引用

  • block 外定義弱指針,block 內(nèi)部定義強(qiáng)指針,網(wǎng)絡(luò)請(qǐng)求的 block 可以向下面這樣處理。
__weak typeof(self) ws = self;
self.block = ^{
    typeof(ws) strongSelf = ws;
    NSLog(@"%@", strongSelf);
};
  • 控制器作為 block 的參數(shù)
self.block = ^(UIViewController *vc) {
    NSLog(@"%@", vc);
};
3、NSTimer
[NSTimer timerWithTimeInterval:1 target:self selector:@selector(timerHandleMethod) userInfo:nil repeats:YES];

使用timer時(shí),timer 強(qiáng)引用 target,把 timer 添加到 runloop 中后,runloop 持有timer,此時(shí)便形成了一個(gè)強(qiáng)引用鏈:Runloop -> NSTimer -> Target。所以在控制器中使用timer ,界面退出時(shí)一定要調(diào)用 invalidate 方法

- (void)viewDidDisappear:(BOOL)animated {
    [super viewDidDisappear:animated];
    
    if (self.isBeingDismissed || self.isMovingFromParentViewController) {
        [self.timer invalidate];
    }
}

Autorelease Pool

Autorelase Pool 提供了向一個(gè)對(duì)象延遲發(fā)送release消息的機(jī)制。在 ARC 下,我們并不需要手動(dòng)調(diào)用 autorelease 有關(guān)的方法,甚至可以完全不知道 autorelease 的存在,就可以正確管理好內(nèi)存。因?yàn)?Cocoa Touch 的 Runloop 中,每個(gè) runloop circle 中系統(tǒng)都自動(dòng)加入了 Autorelease Pool 的創(chuàng)建和釋放。

當(dāng)我們需要?jiǎng)?chuàng)建和銷毀大量的對(duì)象時(shí),使用手動(dòng)創(chuàng)建的 autoreleasepool 可以有效的避免內(nèi)存峰值的出現(xiàn)。因?yàn)槿绻皇謩?dòng)創(chuàng)建的話,外層系統(tǒng)創(chuàng)建的 pool 會(huì)在整個(gè) runloop circle 結(jié)束之后才進(jìn)行 drain,手動(dòng)創(chuàng)建的話,會(huì)在 block 結(jié)束之后就進(jìn)行 drain 操作。一個(gè)普遍被使用的例子如下:

for (int i = 0; i < 100000000; i++)
{
    @autoreleasepool
    {
        NSString* string = @"ab c";
        NSArray* array = [string componentsSeparatedByString:string];
    }
}

如果不使用 autoreleasepool ,需要在循環(huán)結(jié)束之后釋放 100000000 個(gè)字符串,如果 使用的話,則會(huì)在每次循環(huán)結(jié)束的時(shí)候都進(jìn)行 release 操作。

檢測(cè)循環(huán)引用

1、Instruments - Leaks
instruments_Leaks.png

Xcode提供的內(nèi)存分析工具 Leaks 可以檢測(cè)內(nèi)存泄露。先在控制器A里寫一段循環(huán)引用的代碼,由根控制器跳進(jìn) A,退出A 控制器后,A 肯定釋放不了:

@property (copy, nonatomic) dispatch_block_t block;

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    self.block = ^{
        NSLog(@"%@", self);
    };
}

將代碼運(yùn)行在真機(jī)上,在 Leaks 左上角選擇設(shè)備和檢測(cè)的app,點(diǎn)擊 左邊的紅色按鈕啟動(dòng)即可,進(jìn)入A 控制器再退出,反復(fù)多試幾次,可以檢測(cè)到循環(huán)引用

leaks_detack_memroy_cycle.png
2、調(diào)試內(nèi)存圖

從 Xcode 8 開始,Xcode 的 debug 欄提供了查看內(nèi)存圖的功能,可以很方便的查看程序的內(nèi)存情況

memory_graph_button.png

點(diǎn)擊之后app 會(huì)停住,顯示當(dāng)前活著的對(duì)象,上個(gè)demo 里面的內(nèi)存引用圖是這樣的


Retain_cycle_block_ratain.png
3、精準(zhǔn)的內(nèi)存檢測(cè)工具 MLeaksFinder

MLeaksFinder 是一個(gè)在開發(fā)階段可以及時(shí)檢測(cè)內(nèi)存泄露的開源庫(kù),GitHub 地址:MLeaksFinder

使用 pod 'MLeaksFinder 命令安裝后不用其他任何操作,出現(xiàn)循環(huán)引用后,會(huì)自動(dòng)彈出提示框,默認(rèn)只在 debug 下生效,也可以通過(guò) MLeaksFinder.h 里的 //#define MEMORY_LEAKS_FINDER_ENABLED 0 來(lái)手動(dòng)控制開關(guān)。

MLLeakFind_retain_cycle.png

2、CoreFoundation 下的內(nèi)存管理

iOS 開發(fā)中基本都是使用自動(dòng)引用計(jì)數(shù)方式,編程中基本感受不到內(nèi)存管理的存在,但在 CoreFoundation 中是無(wú)法使用自動(dòng)引用計(jì)數(shù),其內(nèi)存管理還需手動(dòng)調(diào)用代碼-CFRetain(CFTypeRef) CFRelease(CFTypeRef)。CoreFoundation 對(duì)象和 Foundation 對(duì)象可以無(wú)縫橋接,但橋接時(shí)需要指定轉(zhuǎn)換后內(nèi)存由誰(shuí)管理,橋接修飾詞-__bridge、 __bridge_retained、 __bridge_transfer

__bridge

只進(jìn)行轉(zhuǎn)換,原轉(zhuǎn)換對(duì)象引用計(jì)數(shù)不變,原對(duì)象需要調(diào)用 CFRelease 釋放內(nèi)存。使用 __bridge 轉(zhuǎn)換安全性會(huì)很低,如果不注意對(duì)象的所有者,會(huì)引發(fā)野指針崩潰。下面來(lái)看對(duì)應(yīng)的等效代碼為:

// ARC
id obj = [NSObject new];
void *p = (__bridge void *)obj;

對(duì)應(yīng)的 MRC 等效代碼為:

/* MRC */
id obj = [NSObject new];
void *p = (__bridge void *)obj;

轉(zhuǎn)換后 p 沒(méi)有 retain 該對(duì)象,obj 釋放后 p 是個(gè)野指針,使用 p 會(huì)出現(xiàn)崩潰,ep:

- (void)testBridge {
    /* MRC */
    id obj = [NSObject new];
    void *p = (__bridge void *)obj;

    [obj release];
    obj = nil;

    NSLog(@"%@", p); // EXC_BAD_ACCESS
}

使用 __bridge轉(zhuǎn)換,p 沒(méi)有持有對(duì)象,[obj release]; 后對(duì)象就釋放了,上面的程序會(huì)出現(xiàn) EXC_BAD_ACCESS 崩潰。

__bridge_retained

轉(zhuǎn)換后的指針也持有所賦值的對(duì)象??吹刃Тa:

// ARC
id obj= [NSObject new];
void *p = (__bridge_retained void*)obj;

上面是 ARC 下的 __bridge_retained轉(zhuǎn)換,對(duì)應(yīng)的 MRC 等效代碼為:

// MRC
id obj = [NSObject new];
void *p = obj;
[(id)p retain];

轉(zhuǎn)換后的 p 持有對(duì)象,obj 釋放后,p 不會(huì)像 __bridge 出現(xiàn)野指針的情況。

__bridge_transfer

引用計(jì)數(shù)交給 ARC 管理,轉(zhuǎn)換對(duì)象持有的內(nèi)存在轉(zhuǎn)換完成后隨之釋放。

id obj = (__bridge_transfer id)p;

上面代碼對(duì)應(yīng)于 MRC 下的等效代碼:

id obj = (id)p;
[obj retain];
[(id)p release];

驗(yàn)證 __bridge_transfer 執(zhí)行后,原內(nèi)存會(huì)釋放。

/* ARC */
id o = [NSObject new];
void *p = (__bridge void *)o;

id obj = (__bridge_transfer id)p;
NSLog(@"%@", [obj class]);

id obj2 = (__bridge id)p;
NSLog(@"obj2:%@", [obj2 class]); // EXC_BAD_ACCESS

執(zhí)行結(jié)果為:

2019-01-27 12:18:53.820708+0800 testThread[9588:293724] NSObject
2019-01-27 12:18:53.820897+0800 testThread[9588:293724] obj2:NSObject

第一次轉(zhuǎn)換為 obj 后 p 的內(nèi)存就釋放了,隨后轉(zhuǎn)換為 obj 2 遇到了 EXC_BAD_ACCESS 錯(cuò)誤,結(jié)論得以驗(yàn)證。若將 id obj = (__bridge_transfer id)p; 修改成 id obj = (__bridge id)p; 則程序不會(huì)崩潰。

Reference

最后編輯于
?著作權(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ù)。

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