iOS之runtime詳解api(二)

hoping_sir發表於2019-03-03

上一篇我們講解了runtime裡面關於類和分類的函式,那麼,我們這一篇就講解下關於Method的那些函式。

3.objc_method or Method

objc_method或者Method(這兩個其實是同一個)這個結構體在runtime.h檔案裡並沒有詳細的告訴我們其中的成員變數,這個在這個階段也不是很重要,後面我將會在分析runtime原始碼的時候,再去分析這個結構體,現在我們只需知道這個是關於函式的結構體。 除了objc_method這個結構體還有一個objc_method_description,結構如下:

struct objc_method_description {
    SEL _Nullable name;               /**< The name of the method */
    char * _Nullable types;           /**< The types of the method arguments */
};
複製程式碼

name在這裡指的是函式名稱,types指的是函式的引數返回值的型別。 我們就要通過這個method_getDescription這個方法,可以獲得objc_method_description的結構體:

struct objc_method_description * _Nonnull
method_getDescription(Method _Nonnull m)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
複製程式碼

通過傳入Method返回objc_method_description: 除了這個objc_method_description結構體,我們還要了解2個概念,SELIMP: SEL:函式的選擇器,一般用函式名進行繫結。 IMP:函式的地址,用過這個可以執行函式。 關於SEL我們應該很熟悉,在OC中,有個方法是SELNSString互相轉換,方法是SEL NSSelectorFromString(NSString *aSelectorName)NSString *NSStringFromSelector(SEL aSelector),在runtime裡面也有:

const char * _Nonnull
sel_getName(SEL _Nonnull sel)
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

SEL _Nonnull
sel_registerName(const char * _Nonnull str)
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
複製程式碼

這兩個方法就可以是上面兩個方法的替代方法。現在準備寫個方法如何列印關於Method的資訊:

-(void)logMethodDescription:(Method)method {
    if (method) {
        struct objc_method_description * description = method_getDescription(method);
        SEL selector = description->name;
        char* types = description->types;
        NSLog(@"selector=%s,type=%s", sel_getName(selector),types);
    } else {
        NSLog(@"Method 為 null");
    }
}
複製程式碼

後面我們去列印Method相關的資訊就可以呼叫logMethodDescription方法。 另外,runtime為了能更好的表示函式的引數和返回值的結構,特別有一張表:

#define _C_ID       '@'
#define _C_CLASS    '#'
#define _C_SEL      ':'
#define _C_CHR      'c'
#define _C_UCHR     'C'
#define _C_SHT      's'
#define _C_USHT     'S'
#define _C_INT      'i'
#define _C_UINT     'I'
#define _C_LNG      'l'
#define _C_ULNG     'L'
#define _C_LNG_LNG  'q'
#define _C_ULNG_LNG 'Q'
#define _C_FLT      'f'
#define _C_DBL      'd'
#define _C_BFLD     'b'
#define _C_BOOL     'B'
#define _C_VOID     'v'
#define _C_UNDEF    '?'
#define _C_PTR      '^'
#define _C_CHARPTR  '*'
#define _C_ATOM     '%'
#define _C_ARY_B    '['
#define _C_ARY_E    ']'
#define _C_UNION_B  '('
#define _C_UNION_E  ')'
#define _C_STRUCT_B '{'
#define _C_STRUCT_E '}'
#define _C_VECTOR   '!'
#define _C_CONST    'r'
複製程式碼

這些代表什麼等我們後面用到再說。 我們知道方法分為例項方法和類方法,那麼怎麼獲得他們呢,就用這兩個函式:

Method _Nullable
class_getInstanceMethod(Class _Nullable cls, SEL _Nonnull name)
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

Method _Nullable
class_getClassMethod(Class _Nullable cls, SEL _Nonnull name)
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
複製程式碼

這兩個函式分別代表,獲取某個類的某個方法。

在專案裡面,新增一個類Car,定義兩個例項方法-(void)function1-(void)function2,兩個類方法+(void)function1_class+(void)function2_class。但是隻實現-(void)function1+(void)function1_class。我們看下是否都可以找到。

@interface Car : NSObject
-(void)function1;
-(void)function2;
+(void)function1_class;
+(void)function2_class;
@end

@implementation Car

-(void)function1 {
    NSLog(@"----function1----");
}
+(void)function1_class {
    NSLog(@"----function1_class----");
}
@end
複製程式碼

然後在ViewController裡面去獲取這四個方法。

- (void)getMethod {
    //獲取function1
    SEL selector = sel_registerName("function1");
    Method method = class_getInstanceMethod(objc_getClass("Car"), selector);
    [self logMethodDescription:method];
    
    //獲取function1
    SEL selector1 = sel_registerName("function2");
    Method method1 = class_getInstanceMethod(objc_getClass("Car"), selector1);
    [self logMethodDescription:method1];
    
    //獲取function1_class
    SEL selector2 = sel_registerName("function1_class");
    Method method2 = class_getInstanceMethod(objc_getMetaClass("Car"), selector2);
    [self logMethodDescription:method2];
    
    //獲取function2_class
    SEL selector3 = sel_registerName("function2_class");
    Method method3 = class_getInstanceMethod(objc_getMetaClass("Car"), selector3);
    [self logMethodDescription:method3];
}
複製程式碼

列印結果(如果只宣告方法,但是不實現Method就會為null):

2019-02-25 11:40:07.336465+0800 Runtime-Demo[41836:4488074] selector=function1,type=v16@0:8
2019-02-25 11:40:07.336513+0800 Runtime-Demo[41836:4488074] Method 為 null
2019-02-25 11:40:07.336523+0800 Runtime-Demo[41836:4488074] selector=function1_class,type=v16@0:8
2019-02-25 11:40:07.336539+0800 Runtime-Demo[41836:4488074] Method 為 null
複製程式碼

