iOS常見面試題(block,runtime,runloop,類結構)附參考答案

趁着開年的空閒時間,找了一些面試題寫寫,算是回顧總結一下iOS開發方面的知識, 水平渣,答案僅做參考! 歡迎指導討論,寫的不對或不妥的地方望及時提出! 原題在這裏html

iOS常見面試題基礎篇(附參考答案)看這裏ios

  • Blockgit

  • block的實質是什麼?一共有幾種block?都是什麼狀況下生成的?github

    • 簡單的來說,block在OC中的表現能夠看做爲帶有自動變量(局部變量)的匿名函數面試

      C語言函數定義的時候,能夠將函數的地址賦值給函數指針類型的變量,以下:數據結構

      int func(int count){
          return count + 1;
      }
      // 賦值
      int (*funcprt)(int) = &func;
      int result = (*funcprt)(10);複製代碼

      同理,咱們也能夠將block語法賦值給聲明爲block類型的變量中。以下:函數

      // 聲明一個block類型的變量,其與函數指針類型的變量不一樣之處只有'*'改成'^'
      int (^blk)(int);
      // 賦值
      int (^blk)(int) = ^(int count){return count + 1;};
      int result = blk(10);複製代碼

      還能夠經過typedef來聲明blk_t類型變量,以下:oop

      typedef int (^blk_t)(int);
      blk_t blk = ^(int count){return count + 1;};
      int result = blk(10)複製代碼

      以上解釋了匿名函數,如今來解釋一下帶有自動變量post

      int value = 10;
      void(^blk)() = ^{
          NSLog(@"value === %d", value);
      };
      blk();
      value = 2;
      NSLog(@"%@", value);複製代碼

      value結果爲 10 。在block中,block表達式截獲所使用的自動變量的值,即保存該變量的瞬間值,因此在執行了block後,改變block外自動變量的值,並不會影響block執行時自動變量的值。ui

    • block的本質

      關於block的本質, ibireme大神objc 中的 block這篇博客裏,有詳細的分析。

      struct Block_descriptor_1 {
          uintptr_t reserved;
          uintptr_t size;
      };
      
      struct Block_layout {
          void *isa;
          volatile int32_t flags; // contains ref count
          int32_t reserved; 
          void (*invoke)(void *, ...);
          struct Block_descriptor_1 *descriptor;
          // imported variables
      };複製代碼

      根據block的數據結構能夠發現,它是含有*isa指針的,在OC中根據對象的定義,凡是首地址是*isa的結構體指針,均可以認爲是對象(id)。這樣在objc中,block實際上就算是對象。

    • 既然OC處理Block是按照對象來處理的。在iOS中,常見的就是_NSConcreteStackBlock_NSConcreteMallocBlock_NSConcreteGlobalBlock這3種,還有另外幾種,暫不作討論。

    • ARC下:

      void(^blockA)() = ^{
          NSLog(@"just a block");
      };
      NSLog(@"%@", blockA);
      
      int value = 10;
      void(^blockB)() = ^{
          NSLog(@"just a block === %d", value);
      };
      NSLog(@"%@", blockB);複製代碼

      ARC下打印結果以下:

      <__NSGlobalBlock__: 0x10c81c308> <__NSMallocBlock__: 0x60400025d160>

      MRC下打印結果以下: <__NSGlobalBlock__: 0x1056bf308> <__NSStackBlock__: 0x7ffeea540a48>

      咱們對棧上的block作copy操做:

      NSLog(@"%@", [blockB copy])

      結果爲: <__NSMallocBlock__: 0x600000444b60>

      由此咱們能夠得出如下結果:

      • 當block沒有引用外部局部變量的時候,block爲全局block

      • 當block引用外部局部變量的時候,ARC下爲堆block,MRC下爲棧block,此時對MRC下的棧block進行copy,棧block就變爲堆block。在ARC下,編譯器會把block從棧拷貝到堆。

      • 經驗證,當block只引用靜態變量,全局變量的時候,block均爲全局block

  • 爲何在默認狀況下沒法修改被block捕獲的變量? __block都作了什麼?

    • 默認狀況下,在block中是沒法修改被block捕獲的自動變量,由於block捕獲自動變量時,僅僅捕獲到該自動變量的值,並不是是內存地址,所以早block內部沒法改變自動變量的值。
    • __block的實現原理詳見深刻研究 Block 捕獲外部變量和 __block 實現原理。大體意思是說: 帶有__block的自動變量,通過編譯後會變成一個結構體,經過結構體中的__forwarding指針能夠訪問到變量,天然就能夠修改變量了。
  • 模擬一下循環引用的一個狀況?block實現界面反向傳值如何實現?

    • 循環引用場景

      // 當block做爲屬性時:
      @property(nonatomic, copy) void(^block)();
      
      self.block = ^{
          NSLog(@"%@",self);
      }
      複製代碼

      此時會出現警告:

      這時,能夠用__weak修飾self,避免循環引用:

    • 界面反向傳值

      //SecondViewController.h
      #import < UIKit/UIKit.h>
      
      typedef void(^CallBackBlock) (NSString *string);
      
      @interface SecondViewController : UIViewController
      
      @property (nonatomic,strong)UItextField *textField;
      @property (nonatomic,copy)CallBackBlcok callBackBlock;
      
      @end
      
      // 在implementation中添加一個點擊事件:
      - (IBAction)click:(id)sender {
         self.callBackBlock(_textField.text);
          [self.navigationController popToRootViewControllerAnimated:YES];
      }
      複製代碼

      在FirstViewController中:

      // FirstViewController.m
      - (IBAction)push:(id)sender {
          SecondViewController *secondVC = [[SecondViewController alloc]init]
          secondVC.callBackBlock = ^(NSString *string){
              NSLog(@"string is %@",string);
              self.label.text = string;
          };
          [self.navigationController pushViewController:secondVC animated:YES];
      }複製代碼
  • 思考一下這個問題: ARC下會發生什麼? MRC呢?若blockA並無引用自動變量val的話狀況又是什麼樣?

    @property(nonatomic, weak) void(^block)();
      
      - (void)viewDidLoad {
          [superviewDidLoad];
          int val = 10;
          void(^blockA)() = ^{
              NSLog(@"val:%d", val);
          };
          NSLog(@"%@", blockA);
          _block = blockA;
      }
          
      -(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
          NSLog(@"%@", _block);
      }複製代碼
  • Runtime

  • objc在向一個對象發送消息時,發生了什麼?

    • 衆所周知,在objc中方法調用的本質是發消息,例如:

      [obj message];
      // 運行時會轉化爲:
      objc_msgSend(obj, selector)
      
      // 當有參數時:
      [obj message:(id)arg...];
      // 運行時會轉化爲:
      objc_msgSend(obj, selector, arg1, arg2, ...)複製代碼

      消息在運行時纔會與方法綁定

      當向一個對象發送消息時:

      1.首先檢測這個 selector 是否是要忽略。好比 Mac OS X 開發,有了垃圾回收就不理會 retain,release 這些函數。

      2.檢測這個 selector 的 target 是否是 nil,Objc 容許咱們對一個 nil 對象執行任何方法不會 Crash,由於運行時會被忽略掉。

      3.若是上面兩步都經過了,那麼就開始查找這個類的實現 IMP,先從 cache 裏查找,若是找到了就運行對應的函數去執行相應的代碼。

      4.若是 cache 找不到就找類的方法列表中是否有對應的方法。 若是類的方法列表中找不到就到父類的方法列表中查找,一直找到 NSObject 類爲止。

      5.若是還找不到,就要開始進入消息轉發流程

      以下圖所示:

  • 何時會報unrecognized selector錯誤?iOS有哪些機制來避免走到這一步?

    • 當調用某對象上某個方法,而該對象並無實現這個方法的時候, 能夠經過消息轉發進行解決。

    • 消息轉發步驟以下:

      1. objc運行時會調用 +resolveInstanceMethod:或者 +resolveClassMethod:,讓咱們有機會提供一個函數實現。若是你添加了函數,那運行時系統就會從新啓動一次消息發送的過程,不然,運行時就會移到下一步,消息轉發(Message Forwarding)。

      2. 調用forwardingTargetForSelector:方法,嘗試找到一個能響應該消息的對象。若是獲取到,則直接轉發給它。若是返回了nil,繼續下面的動做。

      3. 調用methodSignatureForSelector:方法,嘗試得到一個方法簽名。若是獲取不到,則直接調用doesNotRecognizeSelector拋出異常。若是返回了一個方法簽名,Runtime就會建立一個NSInvocation對象,而後繼續進行第四步

      4. 調用forwardInvocation:方法,將地3步獲取到的方法簽名包裝成Invocation傳入,如何處理就在這裏面了。

      如圖所示:

  • 可否向編譯後獲得的類中增長實例變量?可否向運行時建立的類中添加實例變量?爲何?

    • 不能向編譯後獲得的類中增長實例變量
    • 能向運行時建立的類中添加實例變量
    • 由於編譯後的類已經註冊在 runtime 中,類結構體中的 objc_ivar_list 實例變量的鏈表 和 instance_size 實例變量的內存大小已經肯定,同時runtime 會調用 class_setIvarLayout 或 class_setWeakIvarLayout 來處理 strong weak 引用。因此不能向存在的類中添加實例變量。運行時建立的類是能夠添加實例變量,調用 class_addIvar 函數。可是得在調用 objc_allocateClassPair 以後,objc_registerClassPair 以前,緣由同上。
  • runtime如何實現weak變量的自動置nil?

    • weak修飾符表示該屬性是非擁有關係,運行期系統會將每個類的weak變量放入相應的一個hash表中,在這個表中以weak變量所指向的對象的內存地址爲key,當weak指向的對象引用計數爲0執行dealloc方法,對象被銷燬,運行期系統經過key去hash表中找到相應的weak對象將他們設置成nil。
  • 給類添加一個屬性後,在類結構體裏哪些元素會發生變化?

    • 類的結構體以下:
      struct objc_class {
          Class isa  OBJC_ISA_AVAILABILITY;
      
      #if !__OBJC2__
          Class super_class                                        OBJC2_UNAVAILABLE;
          const char *name                                         OBJC2_UNAVAILABLE;
          long version                                             OBJC2_UNAVAILABLE;
          long info                                                OBJC2_UNAVAILABLE;
          long instance_size                                       OBJC2_UNAVAILABLE;
          struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;
          struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;
          struct objc_cache *cache                                 OBJC2_UNAVAILABLE;
          struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;
      #endif
      } OBJC2_UNAVAILABLE;
      /* Use `Class` instead of `struct objc_class *` */複製代碼
    • 當咱們給類添加屬性後,實例對象的內存大小:instance_size和屬性列表:objc_ivar_list *ivars會發生改變。

