iOS 問題整理04----Runtime

根本停不下來發表於2018-07-31

本篇文章主要解決以下問題

  1. 說說你對 runtime 的理解。
  2. 你瞭解 isa 指標嗎?
  3. 類的結構是怎樣的?
    • class_rw_t 與 class_ro_t 的區別?
    • runtime 中,SEL 和 IMP 的區別?
    • runtime 如何通過 selector 找到對應 IMP 的地址?
  4. objc 的訊息傳送機制是怎樣的?
    • objc 中向一個 nil 物件傳送訊息,會發生什麼?
    • 訊息轉發到 forwordinginvacation 方法,如何拿到返回值?
    • 什麼時候會報 unrecognized selector 異常?
  5. runtime 中常用的方法。
    • runtime怎麼新增屬性、方法等
    • 使用runtime Associate方法關聯的物件,需要在主物件dealloc的時候釋放麼?
    • 什麼是method swizzling?
    • 能否向編譯後得到的類中增加例項變數?能否向執行時建立的類中新增例項變數?為什麼?
  6. 你在專案中用過runtime嗎?舉個例子。

1. 說說你對 runtime 的理解。

答:

Runtime 提供了一套 C/C++ 的 API,使 OC 程式可以在執行期改變其結構,因此 OC 是一門動態性比較強的語言

在 iOS 系統中 KVO、Category、weak 等都是基於 runtime 實現的。

自己也經常利用 runtime 的特性來實現一些東西。(詳見本篇第 6 問)

****** 低調的分割線 ******

我覺得上面第一部分的答案還是不夠口語化,也不太好,顯得空洞,感覺面試答起來比較像是背的,聽起來比較尷尬,我重新反思了一下,這是新的第一部分的答案:

像其它的語言,比如說 C 語言,在編譯的時候就已經知道了程式應該執行的程式碼。但是在 OC 中,會盡可能地把決策去推遲到執行的時候再去做。比如到執行的時候再去確定物件的型別、確定方法的接收者,給物件新增方法等等。這些都是可以通過 runtime 的 api 來實現的,用書面一點的話來說就是:runtime 的 API 使 OC 程式可以在它執行到時候改變結構,所以 OC 是一門動態性比較強的語言。

對於 runtime 來說,最重要的就是訊息傳遞機制,要理解清楚訊息傳遞機制就需要弄明白 isa 的作用,再弄清楚類的的結構,再去理解訊息傳遞機制的流程,才能理解的透徹。(扯到這一步就好說了,這些問題下邊都有講到)

分析:

這種開放的問題難以把握如何回答好,筆者認為首先要答到 objc 是一門動態性比較強的語言(為什麼說比較強,因為還有更強的),然後再說說系統的一些基於 runtime 實現的東西,再說說自己專案中基於 runtime 實現的一些東西。

2. 你瞭解 isa 指標嗎?

答:

在 64bit 之前 isa 是一個普通的指標,指向 Class/Meta-Class。
在 64bit 之後,蘋果對其進行了優化,把它做成了一個共用體,運用了位域的技術儲存了更多的資訊。(isa 還是隻佔用 8 個位元組,用 8 個位元組儲存了更多的資訊,但是要找到相應了 class 或者 meta-class 需要與 ISA_MASK 進行一次位運算,要更詳細的瞭解可以點選這裡。)

我們可以簡單地認為,instance 的 isa 指向 class,class 的 isa 指向 meta-class,meta-class 的 isa 指向基類的 meta-class。

在使用 objc_msgSend 的時候,是通過 receiverisa 找到它的類或者元類,然後去找方法的。

分析:

回顧一下之前的內容,OC 中的物件分為 instance、class與meta-class,它們的 isa 與 superclass 的指向關係如圖所示。

iOS 問題整理04----Runtime
這裡需要注意的是途中右上角有一根看起來不太和諧的箭頭,即 Root class(meta) 的 superclass 指向 Root class(class)。

這個問題主要也是談三點:

  • isa 是什麼
  • 怎麼指向的
  • 在訊息傳送中的作用

3. 類的結構是怎樣的?

答:

iOS 問題整理04----Runtime

分析:

前面的文章在講 OC 物件的儲存結構時說到 class 物件中儲存的有 isa、superclass 指標、方法列表、屬性列表、協議列表。meta-classclass 都是 Class 型別的,所以他們的結構其實是一樣的,只不過儲存的東西有區別,meta-class 的方法列表中儲存的是類方法,class 的方法列表中儲存的是物件方法。

具體的程式碼結構如上圖所示。(將就看一下吧,作圖工具:網頁版美圖秀秀....)

這是在原始碼中找到的結構,筆者對其做了精簡,保留了現在我們需要關注的資訊,原始碼可以點選這裡下載。

現在分析一下這些"美圖"。過程比較長,但這是這年頭出去面試毫無疑問必須肯定百分之百要掌握的,建議準備面試的同學每天默寫一遍筆者下邊敲出來的程式碼。

struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;
    class_data_bits_t bits;
}
複製程式碼

在原始碼中可以找到這樣一句程式碼 typedef struct objc_class *Class;,所以 Class 其實本質上就是 objc_class結構體。

我們都知道 Class 中有 isa 和 superclass,但是這裡成員變數 ISA 前面有兩個斜線,它被註釋掉了,很奇怪,這是因為 objc_class 是繼承於 objc_object 的,來看一下 objc_object 的結構就明白了。

iOS 問題整理04----Runtime
objc_object 中有 isa 這個共用體,所以objc_class中也有它。

superclass 指向父類。(回憶:基類的元類物件有什麼特殊的地方?)

cache 是方法快取,用於儲存先前已經呼叫過的方法的資訊。

使用 objc_msgSend 向接受者傳送訊息時,會先根據 isa 找到 class/meta-class,然後去 class/meta-class 的成員 cache 中找方法,如果沒有找到,才會去方法列表中找。這樣可以提升效率。

bits 是一串很長的數字,它儲存了多項資訊,把它和 FAST_DATA_MASK 進行與運算,可以得到一個新的數字,這個數字是一個地址,指向 class_rw_t 結構體。

objc_class 的成員變數已經簡單地介紹完了,現在來看一看更具體的。

先來看一看 cache_tcache 的型別是 cache_t

struct cache_t {
    struct bucket_t *_buckets;
    mask_t _mask;
    mask_t _occupied;
}
複製程式碼

_buckets 是一個陣列,用於存放方法資訊,陣列中元素的型別是 struct bucket_t

_mask 翻譯過來就是掩碼,看見 mask 就要意識到它是用來做位運算的,比如 ISA_MASKFAST_DATA_MASK。先記住一個結論,_mask 的長度是 _buckets 的長度減一

_oppcupied 表示的是 _buckets 中儲存的方法的數量。

bucket_t 的結構是這樣的:

struct bucket_t {
    SEL _key; //筆者這裡與原始碼不同,因為這裡實際上就是以 @selector(xxx) 作為 key 的,這樣寫方便記憶一些
    IMP _imp; //函式的記憶體地址
}
複製程式碼

方法快取的機制並不是簡單的呼叫一個就往 _buckets 中新增一個,否則這裡也就不需要有 _mask_occupied 這兩個成員了。它是一個雜湊表

當建立一個類之後,系統自動會給 _buckets 分配一段記憶體空間,它裡面有 N 個元素,都用 NULL 填充,_mask 的值是 N-1