首先,我們看到了沒有實現的方法是找不到的,只有實現的方法才能找到,另外,我們發現找類方法時候傳入的cls是通過objc_getMetaClass這個方法獲得的,為什麼?我們在上一篇中說過了,類方法是存在元類物件裡面,而例項方法是存在類物件裡面。然後我們再看看列印出來的type是一段奇怪的符號v16@0:8,是不是看起來很奇怪,在這裡我就直接告訴你答案:

  • 這個方法返回值是void(可以從上面的表格裡面去找)
  • 這個方法第一個引數是id型別(所有方法都預設有2個引數,第一個是target,第二個selector
  • 第二個引數是SEL型別
  • 這個方法所有引數的長度是16個位元組
  • 第0個位元組開始是第一個引數
  • 第8個位元組開始是第二個引數 如果不信?你可以去嘗試更多的方法,給方法加引數等等。

那我們繼續,既然可以獲得方法的結構體,那麼也可以獲得方法的地址,這可以讓我們對方法進行呼叫:

IMP _Nullable
class_getMethodImplementation(Class _Nullable cls, SEL _Nonnull name) 
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

IMP _Nonnull
method_getImplementation(Method _Nonnull m) 
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

複製程式碼

這兩個方法都可以獲得IMP,只是傳參不同,第一種更直接一點。 我們依舊以-(void)function1+(void)function1_class為測試物件,看看能否進行呼叫

-(void)getMethodImplementation {
    IMP imp1 = class_getMethodImplementation(objc_getClass("Car"), sel_registerName("function1"));
    imp1();
    
    IMP imp2 = class_getMethodImplementation(objc_getMetaClass("Car"), sel_registerName("function1_class"));
    imp2();
    
    Method method1 = class_getInstanceMethod(objc_getClass("Car"), sel_registerName("function1"));
    IMP imp3 = method_getImplementation(method1);
    imp3();
    
    Method method2 = class_getInstanceMethod(objc_getMetaClass("Car"), sel_registerName("function1_class"));
    IMP imp4 = method_getImplementation(method2);
    imp4();
    
}
複製程式碼

列印結果:

2019-02-25 13:42:16.757607+0800 Runtime-Demo[43515:4532002] ----function1----
2019-02-25 13:42:16.757647+0800 Runtime-Demo[43515:4532002] ----function1_class----
2019-02-25 13:42:16.757659+0800 Runtime-Demo[43515:4532002] ----function1----
2019-02-25 13:42:16.757667+0800 Runtime-Demo[43515:4532002] ----function1_class----
複製程式碼

我們可以看到列印的內容確實是實現的內容。我們不僅可以找到已經存在的IMP,而且還可以為一個方法設定新的IMP

IMP _Nonnull
method_setImplementation(Method _Nonnull m, IMP _Nonnull imp)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
複製程式碼

傳入的是你要設定的Method和新的IMP,返回的是舊的IMP; 我們設定一個功能,讓+(void)function1_class實現-(void)function1方法裡的實現:

-(void)setImplementation {
    Method method = class_getClassMethod(objc_getMetaClass("Car"), sel_registerName("function1_class"));
    IMP imp = class_getMethodImplementation(objc_getClass("Car"), sel_registerName("function1"));
    
    IMP oldIMP = method_setImplementation(method, imp);
    
    oldIMP();
    
    IMP newIMP = method_getImplementation(method);
    
    newIMP();
}
複製程式碼

執行結果:

2019-02-25 13:55:52.261447+0800 Runtime-Demo[43745:4536866] ----function1_class----
2019-02-25 13:55:52.261486+0800 Runtime-Demo[43745:4536866] ----function1----
複製程式碼

我們可以看到舊的IMP是原來的IMP,列印的也是原來的實現,newIMP是設定的新的IMP,列印的是-(void)function1,所以沒問題。

Method _Nonnull * _Nullable
class_copyMethodList(Class _Nullable cls, unsigned int * _Nullable outCount)
複製程式碼

這個函式是獲取某個類物件的例項物件列表或者元類物件的類方法列表,outCount是一個列表數量的指標,我們可以通過outCount知道列表的數量。 我們在Car類裡面再新增兩個方法-(void)function3+(void)function3_class,並且實現,實現內容就是列印他們的方法名。 然後,在ViewController去使用class_copyMethodList方法

-(void)copyMethodList {
    unsigned int count1 ;
    Method* instanceMethods = class_copyMethodList(objc_getClass("Car"), &count1);
    
    unsigned int count2 ;
    Method* classMethods = class_copyMethodList(objc_getMetaClass("Car"), &count2);
    NSLog(@"--------------------------");
    for (unsigned int i = 0; i < count1; i++) {
        Method method = instanceMethods[i];
        [self logMethodDescription:method];
    }
    NSLog(@"--------------------------");

    for (unsigned int i = 0; i < count1; i++) {
        Method method = classMethods[i];
        [self logMethodDescription:method];
    }

    free(instanceMethods);
    free(classMethods);
}
複製程式碼

列印結果:

2019-02-25 13:31:14.263003+0800 Runtime-Demo[43333:4527940] --------------------------
2019-02-25 13:31:14.263045+0800 Runtime-Demo[43333:4527940] selector=function1,type=v16@0:8
2019-02-25 13:31:14.263057+0800 Runtime-Demo[43333:4527940] selector=function3,type=v16@0:8
2019-02-25 13:31:14.263065+0800 Runtime-Demo[43333:4527940] --------------------------
2019-02-25 13:31:14.263073+0800 Runtime-Demo[43333:4527940] selector=function1_class,type=v16@0:8
2019-02-25 13:31:14.263082+0800 Runtime-Demo[43333:4527940] selector=function3_class,type=v16@0:8
複製程式碼

我們看到了,仍然是隻有實現的方法才能找出來 SEL除了通過Classname獲得以外,還可以通過Method來獲取:

SEL _Nonnull
method_getName(Method _Nonnull m) 
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
複製程式碼

例子如下:

-(void)getSEL {
    Method method = class_getClassMethod(objc_getMetaClass("Car"), sel_registerName("function1_class"));
    SEL sel = method_getName(method);
    NSLog(@"sel = %s",sel_getName(sel));
}
複製程式碼

執行結果:

sel = function1_class
複製程式碼

可以通過method來獲得SEL。關於SEL還有兩個方法:

BOOL
sel_isEqual(SEL _Nonnull lhs, SEL _Nonnull rhs)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

BOOL
class_respondsToSelector(Class _Nullable cls, SEL _Nonnull sel)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
複製程式碼

sel_isEqual是用來兩個SEL是否相等,class_respondsToSelector表示某個類是否響應特定的選擇器。 下面我們來看一組api,這些都是關於引數或者返回值的函式:

//獲得方法的格式
const char * _Nullable
method_getTypeEncoding(Method _Nonnull m) 
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

//獲得方法引數的個數
unsigned int
method_getNumberOfArguments(Method _Nonnull m)
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

//獲得返回型別
char * _Nonnull
method_copyReturnType(Method _Nonnull m)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

//獲得index處的引數型別
char * _Nullable
method_copyArgumentType(Method _Nonnull m, unsigned int index)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

//獲得返回型別
void
method_getReturnType(Method _Nonnull m, char * _Nonnull dst, size_t dst_len)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

//獲得index處的引數型別
void
method_getArgumentType(Method _Nonnull m, unsigned int index,
                       char * _Nullable dst, size_t dst_len)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
複製程式碼

我們寫個方法測試下這些函式:

-(void)getType {
    Method method = class_getInstanceMethod(objc_getClass("Car"), sel_registerName("function1"));
    const char* type = method_getTypeEncoding(method);
    NSLog(@"type = %s",type);
    
    int count  = method_getNumberOfArguments(method);
    NSLog(@"count = %d",count);

    char* returnChar = method_copyReturnType(method);
    NSLog(@"returnChar = %s",returnChar);
    
    for (int i = 0; i < count; i++) {
        char* argumentType = method_copyArgumentType(method,i);
        NSLog(@"第%d個引數型別為%s",i,argumentType);
    }
    
    char dst[2] = {};
    method_getReturnType(method,dst,2);
    NSLog(@"returnType = %s",dst);
    
    
    for (int i = 0; i < count; i++) {
        char dst2[2] = {};
        method_getArgumentType(method,i,dst2,2);
        NSLog(@"第%d個引數型別為%s",i,dst2);
    }
    
}
複製程式碼

執行結果:

2019-02-25 14:37:53.641383+0800 Runtime-Demo[44457:4553505] type = v16@0:8
2019-02-25 14:37:53.641424+0800 Runtime-Demo[44457:4553505] count = 2
2019-02-25 14:37:53.641439+0800 Runtime-Demo[44457:4553505] returnChar = v
2019-02-25 14:37:53.641459+0800 Runtime-Demo[44457:4553505] 第0個引數型別為@
2019-02-25 14:37:53.641471+0800 Runtime-Demo[44457:4553505] 第1個引數型別為:
2019-02-25 14:37:53.641495+0800 Runtime-Demo[44457:4553505] returnType = v
2019-02-25 14:37:53.641508+0800 Runtime-Demo[44457:4553505] 第0個引數型別為@
2019-02-25 14:37:53.641519+0800 Runtime-Demo[44457:4553505] 第1個引數型別為:
複製程式碼

返回的這些值也可以驗證我們之前說的type的含義。

BOOL
class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, 
                const char * _Nullable types) 
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
複製程式碼