RunLoop

  • runloop是來作什麼的?runloop和線程有什麼關係?主線程默認開啓了runloop麼?子線程呢?

    • runloop字面的意思就是跑圈,實際上App能一直不停的運行下去,runloop功不可沒!咱們來分析一下main函數:

      int main(int argc, char * argv[]) {
            @autoreleasepool {
                return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
            }
        }複製代碼

      若是沒有runloop,線程執行完以後就會退出,就不能再執行任務了。這時咱們就須要採用一種方式來讓線程可以處理任務,並不退出。因此,咱們就有了RunLoop,其中UIApplicationMain函數內部幫咱們開啓了主線程的RunLoop,UIApplicationMain內部擁有一個無線循環的代碼。

    • runloop與線程對應關係以下:

      1. 一條線程對應一個RunLoop對象,每條線程都有惟一一個與之對應的RunLoop對象。
      2. 咱們只能在當前線程中操做當前線程的RunLoop,而不能去操做其餘線程的RunLoop。
      3. RunLoop對象在第一次獲取RunLoop時建立,銷燬則是在線程結束的時候。
      4. 主線程的RunLoop對象系統自動幫助咱們建立好了(原理以下),而子線程的RunLoop對象須要咱們主動建立。
  • runloop的mode是用來作什麼的?有幾種mode?

    • 在ibireme大神的深刻理解 Runloop一文中,詳細的介紹了一個Runloop包含了哪些東西,以下圖所示:

      Mode表明RunLoop的運行模式,一個RunLoop能夠包含若干個Mode,每一個Mode又包含若干個Source/Timer/Observer。每次調用RunLoop的主函數時,只能指定其中一個Mode,這個Mode被稱做CurrentMode。若是須要切換Mode,只能退出Loop,再從新指定一個Mode進入。這樣作主要是爲了分隔開不一樣組的Source/Timer/Observer,讓其互不影響。

    • 常見的Mode以下:

      1.kCFRunLoopDefaultMode:App的默認運行模式,一般主線程是在這個運行模式下運行 2.UITrackingRunLoopMode:跟蹤用戶交互事件(用於 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其餘Mode影響) 3.UIInitializationRunLoopMode:在剛啓動App時第進入的第一個 Mode,啓動完成後就再也不使用 4.GSEventReceiveRunLoopMode:接受系統內部事件,一般用不到 5.kCFRunLoopCommonModes:僞模式,不是一種真正的運行模式

  • 爲何把NSTimer對象以NSDefaultRunLoopMode(kCFRunLoopDefaultMode)添加到主運行循環之後,滑動scrollview的時候NSTimer卻不動了?

    • 1.當咱們不作任何操做的時候,RunLoop處於NSDefaultRunLoopMode下。
    • 2.而當咱們拖動scrollview的時候,RunLoop就結束NSDefaultRunLoopMode,切換到了UITrackingRunLoopMode模式下,這個模式下沒有添加NSTimer,因此咱們的NSTimer就不工做了。
    • 3.但當咱們鬆開鼠標的時候,RunLoop就結束UITrackingRunLoopMode模式,又切換回NSDefaultRunLoopMode模式,因此NSTimer就又開始正常工做了
    • 4.咱們能夠把NSTimer也添加到UITrackingRunLoopMode模式下,或者開啓新的線程,把NSTimer添加到子線程的Runloop中,這樣就能夠解決滑動時NSTimer失效的問題
  • 蘋果是如何實現Autorelease Pool的?

    • autorelease

      一個被autorelease修飾的對象會被加到最近的autoreleasePool中,當這個autoreleasePool自身drain的時候,其中的autoreleased對象會被release

    • autoreleasePool是怎麼實現的?

      1.AutoreleasePool並無單獨的結構,而是由若干個AutoreleasePoolPage以雙向鏈表的形式組合而成

      2.AutoreleasePoolPage對象會記錄autorelease對象地址

      3.AutoreleasePool的操做時經過如下這幾個函數實現的:objc_autoreleasepoolPush,objc_autoreleasepoolPop,objc_autorelease

      4.Autorelease對象是在當前的runloop迭代結束時釋放的,而它可以釋放的緣由是系統在每一個runloop迭代中都加入了autoreleasePush和Pop

      推薦你們閱讀這篇黑幕背後的Autorelease

