iOS面試題--原理篇

runtime怎么添加屬性、方法等

  • ivar表示成員變量
  • class_addIvar
  • class_addMethod
  • class_addProperty
  • class_addProtocol
  • class_replaceProperty

是否可以把比較耗時(shí)的操作放在NSNotificationCenter中

  • 首先必須明確通知在哪個(gè)線程中發(fā)出,那么處理接受到通知的方法也在這個(gè)線程中調(diào)用
  • 如果在異步線程發(fā)的通知,那么可以執(zhí)行比較耗時(shí)的操作;
  • 如果在主線程發(fā)的通知,那么就不可以執(zhí)行比較耗時(shí)的操作

runtime 如何實(shí)現(xiàn) weak 屬性

首先要搞清楚weak屬性的特點(diǎn)

weak策略表明該屬性定義了一種“非擁有關(guān)系” (nonowning relationship)。
為這種屬性設(shè)置新值時(shí),設(shè)置方法既不保留新值,也不釋放舊值。此特質(zhì)同assign類似;
然而在屬性所指的對象遭到摧毀時(shí),屬性值也會清空(nil out)

作為一個(gè)開發(fā)者,有一個(gè)學(xué)習(xí)的氛圍跟一個(gè)交流圈子特別重要,這是一個(gè)我的iOS交流群:413038000,不管你是大牛還是小白都?xì)g迎入駐 ,分享BAT,阿里面試題、面試經(jīng)驗(yàn),討論技術(shù), 大家一起交流學(xué)習(xí)成長!

推薦閱讀

iOS開發(fā)——最新 BAT面試題合集(持續(xù)更新中)

那么runtime如何實(shí)現(xiàn)weak變量的自動置nil?

runtime對注冊的類,會進(jìn)行布局,會將 weak 對象放入一個(gè) hash 表中。
用 weak 指向的對象內(nèi)存地址作為 key,當(dāng)此對象的引用計(jì)數(shù)為0的時(shí)候會調(diào)用對象的 dealloc 方法,
假設(shè) weak 指向的對象內(nèi)存地址是a,那么就會以a為key,在這個(gè) weak hash表中搜索,找到所有以a為key的 weak 對象,從而設(shè)置為 nil。

weak屬性需要在dealloc中置nil么

  • 在ARC環(huán)境無論是強(qiáng)指針還是弱指針都無需在 dealloc 設(shè)置為 nil , ARC 會自動幫我們處理
  • 即便是編譯器不幫我們做這些,weak也不需要在dealloc中置nil
  • 在屬性所指的對象遭到摧毀時(shí),屬性值也會清空
// 模擬下weak的setter方法,大致如下
- (void)setObject:(NSObject *)object
{
    objc_setAssociatedObject(self, "object", object, OBJC_ASSOCIATION_ASSIGN);
    [object cyl_runAtDealloc:^{
        _object = nil;
    }];
}

一個(gè)Objective-C對象如何進(jìn)行內(nèi)存布局?(考慮有父類的情況)

  • 所有父類的成員變量和自己的成員變量都會存放在該對象所對應(yīng)的存儲空間中
  • 父類的方法和自己的方法都會緩存在類對象的方法緩存中,類方法是緩存在元類對象中
  • 每一個(gè)對象內(nèi)部都有一個(gè)isa指針,指向他的類對象,類對象中存放著本對象的如下信息
    • 對象方法列表
    • 成員變量的列表
    • 屬性列表
  • 每個(gè) Objective-C 對象都有相同的結(jié)構(gòu),如下圖所示
Objective-C 對象的結(jié)構(gòu)圖
ISA指針
根類(NSObject)的實(shí)例變量
倒數(shù)第二層父類的實(shí)例變量
...
父類的實(shí)例變量
類的實(shí)例變量
  • 根類對象就是NSObject,它的super class指針指向nil
  • 類對象既然稱為對象,那它也是一個(gè)實(shí)例。類對象中也有一個(gè)isa指針指向它的元類(meta class),即類對象是元類的實(shí)例。元類內(nèi)部存放的是類方法列表,根元類的isa指針指向自己,superclass指針指向NSObject類

一個(gè)objc對象的isa的指針指向什么?有什么作用?

  • 每一個(gè)對象內(nèi)部都有一個(gè)isa指針,這個(gè)指針是指向它的真實(shí)類型
  • 根據(jù)這個(gè)指針就能知道將來調(diào)用哪個(gè)類的方法

下面的代碼輸出什么?

@implementation Son : Father
- (id)init
{
    self = [super init];
    if (self) {
        NSLog(@"%@", NSStringFromClass([self class]));
        NSLog(@"%@", NSStringFromClass([super class]));
    }
    return self;
}
@end
  • 答案:都輸出 Son

  • 這個(gè)題目主要是考察關(guān)于objc中對 self 和 super 的理解:

    • self 是類的隱藏參數(shù),指向當(dāng)前調(diào)用方法的這個(gè)類的實(shí)例。而 super 本質(zhì)是一個(gè)編譯器標(biāo)示符,和 self 是指向的同一個(gè)消息接受者

    • 當(dāng)使用 self 調(diào)用方法時(shí),會從當(dāng)前類的方法列表中開始找,如果沒有,就從父類中再找;

    • 而當(dāng)使用 super時(shí),則從父類的方法列表中開始找。然后調(diào)用父類的這個(gè)方法

    • 調(diào)用[self class] 時(shí),會轉(zhuǎn)化成 objc_msgSend函數(shù)

      id objc_msgSend(id self, SEL op, ...)
      
    • 調(diào)用 [super class]時(shí),會轉(zhuǎn)化成 objc_msgSendSuper函數(shù)

      id objc_msgSendSuper(struct objc_super *super, SEL op, ...)
      
    • 第一個(gè)參數(shù)是 objc_super 這樣一個(gè)結(jié)構(gòu)體,其定義如下

      struct objc_super {
      __unsafe_unretained id receiver;
      __unsafe_unretained Class super_class;
      };
      
    • 第一個(gè)成員是 receiver, 類似于上面的 objc_msgSend函數(shù)第一個(gè)參數(shù)self

    • 第二個(gè)成員是記錄當(dāng)前類的父類是什么,告訴程序從父類中開始找方法,找到方法后,最后內(nèi)部是使用 objc_msgSend(objc_super->receiver, @selector(class))去調(diào)用, 此時(shí)已經(jīng)和[self class]調(diào)用相同了,故上述輸出結(jié)果仍然返回 Son

    • objc Runtime開源代碼對- (Class)class方法的實(shí)現(xiàn)

    -(Class)class {
      return object_getClass(self);
    }

runtime如何通過selector找到對應(yīng)的IMP地址?(分別考慮類方法和實(shí)例方法)

  • 每一個(gè)類對象中都一個(gè)對象方法列表(對象方法緩存)
  • 類方法列表是存放在類對象中isa指針指向的元類對象中(類方法緩存)
  • 方法列表中每個(gè)方法結(jié)構(gòu)體中記錄著方法的名稱,方法實(shí)現(xiàn),以及參數(shù)類型,其實(shí)selector本質(zhì)就是方法名稱,通過這個(gè)方法名稱就可以在方法列表中找到對應(yīng)的方法實(shí)現(xiàn).
  • 當(dāng)我們發(fā)送一個(gè)消息給一個(gè)NSObject對象時(shí),這條消息會在對象的類對象方法列表里查找
  • 當(dāng)我們發(fā)送一個(gè)消息給一個(gè)類時(shí),這條消息會在類的Meta Class對象的方法列表里查找

objc中的類方法和實(shí)例方法有什么本質(zhì)區(qū)別和聯(lián)系

類方法

1 類方法是屬于類對象的
2 類方法只能通過類對象調(diào)用
3 類方法中的self是類對象
4 類方法可以調(diào)用其他的類方法
5 類方法中不能訪問成員變量
6 類方法中不能直接調(diào)用對象方法
7 類方法是存儲在元類對象的方法緩存中

實(shí)例方法

1 實(shí)例方法是屬于實(shí)例對象的
2 實(shí)例方法只能通過實(shí)例對象調(diào)用
3 實(shí)例方法中的self是實(shí)例對象
4 實(shí)例方法中可以訪問成員變量
5 實(shí)例方法中直接調(diào)用實(shí)例方法
6 實(shí)例方法中可以調(diào)用類方法(通過類名)
7 實(shí)例方法是存放在類對象的方法緩存中

使用runtime Associate方法關(guān)聯(lián)的對象,需要在主對象dealloc的時(shí)候釋放么?

  • 無論在MRC下還是ARC下均不需要
  • 被關(guān)聯(lián)的對象在生命周期內(nèi)要比對象本身釋放的晚很多,它們會在被 NSObject -dealloc 調(diào)用的 object_dispose()方法中釋放
  • 補(bǔ)充:對象的內(nèi)存銷毀時(shí)間表,分四個(gè)步驟
1.調(diào)用 -release :引用計(jì)數(shù)變?yōu)榱? * 對象正在被銷毀,生命周期即將結(jié)束.
 * 不能再有新的 __weak 弱引用,否則將指向 nil.
 * 調(diào)用 [self dealloc]
2\. 父類調(diào)用 -dealloc
 * 繼承關(guān)系中最直接繼承的父類再調(diào)用 -dealloc
 * 如果是 MRC 代碼 則會手動釋放實(shí)例變量們(iVars)
 * 繼承關(guān)系中每一層的父類 都再調(diào)用 -dealloc
3\. NSObject 調(diào) -dealloc
 * 只做一件事:調(diào)用 Objective-C runtime 中的 object_dispose() 方法
4\. 調(diào)用 object_dispose()
 * 為 C++ 的實(shí)例變量們(iVars)調(diào)用 destructors
 * 為 ARC 狀態(tài)下的 實(shí)例變量們(iVars) 調(diào)用 -release
 * 解除所有使用 runtime Associate方法關(guān)聯(lián)的對象
 * 解除所有 __weak 引用
 * 調(diào)用 free()

_objc_msgForward函數(shù)是做什么的?直接調(diào)用它將會發(fā)生什么?

  • _objc_msgForward是IMP類型,用于消息轉(zhuǎn)發(fā)的:當(dāng)向一個(gè)對象發(fā)送一條消息,但它并沒有實(shí)現(xiàn)的時(shí)候,_objc_msgForward會嘗試做消息轉(zhuǎn)發(fā)
  • 直接調(diào)用_objc_msgForward是非常危險(xiǎn)的事,這是把雙刃刀,如果用不好會直接導(dǎo)致程序Crash,但是如果用得好,能做很多非??岬氖?/li>
  • JSPatch就是直接調(diào)用_objc_msgForward來實(shí)現(xiàn)其核心功能的

能否向編譯后得到的類中增加實(shí)例變量?能否向運(yùn)行時(shí)創(chuàng)建的類中添加實(shí)例變量?為什么?

  • 不能向編譯后得到的類中增加實(shí)例變量;
  • 能向運(yùn)行時(shí)創(chuàng)建的類中添加實(shí)例變量;
  • 分析如下:
    • 因?yàn)榫幾g后的類已經(jīng)注冊在runtime中,類結(jié)構(gòu)體中的objc_ivar_list 實(shí)例變量的鏈表和instance_size實(shí)例變量的內(nèi)存大小已經(jīng)確定,同時(shí)runtime 會調(diào)用class_setIvarLayout 或 class_setWeakIvarLayout來處理strong weak引用,所以不能向存在的類中添加實(shí)例變量
    • 運(yùn)行時(shí)創(chuàng)建的類是可以添加實(shí)例變量,調(diào)用 class_addIvar函數(shù),但是得在調(diào)用objc_allocateClassPair之后,objc_registerClassPair之前,原因同上。

runloop和線程有什么關(guān)系?

  • 每條線程都有唯一的一個(gè)RunLoop對象與之對應(yīng)的
  • 主線程的RunLoop是自動創(chuàng)建并啟動
  • 子線程的RunLoop需要手動創(chuàng)建
  • 子線程的RunLoop創(chuàng)建步驟如下:
    • 在子線程中調(diào)用[NSRunLoop currentRunLoop]創(chuàng)建RunLoop對象(懶加載,只創(chuàng)建一次)

    • 獲得RunLoop對象后要調(diào)用run方法來啟動一個(gè)運(yùn)行循環(huán)

      // 啟動RunLoop
      [[NSRunLoop currentRunLoop] run];
      
    • RunLoop的其他啟動方法

      // 第一個(gè)參數(shù):指定運(yùn)行模式
      // 第二個(gè)參數(shù):指定RunLoop的過期時(shí)間,即:到了這個(gè)時(shí)間后RunLoop就失效了
      [[NSRunLoop currentRunLoop] runMode:kCFRunLoopDefaultMode beforeDate:[NSDate distantFuture]];
      

runloop的mode作用是什么?

  • 用來控制一些特殊操作只能在指定模式下運(yùn)行,一般可以通過指定操作的運(yùn)行mode來控制執(zhí)行時(shí)機(jī),以提高用戶體驗(yàn)
  • 系統(tǒng)默認(rèn)注冊了5個(gè)Mode
    • kCFRunLoopDefaultMode:App的默認(rèn)Mode,通常主線程是在這個(gè)Mode下運(yùn)行,對應(yīng)OC中的:NSDefaultRunLoopMode
    • UITrackingRunLoopMode:界面跟蹤 Mode,用于 ScrollView 追蹤觸摸滑動,保證界面滑動時(shí)不受其他Mode影響
    • kCFRunLoopCommonModes:這是一個(gè)標(biāo)記Mode,不是一種真正的Mode,事件可以運(yùn)行在所有標(biāo)有common modes標(biāo)記的模式中,對應(yīng)OC中的NSRunLoopCommonModes,帶有common modes標(biāo)記的模式有:UITrackingRunLoopMode和kCFRunLoopDefaultMode
    • UIInitializationRunLoopMode:在啟動 App時(shí)進(jìn)入的第一個(gè) Mode,啟動完成后就不再使用
    • GSEventReceiveRunLoopMode:接受系統(tǒng)事件的內(nèi)部Mode,通常用不到

以+scheduledTimerWithTimeInterval...的方式觸發(fā)的timer,在滑動頁面上的列表時(shí),timer會暫定回調(diào),為什么?如何解決?

  • 這里強(qiáng)調(diào)一點(diǎn):在主線程中以+scheduledTimerWithTimeInterval...的方式觸發(fā)的timer默認(rèn)是運(yùn)行在NSDefaultRunLoopMode模式下的,當(dāng)滑動頁面上的列表時(shí),進(jìn)入了UITrackingRunLoopMode模式,這時(shí)候timer就會停止

  • 可以修改timer的運(yùn)行模式為NSRunLoopCommonModes,這樣定時(shí)器就可以一直運(yùn)行了

  • 以下是我的筆記補(bǔ)充:

    • 在子線程中通過scheduledTimerWithTimeInterval:...方法來構(gòu)建NSTimer

      • 方法內(nèi)部已經(jīng)創(chuàng)建NSTimer對象,并加入到RunLoop中,運(yùn)行模式為NSDefaultRunLoopMode
      • 由于Mode有timer對象,所以RunLoop就開始監(jiān)聽定時(shí)器事件了,從而開始進(jìn)入運(yùn)行循環(huán)
      • 這個(gè)方法僅僅是創(chuàng)建RunLoop對象,并不會主動啟動RunLoop,需要再調(diào)用run方法來啟動
    • 如果在主線程中通過scheduledTimerWithTimeInterval:...方法來構(gòu)建NSTimer,就不需要主動啟動RunLoop對象,因?yàn)橹骶€程的RunLoop對象在程序運(yùn)行起來就已經(jīng)被啟動了

      // userInfo參數(shù):用來給NSTimer的userInfo屬性賦值,userInfo是只讀的,只能在構(gòu)建NSTimer對象時(shí)賦值
      [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(run:) userInfo:@"ya了個(gè)hoo" repeats:YES];
      
      // scheduledTimer...方法創(chuàng)建出來NSTimer雖然已經(jīng)指定了默認(rèn)模式,但是【允許你修改模式】
      [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
      
      // 【僅在子線程】需要手動啟動RunLoop對象,進(jìn)入運(yùn)行循環(huán)
      [[NSRunLoop currentRunLoop] run];
      

猜想runloop內(nèi)部是如何實(shí)現(xiàn)的?

  • 從字面意思看:運(yùn)行循環(huán)、跑圈;
  • 本質(zhì):內(nèi)部就是do-while循環(huán),在這個(gè)循環(huán)內(nèi)部不斷地處理各種事件(任務(wù)),比如:Source、Timer、Observer;
  • 每條線程都有唯一一個(gè)RunLoop對象與之對應(yīng),主線程的RunLoop默認(rèn)已經(jīng)啟動,子線程的RunLoop需要手動啟動;
  • 每次RunLoop啟動時(shí),只能指定其中一個(gè) Mode,這個(gè)Mode被稱作 CurrentMode,如果需要切換Mode,只能退出Loop,再重新指定一個(gè)Mode進(jìn)入,這樣做主要是為了隔離不同Mode中的Source、Timer、Observer,讓其互不影響;

不手動指定autoreleasepool的前提下,一個(gè)autorealese對象在什么時(shí)刻釋放?(比如在一個(gè)vc的viewDidLoad中創(chuàng)建)

  • 分兩種情況:手動干預(yù)釋放時(shí)機(jī)、系統(tǒng)自動去釋放
    • 手動干預(yù)釋放時(shí)機(jī):指定autoreleasepool就是所謂的:當(dāng)前作用域大括號結(jié)束時(shí)就立即釋放
    • 系統(tǒng)自動去釋放:不手動指定autoreleasepool,Autorelease對象會在當(dāng)前的 runloop 迭代結(jié)束時(shí)釋放,下面詳細(xì)說明釋放時(shí)機(jī)
      • RunLoop中的三個(gè)狀態(tài)會處理自動釋放池,通過打印代碼發(fā)現(xiàn)有兩個(gè)Observer監(jiān)聽到狀態(tài)值為:1和160(32+128)
        • kCFRunLoopEntry(1) // 第一次進(jìn)入會創(chuàng)建一個(gè)自動釋放池
        • kCFRunLoopBeforeWaiting(32) // 進(jìn)入休眠狀態(tài)前先銷毀自動釋放池,再創(chuàng)建一個(gè)新的自動釋放池
        • kCFRunLoopExit(128) // 退出RunLoop時(shí)銷毀最后一次創(chuàng)建的自動釋放池
  • 如果在一個(gè)vc的viewDidLoad中創(chuàng)建一個(gè)Autorelease對象,那么該對象會在 viewDidAppear 方法執(zhí)行前就被銷毀了(是這樣的嗎???)

蘋果是如何實(shí)現(xiàn)autoreleasepool的?

  • autoreleasepool以一個(gè)隊(duì)列數(shù)組的形式實(shí)現(xiàn),主要通過下列三個(gè)函數(shù)完成.
objc_autoreleasepoolPush
objc_autoreleasepoolPop
objc_aurorelease
  • 看函數(shù)名就可以知道,對autorelease分別執(zhí)行push,和pop操作。銷毀對象時(shí)執(zhí)行release操作

GCD的隊(duì)列(dispatch_queue_t)分哪兩種類型?背后的線程模型是什么樣的?

  • 串行隊(duì)列
  • 并行隊(duì)列
  • dispatch_global_queue();是全局并發(fā)隊(duì)列
  • dispatch_main_queue();是一種特殊串行隊(duì)列
  • 背后的線程模型:自定義隊(duì)列 dispatch_queue_t queue; 可以自定義是并行:DISPATCH_QUEUE_CONCURRENT 或者 串行DISPATCH_QUEUE_SERIAL

蘋果為什么要廢棄dispatch_get_current_queue?

  • 容易誤用造成死鎖

如何用GCD同步若干個(gè)異步調(diào)用?(如根據(jù)若干個(gè)url異步加載多張圖片,然后在都下載完成后合成一張整圖)

  • 必須是并發(fā)隊(duì)列才起作用
  • 需求分析
    • 首先,分別異步執(zhí)行2個(gè)耗時(shí)的操作
    • 其次,等2個(gè)異步操作都執(zhí)行完畢后,再回到主線程執(zhí)行一些操作
  • 使用隊(duì)列組實(shí)現(xiàn)上面的需求
// 創(chuàng)建隊(duì)列組
dispatch_group_t group =  dispatch_group_create();

// 獲取全局并發(fā)隊(duì)列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

// 往隊(duì)列組中添加耗時(shí)操作
dispatch_group_async(group, queue, ^{
    // 執(zhí)行耗時(shí)的異步操作1
});

// 往隊(duì)列組中添加耗時(shí)操作
dispatch_group_async(group, queue, ^{
    // 執(zhí)行耗時(shí)的異步操作2
});

// 當(dāng)并發(fā)隊(duì)列組中的任務(wù)執(zhí)行完畢后才會執(zhí)行這里的代碼
dispatch_group_notify(group, queue, ^{
    // 如果這里還有基于上面兩個(gè)任務(wù)的結(jié)果繼續(xù)執(zhí)行一些代碼,建議還是放到子線程中,等代碼執(zhí)行完畢后在回到主線程

    // 回到主線程
    dispatch_async(group, dispatch_get_main_queue(), ^{
        // 執(zhí)行相關(guān)代碼...
    });
});

dispatch_barrier_async的作用是什么?

  • 函數(shù)定義
dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block);
  • 必須是并發(fā)隊(duì)列,要是串行隊(duì)列,這個(gè)函數(shù)就沒啥意義了
  • 注意:這個(gè)函數(shù)的第一個(gè)參數(shù)queue不能是全局的并發(fā)隊(duì)列
  • 作用:在它前面的任務(wù)執(zhí)行結(jié)束后它才執(zhí)行,在它后面的任務(wù)等它執(zhí)行完成后才會執(zhí)
  • 示例代碼
