Objective-C是一門動態語言,不同於許多靜態語言,例如C語言,只能在編譯和鏈接階段把程序運行的上下文做好,在運行期間,無法修改,缺少動態性。Objective-C的動態性,給開發者提供了一種在運行期,修改程序執行流程的機會,這要歸功於其強大的Runtime機制。
這篇文章主要介紹,目前,Runtime機制在我們項目中的應用場景。
- 前言
ObjC語言中,runtime運行機制主要依賴於兩個頭文件
#include <objc/runtime.h>
#include <objc/message.h>
其中runtime.h聲明瞭一些類,實例變量操作相關的東西,message.h聲明瞭一些消息發送相關的東西。
下面列舉一下,runtime.h中常用的方法,以及它們的用法。
object_getClass: 獲取一個對象所屬的類
object_getIvar: 讀取一個對象中某個變量的值
objc_getClass: 通過字符串獲取到類Class
class_getSuperclass:獲取一個類的父類
class_getInstanceMethod:獲取實例方法,返回Method
class_getMethodImplementation:獲取實例方法的實現
class_getProperty: 獲取某個類的屬性
class_addMethod: 增加方法。
...等等
上面大部分列舉了get相關的方法。runtime.h提供了獲取(get),設置(set)(類的屬性,實例變量,實例方法,類方法)的操作。
下面列舉一下message.h的相關方法。
objc_msgSend: 給對象發送消息。
objc_msgSendSuper: 向父類的發送消息
message.h類,主要提供了這兩個關鍵的方法。
- 下面總結一下Runtime有哪些使用場景,並通過具體的代碼,來說明各種使用場景如何運用Runtime。
總結了如下四種應用場景:
1.動態拼接URL
2.交換兩個方法的實現(用自己的方法,替換系統的方法)
3.給系統已有的方法添加新的功能(不影響系統方法原來的功能)
4.消息轉發中Runtime的應用
下面結合項目中使用到Runtime的地方進行說明,並列舉了一些代碼示例。
第一個使用場景:動態拼接URL
在做app開發的時候,肯定會遇到http請求,URL拼接的問題。初期大部分採取的方案是,採用NSString的格式化方法,自己手動去拼接URL. 例如:
NSMutableString *stringUrl = [NSMutableString stringWithFormat:@"http:mytest/host/code=%@",code];
[stringUrl appendString:@"&uin=45"];
[stringUrl appendString:@"&my=5"];
[stringUrl appendString:@"&your=6"];
這樣寫有幾個問題:
1. 手動書寫,很容易寫錯。
2. 如果很多類似的請求,都需要uin或者其他通用的參數,那麼就會每個請求都需要寫一遍。不滿足OOP的特性。
3. 參數所代表的含義不夠清晰,需要開發去猜測。
解決方法:
把url拼接,抽象成一個類。
上面的參數code, uin,my,your可以當成類的屬性。通用的屬性有uin,可以抽象出一個基類。實現方法如下:
CURLParamBase類
@interface CURLParamBase : NSObject
@property (nonatomic, strong) NSString *uin;
@end
需要構造的請求類XXX,如下定義:
@interface CURLParamXXX : CURLParamBase
@property (nonatomic, strong) NSString* my;
@property (nonatomic, strong) NSString* your;
@end
使用:定義CURLParamXXX的類的實例,使用Runtime來獲取屬性名和屬性的value來拼接參數,如下:
unsigned int propertyCount;
//獲取所有屬性
objc_property_t *properties =
class_copyPropertyList(class, &propertyCount);
for (unsigned int i = 0; i < propertyCount; i++)
{
objc_property_t property = properties[i];
const char *propertyName = property_getName(property);
//獲取實例變量,某個屬性的值
object_getInstanceVariable(self,propertyName,&value);
}
-第二個使用場景:交換兩個方法的實現
使用newSEL方法名,新的newIMP實現,替換原有的origSEL方法及其實現。
實現代碼如下,代碼中添加了相關的註釋。
/**
**新方法的實現替換舊方法
**成功:返回YES,
**失敗:返回NO.
*/
BOOL replaceMethodNewImpl(Class c, SEL origSEL, SEL newSEL, IMP newIMP)
{
//新方法已經存在
if ([c instancesRespondToSelector:newSEL])
{
return YES;
}
//舊方法
Method origMethod = class_getInstanceMethod(c, origSEL);
//先把新方法加入到class的方法列表中
if (!class_addMethod(c, newSEL, newIMP, method_getTypeEncoding(origMethod)))
{
//如果加入失敗,直接返回
NSLog(@"Failed to add method:%@ on %@",NSStringFromSelector(newSEL),c);
return NO;
}
else
{
//如果加入成功
Method newMethod = class_getInstanceMethod(c, newSEL);
//給original添加新的實現
//有可能失敗失敗原因:(for example, the class already contains a method implementation with that name).
if (class_addMethod(c, origSEL, method_getImplementation(newMethod), method_getTypeEncoding(origMethod)))
{
//新方法的實現替換成舊方法的實現。
class_replaceMethod(c, newSEL, method_getImplementation(origMethod), method_getTypeEncoding(newMethod));
}
else
{
//交換實現
method_exchangeImplementations(origMethod, newMethod);
}
}
return YES;
}
- 第三個使用場景:給系統已有的方法添加功能
例如給系統的UIView下的方法drawRect,添加NSLog的功能,這個可以參考上面說明的第三種使用場景。變化的只是,新方法的實現要去調用老的方法的實現。在override_drawRect方法中,調用drawRect方法。代碼如下:
- (void)override_drawRect:(CGRect)r
{
// 調用舊的實現。因爲它們已經被替換了
[self override_drawRect: r];
NSLog(@"rect = %@",NSStringFromCGRect(r));
}
- 第四使用場景:消息轉發
開發過程中,經常遇到unrecognized selector sent to instance 0x87 Terminating app due to uncaught exception
NSInvalidArgumentException’, 這個問題。
這是什麼原因?直觀上看,是系統沒有處理某個消息。
情況分兩種,第一,接受消息的對象錯了。第二,對象沒錯,發送的消息不對。
消息轉發的整個流程如下圖所示:
總體來說,就是給某個實例,發送某個消息。
首先如果沒找到響應方法,系統會給你轉到其他方法的機會,只要實現了resolveInstanceMethod方法即可。
其次,如果沒有實現,你還可以修改接受消息的對象,讓其他對象去響應消息,覆蓋方法forwardingTargetSelector即可。
如果這兩者都沒做,你還可以在forwardInvocation,做自己的邏輯處理,是否繼續處理消息。
這篇文章不具體討論這個轉發流程的細節,只是爲了說明runtime在整個過程中的運用。
Runtime在其中的運用。
在resolveInstanceMethod方法中,可以通過覆蓋resolveInstanceMethod方法,如下:
+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSString *selectorString = NSStringFromSelector(sel);
if ([selectorString hasPrefix:@"set"]) {
class_addMethod(self, sel, (IMP)mySetMethod, "v@:@");
} else {
class_addMethod(self, sel, (IMP)myGetMethod, "@@:");
}
return YES;
}
代碼中,本類實現了動態的添加選擇子,把選擇子關聯到自己定義的方法上,這樣在訪問某個屬性的時候,會動態的訪問我們自己定義的方法。在消息轉發的流程中,實現了動態添加方法實現的能力。
綜上所述,Runtime提供了Objective-C強大的動態性,可謂是方便靈活,運用起來能做很多事情,也歡迎大家補充說明在你們的項目中,Runtime都爲你們做了什麼。
(完)