當呼叫 objc_msgSend(obj, foo) 的時候

  • objClass 中找到了 cache
  • 然後得到@selector(foo) 的地址p,計算 index = p & _mask
  • 使這個 index 做為 _buckets的索引,找到 _buckets[index]
    • 兩個數相與 C = A & B,那麼 C 必定不大於 A 和 B 中最小的數。所以 _buckets 的索引最大的就是 _mask,所以 _mask 的值是 _buckets 的長度減一。
  • 判斷 if (_bucket[index]._key == @selector(foo))
    • 如果相等的話就證明找到了,就可以直接使用了。
    • 如果 _bucket[index]._key == 0,證明這個方法沒有存入陣列中並且這個位置還沒有被別的方法佔用,將 @selector(foo) 存入 _bucket[index] 做為 _key,將函式 foo 的地址存入 _imp
    • 如果 _bucket[index]._key != 0 && _bucket[index]._key != @selector(foo)
      • 不同的兩個數和 _mask 相與,是有可能得到一個結果的。
      • 出現這種情況說明這個位置被比 foo 先呼叫的一個函式佔用了。現在還不能確定 foo 在不在 _buckets 中。
      • 這時讓 index -= 1,然後又進入上一步:使 index 作為索引...
      • 如果 index 已經減到零了,還是沒有空位,就令 index = _mask,然後繼續找
      • 如果找完一圈之後,還是沒有找到
        • 那麼就證明 foo 確實不在雜湊表中
        • 也說明雜湊表已經滿了
        • 雜湊表會擴容變成原來的兩倍,然後修改 _mask 的值
        • 將雜湊表清空。因為 _mask 已經變了,對於之前的元素來說,已經不能通過 _mask 準確地計算出索引了
        • 計算 @selector(foo) 的地址,與新的 _mask 相與得到新的索引 index,然後將 foo 存入雜湊表
      • 查詢過程的程式碼如下
        bucket_t * cache_t::find(cache_key_t k, id receiver)
        {
            assert(k != 0);
        
            bucket_t *b = buckets();
            mask_t m = mask();
            mask_t begin = cache_hash(k, m);
            mask_t i = begin;
            do {
                if (b[i].key() == 0  ||  b[i].key() == k) {
                    return &b[i];
                }
            } while ((i = cache_next(i, m)) != begin);
        
            // hack
            Class cls = (Class)((uintptr_t)this -     offsetof(objc_class, cache));
            cache_t::bad_cache(receiver, (SEL)k, cls);
        }
        
        static inline mask_t cache_next(mask_t i, mask_t mask) {
            return i ? i-1 : mask;
        }
        複製程式碼

cache 快取以及方法查詢方式是要重點掌握的,至少得知道是以雜湊表的方式,然後以什麼為索引,通過什麼進行比較。這裡已經介紹完畢,我先去抽根菸,呆會說說 bits

bits 是一串 64 位的二進位制數。不同的位上寫著不同的數,控制它和不同的掩碼 A_MASKB_MASKC_MASK 相與,就可以得到我們想要的位上的資料。bits & FAST_DATA_MASK 就可以得到 class_rw_t 的地址。

iOS 問題整理04----Runtime

class_rw_t 中我們關注有一個指向 struct class_ro_t 的指標 ro,以及存放類資訊的二維陣列們。這裡的程式碼就不敲了,要記住的是 class_rw_t 中有 ro 指標,還有存放方法列表、屬性列表、協議列表的二維陣列

method_array_t 是一個二維陣列,它裡面存放的是 method_list_tmethod_list_t 中存放的是 method_t。(property_arry_tprotocol_array_t 也是,它們類似。)

method_t 結構如下

struct method_t {
    SEL name;
    const char * types;
    IMP imp;
}
複製程式碼

name 就代表的是方法的名稱,imp 代表的是方法實現的地址types 是方法的型別編碼,主要就是返回值以及各個引數的型別。當我們想找一個方法的時候,我們找到了 method_t,也就知道了它的名字,地址,以及返回值、引數的型別,不就是找到它了嗎?當你想報復一個人的時候,你有他的名字、地址以及鑰匙......

class_rw_tclass_ro_t

  • 從名字上來看,一個是 readwrite 、一個是readonly。所以你懂的,前者是可讀寫的,後者是隻讀的。
  • class_ro_t 包含的是類的初始資訊,class_rw_t 會包含分類中的資訊。
    • 前面介紹過了,category 編譯之後是一個結構體,裡面有各種資訊。執行過程中 runtime 會把這些資訊合併到 class/meta-class 中去,其實就是合併到 class/meta-class 中通過 bits 找到的 class_rw_t 的那些二維陣列中來。而類初始資訊只有一份,所以 class_ro_t 結構體中的那些陣列用一維的就可以了。
    • 你可能注意到了(我知道你沒有),class_ro_t 中有一個 ivarsclass_rw_t 中是沒有的。這個是類的成員變數。ivars 存在於一個 readonly 陣列中,這也可以作為解釋以下兩個問題的一個角度
      • 為啥 category 不能新增成員變數呢?
        • 為啥能新增方法呢?
      • 為啥不能向編譯後的類中新增例項變數呢?