-(void)barrier
{
    dispatch_queue_t queue = dispatch_queue_create("12342234", DISPATCH_QUEUE_CONCURRENT);

    dispatch_async(queue, ^{
        NSLog(@"----1-----%@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"----2-----%@", [NSThread currentThread]);
    });

    // 在它前面的任務(wù)執(zhí)行結(jié)束后它才執(zhí)行,在它后面的任務(wù)等它執(zhí)行完成后才會執(zhí)行
    dispatch_barrier_async(queue, ^{
        NSLog(@"----barrier-----%@", [NSThread currentThread]);
    });

    dispatch_async(queue, ^{
        NSLog(@"----3-----%@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"----4-----%@", [NSThread currentThread]);
    });
}

以下代碼運(yùn)行結(jié)果如何?

- (void)viewDidLoad
{
    [super viewDidLoad];
    NSLog(@"1");
    dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@"2");
    });
    NSLog(@"3");
}
  • 答案:主線程死鎖

lldb(gdb)常用的調(diào)試命令?

  • po:打印對象,會調(diào)用對象description方法。是print-object的簡寫
  • expr:可以在調(diào)試時(shí)動態(tài)執(zhí)行指定表達(dá)式,并將結(jié)果打印出來,很有用的命令
  • print:也是打印命令,需要指定類型
  • bt:打印調(diào)用堆棧,是thread backtrace的簡寫,加all可打印所有thread的堆棧
  • br l:是breakpoint list的簡寫

BAD_ACCESS在什么情況下出現(xiàn)?

  • 訪問一個(gè)僵尸對象,訪問僵尸對象的成員變量或者向其發(fā)消息
  • 死循環(huán)

如何調(diào)試BAD_ACCESS錯誤

  • 設(shè)置全局?jǐn)帱c(diǎn)快速定位問題代碼所在行
image
  • 開啟僵尸對象調(diào)試功能
image

簡述下Objective-C中調(diào)用方法的過程(runtime)

  • Objective-C是動態(tài)語言,每個(gè)方法在運(yùn)行時(shí)會被動態(tài)轉(zhuǎn)為消息發(fā)送,即:objc_msgSend(receiver, selector),整個(gè)過程介紹如下:
    • objc在向一個(gè)對象發(fā)送消息時(shí),runtime庫會根據(jù)對象的isa指針找到該對象實(shí)際所屬的類
    • 然后在該類中的方法列表以及其父類方法列表中尋找方法運(yùn)行
    • 如果,在最頂層的父類(一般也就NSObject)中依然找不到相應(yīng)的方法時(shí),程序在運(yùn)行時(shí)會掛掉并拋出異常unrecognized selector sent to XXX
    • 但是在這之前,objc的運(yùn)行時(shí)會給出三次拯救程序崩潰的機(jī)會,這三次拯救程序奔潰的說明見問題《什么時(shí)候會報(bào)unrecognized selector的異?!分械恼f明
  • 補(bǔ)充說明:Runtime 鑄就了Objective-C 是動態(tài)語言的特性,使得C語言具備了面向?qū)ο蟮奶匦?,在程序運(yùn)行期創(chuàng)建,檢查,修改類、對象及其對應(yīng)的方法,這些操作都可以使用runtime中的對應(yīng)方法實(shí)現(xiàn)。

什么是method swizzling(俗稱黑魔法)

  • 簡單說就是進(jìn)行方法交換
  • 在Objective-C中調(diào)用一個(gè)方法,其實(shí)是向一個(gè)對象發(fā)送消息,查找消息的唯一依據(jù)是selector的名字。利用Objective-C的動態(tài)特性,可以實(shí)現(xiàn)在運(yùn)行時(shí)偷換selector對應(yīng)的方法實(shí)現(xiàn),達(dá)到給方法掛鉤的目的
  • 每個(gè)類都有一個(gè)方法列表,存放著方法的名字和方法實(shí)現(xiàn)的映射關(guān)系,selector的本質(zhì)其實(shí)就是方法名,IMP有點(diǎn)類似函數(shù)指針,指向具體的Method實(shí)現(xiàn),通過selector就可以找到對應(yīng)的IMP
image
  • 交換方法的幾種實(shí)現(xiàn)方式
    • 利用 method_exchangeImplementations 交換兩個(gè)方法的實(shí)現(xiàn)
    • 利用 class_replaceMethod 替換方法的實(shí)現(xiàn)
    • 利用 method_setImplementation 來直接設(shè)置某個(gè)方法的IMP
image

