runtime從入門到精通(六)—— runtime在實際開發中的應用
上一篇文章,我們學習了runtime的訊息傳送和訊息轉發機制(檢視連結: runtime從入門到精通(五)—— 訊息傳送和訊息轉發 ),倒到此為止,有關runtime的理論知識介紹就先告於段落,小夥伴們,真正的乾貨來了,runtime在實際的開發中到底有何牛X的作用?我們該怎麼使用這麼牛X的工具呢?
想使用runtime,首先在寫執行時程式碼之前,要先加上標頭檔案:
#import <objc/objc-runtime.h> // 模擬器
或者
#import <objc/runtime.h> // 真機
#import <objc/message.h> // 真機
一、動態新增一個類
(“KVO”的實現是利用了runtime能夠動態新增類)
原來當你對一個物件進行觀察時, 系統會自動新建一個類繼承自原類, 然後重寫被觀察屬性的setter方法. 然後重寫的setter方法會負責在呼叫原setter方法前後通知觀察者. 然後把原物件的isa指標指向這個新類, 我們知道, 物件是通過isa指標去查詢自己是屬於哪個類, 並去所在類的方法列表中查詢方法的, 所以這個時候這個物件就自然地變成了新類的例項物件.
就像KVO一樣, 系統是在程式執行的時候根據你要監聽的類, 動態新增一個新類繼承自該類, 然後重寫原類的setter方法並在裡面通知observer的.
那麼, 如何動態新增一個類呢? 直接上程式碼:
// 建立一個類(size_t extraBytes該引數通常指定為0, 該引數是分配給類和元類物件尾部的索引ivars的位元組數。)
Class clazz = objc_allocateClassPair([NSObject class], "GoodPerson", 0);
// 新增ivar
// @encode(aType) : 返回該型別的C字串
class_addIvar(clazz, "_name", sizeof(NSString *), log2(sizeof(NSString *)), @encode(NSString *));
class_addIvar(clazz, "_age" , sizeof(NSUInteger), log2(sizeof(NSUInteger)), @encode(NSUInteger));
// 註冊該類
objc_registerClassPair(clazz);
// 建立例項物件
id object = [[clazz alloc] init];
// 設定ivar
[object setValue:@"Tracy" forKey:@"name"];
Ivar ageIvar = class_getInstanceVariable(clazz, "_age");
object_setIvar(object, ageIvar, @18);
// 列印物件的類和記憶體地址
NSLog(@"%@", object);
// 列印物件的屬性值
NSLog(@"name = %@, age = %@", [object valueForKey:@"name"], object_getIvar(object, ageIvar));
// 當類或者它的子類的例項還存在,則不能呼叫objc_disposeClassPair方法
object = nil;
// 銷燬類
objc_disposeClassPair(clazz);
執行結果為:
2016-09-04 17:04:08.328 Runtime-實踐篇[13699:1043458] <GoodPerson: 0x1002039b0>
2016-09-04 17:04:08.329 Runtime-實踐篇[13699:1043458] name = Tracy, age = 18
這樣, 我們就在程式執行時動態添加了一個繼承自NSObject的GoodPerson類, 併為該類添加了name和age成員變數.
二、通過runtime獲取一個類的所有屬性,我們可以做些什麼?
1. 列印一個類的所有ivar, property 和 method(簡單直接的使用)
Person *p = [[Person alloc] init];
[p setValue:@"Kobe" forKey:@"name"];
[p setValue:@18 forKey:@"age"];
// p.address = @"廣州大學城";
p.weight = 110.0f;
// 1.列印所有ivars
unsigned int ivarCount = 0;
// 用一個字典裝ivarName和value
NSMutableDictionary *ivarDict = [NSMutableDictionary dictionary];
Ivar *ivarList = class_copyIvarList([p class], &ivarCount);
for(int i = 0; i < ivarCount; i++){
NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivarList[i])];
id value = [p valueForKey:ivarName];
if (value) {
ivarDict[ivarName] = value;
} else {
ivarDict[ivarName] = @"值為nil";
}
}
// 列印ivar
for (NSString *ivarName in ivarDict.allKeys) {
NSLog(@"ivarName:%@, ivarValue:%@",ivarName, ivarDict[ivarName]);
}
// 2.列印所有properties
unsigned int propertyCount = 0;
// 用一個字典裝propertyName和value
NSMutableDictionary *propertyDict = [NSMutableDictionary dictionary];
objc_property_t *propertyList = class_copyPropertyList([p class], &propertyCount);
for(int j = 0; j < propertyCount; j++){
NSString *propertyName = [NSString stringWithUTF8String:property_getName(propertyList[j])];
id value = [p valueForKey:propertyName];
if (value) {
propertyDict[propertyName] = value;
} else {
propertyDict[propertyName] = @"值為nil";
}
}
// 列印property
for (NSString *propertyName in propertyDict.allKeys) {
NSLog(@"propertyName:%@, propertyValue:%@",propertyName, propertyDict[propertyName]);
}
// 3.列印所有methods
unsigned int methodCount = 0;
// 用一個字典裝methodName和arguments
NSMutableDictionary *methodDict = [NSMutableDictionary dictionary];
Method *methodList = class_copyMethodList([p class], &methodCount);
for(int k = 0; k < methodCount; k++){
SEL methodSel = method_getName(methodList[k]);
NSString *methodName = [NSString stringWithUTF8String:sel_getName(methodSel)];
unsigned int argumentNums = method_getNumberOfArguments(methodList[k]);
methodDict[methodName] = @(argumentNums - 2); // -2的原因是每個方法內部都有self 和 selector 兩個引數
}
// 列印method
for (NSString *methodName in methodDict.allKeys) {
NSLog(@"methodName:%@, argumentsCount:%@", methodName, methodDict[methodName]);
}
列印結果為 :
2016-09-04 17:06:49.070 Runtime-實踐篇[13723:1044813] ivarName:_name, ivarValue:Kobe
2016-09-04 17:06:49.071 Runtime-實踐篇[13723:1044813] ivarName:_age, ivarValue:18
2016-09-04 17:06:49.071 Runtime-實踐篇[13723:1044813] ivarName:_weight, ivarValue:110
2016-09-04 17:06:49.072 Runtime-實踐篇[13723:1044813] ivarName:_address, ivarValue:值為nil
2016-09-04 17:06:49.072 Runtime-實踐篇[13723:1044813] propertyName:address, propertyValue:值為nil
2016-09-04 17:06:49.072 Runtime-實踐篇[13723:1044813] propertyName:weight, propertyValue:110
2016-09-04 17:06:49.073 Runtime-實踐篇[13723:1044813] methodName:setWeight:, argumentsCount:1
2016-09-04 17:06:49.073 Runtime-實踐篇[13723:1044813] methodName:weight, argumentsCount:0
2016-09-04 17:06:49.074 Runtime-實踐篇[13723:1044813] methodName:setAddress:, argumentsCount:1
2016-09-04 17:06:49.074 Runtime-實踐篇[13723:1044813] methodName:address, argumentsCount:0
2016-09-04 17:06:49.074 Runtime-實踐篇[13723:1044813] methodName:.cxx_destruct, argumentsCount
2. 動態變數控制
在程式中,XiaoMing的age是10,後來被runtime變成了20,來看看runtime是怎麼做到的:
-(void)changeAge{
unsigned int count = 0;
//動態獲取XiaoMing類中的所有屬性[當然包括私有]
Ivar *ivar = class_copyIvarList([self.xiaoMing class], &count);
//遍歷屬性找到對應age欄位
for (int i = 0; i<count; i++) {
Ivar var = ivar[i];
const char *varName = ivar_getName(var);
NSString *name = [NSString stringWithUTF8String:varName];
if ([name isEqualToString:@"_age"]) {
//修改對應的欄位值成20
object_setIvar(self.xiaoMing, var, @"20");
break;
}
}
NSLog(@"XiaoMing's age is %@",self.xiaoMing.age);
}
3. 在NSObject的分類中增加方法來避免使用KVC賦值的時候出現崩潰
在有些時候我們需要通過KVC去修改某個類的私有變數,但是又不知道該屬性是否存在,如果類中不存在該屬性,那麼通過KVC賦值就會crash,這時也可以通過執行時進行判斷。同樣我們在NSObject的分類中增加如下方法。
/**
* 判斷類中是否有該屬性
*
* @param property 屬性名稱
*
* @return 判斷結果
*/
-(BOOL)hasProperty:(NSString *)property {
BOOL flag = NO;
u_int count = 0;
Ivar *ivars = class_copyIvarList([self class], &count);
for (int i = 0; i < count; i++) {
const char *propertyName = ivar_getName(ivars[i]);
NSString *propertyString = [NSString stringWithUTF8String:propertyName];
if ([propertyString isEqualToString:property]){
flag = YES;
}
}
}
4. 自動的歸檔和解檔
5. 字典轉模型
三、利用runtime的動態交換方法實現,我們可以做什麼?
1. 方法簡單的交換
建立一個Person類,類中實現以下兩個類方法,並在.h 檔案中宣告
+ (void)run {
NSLog(@"跑");
}
+ (void)study {
NSLog(@"學習");
}
控制器中呼叫,則先列印跑,後列印學習
[Person run];
[Person study];
下面通過runtime 實現方法交換,類方法用class_getClassMethod
,物件方法用class_getInstanceMethod
// 獲取兩個類的類方法
Method m1 = class_getClassMethod([Person class], @selector(run));
Method m2 = class_getClassMethod([Person class], @selector(study));
// 開始交換方法實現
method_exchangeImplementations(m1, m2);
// 交換後,先列印學習,再列印跑!
[Person run];
[Person study];
2. 攔截系統方法(Swizzle 黑魔法),也可以說成對系統的方法進行替換
由於某種原因,我們要改變這個方法的實現,但是又不能去動它的原始碼(系統的方法或者一些開源庫出現問題的時候),這個時候runtime就派上用場了。
需求:比如iOS6 升級 iOS7 後需要版本適配,根據不同系統使用不同樣式圖片(擬物化和扁平化),如何通過不去手動一個個修改每個UIImage的imageNamed:方法就可以實現為該方法中加入版本判斷語句?
步驟:
1、為UIImage建一個分類(UIImage+Category)
2、在分類中實現一個自定義方法,方法中寫要在系統方法中加入的語句,比如版本判斷
+ (UIImage *)xh_imageNamed:(NSString *)name {
double version = [[UIDevice currentDevice].systemVersion doubleValue];
if (version >= 7.0) {
// 如果系統版本是7.0以上,使用另外一套檔名結尾是‘_os7’的扁平化圖片
name = [name stringByAppendingString:@"_os7"];
}
return [UIImage xh_imageNamed:name];
}
3、分類中重寫UIImage的load方法,實現方法的交換(只要能讓其執行一次方法交換語句,load再合適不過了)
+ (void)load {
// 獲取兩個類的類方法
Method m1 = class_getClassMethod([UIImage class], @selector(imageNamed:));
Method m2 = class_getClassMethod([UIImage class], @selector(xh_imageNamed:));
// 開始交換方法實現
method_exchangeImplementations(m1, m2);
}
注意:自定義方法中最後一定要再呼叫一下系統的方法,讓其有載入圖片的功能,但是由於方法交換,系統的方法名已經變成了我們自定義的方法名(有點繞,就是用我們的名字能呼叫系統的方法,用系統的名字能呼叫我們的方法),這就實現了系統方法的攔截!
利用以上思路,我們還可以給 NSObject 新增分類,統計建立了多少個物件,給控制器新增分類,統計有建立了多少個控制器,特別是公司需求總變的時候,在一些原有控制元件或模組上新增一個功能,建議使用該方法!
交換原理:
交換之前:
交換之後:
3. 執行時實現多繼承的效果
既然方法我們可以攔截,可以交換,那麼實現多繼承的效果就留給讀者自己思考了(避免篇幅太長,後續在部落格中再來探討這個問題)
四、動態新增方法
開發使用場景:如果一個類方法非常多,載入類到記憶體的時候也比較耗費資源,需要給每個方法生成對映表,可以使用動態給某個類,新增方法解決。
經典面試題:有沒有使用performSelector,其實主要想問你有沒有動態新增過方法。
簡單使用:
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
Person *p = [[Person alloc] init];
// 預設person,沒有實現eat方法,可以通過performSelector呼叫,但是會報錯。
// 動態新增方法就不會報錯
[p performSelector:@selector(eat)];
}
@end
@implementation Person
// void(*)()
// 預設方法都有兩個隱式引數,
void eat(id self,SEL sel)
{
NSLog(@"%@ %@",self,NSStringFromSelector(sel));
}
// 當一個物件呼叫未實現的方法,會呼叫這個方法處理,並且會把對應的方法列表傳過來.
// 剛好可以用來判斷,未實現的方法是不是我們想要動態新增的方法
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
if (sel == @selector(eat)) {
// 動態新增eat方法
// 第一個引數:給哪個類新增方法
// 第二個引數:新增方法的方法編號
// 第三個引數:新增方法的函式實現(函式地址)
// 第四個引數:函式的型別,(返回值+引數型別) v:void @:物件->self :表示SEL->_cmd
class_addMethod(self, @selector(eat), eat, "[email protected]:");
}
return [super resolveInstanceMethod:sel];
}
@end
五、利用執行時set和get這兩個API,可以讓類別可以新增屬性
步驟:
1、建立一個類別,比如給任何一個物件都新增一個name屬性,就是NSObject新增分類(NSObject+Category)
2、先在.h 中@property 宣告出get 和 set 方法,方便點語法呼叫
@property(nonatomic,copy)NSString *name;
3、在.m 中重寫set 和 get 方法,內部利用runtime 給屬性賦值和取值
char nameKey;
- (void)setName:(NSString *)name {
// 將某個值跟某個物件關聯起來,將某個值儲存到某個物件中
objc_setAssociatedObject(self, &nameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)name {
return objc_getAssociatedObject(self, &nameKey);
}
六、萬能介面跳轉(使用了runtime的N多個方法)
由於文章篇幅長度原因,把這塊內容提取出來作為單獨一篇文章,跳轉連結 ——>runtime從入門到精通(九)—— 萬能介面跳轉
七、外掛開發
外掛入門
XCode 有個很坑爹的地方,就是它並不官方支援外掛開發,官方沒有文件,XCode 也沒有開源,但由於 XCode 是 Objective-C 寫的,OC 動態性太強大,導致在這麼封閉的情況下民間還是可以做出各種外掛,其核心開發方式就是:
dump 出 Xcode 所有標頭檔案,知道 Xcode 裡有哪些類和介面。
通過標頭檔案方法名猜測方法的作用,swizzle 這些方法,插入自己的程式碼實現外掛邏輯。
通過 NSNotificationCenter 監聽各種事件的發生。
更詳細的開發教程網上有不少文章,有興趣的自行搜尋吧。