这是我2017年的第一篇文章,碰巧你看到了,就是一种缘分。也捎带祝您新年身体康健,新年进步! 这算是一个彩蛋吧?等等,彩蛋不是都在最后吗?git
KVO 是 key-value observing 的简写,它的原理大体是:github
- 1.当一个 object(对象) 有观察者时候,动态建立这个 object(对象) 的类的子类(以 NSKVONotifying_ 打头的类)
* 2.对于每一个被观察的 property(属性),重写其 setter 方法 ** 3.在重写的 setter 方法中调用如下方法通知观察者 : ** -willChangeValueForKey: **-didChangeValueForKey: *4.当一个移除观察者时,删除重写的方法- 5.当没有 observer(观察者) 观察任何一个 property(属性) 时,删除动态建立的子类
这些在网上一搜一大篇的 KVO 原理,通过个人细致测试之后,发现都是值得商榷的,因此我特地写了一篇文章来阐释我从代码出发来总结 KVO 的原理的文章 [iOS]用代码探究 KVO 原理(真原创)。缓存
这里有滴滴构架师 sunnyxx 的一篇文章 objc kvo简单探索。用详细的代码解释了 KVO 的原理。bash
咱们大体使用 KVO 的场景主要是,监听某一个属性的值的变化。比方说有一我的的类 Person,他有一个体重的属性 height,若是要监听 height 的变化就能够采用 KVO。框架
可是你有没有碰到过,若是这个 height 是被关键字 readonly 修饰的状况呢?我碰到了,而且在 Google 上找不到相关的资料,因此咱们今天来探讨一下这个问题。ide
若是你是个人老读者朋友,而且看过我以前写的一个框架测试
JPVideoPlayer 的源码,里面有一个细节,我是认真思考了好久,尝试了四种不一样的实现方式才肯定的。可能不少朋友都没看过,那你能够读我以前的简书文章:ui
0一、[iOS]仿微博视频边下边播之封装播放器 讲述如何封装一个实现了边下边播而且缓存的视频播放器。 0二、[iOS]仿微博视频边下边播之滑动TableView自动播放 讲述如何实如今tableView中滑动播放视频,而且是流畅,不阻塞线程,没有任何卡顿的实现滑动播放视频。同时也将讲述当tableView滚动时,以什么样的策略,来肯定究竟哪个cell应该播放视频。atom
我如今简单描述一下这个问题的场景。咱们播放视频的时候,图像的是在 AVPlayerLayer
的一个实例对象上显示的,因此框架须要开发者传进来一个视频图像的载体 showView
,用来显示视频图像,也就是把 AVPlayerLayer
的实例对象添加到这个 showView
的 layer
上。spa
由于 JPVideoPlayer
是一个单例,因此框架不该该以 strong
形式持有视频的载体 showView
,以防止 showView
在它的父控件 dealloc
之后不能 dealloc
,形成内存泄漏。因此框架对 showView
的持有是以 weak
修饰的。
/**
* The view of video will play on.
* 视频图像载体View
*/
@property (nonatomic, weak)UIView *showView;
复制代码
如今有一个使用场景,就是用户打开一个界面,这个界面须要播放视频,而后当用户关闭这个界面的以后,须要同时中止视频播放。这个固然可让开发者在这个界面的 dealloc
方法中中止视频播放,可是我想不用开发者操心这件事,想在框架内部就把这件事情给作了。
因此任务就是要监听到 showView
的 dealloc
,并中止视频播放。
我想到了四种解决方案来处理达成这个任务。一块儿来看一下。
这个是有经验的开发者最容易想到的。可是我最后并无采用,我有一个原则,“不到万不得已不要使用 hook,hook 越少越好,尤为是在框架里”。若是你对 hook(方法交换)感兴趣,能够看我以前的简书文章 [iOS]1行代码快速集成按钮延时处理(hook实战)。
若是要用 hook 来实现的话,大概能够简单的描述一下这个过程。
load
方法,在这个方法里把本身写的 dealloc
方法和系统的 dealloc
方法进行交换。dealloc
方法里判断当前 dealloc
的 view
是否是当前承载视频图像的 showView
,若是是,就通知 JPVideoPlayer
中止视频播放。同时也捎带提醒一句,若是你发现你 hook 系统的方法不起做用的时候,或许能够检查一下你项目里引入的第三方框架里是否也 hook 了和你同样的系统方法。
若是咱们把焦点集中到 AVPlayerLayer
上,也就是图像层的时候,咱们也能够继承 AVPlayerLayer
自定义一个 JPPlayerLayer
,而后建立自定义的 JPPlayerLayer
实例对象来显示视频的图像。而后在 JPPlayerLayer
实例对象中重载 removeFromSuperLayer
方法,期待在这个方法中监听 showView
的释放。
可是这个方案从根本上就被否决了。
缘由就是,在咱们的场景里,当 showView dealloc
的时候是不会先调用 JPPlayerLayer
实例对象的 removeFromSuperLayer
方法的。想象一下,咱们如今有一个红色的 redView
和绿色的 greenView
,咱们把红色的 redView
添加到 greenView
上,而后当咱们绿色的 greenView dealloc
的时候,redView
是不会收到 removeFromSuperView
的调用的。
这里回到了咱们开头 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
的改变。
否认了上面三种方案之后,我采起了最笨也是最可靠的方式来处理这个问题。我经过添加定时器,定时去检测 showView
是否被释放来决定是否须要中止视频的播放。
定时器?你可能会以为太浪费资源了。可是我所指的定时器不是任什么时候候都在运行,框架里的定时器都是绑定了视频的,若是一个视频开始播放,就会开一个定时器,若是这个视频播放中止了,定时器也会被置空,不会在后台占用资源。
最后说一下假如真的碰到属性必须是 readonly
的,同时又要使用 KVO 来监听的状况的处理方案。这种方案只能是本身建立的类的属性,可是对于系统的属性,不起做用。
// 方案一
-(void)careDog:(Dog *)dog{
[self willChangeValueForKey:@"aDog"];
_aDog = dog;
[self didChangeValueForKey:@"aDog"];
}
复制代码
// 方案二由 哪里有会生气的龙 提供
-(void)careDog:(id)dog{
[self setValue:dog forKey:@"dog"];
}
复制代码
方案一也就是帮系统补齐它本应该在 setter
方法里添加的两个通知观察者的方法。