KVO的原理初探及應(yīng)用

1. 實(shí)現(xiàn)原理

關(guān)于KVO的實(shí)現(xiàn)原理,蘋果有如下說明:

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技術(shù),當(dāng)有屬性被觀察則會(huì)將對(duì)象的isa指向一個(gè)中間類而不是本身的類;這樣做的好處就是不會(huì)影響到實(shí)例對(duì)象的原來的類
  • 同時(shí)我們不應(yīng)該依賴于isa去判斷類的繼承關(guān)系,應(yīng)該使用class方法去判斷;這里從側(cè)面說明class返回的還是原來的類

接下來通過例子來探究一下系統(tǒng)的實(shí)現(xiàn)

1.1 isa-swizzling

isa-swizzling簡單理解為:通過修改isa的指向,是isa指向另一個(gè)類來達(dá)到對(duì)對(duì)象的一些行為的修改

下面通過例子來看一下:
被觀察對(duì)象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);
    }
}

在添加觀察者前打上斷點(diǎn):


圖片.png

在添加觀察者后打上斷點(diǎn):


圖片.png

發(fā)現(xiàn)實(shí)例對(duì)象的isa的指向變了,變成了NSKVONotifying_TestKVOObject

用lldb打印一下看新創(chuàng)建一個(gè)TestKVOObject的實(shí)例,看看它的isa是否有變化,可以看到系統(tǒng)的kvo的isa-swizzling只對(duì)被觀察的對(duì)象實(shí)例產(chǎn)生了影響,并且class方法返回的還是原來的類;
原類在實(shí)例化一個(gè)對(duì)象出來它的isa指向的還是原來的類對(duì)象。

