我們都知道OC是一種面向?qū)ο蟮膭討B(tài)語言,它將很多在編譯和鏈接時期的事放到了runtime運行時來處理,runtime無疑是OC的靈魂。
那么對于我們開發(fā)來說,runtime有什么實際的應(yīng)用嗎?答案是有的。今天我們就具體應(yīng)用這方面來聊聊。
常見應(yīng)用方式:
1.runtime交換方法
場景:當(dāng)?shù)谌娇蚣芑蛘呦到y(tǒng)原生的方法不滿足需求的時候,可以在不改動原有方法的基礎(chǔ)上,添加額外的功能。
需求:加載一張圖片直接用[UIImage imageNamed:@"image"];是無法知道到底有沒有加載成功。給系統(tǒng)的imageNamed添加額外功能(是否加載圖片成功)。
方案一:繼承系統(tǒng)的類,重寫方法.
方案二:使用 runtime,交換方法.
步驟:1.給系統(tǒng)的方法添加分類 2.自己實現(xiàn)一個帶有擴展功能的方法 3.使用runtime,交換方法。
給UIImage添加分類代碼
// 加載圖片 且 帶判斷是否加載成功
+ (UIImage *)ln_imageNamed:(NSString *)name {
UIImage *image = [UIImage ln_imageNamed:name];
if (image) {
NSLog(@"runtime交互方法 -> 圖片加載成功");
} else {
NSLog(@"runtime交互方法 -> 圖片加載失敗");
}
return image;
}
/
作用:把類加載進內(nèi)存的時候調(diào)用,只會調(diào)用一次
調(diào)用:方法應(yīng)先交換,再去調(diào)用
*/
+ (void)load {
// 1.獲取 imageNamed方法地址
Method imageNamedMethod = class_getClassMethod(self, @selector(imageNamed:));
// 2.獲取 ln_imageNamed方法地址
Method ln_imageNamedMethod = class_getClassMethod(self, @selector(ln_imageNamed:));
// 3.交換方法地址,相當(dāng)于交換實現(xiàn)方式;「method_exchangeImplementations 交換兩個方法的實現(xiàn)」
method_exchangeImplementations(imageNamedMethod, ln_imageNamedMethod);
}
調(diào)用的時候:
- (void)viewDidLoad
{
[super viewDidLoad];
self.imageView.image = [UIImage imageNamed:@"CoerLN"];
}
- - -
// 打印輸出 --------- runtime交互方法 -> 圖片加載成功
總結(jié):我們所做的就是在方法調(diào)用流程第三步的時候,交換兩個方法地址指向。而且我們改變指向要在系統(tǒng)的imageNamed:方法調(diào)用前,所以將代碼寫在了分類的load方法里。最后當(dāng)運行的時候系統(tǒng)的方法就會去找我們的方法的實現(xiàn)。
2.給系統(tǒng)分類動態(tài)添加屬性
場景:給系統(tǒng)的類添加額外屬性的時候,可以使用runtime動態(tài)添加屬性方法。
原理:給一個類聲明屬性,其實本質(zhì)就是給這個類添加關(guān)聯(lián),并不是直接把這個值的內(nèi)存空間添加到類存空間。
注解:給系統(tǒng) NSObject 添加一個分類,我們知道在分類中是不能夠添加成員屬性的,雖然我們用了@property,但是僅僅會自動生成get和set方法的聲明,并沒有帶下劃線的屬性和方法實現(xiàn)生成。但是我們可以通過runtime就可以做到給它方法的實現(xiàn)。
需求:給系統(tǒng) NSObject 類動態(tài)添加屬性 name 字符串。
給UIImage添加分類代碼
- (NSString *)name
{
// 利用參數(shù)key 將對象object中存儲的對應(yīng)值取出來
return objc_getAssociatedObject(self, @"name");
}
- (void)setName:(NSString *)name
{
/**
將某個值跟某個對象關(guān)聯(lián)起來,將某個值存儲到某個對象中
objc_setAssociatedObject(<#id _Nonnull object#>:給哪個對象添加屬性, <#const void * _Nonnull key#>:屬性名稱, <#id _Nullable value#>:屬性值, <#objc_AssociationPolicy policy#>:保存策略)
*/
objc_setAssociatedObject(self, @"name", name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
NSLog(@"name---->%p",name);
}
調(diào)用代碼
NSObject *objc = [[NSObject alloc] init];
objc.name = @"Cary";
NSLog(@"runtime動態(tài)添加屬性name==%@",objc.name);
+++++++++++++++
// 打印輸出----------------- runtime動態(tài)添加屬性name == Cary
總結(jié):屬性賦值的本質(zhì),就是讓屬性與一個對象產(chǎn)生關(guān)聯(lián),所以要給NSObject的分類的name屬性賦值就是讓name和NSObject產(chǎn)生關(guān)聯(lián),而runtime可以做到這一點。
3.字典轉(zhuǎn)模型
思路:利用運行時,遍歷模型中所有屬性,根據(jù)模型的屬性名,去字典中查找key,取出對應(yīng)的值,給模型的屬性賦值(從提醒:字典中取值,不一定要全部取出來);提供一個NSObject分類,專門字典轉(zhuǎn)模型,以后所有模型都可以通過這個分類實現(xiàn)字典轉(zhuǎn)模型。
考慮情況:
1.當(dāng)字典的key和模型的屬性匹配不上。
2.模型中嵌套模型(模型屬性是另外一個模型對象)。
3.數(shù)組中裝著模型(模型的屬性是一個數(shù)組,數(shù)組中是一個個模型對象)。
注解:
根據(jù)上面的三種特殊情況,先是字典的key和模型的屬性不對應(yīng)的情況。不對應(yīng)有兩種,一種是字典的鍵值大于模型屬性數(shù)量,這時候我們不需要任何處理,因為runtime是先遍歷模型所有屬性,再去字典中根據(jù)屬性名找對應(yīng)值進行賦值,多余的鍵值對也當(dāng)然不會去看了;另外一種是模型屬性數(shù)量大于字典的鍵值對,這時候由于屬性沒有對應(yīng)值會被賦值為nil,就會導(dǎo)致crash,我們只需加一個判斷即可??紤]三種情況下面一一注解
1、runtime 字典轉(zhuǎn)模型-->字典的 key 和模型的屬性不匹配「模型屬性數(shù)量大于字典鍵值對數(shù)」,這種情況處理如下:
#import "NSObject+Model.h"
#import <objc/message.h>
@implementation NSObject (Model)
// 思路:利用runtime 遍歷模型中所有屬性,根據(jù)模型中屬性,去字典中取出對應(yīng)的value給模型屬性賦值
+ (instancetype)modelWithDict:(NSDictionary *)dict
{
// 1.創(chuàng)建對應(yīng)的對象
id objc = [[self alloc] init];
// 2.利用runtime給對象中的屬性賦值
/**
獲取類中的所有成員變量
class_copyIvarList(Class _Nullable cls:表示獲取哪個類中的成員變量, unsigned int * _Nullable outCount:表示這個類有多少成員變量,傳入一個Int變量地址,會自動給這個變量賦值)
返回值Ivar * =
指的是一個ivar數(shù)組,會把所有成員屬性放在一個數(shù)組中,通過返回的數(shù)組就能全部獲取到
*/
// 成員變量個數(shù)
unsigned int count = 0;
// 獲取類中的所有成員變量
Ivar *ivarList = class_copyIvarList(self, &count);
// 遍歷所有成員變量
for (int i = 0; i < count; i++) {
// 根據(jù)角標(biāo),從數(shù)組取出對應(yīng)的成員變量(Ivar:成員變量,以下劃線開頭)
Ivar ivar = ivarList[i];
// 獲取成員變量名字
NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
// 處理成員變量名,字典中的key(去掉 _ ,從第一個角標(biāo)開始截取)
NSString *key = [ivarName substringFromIndex:1];
// 根據(jù)成員屬性名去字典中查找對應(yīng)的value
id value = dict[key];
//【如果模型屬性數(shù)量大于字典鍵值對數(shù)理,模型屬性會被賦值為nil】
// 而報錯 (could not set nil as the value for the key age.)
if (value) {
// 給模型中屬性賦值
[objc setValue:value forKey:key];
}
}
return objc;
}
注解: 這里在獲取模型類中的所有屬性名,是采取 class_copyIvarList 先獲取成員變量(以下劃線開頭) ,然后再處理成員變量名,字典中的key(去掉 _ ,從第一個角標(biāo)開始截取) 得到屬性名。
2、runtime 字典轉(zhuǎn)模型-->模型中嵌套模型「模型屬性是另外一個模型對象」,這種情況處理如下:
// 思路:利用runtime 遍歷模型中所有屬性,根據(jù)模型中屬性,去字典中取出對應(yīng)的value給模型屬性賦值
+ (instancetype)modelWithDict2:(NSDictionary *)dict
{
// 1.創(chuàng)建對應(yīng)的對象
id objc = [[self alloc] init];
// 2.利用runtime給對象中的屬性賦值
// 成員變量個數(shù)
unsigned int count = 0;
// 獲取類中的所有成員變量
Ivar *ivarList = class_copyIvarList(self, &count);
// 遍歷所有成員變量
for (int i = 0; i < count; i++) {
// 根據(jù)角標(biāo),從數(shù)組取出對應(yīng)的成員變量(Ivar:成員變量,以下劃線開頭)
Ivar ivar = ivarList[i];
// 獲取成員變量名字
NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
// 獲取成員變量類型
NSString *ivarType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
// 替換: @\"User\" -> User
ivarType = [ivarType stringByReplacingOccurrencesOfString:@"\"" withString:@""];
ivarType = [ivarType stringByReplacingOccurrencesOfString:@"@" withString:@""];
// 處理成員變量名->字典中的key(去掉 _ ,從第一個角標(biāo)開始截取)
NSString *key = [ivarName substringFromIndex:1];
// 根據(jù)成員屬性名去字典中查找對應(yīng)的value
id value = dict[key];
// 二級轉(zhuǎn)換:如果字典中還有字典,也需要把對應(yīng)的字典轉(zhuǎn)換成模型
// 判斷下value是否是字典,并且是自定義對象才需要轉(zhuǎn)換
if ([value isKindOfClass:[NSDictionary class]] && ![ivarType hasPrefix:@"NS"]) {
// 字典轉(zhuǎn)換成模型 userDict => User模型, 轉(zhuǎn)換成哪個模型
// 根據(jù)字符串類名生成類對象
Class modelClass = NSClassFromString(ivarType);
if (modelClass) { // 有對應(yīng)的模型才需要轉(zhuǎn)
// 把字典轉(zhuǎn)模型
value = [modelClass modelWithDict2:value];
}
}
// 給模型中屬性賦值
if (value) {
[objc setValue:value forKey:key];
}
}
return objc;
}
3、runtime 字典轉(zhuǎn)模型-->數(shù)組中裝著模型「模型的屬性是一個數(shù)組,數(shù)組中是字典模型對象」,這種情況處理如下:
// 思路:利用runtime 遍歷模型中所有屬性,根據(jù)模型中屬性,去字典中取出對應(yīng)的value給模型屬性賦值
+ (instancetype)modelWithDict3:(NSDictionary *)dict
{
// 1.創(chuàng)建對應(yīng)的對象
id objc = [[self alloc] init];
// 2.利用runtime給對象中的屬性賦值
// 成員變量個數(shù)
unsigned int count = 0;
// 獲取類中的所有成員變量
Ivar *ivarList = class_copyIvarList(self, &count);
// 遍歷所有成員變量
for (int i = 0; i < count; i++) {
// 根據(jù)角標(biāo),從數(shù)組取出對應(yīng)的成員變量(Ivar:成員變量,以下劃線開頭)
Ivar ivar = ivarList[i];
// 獲取成員變量名字
NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
// 處理成員屬性名->字典中的key(去掉 _ ,從第一個角標(biāo)開始截取)
NSString *key = [ivarName substringFromIndex:1];
// 根據(jù)成員屬性名去字典中查找對應(yīng)的value
id value = dict[key];
//--------------------------- <#我是分割線#> ------------------------------//
//
// 三級轉(zhuǎn)換:NSArray中也是字典,把數(shù)組中的字典轉(zhuǎn)換成模型.
// 判斷值是否是數(shù)組
if ([value isKindOfClass:[NSArray class]]) {
// 判斷對應(yīng)類有沒有實現(xiàn)字典數(shù)組轉(zhuǎn)模型數(shù)組的協(xié)議
// arrayContainModelClass 提供一個協(xié)議,只要遵守這個協(xié)議的類,都能把數(shù)組中的字典轉(zhuǎn)模型
if ([self respondsToSelector:@selector(arrayContainModelClass)]) {
// 轉(zhuǎn)換成id類型,就能調(diào)用任何對象的方法
id idSelf = self;
// 獲取數(shù)組中字典對應(yīng)的模型
NSString *type = [idSelf arrayContainModelClass][key];
// 生成模型
Class classModel = NSClassFromString(type);
NSMutableArray *arrM = [NSMutableArray array];
// 遍歷字典數(shù)組,生成模型數(shù)組
for (NSDictionary *dict in value) {
// 字典轉(zhuǎn)模型
id model = [classModel modelWithDict3:dict];
[arrM addObject:model];
}
// 把模型數(shù)組賦值給value
value = arrM;
}
}
// 如果模型屬性數(shù)量大于字典鍵值對數(shù)理,模型屬性會被賦值為nil,而報錯
if (value) {
// 給模型中屬性賦值
[objc setValue:value forKey:key];
}
}
return objc;
}
4.動態(tài)添加方法
場景:如果一個類方法非常多,加載類到內(nèi)存的時候也比較耗費資源,需要給每個方法生成映射表,可以使用動態(tài)給某個類,添加方法解決。
注解:OC 中我們很習(xí)慣的會用懶加載,當(dāng)用到的時候才去加載它,但是實際上只要一個類實現(xiàn)了某個方法,就會被加載進內(nèi)存。當(dāng)我們不想加載這么多方法的時候,就會使用到 runtime 動態(tài)的添加方法。
需求:runtime 動態(tài)添加方法處理調(diào)用一個未實現(xiàn)的方法 和 去除報錯。
場景代碼:方法+調(diào)用+打印輸出
#import "Person.h"
#import <objc/message.h>
@implementation Person
/**
調(diào)用:只要一個對象調(diào)用了一個未實現(xiàn)的方法就會調(diào)用這個方法,進行處理
作用:動態(tài)添加方法,處理未實現(xiàn)
注解:任何方法默認都有兩個隱式參數(shù),self,_cmd(當(dāng)前方法的方法編號)
*/
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == NSSelectorFromString(@"roll:")) {
/**
class_addMethod(<#Class _Nullable __unsafe_unretained cls#>:給哪個類添加方法, <#SEL _Nonnull name#>:添加哪個方法,即添加方法的方法編號, <#IMP _Nonnull imp#>:方法實現(xiàn) => 函數(shù) => 函數(shù)入口 => 函數(shù)名(添加方法的函數(shù)實現(xiàn)(函數(shù)地址)), <#const char * _Nullable types#>:方法類型,(返回值+參數(shù)類型) v:void @:對象->self :表示SEL->_cmd)
*/
// 給類添加roll:滾了多遠方法
class_addMethod(self, sel, (IMP)LNRoll, "v@:@");
return YES;
}
if ([NSStringFromSelector(sel) isEqualToString:@"go:"]) {
// 給類添加go:走了多遠方法
class_addMethod(self, sel, (IMP)LNGO, "v@:@");
return YES;
}
return [super resolveInstanceMethod:sel];
}
// 調(diào)用
Person *p = [[Person alloc] init];
// 執(zhí)行某個方法
[p performSelector:@selector(roll:) withObject:@"11"];
[p performSelector:@selector(go:) withObject:@10];
// 打印輸出
2016-03-17 19:05:03.917 runtime[12761:543574] 我滾了 11 米遠
2016-03-17 19:05:04.617 runtime[12761:543574] 我走了 10 公里才到的家
5.實現(xiàn)NSCoding的歸檔接檔
原理描述:用runtime提供的函數(shù)遍歷Model自身所有屬性,并對屬性進行encode和decode操作。
核心方法:在Model的基類中重寫方法:
- (id)initWithCoder:(NSCoder *)aDecoder {
if (self = [super init]) {
unsigned int outCount;
Ivar * ivars = class_copyIvarList([self class], &outCount);
for (int i = 0; i < outCount; i ++) {
Ivar ivar = ivars[i];
NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)];
[self setValue:[aDecoder decodeObjectForKey:key] forKey:key];
}
free(ivars);
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)aCoder {
unsigned int outCount;
Ivar * ivars = class_copyIvarList([self class], &outCount);
for (int i = 0; i < outCount; i ++) {
Ivar ivar = ivars[i];
NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)];
[aCoder encodeObject:[self valueForKey:key] forKey:key];
}
free(ivars);
}