廢話開篇:簡單代碼模擬 KVO 派生類實(shí)現(xiàn)方式,當(dāng)然,有人會(huì)有疑問,“網(wǎng)上的例子多的是,看看就行,為什么還要寫?”。其實(shí)個(gè)人理解的話有些知識(shí)點(diǎn)光看不行,寫一寫,再說一說會(huì)對(duì)自身的認(rèn)知是有一個(gè)提升的。尤其是與掘友分享交流還能有新的感悟,共同學(xué)習(xí)進(jìn)步。
一、實(shí)現(xiàn)邏輯前的鋪墊
1、如何理解派生類
其實(shí)“派生”個(gè)人理解它沒有特別的含義,它也僅僅是個(gè)類,只不過它是為了服務(wù)某個(gè)特定場景而創(chuàng)建的,在實(shí)際開發(fā)中,系統(tǒng)產(chǎn)生的“派生”類是隱密起來的類,不會(huì)像 UIButton 這樣的類呼之即來。
2、類到底是什么
類其實(shí)也是對(duì)象,可能話剛說完就會(huì)有人反駁,“不對(duì)!如果類是對(duì)象,那么,實(shí)例對(duì)象是什么?”。從代碼邏輯上或許根本沒有“類”、“對(duì)象”的區(qū)分,只不過是為了讓程序的更好的被理解人為的給某些特定的代碼統(tǒng)稱為類。load 方法的執(zhí)行就理解為一個(gè)類開辟了內(nèi)存地址,而類對(duì)應(yīng)的內(nèi)存區(qū)域內(nèi)會(huì)根據(jù)程序設(shè)定保存著相關(guān)的屬性列表、方法列表等。一個(gè)類的實(shí)例對(duì)象的isa指針就是指向類的內(nèi)存地址,所以,一個(gè)對(duì)象的可以響應(yīng)的方法或者能夠保存的屬性都是通過isa找到類,去查找能否響應(yīng)方法或者是否有對(duì)應(yīng)屬性。
如果你正在面試,或者正準(zhǔn)備跳槽,不妨看看我精心總結(jié)的iOS大廠面試資料:https://gitee.com/Mcci7/i-oser 來獲取一份詳細(xì)的大廠面試資料 為你的跳槽加薪多一份保障
這里說點(diǎn)別的,大家也知道JS下也有原型鏈的概念,展示一段代碼:
//動(dòng)物類
var Animal = function(){
this.run = function(){
console.log('我是animal類,我在跑');
}
};
var animal = new Animal();
animal.eat = function(){
console.log('我是animal類,我在吃飯');
}
//人類
var Person = function(){};
//設(shè)置animal為Person類的父類
Person.prototype = animal;
var person = new Person();
//執(zhí)行父類存在的方法
person.run();
person.eat();
復(fù)制代碼
運(yùn)行結(jié)果:
[圖片上傳失敗...(image-2507aa-1647873295887)]
可以看出,給 Person 這個(gè)類設(shè)置prototype的值為一個(gè)Animal類的實(shí)例對(duì)象。那么,person 的實(shí)例對(duì)象就可以繼承Animal類的所有方法。
3、修改isa指針會(huì)發(fā)生什么
isa指針是實(shí)例對(duì)象的一個(gè)默認(rèn)“成員變量”,它保存的是類的地址。修改isa后僅僅是修改了類的指向,并不會(huì)對(duì)實(shí)例對(duì)象已開辟的內(nèi)存數(shù)據(jù)存在影響。創(chuàng)建一個(gè)“派生”類后,通過runtime對(duì)“派生”類的結(jié)構(gòu)進(jìn)行動(dòng)態(tài)調(diào)整,再將實(shí)例對(duì)象的isa指針指向“派生”類,那么,實(shí)例對(duì)象就會(huì)響應(yīng)“派生”類一切新增方法。為了保證運(yùn)行的穩(wěn)定性,“派生”類最好繼承自當(dāng)前實(shí)例對(duì)象所屬類。
二、代碼簡單實(shí)現(xiàn)
1、創(chuàng)建Animal類
@interface Animal : NSObject
@property(nonatomic,strong) NSString * name;
@property(nonatomic,strong) NSString * address;
@property(nonatomic,strong) NSString * color;
@end
@implementation Animal
- (instancetype)init
{
if (self = [super init]) {
self.color = @"紅色";
}
return self;
}
- (void)eat
{
NSLog(@"我是動(dòng)物類,我在吃飯");
}
- (void)setName:(NSString *)name
{
_name = name;
NSLog(@"我是原setName方法\n");
}
- (void)dealloc
{
NSLog(@"動(dòng)物%@銷毀了",self);
}
@end
復(fù)制代碼
很簡單的一個(gè)類,聲明 name、 address、 color三個(gè)屬性。為證明修改isa指針不影響實(shí)例對(duì)象的內(nèi)存數(shù)據(jù),在初始化的時(shí)候就對(duì) Animal 的顏色進(jìn)行紅色設(shè)置。
2、創(chuàng)建 NSObject+WSLObserver 分類
(1)NSObject+WSLObserver.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface NSObject (WSLObserver)
//添加觀察者
- (void)addWSLObserverWithKey:(NSString *)key callBack:(void(^)(id _self,id obj))callBack;
@end
NS_ASSUME_NONNULL_END
復(fù)制代碼
這里就 addWSLObserverWithKey 方法,參數(shù) key 是要觀察的屬性,callBack 是修改屬性的時(shí)候進(jìn)行的回調(diào)通知。
(2)NSObject+WSLObserver.m
#import "NSObject+WSLObserver.h"
#import <objc/runtime.h>
#import <objc/message.h>
static char * callBackDicStr;
@implementation NSObject (WSLObserver)
//C 方法實(shí)現(xiàn)
void setValue(id self,SEL _sel,id value){
Class currentClass = [self class];
//將isa指向父類
object_setClass(self, class_getSuperclass([self class]));
//執(zhí)行父類方法
((void (*) (id, SEL, id)) (void *)objc_msgSend)(self, _sel, value);
//設(shè)置為派生類
object_setClass(self, currentClass);
//執(zhí)行派生類方法
NSMutableDictionary * callBackDic = objc_getAssociatedObject(self, &callBackDicStr);
void(^callBack)(id,id) = (void(^)(id,id))callBackDic[NSStringFromSelector(_sel)];
if (callBack) {
callBack(self,value);
}
}
- (void)addWSLObserverWithKey:(NSString *)key callBack:(void(^)(id _self,id obj))callBack
{
//當(dāng)前類
Class class = [self class];
//派生類
Class newClass = class;
//判斷key值是否存在
if (![self checkIvarIsExist:class key:key]) {
NSLog(@"暫無 %@ 屬性",key);
return;
}
//創(chuàng)建派生類
NSString * classStr = NSStringFromClass(class);
NSString * newClassStr = [NSString stringWithFormat:@"WSLDerived%@",classStr];
char * newClassChar = (char*) [newClassStr UTF8String];
//是否為派生類
BOOL isDerived = [NSStringFromClass(class) hasPrefix:@"WSLDerived"];
if (!isDerived) {
//派生類
newClass = objc_allocateClassPair(class,newClassChar,0);
//修改isa指針
object_setClass(self, newClass);
}
//重寫觀察屬性的 setter 方法
NSString * setKey = @"";
if (key.length > 0) {
setKey = [key stringByReplacingCharactersInRange:NSMakeRange(0,1) withString:[[key substringToIndex:1] capitalizedString]];
} else {
return;
}
//關(guān)聯(lián)屬性
NSMutableDictionary * callBackDic = objc_getAssociatedObject(self, &callBackDicStr);
if (!callBackDic) {
callBackDic = [[NSMutableDictionary alloc] init];
objc_setAssociatedObject(self,&callBackDicStr,callBackDic, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
callBackDic[[NSString stringWithFormat:@"set%@:",setKey]] = callBack;
//添加方法
SEL setSel = NSSelectorFromString([NSString stringWithFormat:@"set%@:",setKey]);
class_addMethod(newClass, setSel, (IMP)setValue,"v@:@");
}
//判斷key值是否存在
- (BOOL)checkIvarIsExist:(Class)class key:(NSString *)key
{
BOOL isExist = NO;
unsigned int outCount, i;
//當(dāng)前類
Ivar * ivars = class_copyIvarList(class, &outCount);
for (i = 0; i < outCount; i++){
Ivar ivar = ivars[i];
NSString * ivarName = [[NSString alloc] initWithCString:ivar_getName(ivar) encoding:NSUTF8StringEncoding];
if ([ivarName isEqualToString:[NSString stringWithFormat:@"_%@",key]]) {
free(ivars);
return YES;
}
}
//父類遞歸判斷
if (!isExist) {
if ([class superclass]) {
isExist = [self checkIvarIsExist:[class superclass] key:key];
}
}
return isExist;
}
復(fù)制代碼
首先,開頭聲明了一個(gè) setValue c語言函數(shù),這里方法名字其實(shí)無關(guān)緊要,目的是要它的IMP。它的方法體部分主要是來實(shí)現(xiàn)調(diào)用父類的set方法及自身關(guān)聯(lián)對(duì)象保存的回調(diào)callBack的執(zhí)行。其實(shí)就相當(dāng)于重寫了set方法并調(diào)用super方法。
再次,addWSLObserverWithKey方法里,創(chuàng)建了一個(gè)繼承自實(shí)例對(duì)象所屬類的派生類,修改isa指針指向派生類,為派生類添加了對(duì)于需要觀察屬性的set方法。將addWSLObserverWithKey方法傳進(jìn)來的回調(diào)閉包保存在一個(gè)NSMutableDictionary類型的關(guān)聯(lián)屬性下,以 set方法名作為key,以回調(diào)callBack作為value。
最后,animal 在做屬性賦值的時(shí)候,進(jìn)行的其實(shí)是派生類的set方法。
三、外部調(diào)用
邏輯代碼:
Animal * animal = [[Animal alloc] init];
NSLog(@"修改isa指針前 animal = %@",animal);
[animal addWSLObserverWithKey:@"name" callBack:^(id _Nonnull _self, id _Nonnull obj) {
NSLog(@"\n name = %@ \n",obj);
}];
[animal addWSLObserverWithKey:@"address" callBack:^(id _Nonnull _self, id _Nonnull obj) {
Animal * animal = (Animal *)_self;
NSLog(@"\n self.color = %@;address = %@ \n",animal.color,obj);
}];
animal.name = @"cat";
animal.address = @"拉尼亞凱亞超星系群";
NSLog(@"修改isa指針后 animal = %@",animal);
打印結(jié)果:

可以看到,修改isa并沒影響對(duì)象的內(nèi)存地址,并且對(duì)象的 color 屬性也沒有收到任何影響。
四、總結(jié)
有朋友會(huì)指出,到對(duì)象銷毀,isa 指針也沒有變回去,這不符合要求。是的,其實(shí)是可以變回去的,就是不再一開始設(shè)置觀察屬性的時(shí)候就進(jìn)行isa修改,而是交換一下原類屬性的set方法實(shí)現(xiàn),在執(zhí)行設(shè)置的時(shí)候進(jìn)行修改isa指針,在設(shè)置完了再換回到原類的isa指針。但是如果這樣做那么著實(shí)沒有創(chuàng)建派生類的意義了,或許系統(tǒng)有更多的考慮而自身認(rèn)知水平的有限。
還有就是不要盲目的使用runtime的交換方法實(shí)現(xiàn)(method_exchangeImplementations)的API,最好是保證當(dāng)前類兩個(gè)方法都存在,不存在的進(jìn)行創(chuàng)建,因?yàn)椋?dāng)交換的兩個(gè)方法一個(gè)屬于父類的話,那么,父類再調(diào)用交換完的方法時(shí)會(huì)因?yàn)檎也坏綄?shí)現(xiàn)而崩潰,因?yàn)閷?shí)現(xiàn)寫在子類里了。
學(xué)習(xí)總結(jié),大神勿笑[抱拳][抱拳][抱拳]
作者:頭疼腦脹的代碼搬運(yùn)工
鏈接:https://juejin.cn/post/7056216238581612558