Runtime簡介
因?yàn)镺bjc是一門動態(tài)語言,所以它總是想辦法把一些決定工作從編譯連接推遲到運(yùn)行時。也就是說只有編譯器是不夠的,還需要一個運(yùn)行時系統(tǒng) (runtime system) 來執(zhí)行編譯后的代碼。這就是 Objective-C Runtime 系統(tǒng)存在的意義,它是整個Objc運(yùn)行框架的一塊基石。
Runtime其實(shí)有兩個版本:“modern”和 “l(fā)egacy”。我們現(xiàn)在用的 Objective-C 2.0 采用的是現(xiàn)行(Modern)版的Runtime系統(tǒng),只能運(yùn)行在 iOS 和 OS X 10.5 之后的64位程序中。而OS X較老的32位程序仍采用 Objective-C 1中的(早期)Legacy 版本的 Runtime 系統(tǒng)。這兩個版本最大的區(qū)別在于當(dāng)你更改一個類的實(shí)例變量的布局時,在早期版本中你需要重新編譯它的子類,而現(xiàn)行版就不需要。
Runtime基本是用C和匯編寫的,可見蘋果為了動態(tài)系統(tǒng)的高效而作出的努力。你可以在這里下到蘋果維護(hù)的開源代碼。蘋果和GNU各自維護(hù)一個開源的runtime版本,這兩個版本之間都在努力的保持一致。
運(yùn)行時在開發(fā)中的主要應(yīng)用場景
- 字典轉(zhuǎn)模型
- 給分類增加關(guān)聯(lián)對象,開發(fā)框架時解耦
- 交換方法,在無法修改系統(tǒng)或者第三方框架的方式時
- 利用交換方法,先執(zhí)行自己的方法
- 再執(zhí)行系統(tǒng)或第三方框架的方法
- 黑魔法,對系統(tǒng)/框架版本有很強(qiáng)的依賴性
1、動態(tài)獲取類的屬性
常用與字典轉(zhuǎn)模型的時候使用
我這里就用一個NSObject的分類給大家詳細(xì)的講解一下
大概思路:
1、class_copyPropertyList 獲取屬性的數(shù)組
2、遍歷數(shù)組,property_getName 獲取每一個屬性的名稱
3、添加到數(shù)組中
4、free 釋放數(shù)組
首先我先創(chuàng)建了一個繼承自NSObject的類,里面有兩個成員變量,然后在創(chuàng)建一個分類,我們要在分類中獲取person類中的成員變量
在寫之前我們肯定要讓分類中添加#import <objc/runtime.h>
在分類方法中
- 第一步:調(diào)用運(yùn)行時方法,獲取類的屬性列表
調(diào)用的是class_copy...
方法
我們可以看到一共聯(lián)想出了4中方法,Ivar 成員變量
,Method 方法
,Property 屬性
,Protocol 協(xié)議
,通過這四種方法,我們可以獲取所有我們想要知道的。
這里我們想要獲取Person類的屬性,所以我們就調(diào)用了class_copyPropertyList(<#__unsafe_unretained Class cls#>, <#unsigned int *outCount#>)
這個方法。
方法解析
我們可以看到這個方法里面一共有兩個參數(shù),我們點(diǎn)住該方法,按住control
鍵,可以看到該方法的一些具體內(nèi)容,內(nèi)容如下
/**
參數(shù)
1. 要獲取的類
2. 類屬性的個數(shù)指針
返回值
所有屬性的`數(shù)組`,C 語言中,數(shù)組的名字,就是指向第一個元素的地址
retain/create/copy 需要 release,最好 option + click
*/
unsigned int count = 0;
objc_property_t *proList = class_copyPropertyList([self class], &count);
NSLog(@"屬性的數(shù)量 %d", count);
釋放數(shù)組的方法free(proList);
這個時候,我們打印count
,會發(fā)現(xiàn)count
=2;有圖有真相,請看下面打印
這個時候,我們雖然獲取到了2
,但是我們是想要具體的內(nèi)容,而不是這個,所以還需要繼續(xù)向下走
- 第二步:遍歷數(shù)組,拿到我們想要的東西
這里面有兩個主要的方法- objc_property_t pty = proList[i];
- const char *cName = property_getName(pty);
// 遍歷所有的屬性
for (unsigned int i = 0; i < count; i++) {
// 1. 從數(shù)組中取得屬性
/**
C 語言的結(jié)構(gòu)體指針,通常不需要 `*`
*/
objc_property_t pty = proList[i];
// 2. 從 pty 中獲得屬性的名稱
const char *cName = property_getName(pty);
NSString *name = [NSString stringWithCString:cName encoding:NSUTF8StringEncoding];
NSLog(@"%@", name);
}
打印結(jié)果
第三步:獲取關(guān)聯(lián)對象,動態(tài)添加屬性,當(dāng)
Person
對象屬性已經(jīng)獲取的時候,就直接返回,防止多次調(diào)用運(yùn)行時方法提高效率
需要用到的兩個方法objc_getAssociatedObject
objc_setAssociatedObject
const char * kPropertiesListKey = "CZPropertiesListKey";
// --- 1. 從`關(guān)聯(lián)對象`中獲取對象屬性,如果有,直接返回!
/**
獲取關(guān)聯(lián)對象 - 動態(tài)添加的屬性
參數(shù):
1. 對象 self
2. 動態(tài)屬性的 key
返回值
動態(tài)添加的`屬性值`
*/
NSArray *ptyList = objc_getAssociatedObject(self, kPropertiesListKey);
if (ptyList != nil) {
return ptyList;
}
// --- 2. 到此為止,對象的屬性數(shù)組已經(jīng)獲取完畢,利用關(guān)聯(lián)對象,動態(tài)添加屬性
/**
參數(shù)
1. 對象 self [OC 中 class 也是一個特殊的對象]
2. 動態(tài)添加屬性的 key,獲取值的時候使用
3. 動態(tài)添加的屬性值
4. 對象的引用關(guān)系
*/
objc_setAssociatedObject(self, kPropertiesListKey, arrayM.copy, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
動態(tài)獲取類的屬性,方法講解完畢,大家感覺如何,下面是完整的代碼
{
const char * kPropertiesListKey = "CZPropertiesListKey";
// --- 1. 從`關(guān)聯(lián)對象`中獲取對象屬性,如果有,直接返回!
/**
獲取關(guān)聯(lián)對象 - 動態(tài)添加的屬性
參數(shù):
1. 對象 self
2. 動態(tài)屬性的 key
返回值
動態(tài)添加的`屬性值`
*/
NSArray *ptyList = objc_getAssociatedObject(self, kPropertiesListKey);
if (ptyList != nil) {
return ptyList;
}
// 調(diào)用運(yùn)行時方法,取得類的屬性列表
// Ivar 成員變量
// Method 方法
// Property 屬性
// Protocol 協(xié)議
/**
參數(shù)
1. 要獲取的類
2. 類屬性的個數(shù)指針
返回值
所有屬性的`數(shù)組`,C 語言中,數(shù)組的名字,就是指向第一個元素的地址
retain/create/copy 需要 release,最好 option + click
*/
unsigned int count = 0;
objc_property_t *proList = class_copyPropertyList([self class], &count);
NSLog(@"屬性的數(shù)量 %d", count);
// 創(chuàng)建數(shù)組
NSMutableArray *arrayM = [NSMutableArray array];
// 遍歷所有的屬性
for (unsigned int i = 0; i < count; i++) {
// 1. 從數(shù)組中取得屬性
/**
C 語言的結(jié)構(gòu)體指針,通常不需要 `*`
*/
objc_property_t pty = proList[i];
// 2. 從 pty 中獲得屬性的名稱
const char *cName = property_getName(pty);
NSString *name = [NSString stringWithCString:cName encoding:NSUTF8StringEncoding];
// NSLog(@"%@", name);
// 3. 屬性名稱添加到數(shù)組
[arrayM addObject:name];
}
// 釋放數(shù)組
free(proList);
// --- 2. 到此為止,對象的屬性數(shù)組已經(jīng)獲取完畢,利用關(guān)聯(lián)對象,動態(tài)添加屬性
/**
參數(shù)
1. 對象 self [OC 中 class 也是一個特殊的對象]
2. 動態(tài)添加屬性的 key,獲取值的時候使用
3. 動態(tài)添加的屬性值
4. 對象的引用關(guān)系
*/
objc_setAssociatedObject(self, kPropertiesListKey, arrayM.copy, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
return arrayM.copy;
}
2、字典轉(zhuǎn)模型
上面那一個方法可以使我們獲取Person
對象的所有屬性,那么字典轉(zhuǎn)模型使用一個KVC就可以了。
具體方法
// 所有字典轉(zhuǎn)模型框架,核心算法!
+ (instancetype)objWithDict:(NSDictionary *)dict {
// 實(shí)例化對象
id object = [[self alloc] init];
// 使用字典,設(shè)置對象信息
// 1> 獲得 self 的屬性列表
NSArray *proList = [self getPersonArray];
// 2> 遍歷字典
[dict enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {
NSLog(@"key %@ --- value %@", key, obj);
// 3> 判斷 key 是否在 proList 中
if ([proList containsObject:key]) {
// 說明屬性存在,可以使用 `KVC` 設(shè)置數(shù)值
[object setValue:obj forKey:key];
}
}];
return object;
}
3、交叉方法
進(jìn)行時之所以那么被廣大的iOS程序員所敬仰,很大的一部分原因就是因?yàn)檫@個交叉方法,我們可以用這個方法更改任意代碼,交叉方法也被稱為黑魔法
。但是,我們平時不到萬不得已最好不要用這個黑魔法
,就像斗地主一樣,你上來就王炸,那么以后的路肯定就不會好走了。
援引一段AFNetWorking作者的一段話
最后,請記住僅在不得已的情況下使用 Objective-C runtime。隨便修改基礎(chǔ)框架或所使用的三方代碼是毀掉你的應(yīng)用的絕佳方法哦。請務(wù)必要小心哦。
交叉方法一般使用的地方
- 我們使用第三方框架時,我們發(fā)現(xiàn)了一些錯誤,我們不用修改第三方的代碼
- 第三方框架或者系統(tǒng)原生方法十分不夠我們的使用,我們強(qiáng)烈希望增加一些功能
這里面有三個常用的方法
- 獲取類方法
Class PersonClass = object_getClass([Person class]);
SEL oriSEL = @selector(test1);
Method oriMethod = class_getInstanceMethod(xiaomingClass, oriSEL);
- 替換原方法實(shí)現(xiàn)
class_replaceMethod(toolClass, cusSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
- 交換兩個方法
method_exchangeImplementations(oriMethod, cusMethod);
這里我給imageView做了一個交換方法,調(diào)整圖像尺寸
代碼如下
// 在類被加載到運(yùn)行時的時候,就會執(zhí)行
+ (void)load {
// 1. 獲取 UIImageView 類的 實(shí)例方法 `setImage:`
Method originalMethod = class_getInstanceMethod([self class], @selector(setImage:));
// 2. 獲取 UIImageView 類的 實(shí)例方法 `cz_setImage:`,本身定義在分類中
Method swizzledMethod = class_getInstanceMethod([self class], @selector(cz_setImage:));
// 3. 交換方法 setImage 和 cz_setImage,交換完成之后
// 1> 調(diào)用 setImage 相當(dāng)于調(diào)用 cz_setImage
// 2> 調(diào)用 cz_setImage 相當(dāng)于調(diào)用 setImage
method_exchangeImplementations(originalMethod, swizzledMethod);
}
/// 1. 當(dāng)在其他位置調(diào)用 `setImage` 方法時,`自動`調(diào)用 cz_setImage: 方法
- (void)cz_setImage:(UIImage *)image {
NSLog(@"%s %@", __FUNCTION__, image);
// 1. 根據(jù) imageView 的大小,重新調(diào)整 image 的大小
// 使用 `CG` 重新生成一張和目標(biāo)尺寸相同的圖片
UIGraphicsBeginImageContextWithOptions(self.bounds.size, YES, 0);
// 繪制圖像
[image drawInRect:self.bounds];
// 取得結(jié)果
UIImage *result = UIGraphicsGetImageFromCurrentImageContext();
// 關(guān)閉上下文
UIGraphicsEndImageContext();
// 調(diào)用系統(tǒng)默認(rèn)的 setImage 方法
[self cz_setImage:result];
}
4、給分類添加屬性
給分類添加屬性其實(shí)就是獲取關(guān)聯(lián)對象,然后添加
內(nèi)容比較簡單,直接上代碼
.h文件
//分類的頭文件
@interface ClassName (CategoryName)
@property (nonatomic, strong) NSString *str;
@end
.m文件
//實(shí)現(xiàn)文件
#import "ClassName + CategoryName.h"
#import <objc/runtime.h>
static void *strKey = &strKey;
@implementation ClassName (CategoryName)
-(void)setStr:(NSString *)str
{
objc_setAssociatedObject(self, & strKey, str, OBJC_ASSOCIATION_COPY);
}
-(NSString *)str
{
return objc_getAssociatedObject(self, &strKey);
}
@end
5、NSClassFromString(根據(jù)一個字符串生成一個類)
使用NSClassFromString 使用NSClassFromString可以直接從字符串初始化出對象出來,即使不引用頭文件也沒關(guān)系。
這個方法判斷類是否存在,如果存在就動態(tài)加載的,不存為就返回一個空對象;
簡單使用方法
id myObj = [[NSClassFromString(@"MySpecialClass") alloc] init];
正常情況下等價于:
id myObj = [[MySpecialClass alloc] init];
但是,如果你的程序中并不存在MySpecialClass這個類,下面的寫法會出錯,而上面的寫法只是返回一個空對象而已。
其中對這個方法有一個比較經(jīng)典的用法,iOS 萬能跳轉(zhuǎn)界面方法 (runtime實(shí)用篇一)
想要了解的小伙伴們可以點(diǎn)進(jìn)去看一看作者的思路。
最后在給大家推薦幾篇比較好的文章,有興趣的同學(xué)可以看一看
runtime詳解
OC最實(shí)用的runtime總結(jié),面試、工作你看我就足夠了!
Runtime 10種用法(沒有比這更全的了)
iOS 萬能跳轉(zhuǎn)界面方法 (runtime實(shí)用篇一)
button防止被重復(fù)點(diǎn)擊的相關(guān)方法(詳細(xì)版)