(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

接下來我們看看被觀察對(duì)象的isa指向的一個(gè)新類NSKVONotifying_XXX是怎樣設(shè)計(jì)的;通過
_shortMethodDescription命令在lldb打印出該類的方法及實(shí)例

(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個(gè)實(shí)例方法

- (void) setTestString:(id)arg1; (0x7fff207b5b57)
- (Class) class; (0x7fff207b4662)
- (void) dealloc; (0x7fff207b440b)
- (BOOL) _isKVOA; (0x7fff207b4403)

接下來一個(gè)個(gè)看這些方法是干了啥

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

內(nèi)部實(shí)現(xiàn)調(diào)用的是_NSSetObjectValueAndNotify,下個(gè)符號(hào)斷點(diǎn)看看是怎么實(shí)現(xiàn)的:

圖片.png

或者使用hopper查看Foundation.framework的偽代碼


圖片.png

通過hopper我們還發(fā)現(xiàn)對(duì)于不同的數(shù)據(jù)類型都有不同的set方法實(shí)現(xiàn),我們定義的是NSString類型,則調(diào)用的是_NSSetObjectValueAndNotify,如果你定義的是BOOL那么就會(huì)是_NSSetBoolValueAndNotify

大致的實(shí)現(xiàn)就是:

  • willChangeValueForKey
  • 調(diào)用原始的set方法
  • didChangeValueForKey

didChangeValueForKey內(nèi)部實(shí)現(xiàn)

void -[NSObject(NSKeyValueObserverNotification) didChangeValueForKey:](int arg0) {
    _NSKeyValueDidChangeWithPerThreadPendingNotifications(arg0, rdx, 0x0, _NSKeyValueDidChangeBySetting, 0x0);
    return;
}

為了看清didChangeValueForKey內(nèi)部的實(shí)現(xiàn),我在類中重寫了一下該方法,你也可以斷點(diǎn)調(diào)試一下

@implementation TestKVOObject

- (void)didChangeValueForKey:(NSString *)key {
    [super didChangeValueForKey:key];
}

@end

調(diào)用堆棧信息:

(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的調(diào)用堆棧,經(jīng)過一系列的函數(shù)跳轉(zhuǎn)最后執(zhí)行了
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被觀察者是派生出了一個(gè)子類來實(shí)現(xiàn)的,而且將派生的類的class返回的是原來的類

 - (Class)class {
     return object_getClass(self); // 返回的就是isa
 }
 
Class object_getClass(id obj)
 {
     if (obj) return obj->getIsa();
     else return Nil;
 }

對(duì)于為什么要重寫class方法,我個(gè)人認(rèn)為:

  • class返回被觀察者原來的類,這樣上層使用者就不必關(guān)注內(nèi)部的實(shí)現(xiàn)細(xì)節(jié),對(duì)于實(shí)例的使用,如果不使用runtime API的話,那么跟原來的類基本一致;比如isKindOfClass、isMemberOfClass等的判斷;
  • 類的class方法默認(rèn)是返回isa的,如果沒有重寫,那么返回的就是子類了,這樣假如上層使用者再通過isMemberOfClass去判斷的時(shí)候,那就出問題了啊,上層不知道有這個(gè)子類的存在的;
  • class方法和object_getClass返回的不一樣,也可以作為判斷該實(shí)例對(duì)象有沒有被isa-swizzling的判斷

1.2.3 重寫的dealloc方法

// 0x7fff207b4403位dealloc的地址
(lldb) dis -s 0x7fff207b4403
 Foundation`NSKVODeallocate:

內(nèi)部調(diào)用的是NSKVODeallocate,使用hopper看下偽代碼實(shí)現(xiàn):

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;
}

匯編看不懂,可以參照著網(wǎng)上大神根據(jù)偽代碼實(shí)現(xiàn)的邏輯對(duì)照看看
KVO實(shí)現(xiàn)

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方法并調(diào)用
  • 判斷是否有observationInfo,如果有的話就跑出異常

1.2.4 _isKVOA

(lldb) dis -s 0x7fff207b4403
Foundation`NSKVOIsAutonotifying:

內(nèi)部調(diào)用的是NSKVOIsAutonotifying
使用hopper查看偽代碼實(shí)現(xiàn):

圖片.png

這里返回的是0x1

使用hopper搜索下_isKVOA的實(shí)現(xiàn)

圖片.png

這里返回的是0x0

一個(gè)返回1一個(gè)返回0,那么這個(gè)猜測就是內(nèi)部用來做KVO類和非KVO的區(qū)分的判斷的;但是斷點(diǎn)發(fā)現(xiàn)在觸發(fā)kvo的時(shí)候并沒有調(diào)用該方法,一時(shí)無法知道它的內(nèi)部作用是啥;從源碼中搜索一下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中看下系統(tǒng)的實(shí)現(xiàn):

int __NSKVONotifyingOriginalClassForIsa(int arg0) {
    rbx = arg0;
    if (class_getMethodImplementation(arg0, *0x7fff86b9d430) == _NSKVOIsAutonotifying) {
            rbx = *object_getIndexedIvars(rbx);
    }
    rax = rbx;
    return rax;
}

最后大致得知他的作用:KVO內(nèi)部去獲取原始類的時(shí)候,用來判斷的,如果_isKVOA的實(shí)現(xiàn)是_NSKVOIsAutonotifying那么就去獲取它的原始類返回,否則就直接返回傳入的類。

這里舉個(gè)例子理解一下:
假設(shè)傳入的是NSKVONotifying_TestKVOObject此時(shí)獲取它的_isKVOA的實(shí)現(xiàn)就是_NSKVOIsAutonotifying這時(shí)候就需要去獲取到它的原始類的class返回,如果傳入的是TestKVOObject那么它的_isKVOA的實(shí)現(xiàn)就不是_NSKVOIsAutonotifying,而是-[NSObject(NSKeyValueObserverNotifying) _isKVOA],那么就直接返回了

至此KVO派生出的類中的幾個(gè)方法的作用及大概實(shí)現(xiàn)已經(jīng)看的差不多了;然而這里只是系統(tǒng)KVO實(shí)現(xiàn)的冰山一角,還有好多細(xì)節(jié)需要去探索

2.問答環(huán)節(jié)

看了上面的一大段講解,通過下面問題來回顧一下
如何觸發(fā)KVO
前提是添加了觀察者

  • 1.通過set方法設(shè)置屬性
  • 2.使用kvc的方式設(shè)置值
    kvc會(huì)找是否有set方法去調(diào)用,對(duì)于屬性通過kvc的方式去觸發(fā)kvo是很容易理解的,測試發(fā)現(xiàn)定義的實(shí)例變量ivar,通過kvc也是可以觸發(fā)kvo的,你知道為什么嗎
  • 3.手動(dòng)調(diào)用willChangeValueForKey、didChangeValueForKey
    看上面的實(shí)現(xiàn),didChangeValueForKey內(nèi)部最后調(diào)用到observer回調(diào)方法,那么只調(diào)用didChangeValueForKey會(huì)觸發(fā)kvo嗎?

如何禁止KVO

  • 實(shí)現(xiàn)+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key,返回NO
  • 自己實(shí)現(xiàn)一個(gè)NSKVONotifying_XXX類
@interface NSKVONotifying_TestKVOObject : TestKVOObject

@end

@implementation NSKVONotifying_TestKVOObject

@end

當(dāng)添加觀察者的時(shí)候就會(huì)報(bào)一下日志,同時(shí)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某個(gè)實(shí)例對(duì)象

假如我只想hook某個(gè)類的某個(gè)實(shí)例的行為,對(duì)于類本身不產(chǎn)生影響--新創(chuàng)建的實(shí)例的行為還保持以前的邏輯

一般我們hook一個(gè)實(shí)例的方法,是通過交換方法的imp來達(dá)到目的,而這種實(shí)現(xiàn)則是對(duì)class中的方法的imp的交換,不符合我們上述的場景,當(dāng)然你也可以在交換的imp中去判斷是否是hook的實(shí)例來判斷是否走h(yuǎn)ook之后的實(shí)現(xiàn),還是以前的實(shí)現(xiàn);這樣也可以達(dá)到目的,但不太優(yōu)雅

在看了KVO的實(shí)現(xiàn)之后,我們大概有了思路去hook某個(gè)類的某個(gè)實(shí)例對(duì)象的行為,而不去影響原類本身;那就是isa-swizzling技術(shù)。

大致思路:

動(dòng)態(tài)派生一個(gè)子類
將子類的isa指向新創(chuàng)建的類
將子類的class方法hook掉返回原類方法
將需要hook的方法實(shí)現(xiàn)重寫

我自己簡易實(shí)現(xiàn)了一下KVO的邏輯,大致代碼如下:
這個(gè)實(shí)現(xiàn)很簡陋,但也大概實(shí)現(xiàn)了如何hook一個(gè)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) {
        // 已經(jīng)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) {
        // 這里如果需要對(duì)該實(shí)例的所有方法都做hook的話,比如用來記錄一些執(zhí)行的信息;那么就可以將方便列表遍歷進(jìn)行hook,但是需要一個(gè)統(tǒng)一的跳板來處理不同方法的不同參數(shù)及參數(shù)個(gè)數(shù)
        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 需要一個(gè)通用的跳板來hook所有的方法
        }
        free(mList);
    }
    for (Class class in @[hookClass, object_getClass(hookClass)]) {
        SEL classSEL = @selector(class);
        Method oldMethod = class_getInstanceMethod(class, classSEL);
        // 由于類的class的內(nèi)部實(shí)現(xiàn)直接返回的類(self);實(shí)例對(duì)象的class內(nèi)部實(shí)現(xiàn)是調(diào)用的object_getClass(self)
        // 我們修改實(shí)例的isa的話,如果不做處理,就會(huì)在需要unhook的時(shí)候無法知道原class
        // 所以我們這里將類以及實(shí)例的class方法hook掉返回原始的類,此時(shí)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) {
    // 這里應(yīng)該將hook的setter方法的實(shí)現(xiàn)修改回去
    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); // 拿到父類中的方法實(shí)現(xiàn),也就是set方法
    Method hookedMethod = class_getInstanceMethod(object_getClass(instance), setSelector); // 拿到kvo類的方法
    if (!oldMethod) {
        return;
    }
    method_setImplementation(hookedMethod, method_getImplementation(oldMethod)); // 將kvo類的set方法的實(shí)現(xiàn)修改成之前的實(shí)現(xiàn)
}

