从使用 KVO 监听 readonly 属性说起

01.KVO 原理

KVO 是 key-value observing 的简写,它的原理大致是:

  • 1.当一个 object(对象) 有观察者时候,动态创建这个 object(对象) 的类的子类(以 NSKVONotifying_ 打头的类)
  • 2.对于每个被观察的 property(属性),重写其 setter 方法 
  • 3.在重写的 setter 方法中调用以下方法通知观察者 : 

    -willChangeValueForKey:  
    -didChangeValueForKey: 

  • 4.当一个移除观察者时,删除重写的方法 
  • 5.当没有 observer(观察者) 观察任何一个 property(属性) 时,删除动态创建的子类

这些在网上一搜一大篇的 KVO 原理,经过我的细致测试以后,发现都是值得商榷的,所以我特意写了一篇文章来阐释我从代码出发来总结 KVO 的原理的文章 [iOS]用代码探究 KVO 原理(真原创)

这里有滴滴构架师 sunnyxx 的一篇文章 objc kvo简单探索。用详细的代码解释了 KVO 的原理。

我们大致使用 KVO 的场景主要是,监听某一个属性的值的变化。比方说有一个人的类 Person,他有一个体重的属性 height,如果要监听 height 的变化就可以采用 KVO。

但是你有没有碰到过,如果这个 height 是被关键字 readonly 修饰的情况呢?我碰到了,并且在 Google 上找不到相关的资料,所以我们今天来探讨一下这个问题。

02.什么场景下碰到的这个问题?

如果你是我的老读者朋友,并且看过我之前写的一个框架 JPVideoPlayer 的源码,里面有一个细节,我是认真思考了很久,尝试了四种不同的实现方式才确定的。可能很多朋友都没看过,那你可以读我之前的简书文章:

01、[iOS]仿微博视频边下边播之封装播放器 讲述如何封装一个实现了边下边播并且缓存的视频播放器。
02、[iOS]仿微博视频边下边播之滑动TableView自动播放 讲述如何实现在tableView中滑动播放视频,并且是流畅,不阻塞线程,没有任何卡顿的实现滑动播放视频。同时也将讲述当tableView滚动时,以什么样的策略,来确定究竟哪一个cell应该播放视频。

我现在简单描述一下这个问题的场景。我们播放视频的时候,图像的是在 AVPlayerLayer 的一个实例对象上显示的,所以框架需要开发者传进来一个视频图像的载体 showView,用来显示视频图像,也就是把 AVPlayerLayer 的实例对象添加到这个 showView 的 layer 上。

因为 JPVideoPlayer 是一个单例,所以框架不应该以 strong 形式持有视频的载体 showView,以防止 showView 在它的父控件 dealloc 以后不能 dealloc,造成内存泄漏。所以框架对 showView 的持有是以 weak 修饰的。

  /**
   * The view of video will play on.
   * 视频图像载体View
   */
  @property (nonatomic, weak)UIView *showView;

现在有一个使用场景,就是用户打开一个界面,这个界面需要播放视频,然后当用户关闭这个界面的之后,需要同时停止视频播放。这个当然可以让开发者在这个界面的 dealloc 方法中停止视频播放,但是我想不用开发者操心这件事,想在框架内部就把这件事情给做了。

所以任务就是要监听到 showView 的 dealloc,并停止视频播放。

03.解决方案

我想到了四种解决方案来处理达成这个任务。一起来看一下。

3.1、方案一:hook

这个是有经验的开发者最容易想到的。但是我最后并没有采用,我有一个原则,“不到万不得已不要使用 hook,hook 越少越好,尤其是在框架里”。如果你对 hook(方法交换)感兴趣,可以看我之前的简书文章 [iOS]1行代码快速集成按钮延时处理(hook实战)

如果要用 hook 来实现的话,大概可以简单的描述一下这个过程。

  • 在 UIView 的分类里重载 load 方法,在这个方法里把自己写的 dealloc 方法和系统的 dealloc 方法进行交换。
  • 在自定义的 dealloc 方法里判断当前 dealloc 的 view 是不是当前承载视频图像的 showView,如果是,就通知 JPVideoPlayer 停止视频播放。

我放弃了这个方案的另外一个原因是,有可能开发者自己也 hook 了UIView 的 dealloc 方法,这样一来,就不能保证我们写的 hook 是否能生效了。想象一下,我们把系统的 dealloc 方法交换成我们自定义的,开发者也把系统的 dealloc 方法和他自己的方法进行了交换。到最后,究竟是我们还是开发者能成功交换系统的 dealloc 方法,就要取决于看谁最先 hook。如果某个分类里 hook 了系统的方法, 然后又没有调用系统的方法, 那这个方法链到这里就断了。所以这么方案是很有问题的。这个也是滥用 hook 的恶果。

同时也捎带提醒一句,如果你发现你 hook 系统的方法不起作用的时候,或许可以检查一下你项目里引入的第三方框架里是否也 hook 了和你一样的系统方法。

3.2、方案二:重写 removeFromSuperLayer

如果我们把焦点集中到 AVPlayerLayer 上,也就是图像层的时候,我们也可以继承 AVPlayerLayer 自定义一个 JPPlayerLayer,然后创建自定义的 JPPlayerLayer 实例对象来显示视频的图像。然后在 JPPlayerLayer 实例对象中重载 removeFromSuperLayer 方法,期待在这个方法中监听 showView 的释放。