類的結構說完了,你能否回答這兩道面試題?

  • runtime 中,SEL 和 IMP 的區別?
  • runtime 如何通過 selector 找到對應 IMP 的地址?

4. objc 的訊息傳送機制是怎樣的?

ObjC 的動態特性是基於 Runtime 的訊息傳遞機制的,在 ObjC 中,訊息的傳遞都是動態的。

ObjC - 基於 Runtime 的語言,它會盡可能地把決策從編譯時和連線時推遲到執行時(簡單來說,就是編譯後的檔案不全是機器指令,還有一部分中間程式碼,在執行的時候,通過 Runtime 再把需要轉換的中間程式碼在翻譯成機器指令)這使得 ObjC 有著很大的靈活性。比如:
1、動態的確定型別
2、我們可以動態的確定訊息傳遞的物件
3、動態的給物件增加方法的實現 等等

什麼是訊息傳遞?和 C語言 的呼叫函式有什麼區別?

  • 函式呼叫就是直接跳到地址執行。程式碼在編譯、優化之後生成了彙編程式碼,然後連線各種庫,完了就生成了可以執行的程式碼。
  • C語言 在編譯時就已經決定了程式所應執行的程式碼。
  • Objc 中向 receiver 傳送訊息,receiver 並不一定呼叫這個方法,而是到了執行時才會去看 receiver 是否響應這個訊息,再決策是執行這個方法還是其它方法,或者轉發給其它物件。

Tips: 編譯時,編譯器只是簡單的進行語法分析。比如 NSData *obj = [[NSObject alloc] init];,在編譯時 objNSData 型別的,在執行時它是 NSObject 型別的。

接下來對訊息機制進行講述。

當我們程式執行 [obj foo]的時候,你可能會和筆者一樣去想foo是個什麼鬼東西?

當我們程式執行 [obj foo] 的時候,這句程式碼會被編譯成 objc_msgSend(obj, @selector(foo)),即向 obj 傳送訊息 foo。然後就會進入訊息機制的三大階段,訊息傳送、動態解析、訊息轉發。

  • 訊息傳送runtime 會先找到 objisa,然後通過 isa 找到 class/meta-class,在 class/meta-class 以及他們的父類的 cache 和方法列表中去找 foo
  • 動態解析:如果在上階段沒有找到就會進入該階段。runtime 就會給 class/meta-class 傳送 resolveInstanceMethod:/resolveClassMethod: 訊息,在這裡可以給接受者增加執行方法。
  • 訊息轉發:如果沒有在動態解析中進行處理,就會進入到該階段。

下邊這張圖描述了訊息傳送的具體流程:

iOS 問題整理04----Runtime

這張圖注意兩個地方:

  • 可以向 nil 傳送訊息,不會崩潰,也不會有結果。
  • 就算是從父類找到的方法,最後也會快取到訊息接收者的 class/meta-class 的 cache 中。

這一張是動態方法解析的流程圖:

iOS 問題整理04----Runtime

我們已經提到了好幾遍動態解析這個詞語,你可能還不明白它是什麼意思。我們來看一個實際的例子:

@interface Person : NSObject
- (void)run;
@end

@implementation Person
@end
複製程式碼

實現一個 Person 類,宣告一個 -(void)run 方法,但並不實現它。然後在某個 main 函式中讓 person 例項物件呼叫 run,嗯,你知道的它會崩潰。

現在,我們在 Personimplementation 中新增程式碼:

@implementation Person

+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    return [super resolveInstanceMethod:sel];
}

@end
複製程式碼

然後在方法內打上斷點,再執行程式,通過斷點可以發現,程式會進入這個方法。

iOS 問題整理04----Runtime
既然它來了,我們就可以在這裡做點事情。
我們用 Runtime API 給類把新增一個 run 方法吧,在 OC 中,編譯的時候你沒新增方法的實現,沒關係,執行的時候動態地新增一個也是 OK 的。

void aTmpMethod(id self, SEL _cmd)
{
    NSLog(@"%s", __func__);
}

