runtime在實際的開發中到底有何牛X的作用?我們該怎麼使用這麼牛X的工具呢?
想使用runtime,首先在寫運行時代碼之前,要先加上頭文件:
#import <objc/objc-runtime.h> // 模擬器
或者
#import <objc/runtime.h> // 真機
#import <objc/message.h> // 真機
- 1
- 2
- 3
- 4
一、動態添加一個類
(“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);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
運行結果爲:
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
- 1
- 2
- 3
這樣, 我們就在程序運行時動態添加了一個繼承自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]);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
打印結果爲 :
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
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
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);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
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;
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
4. 自動的歸檔和解檔
由於文章篇幅長度原因,把這塊內容提取出來作爲單獨一篇文章,跳轉鏈接 ——> runtime從入門到精通(七)—— 自動歸檔和解檔
5. 字典轉模型
由於文章篇幅長度原因,把這塊內容提取出來作爲單獨一篇文章,跳轉鏈接 ——> runtime從入門到精通(八)—— 使用runtime實現字典轉模型
三、利用runtime的動態交換方法實現,我們可以做什麼?
1. 方法簡單的交換
創建一個Person類,類中實現以下兩個類方法,並在.h 文件中聲明
+ (void)run {
NSLog(@"跑");
}
+ (void)study {
NSLog(@"學習");
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
控制器中調用,則先打印跑,後打印學習
[Person run];
[Person study];
- 1
- 2
下面通過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];
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
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];
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
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);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
注意:自定義方法中最後一定要再調用一下系統的方法,讓其有加載圖片的功能,但是由於方法交換,系統的方法名已經變成了我們自定義的方法名(有點繞,就是用我們的名字能調用系統的方法,用系統的名字能調用我們的方法),這就實現了系統方法的攔截!
利用以上思路,我們還可以給 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, "v@:");
}
return [super resolveInstanceMethod:sel];
}
@end
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
五、利用運行時set和get這兩個API,可以讓類別可以添加屬性
步驟:
1、創建一個類別,比如給任何一個對象都添加一個name屬性,就是NSObject添加分類(NSObject+Category)
2、先在.h 中@property 聲明出get 和 set 方法,方便點語法調用
@property(nonatomic,copy)NSString *name;
- 1
- 2
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);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
六、萬能界面跳轉(使用了runtime的N多個方法)
由於文章篇幅長度原因,把這塊內容提取出來作爲單獨一篇文章,跳轉鏈接 ——>runtime從入門到精通(九)—— 萬能界面跳轉
七、插件開發
插件入門
XCode 有個很坑爹的地方,就是它並不官方支持插件開發,官方沒有文檔,XCode 也沒有開源,但由於 XCode 是 Objective-C 寫的,OC 動態性太強大,導致在這麼封閉的情況下民間還是可以做出各種插件,其核心開發方式就是:
dump 出 Xcode 所有頭文件,知道 Xcode 裏有哪些類和接口。
通過頭文件方法名猜測方法的作用,swizzle 這些方法,插入自己的代碼實現插件邏輯。
通過 NSNotificationCenter 監聽各種事件的發生。
更詳細的開發教程網上有不少文章,有興趣的自行搜索吧。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12