Block循環引用問題研究


  自從蘋果在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的操作。那樣就可以打破循環引用的環了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM