iOS--RunTime學(xué)習(xí)筆記(一)

我們都知道OC是一門動(dòng)態(tài)語言,那么什么是動(dòng)態(tài)語言呢?動(dòng)態(tài)語言,是指程序在運(yùn)行時(shí)可以改變其結(jié)構(gòu):新的函數(shù)可以被引進(jìn),已有的函數(shù)可以被刪除,類型的檢查在運(yùn)行時(shí)進(jìn)行。學(xué)習(xí)了OC的動(dòng)態(tài)特性以后,我們就會(huì)知道,為什么會(huì)說OC是一門動(dòng)態(tài)語言。

OC具有相當(dāng)多的動(dòng)態(tài)特性,經(jīng)常被提到的有:動(dòng)態(tài)類型,動(dòng)態(tài)識(shí)別,動(dòng)態(tài)綁定,動(dòng)態(tài)加載。

動(dòng)態(tài)類型

動(dòng)態(tài)類型特性能使程序直到執(zhí)行時(shí)才確定對象所屬類型。比如說id類型,id類型即通用的對象類型,可以指向任何對象。結(jié)合isKindOfClass方法,在確定對象為某個(gè)類的成員后,再安全的進(jìn)行強(qiáng)制轉(zhuǎn)換,執(zhí)行對應(yīng)的代碼。

動(dòng)態(tài)綁定

基于動(dòng)態(tài)類型,在某個(gè)實(shí)例對象的類型被確定后,該對象對應(yīng)的屬性和響應(yīng)的消息也被完全確定。這樣就使得程序直到執(zhí)行時(shí)才確定對象要調(diào)用的實(shí)際方法。在繼續(xù)之前,需要明確OC中消息的概念。由于OC的動(dòng)態(tài)特性,在OC中其實(shí)很少提及“函數(shù)”概念,傳統(tǒng)的函數(shù)一般在編譯時(shí)就已經(jīng)把參數(shù)信息和函數(shù)實(shí)現(xiàn)打包到編譯后的源碼中了,而在OC中最常用的是消息機(jī)制。

OC中的消息發(fā)送機(jī)制(補(bǔ)充)

在C語言中,我們調(diào)用函數(shù)時(shí),必須先聲明函數(shù)(或者自上而下),而實(shí)際上,聲明函數(shù)的過程就是獲取函數(shù)地址,調(diào)用函數(shù)的過程就是直接跳到地址執(zhí)行,代碼在被編譯器解析、優(yōu)化后,就成為了一堆匯編代碼,然后連接各種庫,完了生成可執(zhí)行的代碼(即是靜態(tài)的)。

在OC中,消息發(fā)送機(jī)制則是Runtime通過selector快速查找IMP的過程,有了函數(shù)指針就可以執(zhí)行對應(yīng)的方法實(shí)現(xiàn)。所有的[object message]都會(huì)轉(zhuǎn)換為objc_msgSend(object, @selector(message))(如果是有參數(shù)的方法,可以在后面用“,”分隔對參數(shù)進(jìn)行拼接),objc_msgSend方法又會(huì)調(diào)用class_getMethodImplementation獲取IMP。

這里需要普及一下什么是selector和IMP。
selector:OC在編譯時(shí),會(huì)根據(jù)方法的名字生成一個(gè)用來區(qū)分這個(gè)方法的唯一的一個(gè)ID,本質(zhì)上就是一個(gè)字符串。只要方法名稱相同,就算參數(shù)的類型不同,它們的ID也是相同的。
IMP:實(shí)際上就是一個(gè)函數(shù)指針,指向方法實(shí)現(xiàn)的首地址。

運(yùn)行時(shí)類中的方法可能會(huì)增加,需要先做讀操作加鎖,使得方法查找和緩存填充成為原子操作。添加category會(huì)刷新緩存,之后如果舊數(shù)據(jù)又被重新填充到緩存中,category添加操作就會(huì)被忽略掉。

獲取IMP的邏輯整理和流程:

  1. 檢查selector是否需要忽略,如果selector是需要被忽略的垃圾回收用到的方法,則將IMP結(jié)果設(shè)為_objc_ignored_method,這是個(gè)匯編程序入口,可以理解為一個(gè)標(biāo)記。對此種情況進(jìn)行緩存填充操作后,跳到第8步,否則執(zhí)行下一步。
  2. 檢查target是否為nil,如果是nil就直接cleanup,然后return。
  3. 查找當(dāng)前類中的緩存,如果命中緩存獲取到了IMP,則直接跳到第8步,否則執(zhí)行下一步。
  4. 在當(dāng)前類中的方法列表(method list)中進(jìn)行查找,也就是根據(jù)selector查找到Method后,獲取Method中的IMP,并填充到緩存中。查找過程比較復(fù)雜,會(huì)針對已經(jīng)排序的列表使用二分法查找,未排序的列表則是線性遍歷(順序查找)。如果成功查找到Method對象,則直接跳到第8步,否則執(zhí)行下一步。
  5. 在繼承層級(jí)中遞歸向父類中查找,情況跟上一步類似,也是先查找緩存,緩存中沒有就查找方法列表。查找成功,進(jìn)入第8步,查找失敗,進(jìn)入下一步。
  6. 進(jìn)入動(dòng)態(tài)方法解析,這是消息轉(zhuǎn)發(fā)前的最后一次機(jī)會(huì)。此時(shí)釋放讀入鎖,接著間接的發(fā)送+resolveInstanceMethod(如果查找的是實(shí)例方法,調(diào)用該方法)或+resolveClassMethod(如果查找的是類方法,調(diào)用該方法)消息。這相當(dāng)于告訴程序員,趕緊用runtime給類里這個(gè)selector弄個(gè)對應(yīng)的IMP吧,因?yàn)榇藭r(shí)鎖已經(jīng)unlock了所以不會(huì)緩存結(jié)果。這些工作都是在非線程安全下進(jìn)行的,完成后需要回到第1步再次查找IMP。
  7. 此時(shí)不僅沒查到IMP,動(dòng)態(tài)方法解析也不奏效,即將進(jìn)入消息轉(zhuǎn)發(fā)。
  8. 讀操作解鎖,并將之前找到的IMP返回。

查找類方法的IMP的流程和查找實(shí)例方法的一樣,不過實(shí)例方法的IMP是在類的緩存和方法列表里面查找,類方法的IMP是在元類的緩存和方法列表里面查找。關(guān)于元類的定義,建議大家參考這篇文章,寫的很詳細(xì)。

OC中的消息轉(zhuǎn)發(fā)機(jī)制(補(bǔ)充)

當(dāng)發(fā)送的消息在類的緩存和方法列表里面查找不到時(shí),為了防止crash,還有3種補(bǔ)救方法:

  1. 動(dòng)態(tài)方法解析,就是上面的第6步操作。對應(yīng)的具體方法是+(BOOL)resolveInstanceMethod:(SEL)sel和+(BOOL)resolveClassMethod:(SEL)sel,當(dāng)查找的方法是實(shí)例方法時(shí)調(diào)用前者,當(dāng)查找的方法是類方法時(shí),調(diào)用后者。

    、、、
    void eat(id target, SEL sel, NSString *name) {
    }
    
    + (BOOL)resolveInstanceMethod:(SEL)sel
    {
        if (sel == @selector(name)) {
          class_addMethod(self, sel, (IMP)eat, "v@:@");
          return YES;
        }
        return [super resolveInstanceMethod:sel];
    }
     、、、
    

這里需要說明一下class_addMethod的第四個(gè)參數(shù),它是一個(gè)const char * _Nullable types類型,Nullable代表這個(gè)參數(shù)可以為空,char *表示這是一個(gè)char型指針,所以不要試圖用@“”。看我們傳進(jìn)去的參數(shù)v@:@,有人可能有點(diǎn)迷糊,但其實(shí)很好理解:v代表這是一個(gè)返回值為void的函數(shù),@代表這是一個(gè)參數(shù),:代表這是一個(gè)SEL類型,所以跟我們的eat方法對應(yīng)起來就是v@:@。關(guān)于這些類型的詳細(xì)對應(yīng)關(guān)系,大家可以在官方文檔中查找。

  1. 轉(zhuǎn)移消息的接受者。通過方法-(id)forwardingTargetForSelector:(SEL)aSelector可以轉(zhuǎn)移消息的接收者,當(dāng)返回非self/非nil時(shí),消息被轉(zhuǎn)給新對象執(zhí)行。如果該對象實(shí)現(xiàn)了該方法(不管該方法是私有還是公有,其實(shí)OC是沒有嚴(yán)格意義上的私有方法的,因?yàn)橛衦untime的存在,你想要調(diào)用總可以辦到的),就直接讓那個(gè)對象來處理消息。這種方式只是轉(zhuǎn)移消息的接收者,不能對傳遞的消息做修改。

     、、、
    - (id)forwardingTargetForSelector:(SEL)aSelector{
       if(selector == @selector(name)){
         Student *s = [[Student alloc] init];
         if([s respondsToSelector:@selector(name)]){
           return s;
         }
       }
       return[super forwardingTargetForSelector:aSelector];
    }
    、、、
    
  2. 完整消息轉(zhuǎn)發(fā)。如果上述方式?jīng)]有對轉(zhuǎn)發(fā)的消息做處理,那么系統(tǒng)就會(huì)走完整的轉(zhuǎn)發(fā)流程。系統(tǒng)會(huì)先通過-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector方法,找到轉(zhuǎn)發(fā)的消息的方法簽名,然后再通過-(void)forwardInvocation:(NSInvovation *)anInvocation來轉(zhuǎn)發(fā)消息。注意,前面一個(gè)方法不能返回nil,如果返回nil,第二個(gè)方法是不會(huì)執(zhí)行的。通過這種方式轉(zhuǎn)發(fā)的消息,我們不僅可以轉(zhuǎn)換消息的接收者,轉(zhuǎn)發(fā)給多個(gè)對象,還可以對轉(zhuǎn)發(fā)的消息做相應(yīng)的修改。

       - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
           if (aSelector == @selector(eatChicken:)) {
               // 這個(gè)方法的參數(shù)對應(yīng)的type和上面提到的TypeEncoding的對應(yīng)關(guān)系是一樣的。signatureWithObjCTypes的參數(shù)應(yīng)該與aSelector的返回值和參數(shù)的類型和個(gè)數(shù)相對應(yīng),比如eatNothing是一個(gè)無返回值無參數(shù)的方法,所以type就對應(yīng)為v@:(可能有的童鞋把type寫成v@:@也能成功運(yùn)行,但是成功運(yùn)行不代表就正確。如果你在forwardInvocation中對NSInvocation的returnvalue和argument進(jìn)行操作的話,就會(huì)報(bào)錯(cuò)。有可能會(huì)報(bào)參數(shù)越界,也有可能會(huì)什么錯(cuò)誤信息都沒有。如果出現(xiàn)這種情況的童鞋不妨檢查一下自己的type和selector是否一一對應(yīng))。
               NSMethodSignature *sign = [NSMethodSignature signatureWithObjCTypes:"v@:"];
               // 為了防止上面的錯(cuò)誤出現(xiàn),我們也可以采取這種寫法,EatFood是聲明了aSelector方法的類。
               // sign = [[EatFood alloc] init] methodSignatureForSelector:aSelector];
               return sign;
           }
           NSMethodSignature *sign = [super methodSignatureForSelector:aSelector];
           return sign;
       }
    
       - (void)forwardInvocation:(NSInvocation *)anInvocation{
           NSNumber *number = @3;
    
           // 修改轉(zhuǎn)發(fā)消息的參數(shù),注意因?yàn)橛袃蓚€(gè)隱藏參數(shù)的存在,所以index應(yīng)該從2開始算起
           [anInvocation setArgument:&number atIndex:2];
    
           // 修改轉(zhuǎn)發(fā)的消息
           anInvocation.selector = @selector(eatNothing);
    
           // 把消息轉(zhuǎn)發(fā)給多個(gè)對象
           if ([EatChicken instancesRespondToSelector:anInvocation.selector]) {
               [anInvocation invokeWithTarget:[[EatChicken alloc] init]];
           }
          if ([EatFood instancesRespondToSelector:anInvocation.selector]) {
               [anInvocation invokeWithTarget:[[EatFood alloc] init]];
           }
       }        
       、、、
    

如果上述3個(gè)方法都沒有來處理這個(gè)消息,就會(huì)進(jìn)入NSObject的-(void)doesNotRecognizeSelector:(SEL)aSelector方法中,拋出異常。
如果想要通過運(yùn)行時(shí)為類添加方法,使用第一種方案;如果想要把消息轉(zhuǎn)發(fā)給另外另外一個(gè)類的對象時(shí),使用第二種方案;如果想要把消息轉(zhuǎn)發(fā)給多個(gè)類的對象(OC的多繼承也可以通過該方法實(shí)現(xiàn))時(shí),使用第三種方案,該方案還可以對消息的selector和參數(shù)進(jìn)行修改。步驟越往后,處理消息的代價(jià)就越大。

那么說了那么多,消息轉(zhuǎn)發(fā)在實(shí)際開發(fā)中究竟有什么應(yīng)用呢?相信大家在調(diào)試接口的時(shí)候都遇到過這種問題,后臺(tái)開發(fā)接口的時(shí)候,大家說好的沒有數(shù)據(jù)返回空數(shù)組,空字典或者空字符串,但是他啪給你返回來一個(gè)NSNull,然后在你開開心心去用dic[@"happy"]去取值的時(shí)候給你來一個(gè)大大的崩潰,很心酸有沒有。這時(shí)候我們可以怎么做呢,給NSNull添加一個(gè)category,然后重寫methodSignatureForSelector和forwardInvocation來處理異常情況。 (PS:實(shí)現(xiàn)這兩個(gè)方法的時(shí)候會(huì)報(bào)一個(gè)警告:Category is implementing a method which will also be implemented by its primary class。意思是類目里面重寫了基類里面的方法,蘋果是不建議在類目里面重寫類的方法的,因?yàn)轭惸坷镏貙懙姆椒ㄗ饔糜蚴侨值模锌赡軙?huì)導(dǎo)致一些未知的錯(cuò)誤,坑隊(duì)友啊。而且有一些框架里面也有可能會(huì)重寫這個(gè)方法,這樣哪個(gè)類目里的方法會(huì)被執(zhí)行就是未知的。在類目里面聲明一些方法的時(shí)候,盡量帶上前綴,以防止跟系統(tǒng)或者三方框架里面的方法重名。另外,重寫方法的話盡量使用繼承的方式,繼承的作用域只在于子類,不會(huì)對父類產(chǎn)生影響。
、、、

      @implementation NSNull (Forward)

      - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
      {
           NSMethodSignature *sign = [super methodSignatureForSelector:aSelector];
          if (sign == nil) {
              sign = [NSMethodSignature signatureWithObjCTypes:"@^v^c"];
          }
          return sign;
      }

      - (void)forwardInvocation:(NSInvocation *)anInvocation
      {
          if ([self respondsToSelector:anInvocation.selector]) {
              [anInvocation invokeWithTarget:self];
          }
      }

      @end
      、、、
通過運(yùn)行時(shí)避免動(dòng)態(tài)綁定來優(yōu)化方法調(diào)用的一個(gè)小tips

如果需要頻繁對一個(gè)消息進(jìn)行多次調(diào)用,而且我們希望節(jié)省每次調(diào)用方法都要發(fā)送消息的開銷時(shí),我們可以通過取得方法的地址,并且直接像調(diào)用函數(shù)一樣調(diào)用該方法來達(dá)到目的。

利用NSObject類中的methodForSelector:方法,我們可以獲得一個(gè)指向方法實(shí)現(xiàn)的指針,并且可以使用該指針直接調(diào)用方法實(shí)現(xiàn)。methodForSelector:返回的指針和賦值的變量類型必須完全一致,包括方法的參數(shù)類型和返回值類型都在類型識(shí)別的考慮范圍中。
下面的例子展示了怎么使用指針來調(diào)用setFilled:方法的實(shí)現(xiàn):

 void (*setter) (id, SEL, BOOL);
 int i;
 setter = (void (*) (id, SEL, BOOL))[target methodForSelector:@selector(setFilled)];
 for (int i = 0; i < 1000, i++){
     setter(targetList[i], @selector(setFilled:), YES);
 }
 、、、

