OC-KVC原理分析

OC之Method_Swizling一些坑點、KVC原理分析

Method_Swizling

Method_Swizling我們并不陌生,通過交換兩個方法SEL的IMP指向,達到方法交換的目的。一般來說,我們通常寫在cateogry里,在+load()方法里實現(xiàn)方法的交換。

常規(guī)的方法交換

// SSJPerson.h
@interface SSJPerson : NSObject

  • (void)person_walk;
    @end

// SSJPerson.m

import "SSJPerson.h"

@implementation SSJPerson

  • (void)person_walk{
    NSLog(@"SSJPerson ---> person_walk");
    }

// SSJStudent.h
// SSJStudent 繼承自 SSJPerson
@interface SSJStudent : SSJPerson

  • (void)student_sleep;
    @end

// SSJStudent.m

import "SSJStudent.h"

@implementation SSJStudent

  • (void)student_sleep{
    NSLog(@"SSJStudent ---> student_sleep");
    }
    @end

//SSJStudent+category.m

import "SSJStudent+category.h"

import <objc/runtime.h>

@implementation SSJStudent (category)

  • (void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
    Method originalMethod = class_getInstanceMethod([self class], @selector(student_sleep));
    Method swizzlingMethod = class_getInstanceMethod([self class], @selector(student_sleepNew));
    method_exchangeImplementations(originalMethod, swizzlingMethod);
    });

}

  • (void)student_sleepNew{
    NSLog(@"替換過的方法 -- > student_sleepNew");
    [self student_sleepNew];
    }
    @end
    復制代碼
    調用的時候:

SSJStudent *stu = [SSJStudent new];
[stu student_sleep];
復制代碼
運行也沒問題:

image.png

父類實現(xiàn),子類沒有實現(xiàn)

那么替換一個父類的已經(jīng)實現(xiàn)了,但當前cateogry類沒實現(xiàn)的方法呢?

對代碼進行修改:

// SSJStudent+category.m

import "SSJStudent+category.h"

import <objc/runtime.h>

@implementation SSJStudent (category)

  • (void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
    /// 替換父類方法:person_walk
    /// 父類實現(xiàn)了person_walk,子類并沒實現(xiàn)person_walk
    Method originalMethod = class_getInstanceMethod([self class], @selector(person_walk));
    Method swizzlingMethod = class_getInstanceMethod([self class], @selector(person_walkNew));
    method_exchangeImplementations(originalMethod, swizzlingMethod);
    });

}

//- (void)student_sleepNew{
// NSLog(@"替換過的方法 -- > student_sleepNew");
// [self student_sleepNew];
//}

  • (void)person_walkNew{
    NSLog(@"替換過的方法 -- > person_walkNew");
    [self person_walkNew];
    }

@end
復制代碼
// ViewController.m
@implementation ViewController

  • (void)viewDidLoad {
    [super viewDidLoad];

    SSJStudent *student = [SSJStudent new];
    /// 這里換成person_walk
    [student person_walk];
    }

@end
復制代碼
運行:

image.png

看打印結果,子類調用person_walk都沒問題。

這里對ViewController.m添加兩行代碼:

// ViewController.m
@implementation ViewController

  • (void)viewDidLoad {
    [super viewDidLoad];

    SSJStudent *student = [SSJStudent new];
    /// 這里換成person_walk
    [student person_walk];
    NSLog(@"\n");
    /// 添加代碼,父類也調用person_walk
    SSJPerson *person = [SSJPerson new];
    [person person_walk];
    }

@end
復制代碼
再次運行,就發(fā)現(xiàn)提示找不到方法:

image.png

這邊我畫了一張圖:

image.png

由于SSJStudent+category內(nèi)部實現(xiàn)了+load()方法,導致程序在load_images階段,就調用了+load()方法。

而+load()方法里對父類(SSJPerson)的person_walk方法進行了替換,導致父類在調用自己方法person_walk的時候,提示找不到具體的person_walkNew實現(xiàn),因為父類根本就沒這個方法。

在實際多人開發(fā)過程中,提供父類的那個人他不一定知道你交換了父類的方法,當他調用自己父類的方法時,可能就一下子對這個報錯感到莫名其妙:我明明沒有調用這個方法啊,為什么提示這個錯誤?

如何避免這種子類替換了父類的方法,子類自己卻沒有實現(xiàn)父類方法的情況呢?
我們對SSJStudent+category.m的+load()方法進行修改:

// SSJStudent+category.m

import "SSJStudent+category.h"

import <objc/runtime.h>

@implementation SSJStudent (category)

  • (void)load{
    /// 替換父類方法:person_walk
    /// 父類實現(xiàn)了person_walk,子類并沒實現(xiàn)person_walk
    Method originalMethod = class_getInstanceMethod([self class], @selector(person_walk));
    Method swizzlingMethod = class_getInstanceMethod([self class], @selector(person_walkNew));
    //添加一個Method(SEL - person_walk,IMP - person_walkNew)
    BOOL isAdded = class_addMethod([self class], method_getName(originalMethod), method_getImplementation(swizzlingMethod), method_getTypeEncoding(originalMethod));
    if(isAdded){
    /// 添加成功 ,就說明子類沒有實現(xiàn)父類person_walk對應的IMP方法.
    /// 經(jīng)過class_addMethod這一步,子類已經(jīng)有了一個person_walk方法,并且IMP指向person_walkNew
    /// 接下來,直接添加一個Method(SEL - person_walkNew,IMP - person_walk)
    /// 然后子類就實現(xiàn)了有了兩個IMP互相交換的Method,最終效果跟method_exchangeImplementations一樣
    class_replaceMethod([self class], method_getName(swizzlingMethod), method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
    }else{
    ///添加不成功,說明子類本身就已經(jīng)實現(xiàn)了person_walk的IMP方法,那就直接交換兩個Method的IMP即可
    method_exchangeImplementations(originalMethod, swizzlingMethod);
    }

}
復制代碼
運行效果:

image.png

簡單來說,就是:

class_addMethod添加Method(SEL - person_walk,IMP - person_walkNew)

成功 -》則調用class_replaceMethod添加Method(SEL - person_walkNew,IMP - person_walk)。

失敗 -》則調用method_exchangeImplementations交換兩個Method的IMP指向。

為了便于理解,我畫了張圖

image.png

說明:

class_addMethod:只能在SEL沒有IMP指向時才可以添加成功;
class_replaceMethod:不管SEL 有沒有IMP實現(xiàn),都可以添加成功;
父類沒有實現(xiàn),子類也沒有實現(xiàn)

把父類實現(xiàn)部分注釋

image.png

然后再運行:

image.png

提示找不到這個person_walk這個方法實現(xiàn),那就說明category那里出了問題。
我們在class_getInstanceMethod那一行打上斷點:

image.png

由于父類和子類都沒有實現(xiàn)person_walk,導致這邊獲取的originalMethod為空。

我們對category的+load()方法進行修改,添加originalMethod空值處理:

  • (void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{

      /// 替換父類方法:person_walk
      /// 父類實現(xiàn)了person_walk,子類并沒實現(xiàn)person_walk
      Method originalMethod = class_getInstanceMethod([self class], @selector(person_walk));
      Method swizzlingMethod = class_getInstanceMethod([self class], @selector(person_walkNew));
      if (!originalMethod) {
      /// 沒有,那就添加一個person_walk方法,并且手動添加一個臨時處理的IMP實現(xiàn)
         class_addMethod([self class], @selector(person_walk), method_getImplementation(swizzlingMethod), method_getTypeEncoding(swizzlingMethod));
          /// originalMethod需要重新獲取一邊,不然依舊是空的
          originalMethod = class_getInstanceMethod([self class], @selector(person_walk));
          method_setImplementation(originalMethod, imp_implementationWithBlock(^(id self,SEL _cmd){
              NSLog(@"臨時方法");
          }));
      }
      //添加一個Method(SEL - person_walk,IMP - person_walkNew)
      BOOL isAdded = class_addMethod([self class], method_getName(originalMethod), method_getImplementation(swizzlingMethod), method_getTypeEncoding(originalMethod));
      if(isAdded){
          /// 添加成功 ,就說明子類沒有實現(xiàn)父類person_walk對應的IMP方法.
          /// 經(jīng)過class_addMethod這一步,子類已經(jīng)有了一個person_walk方法,并且IMP指向person_walkNew
          /// 接下來,直接添加一個Method(SEL - person_walkNew,IMP - person_walk)
          /// 然后子類就實現(xiàn)了有了兩個IMP互相交換的Method,最終效果跟method_exchangeImplementations一樣
          class_replaceMethod([self class], method_getName(swizzlingMethod), method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
      }else{
          ///添加不成功,說明子類本身就已經(jīng)實現(xiàn)了person_walk的IMP方法,那就直接交換兩個Method的IMP即可
          method_exchangeImplementations(originalMethod, swizzlingMethod);
      }
    

    });

}
復制代碼
運行:

image.png

KVC分析

在講解KVC之前,請先允許我演示一段騷操作,對SSJPerson類的未開放屬性進行讀寫操作:

// SSJPerson.h

import <Foundation/Foundation.h>

@interface SSJPerson : NSObject
@end

// SSJPerson.m

import "SSJPerson.h"

@interface SSJPerson ()
/// 昵稱
@property (nonatomic , strong) NSString *nickName_private;
@end
@implementation SSJPerson
@end
復制代碼
image.png

如圖所示,對于一個未開放出來的屬性,我們無法通過對象.屬性名這種常規(guī)的方式進行訪問。但是我們可以利用setValue:forKey:這方式進行賦值。

這種通過setValue:forKey:方式進行賦值的操作,我們稱之為KVC。
KVC是一種設計模式,那么它的原理又是什么呢?為什么可以對未開放屬性進行直接操作呢?

存在即是真理。帶著探索的思維,我們決定去看一下setValue:forKey:的底層實現(xiàn)。

進入蘋果官方文檔:KVC部分

image.png

大概意思是:
NSObject提供的NSKeyValueCoding協(xié)議,默認實現(xiàn)使用一組明確定義的規(guī)則,將基于密鑰的訪問器調用映射到對象的底層屬性。這些協(xié)議方法使用一個關鍵參數(shù)來搜索它們自己的對象實例,以查找訪問器、實例變量和遵循某些命名約定的相關方法。

Setter

接下來看一下Setter搜索模式: image.png

按照圖上所說,Setter搜索模式分為3步:

找set<Key>: 或 _set<Key>,找到了就調用它;

如果沒找到,就去依次查找_<key>, is<Key>, <key>, 或is<Key>,找到了就用輸入值設置變量(比如找到了查找<key>,那么后面的_is<Key>等就不需要找了)。

如果還是沒找到,就會調用setValue:forUndefinedKey:并引發(fā)異常。

我們來根據(jù)這3個步驟,實操一下:

// SSJPerson.h
@interface SSJPerson : NSObject{
@public
NSString *boddy;
NSString *_boddy;
NSString *isBoddy;
NSString *_isBoddy;
}
@end

// SSJPerson.m
//(根據(jù)第2步,找個要設置為YES)
@implementation SSJPerson

  • (BOOL)accessInstanceVariablesDirectly{
    return true;
    }
    @end

// ViewController.m

  • (void)viewDidLoad {
    [super viewDidLoad];
    /// 設置值
    [personA setValue:@"足球" forKey:@"boddy"];
    /// 打印內(nèi)容,我們要看一下具體賦值給哪個值
    NSLog(@"_<key>---%@",personA->_boddy);
    NSLog(@"_is<Key>---%@",personA ->_isBoddy);
    NSLog(@"<key>---%@",personA ->boddy);
    NSLog(@"is<Key>---%@",personA ->isBoddy);
    }
    復制代碼
    運行:

image.png

注釋_boddy:

image.png

注釋_isBoddy:

image.png

這也就驗證了里第2點:當存在多個類似變量,會依次查找_<key>, _is<Key>, <key>, 或is<Key>,找到了就給它賦值,后面的就賦值了。
思考:關于第一點set<Key>:,是不是也有第2點類似的關系呢?

image.png

注釋setBoddy方法:

image.png

注釋_setBoddy方法:

image.png

注釋setIsBoddy方法:

image.png

經(jīng)過4次打印,我們發(fā)現(xiàn),在我們調用setValue:forKey:的時候,會依次查找: set<Key>: > _set<Key> > setIs<Key>。找到后就用輸入值賦值給變量

Getter

接下來看一下Getter:

IMG_0978.JPG

大概意思如下:

1、查找 get<Key>, <key>, is<Key>, or _<key>,找到了就進入步驟5;找不到就進入步驟2;

2、在實例方法中搜索:countOf<Key>和objectIn<Key>AtIndex和<key>AtIndexes:,
countOf<Key>必須實現(xiàn),另外兩個找到了其中一個,就創(chuàng)建集合代理對象,找不到就進入步驟3;

3、改為搜索countOf<Key>、Enumeratorf<Key>和memberOf<Key>:,3個方法都存在才行,否則就進入步驟4;

4、當確定AccessInstanceVariables方法返回YES(默認也是YES),
順序搜索名為 _<key>, _is<Key>, <key>, 或is<Key>的實例變量。
如果找到,直接獲取實例變量的值并繼續(xù)執(zhí)行步驟5。否則,繼續(xù)執(zhí)行步驟6。

5、如果檢索到的屬性值是對象指針,只需返回結果。
如果該值是NSNumber支持的標量類型,請將其存儲在NSNumber實例中并返回該值。
如果結果是NSNumber不支持的標量類型,請轉換為NSValue對象并返回該對象。

6、如果都找不到,調用valueForUndefinedKey:并拋出異常。
復制代碼
針對第1點,我們來進行實操:

// SSJPerson.m

  • (NSString *)getBoddy{
    NSLog(@"%s -->Getter",func);
    return boddy;
    }

  • (NSString *)boddy{
    NSLog(@"%s -->Getter",func);
    return boddy;
    }

  • (NSString *)isBoddy{
    NSLog(@"%s -->Getter",func);
    return isBoddy;
    }

  • (NSString *)_boddy{
    NSLog(@"%s -->Getter",func);
    return _boddy;
    }

// ViewController.m

  • (void)viewDidLoad {
    [super viewDidLoad];
    SSJPerson *personA = [SSJPerson new];
    /// 設置值
    [personA setValue:@"足球" forKey:@"boddy"];
    NSLog(@"打印---%@",[personA valueForKey:@"boddy"]);
    }

復制代碼
運行:

image.png

注釋getBoddy:

image.png

注釋boddy:

image.png

注釋isBoddy: image.png

至于為什么只有boddy方法執(zhí)行之后,valueForKey才打印出內(nèi)容,那是因為默認情況下,setter和getter是一一對應的關系,setter模式優(yōu)先執(zhí)行<key>方法,getter模式對應著_boddy方法。

針對第2點,我們來進行實操

在實例方法中搜索:countOf<Key>和objectIn<Key>AtIndex和<key>AtIndexes:,
countOf<Key>必須實現(xiàn),另外兩個找到了其中一個,就創(chuàng)建集合代理對象,找不到就進入步驟
復制代碼
這里不能使用boddyArray,不然會走第一步的Getter方法:

image.png

把key改為myBoddyArray

image.png

注釋掉objectInMyBoddyArrayAtIndex:方法:

image.png

針對第3點,我們來進行實操

改為搜索countOf<Key>、Enumeratorf<Key>和memberOf<Key>:,3個方法都存在才行,否則就進入步驟4;
復制代碼
image.png

針對第4點,我們來進行實操

當確定AccessInstanceVariables方法返回YES(默認也是YES),
順序搜索名為 _<key>, _is<Key>, <key>, 或is<Key>的實例變量。
如果找到,直接獲取實例變量的值并繼續(xù)執(zhí)行步驟5。否則,繼續(xù)執(zhí)行步驟6。
復制代碼
image.png 注釋_boddy:

image.png

注釋_isBoddy:

image.png

注釋boddy:

image.png

針對第6點,我們來進行實操

如果都找不到,調用valueForUndefinedKey:并拋出異常
復制代碼
在不做處理的情況下,用一個不存在的key去訪問,會報錯:

image.png

我們實現(xiàn)一下valueForUndefinedKey:,然后:

image.png

總結一下KVC的Setter和Getter

Setter:

依次查找set<Key>: 或 _set<Key>,找到了就調用方法;找不到就進入步驟2;

先確定AccessInstanceVariables返回YES,然后依次查找_<key>, _is<Key>, <key>, 或is<Key>,找到了就用輸入值設置變量。找不到就進入步驟3;

(比如找到了查找_<key>,那么后面的_is<Key>等就不需要找了)。

調用setValue:forUndefinedKey:并引發(fā)異常。
Getter:

查找 get<Key>, <key>, is<Key>, or _<key>,找到了就進入步驟5;找不到就進入步驟2;

在實例中搜索:countOf<Key>和objectIn<Key>AtIndex和<key>AtIndexes:, 其中countOf<Key>必須實現(xiàn),另外兩個找到了其中一個,就創(chuàng)建集合代理對象,找不到就進入步驟3;

改為搜索countOf<Key>、enumeratorf<Key>和memberOf<Key>:,3個方法都存在才行,否則就進入步驟4;

當確定AccessInstanceVariables方法返回YES(默認也是YES), 順序搜索名為 _<key>, _is<Key>, <key>, 或is<Key>的實例變量。 如果找到,直接獲取實例變量的值并繼續(xù)執(zhí)行步驟5。否則,繼續(xù)執(zhí)行步驟6。

如果檢索到的屬性值是對象指針,只需返回結果。 如果該值是NSNumber支持的標量類型,請將其存儲在NSNumber實例中并返回該值。 如果結果是NSNumber不支持的標量類型,請轉換為NSValue對象并返回該對象。

如果都找不到,調用valueForUndefinedKey:并拋出異常。

至此,我們就完成了KVC的探索。

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

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