運行時(runtime)技術的幾個要點總結
Objective C的runtime技術功能非常強大,能夠在運行時獲取並修改類的各種信息,包括獲取方法列表、屬性列表、變量列表,修改方法、屬性,增加方法,屬性等等,本文對相關的幾個要點做了一個小結。
目錄:
(1)使用class_replaceMethod/class_addMethod函數在運行時對函數進行動態替換或增加新函數
(2)重載forwardingTargetForSelector,將無法處理的selector轉發給其他對象
(3)重載resolveInstanceMethod,從而在無法處理某個selector時,動態添加一個selector
(4)使用class_copyPropertyList及property_getName獲取類的屬性列表及每個屬性的名稱
(5) 使用class_copyMethodList獲取類的所有方法列表
(1)在運行時對函數進行動態替換 : class_replaceMethod
使用該函數可以在運行時動態替換某個類的函數實現,這樣做有什麼用呢?最起碼,可以實現類似windows上hook效果,即截獲系統類的某個實例函數,然後塞一些自己的東西進去,比如打個log什麼的。
示例代碼:
IMP orginIMP; NSString * MyUppercaseString(id SELF, SEL _cmd) { NSLog(@"begin uppercaseString"); NSString *str = orginIMP (SELF, _cmd);(3) NSLog(@"end uppercaseString"); return str; } -(void)testReplaceMethod { Class strcls = [NSString class]; SEL oriUppercaseString = @selector(uppercaseString); orginIMP = [NSStringinstanceMethodForSelector:oriUppercaseString]; (1) IMP imp2 = class_replaceMethod(strcls,oriUppercaseString,(IMP)MyUppercaseString,NULL);(2) NSString *s = "hello world"; NSLog(@"%@",[s uppercaseString]]; }
執行結果爲:
begin uppercaseString
end uppercaseString
HELLO WORLD
這段代碼的作用就是
(1)得到uppercaseString這個函數的函數指針存到變量orginIMP中
(2)將NSString類中的uppercaseString函數的實現替換爲自己定義的MyUppercaseString
(3)在MyUppercaseString中,先執行了自己的log代碼,然後再調用之前保存的uppercaseString的系統實現,這樣就在系統函數執行之前加入自己的東西,後面每次對NSString調用uppercaseString的時候,都會打印出log來
與class_replaceMethod相仿,class_addMethod可以在運行時爲類增加一個函數。
(2)當某個對象不能接受某個selector時,將對該selector的調用轉發給另一個對象:- (id)forwardingTargetForSelector:(SEL)aSelector
forwardingTargetForSelector是NSObject的函數,用戶可以在派生類中對其重載,從而將無法處理的selector轉發給另一個對象。還是以上面的uppercaseString爲例,如果用戶自己定義的CA類的對象a,沒有uppercaseString這樣一個實例函數,那麼在不調用respondSelector的情況下,直接執行[a performSelector:@selector"uppercaseString"],那麼執行時一定會crash,此時,如果CA實現了forwardingTargetForSelector函數,並返回一個NSString對象,那麼就相對於對該NSString對象執行了uppercaseString函數,此時就不會crash了。當然實現這個函數的目的並不僅僅是爲了程序不crash那麼簡單,在實現裝飾者模式時,也可以使用該函數進行消息轉發。
示例代碼:
1 @interface CA : NSObject 3 -(void)f; 4 5 @end 6 7 @implementation CA 8 9 - (id)forwardingTargetForSelector:(SEL)aSelector 11 { 13 if (aSelector == @selector(uppercaseString)) 15 { 17 return@"hello world"; 19 } 21 }
測試代碼:
CA *a = [CA new]; NSString * s = [a performSelector:@selector(uppercaseString)]; NSLog(@"%@",s);
測試代碼的輸出爲:HELLO WORLD
ps:這裏有個問題,CA類的對象不能接收@selector(uppercaseString),那麼如果我在forwardingTargetForSelector函數中用class_addMethod給CA類增加一個uppercaseString函數,然後返回self,可行嗎?經過試驗,這樣會crash,此時CA類其實已經有了uppercaseString函數,但是不知道爲什麼不能調用,如果此時new一個CA類的對象,並返回,是可以成功的。
(3)當某個對象不能接受某個selector時,向對象所屬的類動態添加所需的selector:
+ (BOOL) resolveInstanceMethod:(SEL)aSEL
這個函數與forwardingTargetForSelector類似,都會在對象不能接受某個selector時觸發,執行起來略有差別。前者的目的主要在於給客戶一個機會來向該對象添加所需的selector,後者的目的在於允許用戶將selector轉發給另一個對象。另外觸發時機也不完全一樣,該函數是個類函數,在程序剛啓動,界面尚未顯示出時,就會被調用。
在類不能處理某個selector的情況下,如果類重載了該函數,並使用class_addMethod添加了相應的selector,並返回YES,那麼後面forwardingTargetForSelector就不會被調用,如果在該函數中沒有添加相應的selector,那麼不管返回什麼,後面都會繼續調用forwardingTargetForSelector,如果在forwardingTargetForSelector並未返回能接受該selector的對象,那麼resolveInstanceMethod會再次被觸發,這一次,如果仍然不添加selector,程序就會報異常
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
|
代碼示例一: 1
@implementation CA 3
void dynamicMethodIMP( id self ,
SEL _cmd) 5
{ 7
printf( "SEL
%s did not exist\n" ,sel_getName(_cmd)); 9
} 10
11
+ ( BOOL )
resolveInstanceMethod:( SEL )aSEL 13
{ 15
if (aSEL
== @selector (t)) 17
{ 19
class_addMethod([selfclass], aSEL, (IMP) dynamicMethodIMP, "v@:" ); 21
return YES ; 23
} 25
return [superresolveInstanceMethod:aSEL]; 27
} 28
29
@end 測試代碼: CA
* ca = [CA new ] [ca
performSelector: @selector (t)];
|
執行結果
SEL t did not exist
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
示例代碼二: @implementation CA void dynamicMethodIMP( id self ,
SEL _cmd) { printf( "SEL
%s did not exist\n" ,sel_getName(_cmd)); } +
( BOOL )
resolveInstanceMethod:( SEL )aSEL { return YES ; } -
( id )forwardingTargetForSelector:( SEL )aSelector { if (aSelector
== @selector (uppercaseString)) { return @ "hello
world" ; } } 測試代碼
: a
= [[CA alloc]init]; NSLog (@ "%@" ,[a
performSelector: @selector (uppercaseString)]; |
該測試代碼的輸出爲:HELLO WORLD
對於該測試代碼,由於a沒有uppercaseString函數,因此會觸發resolveInstanceMethod,但是由於該函數並沒有添加selector,因此運行時發現找不到該函數,會觸發
forwardingTargetForSelector函數,在forwardingTargetForSelector函數中,返回了一個NSString "hello world",因此會由該string來執行uppercaseString函數,最終返回大寫的hello world。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
示例代碼三: @implementation CA +
( BOOL )
resolveInstanceMethod:( SEL )aSEL { return YES ; } -
( id )forwardingTargetForSelector:( SEL )aSelector { return nil ; } 測試代碼: 1
a = [[CA alloc]init]; 2 NSLog (@ "%@" ,[a
performSelector: @selector (uppercaseString)]; |
這段代碼的執行順序爲:
(1):首先在程序剛執行,AppDelegate都還沒有出來時,resolveInstanceMethod就被觸發,
(2)等測試代碼執行時,forwardingTargetForSelector被調用
(3)由於forwardingTargetForSelector返回了nil,因此運行時還是找不到uppercaseString selector,這時又會觸發resolveInstanceMethod,由於還是沒有加入selector,於是會crash。
(4) 使用class_copyPropertyList及property_getName獲取類的屬性列表及每個屬性的名稱
u_int
count; objc_property_t*
properties= class_copyPropertyList([UIView class ],
&count); for ( int i
= 0; i < count ; i++) { const char *
propertyName = property_getName(properties[i]); NSString *strName
= [ NSString stringWithCString:propertyName
encoding: NSUTF8StringEncoding ]; NSLog (@ "%@" ,strName); } |
以上代碼獲取了UIView的所有屬性並打印屬性名稱, 輸出結果爲:
skipsSubviewEnumeration viewTraversalMark viewDelegate monitorsSubtree backgroundColorSystemColorName gesturesEnabled deliversTouchesForGesturesToSuperview userInteractionEnabled tag layer _boundsWidthVariable _boundsHeightVariable _minXVariable _minYVariable _internalConstraints _dependentConstraints _constraintsExceptingSubviewAutoresizingConstraints _shouldArchiveUIAppearanceTags |
(5) 使用class_copyMethodList獲取類的所有方法列表
獲取到的數據是一個Method數組,Method數據結構中包含了函數的名稱、參數、返回值等信息,以下代碼以獲取名稱爲例:
u_int
count; Method*
methods= class_copyMethodList([UIView class ],
&count); for ( int i
= 0; i < count ; i++) { SEL name
= method_getName(methods[i]); NSString *strName
= [ NSString stringWithCString:sel_getName(name)
encoding: NSUTF8StringEncoding ]; NSLog (@ "%@" ,strName); } |
代碼執行後將輸出UIView所有函數的名稱,具體結果略。
其他一些相關函數:
1.SEL method_getName(Method m) 由Method得到SEL 2.IMP method_getImplementation(Method m) 由Method得到IMP函數指針 3.const char *method_getTypeEncoding(Method m) 由Method得到類型編碼信息 4.unsigned int method_getNumberOfArguments(Method m)獲取參數個數 5.char *method_copyReturnType(Method m) 得到返回值類型名稱 6.IMP method_setImplementation(Method m, IMP imp) 爲該方法設置一個新的實現
總而言之,使用runtime技術能做些什麼事情呢?
可以在運行時,在不繼承也不category的情況下,爲各種類(包括系統的類)做很多操作,具體包括:
- 增加
增加函數:class_addMethod 增加實例變量:class_addIvar 增加屬性: @dynamic 標籤,或者class_addMethod,因爲屬性其實就是由getter和setter函數組成 增加Protocol:class_addProtocol
(說實話我真不知道動態增加一個protocol有什麼用,-_-!!) |
- 獲取
獲取函數列表及每個函數的信息(函數指針、函數名等等):class_getClassMethod
method_getName ... 獲取屬性列表及每個屬性的信息:class_copyPropertyList
property_getName 獲取類本身的信息,如類名等:class_getName
class_getInstanceSize 獲取變量列表及變量信息:class_copyIvarList 獲取變量的值 |
- 替換
將實例替換成另一個類:object_setClass 將函數替換成一個函數實現:class_replaceMethod 直接通過 char *格式的名稱來修改變量的值,而不是通過變量 |
參考資料:(1)Objective-C Runtime Reference
(2)深入淺出Cocoa之消息
(3)objective-c 編程總結(第六篇)運行時操作 - 方法交換
消息轉發
一.消息轉發流程
當向Objective-C對象發送一個消息,但runtime在當前類及父類中找不到此selector對應的方法時,消息轉發(message forwarding)流程開始啓動。
-
動態方法解析(Dynamic Method Resolution或Lazy method resolution)
向當前類(Class)發送resolveInstanceMethod:(對於類方法則爲resolveClassMethod:)消息,如果返回YES,則系統認爲請求的方法已經加入到了,則會重新發送消息。 -
快速轉發路徑(Fast forwarding path)
若果當前target實現了forwardingTargetForSelector:方法,則調用此方法。如果此方法返回除nil和self的其他對象,則向返回對象重新發送消息。 -
慢速轉發路徑(Normal forwarding path)
首先runtime發送methodSignatureForSelector:消息查看Selector對應的方法簽名,即參數與返回值的類型信息。如果有方法簽名返回,runtime則根據方法簽名創建描述該消息的NSInvocation,向當前對象發送forwardInvocation:消息,以創建的NSInvocation對象作爲參數;若methodSignatureForSelector:無方法簽名返回,則向當前對象發送doesNotRecognizeSelector:消息,程序拋出異常退出。Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[MessageInterceptor test]: unrecognized selector sent to instance 0x9589830'
二.動態解析(Lazy Resolution)
runtime發送消息的流程即查找該消息對應的方法或IMP,然後跳轉至對應的IMP。有時候我們不想事先在類中設置好方法,而想在運行時動態的在類中插入IMP。這種方法是真正的快速”轉發”,因爲一旦對應的方法被添加到類中,後續的方法調用就是正常的消息發送流程。此方法的缺點是不夠靈活,你必須有此方法的實現(IMP),這意味這你必須事先預測此方法的參數和返回值類型。
@dynamic屬性是使用動態解析的一個例子,@dynamic告訴編譯器該屬性對應的getter或setter方法會在運行時提供,所以編譯器不會出現warning; 然後實現resolveInstanceMethod:方法在運行時將屬性相關的方法加入到Class中。
當respondsToSelector:或instancesRespondToSelector:方法被調用時,若該方法在類中未實現,動態方法解析器也會被調用,這時可向類中增加IMP,並返回YES,則對應的respondsToSelector:的方法也返回YES。
三.快速轉發(Fast Forwarding)
runtime然後會檢查你是否想將此消息不做改動的轉發給另外一個對象,這是比較常見的消息轉發情形,可以用較小的消耗完成。
快速轉發技術可以用來實現僞多繼承,你只需編寫如下代碼
- (id)forwardingTargetForSelector:(SEL)sel { return _otherObject; }
這樣做會將任何位置的消息都轉發給_otherObject對象,儘管當前對象與_otherObject對象是包含關係,但從外界看來當前對象和_otherObject像是同一個對象。
僞多繼承與真正的多繼承的區別在於,真正的多繼承是將多個類的功能組合到一個對象中,而消息轉發實現的僞多繼承,對應的功能仍然分佈在多個對象中,但是將多個對象的區別對消息發送者透明。
四.慢速轉發(Normal Forwarding)
以上兩者方式是對消息轉發的優化,如果你不使用上述兩種方式,則會進入完整的消息轉發流程。這會創建一個NSInvocation對象來完全包含發送的消息,其中包括target,selector,所有的參數,返回值。
在runtime構建NSInvocation之前首先需要一個NSMethodSignature,所以它通過-methodSignatureForSelector:方法請求。一旦NSInvocation創建完成,runtime就會調用forwardInvocation:方法,在此方法內你可以使用參數中的invocation做任何事情。無限可能…
舉個例子,如果你想對一個NSArray中的所有對象調用同一個方法,而又不想一直寫循環代碼時,想直接操作NSArray時,可這樣處理:
@implementation NSArray (ForwardingIteration)
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel
{
NSMethodSignature *sig = [super methodSignatureForSelector:sel];
if(!sig)
{
for(id obj in self)
if((sig = [obj methodSignatureForSelector:sel]))
break;
}
return sig;
}
- (void)forwardInvocation:(NSInvocation *)inv
{
for(id obj in self)
[inv invokeWithTarget:obj];
}
@end
然後就可以這樣使用
[(NSWindow *)windowsArray setHidesOnDeactivate:YES];
不過不建議這樣使用,因爲若NSArray實現了此方法,就不會進入轉發流程。實現這種功能的一種比較好的方法是使用NSProxy。
五.方法聲明
雖然上述機制可以轉發當前類中沒有實現的方法,但發送消息時仍然需要知道每個消息的方法簽名,否則就會有編譯器告警。可以通過category來聲明轉發消息的方法。
六.使用消息轉發在子類中處理Delegate消息
當繼承一個具有delgate的類,而又需要在子類中處理某些delegate消息,而又不影響對正常Delegate消息的調用時,需要如何處理呢?
一種方法是將子類對象設爲自身的delegate,而將外部設置的delegate存儲到另一個參數中。在子類中實現所有的delegate方法,處理子類中需要處理的delegate消息,而將子類中不處理的delegate消息再發送到外部delegate。這種方法的缺點在於實現繁瑣,在子類中需要實現所有delegate方法,儘管大部分delegate消息又直接轉給了外部delegate處理。
另一種比較優雅的方式是使用消息轉發,創建一個proxy類,將proxy類設置爲父類的delegate,在proxy中分別將消息轉發給子類或外部Delegate。
比如,創建一個UISCrollView的子類可使用如下代碼
MessageInterceptor.h
@interface MessageInterceptor : NSObject {
id receiver;
id middleMan;
}
@property (nonatomic, assign) id receiver;
@property (nonatomic, assign) id middleMan;
@end
MessageInterceptor.m
@implementation MessageInterceptor
@synthesize receiver;
@synthesize middleMan;
- (id)forwardingTargetForSelector:(SEL)aSelector {
if ([middleMan respondsToSelector:aSelector]) { return middleMan; }
if ([receiver respondsToSelector:aSelector]) { return receiver; }
return [super forwardingTargetForSelector:aSelector];
}
- (BOOL)respondsToSelector:(SEL)aSelector {
if ([middleMan respondsToSelector:aSelector]) { return YES; }
if ([receiver respondsToSelector:aSelector]) { return YES; }
return [super respondsToSelector:aSelector];
}
@end
MyScrollView.h
#import "MessageInterceptor.h"
@interface MyScrollView : UIScrollView {
MessageInterceptor * delegate_interceptor;
//...
}
//...
@end
MyScrollView.m
@implementation MyScrollView
- (id)delegate { return delegate_interceptor.receiver; }
- (void)setDelegate:(id)newDelegate {
[super setDelegate:nil];
[delegate_interceptor setReceiver:newDelegate];
[super setDelegate:(id)delegate_interceptor];
}
- (id)init* {
//...
delegate_interceptor = [[MessageInterceptor alloc] init];
[delegate_interceptor setMiddleMan:self];
[super setDelegate:(id)delegate_interceptor];
//...
}
- (void)dealloc {
//...
[delegate_interceptor release];
//...
}
// delegate method override:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
// 1. your custom code goes here
// 2. forward to the delegate as usual
if ([self.delegate respondsToSelector:@selector(scrollViewDidScroll:)]) {
[self.delegate scrollViewDidScroll:scrollView];
}
}
@end
MessageInterceptor對象會自動將將子類中實現的delegate消息轉發給子類,而將其他所有delegate消息轉發給外部設置的delegate對象。
在MessageInterceptor中除了實現forwardingTargetForSelector:方法外,還實現了respondsToSelector:方法,因爲UIScrollView在發送delegate消息之前會首先使用respondsToSelector:判斷delegate是否實現了該方法,而轉發的消息對respondsToSelector:也應返回YES。
參考:
Friday
Q&A 2009-03-27: Objective-C Message Forwarding
Objective-C
Runtime Programming Guide – Dynamic Method Resolution
Objective-C
Runtime Programming Guide – Message Forwarding
Intercept
obj-c delegate messages within a subclass
Hacking
Block Support Into UIMenuItem
NSObject
Class Reference
NSObject
Protocol Reference
NSInvocation
Class Reference
NSMethodSignature
Class Reference