ReactiveCocoa中潛在的內(nèi)存泄漏及解決方案
ReactiveCocoa是GitHub開源的一個函數(shù)響應(yīng)式編程框架,目前在美團App中大量使用。用過它的人都知道很好用,也確實為我們的生活帶來了很多便利,特別是跟MVVM模式結(jié)合使用,更是如魚得水。不過剛開始使用的時候,可能容易疏忽掉一些隱藏的細(xì)節(jié),從而導(dǎo)致內(nèi)存泄漏等問題。本文就帶大家深入了解下ReactiveCocoa中隱藏的一些細(xì)節(jié),幫助大家以更加正確的姿勢使用ReactiveCocoa。
以下代碼和示例基于ReactiveCocoa v2.5。
RACObserve引發(fā)的血案
RACObserve是ReactiveCocoa中一個相當(dāng)常用也相當(dāng)好用的宏,它可以用來監(jiān)聽屬性值的改變,然后傳遞給訂閱者。不過在使用的時候有一點需要稍微注意一下,為了直觀說明,先上一個小Demo。
- - (void)viewDidLoad
- {
- [super viewDidLoad];
- RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) { //1
- MTModel *model = [[MTModel alloc] init]; // MTModel有一個名為的title的屬性
- [subscriber sendNext:model];
- [subscriber sendCompleted];
- return nil;
- }];
- self.flattenMapSignal = [signal flattenMap:^RACStream *(MTModel *model) { //2
- return RACObserve(model, title);
- }];
- [self.flattenMapSignal subscribeNext:^(id x) { //3
- NSLog(@"subscribeNext - %@", x);
- }];
- }
- 創(chuàng)建一個signal,該signal被訂閱后會發(fā)送一個MTModel的實例;
- 對第一步創(chuàng)建的signal進(jìn)行flattenMap操作,并將返回的信號保留(之所以要保留,是因為可能希望在其它地方訂閱,不過這里為了簡單,就直接在第三步進(jìn)行訂閱);
- 對第二步產(chǎn)生的信號(self.flattenMapSignal)進(jìn)行訂閱。
這段代碼看起來很正常,工作也相當(dāng)良好,但是當(dāng)從添加了這段代碼的控制器返回時,控制器并沒有被釋放。這又是為啥呢?看下RACObserve的定義:
- #define RACObserve(TARGET, KEYPATH) \
- ({ \
- _Pragma("clang diagnostic push") \
- _Pragma("clang diagnostic ignored \"-Wreceiver-is-weak\"") \
- __weak id target_ = (TARGET); \
- [target_ rac_valuesForKeyPath:@keypath(TARGET, KEYPATH) observer:self]; \
- _Pragma("clang diagnostic pop") \
- })
注意這一句:
- [target_ rac_valuesForKeyPath:@keypath(TARGET, KEYPATH) observer:self];
如果將宏簡單展開就變成了下面這樣:
- - (void)viewDidLoad
- {
- [super viewDidLoad];
- RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id < RACSubscriber > subscriber) { //1
- GJModel *model = [[GJModel alloc] init];
- [subscriber sendNext:model];
- [subscriber sendCompleted];
- return nil;
- }];
- self.flattenMapSignal = [signal flattenMap:^RACStream *(GJModel *model) {//2
- __weak GJModel *target_ = model;
- return [target_ rac_valuesForKeyPath:@keypath(target_, title) observer:self];
- }];
- [self.flattenMapSignal subscribeNext:^(id x) {//3
- NSLog(@"subscribeNext - %@", x);
- }];
- }
看到這里,應(yīng)該發(fā)現(xiàn)哪里不對了吧?沒錯,flattenMap操作接收的block里面出現(xiàn)了self,對self進(jìn)行了持有,而flattenMap操作返回的信號又由self的屬性flattenMapSignal進(jìn)行了持有,這就造成了循環(huán)引用。
注意:2是間接持有,從邏輯上來講,flattenMapSignal會有一個didSubscribeBlock,為了讓傳遞給flattenMap操作的block有意義,didSubscribeBlock會對該block進(jìn)行持有,從而也就間接持有了self,感興趣的讀者可以去看下相關(guān)源碼。
OK,找到了問題所在,解決起來也就簡單了,使用@weakify和@strongify即可:
- - (void)viewDidLoad
- {
- [super viewDidLoad];
- RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id < RACSubscriber > subscriber) {
- GJModel *model = [[GJModel alloc] init];
- [subscriber sendNext:model];
- [subscriber sendCompleted];
- return nil;
- }];
- @weakify(self); //
- self.signal = [signal flattenMap:^RACStream *(GJModel *model) {
- @strongify(self); //
- return RACObserve(model, title);
- }];
- [self.signal subscribeNext:^(id x) {
- NSLog(@"subscribeNext - %@", x);
- }];
- }
這里之所以容易疏忽,是因為在block里沒有很直觀的看到self,但是RACObserve的定義里面卻用到了self。
其實RACObserve的解釋中已經(jīng)很明確地說明了這個問題。
- /// Creates a signal which observes `KEYPATH` on `TARGET` for changes.
- ///
- /// In either case, the observation continues until `TARGET` _or self_ is
- /// deallocated. If any intermediate object is deallocated instead, it will be
- /// assumed to have been set to nil.
- ///
- /// Make sure to `@strongify(self)` when using this macro within a block! The
- /// macro will _always_ reference `self`, which can silently introduce a retain
- /// cycle within a block. As a result, you should make sure that `self` is a weak
- /// reference (e.g., created by `@weakify` and `@strongify`) before the
- /// expression that uses `RACObserve`.
- ///
- /// Examples
- ///
- /// // Observes self, and doesn't stop until self is deallocated.
- /// RACSignal *selfSignal = RACObserve(self, arrayController.items);
- ///
- /// // Observes the array controller, and stops when self _or_ the array
- /// // controller is deallocated.
- /// RACSignal *arrayControllerSignal = RACObserve(self.arrayController, items);
- ///
- /// // Observes obj.arrayController, and stops when self _or_ the array
- /// // controller is deallocated.
- /// RACSignal *signal2 = RACObserve(obj.arrayController, items);
- ///
- /// @weakify(self);
- /// RACSignal *signal3 = [anotherSignal flattenMap:^(NSArrayController *arrayController) {
- /// // Avoids a retain cycle because of RACObserve implicitly referencing
- /// // self.
- /// @strongify(self);
- /// return RACObserve(arrayController, items);
- /// }];
- ///
- /// Returns a signal which sends the current value of the key path on
- /// subscription, then sends the new value every time it changes, and sends
- /// completed if self or observer is deallocated.
- #define RACObserve(TARGET, KEYPATH) \
- ({ \
- _Pragma("clang diagnostic push") \
- _Pragma("clang diagnostic ignored \"-Wreceiver-is-weak\"") \
- __weak id target_ = (TARGET); \
- [target_ rac_valuesForKeyPath:@keypath(TARGET, KEYPATH) observer:self]; \
- _Pragma("clang diagnostic pop") \
- })
通過這個例子,相信你已經(jīng)知道了RACObserve的正確使用姿勢,也意識到了閱讀文檔的重要性。
如果說RACObserve潛在的內(nèi)存泄漏只要稍加留意,使用的時候查看下文檔就能避免;那么下面的情況,就相當(dāng)隱蔽了,就算是看了文檔也不一定能看出來。
不信?接著往下看。
RACSubject帶來的悲劇
RACSubject是非RAC到RAC的一個橋梁,使用起來也很簡單方便,基本的用法如下:
- - (void)viewDidLoad {
- [super viewDidLoad];
- RACSubject *subject = [RACSubject subject]; //1
- [subject.rac_willDeallocSignal subscribeCompleted:^{ //2
- NSLog(@"subject dealloc");
- }];
- [subject subscribeNext:^(id x) { //3
- NSLog(@"next = %@", x);
- }];
- [subject sendNext:@1]; //4
- }
- 創(chuàng)建一個RACSubject的實例;
- 訂閱subject的dealloc信號,在subject被釋放的時候會發(fā)送完成信號;
- 訂閱subject;
- 使用subject發(fā)送一個值。
接下來看一下輸出的結(jié)果:
- 2016-06-13 09:15:25.426 RAC[5366:245360] next = 1
- 2016-06-13 09:15:25.428 RAC[5366:245360] subject dealloc
工作相當(dāng)良好,接下來改造下程序,要求對subject發(fā)送的所有值進(jìn)行乘3,這用map很容易就實現(xiàn)了。
- - (void)viewDidLoad {
- [super viewDidLoad];
- RACSubject *subject = [RACSubject subject];
- [subject.rac_willDeallocSignal subscribeCompleted:^{
- NSLog(@"subject dealloc");
- }];
- [[subject map:^id(NSNumber *value) {
- return @([value integerValue] * 3);
- }] subscribeNext:^(id x) {
- NSLog(@"next = %@", x);
- }];
- [subject sendNext:@1];
- }
跟之前大體不變,只是對subject進(jìn)行了map操作然后再訂閱,看下輸出結(jié)果:
- 2016-06-13 09:21:42.450 RAC[5404:248584] next = 3
的確是進(jìn)行了乘3操作,符合預(yù)期,但是這里有一個很嚴(yán)重的問題,subject dealloc沒有輸出,也就是說subject沒有釋放。
這不科學(xué)啊!subject看上去沒有被任何對象持有。
那究竟是什么情況?下面我們將RACSubject換成RACSignal試試:
- - (void)viewDidLoad {
- [super viewDidLoad];
- RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
- [subscriber sendNext:@1];
- return nil;
- }];
- [signal.rac_willDeallocSignal subscribeCompleted:^{
- NSLog(@"signal dealloc");
- }];
- [[signal map:^id(NSNumber *value) {
- return @([value integerValue] * 3);
- }] subscribeNext:^(id x) {
- NSLog(@"next = %@", x);
- }];
- }
邏輯跟之前一樣,看一下輸出結(jié)果:
- 2016-06-12 23:32:31.669 RACDemo[5085:217082] next = 3
- 2016-06-12 23:32:31.674 RACDemo[5085:217082] signal dealloc
很明顯,signal被釋放了。同樣的邏輯,signal能正常釋放,subject卻不能正常釋放,太神奇了!
細(xì)心的讀者看到這里,應(yīng)該會發(fā)現(xiàn)一個問題:上面的幾次試驗,不管是RACSubject還是RACSignal都沒有調(diào)用sendCompleted。
難道跟這個有關(guān)系?帶著這個疑問,再進(jìn)行如下試驗,給RACSubject發(fā)送一個完成信號:
- - (void)viewDidLoad {
- [super viewDidLoad];
- RACSubject *subject = [RACSubject subject];
- [subject.rac_willDeallocSignal subscribeCompleted:^{
- NSLog(@"subject dealloc");
- }];
- [[subject map:^id(NSNumber *value) {
- return @([value integerValue] * 3);
- }] subscribeNext:^(id x) {
- NSLog(@"next = %@", x);
- }];
- [subject sendNext:@1];
- [subject sendCompleted];
- }
輸出結(jié)果:
- 2016-06-12 23:40:19.148 RAC_bindSample[5168:221902] next = 3
- 2016-06-12 23:40:19.153 RAC_bindSample[5168:221902] subject dealloc
subject被釋放了,確實修正了內(nèi)存泄漏問題。到這里,我們可以得出結(jié)論:
使用RACSubject,如果進(jìn)行了map操作,那么一定要發(fā)送完成信號,不然會內(nèi)存泄漏。
雖然得出了結(jié)論,但是留下的疑問也是不少,如果你希望知道這其中的緣由,請繼續(xù)往下看。
簡單來說,留下的疑問有:
為什么對RACSubject的實例進(jìn)行map操作之后會產(chǎn)生內(nèi)存泄漏?
為什么RACSignal不管是否有map操作,都不會產(chǎn)生內(nèi)存泄漏?
針對第一個問題,為什么發(fā)送完成可以修復(fù)內(nèi)存泄漏?
帶著疑問,咱們繼續(xù)一探究竟。
講道理,RACSignal和RACSubject雖然都是信號,但是它們有一個本質(zhì)的區(qū)別:
RACSubject會持有訂閱者(因為RACSubject是熱信號,為了保證未來有事件發(fā)送的時候,訂閱者可以收到信息,所以需要對訂閱者保持狀態(tài),做法就是持有訂閱者),而RACSignal不會持有訂閱者。
關(guān)于這一點,更詳細(xì)的說明請看《細(xì)說ReactiveCocoa的冷信號與熱信號(三):怎么處理冷信號與熱信號》。
那么持不持有訂閱者,跟內(nèi)存無法釋放又有啥關(guān)系呢?不急,先記著有這樣一個特性,咱們看看實現(xiàn)。
從上面提出第一個問題可以發(fā)現(xiàn),關(guān)鍵點在于map操作,那么map操作究竟干了什么事情,看下map的實現(xiàn):
- - (instancetype)map:(id (^)(id value))block {
- NSCParameterAssert(block != nil);
- Class class = self.class;
- return [[self flattenMap:^(id value) {
- return [class return:block(value)];
- }] setNameWithFormat:@"[%@] -map:", self.name];
- }
很簡單,只是調(diào)用了一下flattenMap,再看下flattenMap怎么實現(xiàn)的:
- - (instancetype)flattenMap:(RACStream * (^)(id value))block {
- Class class = self.class;
- return [[self bind:^{
- return ^(id value, BOOL *stop) {
- id stream = block(value) ?: [class empty];
- NSCAssert([stream isKindOfClass:RACStream.class], @"Value returned from -flattenMap: is not a stream: %@", stream);
- return stream;
- };
- }] setNameWithFormat:@"[%@] -flattenMap:", self.name];
也很簡單,只是調(diào)用了一下bind,再看看bind的實現(xiàn),bind的實現(xiàn)位于RACSignal.m的92行左右。
- - (RACSignal *)bind:(RACStreamBindBlock (^)(void))block {
- NSCParameterAssert(block != NULL);
- /*
- * -bind: should:
- *
- * 1. Subscribe to the original signal of values.
- * 2. Any time the original signal sends a value, transform it using the binding block.
- * 3. If the binding block returns a signal, subscribe to it, and pass all of its values through to the subscriber as they're received.
- * 4. If the binding block asks the bind to terminate, complete the _original_ signal.
- * 5. When _all_ signals complete, send completed to the subscriber.
- *
- * If any signal sends an error at any point, send that to the subscriber.
- */
- return [[RACSignal createSignal:^(id<RACSubscriber> subscriber) {
- RACStreamBindBlock bindingBlock = block();
- NSMutableArray *signals = [NSMutableArray arrayWithObject:self];
- // 此處省略了80行代碼
- // ...
- }] setNameWithFormat:@"[%@] -bind:", self.name];
如果你下載了源代碼(不想下源碼的話,也可以在線查看),并且看到了這里,相信你的感覺一定是一臉懵逼的,不要激動,雖然這個方法很長,看上去也不那么好懂,但是關(guān)鍵點就那么幾個地方,掌握了關(guān)鍵點就基本能get了。
ReactiveCocoa的作者更是罕見地在實現(xiàn)文件了寫了一大段注釋來說明bind方法的用途,根據(jù)作者的注釋再去理解這個方法會輕松很多。
這里貼一個圖,方便大家理解:
OK,了解了bind操作的用途,也是時候回歸主題了——內(nèi)存是怎么泄露的。
首先我們看到,在didSubscribe的開頭,就創(chuàng)建了一個數(shù)組signals,并且持有了self,也就是源信號:
- NSMutableArray *signals = [NSMutableArray arrayWithObject:self];
(p.s. 如果你不知道didSubscribe是什么,也不了解ReactiveCocoa中信號的訂閱過程,可以先看下《RACSignal的Subscription深入分析》)
接下來會對源信號進(jìn)行訂閱:
- RACDisposable *bindingDisposable = [self subscribeNext:^(id x) {
- // Manually check disposal to handle synchronous errors.
- if (compoundDisposable.disposed) return;
- BOOL stop = NO;
- id signal = bindingBlock(x, &stop);
- @autoreleasepool {
- if (signal != nil) addSignal(signal);
- if (signal == nil || stop) {
- [selfDisposable dispose];
- completeSignal(self, selfDisposable);
- }
- }
- } error:^(NSError *error) {
- //...
- } completed:^{
- //...
- }];
訂閱者會持有nextBlock、errorBlock、completedBlock三個block,為了簡單,我們只討論nextBlock。
從nextBlock中的completeSignal(self, selfDisposable);這一行代碼可以看出,nextBlock對self,也就是源信號進(jìn)行了持有,再看到if (signal != nil) addSignal(signal);這一行,nextBlock對addSignal進(jìn)行了持有,addSignal是在訂閱self之前定義的一個block。
- void (^addSignal)(RACSignal *) = ^(RACSignal *signal) {
- @synchronized (signals) {
- [signals addObject:signal];
- }
- //...
- };
addSignal這個block里面對一開始創(chuàng)建的數(shù)組signals進(jìn)行了持有,用一幅圖來描述下剛才所說的關(guān)系:
如果這個signal是一個RACSignal,那么是沒有任何問題的;如果是signal是一個RACSubject,那問題就來了。還記得前面說過的RACSignal和RACSubject的區(qū)別嗎?RACSubject會持有訂閱者,而RACSignal不會持有訂閱者,如果signal是一個RACSubject,那么圖應(yīng)該是這樣的:
很明顯,產(chǎn)生了循環(huán)引用!!!到這里,也就解答了前面提出的三個問題的前兩個:
對一個信號進(jìn)行了map操作,那么最終會調(diào)用到bind。
如果源信號是RACSubject,由于RACSubject會持有訂閱者,所以產(chǎn)生了循環(huán)引用(內(nèi)存泄漏);
如果源信號是RACSignal,由于RACSignal不會持有訂閱者,那么也就不存在循環(huán)引用。
還剩下最后一個問題:如果源信號是RACSubject,為什么發(fā)送完成可以修復(fù)內(nèi)存泄漏?
來看下訂閱者收到完成信號之后干了些什么:
- RACDisposable *bindingDisposable = [self subscribeNext:^(id x) {
- //...
- } error:^(NSError *error) {
- //...
- } completed:^{
- @autoreleasepool {
- completeSignal(self, selfDisposable);
- }
- }];
很簡單,只是調(diào)用了一下completeSignal這個block。再看下這個block內(nèi)部在干嘛:
- void (^completeSignal)(RACSignal *, RACDisposable *) = ^(RACSignal *signal, RACDisposable *finishedDisposable) {
- BOOL removeDisposable = NO;
- @synchronized (signals) {
- [signals removeObject:signal]; //1
- if (signals.count == 0) {
- [subscriber sendCompleted]; //2
- [compoundDisposable dispose]; //3
- } else {
- removeDisposable = YES;
- }
- }
- if (removeDisposable) [compoundDisposable removeDisposable:finishedDisposable]; //4
- };
//1這里從signals這個數(shù)組中移除傳入的signal,也就斷掉了signals持有subject這條線。
//2、//3、//4其實干的事情差不多,都是拿到對應(yīng)的disposable調(diào)用dispose,這樣資源就得到了回收,subject就不會再持有subscriber,subscriber也會對自己的nextBlock、errorBlock、completedBlock三個block置為nil,就不會存在引用關(guān)系,所有的對象都得到了釋放。
有興趣的同學(xué)可以去了解下RACDisposable,它也是ReactiveCocoa中的重要一員,對理解源碼有很大的幫助。
map只是一個很典型的操作,其實在ReactiveCocoa的實現(xiàn)中,幾乎所有的操作底層都會調(diào)用到bind這樣一個方法,包括但不限于:
- map、filter、merge、combineLatest、flattenMap ……
所以在使用ReactiveCocoa的時候也一定要仔細(xì),對信號操作完成之后,記得發(fā)送完成信號,不然可能在不經(jīng)意間就導(dǎo)致了內(nèi)存泄漏。
RACSubject就是一個比較典型直接的例子。除此之外,如果在對一個信號進(jìn)行類似replay這樣的操作之后,也一定要保證源信號發(fā)送完成;不然,也是會有內(nèi)存泄漏的。
- RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
- [subscriber sendNext:@1];
- [subscriber sendCompleted]; // 保證源信號發(fā)送完成
- return nil;
- }];
- RACSignal *replaySignal = [signal replay]; // 這里返回的其實是一個RACReplaySubject
- [[replaySignal map:^id(NSNumber *value) {
- return @([value integerValue] * 3);
- }] subscribeNext:^(id x) {
- NSLog(@"subscribeNext - %@", x);
- }];
總之,一句話:使用ReactiveCocoa必須要保證信號發(fā)送完成或者發(fā)送錯誤。