1. 实现原理
关于KVO的实现原理,苹果有如下说明:
Key-Value Observing Implementation Details
Automatic key-value observing is implemented using a technique called isa-swizzling.
The isa pointer, as the name suggests, points to the object's class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.
When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.
You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.
从官方的说明来看:
- 采用了isa-swizzling技术,当有属性被观察则会将对象的isa指向一个中间类而不是本身的类;这样做的好处就是不会影响到实例对象的原来的类
- 同时我们不应该依赖于isa去判断类的继承关系,应该使用class方法去判断;这里从侧面说明class返回的还是原来的类
接下来通过例子来探究一下系统的实现
1.1 isa-swizzling
isa-swizzling简单理解为:通过修改isa的指向,是isa指向另一个类来达到对对象的一些行为的修改
下面通过例子来看一下:
被观察对象TestKVOObject
@interface TestKVOObject : NSObject
@property (nonatomic, copy) NSString *testString;
@end
@implementation TestKVOObject
@end
添加观察者
- (void)testSystemKVO {
TestKVOObject *test = [TestKVOObject new];
[test addObserver:self forKeyPath:@"testString" options:NSKeyValueObservingOptionNew context:nil];
test.testString = @"testString";
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"testString"]) {
NSLog(@"testString: %@", change);
}
}
在添加观察者前打上断点:
在添加观察者后打上断点:
发现实例对象的isa的指向变了,变成了NSKVONotifying_TestKVOObject
用lldb打印一下看新创建一个TestKVOObject
的实例,看看它的isa是否有变化,可以看到系统的kvo的isa-swizzling只对被观察的对象实例产生了影响,并且class方法返回的还是原来的类;
原类在实例化一个对象出来它的isa指向的还是原来的类对象。
(lldb) p test->isa
(Class) $2 = NSKVONotifying_TestKVOObject
(lldb) p [TestKVOObject new]->isa
(__unsafe_unretained Class) $3 = TestKVOObject
(lldb) p [test class]
(Class) $4 = TestKVOObject
(lldb)
1.2 NSKVONotifying_XXX
接下来我们看看被观察对象的isa指向的一个新类NSKVONotifying_XXX
是怎样设计的;通过
_shortMethodDescription
命令在lldb打印出该类的方法及实例
(lldb) po [NSKVONotifying_TestKVOObject _shortMethodDescription]
<NSKVONotifying_TestKVOObject: 0x600001e558c0>:
in NSKVONotifying_TestKVOObject:
Instance Methods:
- (void) setTestString:(id)arg1; (0x7fff207b5b57)
- (Class) class; (0x7fff207b4662)
- (void) dealloc; (0x7fff207b440b)
- (BOOL) _isKVOA; (0x7fff207b4403)
in TestKVOObject:
Properties:
@property (copy, nonatomic) NSString* testString; (@synthesize testString = _testString;)
Instance Methods:
- (id) testString; (0x1071074f0)
- (void) setTestString:(id)arg1; (0x107107540)
- (void) dealloc; (0x107107490)
- (void) .cxx_destruct; (0x1071075a0)
(NSObject ...)
可以看到新的类中有4个实例方法
- (void) setTestString:(id)arg1; (0x7fff207b5b57)
- (Class) class; (0x7fff207b4662)
- (void) dealloc; (0x7fff207b440b)
- (BOOL) _isKVOA; (0x7fff207b4403)
接下来一个个看这些方法是干了啥
1.2.1 set方法
通过lldb打印一下
(lldb) dis -s 0x7fff207b5b57
Foundation`_NSSetObjectValueAndNotify:
0x7fff207b5b57 <+0>: pushq %rbp
0x7fff207b5b58 <+1>: movq %rsp, %rbp
0x7fff207b5b5b <+4>: pushq %r15
0x7fff207b5b5d <+6>: pushq %r14
0x7fff207b5b5f <+8>: pushq %r13
0x7fff207b5b61 <+10>: pushq %r12
0x7fff207b5b63 <+12>: pushq %rbx
0x7fff207b5b64 <+13>: subq $0x58, %rsp
0x7fff207b5b68 <+17>: movq %rdx, -0x78(%rbp)
0x7fff207b5b6c <+21>: movq %rsi, %r15
0x7fff207b5b6f <+24>: movq %rdi, %r13
内部实现调用的是_NSSetObjectValueAndNotify
,下个符号断点看看是怎么实现的:
或者使用hopper查看Foundation.framework的伪代码
通过hopper我们还发现对于不同的数据类型都有不同的set方法实现,我们定义的是NSString类型,则调用的是_NSSetObjectValueAndNotify
,如果你定义的是BOOL那么就会是_NSSetBoolValueAndNotify
大致的实现就是:
- willChangeValueForKey
- 调用原始的set方法
- didChangeValueForKey
didChangeValueForKey内部实现
void -[NSObject(NSKeyValueObserverNotification) didChangeValueForKey:](int arg0) {
_NSKeyValueDidChangeWithPerThreadPendingNotifications(arg0, rdx, 0x0, _NSKeyValueDidChangeBySetting, 0x0);
return;
}
为了看清didChangeValueForKey内部的实现,我在类中重写了一下该方法,你也可以断点调试一下
@implementation TestKVOObject
- (void)didChangeValueForKey:(NSString *)key {
[super didChangeValueForKey:key];
}
@end
调用堆栈信息:
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 46.1
* frame #0: 0x000000010b3fedf8 RuntimeLearning`-[ViewController observeValueForKeyPath:ofObject:change:context:](self=0x0000000000000000, _cmd=<no value available>, keyPath=0x0000000000000000, object=0x0000000000000000, change=0x0000000000000000, context=0x4046000000000000) at ViewController.m:293
frame #1: 0x00007fff207b96f4 Foundation`NSKeyValueNotifyObserver + 329
frame #2: 0x00007fff207bce28 Foundation`NSKeyValueDidChange + 439
frame #3: 0x00007fff207b8bff Foundation`NSKeyValueDidChangeWithPerThreadPendingNotifications + 146
frame #4: 0x000000010b3fe3e2 RuntimeLearning`-[TestKVOObject didChangeValueForKey:](self=0x0000600003776d00, _cmd="didChangeValueForKey:", key=@"testString") at ViewController.m:72:5
frame #5: 0x00007fff207b5c09 Foundation`_NSSetObjectValueAndNotify + 178
frame #6: 0x000000010b402923 RuntimeLearning`-[ViewController testSystemKVO](self=0x00007f976e606f60, _cmd="testSystemKVO") at ViewController.m:1087:10
可以看到didChangeValueForKey的调用堆栈,经过一系列的函数跳转最后执行了
observeValueForKeyPath:ofObject:change:context
方法
1.2.2 重写的class方法
我们先通过lldb来打印查看一下被观察者的class、isa、superclass
(lldb) p [test class]
(Class) $6 = TestKVOObject
(lldb) p test->isa
(Class) $7 = NSKVONotifying_TestKVOObject
(lldb) p class_getSuperclass(test->isa)
(Class _Nullable) $8 = TestKVOObject
(lldb) p object_getClass(test)
(Class _Nullable) $9 = NSKVONotifying_TestKVOObject
(lldb)
class指向的是原来的类
isa、object_getClass都返回的是新的类
superclass指向的是原来的类
这里可以看到KVO被观察者是派生出了一个子类来实现的,而且将派生的类的class返回的是原来的类
- (Class)class {
return object_getClass(self); // 返回的就是isa
}
Class object_getClass(id obj)
{
if (obj) return obj->getIsa();
else return Nil;
}
对于为什么要重写class方法,我个人认为:
- class返回被观察者原来的类,这样上层使用者就不必关注内部的实现细节,对于实例的使用,如果不使用runtime API的话,那么跟原来的类基本一致;比如
isKindOfClass、isMemberOfClass
等的判断; - 类的class方法默认是返回isa的,如果没有重写,那么返回的就是子类了,这样假如上层使用者再通过isMemberOfClass去判断的时候,那就出问题了啊,上层不知道有这个子类的存在的;
- class方法和object_getClass返回的不一样,也可以作为判断该实例对象有没有被isa-swizzling的判断
1.2.3 重写的dealloc方法
// 0x7fff207b4403位dealloc的地址
(lldb) dis -s 0x7fff207b4403
Foundation`NSKVODeallocate:
内部调用的是NSKVODeallocate
,使用hopper看下伪代码实现:
int _NSKVODeallocate(int arg0, int arg1) {
r13 = rdi;
var_-48 = **___stack_chk_guard;
rax = object_getClass(rdi);
r12 = __NSKVOUsesBaseClassObservationInfoImplementationForClass(rax);
rax = object_getIndexedIvars(rax);
r14 = rax;
rbx = class_getInstanceMethod(*rax, rsi);
if (r12 == 0x0) goto loc_7fff207b448e;
loc_7fff207b4461:
if (**___stack_chk_guard == var_-48) {
rdi = r13;
rsi = rbx;
rax = method_invoke(rdi, rsi);
}
else {
rax = __stack_chk_fail();
}
return rax;
loc_7fff207b448e:
rax = __NSKeyValueRetainedObservationInfoForObject(r13, 0x0);
*var_-72 = r13;
*(var_-72 + 0x8) = rax;
*(var_-72 + 0x10) = 0x0;
__NSKeyValueAddObservationInfoWatcher(var_-72);
r12 = __NSKVOObservationInfoOverridenObjectMayThrowOnDealloc(r13);
method_invoke(r13, rbx);
if (var_-64 == 0x0) goto loc_7fff207b4570;
loc_7fff207b44d1:
r15 = dyld_get_program_sdk_version();
if (r12 != 0x0) {
r12 = (*_objc_msgSend)(var_-64, *0x7fff86b9d448) ^ 0x1;
}
else {
r12 = 0x0;
}
*(int8_t *)var_-73 = 0x0;
rax = CFPreferencesGetAppBooleanValue(@"NSKVODeallocateCleansUpBeforeThrowing", **_kCFPreferencesCurrentApplication, var_-73);
rcx = 0x0;
CMP(r15, 0x7ffff);
rdx = r12 & 0xff;
rsi = 0x0;
asm{ cmova esi, edx };
rbx = (var_-73 == rcx ? 0x1 : 0x0) | (rax == 0x0 ? 0x1 : 0x0);
if (rbx == 0x0) {
rsi = rdx;
}
if (rsi != 0x0) goto loc_7fff207b45b4;
loc_7fff207b4542:
if ((r15 < 0x80000) || (r12 != 0x0)) {
_NSLog(@"An instance %p of class %@ was deallocated while key value observers were still registered with it. Observation info was leaked, and may even become mistakenly attached to some other object. Set a breakpoint on NSKVODeallocateBreak to stop here in the debu…", r13, *r14);
_NSKVODeallocateBreak(r13);
}
goto loc_7fff207b4570;
loc_7fff207b4570:
__NSKeyValueRemoveObservationInfoWatcher(var_-72);
[var_-64 release];
if (0x0 == 0x0) {
rax = *___stack_chk_guard;
rax = *rax;
if (rax != var_-48) {
rax = __stack_chk_fail();
}
}
else {
rax = objc_exception_rethrow();
}
return rax;
loc_7fff207b45b4:
r15 = (*_objc_msgSend)(var_-64, *0x7fff86b9a5e8);
if (rbx == 0x0) {
__NSKeyValueRemoveObservationInfoForObject(var_-72);
}
rax = (*_objc_msgSend)(@class(NSString), *0x7fff86b9a4b8);
rax = (*_objc_msgSend)(@class(), *0x7fff86b9a700);
rax = objc_exception_throw(rax);
return rax;
}
汇编看不懂,可以参照着网上大神根据伪代码实现的逻辑对照看看
KVO实现
void DSKVODeallocate(id object, SEL selector) {
DSKeyValueObservationInfo *observationInfo = _DSKeyValueRetainedObservationInfoForObject(object, nil);
ObservationInfoWatcher watcher = {object, observationInfo, NULL};
_DSKeyValueAddObservationInfoWatcher(&watcher);
DSKeyValueNotifyingInfo *notifyInfo = (DSKeyValueNotifyingInfo *)object_getIndexedIvars(object_getClass(object));
Method originDellocMethod = class_getInstanceMethod(notifyInfo->originalClass, selector);
((id (*)(id,Method))method_invoke)(object, originDellocMethod);
@try {
if(watcher.observationInfo) {
BOOL keyExistsAndHasValidFormat = false;
BOOL cleansUpBeforeThrowing = false;
cleansUpBeforeThrowing = (BOOL)CFPreferencesGetAppBooleanValue(CFSTR("NSKVODeallocateCleansUpBeforeThrowing"), kCFPreferencesCurrentApplication, (Boolean *)&keyExistsAndHasValidFormat);
cleansUpBeforeThrowing = cleansUpBeforeThrowing && keyExistsAndHasValidFormat;
if (dyld_get_program_sdk_version() > 0x7FFFF || cleansUpBeforeThrowing) {
if (cleansUpBeforeThrowing) {
_DSKeyValueRemoveObservationInfoForObject(object, watcher.observationInfo);
}
[NSException raise:NSInternalInconsistencyException format:@"An instance %p of class %@ was deallocated while key value observers were still registered with it. Current observation info: %@", object, notifyInfo->originalClass, watcher.observationInfo];
}
else {
NSLog(@"An instance %p of class %@ was deallocated while key value observers were still registered with it. Observation info was leaked, and may even become mistakenly attached to some other object. Set a breakpoint on NSKVODeallocateBreak to stop here in the debugger. Here's the current observation info:\n%@", object, notifyInfo->originalClass, watcher.observationInfo);
DSKVODeallocateBreak(object);
}
}
}
@catch (NSException *exception) {
[exception raise];
}
@finally {
_DSKeyValueRemoveObservationInfoWatcher(&watcher);
[watcher.observationInfo release];
}
}
大致流程:
- 获取跟该类的observationInfo
- 获取该类的原始类(父类)的dealloc方法并调用
- 判断是否有observationInfo,如果有的话就跑出异常
1.2.4 _isKVOA
(lldb) dis -s 0x7fff207b4403
Foundation`NSKVOIsAutonotifying:
内部调用的是NSKVOIsAutonotifying
使用hopper查看伪代码实现:
这里返回的是0x1
使用hopper搜索下_isKVOA
的实现
这里返回的是0x0
一个返回1一个返回0,那么这个猜测就是内部用来做KVO类和非KVO的区分的判断的;但是断点发现在触发kvo的时候并没有调用该方法,一时无法知道它的内部作用是啥;从源码中搜索一下KVOIsAutonotifying
Class _DSKVONotifyingOriginalClassForIsa(Class isa) {
if(class_getMethodImplementation(isa, ISKVOA_SELECTOR) == (IMP)DSKVOIsAutonotifying) {
void *ivars = object_getIndexedIvars(isa);
return ((DSKeyValueNotifyingInfo *)ivars)->originalClass;
}
return isa;
}
再去hopper中看下系统的实现:
int __NSKVONotifyingOriginalClassForIsa(int arg0) {
rbx = arg0;
if (class_getMethodImplementation(arg0, *0x7fff86b9d430) == _NSKVOIsAutonotifying) {
rbx = *object_getIndexedIvars(rbx);
}
rax = rbx;
return rax;
}
最后大致得知他的作用:KVO内部去获取原始类的时候,用来判断的,如果_isKVOA的实现是_NSKVOIsAutonotifying
那么就去获取它的原始类返回,否则就直接返回传入的类。
这里举个例子理解一下:
假设传入的是NSKVONotifying_TestKVOObject
此时获取它的_isKVOA的实现就是_NSKVOIsAutonotifying
这时候就需要去获取到它的原始类的class返回,如果传入的是TestKVOObject
那么它的_isKVOA的实现就不是_NSKVOIsAutonotifying
,而是-[NSObject(NSKeyValueObserverNotifying) _isKVOA]
,那么就直接返回了
至此KVO派生出的类中的几个方法的作用及大概实现已经看的差不多了;然而这里只是系统KVO实现的冰山一角,还有好多细节需要去探索
2.问答环节
看了上面的一大段讲解,通过下面问题来回顾一下
如何触发KVO
前提是添加了观察者
- 1.通过set方法设置属性
- 2.使用kvc的方式设置值
kvc会找是否有set方法去调用,对于属性通过kvc的方式去触发kvo是很容易理解的,测试发现定义的实例变量ivar,通过kvc也是可以触发kvo的,你知道为什么吗 - 3.手动调用
willChangeValueForKey
、didChangeValueForKey
看上面的实现,didChangeValueForKey内部最后调用到observer回调方法,那么只调用didChangeValueForKey
会触发kvo吗?
如何禁止KVO
- 实现
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
,返回NO - 自己实现一个NSKVONotifying_XXX类
@interface NSKVONotifying_TestKVOObject : TestKVOObject
@end
@implementation NSKVONotifying_TestKVOObject
@end
当添加观察者的时候就会报一下日志,同时KVO也失效了:
RuntimeLearning[36847:2908186] [general] KVO failed to allocate class pair for name NSKVONotifying_TestKVOObject, automatic key-value observing will not work for this class
如何hook某个实例对象
假如我只想hook某个类的某个实例的行为,对于类本身不产生影响--新创建的实例的行为还保持以前的逻辑
一般我们hook一个实例的方法,是通过交换方法的imp来达到目的,而这种实现则是对class中的方法的imp的交换,不符合我们上述的场景,当然你也可以在交换的imp中去判断是否是hook的实例来判断是否走hook之后的实现,还是以前的实现;这样也可以达到目的,但不太优雅
在看了KVO的实现之后,我们大概有了思路去hook某个类的某个实例对象的行为,而不去影响原类本身;那就是isa-swizzling技术。
大致思路:
动态派生一个子类
将子类的isa指向新创建的类
将子类的class方法hook掉返回原类方法
将需要hook的方法实现重写
我自己简易实现了一下KVO的逻辑,大致代码如下:
这个实现很简陋,但也大概实现了如何hook一个instance
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface HCSwizzleInstance : NSObject
void HCSwizzleHookInstance(id instance);
void HCSwizzleUnhookInstance(id instance);
// test
void HCObserveValueForKey(id instance, NSString *key);
void HCRemoveObserveValueForKey(id instance, NSString *key);
@end
//
// HCSwizzleInstance.m
// RuntimeLearning
//
// Created by 贺超 on 2020/5/22.
// Copyright © 2020 hechao. All rights reserved.
//
#import "HCSwizzleInstance.h"
#import <objc/runtime.h>
#import <objc/message.h>
#import <UIKit/UIKit.h>
#import <libffi-iOS/ffi.h>
#define kHCHookPrefix @"HC_HOOK_"
@interface HCKVOSetter : NSObject
- (void)testSetter:(id)obj;
@end
@implementation HCSwizzleInstance
void HCSwizzleHookInstance(id instance) {
_HCSwizzleHookInstance(instance, true);
}
void _HCSwizzleHookInstance(id instance, bool hookMethods) {
Class originalClass = object_getClass(instance);
if ([instance class] != originalClass) {
// 已经hook过了
return;
}
Class hookClass = objc_allocateClassPair(originalClass, HCHookClassName(originalClass), 0);
if (!hookClass) {
// The new class, or Nil if the class could not be created (for example, the desired name is already in use).
hookClass = objc_getClass(HCHookClassName(originalClass));
if (hookClass) {
object_setClass(instance, hookClass);
return;
}
}
if (hookMethods) {
// 这里如果需要对该实例的所有方法都做hook的话,比如用来记录一些执行的信息;那么就可以将方便列表遍历进行hook,但是需要一个统一的跳板来处理不同方法的不同参数及参数个数
unsigned int count;
Method *mList = class_copyMethodList(originalClass, &count);
for (unsigned int i = 0; i < count; i++) {
Method method = mList[i];
SEL selector = method_getName(method);
if ([NSStringFromSelector(selector) hasPrefix:kHCHookPrefix]) {
continue;
}
const char *mType = method_getTypeEncoding(method);
IMP originImp = method_getImplementation(method);
class_addMethod(hookClass, selector, originImp, mType);
class_addMethod(hookClass, selector, imp_implementationWithBlock(^(void){
return originImp;
}), mType);
// TODO:trampoline 需要一个通用的跳板来hook所有的方法
}
free(mList);
}
for (Class class in @[hookClass, object_getClass(hookClass)]) {
SEL classSEL = @selector(class);
Method oldMethod = class_getInstanceMethod(class, classSEL);
// 由于类的class的内部实现直接返回的类(self);实例对象的class内部实现是调用的object_getClass(self)
// 我们修改实例的isa的话,如果不做处理,就会在需要unhook的时候无法知道原class
// 所以我们这里将类以及实例的class方法hook掉返回原始的类,此时isa的值是修改之后的值
class_replaceMethod(class, classSEL, imp_implementationWithBlock(^(void){
return originalClass;
}), method_getTypeEncoding(oldMethod));
}
objc_registerClassPair(hookClass);
object_setClass(instance, hookClass);
}
void HCSwizzleUnhookInstance(id instance) {
Class hookClass = object_getClass(instance);
if ([instance class] != hookClass) {
object_setClass(instance, [instance class]);
//const char *name = class_getName(hookClass);
//objc_duplicateClass(hookClass, name, 0);
}
}
#pragma mark - Test KVO
void HCObserveValueForKey(id instance, NSString *key) {
if (!key || key.length == 0) {
return;
}
_HCSwizzleHookInstance(instance, false);
NSString *firstCharacter = [key substringToIndex:1];
NSString *tmpKey = [key stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:firstCharacter.uppercaseString];
SEL setSelector = NSSelectorFromString([NSString stringWithFormat:@"set%@:", tmpKey]);
Method oldMethod = class_getInstanceMethod([instance class], setSelector);
if (!oldMethod) {
return;
}
const char *mType = method_getTypeEncoding(oldMethod);
/*
IMP originImp = method_getImplementation(oldMethod);
class_replaceMethod([instance class], setSelector, imp_implementationWithBlock(^(void){
[instance willChangeValueForKey:key];
((void(*)(id, SEL, id))originImp)(instance, setSelector, @"1111");
[instance didChangeValueForKey:key];
}), mType);
*/
Method hookImpMethod = class_getInstanceMethod(HCKVOSetter.class, @selector(testSetter:));
IMP hookImp = method_getImplementation(hookImpMethod);
if (class_addMethod(object_getClass(instance), setSelector, hookImp, mType) == NO) {
class_replaceMethod(object_getClass(instance), setSelector, hookImp, mType);
}
}
void HCRemoveObserveValueForKey(id instance, NSString *key) {
// 这里应该将hook的setter方法的实现修改回去
if (!key || key.length == 0) {
return;
}
NSString *firstCharacter = [key substringToIndex:1];
NSString *tmpKey = [key stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:firstCharacter.uppercaseString];
SEL setSelector = NSSelectorFromString([NSString stringWithFormat:@"set%@:", tmpKey]);
Method oldMethod = class_getInstanceMethod([instance class], setSelector); // 拿到父类中的方法实现,也就是set方法
Method hookedMethod = class_getInstanceMethod(object_getClass(instance), setSelector); // 拿到kvo类的方法
if (!oldMethod) {
return;
}
method_setImplementation(hookedMethod, method_getImplementation(oldMethod)); // 将kvo类的set方法的实现修改成之前的实现
}
#pragma mark - Private Method
static const char *HCHookClassName(Class class) {
return [kHCHookPrefix stringByAppendingString:NSStringFromClass(class)].UTF8String;
}
@end
@implementation HCKVOSetter
/*
1.KVO监听,系统会动态子类化一个类NSKVONotifying_ClassName,并且重写了class的实现、set方法、delloc、_isKVOA
2.class内部实现返回的是KVO监听的类,而类的isa则指向的是NSKVONotifying_ClassName,可通过objc_getClass获取到
3.set方法内部实现,会调用willChangeValueForKey、调用原始的实现、didChangeValueForKey;didChangeValueForKey中会去调用observeValueForKeyPath
4.dealloc方法内部会做清理工作
5._isKVOA是做什么的
*/
static NSString * _Nullable getKey(SEL cmd);
- (void)testSetter:(id)obj {
NSString *key = getKey(_cmd);
if (!key) {
return;
}
Class cls = [self class];
void (*imp)(id, SEL, id);
Method originMethod = class_getInstanceMethod(cls, _cmd); // 获取原始的实现
imp = (void(*)(id, SEL, id))method_getImplementation(originMethod); // 拿到原始函数的imp
if ([cls automaticallyNotifiesObserversForKey:key]) {
//[self willChangeValueForKey:key]; // 走系统的那一套,找到observer去执行
id oldValue = [self valueForKey:key];
NSMutableDictionary *change = [NSMutableDictionary dictionary];
if (oldValue) {
[change setObject:oldValue forKey:@"old"];
}
if (obj) {
[change setObject:obj forKey:@"new"];
}
imp(self, _cmd, obj); // 得到imp去直接调用
//[self didChangeValueForKey:key];
[self observeValueForKeyPath:key ofObject:nil change:change.copy context:nil];
} else {
imp(self, _cmd, obj);
}
}
NSString *getKey(SEL cmd) {
//const char *selName = sel_getName(cmd);
NSString *selString = NSStringFromSelector(cmd);
if (!selString) {
return nil;
}
NSString *lowerSelString = selString.lowercaseString;
BOOL checkIsVaildSetter = [lowerSelString containsString:@"set"] && [lowerSelString containsString:@":"];
if (!checkIsVaildSetter) {
return nil;
}
NSRange setRange = [lowerSelString rangeOfString:@"set"];
NSInteger keyStart = setRange.location + setRange.length;
NSRange colonRange = [lowerSelString rangeOfString:@":"];
NSInteger keyEnd = colonRange.location;
if (keyEnd < keyStart) {
return nil;
}
NSString *tmpKeyString = [selString substringWithRange:NSMakeRange(keyStart, keyEnd - keyStart)];
NSString *firstCharacter = [tmpKeyString substringToIndex:1];
NSString *key = [tmpKeyString stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:firstCharacter.lowercaseString];
return key;
}
@end