这是我2017年的第一篇文章,碰巧你看到了,就是一种缘分。也捎带祝您新年身体康健,新年进步! 这算是一个彩蛋吧?等等,彩蛋不是都在最后吗?
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.解决方案
我想到了四种解决方案来处理达成这个任务。一起来看一下。
03.1、方案一:hook
这个是有经验的开发者最容易想到的。但是我最后并没有采用,我有一个原则,“不到万不得已不要使用 hook,hook 越少越好,尤其是在框架里”。如果你对 hook(方法交换)感兴趣,可以看我之前的简书文章 [iOS]1行代码快速集成按钮延时处理(hook实战)。
如果要用 hook 来实现的话,大概可以简单的描述一下这个过程。
- 在 UIView 的分类里重载
load
方法,在这个方法里把自己写的dealloc
方法和系统的dealloc
方法进行交换。 - 在自定义的
dealloc
方法里判断当前dealloc
的view
是不是当前承载视频图像的showView
,如果是,就通知JPVideoPlayer
停止视频播放。
同时也捎带提醒一句,如果你发现你 hook 系统的方法不起作用的时候,或许可以检查一下你项目里引入的第三方框架里是否也 hook 了和你一样的系统方法。
03.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
的改变。
03.4、方案四:使用定时器 NSTimer
否定了上面三种方案以后,我采取了最笨也是最可靠的方式来处理这个问题。我通过添加定时器,定时去检测 showView
是否被释放来决定是否需要停止视频的播放。
定时器?你可能会觉得太浪费资源了。但是我所指的定时器不是任何时候都在运行,框架里的定时器都是绑定了视频的,如果一个视频开始播放,就会开一个定时器,如果这个视频播放停止了,定时器也会被置空,不会在后台占用资源。
04.怎么用 KVO 来监听 readonly 的属性?
最后说一下假如真的碰到属性必须是 readonly
的,同时又要使用 KVO 来监听的情况的处理方案。这种方案只能是自己创建的类的属性,但是对于系统的属性,不起作用。
// 方案一
-(void)careDog:(Dog *)dog{
[self willChangeValueForKey:@"aDog"];
_aDog = dog;
[self didChangeValueForKey:@"aDog"];
}
复制代码
// 方案二由 哪里有会生气的龙 提供
-(void)careDog:(id)dog{
[self setValue:dog forKey:@"dog"];
}
复制代码
方案一也就是帮系统补齐它本应该在 setter
方法里添加的两个通知观察者的方法。