這個方法是給一個類增加方法。 我們為Car類增加一個-(void)test方法,傳入的imp則是-(void)function1imptypes就是無參無返回值。

-(void)addMethod {
    IMP imp = class_getMethodImplementation(objc_getClass("Car"), sel_registerName("function1"));
    BOOL addSuccess = class_addMethod(objc_getClass("Car"), sel_registerName("test"), imp, "v@:");
    NSLog(@"增加不存在的方法 = %d",addSuccess);
    
    BOOL addSuccess2 = class_addMethod(objc_getClass("Car"), sel_registerName("function1"), imp, "v@:");
    NSLog(@"增加已存在的方法 = %d",addSuccess2);
}
複製程式碼

執行結果:

2019-02-25 14:55:29.265128+0800 Runtime-Demo[44747:4559426] 增加不存在的方法 = 1
2019-02-25 14:55:29.265327+0800 Runtime-Demo[44747:4559426] 增加已存在的方法 = 0
複製程式碼

這個方法是給一個類增加方法,增加成功,返回YES,增加失敗(例如,已經存在),返回NO

IMP _Nullable
class_replaceMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, 
                    const char * _Nullable types) 
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
複製程式碼

這個方法有2種情況: (一)如果SEL存在,則相當於呼叫method_setImplementation方法 (二)如果SEL不存在,則相當於呼叫class_addMethod方法

void
method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
複製程式碼

這個函式堪稱黑魔法,一般在實際用途中,是和系統方法進行交換。 我們將function1function3交換一波:

-(void)exchangeImplementations {
    Method method1 = class_getInstanceMethod(objc_getClass("Car"), sel_registerName("function1"));
    Method method2 = class_getInstanceMethod(objc_getClass("Car"), sel_registerName("function3"));
    //交換方法
    method_exchangeImplementations(method1, method2);
    //此時的function1的實現應該是`function3`的實現
    IMP imp1 = method_getImplementation(method1);
    //此時的function3的實現應該是`function1`的實現
    IMP imp2 = method_getImplementation(method2);

    imp1();
    imp2();
}
複製程式碼

執行結果:

2019-02-25 15:16:19.507945+0800 Runtime-Demo[45112:4568994] ----function3----
2019-02-25 15:16:19.507982+0800 Runtime-Demo[45112:4568994] ----function1----
複製程式碼

確實交換過來了。第二篇就這麼結束了,第三篇我將講解關於屬性和變數的函式。 對了,這個是demo,喜歡的可以點個星。

相關文章