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 修饰的。

  1. /**
  2. * The view of video will play on.
  3. * 视频图像载体View
  4. */
  5. @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 文件。

  1. #import <Foundation/Foundation.h>
  2. @class Dog;
  3. @interface Person : NSObject
  4. /** dog */
  5. @property(nonatomic, weak, readonly)Dog *aDog;
  6. // 寄养一条狗
  7. -(void)careDog:(Dog *)dog;
  8. @end
  9. #import "Person.h"
  10. @interface Person()
  11. @end
  12. @implementation Person
  13. -(void)careDog:(Dog *)dog{
  14. _aDog = dog;
  15. }
  16. @end

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

  1. #import "ViewController.h"
  2. #import "Person.h"
  3. #import "Dog.h"
  4. @interface ViewController ()
  5. /** 人 */
  6. @property(nonatomic, strong)Person *aPerson;
  7. /** 狗 */
  8. @property(nonatomic, strong)Dog *aDog;
  9. @end
  10. @implementation ViewController
  11. - (void)viewDidLoad {
  12. [super viewDidLoad];
  13. self.aPerson = [Person new];
  14. [self.aPerson addObserver:self forKeyPath:@"aDog" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
  15. self.aDog = [Dog new];
  16. [self.aPerson careDog:self.aDog];
  17. }
  18. - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
  19. NSLog(@"%@ %@ %@ %@", object, keyPath, change, context);
  20. }

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

  1. [self.aPerson careDog:self.aDog];

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

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

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

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

  1. -willChangeValueForKey:
  2. -didChangeValueForKey:

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

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

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

  1. /* The receiver's superlayer object. Implicitly changed to match the
  2. * hierarchy described by the `sublayers' properties. */
  3. @property(nullable, readonly) CALayer *superlayer;

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

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

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

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

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

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

  1. // 方案一
  2. -(void)careDog:(Dog *)dog{
  3. [self willChangeValueForKey:@"aDog"];
  4. _aDog = dog;
  5. [self didChangeValueForKey:@"aDog"];
  6. }
  7. // 方案二由 [哪里有会生气的龙](http://www.jianshu.com/users/371e7dfb9a55) 提供
  8. -(void)careDog:(id)dog{
  9. [self setValue:dog forKey:@"dog"];
  10. }

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

他的文章集合

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

我的文章集合索引

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

从使用 KVO 监听 readonly 属性说起的更多相关文章

  1. iOS: 使用KVO监听控制器中数组的变化

    一.介绍: KVO是一种能动态监听到属性值的改变的方式,使用场景非常广泛,这里我只讲如何监听控制器ViewController中数组的变化. 二.了解: 首先我们应该知道KVO是不能直接监听控制器Vi ...

  2. Object.defineProperty 监听对象属性变化

    <!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8&quo ...

  3. 侦听器watch 监听单个属性

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  4. iOS-WKWebview 带有进度条加载的ViewController【KVO监听Webview加载进度】

    前言 为什么要说 WKWebview,在之前做电子书笔记时已经提过 WKWebview 在iOS8之后已完全替代 Webview,原因就不多说了,主要还是内存过大: 封装 封装一个基于 UIViewC ...

  5. adb shell getprop,setprop,watchprops更改,查看,监听系统属性

    1.简介 每个属性都有一个名称和值,他们都是字符串格式.属性被大量使用在Android系统中,用来记录系统设置或进程之间的信息交换.属性是在整个系统中全局可见的.每个进程可以get/set属性.  在 ...

  6. vue2.0使用watch监听对象属性

    二话不说直接代码,找了一个百度都没找到.... var head=new Vue({ data:{ checkBoxState:{//监听设置开关勾选状态 notice:true, sound:tru ...

  7. Rudolph javascript 监听简单对象属性的变化 -- 回调函数的应用

    http://www.oschina.net/code/snippet_1590754_46481 //简单对象的属性的变化监控 //通过setAttr改变属性的值 var o = { 'a':2, ...

  8. JS监听对象属性改变

    设想这么一个需求: user.name = '张三' 对user数据进行操作的时候,同步的修改页面上的用户名为张三. 这就是个数据绑定的概念. 针对这类需求 ES5提供了Object.definePr ...

  9. KVO监听数组的变化

    #import "ViewController.h" @interface ViewController () @property(nonatomic,strong)NSMutab ...

随机推荐

  1. 网站流量分析指标-PV/UV/PR/IP

    网站数据分析,经常会统计一个页面或者一个网站或者其他情况的PV/UV.下面简单说一下,这些量PV/UV/PR/IP. 1.PV PV(page view),即页面浏览量,或点击量.通常是衡量一个网络新 ...

  2. 使用jQuery在上传图片之前实现缩略图预览

    使用jQuery在上传图片之前实现缩略图预览 jQuery代码 01 $("#uploadImage").on("change", function(){ 02 ...

  3. 微信小程序开发及相关设置小结

    今年过年,主要看了<奇葩说>和<电锯惊魂>,很不错,好东西的确需要留出足够的时间来看,匆匆忙忙走马观花是对作者的不尊重.除此之外,就是研究了一下微信小程序开发,先说对小程序的看 ...

  4. Java程序调用带参数的shell脚本返回值

    Java程序调用带参数的shell脚本返回值 首先来看看linux中shell变量(\(#,\)@,$0,$1,\(2)的含义解释 变量说明: -  \)$  Shell本身的PID(ProcessI ...

  5. leetCode 41.First Missing Positive (第一个丢失的正数) 解题思路和方法

    First Missing Positive  Given an unsorted integer array, find the first missing positive integer. Fo ...

  6. iOS中ActionSheet和Alert的区别

    首先,样子长得就不一样 看下图:

  7. access2003的使用

    access2003中如何用sql语句创建表 http://zhidao.baidu.com/link?url=dinVbwoI20Xz__NbcIeBPdkjeXRWmZNB0xJvdr0eMBqN ...

  8. Platinum UPnP

    http://www.plutinosoft.com/platinum http://blog.csdn.net/lancees/article/details/9178385 Note that P ...

  9. 自定义UITableViewCell 的delete按钮

    自定义UITableViewCell上的delete按钮 滑动列表行(UITableViewCell)出现删除按钮时,默认是英文“delete”,这份代码片段能够将“delete”变成中文”删除“,甚 ...

  10. 微信小程序 - 自定义swiper(dot)指示点

    点击下载示例:自定义swiper(dot)指示点