#pragma mark - Private Method

static const char *HCHookClassName(Class class) {
  return [kHCHookPrefix stringByAppendingString:NSStringFromClass(class)].UTF8String;
}

@end

@implementation HCKVOSetter
/*
 1.KVO監(jiān)聽,系統(tǒng)會(huì)動(dòng)態(tài)子類化一個(gè)類NSKVONotifying_ClassName,并且重寫了class的實(shí)現(xiàn)、set方法、delloc、_isKVOA
 2.class內(nèi)部實(shí)現(xiàn)返回的是KVO監(jiān)聽的類,而類的isa則指向的是NSKVONotifying_ClassName,可通過objc_getClass獲取到
 3.set方法內(nèi)部實(shí)現(xiàn),會(huì)調(diào)用willChangeValueForKey、調(diào)用原始的實(shí)現(xiàn)、didChangeValueForKey;didChangeValueForKey中會(huì)去調(diào)用observeValueForKeyPath
 4.dealloc方法內(nèi)部會(huì)做清理工作
 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); // 獲取原始的實(shí)現(xiàn)
    imp = (void(*)(id, SEL, id))method_getImplementation(originMethod); // 拿到原始函數(shù)的imp
    if ([cls automaticallyNotifiesObserversForKey:key]) {
        //[self willChangeValueForKey:key]; // 走系統(tǒng)的那一套,找到observer去執(zhí)行
        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去直接調(diào)用
        //[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

4. 參考文檔

Key-Value Observing Implementation Details
DIS_KVC_KVO

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容