objc中向一個(gè)nil對象發(fā)送消息將會發(fā)生什么?

  • 在Objective-C中向nil發(fā)送消息是完全有效的——只是在運(yùn)行時(shí)不會有任何作用

    • 如果一個(gè)方法返回值是一個(gè)對象,那么發(fā)送給nil的消息將返回0(nil)
    • 如果方法返回值為指針類型,其指針大小為小于或者等于sizeof(void*)
    • float,double,long double 或者long long的整型標(biāo)量,發(fā)送給nil的消息將返回0
    • 如果方法返回值為結(jié)構(gòu)體,發(fā)送給nil的消息將返回0。結(jié)構(gòu)體中各個(gè)字段的值將都是0
    • 如果方法的返回值不是上述提到的幾種情況,那么發(fā)送給nil的消息的返回值將是未定義的
  • 具體原因分析

    • objc是動態(tài)語言,每個(gè)方法在運(yùn)行時(shí)會被動態(tài)轉(zhuǎn)為消息發(fā)送,即:objc_msgSend(receiver, selector)
    • 為了方便理解這個(gè)內(nèi)容,還是貼一個(gè)objc的源代碼
struct objc_class
{
// isa指針指向Meta Class,因?yàn)镺bjc的類的本身也是一個(gè)Object,
// 為了處理這個(gè)關(guān)系,runtime就創(chuàng)造了Meta Class,
// 當(dāng)給類發(fā)送[NSObject alloc]這樣消息時(shí),實(shí)際上是把這個(gè)消息發(fā)給了Class Object
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE; // 父類
const char *name OBJC2_UNAVAILABLE; // 類名
long version OBJC2_UNAVAILABLE; // 類的版本信息,默認(rèn)為0
long info OBJC2_UNAVAILABLE; // 類信息,供運(yùn)行期使用的一些位標(biāo)識
long instance_size OBJC2_UNAVAILABLE; // 該類的實(shí)例變量大小
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE; // 該類的成員變量鏈表
struct objc_method_list **methodLists OBJC2_UNAVAILABLE; // 方法定義的鏈表
// 方法緩存,對象接到一個(gè)消息會根據(jù)isa指針查找消息對象,
// 這時(shí)會在method Lists中遍歷,
// 如果cache了,常用的方法調(diào)用時(shí)就能夠提高調(diào)用的效率。
// 這個(gè)方法緩存只存在一份,不是每個(gè)類的實(shí)例對象都有一個(gè)方法緩存
// 子類會在自己的方法緩存中緩存父類的方法,父類在自己的方法緩存中也會緩存自己的方法,而不是說子類就不緩存父類方法了
struct objc_cache *cache OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; // 協(xié)議鏈表
#endif
} OBJC2_UNAVAILABLE;

  • objc在向一個(gè)對象發(fā)送消息時(shí),runtime庫會根據(jù)對象的isa指針找到該對象實(shí)際所屬的類,然后在該類中的方法列表以及其父類方法列表中尋找方法運(yùn)行,然后再發(fā)送消息的時(shí)候,objc_msgSend方法不會返回值,所謂的返回內(nèi)容都是具體調(diào)用時(shí)執(zhí)行的。
  • 如果向一個(gè)nil對象發(fā)送消息,首先在尋找對象的isa指針時(shí)就是0地址返回了,所以不會出現(xiàn)任何錯誤

objc中向一個(gè)對象發(fā)送消息[obj foo]和objc_msgSend()函數(shù)之間有什么關(guān)系?

  • [obj foo];在objc動態(tài)編譯時(shí),會被轉(zhuǎn)意為:objc_msgSend(obj, @selector(foo));

