Objective-C Runtime(三)Method Swizzling

Runtime 3 Method Swizzling

Objective-C Runtime(一)

  • 簡(jiǎn)介
  • 對(duì)象、類(lèi)的結(jié)構(gòu)
    • objc_object
    • objc_class
  • 消息傳遞(Messaging)
    • objc_method
    • objc_msgSend

Objective-C Runtime(二)

  • 動(dòng)態(tài)方法解析和轉(zhuǎn)發(fā)
    • 動(dòng)態(tài)方法解析
    • 快速消息轉(zhuǎn)發(fā)
    • 標(biāo)準(zhǔn)消息轉(zhuǎn)發(fā)
    • 消息轉(zhuǎn)發(fā)與多繼承
    • 消息轉(zhuǎn)發(fā)與代理對(duì)象

Objective-C Runtime(三)

  • Method Swizzling
    • class_replaceMethod
    • method_setImplementation
    • method_exchangeImplementations
    • Method Swizzling 的應(yīng)用
    • Method Swizzling 注意事項(xiàng)

Objective-C Runtime(四)

  • isa swizzling
    • 介紹
    • 應(yīng)用之KVO
    • 注意

持續(xù)更新中...

Method Swizzling

其實(shí)runtime的概念坊饶、特性已經(jīng)講完了∪福現(xiàn)在來(lái)說(shuō)一下runtime很強(qiáng)大的一個(gè)黑色技能:Method Swizzling高每。

Method Swizzling 利用 Runtime 特性把一個(gè)方法的實(shí)現(xiàn)與另一個(gè)方法的實(shí)現(xiàn)進(jìn)行替換淤井。

消息傳遞 中有講到腺晾,每個(gè)類(lèi)里都有一個(gè) dispatch table 捆昏,將方法的名字(SEL)跟方法的實(shí)現(xiàn)(IMP煌恢,指向 C 函數(shù)的指針)一一對(duì)應(yīng)。swizzle 一個(gè)方法其實(shí)就是在程序運(yùn)行時(shí)在 dispatch table 里做點(diǎn)改動(dòng)瑰抵,讓這個(gè)方法的名字(SEL)對(duì)應(yīng)到另個(gè) IMP 。

轉(zhuǎn)換前谍憔,一一對(duì)應(yīng):

method_swizzling_1.png

轉(zhuǎn)換后,交換一一對(duì)應(yīng):

method_swizzling_2.png

objc/runtime.h中逛球,OC提供了以下API來(lái)動(dòng)態(tài)替換方法的實(shí)現(xiàn):

  • class_replaceMethod
  • method_setImplementation
  • method_exchangeImplementations

這些方法歸根結(jié)底苫昌,都是偷換了method的IMP。

class_replaceMethod

class_replaceMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, 
                    const char * _Nullable types) 

在文檔中詳細(xì)說(shuō)明了祟身,它有兩種不同的行為。當(dāng)類(lèi)中沒(méi)有想替換的原方法時(shí)袜硫,該方法會(huì)調(diào)用 class_addMethod 來(lái)為該類(lèi)增加一個(gè)新方法。也因此它需要在調(diào)用時(shí)傳入types參數(shù)婉陷,而method_exchangeImplementationsmethod_setImplementation卻不需要帚称。

method_setImplementation

最簡(jiǎn)單,僅僅是給一個(gè)方法設(shè)置其實(shí)現(xiàn)方式秽澳。

method_exchangeImplementations

顧名思義闯睹,是交換兩個(gè)方法的實(shí)現(xiàn),等同于調(diào)用兩次method_setImplementation

IMP imp1 = method_getImplementation(m1);
IMP imp2 = method_getImplementation(m2);
method_setImplementation(m1, imp2);
method_setImplementation(m2, imp1);

Method Swizzling 的應(yīng)用

有時(shí)候我們想對(duì)已有類(lèi)的已有實(shí)現(xiàn)增加一些額外處理担神,這時(shí)候我們可以在已有類(lèi)的分類(lèi)中做Method Swizzling楼吃。

下面我們?cè)赨IControl的分類(lèi)里,將-setTag:和自定義的-xxx_setTag:方法交換:

- (void)xxx_setTag:(NSInteger)tag {
    NSLog(@"%s  %@  tag=%ld", __FUNCTION__, NSStringFromSelector(_cmd), tag);
    return [self xxx_setTag:tag];
}

上面是自定義-xxx_setTag:方法的實(shí)現(xiàn)妄讯,乍一看像是遞歸孩锡。但是別忘了我們是要調(diào)換方法的IMP的,在runtime的時(shí)候捞挥,函數(shù)實(shí)現(xiàn)已經(jīng)被交換了浮创。調(diào)用-setTag:會(huì)調(diào)用你實(shí)現(xiàn)的-xxx_setTag:,而在 -xxx_setTag:里調(diào)用-xxx_setTag:實(shí)際上調(diào)用的是原來(lái)的-setTag:砌函。

//UIControl+XXXExtension.m
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        
        //1
        SEL originalSelector = @selector(setTag:);
        SEL swizzledSelector = @selector(xxx_setTag:);
        //2
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
        
        // When swizzling a class method, use the following:
        // Class class = object_getClass((id)self);
        // ...
        // Method originalMethod = class_getClassMethod(class, originalSelector);
        // Method swizzledMethod = class_getClassMethod(class, swizzledSelector);

        //3
        BOOL didAddMethod = class_addMethod(class,
                                            originalSelector,
                                            method_getImplementation(swizzledMethod),
                                            method_getTypeEncoding(swizzledMethod));

        if (didAddMethod) {
        //4
            class_replaceMethod(class,
                                swizzledSelector,
                                method_getImplementation(originalMethod),
                                method_getTypeEncoding(originalMethod));
        } else {
        //5
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}
  1. 拿到要交換的兩個(gè)方法名SEL
  2. 通過(guò)方法名SEL平道,拿到方法對(duì)象Method
  3. 在交換方法前草雕,先調(diào)用了class_addMethod。是因?yàn)橐WC所交換的原方法是本類(lèi)的方法,不是父類(lèi)的方法棍丐。class_addMethod會(huì)覆蓋父類(lèi)的方法實(shí)現(xiàn),但是不會(huì)替換本類(lèi)已經(jīng)有的方法實(shí)現(xiàn)娄猫。所以先做一層class_addMethod保證了本類(lèi)本身有原方法的實(shí)現(xiàn)昏鹃。
  4. 如果本類(lèi)沒(méi)有相應(yīng)的原方法實(shí)現(xiàn),class_addMethod會(huì)成功添加一個(gè)原方法贩疙,實(shí)現(xiàn)IMP設(shè)置為新的實(shí)現(xiàn)讹弯。然后通過(guò)class_replaceMethod在新方法名義下設(shè)置原方法的實(shí)現(xiàn)。
  5. 如果本類(lèi)有相應(yīng)的原方法實(shí)現(xiàn)这溅,method_exchangeImplementations交換兩個(gè)方法的實(shí)現(xiàn)组民。

通常我們方法混寫(xiě)是想要在應(yīng)用程序的整個(gè)生命周期中有效,所以把method swizzling的代碼放在+load的dispatch once中悲靴,是為了保證它的執(zhí)行是線程安全的,并且只執(zhí)行一次耸三。了解更多關(guān)于+load

然后我們來(lái)調(diào)用一下混寫(xiě)后的方法:

 UIControl *ctrl = [[UIControl alloc] init];
 ctrl.tag = 10;
    
 UIView *view = [[UIView alloc] init];
 view.tag = 100;

控制臺(tái)輸出:

-[UIControl(XXXExtension) xxx_setTag:]  tag=10

從輸出中仪壮,可以知道新方法只應(yīng)用于UIControl的對(duì)象睛驳,并不影響其父類(lèi)UIView的對(duì)象膜廊。這就因?yàn)槲覀冎唤粨Q了UIControl類(lèi)的方法爪瓜,明顯UIControl本身沒(méi)有-setTag:方法,所以會(huì)通過(guò)class_addMethod添加一個(gè)蝶缀,所以不影響其父類(lèi)UIView翁都。

Method Swizzling 注意事項(xiàng)

  • Method swizzling is not atomic
  • Changes behavior of un-owned code
  • Possible naming conflicts
  • Swizzling changes the method's arguments
  • The order of swizzles matters
  • Difficult to understand (looks recursive)
  • Difficult to debug

- Method swizzling is not atomic

很明顯方法混寫(xiě)的代碼要完整的執(zhí)行柄慰,程序才會(huì)正常執(zhí)行,正如我們?cè)?code>+load方法中執(zhí)行dispatch once藏研。

- Changes behavior of un-owned code

混寫(xiě)的方法不止對(duì)一個(gè)實(shí)例有效蠢挡,是對(duì)目標(biāo)類(lèi)的所有實(shí)例业踏。我們改變了目標(biāo)類(lèi)涧卵,所以swizzling是很重要的事艺演,要十分小心胎撤。

