自從蘋果在objc中添加Block功能支持以后已經過了很久。目前網上對於Block的使用有很多介紹。不過對於Block的內存管理問題,則是眾說紛紜。再加上objc開始使用ARC以后,對於Block的內存管理又有了新的變化。因此在本文中筆者將根據自己的理解梳理一下Block的內存管理問題。
1.Block簡單原理
首先Block的原理要說起來還是挺簡單的,就是將一個函數本身當成參數進行傳遞。而Block的優勢就在於它不止可以訪問自己函數作用域內的數據,它也可以訪問自己作用域范圍外的數據。當然,這也是Block內存管理出現困擾的源頭。
當然,即使Block的內存管理需要特別關注。但是從工程框架來說,Block確實有存在的必要。比如在使用Block之前,當我們在一個對象(A)中需要另一個對象(B)給出解決方案的時候,我們通常會用代理的方式在A中將需要的參數傳遞給B,然后等待B提供的解決方案處理完以后再繼續后續操作。
ObjA.h @protocol ObjADelegate <NSObject> - (NSInteger)doSomething:(NSInteger)value; @end @interface ObjA { __weak id<ObjADelegate> _delegate; } @property (nonatomic, weak) id<ObjADelegate> delegate; @end ObjA.m @implement ObjA @synthesize delegate = _delegate; - (void)function { NSInteger value = 100; if ([_delegate respondsToSelector:@selector(doSomethings:)]) { value = [_delegate doSomethings:value]; } NSLog(@"value: %zd", value); } @end ObjB.h @interface ObjB <ObjADelegate> @end ObjB.m @implement ObjB #pragma mark - ObjADelegate - (NSInteger)doSomething:(NSInteger)value { return value + 100; } @end
事實上在僅僅只有一個代理的時候,Block並不見得比代理方便。但是當一個對象成為了多個代理的實現對象的時候,就會使得這個對象的代碼變的非常臃腫,也很難以管理。比如下面這個類,光是頭文件就能把人看暈了:
@interface MKModelPagesViewController : UIViewController <UIScrollViewDelegate, MKPageViewDelegate, MKModelAddPageViewDelegate, MKModelPreviewDelegate, MKProductInfoDelegate, MKPagePhotoEditBarDelegate, MKPageThemeListViewDelegate, MKModelFilterViewNewDelegate, MKPhotoSaveViewDelegate>
在有多個代理的情況下,使用Block方式就可以使得代碼不再那么臃腫:
ObjA.h @interface ObjA @property (copy, nonatomic) NSInteger (^doSomethings)(NSInteger value); @end ObjA.m @implement ObjA - (void)function { NSInteger value = 100; if (self.doSomethings) { value = self.doSomethings(value); } NSLog(@"value: %zd", value); } @end ObjB.h @interface ObjB @end ObjB.m @implement ObjB - (void)anotherFunction { ObjA* a = [ObjA new]; a.doSomethings = ^(NSInteger value) { return value + 100; }; } @end
可以看到,使用Block以后代碼的結構比使用代理時候要更清晰。當然考慮到根據項目復雜程度,對象之間的通信頻率的高低,我們可以按照自己的喜好選擇使用Block還是代理。
2.Block內存管理
在蘋果使用ARC管理之前,Block的內存管理需要區分是Global(全局)、Stack(棧)還是Heap(堆)。而在使用了ARC之后,蘋果自動會將所有原本應該放在棧中的Block全部放到堆中,所以這使得我們現在的討論可以省去很大一部分的麻煩。下面我們就只討論ARC環境下全局Block和堆Block的內存管理。
首先,全局的Block比較簡單,一句話就可以講完:凡是沒有引用到Block作用域外面的參數的Block都會放到全局內存塊中,在全局內存塊的Block不用考慮內存管理問題。(放在全局內存塊是為了在之后再次調用該Block時能快速反應,當然沒有調用外部參數的Block根本不會出現內存管理問題)。
所以Block的內存管理出現問題的,絕大部分都是在堆內存中的Block出現了問題。實際上屬於Block特有的內存管理問題就只有一個:循環引用。
循環引用
Block的循環引用是比較容易被忽視,原本也是相對比較難檢查出來的問題。當然現在蘋果在XCode編譯的層級就已經做了循環引用的檢查,所以這個問題的檢查就突然變的沒有難度了。
簡單說一下循環引用出現的原理:Block的擁有者在Block作用域內部又引用了自己,因此導致了Block的擁有者永遠無法釋放內存,就出現了循環引用的內存泄漏。下面舉個例子說明一下:
@interface ObjTest () { NSInteger testValue; } @property (copy, nonatomic) void (^block)(); @end @implement ObjTest - (void)function { self.block = ^() { self.testValue = 100; }; } @end
在這個例子中,ObjTest擁有了一個名字叫block的Block對象;然后在這個Block中,又對ObjTest的一個成員變量testValue進行了賦值。於是就產生了循環引用:ObjTest->block->ObjTest。
要避免循環引用的關鍵就在於破壞這個閉合的環。在目前只考慮ARC環境的情況下,筆者所知的只有一種方法可以破壞這個環:在Block內部對擁有者使用弱引用。
@interface ObjTest () { NSInteger testValue; } @property (copy, nonatomic) void (^block)(); @end @implement ObjTest - (void)function { __weak ObjTest* weakSelf = self; self.block = ^() { weakSelf.testValue = 100; }; } @end
請注意這兩段代碼中唯二的差別(加粗的代碼段)。在Block外創建一個對於self的弱引用,然后在Block內引用self的地方全部使用這個弱引用。這樣就使得Block內部不會對self本身做引用計數+1的操作。那樣就可以打破循環引用的環了。
