寫在前面給大家推薦一個不錯的網站 www.joblai.com
前言:
Objective C的runtime技術功能非常強大,能夠在運行時獲取並修改類的各種信息,包括獲取方法列表、屬性列表、變量列表,修改方法、屬性,增加方法,屬性等等,本文對相關的幾個要點做了一個小結。
目錄:
(1)使用class_replaceMethod/class_addMethod函數在運行時對函數進行動態替換或增加新函數
(2)重載forwardingTargetForSelector,將無法處理的selector轉發給其他對象
(3)重載resolveInstanceMethod,從而在無法處理某個selector時,動態添加一個selector
(4)使用class_copyPropertyList及property_getName獲取類的屬性列表及每個屬性的名稱
(5) 使用class_copyMethodList獲取類的所有方法列表
(6) 總結
(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) 為該方法設置一個新的實現
(6)總結
總而言之,使用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
*格式的名稱來修改變量的值,而不是通過變量