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

Toga21發表於2018-03-05

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

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

  • Block

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

    • 簡單的來講,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型別變數,如下:

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

      以上解釋了匿名函式,現在來解釋一下帶有自動變數

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

      value結果為 10 。在block中,block表示式截獲所使用的自動變數的值,即儲存該變數的瞬間值,所以在執行了block後,改變block外自動變數的值,並不會影響block執行時自動變數的值。

    • 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);
      }
      複製程式碼

      此時會出現警告:

      iOS常見面試題(block,runtime,runloop,類結構)附參考答案
      這時,可以用__weak修飾self,避免迴圈引用:
      iOS常見面試題(block,runtime,runloop,類結構)附參考答案

    • 介面反向傳值

      //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.如果還找不到,就要開始進入訊息轉發流程

      如下圖所示:

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

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

    • 當呼叫某物件上某個方法,而該物件並沒有實現這個方法的時候, 可以通過訊息轉發進行解決。

    • 訊息轉發步驟如下:

      1. objc執行時會呼叫 +resolveInstanceMethod:或者 +resolveClassMethod:,讓我們有機會提供一個函式實現。如果你新增了函式,那執行時系統就會重新啟動一次訊息傳送的過程,否則,執行時就會移到下一步,訊息轉發(Message Forwarding)。

      2. 呼叫forwardingTargetForSelector:方法,嘗試找到一個能響應該訊息的物件。如果獲取到,則直接轉發給它。如果返回了nil,繼續下面的動作。

      3. 呼叫methodSignatureForSelector:方法,嘗試獲得一個方法簽名。如果獲取不到,則直接呼叫doesNotRecognizeSelector丟擲異常。如果返回了一個方法簽名,Runtime就會建立一個NSInvocation物件,然後繼續進行第四步

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

      如圖所示:

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

  • 能否向編譯後得到的類中增加例項變數?能否向執行時建立的類中新增例項變數?為什麼?

    • 不能向編譯後得到的類中增加例項變數
    • 能向執行時建立的類中新增例項變數
    • 因為編譯後的類已經註冊在 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包含了哪些東西,如下圖所示:

      iOS常見面試題(block,runtime,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指標指向了自身,如圖:
      iOS常見面試題(block,runtime,runloop,類結構)附參考答案
  • 類方法和例項方法有什麼區別?

    • 呼叫方式不同,類方法由類名直接呼叫,例項方法由該類生成的物件呼叫。原因是類方法是在元類結構體的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

相關文章