RAC和内存管理

(转载注明来自:http://hparis.github.io/blog/2015/07/25/rache-nei-cun-guan-li/

最近在用RAC的时候发现自己对内存管理还是有些困惑,于是自己写了一些代码来验证自己的一些理解。

在一开始接触RAC的时候,我们知道RAC对于block都是copy赋值的。

@implementation RACSignal

#pragma mark Lifecycle

+ (RACSignal *)createSignal:(RACDisposable * (^)(id<RACSubscriber> subscriber))didSubscribe {
    return [RACDynamicSignal createSignal:didSubscribe];
}



@implementation RACDynamicSignal

#pragma mark Lifecycle

+ (RACSignal *)createSignal:(RACDisposable * (^)(id<RACSubscriber> subscriber))didSubscribe {
    RACDynamicSignal *signal = [[self alloc] init];
    signal->_didSubscribe = [didSubscribe copy];
    return [signal setNameWithFormat:@"+createSignal:"];
}

在创建RACSingal的时候会调用其子类RACDynamicSignal去创建,我们也看到RACDynamicSignal对didSuscribe这个block是进行了copy。所以大家可能会被要求注意循环引用的问题,于是大家都用@weakify(target)和@strongify(target)来避免循环引用的问题。那是不是所有用到RAC的地方都需要使用这些宏来避免循环引用的问题,不尽然。比如下面这个:

// 场景1
[RACObserve(self, title) subscribeNext:^(id x) {
NSLog(@"%@", x);
}];

接下来,我们来对比一下几种用法

@interface ViewController()
@property (strong, nonatomic) ViewModel * viewModel;
@end

@implementation ViewController

- (void)viewDidiLoad {
    [super viewDidLoad];

    self.viewModel = [ViewModel new];

    // 场景2
    dispatch_async(dispatch_get_main_queue(), ^{
        self.title = @"你好";
    });

    // 场景3
    [self.viewModel.titleSignal subscribeNext:^(NSString * title) {
        self.title = title;
    }];

    // 场景4
    [RACObserve(self.viewModel, title) subscribeNext:^(NSString * title)     {
        self.title = title;
    }];
}

@end

场景2是我们平常都会用到的,而且我们也没有在这种场景下去考虑循环引用的问题,这是因为dispatch的block不是属于self的(至于这个block是属于谁的,回头我再查点资料或者请各位指教),所以即使你在block使用了self也不会有循环应用的问题。

场景3很明显是有循环引用的问题:self->viewModel->titleSignal->block->self,这个时候如果我们不做处理的话,那么self就永远不会被释放。正确的做法应该是使用@weakify(self)和@strongify(self):

// 场景3
@weakify(self);
[self.viewModel.titleSignal subscribeNext:^(NSString * title) {
    @strongify(self);
    self.title = title;
}];

场景4在我们看来是没有问题的,因为这里看起来只有singal->block->self的引用,它们之间并没有造成循环引用的问题。我们来看看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") \\
})

- (RACSignal *)rac_valuesForKeyPath:(NSString *)keyPath observer:(__weak NSObject *)observer;

其实,看到这里你会认为这里只是调用了一个方法创建了一个Signal,而且这个Signal也并不属于任何对象。我们再来看看具体的实现是怎么样的?

- (RACSignal *)rac_valuesAndChangesForKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options observer:(__weak NSObject *)weakObserver {
    NSObject *strongObserver = weakObserver;
    keyPath = [keyPath copy];

    NSRecursiveLock *objectLock = [[NSRecursiveLock alloc] init];
    objectLock.name = @"org.reactivecocoa.ReactiveCocoa.NSObjectRACPropertySubscribing";

    __weak NSObject *weakSelf = self;

    RACSignal *deallocSignal = [[RACSignal zip:@[
                            self.rac_willDeallocSignal,
                            strongObserver.rac_willDeallocSignal ?: [RACSignal never]
    ]] doCompleted:^{
        // Forces deallocation to wait if the object variables are currently
        // being read on another thread.
        [objectLock lock];
        @onExit {
            [objectLock unlock];
        };
    }];

return [[[RACSignal createSignal:^ RACDisposable * (id<RACSubscriber> subscriber) {
    // Hold onto the lock the whole time we're setting up the KVO
    // observation, because any resurrection that might be caused by our
    // retaining below must be balanced out by the time -dealloc returns
    // (if another thread is waiting on the lock above).
    [objectLock lock];
    @onExit {
        [objectLock unlock];
    };

    __strong NSObject *observer __attribute__((objc_precise_lifetime)) = weakObserver;
    __strong NSObject *self __attribute__((objc_precise_lifetime)) = weakSelf;

    if (self == nil) {
        [subscriber sendCompleted];
        return nil;
    }

    return [self rac_observeKeyPath:keyPath options:options observer:observer block:^(id value, NSDictionary *change, BOOL causedByDealloc, BOOL affectedOnlyLastComponent) {
            [subscriber sendNext:RACTuplePack(value, change)];
    }];
}] takeUntil:deallocSignal] setNameWithFormat:@"%@ -rac_valueAndChangesForKeyPath: %@ options: %lu observer: %@", self.rac_description, keyPath, (unsigned long)options, strongObserver.rac_description];
}

