[iOS]从使用 KVO 监听 readonly 属性提及

这是我2017年的第一篇文章,碰巧你看到了,就是一种缘分。也捎带祝您新年身体康健,新年进步! 这算是一个彩蛋吧?等等,彩蛋不是都在最后吗?git

01.KVO 原理

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

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

若是你是个人老读者朋友,而且看过我以前写的一个框架测试

JPVideoPlayer 的源码,里面有一个细节,我是认真思考了好久,尝试了四种不一样的实现方式才肯定的。可能不少朋友都没看过,那你能够读我以前的简书文章:ui

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

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

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

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

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

因此任务就是要监听到 showViewdealloc,并中止视频播放。

03.解决方案

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

03.一、方案一:hook

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

若是要用 hook 来实现的话,大概能够简单的描述一下这个过程。

  • 在 UIView 的分类里重载 load 方法,在这个方法里把本身写的 dealloc 方法和系统的 dealloc 方法进行交换。
  • 在自定义的 dealloc 方法里判断当前 deallocview 是否是当前承载视频图像的 showView,若是是,就通知 JPVideoPlayer 中止视频播放。

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

03.二、方案二:重写 removeFromSuperLayer

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

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

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

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 属性的改变,也就是 showViewdealloc,若是 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.四、方案四:使用定时器 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 方法里添加的两个通知观察者的方法。

NewPan 的文章集合

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

NewPan 的文章集合索引

若是你有问题,除了在文章最后留言,还能够在微博 @盼盼_HKbuy 上给我留言,以及访问个人 Github

相关文章
相关标签/搜索