iOS - OC 使用運行時來獲取並修改類


前言:

         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那么簡單,在實現裝飾者模式時,也 可以使用該函數進行消息轉發。

示例代碼:

 @interface CA : NSObject

 -(void)f;
@end @implementation CA - (id)forwardingTargetForSelector:(SEL)aSelector { if (aSelector == @selector(uppercaseString)) { return@"hello world"; } }

 

測試代碼:

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,程序就會報異常

 

代碼示例一: 

 @implementation CA

 void dynamicMethodIMP(id self, SEL _cmd)

 {   
      printf("SEL %s did not exist\n",sel_getName(_cmd));
 }

 + (BOOL) resolveInstanceMethod:(SEL)aSEL
 {

     if (aSEL == @selector(t))

     {

         class_addMethod([selfclass], aSEL, (IMP) dynamicMethodIMP, "v@:");

         return YES;

     }
     return [superresolveInstanceMethod:aSEL];
 }
 @end

 

測試代碼:

  CA * ca = [CA new]
  [ca performSelector:@selector(t)]; 

執行結果

  SEL t did not exist

示例代碼二:

@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。

 

示例代碼三:

@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的情況下,為各種類(包括系統的類)做很多操作,具體包括:

 

1.   增加

增加函數:class_addMethod

增加實例變量:class_addIvar

增加屬性:@dynamic標簽,或者class_addMethod,因為屬性其實就是由getter和setter函數組成

增加Protocol:class_addProtocol (說實話我真不知道動態增加一個protocol有什么用,-_-!!)

 

2。  獲取  

 

獲取函數列表及每個函數的信息(函數指針、函數名等等):class_getClassMethod method_getName ...

獲取屬性列表及每個屬性的信息:class_copyPropertyList property_getName

獲取類本身的信息,如類名等:class_getName class_getInstanceSize

獲取變量列表及變量信息:class_copyIvarList

獲取變量的值

 

 

3.    替換

 

將實例替換成另一個類:object_setClass

將函數替換成一個函數實現:class_replaceMethod

直接通過char *格式的名稱來修改變量的值,而不是通過變量

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM