runtime再實(shí)際開發(fā)中主要應(yīng)用
1.動(dòng)態(tài)添加一個(gè)類
2.通過runtime獲取一個(gè)類的所有屬性,我們可以做些什么
? ?2.1. 打印一個(gè)類的所有ivar, property 和 method(簡單直接的使用)
? ?2.2.動(dòng)態(tài)變量控制
? ?3.3.在NSObject的分類中增加方法來避免使用KVC賦值的時(shí)候出現(xiàn)崩潰
? ??3.4.自動(dòng)的歸檔和解檔
? ? 3.5.字典轉(zhuǎn)模型
3.利用runtime的動(dòng)態(tài)交換方法實(shí)現(xiàn),我們可以做什么?
? ? ?3.1. 方法簡單的交換 ?
? ? ?3.2. 攔截系統(tǒng)方法(Swizzle 黑魔法),也可以說成對(duì)系統(tǒng)的方法進(jìn)行替換
? ? ? 3.3. 運(yùn)行時(shí)實(shí)現(xiàn)多繼承的效果
4.動(dòng)態(tài)添加方法
5.利用運(yùn)行時(shí)set和get這兩個(gè)API,可以讓類別可以添加屬性
6.萬能界面跳轉(zhuǎn)(使用了runtime的N多個(gè)方法?
7.插件開發(fā)
一、動(dòng)態(tài)添加一個(gè)類(“KVO”的實(shí)現(xiàn)是利用了runtime能夠動(dòng)態(tài)添加類)
原來當(dāng)你對(duì)一個(gè)對(duì)象進(jìn)行觀察時(shí), 系統(tǒng)會(huì)自動(dòng)新建一個(gè)類繼承自原類, 然后重寫被觀察屬性的setter方法. 然后重寫的setter方法會(huì)負(fù)責(zé)在調(diào)用原setter方法前后通知觀察者. 然后把原對(duì)象的isa指針指向這個(gè)新類, 我們知道, 對(duì)象是通過isa指針去查找自己是屬于哪個(gè)類, 并去所在類的方法列表中查找方法的, 所以這個(gè)時(shí)候這個(gè)對(duì)象就自然地變成了新類的實(shí)例對(duì)象.
就像KVO一樣, 系統(tǒng)是在程序運(yùn)行的時(shí)候根據(jù)你要監(jiān)聽的類, 動(dòng)態(tài)添加一個(gè)新類繼承自該類, 然后重寫原類的setter方法并在里面通知observer的.
那么, 如何動(dòng)態(tài)添加一個(gè)類呢? 直接上代碼:
// 創(chuàng)建一個(gè)類(size_t extraBytes該參數(shù)通常指定為0, 該參數(shù)是分配給類和元類對(duì)象尾部的索引ivars的字節(jié)數(shù)。)
Class clazz = objc_allocateClassPair([NSObjectclass],"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);// 創(chuàng)建實(shí)例對(duì)象idobject = [[clazz alloc] init];// 設(shè)置ivar[object setValue:@"Tracy"forKey:@"name"];
Ivar ageIvar = class_getInstanceVariable(clazz,"_age");
object_setIvar(object, ageIvar, @18);// 打印對(duì)象的類和內(nèi)存地址NSLog(@"%@", object);// 打印對(duì)象的屬性值NSLog(@"name = %@, age = %@", [object valueForKey:@"name"], object_getIvar(object, ageIvar));// 當(dāng)類或者它的子類的實(shí)例還存在,則不能調(diào)用objc_disposeClassPair方法object =nil;// 銷毀類objc_disposeClassPair(clazz);
運(yùn)行結(jié)果為:
2016-09-04 17:04:08.328 Runtime-實(shí)踐篇[13699:1043458]?
2016-09-04 17:04:08.329 Runtime-實(shí)踐篇[13699:1043458] name = Tracy, age = 18
這樣, 我們就在程序運(yùn)行時(shí)動(dòng)態(tài)添加了一個(gè)繼承自NSObject的GoodPerson類, 并為該類添加了name和age成員變量.
二、通過runtime獲取一個(gè)類的所有屬性,我們可以做些什么
1. 打印一個(gè)類的所有ivar, property 和 method(簡單直接的使用)
Person *p = [[Person alloc] init];
[p setValue:@"Kobe"forKey:@"name"];
[p setValue:@18forKey:@"age"];//? ? p.address = @"廣州大學(xué)城";p.weight=110.0f;// 1.打印所有ivarsunsignedintivarCount =0;// 用一個(gè)字典裝ivarName和valueNSMutableDictionary*ivarDict = [NSMutableDictionarydictionary];
Ivar *ivarList = class_copyIvarList([p class], &ivarCount);for(inti =0; i < ivarCount; i++){NSString*ivarName = [NSStringstringWithUTF8String:ivar_getName(ivarList[i])];idvalue = [p valueForKey:ivarName];if(value) {
ivarDict[ivarName] = value;
}else{
ivarDict[ivarName] = @"值為nil";
}
}// 打印ivarfor(NSString*ivarName in ivarDict.allKeys) {NSLog(@"ivarName:%@, ivarValue:%@",ivarName, ivarDict[ivarName]);
}// 2.打印所有propertiesunsignedintpropertyCount =0;// 用一個(gè)字典裝propertyName和valueNSMutableDictionary*propertyDict = [NSMutableDictionarydictionary];
objc_property_t *propertyList = class_copyPropertyList([p class], &propertyCount);for(intj =0; j < propertyCount; j++){NSString*propertyName = [NSStringstringWithUTF8String:property_getName(propertyList[j])];idvalue = [p valueForKey:propertyName];if(value) {
propertyDict[propertyName] = value;
}else{
propertyDict[propertyName] = @"值為nil";
}
}// 打印propertyfor(NSString*propertyName in propertyDict.allKeys) {NSLog(@"propertyName:%@, propertyValue:%@",propertyName, propertyDict[propertyName]);
}// 3.打印所有methodsunsignedintmethodCount =0;// 用一個(gè)字典裝methodName和argumentsNSMutableDictionary*methodDict = [NSMutableDictionarydictionary];
Method *methodList = class_copyMethodList([p class], &methodCount);for(intk =0; k < methodCount; k++){
SEL methodSel = method_getName(methodList[k]);NSString*methodName = [NSStringstringWithUTF8String:sel_getName(methodSel)];unsignedintargumentNums = method_getNumberOfArguments(methodList[k]);
methodDict[methodName] = @(argumentNums -2);// -2的原因是每個(gè)方法內(nèi)部都有self 和 selector 兩個(gè)參數(shù)}// 打印methodfor(NSString*methodName in methodDict.allKeys) {NSLog(@"methodName:%@, argumentsCount:%@", methodName, methodDict[methodName]);
}
打印結(jié)果為 :
2. 動(dòng)態(tài)變量控制
在程序中,XiaoMing的age是10,后來被runtime變成了20,來看看runtime是怎么做到的:
-(void)changeAge{unsignedintcount =0;//動(dòng)態(tài)獲取XiaoMing類中的所有屬性[當(dāng)然包括私有]Ivar *ivar = class_copyIvarList([self.xiaoMingclass], &count);//遍歷屬性找到對(duì)應(yīng)age字段for(inti =0; i
Ivar var = ivar[i];constchar*varName = ivar_getName(var);NSString*name = [NSStringstringWithUTF8String:varName];if([name isEqualToString:@"_age"]) {//修改對(duì)應(yīng)的字段值成20object_setIvar(self.xiaoMing, var, @"20");break;
}
}NSLog(@"XiaoMing's age is %@",self.xiaoMing.age);
}
3. 在NSObject的分類中增加方法來避免使用KVC賦值的時(shí)候出現(xiàn)崩潰
在有些時(shí)候我們需要通過KVC去修改某個(gè)類的私有變量,但是又不知道該屬性是否存在,如果類中不存在該屬性,那么通過KVC賦值就會(huì)crash,這時(shí)也可以通過運(yùn)行時(shí)進(jìn)行判斷。同樣我們在NSObject的分類中增加如下方法。
/** * 判斷類中是否有該屬性 * *@paramproperty 屬性名稱 * *@return判斷結(jié)果 */-(BOOL)hasProperty:(NSString *)property {
?????BOOL flag = NO;
????u_int count =0;
????Ivar *ivars = class_copyIvarList([self class], &count);for(inti =0; i < count; i++) ? ? ? ? ? ? ?{constchar*propertyName = ivar_getName(ivars[i]);
????NSString *propertyString = [NSString ????????????stringWithUTF8String:propertyName];if([propertyString ?isEqualToString:property]){
flag = YES;
}
}
}
4. 自動(dòng)的歸檔和解檔
博主在學(xué)習(xí) Runtime 之前,歸檔的時(shí)候是醬紫寫的:
-?(void)encodeWithCoder:(NSCoder?*)aCoder{
[aCoder?encodeObject:self.name?forKey:@"name"];
[aCoder?encodeObject:self.ID?forKey:@"ID"];
}
-?(id)initWithCoder:(NSCoder?*)aDecoder{
if(self?=?[superinit])?{
self.ID?=?[aDecoder?decodeObjectForKey:@"ID"];
self.name?=?[aDecoder?decodeObjectForKey:@"name"];
}
returnself;
}
那么問題來了,如果當(dāng)前 Model 有100個(gè)屬性的話,就需要寫100行這種代碼
[aCoder?encodeObject:self.name?forKey:@"name"];
想想都頭疼,通過 Runtime 我們就可以輕松解決這個(gè)問題:
1.使用 class_copyIvarList 方法獲取當(dāng)前 Model 的所有成員變量.
2.使用 ivar_getName 方法獲取成員變量的名稱.
3.通過 KVC 來讀取 Model 的屬性值(encodeWithCoder:),以及給 Model 的屬性賦值(initWithCoder:).
舉個(gè)栗子,新建一個(gè) Model 類,其.m文件如下:
#import?"TestModel.h"
#import?#import?@implementation?TestModel
-?(void)encodeWithCoder:(NSCoder?*)aCoder{
unsigned?int?outCount?=?0;
Ivar?*vars?=?class_copyIvarList([self?class],?&outCount);
for(int?i?=?0;?i?<?outCount;?i?++)?{
Ivarvar=?vars[i];
const?char?*name?=?ivar_getName(var);
NSString?*key?=?[NSString?stringWithUTF8String:name];
//?注意kvc的特性是,如果能找到key這個(gè)屬性的setter方法,則調(diào)用setter方法
//?如果找不到setter方法,則查找成員變量key或者成員變量_key,并且為其賦值
//?所以這里不需要再另外處理成員變量名稱的“_”前綴
id?value?=?[self?valueForKey:key];
[aCoder?encodeObject:value?forKey:key];
}
}
-?(nullable?instancetype)initWithCoder:(NSCoder?*)aDecoder{
if(self?=?[superinit])?{
unsigned?int?outCount?=?0;
Ivar?*vars?=?class_copyIvarList([self?class],?&outCount);
for(int?i?=?0;?i?<?outCount;?i?++)?{
Ivarvar=?vars[i];
const?char?*name?=?ivar_getName(var);
NSString?*key?=?[NSString?stringWithUTF8String:name];
id?value?=?[aDecoder?decodeObjectForKey:key];
[self?setValue:value?forKey:key];
}
}
returnself;
}
@end
完整的自動(dòng)歸檔代碼在這里 :https://github.com/daiweiping/RuntimeLearn
5. 字典轉(zhuǎn)模型
最開始博主是這樣用字典給 Model 賦值的:
-(instancetype)initWithDictionary:(NSDictionary?*)dict{
if(self?=?[superinit])?{
self.age?=?dict[@"age"];
self.name?=?dict[@"name"];
}
returnself;
}
可想而知,遇到的問題跟歸檔時(shí)候一樣(后來使用MJExtension),這里我們稍微來學(xué)習(xí)一下其中原理,字典轉(zhuǎn)模型的時(shí)候:
1.根據(jù)字典的 key 生成 setter 方法
2.使用 objc_msgSend 調(diào)用 setter 方法為 Model 的屬性賦值(或者 KVC)
模型轉(zhuǎn)字典的時(shí)候:
1.調(diào)用 class_copyPropertyList 方法獲取當(dāng)前 Model 的所有屬性
2.調(diào)用 property_getName 獲取屬性名稱
3.根據(jù)屬性名稱生成 getter 方法
4.使用 objc_msgSend 調(diào)用 getter 方法獲取屬性值(或者 KVC)
代碼如下:
#import?"NSObject+KeyValues.h"
#import?#import?@implementation?NSObject?(KeyValues)
//字典轉(zhuǎn)模型
+(id)objectWithKeyValues:(NSDictionary?*)aDictionary{
id?objc?=?[[self?alloc]?init];
for(NSString?*keyinaDictionary.allKeys)?{
id?value?=?aDictionary[key];
/*判斷當(dāng)前屬性是不是Model*/
objc_property_t?property?=?class_getProperty(self,?key.UTF8String);
unsigned?int?outCount?=?0;
objc_property_attribute_t?*attributeList?=?property_copyAttributeList(property,?&outCount);
objc_property_attribute_t?attribute?=?attributeList[0];
NSString?*typeString?=?[NSString?stringWithUTF8String:attribute.value];
if([typeString?isEqualToString:@"@\"TestModel\""])?{
value?=?[self?objectWithKeyValues:value];
}
/**********************/
//生成setter方法,并用objc_msgSend調(diào)用
NSString?*methodName?=?[NSString?stringWithFormat:@"set%@%@:",[key?substringToIndex:1].uppercaseString,[key?substringFromIndex:1]];
SEL?setter?=?sel_registerName(methodName.UTF8String);
if([objc?respondsToSelector:setter])?{
((void?(*)?(id,SEL,id))?objc_msgSend)?(objc,setter,value);
}
}
returnobjc;
}
//模型轉(zhuǎn)字典
-(NSDictionary?*)keyValuesWithObject{
unsigned?int?outCount?=?0;
objc_property_t?*propertyList?=?class_copyPropertyList([self?class],?&outCount);
NSMutableDictionary?*dict?=?[NSMutableDictionary?dictionary];
for(int?i?=?0;?i?<?outCount;?i?++)?{
objc_property_t?property?=?propertyList[i];
//生成getter方法,并用objc_msgSend調(diào)用
const?char?*propertyName?=?property_getName(property);
SEL?getter?=?sel_registerName(propertyName);
if([self?respondsToSelector:getter])?{
id?value?=?((id?(*)?(id,SEL))?objc_msgSend)?(self,getter);
/*判斷當(dāng)前屬性是不是Model*/
if([value?isKindOfClass:[self?class]]?&&?value)?{
value?=?[value?keyValuesWithObject];
}
/**********************/
if(value)?{
NSString?*key?=?[NSString?stringWithUTF8String:propertyName];
[dict?setObject:value?forKey:key];
}
}
}
returndict;
}
@end
完整代碼在這里 ?https://github.com/daiweiping/RuntimeLearn
三、利用runtime的動(dòng)態(tài)交換方法實(shí)現(xiàn),我們可以做什么?
1. 方法簡單的交換
創(chuàng)建一個(gè)Person類,類中實(shí)現(xiàn)以下兩個(gè)類方法,并在.h 文件中聲明
+ (void)run {NSLog(@"跑");
}
+ (void)study {NSLog(@"學(xué)習(xí)");
}
控制器中調(diào)用,則先打印跑,后打印學(xué)習(xí)
[Person run];
[Person study];
下面通過runtime 實(shí)現(xiàn)方法交換,類方法用class_getClassMethod,對(duì)象方法用class_getInstanceMethod
// 獲取兩個(gè)類的類方法
Methodm1=class_getClassMethod([Personclass], @selector(run));
Methodm2=class_getClassMethod([Personclass], @selector(study));
// 開始交換方法實(shí)現(xiàn)method_exchangeImplementations(m1, m2);// 交換后,先打印學(xué)習(xí),再打印跑!
[Person run];
[Person study];
2. 攔截系統(tǒng)方法(Swizzle 黑魔法),也可以說成對(duì)系統(tǒng)的方法進(jìn)行替換
由于某種原因,我們要改變這個(gè)方法的實(shí)現(xiàn),但是又不能去動(dòng)它的源代碼(系統(tǒng)的方法或者一些開源庫出現(xiàn)問題的時(shí)候),這個(gè)時(shí)候runtime就派上用場了。
需求:比如iOS6 升級(jí) iOS7 后需要版本適配,根據(jù)不同系統(tǒng)使用不同樣式圖片(擬物化和扁平化),如何通過不去手動(dòng)一個(gè)個(gè)修改每個(gè)UIImage的imageNamed:方法就可以實(shí)現(xiàn)為該方法中加入版本判斷語句?
步驟:
1、為UIImage建一個(gè)分類(UIImage+Category)
2、在分類中實(shí)現(xiàn)一個(gè)自定義方法,方法中寫要在系統(tǒng)方法中加入的語句,比如版本判斷
+ (UIImage*)xh_imageNamed:(NSString*)name {doubleversion = [[UIDevice currentDevice].systemVersiondoubleValue];if(version >=7.0) {// 如果系統(tǒng)版本是7.0以上,使用另外一套文件名結(jié)尾是‘_os7’的扁平化圖片name = [name stringByAppendingString:@"_os7"];
}return[UIImagexh_imageNamed:name];
}
3、分類中重寫UIImage的load方法,實(shí)現(xiàn)方法的交換(只要能讓其執(zhí)行一次方法交換語句,load再合適不過了)
+ (void)load {// 獲取兩個(gè)類的類方法Method m1 = class_getClassMethod([UIImageclass],@selector(imageNamed:));
Method m2 = class_getClassMethod([UIImageclass],@selector(xh_imageNamed:));// 開始交換方法實(shí)現(xiàn)method_exchangeImplementations(m1, m2);
}
注意:自定義方法中最后一定要再調(diào)用一下系統(tǒng)的方法,讓其有加載圖片的功能,但是由于方法交換,系統(tǒng)的方法名已經(jīng)變成了我們自定義的方法名(有點(diǎn)繞,就是用我們的名字能調(diào)用系統(tǒng)的方法,用系統(tǒng)的名字能調(diào)用我們的方法),這就實(shí)現(xiàn)了系統(tǒng)方法的攔截!
利用以上思路,我們還可以給 NSObject 添加分類,統(tǒng)計(jì)創(chuàng)建了多少個(gè)對(duì)象,給控制器添加分類,統(tǒng)計(jì)有創(chuàng)建了多少個(gè)控制器,特別是公司需求總變的時(shí)候,在一些原有控件或模塊上添加一個(gè)功能,建議使用該方法!
交換原理:
交換之前:
交換之后:
3. 運(yùn)行時(shí)實(shí)現(xiàn)多繼承的效果
既然方法我們可以攔截,可以交換,那么實(shí)現(xiàn)多繼承的效果就留給讀者自己思考了(避免篇幅太長,后續(xù)在博客中再來探討這個(gè)問題)
四、動(dòng)態(tài)添加方法
開發(fā)使用場景:如果一個(gè)類方法非常多,加載類到內(nèi)存的時(shí)候也比較耗費(fèi)資源,需要給每個(gè)方法生成映射表,可以使用動(dòng)態(tài)給某個(gè)類,添加方法解決。
經(jīng)典面試題:有沒有使用performSelector,其實(shí)主要想問你有沒有動(dòng)態(tài)添加過方法。
簡單使用:
@implementationViewController- (void)viewDidLoad {
[superviewDidLoad];// Do any additional setup after loading the view, typically from a nib.Person *p = [[Person alloc] init];// 默認(rèn)person,沒有實(shí)現(xiàn)eat方法,可以通過performSelector調(diào)用,但是會(huì)報(bào)錯(cuò)。// 動(dòng)態(tài)添加方法就不會(huì)報(bào)錯(cuò)[p performSelector:@selector(eat)];
}@end@implementationPerson// void(*)()// 默認(rèn)方法都有兩個(gè)隱式參數(shù),voideat(idself,SEL sel)
{NSLog(@"%@ %@",self,NSStringFromSelector(sel));
}// 當(dāng)一個(gè)對(duì)象調(diào)用未實(shí)現(xiàn)的方法,會(huì)調(diào)用這個(gè)方法處理,并且會(huì)把對(duì)應(yīng)的方法列表傳過來.// 剛好可以用來判斷,未實(shí)現(xiàn)的方法是不是我們想要?jiǎng)討B(tài)添加的方法+ (BOOL)resolveInstanceMethod:(SEL)sel
{if(sel ==@selector(eat)) {// 動(dòng)態(tài)添加eat方法// 第一個(gè)參數(shù):給哪個(gè)類添加方法// 第二個(gè)參數(shù):添加方法的方法編號(hào)// 第三個(gè)參數(shù):添加方法的函數(shù)實(shí)現(xiàn)(函數(shù)地址)// 第四個(gè)參數(shù):函數(shù)的類型,(返回值+參數(shù)類型) v:void @:對(duì)象->self :表示SEL->_cmdclass_addMethod(self,@selector(eat), eat,"v@:");
}return[superresolveInstanceMethod:sel];
}@end
五、利用運(yùn)行時(shí)set和get這兩個(gè)API,可以讓類別可以添加屬性
步驟:
1、創(chuàng)建一個(gè)類別,比如給任何一個(gè)對(duì)象都添加一個(gè)name屬性,就是NSObject添加分類(NSObject+Category)
2、先在.h 中@property 聲明出get 和 set 方法,方便點(diǎn)語法調(diào)用
? ? ?@property(nonatomic,copy)NSString *name;
3、在.m 中重寫set 和 get 方法,內(nèi)部利用runtime 給屬性賦值和取值
charnameKey;
- (void)setName:(NSString*)name {// 將某個(gè)值跟某個(gè)對(duì)象關(guān)聯(lián)起來,將某個(gè)值存儲(chǔ)到某個(gè)對(duì)象中objc_setAssociatedObject(self, &nameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString*)name {returnobjc_getAssociatedObject(self, &nameKey);
}
六、萬能界面跳轉(zhuǎn)(使用了runtime的N多個(gè)方法)
(特別是根據(jù)推送內(nèi)容跳不同界面) http://www.lxweimin.com/writer#/notebooks/16974385/notes/20278179
?http://blog.csdn.net/coyote1994/article/details/52472670
七、插件開發(fā)
插件入門
XCode 有個(gè)很坑爹的地方,就是它并不官方支持插件開發(fā),官方?jīng)]有文檔,XCode 也沒有開源,但由于 XCode 是 Objective-C 寫的,OC 動(dòng)態(tài)性太強(qiáng)大,導(dǎo)致在這么封閉的情況下民間還是可以做出各種插件,其核心開發(fā)方式就是:
dump 出 Xcode 所有頭文件,知道 Xcode 里有哪些類和接口。
通過頭文件方法名猜測方法的作用,swizzle 這些方法,插入自己的代碼實(shí)現(xiàn)插件邏輯。
通過 NSNotificationCenter 監(jiān)聽各種事件的發(fā)生。
更詳細(xì)的開發(fā)教程網(wǎng)上有不少文章,有興趣的自行搜索吧。