ping怎么這么高?
哈哈,進入正題!
什么是Runtime?
這還要說?run( 運行)、time(時),runtime(運行時),沒毛病!好了,我們都知道Objective-C是基于C衍生出來的動態語言,加入了面向對象特征和消息機制,這都歸功于Runtime,它將靜態語言在編譯和鏈接時期做的事放到了運行時來處理。在我們Objective-C中,runtime是一個運行時庫,是一套純C的API。
-
面向對象,在OC中一切都被設計成對象,它們的基礎數據結構在Runtime庫中用C語言的結構體表示。
當一個類被初始化成一個實例,這個實例就是一個對象,在runtime中用objc_object
結構體表示,可以到objc/objc.h
查看定義
typedef struct objc_class *Class;
/// Represents an instance of a class.
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY; //結構體指針,指向類對象,這樣,當我們向對象發送消息時,runtime庫會根據這個isa指針找到對象所屬的類,然后從類的方法列表及父類方法列表中查找消息對應的selector指向的函數實現,然后執行。
};
/// A pointer to an instance of a class.
typedef struct objc_object *id; //該類型對象可以轉換成任意對象
當然類也是對象,由Class
類型表示,它實際上是一個指向 objc_class
結構體的指針,可以到objc/runtime.h
中查看定義
typedef struct objc_class *Class;
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY; //結構體的指針,每個對象都有一個isa指針,實例的isa指向類對象,類對象的isa指向元類。
#if !__OBJC2__
Class super_class //父類
const char *name //類名
long version //類的版本信息,默認為0
long info //類信息,提供一些標識
long instance_size //實例變量大小
struct objc_ivar_list *ivars //成員變量列表
struct objc_method_list **methodLists //方法列表
struct objc_cache *cache //方法緩存
struct objc_protocol_list *protocols //協議列表
#endif
}
上面引出一個元類(Meta Class):類對象的類,它儲存著一個類所有的類方法,每個類都有一個單獨的meta-class,因為每個類的類方法基本不可能完全相同,那么細想,元類也是有isa
指針的,它指向誰呢?為了不讓這種結構無限延伸下去,isa
指向基類的meta-class,而基類的meta-class的isa
指針指向它自己。
-
消息機制,在OC中任何的方法調用,其本質都是消息發送,
id objc_msgSend(id self, SEL op, ...)
,屬于動態調用的過程,比如[receiver doSomething]
,在運行時就會轉成objc_msgSend(receiver,@selector(doSomething))
,receiver作為一個消息接收對象,@selector(doSomething)
是一個消息體,函數內部執行順序:
- 檢查消息對象是否為nil,如果是,則什么都不做。
- 通過receiver 的
isa
指針找到receiver對應的類,從類的方法緩存中通過SEL
查找IMP
,有,調用;沒有,往下。(類的方法很多,如果每次都去方法列表中查找就會影響到效率,所以每一個類都會有一個方法緩存)。 - 從方法列表中查找,有,調用;沒有,往下。
- 查找父類的方法緩存,有,直接調用;沒有,往下。
- 查找父類的方法列表,有,直接調用;沒有,往下,一直找到基類,以上就是一個正常的消息發送過程。
- 如果在基類也沒有找到,則會調用NSObject的決議方法
+ (BOOL)resolveInstanceMethod:(SEL)sel
或+ (BOOL)resolveClassMethod:(SEL)sel
,返回YES則重啟一次消息的發送過程,返回NO則會進入消息轉發。 - 調用
- (id)forwardingTargetForSelector:(SEL)aSelector
,如果實現了這個方法,并返回一個非nil的對象,則這個對象會作為消息的新接收者,且消息會被分發到這個對象,當然這個對象不能是self自身,否則就是出現無限循環;如果返回的是nil,往下繼續。 - 調用
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
,生成一個方法簽名,接著會創建一個NSInvocation(消息調用對象,包含target,selector,以及方法簽名)
,并傳給- (void)forwardInvocation:(NSInvocation *)anInvocation
,進行轉發調用。
消息異常處理
當消息異常的時候,會執行方法決議以及消息轉發,在上面的消息發送過程中也具體介紹了,這里借用一張圖片來更好的理解
-
在這個過程中,我們可以在方法決議中添加方法實現并返回YES,來阻止crash
@implementation NSObject (ZMSafe) +(void)load{ static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [self zm_swizzleInstanceMethodWithSrcClass:[self class] srcSel:@selector(forwardInvocation:) swizzledSel:@selector(zm_forwardInvocation:)]; [self zm_swizzleInstanceMethodWithSrcClass:[self class] srcSel:@selector(methodSignatureForSelector:) swizzledSel:@selector(zm_methodSignatureForSelector:)]; [self zm_swizzleInstanceMethodWithSrcClass:[self class] srcSel:@selector(forwardingTargetForSelector:) swizzledSel:@selector(zm_forwardingTargetForSelector:)]; [self zm_swizzleClassMethodWithSrcClass:[self class] srcSel:@selector(resolveInstanceMethod:) swizzledSel:@selector(zm_resolveInstanceMethod:)]; }); } +(BOOL)zm_resolveInstanceMethod:(SEL)sel{ if(sel == NSSelectorFromString(@"push")){ NSLog(@"unrecognized selector -[%@ %@]\n%s",NSStringFromClass(self),NSStringFromSelector(sel),__FUNCTION__); /* //這是method的數據結構,在method其實就相當于在SEL跟IMP之間作了一個映射,有了SEL,我們便可以找到對應的IMP struct objc_method { SEL method_name //方法名 char *method_types //方法類型 IMP method_imp //實現地址 } */ Method method = class_getClassMethod([self class], @selector(empty)); // 獲取函數類型,有沒有返回參數,傳入參數 const char *type = method_getTypeEncoding(method); // 添加方法,將未實現的方法編號sel跟自定義的方法實現imp關聯 class_addMethod([self class], sel, method_getImplementation(method), type); // 返回YES,重啟一次消息的發送過程,現在已經添加了方法實現empty,所以會直接調用它 return YES; } return [[self class]zm_resolveInstanceMethod:sel]; } - (void)empty{ NSLog(@"empty"); }
看調用結果,執行了決議方法和自定義的方法實現empty,并沒有crash。
其實在這里還可以做很多的事情,比如版本的適配,在低版本中調用了高版本的方法,在這里就可以把方法名提取出來,再指向我們自定義的方法實現,等等。
-
也可以在
- (id)forwardingTargetForSelector:(SEL)aSelector
替換消息接收對象- (id)zm_forwardingTargetForSelector:(SEL)aSelector{ if(aSelector == NSSelectorFromString(@"push")){ NSLog(@"unrecognized selector -[%@ %@]\n%s",NSStringFromClass([self class]),NSStringFromSelector(aSelector),__FUNCTION__); // 我這里就直接動態創建一個類 Class ZMClass = objc_allocateClassPair([NSObject class], "ZMClass", 0); // 注冊類 objc_registerClassPair(ZMClass); // 獲取自定義empty方法 Method method = class_getClassMethod([self class], @selector(empty)); // 獲取函數類型,有沒有返回參數,傳入參數 const char *type = method_getTypeEncoding(method); // 添加方法,將未實現的方法編號sel跟自定義的方法實現imp關聯 class_addMethod(ZMClass, aSelector, method_getImplementation(method), type); // 返回該對象來接收消息 return [[ZMClass alloc]init]; } return [self zm_forwardingTargetForSelector:aSelector]; }
再看調用結果,效果是一樣的,只是不同的處理方式而已,從打印上可以看出,這是在- (id)zm_forwardingTargetForSelector:(SEL)aSelector
中進行處理的,也是替換的- (id)forwardingTargetForSelector:(SEL)aSelector
方法,找到返回的備用對象去執行調用的方法。
-
或者在最后一步也就是消息真正轉發的方法中做處理,重寫
- (void)forwardInvocation:(NSInvocation *)anInvocation
,同時一定要重寫- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
,因為anInvocation
對象是通過返回方法簽名來創建的。/** 消息轉發方法 @param anInvocation 消息轉發對象 */ - (void)zm_forwardInvocation:(NSInvocation *)anInvocation{ NSLog(@"unrecognized selector -[%@ %@]\n%s",anInvocation.target,NSStringFromSelector([anInvocation selector]),__FUNCTION__); //如果自定義實現方法中什么都沒做,只是為了能在運行時找到該實現方法,不至于crash,那么這里可以不進行消息發送,可以注釋掉 if (![self respondsToSelector:anInvocation.selector]) { // 拿到方法對象 Method method = class_getClassMethod([self class], @selector(empty)); // 獲取函數類型,有沒有返回參數,傳入參數 const char *type = method_getTypeEncoding(method); // 添加方法 class_addMethod([self class], anInvocation.selector, method_getImplementation(method), type); // 轉發給自己,沒毛病 [anInvocation invokeWithTarget:self]; } } /** 構造一個方法簽名,提供給- (void)forwardInvocation:(NSInvocation *)anInvocation方法,如果aSelector沒有對應的IMP,則會生成一個空的方法簽名,最終導致程序報錯崩潰,所以必須重寫。 @param aSelector 方法編號 @return 方法簽名 */ - (NSMethodSignature *)zm_methodSignatureForSelector:(SEL)aSelector { if ([self respondsToSelector:aSelector]) { // 如果能夠響應則返回原始方法簽名 return [self zm_methodSignatureForSelector:aSelector]; }else{ // 構造自定義方法的簽名,不實現則返回nil,導致crash return [[self class] instanceMethodSignatureForSelector: @selector(empty)]; } }
調用結果也是一樣的,這也是異常消息處理最后的機會,錯過了就沒機會了。
小結
這里主要是通過一個異常的消息來演示消息發送以及轉發的過程,并在消息轉發過程中對異常消息的捕捉及處理,我把這些寫到NSObject的類目中主要為了防止開發中調用了不存在的方法導致的crash,當然如果在子類中重寫了這些方法,可以調用super,也是一樣的。
基礎用法
-
對象關聯,
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
,這也是我在實際開發中使用<objc/runtime.h>
的第一個API,方法的意思就是將兩個不相關的對象通過一個特定的key關聯起來,這樣我們拿到對象object可以通過key找到對象Value,最具有代表性的運用就是給類目添加屬性了。@interface NSObject (Property) @property (nonatomic,copy)NSString *text; @end @implementation NSObject (Property) // 手動構造Set方法,讓text對象通過SEL指針跟self關聯起來 - (void)setText:(NSString *)text{ objc_setAssociatedObject(self, @selector(setText:), text, OBJC_ASSOCIATION_COPY_NONATOMIC); } // 手動構造Get方法,通過SEL指針獲取text對象 - (NSString *)text{ return objc_getAssociatedObject(self, @selector(setText:)); } // 移除該對象下所有關聯的對象 - (void)removeProperty{ objc_removeAssociatedObjects(self); }
-
獲取屬性列表,
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
,cls表示獲取該類的屬性列表,outCount表示屬性的總個數。
舉栗:模型轉字典
- (NSDictionary *)dictionary{NSMutableDictionary *dic = [NSMutableDictionary dictionary]; unsigned int count; objc_property_t *propertyList = class_copyPropertyList([self class], &count); for (int i = 0; i < count; i ++) { //生成key NSString *key = [NSString stringWithUTF8String:property_getName(propertyList[i])]; //獲取value id value = [self valueForKey:key]; if (!value) break; [dic setObject:value forKey:key]; } free(propertyList); return dic; }
-
獲取成員變量列表,
Ivar *class_copyIvarList(Class cls, unsigned int *outCount)
,跟獲取屬性列表一個意思,不同的是這里會獲取該類所有的成員變量,當然其中也包括所有的屬性。
舉栗:NSCoding協議,我們想要把模型直接寫成本地文件,是要實現編解碼協議的,而且要一個一個的寫,這里通過拿到屬性列表來對所有屬性來編解碼,一勞永逸。
- (id)initWithCoder:(NSCoder *)aDecoder {
self = [super init];
if (self) {
unsigned int count;
Ivar *ivarList = class_copyIvarList([self class], &count);for (int i = 0; i < count; i ++) { //拿到成員變量 Ivar ivar = ivarList[i]; //生成key NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)]; //獲取value id value = [aDecoder decodeObjectForKey:key]; [self setValue:value forKey:key]; } free(ivarList); } return self; } - (void)encodeWithCoder:(NSCoder *)aCoder{ unsigned int count; Ivar *ivarList = class_copyIvarList([self class], &count); for (int i = 0; i < count; i ++) { //拿到成員變量 Ivar ivar = ivarList[i]; //獲取key NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)]; //獲取value id value = [self valueForKey:key]; [aCoder encodeObject:value forKey:key]; } free(ivarList); }
-
獲取方法列表,
Method *class_copyMethodList(Class cls, unsigned int *outCount)
,可以獲取cls類的方法列表,包括私有方法,這樣我們就可以調用對象的私有方法。
@interface Student : NSObject
@end
@implementation Student
- (void)study{NSLog(@"學習"); } - (void)goHome{ NSLog(@"回家"); } @end @interface ViewController () @end @implementation ViewController - (void)findStudentMethods{ Student *student = [[Student alloc]init]; unsigned int count; Method *methodList = class_copyMethodList([Student class], &count); for (int i = 0; i < count; i ++) { //獲取方法對象 Method method = methodList[i]; //獲取方法名 SEL sel = method_getName(method); NSLog(@"方法名:%@",NSStringFromSelector(sel)); if (sel == NSSelectorFromString(@"study")) { //通過NSInvocation來轉發消息 NSMethodSignature *methodSign = [[Student class] instanceMethodSignatureForSelector:sel]; NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSign]; invocation.selector = sel; [invocation invokeWithTarget:student]; } } }
打印結果如我們所料,能夠拿到所有的方法,也能調用私有方法。
- 動態添加方法,動態創建類,細心的會發現,我在上面第一二段代碼就已經描述過了,這里也不在啰嗦了。
-
Method Swizzling,這個我也不在這里多說了,之前寫過一篇關于
Method Swizzling
的介紹,iOS Method Swizzling理解與運用
總結
這里主要是寫了自己對Runtime的理解,以及在平時開發中的運用。Runtime里面的API有很多,目前對它的理解以及運用程度有限,所以借此來拋磚引玉,同時有什么錯誤的地方,希望朋友們能夠指出改正,謝謝。