但是这个方案从根本上就被否决了。

原因就是,在我们的场景里,当 showView dealloc 的时候是不会先调用 JPPlayerLayer 实例对象的
removeFromSuperLayer 方法的。想象一下,我们现在有一个红色的 redView 和绿色的 greenView,我们把红色的 redView 添加到 greenView 上,然后当我们绿色的 greenView dealloc 的时候,redView 是不会收到 removeFromSuperView 的调用的。

3.3、方案三:KVO

这里回到了我们开头 KVO 的部分了,我们先来分析一个例子。

我们在项目里创建一个类 Person 和一个 Dog 类,下面是 Person 的 .h 文件和 .m 文件。

#import <Foundation/Foundation.h>

@class Dog;

@interface Person : NSObject

/** dog */
@property(nonatomic, weak, readonly)Dog *aDog;

// 寄养一条狗
-(void)careDog:(Dog *)dog;

@end


#import "Person.h"

@interface Person()

@end

@implementation Person

-(void)careDog:(Dog *)dog{
    _aDog = dog;
}

@end

人有一条狗,但是不是他的,是他朋友寄养在他那里的,所以这里用 weak 修饰。开始人没有狗,所以他朋友寄养一条狗给他。寄养一条狗的实现在 .m 文件里。

#import "ViewController.h"
#import "Person.h"
#import "Dog.h"

@interface ViewController ()

/** 人 */
@property(nonatomic, strong)Person *aPerson;

/** 狗 */
@property(nonatomic, strong)Dog *aDog;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    self.aPerson = [Person new];

    [self.aPerson addObserver:self forKeyPath:@"aDog" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];

    self.aDog = [Dog new];
    [self.aPerson careDog:self.aDog];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
    NSLog(@"%@ %@ %@ %@", object, keyPath, change, context);
}

现在用 KVO 去检测这个人的狗的变化。但是下面这行代码执行完以后,控制台并没有打印出任何东西。

[self.aPerson careDog:self.aDog];

同时,我又在 touchesBegan 方法里写了下面这行代码,点击屏幕,也没有打印任何东西。

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    self.aDog = nil;
}

这是为什么呢?按道理,KVO 也设置了,observeValueForKeyPath 方法也实现了,但是 aDog 值的改变,为什么没有监听到呢?问题就在出在这个关键字 readonly 上。还记得上面的 KVO 原理吗?

对于每个被观察的 property(属性),重写其 setter 方法 。
在重写的 setter 方法中调用以下方法通知观察者 :

  -willChangeValueForKey: 
  -didChangeValueForKey:

readonly 这个关键字会导致对应的属性没有 setter 方法。所以接下来的两个方法也没有加入到 setter 方法中。所以,监听也失效了。

回到我们开始讨论的,我们要使用 KVO 来监听 AVPlayerLayer 实例对象的 superlayer 属性的改变,也就是 showView 的 dealloc,如果 showView 释放了,那么 AVPlayerLayer 实例对象的 superlayer 属性将变为 nil,那么监听者将收到通知,从而停止视频播放。

我们来看一下 AVPlayerLayer 实例对象的 superlayer 属性的官方头文件:

/* The receiver's superlayer object. Implicitly changed to match the
 * hierarchy described by the `sublayers' properties. */

@property(nullable, readonly) CALayer *superlayer;

不巧,是 readonly 的。所以和上面的那个例子是同一种情况,无法监测到 superlayer 的改变。

3.4、方案四:使用定时器 NSTimer

否定了上面三种方案以后,我采取了最笨也是最可靠的方式来处理这个问题。我通过添加定时器,定时去检测 showView 是否被释放来决定是否需要停止视频的播放。

定时器?你可能会觉得太浪费资源了。但是我所指的定时器不是任何时候都在运行,框架里的定时器都是绑定了视频的,如果一个视频开始播放,就会开一个定时器,如果这个视频播放停止了,定时器也会被置空,不会在后台占用资源。

04.怎么用 KVO 来监听 readonly 的属性?

最后说一下假如真的碰到属性必须是 readonly 的,同时又要使用 KVO 来监听的情况的处理方案。这种方案只能是自己创建的类的属性,但是对于系统的属性,不起作用。

// 方案一
-(void)careDog:(Dog *)dog{
    [self willChangeValueForKey:@"aDog"];

    _aDog = dog;

    [self didChangeValueForKey:@"aDog"];
}

// 方案二由 [哪里有会生气的龙](http://www.jianshu.com/users/371e7dfb9a55) 提供
-(void)careDog:(id)dog{
    [self setValue:dog forKey:@"dog"];
}

方案一也就是帮系统补齐它本应该在 setter 方法里添加的两个通知观察者的方法。

他的文章集合

下面这个链接是我所有文章的一个集合目录。这些文章凡是涉及实现的,每篇文章中都有 Github 地址,Github 上都有源码。

我的文章集合索引

你还可以关注我自己维护的简书专题iOS开发心得

posted on 2017-03-19 22:53  &#127774;Bob  阅读(931)  评论(0编辑  收藏  举报

导航