+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    if (sel == @selector(run)) {
        class_addMethod(self, sel, (IMP)aTmpMethod, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}
複製程式碼

我們利用 class_addMethodPerson 動態地新增了一個方法,四個引數分類代表要新增給誰,你的名字是啥,你家在哪,鑰匙呢。

如果 aTmpMethod 不是 C 語言的函式,而是一個 OC 方法。那麼可以這樣寫:

Method method = class_getInstanceMethod(self, @selector(aTmpMethod));
class_addMethod(self, self, method_getImplementation(method), method_getTypeEncoding(method))
複製程式碼

這個時候再執行程式,發現它不會崩潰了,並且呼叫了 aTmpMethod 函式。

iOS 問題整理04----Runtime

上述過程我們就是動態地給 Person 新增了一個名為 run 的方法。如果給 Person meta-class 新增方法,流程類似。

現在我們再來回過頭看這張流程圖。

當訊息機制在訊息傳送階段沒有找到方法的後,會來到動態方法解析階段。

  • 系統會判斷,如果這個方法曾經來到過動態解析階段,那麼就直接進入下一個階段-訊息轉發。
    • 因為如果它之前到過動態解析階段,然後有回到訊息傳送階段,但是走完訊息傳送階段又沒有找到方法,說明它上次來的時候就沒有給它動態新增方法。所以就直接進入下一階段。
  • 如果這是第一次來動態方法解析,那麼就會進入 resolveInstanceMethod:/resolveClassMethod:
  • 然後將它標記為:已經動態解析過的。
  • 最後再回到訊息傳送,再走一遍訊息傳送的流程。

最後一個階段是訊息轉發:

iOS 問題整理04----Runtime

如果第一二階段都沒有處理成功,就會來到這一階段-訊息轉發。

  • 訊息轉發會先呼叫 forwardingTargetForSelector: 方法
    • 如果這個方法返回了一個物件,那麼就讓這個物件去處理這個訊息
    • objc_msgSend(物件, sel)
  • 返回值為nil,就會呼叫 methodSignatureForSelector: 方法
    • methodSignatureForSelector: 要求返回一個方法的簽名
      • 方法的簽名指的是方法的返回值型別以及引數的型別,常用的生成方式如下:
      • [NSMethodSignature signatureWithObjCTypes:"v@:"];
    • 如果返回nil,則會直接呼叫doesNotRecognizeSelector: 方法。
  • 如果 methodSignatureForSelector: 返回不為 nil,則會呼叫 forwardInvocation: 方法。
    • 來到這個方法後,就算什麼都不處理,執行程式也不會崩潰了。
      • 比如你這個方法裡面什麼都不寫
      • 比如你只寫一句 NSLog(@"Hello world!");
    • 還可以處理這個引數 anInvocation
    • NSInvocation封裝了方法呼叫者、方法名、方法的簽名。
    • 比如可以修改方法的呼叫者
      • [anInvocation invokeWithTarget:[[Cat alloc] init]]
    • 比如拿到方法的返回值型別
      • 上邊提到 NSInvocation 中有方法、呼叫者、方法簽名
      • 所以只要拿到方法簽名,就能找到返回值的型別
      NSMethodSignature *sig = [anInvocation valueForKey:@"_signature"]; //拿到方法簽名
      const char *returnType = sig.methodReturnType;//這個字串中的第一個字元就是返回值的型別 
      複製程式碼

objc_msgSend 已經講完了,相信你已經可以回答下邊這三個問題了:

  • objc 中向一個 nil 物件傳送訊息,會發生什麼?
  • 訊息轉發到 forwordinginvacation 方法,如何拿到返回值?
  • 什麼時候會報 unrecognized selector 異常?

講一下 super 的問題,先看一個實際的例子:

iOS 問題整理04----Runtime
現在問你列印什麼?如果你對 super 和訊息機制不瞭解的話,這道題你是不明白所以然的。 下邊是列印的結果:

iOS 問題整理04----Runtime

現在講明白為什麼是這樣:

  • self 是什麼?
    • selfrun 的隱藏引數
    • 每一個方法都有隱藏引數 self_cmd
    • 比如 - (void)run 編譯後會變成 - (void)run:(id)self sel: (SEL)_cmd_cmd 就是 @selector(run)
    • 所以 self 是一個物件,型別是方法呼叫者
  • [self class] 發生了什麼?
    • self 傳送訊息 class
    • 通過 selfisa 找到了 Person,然而 Person 中找不到 class 的實現
    • 就通過 Personsuperclass 指標找到了 NSObject
    • NSObject 中找到 class 的實現並呼叫。
  • super 是什麼?
    • super 是編譯器的一個標識、一個關鍵字
    • 它並不是一個物件
  • [super class] 發生了什麼?
    • 編譯器看見這句程式碼的時候會把它編譯成
      struct __rw_objc_super arg = {
          self,
          class_getSuperclass(objc_getClass("Person"))
      }
      objc_msgSendSuper2(arg, @selector(class))
      複製程式碼
    • objc_msgSendSuper2 的第一個引數是結構體,結構體的第一個成員是 self,第二個成員是 Person 的父類
    • 當執行 objc_msgSendSuper2(arg, @selector(class))
      • 會向 arg 的第一個引數 self 傳送訊息 class,即 self 是訊息的接收者
      • 但是會從第二個引數的快取中開始查詢方法 class
      • 制在 NSObject 中找到了 class 方法,進行呼叫,由於訊息的接收者是 self,所以返回的是 self 的類,而不是 NSObject

5. runtime 中常用的方法。

/*
    動態建立一個類
    引數分別是要建立的類的父類、類名、額外的記憶體空間(一般傳0)
    常考問題:為什麼是 Pair,它是是什麼意思?
    答:pair 的意思是一對。建立一個類就是建立 Class 和 Meta-Class
*/
objc_allocateClassPair(Class  _Nullable __unsafe_unretained superclass, const char * _Nonnull name, size_t extraBytes)

//註冊一個類(要在類註冊之前新增成員變數)
void objc_registerClassPair(Class cls)

//銷燬一個類
void objc_disposeClassPair(Class cls)

//獲取isa指向的Class
Class object_getClass(id obj)

//設定isa指向的Class
Class object_setClass(id obj, Class cls)

//判斷一個OC物件是否為Class
BOOL object_isClass(Class cls)

//判斷一個Class是否為元類
BOOL class_isMetaClass(Class cls)

//獲取父類
Class class_getSuperclass(Class cls)

//獲取一個例項變數
Ivar class_getInstanceVariable(Class cls, const char *name)

//拷貝例項變數列表(最後需要呼叫free釋放)
Ivar *class_copyIvarList(Class cls, unsigned int *outCount)

//動態新增成員變數(已經註冊的類是不能動態新增成員變數的)
class_addIvar(Class  _Nullable __unsafe_unretained cls, const char * _Nonnull name, size_t size, uint8_t alignment, const char * _Nullable types)

//獲取成員變數的相關資訊
const char *ivar_getName(Ivar v)
const char *ivar_getTypeEncoding(Ivar v)

//獲取一個屬性
objc_property_t class_getProperty(Class cls, const char *name)

//拷貝屬性列表(最後需要呼叫free釋放)
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)

