Objective-C 中的Runtime的詳細使用
Runtime全面了解
一直以來,OC被大家冠以動態語言的稱謂,其實是因為OC中包含的runtime機制。Runtime 又叫運行時,是一套底層的 C 語言 API,其為 iOS 內部的核心之一,我們平時編寫的 OC 代碼,底層都是基於它來實現的。這一組API可以在Xcode的runtime.h文檔中看到。
關於Runtime的深層次的東西,在很多其他開發者的博客中都有介紹。比如下面這些。
http://www.cnblogs.com/ioshe/ 這篇文章對與初識runtime做了很多基礎性的介紹,並就runtime一些特性做了深入的講解。
https://github.com/ChenYilong/iOSInterviewQuestions/blob/master/01《招聘一個靠譜的iOS》面試題參考答案/《招聘一個靠譜的iOS》面試題參考答案 (上).md 這裏針對一些高質量的iOS面試題做的講解。 其中包含了很多關於runtime 的知識。 看完之後大有裨益。
在本文中,不會很會很深入的進入到Runtime,而是就我們的開發過程中,如何使用runtime來簡便的實現一些功能。主要包含以下方面的內容:
- runtime 獲取類與對象的信息。
- 如何動態給對象添加成員變量。
- 如何動態給成員變量添加屬性。
- 如何動態的給對象添加方法。
- categroy關聯屬性。
- 消息轉發如何實現。
- 如何替換一個已有的方法的實現。
一、runtime 獲取對象的信息。
通過簡單的使用runtime可以獲取到有關於類和對象的一些信息。
@interface GetClassAndIvarInfo () //屬性 @property (nonatomic,copy) NSString* name; @property (nonatomic,assign)int age; @property (nonatomic,assign) BOOL isMan; @end @implementation GetClassAndIvarInfo{ //添加的變量 NSString* _adr; } /** 獲取類相關的信息 */ - (void)getRegisteredClassInfo{ int bufferCount = 0; bufferCount = objc_getClassList(NULL, bufferCount); //開辟一段空間 用於存儲即將獲取的類。 //類型的目的是: 告訴編譯器我需要多大的空間__unsafe_unretained Class *buffer = ( __unsafe_unretained Class *)malloc(sizeof(Class) * bufferCount); objc_getClassList(buffer, bufferCount); for (int i = 0; i < bufferCount; i++) { //查找本類是不是在裏面 if(strcmp(class_getName([self class]), class_getName(buffer[i])) == 0){ NSLog(@"%s", class_getName(buffer[i])); } } } /** 獲取所有的屬性 */ - (void)getAllProp{ unsigned int outCount = 0; NSLog(@"屬性"); objc_property_t *props = class_copyPropertyList([self class], &outCount); for (int i = 0; i < outCount; i++) { NSLog(@"%s",property_getName(props[i])); }
free(props); } /** 獲取所有的變量 */ - (void)getAllIvar{ unsigned int outCount = 0; Ivar *ivars = class_copyIvarList( object_getClass(self),&outCount); NSLog(@"變量"); for (int i = 0; i < outCount; i++) { NSLog(@"%s",ivar_getName(ivars[i])); }
free(ivars); } /** 獲取所有的方法 */ - (void)getAllMethod{ unsigned int outCount = 0; Method *methods = class_copyMethodList(object_getClass(self),&outCount); NSLog(@"方法名"); for (int i = 0; i < outCount; i++) { NSLog(@"%s",sel_getName(method_getName(methods[i]))); }
free(methods);
}
通過上面的這些方法。 我們可以方便的做一些有關方法屬性的工作了。 比如,當對某個類進行歸檔的時候,如果能獲取累類的所有屬性,運用KVC進行賦值和取值。就能用很簡短的代碼實現整個類的歸檔動作。
除了上面的幾個簡單的方法之外,還有很多非常實用的runtime的API:
- OBJC_EXPORT id object_getIvar(id obj, Ivar ivar) //給變量設置值 KVC通過這個方式做
- OBJC_EXPORT void object_setIvar(id obj, Ivar ivar, id value) //獲取成員變量的值 KVC
- OBJC_EXPORT Class objc_getMetaClass(const char *name) //獲取該類的元類, 用於分析isa指針
- Protocol * __unsafe_unretained *class_copyProtocolList(Class cls, unsigned int *outCount) //獲取類遵循的協議中的方法列表
- OBJC_EXPORT Class objc_getFutureClass(const char *name) //toll-free bridging. 分析中,獲取轉換類的名字
二、runtime 給類添加成員變量
oc中,我們還可以給一個類動態的添加成員變量。 但是有一個前提是:被添加成員變量的類必須是動態創建的類。曾經有個人問我,對於已經編譯的類,能否使用運行時添加成員變量, 答案是不行的。 好,下面的代碼演示如何創建一個動態的類。
/* 1. 創建一個類。比如: Car,繼承自NSObjest 2. 給這個類添加兩個成員變量,分別是: 車身的顏色 bodyColor 和 車的最高速度 maxSpeed 3. 添加一些方法。以便可以訪問兩個成員變量。 3. 使用這個類創建對象,並對對象的成員屬性進行訪問。 */ NSString *bodyColorName = @"bodyColor"; //類型為 UIcolor NSString *maxSpeedName = @"maxSpeed"; // 類型為 NSString NSString *className = @"Car"; Class Car = objc_getClass([className UTF8String]); if (!Car) { Class superClass = [NSObject class]; Car = objc_allocateClassPair(superClass, [className UTF8String], 0);
//添加成員變量的代碼必須放在這裏
objc_registerClassPair(Car); //註冊到運行時 }
這些代碼演示添加成員變量
if(class_addIvar([Car class],[maxSpeedName UTF8String], sizeof(NSString *), log2(_Alignof(NSString *)), @encode(NSString *))){ NSLog( @"添加最高速度成功。"); } if(class_addIvar([Car class],[bodyColorName UTF8String], sizeof(UIColor *), log2(_Alignof(UIColor *)), @encode(UIColor *))){ NSLog( @"添加車身速顏色成功。"); }
雖然每次添加成功之後,會打印相關的提示文字,何不驗證一下呢?運用上一節的內容,打印一下car的所有的成員變量吧
id car = [[Car alloc]init]; unsigned int outCount = 0; Ivar *ivars = class_copyIvarList( object_getClass(car),&outCount); NSLog(@"變量"); for (int i = 0; i < outCount; i++) { NSLog(@"%s",ivar_getName(ivars[i])); } free(ivars);
結果是:
2017-05-12 11:11:31.784 runtimeTest[3072:90675] 添加最高速度成功。
2017-05-12 11:11:31.784 runtimeTest[3072:90675] 添加車身速顏色成功。
2017-05-12 11:11:31.784 runtimeTest[3072:90675] maxSpeed
2017-05-12 11:11:31.784 runtimeTest[3072:90675] bodyColor
看來對了。
如何訪問添加的變量? 通過runtime可以做到。
unsigned int outCount = 0; Ivar *ivars = class_copyIvarList( object_getClass(car),&outCount); NSLog(@"變量"); //runtime 賦值 for (int i = 0; i < outCount; i++) { NSLog(@"%s",ivar_getName(ivars[i])); if(strcmp(ivar_getName(ivars[i]), [bodyColorName UTF8String])){ object_setIvar(car, ivars[i] , [UIColor blueColor]); } if(strcmp(ivar_getName(ivars[i]), [maxSpeedName UTF8String])){ object_setIvar(car, ivars[i] ,@"205.5 km/h"); } } //runtime 取值 for (int i = 0; i < outCount; i++) { if(strcmp(ivar_getName(ivars[i]), [bodyColorName UTF8String])){ NSLog(@"車的顏色是%@", object_getIvar(car, ivars[i])); } if(strcmp(ivar_getName(ivars[i]), [maxSpeedName UTF8String])){ object_getIvar(car, ivars[i]); NSLog(@"速度是%@",object_getIvar(car, ivars[i])); } } free(ivars);
當然這樣每次寫起來實在是不怎麽友好。 其實又個更簡單的辦法,利用KVC。
//利用KVC賦值 取值 [car setValue:[UIColor redColor] forKey:bodyColorName]; [car setValue:@"199.6 km/h" forKey:maxSpeedName]; NSLog(@"車的顏色是%@, 速度是%@",[car valueForKey:bodyColorName],[car valueForKey:maxSpeedName]);
三、如何動態的給成員變量添加屬性。
剛才我創建了一個類,並給他添加了成員變量,並且做到了如何進行訪問。 接下來我還希望能給這些成員變量添加添加的屬性,以便編譯器更好的幫我們做內存的管理等。比如nonatomic、copy之類的屬性。
比如我們要為成員變量 maxSpeedName 添加 nonatomic、copy屬性。看這些代碼.
/* 添加成員變量的屬性 */ //在添加之前,需要先編輯屬性。 //這裏給 maxSpeed成員變量添加屬性。 這些屬性的 encode 可以官網看到。 objc_property_attribute_t type = { "T", [[NSString stringWithFormat:@"@\"%@\"",NSStringFromClass([car class])] UTF8String] }; objc_property_attribute_t ownership = { "&", "N" }; objc_property_attribute_t ownership0 = { "C", "" }; // C = copy objc_property_attribute_t ownership1 = { "N", "" }; // N = nonatomic objc_property_attribute_t backingivar = { "V", [[NSString stringWithFormat:@"_%@", maxSpeedName] UTF8String] }; //這裏需要註意的是: type和backingivar 必須放在頭部和尾部 不然會有意想不到的後果 objc_property_attribute_t attrs[] = { type, ownership,ownership0,ownership1,backingivar}; //參數的描述分別是: 對象的類,屬性的預設名字,屬性數組,屬性的個數 if(class_addProperty([car class], [maxSpeedName UTF8String], attrs, 5)){ NSLog(@"添加屬性maxSpeedName 成功"); //打印下 unsigned int outCount = 0; NSLog(@"屬性"); objc_property_t *props = class_copyPropertyList([car class], &outCount); for (int i = 0; i < outCount; i++) { NSLog(@"名字:%s",property_getName(props[i])); //屬性值 NSLog(@"屬性值:%s",property_getAttributes(props[i])); } free(props);
添加屬性之後,如果設置setter和getter方法,那麽這些操作需要根據不同的屬性進行設置,比如,storeWeak 就表示對帶有weak屬性的變量進行存儲。
四、如何動態的給對象添加方法。
添加屬性之後,我們最好還是能添加響應的個體和set方法,這是OC一貫的風格。
//添加get和set方法 class_addMethod([car class], NSSelectorFromString(maxSpeedName), (IMP)getter, "@@:"); class_addMethod([car class], NSSelectorFromString([NSString stringWithFormat:@"set%@:",[maxSpeedName capitalizedString]]), (IMP)setter, "v@:@"); outCount = 0; Method *methods = class_copyMethodList(object_getClass(car),&outCount); NSLog(@"方法名"); for (int i = 0; i < outCount; i++) { NSLog(@"%s",sel_getName(method_getName(methods[i]))); NSLog(@"%p",sel_getName(method_getImplementation(methods[i]))); } free(methods); //調用 [self setMaxSpeed:@"300km/h" target:car]; NSLog(@"%@",[self maxSpeedWithTarget:car]);
- (void)setMaxSpeed:(NSString *)maxSpeed target:(NSObject*)car{ // 動態添加的方法,需要使用performselector調用。 因為在註冊的類中,我們還沒有設置改變類的變量布,也沒有設置方法列表。 if([car respondsToSelector:NSSelectorFromString(@"setMaxspeed:" )]){ [car performSelector:NSSelectorFromString(@"setMaxspeed:") withObject:@"300 km/h"]; } } - (NSString *)maxSpeedWithTarget:(NSObject*)car{ //這裏不僅判斷有可能報錯的
if([car respondsTOSelector:NSSelectorFromString:(@"maxspeed")]){ return [car performSelector:NSSelectorFromString(@"maxspeed")];
} } id getter(id self1, SEL _cmd1) { NSString *key = NSStringFromSelector(_cmd1); Ivar ivar = class_getInstanceVariable([self1 class], [key cStringUsingEncoding:NSUTF8StringEncoding]); NSString *s = object_getIvar(self1, ivar); return s; } void setter(id self1, SEL _cmd1, id newValue) { //移除set NSString *key = [NSStringFromSelector(_cmd1) stringByReplacingCharactersInRange:NSMakeRange(0, 3) withString:@""]; //首字母小寫 NSString *head = [key substringWithRange:NSMakeRange(0, 1)]; head = [head lowercaseString]; key = [key stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:head]; //移除後綴 ":" key = [key stringByReplacingCharactersInRange:NSMakeRange(key.length - 1, 1) withString:@""]; Ivar ivar = class_getInstanceVariable([self1 class], [key cStringUsingEncoding:NSUTF8StringEncoding]); object_setIvar(self1, ivar, newValue); }
對於 BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types), 幾個參數相信很容易理解。 cls是要操作的類,name方法的名字,imp實現函數的指針,types是方法的類型。像文中的 "v@:@" 它是方法的類型,是一種縮寫,有利於編譯時提升效率。 我們還可以通過
- method_copyReturnType
-
method_copyArgumentType
這兩個runtime API獲取對應的方法type。 當然去蘋果官網了解下相信會更明白。
這裏就不詳細介紹。
五、categroy關聯屬性。
剛開始接觸的OC的時候,大部分都會有這麽一個認知,category是不能添加屬性的,只能添加成員變量,並在私有中使用。但是可以使用runtime來添加屬性,使得屬性可在 public中使用,這種操作也即是關聯屬性。
static const NSString* addProp = @"addName"; @implementation NSObject (ClassInfo) /** 設置get方法 @return value */ - (NSString *)name{ return objc_getAssociatedObject(self, [addProp UTF8String]); } /** 設置set方法 @param name newVlaue */ - (void)setName:(NSString *)name{ objc_setAssociatedObject(self, [addProp UTF8String], name, OBJC_ASSOCIATION_COPY_NONATOMIC); }
之後就可以直接調用這個變量了。這種方法用的非常多。不僅可以使得category增加屬性,還特別的簡潔明了。
[self setName:@"test000000"]; NSLog(@"%@",self.name);
六、消息發送/轉發是如何實現。
我們知道。OC中所有的調用其實就是消息的傳遞。在使用OC方法的時候,實際上在runtime中是將放啊放轉化成了C語言的 API :
id objc_msgSend(id self, SEL op, ...) //這裏包含消息的發送者,方法名,方法的類型。舉個簡單的例子: 如果我們要執行一個方法:
[self setName:@"小明"]; ----> objc_msgSend(self,method_getName(method),method_getTypeEncoding(method))
除了 objc_msgSend 還有如下幾個發送消息的API
- objc_msgSend(self,sel); // 發送著為本類的實例對象 如果返回的是常用的類型值的時候,調用
- objc_msgSendSuper(); // 發送著是 超類的實例對象的時候 返回常用類型 調用
- objc_msgSend_stret(); // 發送者是 本類的實例對象, 返回一個結構體 調用
- objc_msgSendSuper_stret(); // 發送者是 超類的實例對象, 返回一個結構體 調用
- objc_msgSend_fpret(); // 本類的實例對象, 返回一個浮點類型 調用
這些方法調用的流程是什麽呢? 通過一副圖片了解下。當一個msgSend執行的時候,經過以下幾個步驟:
1.檢測消息類型是否被忽略,mac上的retain等操作是被忽略的。
2.檢測發送者是不是空指針,如果是,直接retrun ,這裏不會產生Crash。
3.在mothod cache 中尋找對應的IMP,有則執行。沒有進行下一步。
4.在mothod list 中尋找IMP ,有則執行,沒有則下一步。 執行之後會將IMP放入cache,以便下次訪問提升效率。
5.在父類中繼續尋找。有則執行,沒有則進行下一步/ 執行之後會將IMP放入cache,以便下次訪問提升效率。
6.進入消息轉發,或者crash 拋出異常。
在這裏詳細講一下消息的轉發。一張示意圖。
在上面的那副圖中,消息轉發的類型有兩大類,一類是 對象方法,也就是我們說的 - 方法。另一類是 類方法,即 + 方法。-方法 有三次機會可以進行消息的轉發,但是對於 + 方法,只有一次。
我們先看看 +方法。 如果想要轉發+方法,只需要重寫 + (BOOL)resolveClassMethod:(SEL)sel 即可。如下
// runtime中的消息 - (void)testTwo{ //我們隨便發送一個沒有定義過的方法 [[self class] performSelector:@selector(classMethodTest)]; } + (BOOL)resolveClassMethod:(SEL)sel{ //針對類方法 //第一種 使用自定義方法制作IMP 進行轉發 class_addMethod(objc_getMetaClass([NSStringFromClass([self class]) UTF8String]), NSSelectorFromString(@"classMethodTest"), (IMP)classForwardFunc,"V@:" ); return [super resolveClassMethod:sel]; //第二種 使用制作block的方法得到IMP 進行轉發 methodBlock ablock = ^{ NSLog(@"使用 block的 IMP 接到消息的轉發"); }; IMP amethod = imp_implementationWithBlock(ablock); class_addMethod(objc_getMetaClass([NSStringFromClass([self class]) UTF8String]), NSSelectorFromString(@"classMethodTest"), amethod,"V@:" ); return [super resolveClassMethod:sel]; } void classForwardFunc(id self1, SEL _cmd1) { NSLog(@"類消息轉發成功"); }
classMethodTest在self 中是沒有定義的,如果我們強行調用,會提示警報,並且運行會crash. 如果重寫 resolveClassMethod ,會先進入這個方法中,我們在這裏進行 IMP的添加替換,註意這裏操作的對象是self的元類, 因為在OC的內存布局中,元類中存放靜態方法。如果這裏不進行轉發,接下來程序將回崩潰。
對於- 方法有所不同,它有三次機會進行消息的轉發。第一種有點類似的,- 方法也有一個方法用於替換IMP的。
// runtime中的消息 - (void)testTwo{ //我們隨便發送一個沒有定義過的方法 [self performSelector:@selector(instanceMethodTest)]; // [[self class] performSelector:@selector(classMethodTest)]; } + (BOOL)resolveInstanceMethod:(SEL)sel{ //針對實例方法 // 1. class_addMethod([self class], NSSelectorFromString(@"instanceMethodTest"), (IMP)instanceForwardFunc,"V@:" ); return [super resolveInstanceMethod:sel]; // 2. //同樣可以使用block方法得到IMP 進行轉發 methodBlock ablock = ^{ NSLog(@"使用 block的 IMP 接到消息的轉發"); }; IMP amethod = imp_implementationWithBlock(ablock); class_addMethod(objc_getMetaClass([NSStringFromClass([self class]) UTF8String]), NSSelectorFromString(@"instanceMethodTest"), amethod,"V@:" ); return [super resolveClassMethod:sel]; } void instanceForwardFunc(id self1, SEL _cmd1) { NSLog(@"對象消息轉發成功"); }
跟+方法很類似的。
第二種情況 ,替換消息發送者轉發。 如果self 中沒有對應的方法,除了替換IMP達到轉發的目的,替換self也是可以的。這個動作將在下面的方法中實現。
// 這是消息發送者轉發階段 -(id)forwardingTargetForSelector:(SEL)aSelector{ NSLog(@"FlyElephant-http://www.cnblogs.com/xiaofeixiang/"); NSLog(@"forwardingTargetForSelector"); if (aSelector == @selector(instanceMethodTest)) { //對象方法 return [[Other alloc] init]; }return self; }
@interface Other : NSObject - (void)instanceMethodTest; @end @implementation Other - (void)instanceMethodTest{ NSLog(@"更換對象轉發 對象 消息成功"); } @end
如果前面兩種情況我們都沒有使用,蘋果還提供了一種方式用語轉發: 完整轉發! 意思就是將IMP和self都替換掉。 看下面的代碼。
//如果第二種情況還是沒有轉發 第三種情況 整體轉發 - (NSMethodSignature*)methodSignatureForSelector:(SEL)selector { // 返回一個簽名 // 只有包含了selector方法的對象的簽名才是有效的 //用另一個實現了seletor的對象 創建si。 Another *another = [[Another alloc] init]; NSMethodSignature * si = [another methodSignatureForSelector:selector]; if(si){ return si; } return [super methodSignatureForSelector:selector]; } - (void)forwardInvocation:(NSInvocation *)anInvocation{ [anInvocation setSelector:anInvocation.selector]; //這裏有點小技巧, 這裏的 selector 是可以更改的,只需要確保another的method list包含這個selecor // 比如: [anInvocation setSelector:NSSelectorFromString(@"anotherFunc")]; [anInvocation invokeWithTarget:[[Another alloc] init]]; }
@interface Another : NSObject - (void)instanceMethodTest; - (void)anotherFunc; @end @implementation Another - (void)instanceMethodTest{ NSLog(@"更換對象轉發 對象 消息成功 Another對象"); } - (void)anotherFunc{ NSLog(@"消息轉發 同時更改方法名字 Another對象"); } @end
如果將Another的所有的方法都使用這種方式轉發,包括它的屬性的set和get,那麽就做到類似於繼承的效果。 再者,對於多個類做到同樣的效果, 就有了OC的多繼承實現了。
七、如何替換一個已有的方法的實現。
替換一個已有的方法的實現,使用繼承加上重寫就可以做到,但是我今天來說下使用runtime做到不繼承的情況下,實現方法實現的替換。也就是大名鼎鼎的 method swizzling的做法。
首先我們來看看metodSwizzling的原理是什麽。
在runtime中,method 的結構體大概是這樣的。
typedef struct objc_ method {
SEL method_name; 方法名 SEL
char *method_types; 方法類型, 包括了參數和返回值類型 通過method_getTypeEncoding獲得
IMP method_imp; 方法實現的函數指針 IMP
};
在runtime中有幾個API:
- IMP method_getImplementation(Method m) //獲取某個方法的函數的實現
- IMP method_setImplementation(Method m, IMP imp) //設置某個方法的函數的實現
- void method_exchangeImplementations(Method m1, Method m2) //改變某個方法的函數的實現
- Method class_getInstanceMethod(Class cls, SEL name) //通過方法名獲取 method
// runtime的方法交換 - (void)testThree{ Method methodA = class_getInstanceMethod([self class], NSSelectorFromString(@"funcA")); Method methodB = class_getInstanceMethod([self class], NSSelectorFromString(@"funcB")); IMP impA = method_getImplementation(methodA); IMP impB = method_getImplementation(methodB); method_setImplementation(methodA, impB); method_setImplementation(methodB, impA); if([self funcA]){ NSLog(@"執行了 方法A"); } if([self funcB]){ NSLog(@"執行了 方法B"); } } //定義方法A - (BOOL)funcA{ NSLog( @"我是方法A"); return YES; } //定義方法B - (BOOL)funcB{ NSLog( @"我是方法B"); return YES; }
打印:
2017-05-12 11:11:35.595 runtimeTest[3072:90675] 我是方法B
2017-05-12 11:11:35.595 runtimeTest[3072:90675] 執行了方法A
2017-05-12 11:11:35.595 runtimeTest[3072:90675] 我是方法A
2017-05-12 11:11:35.595 runtimeTest[3072:90675] 執行了方法B
方法被交換。
如果使用的 method_exchangeImplementations 也是等效的 ,代碼如下:
Method methodA = class_getInstanceMethod([self class], NSSelectorFromString(@"funcA")); Method methodB = class_getInstanceMethod([self class], NSSelectorFromString(@"funcB")); // IMP impA = method_getImplementation(methodA); // IMP impB = method_getImplementation(methodB); // method_setImplementation(methodA, impB); // method_setImplementation(methodB, impA); //使用 method_exchangeImplementations 等效 method_exchangeImplementations(methodA, methodB); if([self funcA]){ NSLog(@"執行了 方法A"); } if([self funcB]){ NSLog(@"執行了 方法B"); }
仔細想想,這個方式的作用非常有效,我們如果需要替換某個系統的方法的時候,盲目的重寫可能帶來無法預知的後果,並且維護起來也很困難。 使用方法替換可做到一次替換,一直有效,並可在局部進行。 正常情況下,我們會在
+(void)load{ //執行替換 }
替換方法,原因是,再不主動調用的情況下,load只會執行一次,並且不會收到超類或者類別的影響。 當然為了防止程序員手動調用,執行了過多次數的替換,可以把替換的代碼使用 GCD 的oncetime_t中擴寫。這樣保證了絕對的一次調用。(偶數次的調用會回到沒有替換的狀態)。
以上的代碼在https://github.com/lufubinGit/runtimeTest上
相關鏈接:
http://www.cnblogs.com/ioshe/p/5489086.html
http://www.cocoachina.com/ios/20160121/15076.html
http://blog.sunnyxx.com/2015/09/13/class-ivar-layout/
Objective-C 中的Runtime的詳細使用