- Possible naming conflicts

命名沖突貫穿整個(gè)Cocoa的問(wèn)題,我們常常在類(lèi)名和類(lèi)別方法名前加上前綴巫俺,所以我們也在新方法的前面加前綴介汹,就像前面代碼里的-xxx_setTag:嘹承。但如果-xxx_setTag:在別處也定義了怎么辦叹卷?這個(gè)問(wèn)題不僅僅存在于swizzling坪它,這我們可以用別的變通的方法:

直接用新的 IMP 取代原 IMP 往毡,而不是替換。只需要有全局的函數(shù)指針指向原 IMP 就可以懒震。

static void (*gOriginalSetTagIMP)(id self, SEL _cmd, NSInteger tag);

static void xxxSetTag(id self, SEL _cmd, NSInteger tag) {
    // do custom work
    NSLog(@"%s  tag=%ld", __FUNCTION__, tag);
    gOriginalSetTagIMP(self, _cmd, tag);
}

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{    
     
        Method originalMethod = class_getInstanceMethod(self, @selector(setTag:));
        gOriginalSetTagIMP = (void *)method_getImplementation(originalMethod);
        
        if(!class_addMethod(self, @selector(setTag:), (IMP)xxxSetTag, method_getTypeEncoding(originalMethod))) {
            method_setImplementation(originalMethod, (IMP)xxxSetTag);
        }
    });
}

注意這里的自定義實(shí)現(xiàn)xxxSetTag里不能再調(diào)用xxxSetTag本身了挎狸,不然就真的遞歸了锨匆。因?yàn)檫@里調(diào)用的是函數(shù)恐锣,直接調(diào)用舞痰,不走runtime的消息傳遞了响牛。

或者用OC提供的imp_implementationWithBlock,直接用block生成對(duì)應(yīng)的實(shí)現(xiàn)IMP:

Method originalMethod = class_getInstanceMethod(self, @selector(setTag:));
IMP originalImp = method_getImplementation(originalMethod);
SEL originalSel = method_getName(originalMethod);
    
IMP swizzledImp = imp_implementationWithBlock(^(id target, NSInteger tag){
    NSLog(@"%s   tag=%ld", __FUNCTION__, tag);
    void (*func)(id, SEL, NSInteger) = (void(*)(id, SEL, NSInteger))originalImp;
    func(target, originalSel, tag);
});
    
if(!class_addMethod(self, @selector(setTag:), (IMP)swizzledImp, method_getTypeEncoding(originalMethod))) {
    method_setImplementation(originalMethod, (IMP)swizzledImp);
}

- Swizzling changes the method's arguments

method swizzling 后的方法,想正常調(diào)用的話贬丛,將是個(gè)問(wèn)題豺憔。

比如如果想直接調(diào)用xxx_setTag:方法:

[self xxx_setTag:12];

runtime 的做法是:

objc_msgSend(self, @selector(xxx_setTag:), 12);  

runtime去尋找xxx_setTag:的方法實(shí)現(xiàn), _cmd參數(shù)為xxx_setTag: 恭应,但是事實(shí)上runtime找到的方法實(shí)現(xiàn)是原始的setTag:的。

解決方法:使用全局的函數(shù)指針I(yè)MP境肾。

- The order of swizzles matters

多個(gè)swizzle方法的執(zhí)行順序也需要注意准夷。那么應(yīng)該是什么順序呢衫嵌,從父類(lèi)->子類(lèi)的順序交換彻秆,還是從父類(lèi)->子類(lèi)的順序?

舉個(gè)例子酒朵,在UIView蔫耽、UIControl、UIButton的分類(lèi)里面分別自定義如下代碼图甜,準(zhǔn)備與原來(lái)的-setTag:方法進(jìn)行交換:

//  UIView+LYHExtension.m
- (void)viewSetTag:(NSInteger)tag {
    NSLog(@"%s   cmd=%@  tag=%ld", __FUNCTION__, NSStringFromSelector(_cmd), tag);
    return [self viewSetTag:tag];
}

//  UIControl+LYHExtension.m
- (void)controlSetTag:(NSInteger)tag {
    NSLog(@"%s   cmd=%@  tag=%ld", __FUNCTION__, NSStringFromSelector(_cmd), tag);
    return [self controlSetTag:tag];
}

//  UIButton+LYHExtension.m
- (void)buttonSetTag:(NSInteger)tag {
    NSLog(@"%s   cmd=%@  tag=%ld", __FUNCTION__, NSStringFromSelector(_cmd), tag);
    return [self buttonSetTag:tag];
}

從父類(lèi)->子類(lèi)的順序進(jìn)行method swizzle:

[UIView swizzle:@selector(setTag:) withSelector:@selector(viewSetTag:)];
[UIControl swizzle:@selector(setTag:) withSelector:@selector(controlSetTag:)];
[UIButton swizzle:@selector(setTag:) withSelector:@selector(buttonSetTag:)];

這里新寫(xiě)了一個(gè)方法-swizzle: withSelector:黑毅,實(shí)現(xiàn)跟上面+load方法里的方法交換一樣矿瘦。

交換前:

交換前.png

按父類(lèi)->子類(lèi)的順序交換后:

交換后_父類(lèi)子類(lèi)順序.png

創(chuàng)建一個(gè)UIButton的實(shí)例,然后調(diào)用一下-setTag:掘猿,因?yàn)樵谧远x的方法里的實(shí)現(xiàn)是用NSLog打印當(dāng)前方法信息稠通,以及調(diào)用原來(lái)的方法改橘。那就看一下控制臺(tái):

-[UIButton(XXXExtension) buttonSetTag:]   cmd=setTag:  tag=100
-[UIControl(XXXExtension) controlSetTag:]   cmd=buttonSetTag:  tag=100
-[UIView(XXXExtension) viewSetTag:]   cmd=controlSetTag:  tag=100

用圖表示更清晰,按父類(lèi)->子類(lèi)的順序交換后狮惜,調(diào)用子類(lèi)的-setTag:方法:

交換后_父類(lèi)子類(lèi)順序_調(diào)用.png

很明顯碾篡,這種從父類(lèi)->子類(lèi)的交換順序开泽,能正常實(shí)現(xiàn)我們想要的流程穆律,也就是子類(lèi)中拿到的方法,是父類(lèi)已經(jīng)swizzle后的代碼剔蹋。

反過(guò)來(lái)辅髓,子類(lèi)->父類(lèi)的順序進(jìn)行method swizzle:

按子類(lèi)->父類(lèi)的順序交換后:

交換后_子類(lèi)父類(lèi)順序.png

很明顯律想,每層的子類(lèi)(UIButton绍弟、UIControl)都是直接跟擁有原始方法的父類(lèi)(UIView)直接進(jìn)行交換樟遣,不管是否跨層級(jí)身笤。

依然創(chuàng)建一個(gè)UIButton的實(shí)例液荸,調(diào)用一下-setTag:,控制臺(tái):

-[UIButton(XXXExtension) buttonSetTag:]   cmd=setTag:  tag=100

控制臺(tái)只打印了UIButton的自定義方法伤柄,沒(méi)有繼承父類(lèi)UIControl和祖父類(lèi)UIView的自定義方法适刀。

按子類(lèi)->父類(lèi)的順序交換后笔喉,調(diào)用子類(lèi)的-setTag:方法:

交換后_子類(lèi)父類(lèi)順序_調(diào)用.png

多個(gè)有繼承關(guān)系的類(lèi)的對(duì)象swizzle時(shí)常挚,先從父對(duì)象開(kāi)始稽物。 這樣才能保證子類(lèi)方法拿到父類(lèi)中的被swizzle的實(shí)現(xiàn)姨裸。在+(void)load中swizzle不會(huì)出錯(cuò)怨酝,就是因?yàn)閘oad類(lèi)方法會(huì)默認(rèn)從父類(lèi)開(kāi)始調(diào)用农猬。

- Difficult to understand (looks recursive)

新方法的實(shí)現(xiàn)看起來(lái)像遞歸斤葱,但是看看上面已經(jīng)給出的 swizzling 封裝方法, 使用起來(lái)就很易讀懂揍堕。

解決方法:使用全局的函數(shù)指針I(yè)MP汤纸。

- Difficult to debug

使用NSStringFromSelector(_cmd)打印出的方法名調(diào)用的方法名贮泞,__FUNCTION__打印出的方法名是真正實(shí)現(xiàn)的方法名。這塊可能會(huì)混亂囊蓝,畢竟交換了方法聚霜,就像A的實(shí)現(xiàn)是B珠叔,B的實(shí)現(xiàn)是A运杭,不是平常看到的代碼那么直接撇眯,這塊需要思考熊榛,一不小心就會(huì)忘了腕巡。