什么時(shí)候會報(bào)unrecognized selector的異常?

  • 當(dāng)調(diào)用該對象上某個(gè)方法,而該對象上沒有實(shí)現(xiàn)這個(gè)方法的時(shí)候, 可以通過“消息轉(zhuǎn)發(fā)”進(jìn)行解決,如果還是不行就會報(bào)unrecognized selector異常

  • objc是動態(tài)語言,每個(gè)方法在運(yùn)行時(shí)會被動態(tài)轉(zhuǎn)為消息發(fā)送,即:objc_msgSend(receiver, selector),整個(gè)過程介紹如下:

    • objc在向一個(gè)對象發(fā)送消息時(shí),runtime庫會根據(jù)對象的isa指針找到該對象實(shí)際所屬的類
    • 然后在該類中的方法列表以及其父類方法列表中尋找方法運(yùn)行
    • 如果,在最頂層的父類中依然找不到相應(yīng)的方法時(shí),程序在運(yùn)行時(shí)會掛掉并拋出異常unrecognized selector sent to XXX 。但是在這之前,objc的運(yùn)行時(shí)會給出三次拯救程序崩潰的機(jī)會
  • 三次拯救程序崩潰的機(jī)會

    • Method resolution
      • objc運(yùn)行時(shí)會調(diào)用+resolveInstanceMethod:或者 +resolveClassMethod:,讓你有機(jī)會提供一個(gè)函數(shù)實(shí)現(xiàn)。
      • 如果你添加了函數(shù)并返回 YES,那運(yùn)行時(shí)系統(tǒng)就會重新啟動一次消息發(fā)送的過程
      • 如果 resolve 方法返回 NO ,運(yùn)行時(shí)就會移到下一步,消息轉(zhuǎn)發(fā)
    • Fast forwarding
      • 如果目標(biāo)對象實(shí)現(xiàn)了-forwardingTargetForSelector:,Runtime 這時(shí)就會調(diào)用這個(gè)方法,給你把這個(gè)消息轉(zhuǎn)發(fā)給其他對象的機(jī)會
      • 只要這個(gè)方法返回的不是nil和self,整個(gè)消息發(fā)送的過程就會被重啟,當(dāng)然發(fā)送的對象會變成你返回的那個(gè)對象。
      • 否則,就會繼續(xù)Normal Fowarding。
      • 這里叫Fast,只是為了區(qū)別下一步的轉(zhuǎn)發(fā)機(jī)制。因?yàn)檫@一步不會創(chuàng)建任何新的對象,但Normal forwarding轉(zhuǎn)發(fā)會創(chuàng)建一個(gè)NSInvocation對象,相對Normal forwarding轉(zhuǎn)發(fā)更快點(diǎn),所以這里叫Fast forwarding
    • Normal forwarding
      • 這一步是Runtime最后一次給你挽救的機(jī)會。
      • 首先它會發(fā)送-methodSignatureForSelector:消息獲得函數(shù)的參數(shù)和返回值類型。
      • 如果-methodSignatureForSelector:返回nil,Runtime則會發(fā)出-doesNotRecognizeSelector:消息,程序這時(shí)也就掛掉了。
      • 如果返回了一個(gè)函數(shù)簽名,Runtime就會創(chuàng)建一個(gè)NSInvocation對象并發(fā)送-forwardInvocation:消息給目標(biāo)對象

使用系統(tǒng)的某些block api(如UIView的block版本寫動畫時(shí)),是否也考慮循環(huán)引用問題?

  • 系統(tǒng)的某些block api中,UIView的block版本寫動畫時(shí)不需要考慮,但也有一些api 需要考慮
  • 以下這些使用方式不會引起循環(huán)引用的問題
[UIView animateWithDuration:duration animations:^
{ [self.superview layoutIfNeeded]; }];

[[NSOperationQueue mainQueue] addOperationWithBlock:^
{ self.someProperty = xyz; }];

[[NSNotificationCenter defaultCenter] addObserverForName:@"someNotification"
            object:nil
             queue:[NSOperationQueue mainQueue]
        usingBlock:^(NSNotification * notification)
        { self.someProperty = xyz; }];
  • 但如果方法中的一些參數(shù)是 成員變量,那么可以造成循環(huán)引用,如 GCD 、NSNotificationCenter調(diào)用就要小心一點(diǎn),比如 GCD 內(nèi)部如果引用了 self,而且 GCD 的參數(shù)是 成員變量,則要考慮到循環(huán)引用,舉例如下:

    • GCD

      • 分析:self-->_operationsQueue-->block-->self形成閉環(huán),就造成了循環(huán)引用
      __weak __typeof__(self) weakSelf = self;
      dispatch_group_async(_operationsGroup, _operationsQueue, ^
      {
      [weakSelf doSomething];
      [weakSelf doSomethingElse];
      } );
      
    • NSNotificationCenter

      • 分析:self-->_observer-->block-->self形成閉環(huán),就造成了循環(huán)引用
      __weak __typeof__(self) weakSelf = self;
      _observer = [[NSNotificationCenter defaultCenter]
      addObserverForName:@"testKey"
      object:nil
      queue:nil
      usingBlock:^(NSNotification *note){
          [weakSelf dismissModalViewControllerAnimated:YES];
      }];
      

作為一個(gè)開發(fā)者,有一個(gè)學(xué)習(xí)的氛圍跟一個(gè)交流圈子特別重要,這是一個(gè)我的iOS交流群:413038000,不管你是大牛還是小白都?xì)g迎入駐 ,分享BAT,阿里面試題、面試經(jīng)驗(yàn),討論技術(shù), 大家一起交流學(xué)習(xí)成長!

推薦閱讀

iOS開發(fā)——最新 BAT面試題合集(持續(xù)更新中)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容