//動態新增屬性
class_addProperty(Class  _Nullable __unsafe_unretained cls, const char * _Nonnull name, const objc_property_attribute_t * _Nullable attributes, unsigned int attributeCount)

//動態替換屬性
class_replaceProperty(Class  _Nullable __unsafe_unretained cls, const char * _Nonnull name, const objc_property_attribute_t * _Nullable attributes, unsigned int attributeCount)

//獲取屬性的一些資訊
const char *property_getName(objc_property_t property)
const char *property_getAttributes(objc_property_t property)

//獲得一個例項方法、類方法
Method class_getInstanceMethod(Class cls, SEL name)
Method class_getClassMethod(Class cls, SEL name)

//方法實現相關操作
IMP class_getMethodImplementation(Class cls, SEL name)
IMP method_setImplementation(Method m, IMP imp)
void method_exchangeImplementations(Method m1, Method m2)

//拷貝方法列表(最後需要呼叫free釋放)
Method *class_copyMethodList(Class cls, unsigned int *outCount)

//動態新增方法
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)

//動態替換方法
IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types)

//獲取方法相關的資訊(帶有copy的需要呼叫free去釋放)
SEL method_getName(Method m)
IMP method_getImplementation(Method m)
const char *method_getTypeEncoding(Method m)
unsigned int method_getNumberOfArguments(Method m)
char *method_copyReturnType(Method m)
char *method_coayArgumentType(Method m, unsigned int index)
複製程式碼

6. 你在專案中用過runtime嗎?舉個例子。

相關文章