解決方法:充分的文檔,即使只有你一個(gè)人開(kāi)發(fā)豺总。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末喻喳,一起剝皮案震驚了整個(gè)濱河市表伦,隨后出現(xiàn)的幾起案子慷丽,更是在濱河造成了極大的恐慌要糊,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,324評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件赤套,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡车柠,警方通過(guò)查閱死者的電腦和手機(jī)塑猖,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén)塑陵,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)蜡励,“玉大人,你說(shuō)我怎么就攤上這事兼都“绫蹋” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,328評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵蚓土,是天一觀的道長(zhǎng)北戏。 經(jīng)常有香客問(wèn)我嗜愈,道長(zhǎng)莽龟,這世上最難降的妖魔是什么毯盈? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,147評(píng)論 1 292
  • 正文 為了忘掉前任搂赋,我火速辦了婚禮,結(jié)果婚禮上基公,老公的妹妹穿的比我還像新娘轰豆。我一直安慰自己齿诞,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,160評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著但汞,像睡著了一般。 火紅的嫁衣襯著肌膚如雪糙置。 梳的紋絲不亂的頭發(fā)上谤饭,一...
    開(kāi)封第一講書(shū)人閱讀 51,115評(píng)論 1 296
  • 那天揉抵,我揣著相機(jī)與錄音,去河邊找鬼闺兢。 笑死屋谭,一個(gè)胖子當(dāng)著我的面吹牛龟糕,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播我擂,決...
    沈念sama閱讀 40,025評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼校摩,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼衙吩!你這毒婦竟也來(lái)了舶治?” 一聲冷哼從身側(cè)響起霉猛,我...
    開(kāi)封第一講書(shū)人閱讀 38,867評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤惜浅,失蹤者是張志新(化名)和其女友劉穎坛悉,沒(méi)想到半個(gè)月后承绸,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,307評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡轩猩,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,528評(píng)論 2 332
  • 正文 我和宋清朗相戀三年晤锹,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了鞭铆。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片焦影。...
    茶點(diǎn)故事閱讀 39,688評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡斯辰,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出柄沮,到底是詐尸還是另有隱情祖搓,我是刑警寧澤湖苞,帶...
    沈念sama閱讀 35,409評(píng)論 5 343
  • 正文 年R本政府宣布财骨,位于F島的核電站,受9級(jí)特大地震影響该贾,放射性物質(zhì)發(fā)生泄漏杨蛋。R本人自食惡果不足惜理澎,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,001評(píng)論 3 325
  • 文/蒙蒙 一糠爬、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧揩抡,春花似錦、人聲如沸捅膘。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,657評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至俗扇,卻和暖如春箕别,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背除抛。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,811評(píng)論 1 268
  • 我被黑心中介騙來(lái)泰國(guó)打工到忽, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留清寇,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,685評(píng)論 2 368
  • 正文 我出身青樓翩迈,卻偏偏與公主長(zhǎng)得像帽馋,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子姨涡,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,573評(píng)論 2 353

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

  • 轉(zhuǎn)至元數(shù)據(jù)結(jié)尾創(chuàng)建: 董瀟偉赏表,最新修改于: 十二月 23, 2016 轉(zhuǎn)至元數(shù)據(jù)起始第一章:isa和Class一....
    40c0490e5268閱讀 1,709評(píng)論 0 9
  • 前言 到了今天終于要"出院"了,要總結(jié)一下住院幾天的收獲逢慌,談?wù)凴untime到底能為我們開(kāi)發(fā)帶來(lái)些什么好處间狂。當(dāng)然它...
    一縷殤流化隱半邊冰霜閱讀 23,361評(píng)論 56 317
  • 文中的實(shí)驗(yàn)代碼我放在了這個(gè)項(xiàng)目中鉴象。 以下內(nèi)容是我通過(guò)整理[這篇博客] (http://yulingtianxia....
    茗涙閱讀 923評(píng)論 0 6
  • 這篇文章完全是基于南峰子老師博客的轉(zhuǎn)載 這篇文章完全是基于南峰子老師博客的轉(zhuǎn)載 這篇文章完全是基于南峰子老師博客的...
    西木閱讀 30,554評(píng)論 33 466
  • 我們常常會(huì)聽(tīng)說(shuō) Objective-C 是一門(mén)動(dòng)態(tài)語(yǔ)言牛欢,那么這個(gè)「動(dòng)態(tài)」表現(xiàn)在哪呢淆游?我想最主要的表現(xiàn)就是 Obje...
    Ethan_Struggle閱讀 2,192評(píng)論 0 7