這是我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
方法裏添加的兩個通知觀察者的方法。