方法指針的第一個(gè)參數(shù)是接收消息的對象,第二個(gè)參數(shù)是方法的SEL,這兩個(gè)參數(shù)在方法中是隱藏參數(shù),但使用函數(shù)的形式來調(diào)用方法時(shí)必須顯式的給出。使用methodForSelector:來避免動(dòng)態(tài)綁定將減少大部分消息的開銷,但是這只有在指定的消息被重復(fù)發(fā)送給多個(gè)不同類的對象(如果是多次發(fā)送給同一個(gè)對象,效果不是很明顯,因?yàn)橥ㄟ^消息的發(fā)送機(jī)制我們知道,調(diào)用過的method會(huì)被添加到類的方法的cache列表里,從cache列表查詢和直接通過指針調(diào)用區(qū)別并不是很大,不過開銷肯定是減少了的)很多次時(shí)才有意義,例如上面的for循環(huán)。

動(dòng)態(tài)識(shí)別

動(dòng)態(tài)識(shí)別常用的幾個(gè)方法:
- (BOOL)isKindOfClass:(__unsafe_unretained Class)是否是某個(gè)類或者這個(gè)類的子類。
- (BOOL)isMemberOfClass::(__unsafe_unretained Class)是否是一個(gè)類的實(shí)例。
- (BOOL)respondsToSelector:selector類中是否有這個(gè)方法。

動(dòng)態(tài)加載

OC程序可以在運(yùn)行時(shí)鏈接和載入新的類和范疇類。新載入的類和在程序啟動(dòng)時(shí)載入的類并沒有區(qū)別。動(dòng)態(tài)加載可以用在很多地方。例如,系統(tǒng)配置中的模塊就是被動(dòng)態(tài)加載的。在Cocoa環(huán)境中,動(dòng)態(tài)加載一般被用來對應(yīng)用程序進(jìn)行定制。您的程序可以在運(yùn)行時(shí)加載其它程序員編寫的模塊--和Interface Build載入定制的調(diào)色板以及系統(tǒng)配置程序載入定制的模塊的類似。這些模塊通過您許可的方式擴(kuò)展了您的程序,而您無需自己來定義或?qū)崿F(xiàn)。您提供了框架,而其它的程序員提供了實(shí)現(xiàn)。

OC可以動(dòng)態(tài)的創(chuàng)建一個(gè)類嗎?
答案:可以。

  1. 使用objc_allocateClassPair函數(shù)為新的類分配存儲(chǔ)空間,參數(shù)分別是你要繼承的類,新的類的名稱和你要分配的內(nèi)存大小。
  2. 使用class_addMethod為類增加方法,使用class_addIvar為類增加實(shí)例變量。
  3. 用objc_registerClassPair函數(shù)注冊這個(gè)類,以便它能被別人使用。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,327評論 6 537
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,996評論 3 423
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,316評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,406評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,128評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,524評論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,576評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,759評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,310評論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,065評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,249評論 1 371
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,821評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,479評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,909評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,140評論 1 290
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,984評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,228評論 2 375

推薦閱讀更多精彩內(nèi)容

  • 轉(zhuǎn)至元數(shù)據(jù)結(jié)尾創(chuàng)建: 董瀟偉,最新修改于: 十二月 23, 2016 轉(zhuǎn)至元數(shù)據(jù)起始第一章:isa和Class一....
    40c0490e5268閱讀 1,753評論 0 9
  • 我們常常會(huì)聽說 Objective-C 是一門動(dòng)態(tài)語言,那么這個(gè)「動(dòng)態(tài)」表現(xiàn)在哪呢?我想最主要的表現(xiàn)就是 Obje...
    Ethan_Struggle閱讀 2,227評論 0 7
  • 前言 runtime其實(shí)在我們?nèi)粘i_發(fā)過程中很少使用到,尤其是像我現(xiàn)在比較初級(jí)的程序猿就更用不到了。但是去面試很多...
    WolfTin閱讀 649評論 0 2
  • 參考鏈接: http://www.cnblogs.com/ioshe/p/5489086.html 簡介 Runt...
    樂樂的簡書閱讀 2,150評論 0 9
  • 轉(zhuǎn)載:http://yulingtianxia.com/blog/2014/11/05/objective-c-r...
    F麥子閱讀 756評論 0 2