重点观察deallocSignal和[signal takeUntile:deallocSignal],我们把deallocSignal单独拿出来看看:

RACSignal *deallocSignal = [[RACSignal zip:@[
                        self.rac_willDeallocSignal,
                        strongObserver.rac_willDeallocSignal ?: [RACSignal never]
                        ]] doCompleted:^{
    // Forces deallocation to wait if the object variables are currently
    // being read on another thread.
    [objectLock lock];
    @onExit {
    [objectLock unlock];
    };
}];

这里的deallocSignal是只有在self和strongObserve都将要发生dealloc的时候才会触发的。即用RACObserve创建的信号只有在其target和observe都发生dealloc的时候才会被disposable(这个好像是RAC用来销毁自己资源的东西)。不明白的童鞋,我们回头来分析一下场景4的代码:

// 场景4
[RACObserve(self.viewModel, title) subscribeNext:^(NSString * title) {
    self.title = title;
}];

用RACObserve创建的信号看起来只要出了函数体其资源应该就会被回收,但是这个信号其实是只有在self.viewModel.rac_willDeallocSignal和self.rac_willDeallocSignal都发生的情况下才会被释放。所以场景4的引用关系看起来只有signal->block->self,但是这个signal只有在self.rac_willDeallocSignal的时候才会被释放。所以这里如果不打断这种关系的话就会造成循环引用的问题,正确做法应该是:

// 场景4
@weakify(self);
[RACObserve(self.viewModel, title) subscribeNext:^(NSString * title) {
    @strongify(self);
    self.title = title;
}];

最后,在说一个特别需要注意的,就是UITableViewCell和UICollectionViewCell复用和RAC的问题。

- (NSInteger)tableView:(nonnull UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return 1000;
}

- (UITableViewCell *)tableView:(nonnull UITableView *)tableView cellForRowAtIndexPath:(nonnull NSIndexPath *)indexPath {
    UITableViewCell * cell = [tableView dequeueReusableCellWithIdentifier:@"TableViewCell"];

    @weakify(self);
    [RACObserve(cell.textLabel, text) subscribeNext:^(id x) {
        @strongify(self);
        NSLog(@"%@", self);
    }];

    return cell;
}

我们看到这里的RACObserve创建的Signal和self之间已经去掉了循环引用的问题,所以应该是没有什么问题的。但是结合之前我们对RACObserve的理解再仔细分析一下,这里的Signal只要self没有被dealloc的话就不会被释放。虽然每次UITableViewCell都会被重用,但是每次重用过程中创建的信号确实无法被disposable。那我们该怎么做呢?

- (NSInteger)tableView:(nonnull UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return 1000;
}

- (UITableViewCell *)tableView:(nonnull UITableView *)tableView cellForRowAtIndexPath:(nonnull NSIndexPath *)indexPath {
    UITableViewCell * cell = [tableView dequeueReusableCellWithIdentifier:@"TableViewCell"];

    @weakify(self);
    [[RACObserve(cell.textLabel, text) takeUntil:cell.rac_prepareForReuseSignal] subscribeNext:^(id x) {
        @strongify(self);
        NSLog(@"%@", self);
    }];

    return cell;
}

注意,我们在cell里面创建的信号加上takeUntil:cell.rac_prepareForReuseSignal,这个是让cell在每次重用的时候都去disposable创建的信号。

以上所说的关于内存的东西我都用Instrument的Allocations验证过了,但是依旧建议大家自己也去试试。

(PS:第一次这么认真写东西,如果有什么问题,欢迎指出!)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容

  • 打算在项目中大面积使用RAC来开发,所以整理一些常用的实践范例和比较完整的api说明方便开发时随时查阅 声明式编程...
    星光社的戴铭阅读 5,328评论 5 49
  • 前言 很多blog都说ReactiveCocoa好用,然后各种秀自己如何灵活运用ReactiveCocoa,但是感...
    RainyGY阅读 1,322评论 0 1
  • RAC使用测试Demo下载:github.com/FuWees/WPRACTestDemo 1.ReactiveC...
    FuWees阅读 6,353评论 3 10
  • 一 导入ReactiveCocoa框架 通常都会使用CocoaPods(用于管理第三方框架的插件)帮助我们导入po...
    CharType阅读 816评论 0 1
  • 2017.02.22 可以练习,每当这个时候,脑袋就犯困,我这脑袋真是神奇呀,一说让你做事情,你就犯困,你可不要太...
    Carden阅读 1,325评论 0 1