類結構

  • isa指針?(對象的isa,類對象的isa,元類的isa都要說)

    • 上面題目中寫了類通過編譯後的結構體,其中包含有isa指針,在OC中類也是一種對象,它屬於元類metaClasss,對象的isa指針指向類,類的isa指針指向元類,元類的isa指針指向父類的元類,一直到根元類,最後根元類的isa指針指向了自身,如圖:
  • 類方法和實例方法有什麼區別?

    • 調用方式不一樣,類方法由類名直接調用,實例方法由該類生成的對象調用。緣由是類方法是在元類結構體的methodLists裏面,而實例方法位於類結構體的methodLists中。
    • 類方法不能使用該類的屬性,實例方法可使用屬性
    • 類方法中不能調用實例方法,而實例方法中能夠調用類方法
    • 類方法中self表明類自己,實例方法中self表明實例對象自己
  • 介紹一下分類,能用分類作什麼?內部是如何實現的?它爲何會覆蓋掉原來的方法?

      • 擴展已有的類
      • 分散原類的實現
      • 聲明私有方法
      • 模擬多繼承
      • 公開framework的部分私有方法
    • 分類通過編譯後也會成爲一個結構體:

      struct category_t {
          const char *name; // 類名
          classref_t cls;   // 分類所屬的類
          struct method_list_t *instanceMethods;  // 實例方法列表
          struct method_list_t *classMethods;     // 類方法列表
          struct protocol_list_t *protocols;      // 遵循的協議列表
          struct property_list_t *instanceProperties; // 屬性列表
      };複製代碼

      在運行時,category會被附加到類上面,包括把category的實例方法、協議以及屬性添加到類上和category的類方法和協議添加到類的metaclass上。

    • category的方法沒有徹底替換掉原來類已經有的方法,也就是說若是category和原來類都有methodA,那麼category附加完成以後,類的方法列表裏會有兩個methodA,category的方法被放到了新方法列表的前面,而原來類的方法被放到了新方法列表的後面,這也就是咱們日常所說的category的方法會「覆蓋」掉原來類的同名方法,這是由於運行時在查找方法的時候是順着方法列表的順序查找的,它只要一找到對應名字的方法,就會罷休^_^,卻不知後面可能還有同樣名字的方法。

      推薦你們閱讀美團出品的深刻理解Objective-C:Category,我是知識的搬運工~ 也歡迎你們給我推薦高質量的文章!獨樂樂不如衆樂樂~

  • 運行時能增長成員變量麼?能增長屬性麼?若是能,如何增長?若是不能,爲何?

    • class_addIvar給指定的類添加成員變量,可是不能爲已經生成的類添加,運行時規定,只能在objc_allocateClassPairobjc_registerClassPair兩個函數之間爲類添加變量。緣由在上面的題目中有過解釋。
    • class_addProperty給指定的類添加屬性,能夠成功添加了屬性可是不能用點調用法調用,能夠利用KVC/關聯方式來該表這個屬性的值
    • runtimeobjc_setAssociatedObjectobjc_getAssociatedObject方法來實現關聯,給分類添加屬性就是利用這個方法實現的。
  • objc中向一個nil對象發送消息將會發生什麼?(返回值是對象,是標量,結構體)

    • 向 nil 發送消息並不會引發程序crash,只是在運行時不會有任何做用。可是對[NSNull null]對象發送消息時,是會crash的。
    • 當方法返回值爲對象的時候, 給nil發消息返回nil
    • 當方法返回值爲結構體的時候,給nil發消息返回0,結構體中的各個參數也是0
    • 當方法返回值爲指針類型的時候, 給nil發消息返回0

參考博客:

招聘一個靠譜的iOS

深刻研究 Block 捕獲外部變量和 __block 實現原理

深刻理解 Runloop

深刻理解Objective-C:Category

黑幕背後的Autorelease

相關文章
相關標籤/搜索