代碼下載
系統(tǒng)KVO的使用
KVO:是一種鍵值觀察機(jī)制,當(dāng)某個(gè)對(duì)象為某屬性注冊(cè)了觀察后,只要該對(duì)象的此屬性發(fā)生改變,就會(huì)通知觀察者。
用KVO 實(shí)現(xiàn)如下兩個(gè)效果:


- 導(dǎo)航欄的透明度隨著UITableView的滑動(dòng)距離而變化(這個(gè)功能其實(shí)也可以使用UIScrollViewDelegate的代理方法- (void)scrollViewDidScroll:(UIScrollView *)scrollView來(lái)實(shí)現(xiàn))。
(1) 為UITableView的contentOffset屬性注冊(cè)觀察,其中context這個(gè)參數(shù)是為了區(qū)分父類是否對(duì)該消息感興趣。
[self.tableView addObserver:self forKeyPath:@"contentOffset" options:NSKeyValueObservingOptionNew context:KVOContext_ContentOffset];
(2)實(shí)現(xiàn)觀察者回調(diào)方法- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context
{
if (context == KVOContext_ContentOffset) {
if ([keyPath isEqualToString:@"contentOffset"]) {
if (self.navBackView == nil) {
UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, -20, [UIScreen mainScreen].bounds.size.width, 64)];
view.backgroundColor = [UIColor orangeColor];
[self.navigationController.navigationBar insertSubview:view atIndex:0];
self.navBackView = view;
}
CGPoint contentOffset = [change[NSKeyValueChangeNewKey] CGPointValue];
CGFloat alpha = (contentOffset.y - 64)*(1/136.0);
if (alpha >= 1) {
self.navBackView.alpha = 1;
}
else if (alpha > 0 && alpha < 1)
{
self.navBackView.alpha = alpha;
}
else
{
self.navBackView.alpha = 0;
}
}
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
(3)在觀察者銷毀的時(shí)候移除掉觀察
- (void)dealloc
{
[self removeObserver:self.tableView forKeyPath:@"contentOffset"];
}
說(shuō)明:導(dǎo)航欄沒(méi)有設(shè)置透明度的方法和屬性,需要先設(shè)置導(dǎo)航欄的背景圖和陰影圖為空的圖片來(lái)設(shè)置導(dǎo)航欄的透明,接著向?qū)Ш綑诓迦胍粋€(gè)視圖,通過(guò)控制該視圖的透明度來(lái)達(dá)到導(dǎo)航欄的控制導(dǎo)航欄的透明度效果。
[self.navigationController.navigationBar setBackgroundImage:[[UIImage alloc] init] forBarMetrics:UIBarMetricsDefault];
[self.navigationController.navigationBar setShadowImage:[[UIImage alloc] init]];
- 為UITableView添加多個(gè)cell,每個(gè)cell上都有一個(gè)時(shí)間不一樣的倒計(jì)時(shí)。
(1)先包裝一個(gè)時(shí)間數(shù)據(jù)模型
#import <Foundation/Foundation.h>
@interface TimeModel : NSObject
@property (copy, nonatomic) NSString *title;
@property (assign, nonatomic) NSInteger time;
@end
(2)在cell中增加一個(gè)TimeModel屬性,并在設(shè)置屬性的時(shí)候,注冊(cè)觀察,實(shí)現(xiàn)觀察者回調(diào)方法
- (void)setTimeModel:(TimeModel *)timeModel
{
if (timeModel) {
[timeModel addObserver:self forKeyPath:@"time" options:NSKeyValueObservingOptionNew context:KVOContext];
self.textLabel.text = [NSString stringWithFormat:@"%@倒計(jì)時(shí):%i", timeModel.title, (int)timeModel.time];
if (_timeModel) {
[_timeModel removeObserver:self forKeyPath:@"time"];
}
_timeModel = timeModel;
}
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if (context == KVOContext) {
if ([keyPath isEqualToString:@"time"]) {
self.textLabel.text = [NSString stringWithFormat:@"%@倒計(jì)時(shí):%i", self.timeModel.title, [change[NSKeyValueChangeNewKey] intValue]];
}
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
(3)在控制器中用一個(gè)定時(shí)器來(lái)更改倒計(jì)時(shí)的時(shí)間
self.timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerAction:) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
- (void)timerAction:(NSTimer *)timer
{
for (TimeModel *timeModel in self.dataArr) {
if (timeModel.time > 0) {
timeModel.time--;
}
}
}
注意:
<1>在cell中設(shè)置時(shí)間模型的時(shí)候,因?yàn)閏ell是重用的,如果cell中存在時(shí)間模型,得先移除掉對(duì)該時(shí)間模型的觀察,再為新賦值的時(shí)間模型注冊(cè)觀察。
<2>只要設(shè)置了觀察者,就必須移除,在觀察者銷毀的時(shí)候移除掉所有觀察。
KVO的實(shí)現(xiàn)原理
當(dāng)觀察某對(duì)象A時(shí),KVO機(jī)制動(dòng)態(tài)創(chuàng)建一個(gè)對(duì)象A當(dāng)前類的子類,并為這個(gè)新的子類重寫了被觀察屬性keyPath的setter 方法。setter 方法隨后負(fù)責(zé)通知觀察對(duì)象屬性的改變狀況。
Apple 使用了 isa 混寫(isa-swizzling)來(lái)實(shí)現(xiàn) KVO 。當(dāng)觀察對(duì)象A時(shí),KVO機(jī)制動(dòng)態(tài)創(chuàng)建一個(gè)新的名為: NSKVONotifying_A的新類,該類繼承自對(duì)象A的本類,且KVO為NSKVONotifying_A重寫觀察屬性的setter 方法,setter 方法會(huì)負(fù)責(zé)在調(diào)用原 setter 方法之前和之后,通知所有觀察對(duì)象屬性值的更改情況。
在這個(gè)過(guò)程,被觀察對(duì)象的 isa 指針從指向原來(lái)的A類,被KVO機(jī)制修改為指向系統(tǒng)新創(chuàng)建的子類 NSKVONotifying_A類,來(lái)實(shí)現(xiàn)當(dāng)前類屬性值改變的監(jiān)聽;
所以當(dāng)我們從應(yīng)用層面上看來(lái),完全沒(méi)有意識(shí)到有新的類出現(xiàn),這是系統(tǒng)“隱瞞”了對(duì)KVO的底層實(shí)現(xiàn)過(guò)程,讓我們誤以為還是原來(lái)的類。但是此時(shí)如果我們創(chuàng)建一個(gè)新的名為“NSKVONotifying_A”的類(),就會(huì)發(fā)現(xiàn)系統(tǒng)運(yùn)行到注冊(cè)KVO的那段代碼時(shí)程序就崩潰,因?yàn)橄到y(tǒng)在注冊(cè)監(jiān)聽的時(shí)候動(dòng)態(tài)創(chuàng)建了名為NSKVONotifying_A的中間類,并指向這個(gè)中間類了。
(isa 指針的作用:每個(gè)對(duì)象都有isa 指針,指向該對(duì)象的類,它告訴 Runtime 系統(tǒng)這個(gè)對(duì)象的類是什么。所以對(duì)象注冊(cè)為觀察者時(shí),isa指針指向新子類,那么這個(gè)被觀察的對(duì)象就神奇地變成新子類的對(duì)象(或?qū)嵗┝?。?因而在該對(duì)象上對(duì) setter 的調(diào)用就會(huì)調(diào)用已重寫的 setter,從而激活鍵值通知機(jī)制。
KVO的鍵值觀察通知依賴于 NSObject 的兩個(gè)方法:willChangeValueForKey:和 didChangevlueForKey:,在存取數(shù)值的前后分別調(diào)用2個(gè)方法:
被觀察屬性發(fā)生改變之前,willChangeValueForKey:被調(diào)用,通知系統(tǒng)該 keyPath 的屬性值即將變更;當(dāng)改變發(fā)生后, didChangeValueForKey: 被調(diào)用,通知系統(tǒng)該 keyPath 的屬性值已經(jīng)變更;之后, observeValueForKey:ofObject:change:context: 也會(huì)被調(diào)用。且重寫觀察屬性的setter 方法這種繼承方式的注入是在運(yùn)行時(shí)而不是編譯時(shí)實(shí)現(xiàn)的。
KVO的自我實(shí)現(xiàn)
- 創(chuàng)建一個(gè)類,用于存儲(chǔ)觀察者,觀察的屬性,以及觀察者的回調(diào)方法。
@interface QSPKVOInfo : NSObject
@property (weak, nonatomic) id observer;
@property (copy, nonatomic) NSString *key;
@property (copy, nonatomic) KVOBlock block;
@end
@implementation QSPKVOInfo
+ (instancetype)QSPKVOInfo:(id)observer key:(NSString *)key block:(KVOBlock)block
{
return [[self alloc] initWithObserver:observer key:key block:block];
}
- (instancetype)initWithObserver:(id)observer key:(NSString *)key block:(KVOBlock)block
{
if (self = [super init]) {
self.observer = observer;
self.key = key;
self.block = block;
}
return self;
}
@end
- 創(chuàng)建一個(gè)NSObject的分類,并添加一個(gè)添加觀察者和一個(gè)移除觀察者的方法
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
typedef void (^KVOBlock)(id object, id observer, NSString *key, CGPoint oldValue, CGPoint newValue);
@interface NSObject (KVO)
- (void)QSP_addObserver:(NSObject *)observer forkey:(NSString *)key withBlock:(KVOBlock)block;
- (void)QSP_removeObserver:(NSObject *)observer forkey:(NSString *)key;
@end
- 實(shí)現(xiàn)添加觀察者的方法- (void)QSP_addObserver:(NSObject *)observer forkey:(NSString *)key withBlock:(KVOBlock)block
- (void)QSP_addObserver:(NSObject *)observer forkey:(NSString *)key withBlock:(KVOBlock)block
{
//1.檢查對(duì)象的類有沒(méi)有相應(yīng)的 setter 方法。如果沒(méi)有拋出異常;
Class class = [self class];
SEL setterSelector = NSSelectorFromString(setterForGeter(key));
Method setterMethod = class_getInstanceMethod(class, setterSelector);
if (!setterMethod) {
NSLog(@"%@屬性不存在!", key);
return;
}
//2.檢查對(duì)象 isa 指向的類是不是一個(gè) KVO 類。如果不是,新建一個(gè)繼承原來(lái)類的子類,并把 isa 指向這個(gè)新建的子類;
NSString *className = NSStringFromClass(class);
if (![className hasPrefix:KVOClassPrefix]) {
class = [self makeKvoClassWithOriginalClassName:className];
object_setClass(self, class);
}
NSLog(@"%@", [self class]);
//3.檢查對(duì)象的 KVO 類重寫過(guò)沒(méi)有這個(gè) setter 方法。如果沒(méi)有,添加重寫的 setter 方法;
if (![self hasSelector:setterSelector]) {
const char *methodTypes = method_getTypeEncoding(class_getInstanceMethod(class, setterSelector));
NSLog(@"%s", methodTypes);
BOOL success = NO;
NSLog(@"valueClass:%@", [[self valueForKey:key] class]);
if ([[self valueForKey:key] isKindOfClass:[NSObject class]]) {
success = class_addMethod(class, setterSelector, (IMP)kvo_setter, methodTypes);
}
if (success) {
NSLog(@"重寫%@方法成功!", NSStringFromSelector(setterSelector));
}
else
{
NSLog(@"重寫%@方法失?。?, NSStringFromSelector(setterSelector));
}
}
//4.添加這個(gè)觀察者
NSMutableDictionary *infoDic = objc_getAssociatedObject(self, KVOInfoDictionaryName.UTF8String);
if (infoDic == nil) {
infoDic = [NSMutableDictionary dictionaryWithCapacity:1];
objc_setAssociatedObject(self, KVOInfoDictionaryName.UTF8String, infoDic, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
QSPKVOInfo *info = [QSPKVOInfo QSPKVOInfo:observer key:key block:block];
infoDic[key] = info;
}
- 實(shí)現(xiàn)幾個(gè)輔助方法
/**
根據(jù)getter方法名獲取setter方法名
@param key getter方法名
@return setter方法名
*/
NSString * setterForGeter(NSString *key)
{
if (key && key.length > 0) {
NSString *upperKey = [key uppercaseString];
return [NSString stringWithFormat:@"set%@%@:", [upperKey substringToIndex:1], [key substringFromIndex:1]];
}
return nil;
}
/**
根據(jù)setter方法名獲取getter方法名
@param setter setter方法名
@return getter方法名
*/
NSString * getterForSetter(NSString *setter)
{
if ([setter hasPrefix:@"set"] && [setter hasSuffix:@":"]) {
NSString *lowerKey = [setter lowercaseString];
return [NSString stringWithFormat:@"%@%@", [lowerKey substringWithRange:NSMakeRange(3, 1)], [setter substringWithRange:NSMakeRange(4, setter.length - 5)]];
}
return nil;
}
/**
kvo類class方法的IMP
*/
Class kvo_class(id self, SEL _cmd)
{
// return object_getClass(self);
return class_getSuperclass(object_getClass(self));
}
/**
判斷類中是否存在某個(gè)方法
@param selector SLE
*/
- (BOOL)hasSelector:(SEL)selector
{
unsigned int methodListCount = 0;
Method *methodList = class_copyMethodList(object_getClass(self), &methodListCount);
for (int index = 0; index < methodListCount; index++) {
NSLog(@"%@", NSStringFromSelector(method_getName(methodList[index])));
if (selector == method_getName(methodList[index])) {
return YES;
}
}
return NO;
}
/**
kvo類的setter方法的IMP
*/
void kvo_setter(id self, SEL _cmd, CGPoint value)
{
NSString *setterName = NSStringFromSelector(_cmd);
NSString *getterName = getterForSetter(setterName);
if (!getterName) {
NSLog(@"%@屬性不存在!", getterName);
}
id oldValue = [self valueForKey:getterName];
NSLog(@"oldValue:%@", oldValue);
NSLog(@"newValue:%@", NSStringFromCGPoint(value));
struct objc_super superclazz = {
.receiver = self,
.super_class = class_getSuperclass(object_getClass(self))
};
void (*objc_msgSendSuperCasted)(void *, SEL, CGPoint) = (void *)objc_msgSendSuper;
objc_msgSendSuperCasted(&superclazz, _cmd, value);
NSMutableDictionary *infoDic = objc_getAssociatedObject(self, KVOInfoDictionaryName.UTF8String);
QSPKVOInfo *info = infoDic[getterName];
if (info.block) {
info.block(self, info.observer, info.key, [oldValue CGPointValue], value);
}
}
/**
根據(jù)類名創(chuàng)建kvo類
@param originalClazzName 類名
@return kvo類
*/
- (Class)makeKvoClassWithOriginalClassName:(NSString *)originalClazzName
{
NSString *kvoClassName = [KVOClassPrefix stringByAppendingString:originalClazzName];
Class kvoClass = NSClassFromString(kvoClassName);
if (kvoClass) {
return kvoClass;
}
Class originalClass = [self class];
kvoClass = objc_allocateClassPair(originalClass, kvoClassName.UTF8String, 0);
BOOL success = class_addMethod(kvoClass, @selector(class), (IMP)kvo_class, method_getTypeEncoding(class_getClassMethod(originalClass, @selector(class))));
if (success) {
NSLog(@"重寫class方法成功!");
}
else
{
NSLog(@"重寫class方法失敗!");
}
objc_registerClassPair(kvoClass);
return kvoClass;
}
- 實(shí)現(xiàn)移除觀察者的方法
- (void)QSP_removeObserver:(NSObject *)observer forkey:(NSString *)key
{
NSMutableDictionary *infoDic = objc_getAssociatedObject(self, KVOInfoDictionaryName.UTF8String);
QSPKVOInfo *info = infoDic[key];
if ([info.key isEqualToString:key] && info.observer == observer) {
[infoDic removeObjectForKey:key];
}
}
說(shuō)明:在這里我只實(shí)現(xiàn)了對(duì)CGPoint屬性的,如果真的要實(shí)現(xiàn)一個(gè)KVO框架的話,還需定義一個(gè)數(shù)據(jù)模型出來(lái),用來(lái)承載任何類型的屬性,并能夠解析出對(duì)應(yīng)的數(shù)據(jù)。
使用自定義的KVO
使用上面的第一個(gè)示例,對(duì)UITableView的contentOffset屬性進(jìn)行監(jiān)聽,實(shí)現(xiàn)對(duì)導(dǎo)航欄透明度的控制
[self.tableView QSP_addObserver:self forkey:@"contentOffset" withBlock:^(id object, id observer, NSString *key, CGPoint oldValue, CGPoint newValue) {
if (self.navBackView == nil) {
UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, -20, [UIScreen mainScreen].bounds.size.width, 64)];
view.backgroundColor = [UIColor orangeColor];
[self.navigationController.navigationBar insertSubview:view atIndex:0];
self.navBackView = view;
}
CGPoint contentOffset = newValue;
CGFloat alpha = (contentOffset.y - 64)*(1/136.0);
if (alpha >= 1) {
self.navBackView.alpha = 1;
}
else if (alpha > 0 && alpha < 1)
{
self.navBackView.alpha = alpha;
}
else
{
self.navBackView.alpha = 0;
}
}];
記得在dealoc方法中移除觀察
- (void)dealloc
{
[self.tableView QSP_removeObserver:self forkey:@"contentOffset"];
}