答面試題·答J_Knight_《2017年5月iOS招人心得(附面試題)》中的面試題(二)

weixin_33809981發表於2018-04-14

離職找工作中,刷一刷網上的面試題。原文連結

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

block是什麼呢?

block是能夠截獲自動變數(區域性變數)的匿名函式。寫法如下:

^int (int count) {
    return count + 1;
}

^{
    printf("Blocks\n");
}
複製程式碼

block的本質是什麼呢?

定義一個簡單的block

int main(int argc, const char * argv[]) {

    void (^block)() = ^{
        printf("Hello World!");
    };
    
    block();
    
    return 0;
}
複製程式碼

使用Clang命令

clang -rewrite-objc main.m
複製程式碼

得到結果

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("Hello World!");}
複製程式碼

你定義完block之後,其實是建立了一個函式,在建立結構體的時候把函式的指標一起傳給了block,所以之後可以拿出來呼叫。

其實,Block 是轉化為 Block 結構體型別的自動變數,型別定義如下。通過void *isa;,可以知道 Block 也是一個 OC 的物件,這就是 Block 的本質。

一共有幾種block?

根據isa指標,block一共有3種型別的block

* _NSConcreteGlobalBlock 全域性靜態,儲存在資料.data 區域
* _NSConcreteStackBlock 儲存在棧中,出函式作用域就銷燬
* _NSConcreteMallocBlock 儲存在堆中,retainCount == 0銷燬
複製程式碼

這幾種block是什麼情況下生成的

遇到一個Block,我們怎麼判斷這個Block的儲存位置呢?

  • 定義在函式外面的block,或者block不訪問外界變數(包括棧中和堆中的變數)為全域性block。例如
typedef int (^blk_t)(int);
blk_t gBlk = ^(int count) {return count;};
int test(){
    for(int i=0;i<10;i++){
        blk_t blk = ^(int count) {return count;};
    }
}
複製程式碼
  • Block訪問外界變數

    • MRC 環境下:訪問外界變數的 Block 預設儲存棧中。
    • ARC 環境下:訪問外界變數的 Block 預設儲存在堆中(實際是放在棧區,然後ARC情況下自動又拷貝到堆區),自動釋放。

引用於這篇文章

ibireme大神寫的關於block的這篇文章

關於block的詳解這篇文章也寫得很好。

24. 為什麼在預設情況下無法修改被block捕獲的變數? __block都做了什麼?

因為變數被拷貝的到block結構體中了。所以不能修改。對於用 __block 修飾的外部變數引用,block 是複製其引用地址來實現訪問的。block可以修改__block 修飾的外部變數的值。

clang程式碼

__block int val = 10;
轉換成
__Block_byref_val_0 val = {
    0,
    &val,
    0,
    sizeof(__Block_byref_val_0),
    10
};
複製程式碼

會發現一個區域性變數加上__block修飾符後竟然跟block一樣變成了一個__Block_byref_val_0結構體型別的自動變數例項!

23. 模擬一下迴圈引用的一個情況?block實現介面反向傳值如何實現?

迴圈引用

self.someBlock = ^{
    [self dosomething];
};
複製程式碼

block實現介面反向傳值如何實現

這個沒明白什麼意思。難道是這樣?

@interface OneCell : UITableViewCell 
@property (noaotomic,copy)^someAction(OneCell *cell , Action type);
@end
複製程式碼

24. objc在向一個物件傳送訊息時,發生了什麼?

簡單的說大概是:

  1. 判斷該物件是否為nil,為nil則返回nil
  2. 不為nil,就從該物件的isa指標找到該物件的類
  3. 從類的cache裡面查詢方法,如果命中則呼叫該方法
  4. 未命中則從類的方法列表裡面查詢
  5. 依然未命中,則從父類,父類的父類...方法列表裡面查詢
  6. 還是未命中則走訊息轉發機制
    1. 呼叫+(BOOL)resolveInstanceMethord:(SEL)sel;
    2. 呼叫+(BOOL)forwardingTargetForSelector:(SEL)sel
    3. 嗲用+(void)methodSignatureForSelector:(SEL)sel
    4. 呼叫+(void)forwardInvocation:(NSInvocation *)invocation

具體的可以看這篇文章,他從底層程式碼分析了Objective-C 訊息傳送與轉發機制原理

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

當一個類未實現呼叫的方法的時候就會報unrecognized selector。要解決這個問題可以從上一題的訊息轉發機制入手。

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

不能向編譯後的類增加例項變數。但是可以用class_addIvar向執行時建立的類新增例項變數。引用蘋果官方的話說:

This function may only be called after objc_allocateClassPair and before objc_registerClassPair. Adding an instance variable to an existing class is not

給一個類新增變數會更改該類的例項的記憶體佈局。所以,不能動態的修改類的例項變數。

27. runtime如何實現weak變數的自動置nil?

網上有很多種方法。我想得比較簡單。用一個NSTableMap來裝屬性變數。

@interface ViewController(addWeak)
@property(nonatomic,readonly)NSMapTable *propertyMap;
@property(nonatomic,weak)NSObject *someObj;
@end

@implementation ViewController(addWeak)

static NSString *mapKey = @"mapKey";
-(NSMapTable *)propertyMap{
    NSMapTable *table = objc_getAssociatedObject(self, &mapKey);
    if (table==nil) {
        table = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory capacity:1];
    }
    return table;
}

-(NSObject *)someObj{
    return [self.propertyMap objectForKey:@"someObj"];
}

-(void)setSomeObj:(NSObject *)someObj{
    if (someObj == nil) {
        [self.propertyMap removeObjectForKey:@"someObj"];
    }else{
        [self.propertyMap setObject:someObj forKey:@"someObj"];
    }
}

@end
複製程式碼

28. 給類新增一個屬性後,在類結構體裡哪些元素會發生變化?

bestswifter大神的這篇關於Catgory的文章第二段有詳細說明OC2.0中Class的結構。

struct objc_object {
    Class isa  OBJC_ISA_AVAILABILITY;
};

struct objc_class : objc_object {
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags

    class_rw_t *data() { 
        return bits.data();
    }
};
複製程式碼

其中class_rw_t結構體如下:

struct class_rw_t {
    uint32_t flags;
    uint32_t version;

    const class_ro_t *ro;

    method_array_t methods;
    property_array_t properties;
    protocol_array_t protocols;
}
複製程式碼

在新加一個property的時候,objc_objectbit欄位會新增成員變數,data欄位中methors會增加響應的get,set方法,properties陣列會新增元素。

29. runloop是來做什麼的?runloop和執行緒有什麼關係?主執行緒預設開啟了runloop麼?子執行緒呢?

runloop是來做什麼的

runploop是為了讓執行緒一直接受任務不退出的。

runloop和執行緒有什麼關係

Runloop和執行緒是一一對應的。1個執行緒只有1個Runloop。他們的關係是放在一個全域性的Dictionary裡面的。執行緒剛建立時沒有 RunLoop,如果你不主動獲取,那它一直都不會有。蘋果不允許直接建立 RunLoop,它只提供了兩個自動獲取的函式:CFRunLoopGetMain()CFRunLoopGetCurrent()RunLoop 的建立是發生在第一次獲取時,RunLoop 的銷燬是發生線上程結束時。 RunLoop只能在一個執行緒的內部獲取。

主執行緒預設開啟了runloop麼?子執行緒呢?

主執行緒預設開啟了runloop。子執行緒沒有。

30. runloop的mode是用來做什麼的?有幾種mode?

在ibireme大神的深入理解RunLoop有詳細的介紹runloop。一下文字也是引用自這篇文章。

CFRunLoopMode 和 CFRunLoop 的結構大致如下:

struct __CFRunLoopMode {
    CFStringRef _name;            // Mode Name, 例如 @"kCFRunLoopDefaultMode"
    CFMutableSetRef _sources0;    // Set
    CFMutableSetRef _sources1;    // Set
    CFMutableArrayRef _observers; // Array
    CFMutableArrayRef _timers;    // Array
    ...
};
 
struct __CFRunLoop {
    CFMutableSetRef _commonModes;     // Set
    CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer>
    CFRunLoopModeRef _currentMode;    // Current Runloop Mode
    CFMutableSetRef _modes;           // Set
    ...
};
複製程式碼

這裡有個概念叫 “CommonModes”:一個 Mode 可以將自己標記為”Common”屬性(通過將其 ModeName 新增到 RunLoop 的 “commonModes” 中)。每當 RunLoop 的內容發生變化時,RunLoop 都會自動將 _commonModeItems 裡的 Source/Observer/Timer 同步到具有 “Common” 標記的所有Mode裡。

應用場景舉例:主執行緒的 RunLoop 裡有兩個預置的 Mode:kCFRunLoopDefaultMode 和 UITrackingRunLoopMode。這兩個 Mode 都已經被標記為”Common”屬性。DefaultMode 是 App 平時所處的狀態,TrackingRunLoopMode 是追蹤 ScrollView 滑動時的狀態。當你建立一個 Timer 並加到 DefaultMode 時,Timer 會得到重複回撥,但此時滑動一個TableView時,RunLoop 會將 mode 切換為 TrackingRunLoopMode,這時 Timer 就不會被回撥,並且也不會影響到滑動操作。

有時你需要一個 Timer,在兩個 Mode 中都能得到回撥,一種辦法就是將這個 Timer 分別加入這兩個 Mode。還有一種方式,就是將 Timer 加入到頂層的 RunLoop 的 “commonModeItems” 中。”commonModeItems” 被 RunLoop 自動更新到所有具有”Common”屬性的 Mode 裡去。

CFRunLoop對外暴露的管理 Mode 介面只有下面2個:

CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);
CFRunLoopRunInMode(CFStringRef modeName, ...);
複製程式碼

ode 暴露的管理 mode item 的介面有下面幾個:

CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
複製程式碼

你只能通過 mode name 來操作內部的 mode,當你傳入一個新的 mode name 但 RunLoop 內部沒有對應 mode 時,RunLoop會自動幫你建立對應的 CFRunLoopModeRef。對於一個 RunLoop 來說,其內部的 mode 只能增加不能刪除。

蘋果公開提供的 Mode 有兩個:kCFRunLoopDefaultMode (NSDefaultRunLoopMode) 和 UITrackingRunLoopMode,你可以用這兩個 Mode Name 來操作其對應的 Mode。

同時蘋果還提供了一個操作 Common 標記的字串:kCFRunLoopCommonModes (NSRunLoopCommonModes),你可以用這個字串來操作 Common Items,或標記一個 Mode 為 “Common”。使用時注意區分這個字串和其他 mode name。

31. 為什麼把NSTimer物件以NSDefaultRunLoopMode(kCFRunLoopDefaultMode)新增到主執行迴圈以後,滑動scrollview的時候NSTimer卻不動了?

因為當我們滑動ScrollView的時候,當前runloop的model為UITrackingRunLoopMode。所以在NSDefaultRunLoopMode下的NSTimer就不動了。

要解決這個問題。可以用以下幾種方式:

  • 把timer的model新增NSRunLoopCommonModes中。
  • 把timer新增到子執行緒的runloop中。

32. 蘋果是如何實現Autorelease Pool的?

關於Autorelease Poolsunny大神的的黑幕背後的Autorelease介紹得很好。以下回答也是從這篇文章找的。

AutoreleasePoolPage

  • AutoreleasePool並沒有單獨的結構,而是由若干個AutoreleasePoolPage以雙向連結串列的形式組合而成(分別對應結構中的parent指標和child指標)
  • AutoreleasePool是按執行緒一一對應的(結構中的thread指標指向當前執行緒)
  • AutoreleasePoolPage每個物件會開闢4096位元組記憶體(也就是虛擬記憶體一頁的大小),除了上面的例項變數所佔空間,剩下的空間全部用來儲存autorelease物件的地址
  • 上面的id *next指標作為遊標指向棧頂最新add進來的autorelease物件的下一個位置
  • 一個AutoreleasePoolPage的空間被佔滿時,會新建一個AutoreleasePoolPage物件,連線連結串列,後來的autorelease物件在新的page加入

釋放時機

在沒有手加Autorelease Pool的情況下,Autorelease物件是在當前的runloop迭代結束時釋放的,而它能夠釋放的原因是系統在每個runloop迭代中都加入了自動釋放池PushPop.

每當進行一次objc_autoreleasePoolPush呼叫時,runtime向當前的AutoreleasePoolPage中add進一個哨兵物件,值為0(也就是個nil),那麼這一個page就變成了下面的樣子:

objc_autoreleasePoolPush的返回值正是這個哨兵物件的地址,被objc_autoreleasePoolPop(哨兵物件)作為入參,於是:

根據傳入的哨兵物件地址找到哨兵物件所處的page 在當前page中,將晚於哨兵物件插入的所有autorelease物件都傳送一次- release訊息,並向回移動next指標到正確位置 補充2:從最新加入的物件一直向前清理,可以向前跨越若干個page,直到哨兵所在的page 剛才的objc_autoreleasePoolPop執行後,最終變成了下面的樣子:

33. isa指標?(物件的isa,類物件的isa,元類的isa都要說)

物件的isa指向的是類,類的isa指向的元類,元類的isa指向的NSObject

34. 類方法和例項方法有什麼區別?

不是很明白這題的意思。

  • 類方法用類呼叫,例項方法需要例項呼叫
  • 類方法存在於元類的方法列表裡,例項方法存在於類的方法列表裡

35. 介紹一下分類,能用分類做什麼?內部是如何實現的?它為什麼會覆蓋掉原來的方法?

分類的作用

  • 可以把類的實現分開在幾個不同的檔案裡面。
    • 可以減少單個檔案的體積
    • 可以把不同的功能組織到不同的category裡
    • 可以由多個開發者共同完成一個類
    • 可以按需載入想要的category 等等。
  • 宣告私有方法

分類的結構

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; // 屬性列表
};
複製程式碼

分類中的方法是新增到原類方法列表的前面的,並不會替換掉原來的方法。所以就算新增了分類以後依然可以呼叫原方法

Class currentClass = [MyClass class];
MyClass *my = [[MyClass alloc] init];

if (currentClass) {
    unsigned int methodCount;
    Method *methodList = class_copyMethodList(currentClass, &methodCount);
    IMP lastImp = NULL;
    SEL lastSel = NULL;
    for (NSInteger i = 0; i < methodCount; i++) {
        Method method = methodList[i];
        NSString *methodName = [NSString stringWithCString:sel_getName(method_getName(method)) 
                                        encoding:NSUTF8StringEncoding];
        if ([@"printName" isEqualToString:methodName]) {
            lastImp = method_getImplementation(method);
            lastSel = method_getName(method);
        }
    }
    typedef void (*fn)(id,SEL);

    if (lastImp != NULL) {
        fn f = (fn)lastImp;
        f(my,lastSel);
    }
    free(methodList);
}
複製程式碼

關於分類美團的這篇文章寫得很詳細

36. 執行時能增加成員變數麼?能增加屬性麼?如果能,如何增加?如果不能,為什麼?

這題和第26題有些重複吧。執行時不能動態增加成員變數,能動態增加屬性。動態增加屬性的時候需要用objc_setAssociatedObjectobjc_getAssociatedObject來關聯屬性所對應的變數。

37. objc中向一個nil物件傳送訊息將會發生什麼?(返回值是物件,是標量,結構體)

什麼也不會發生。返回值是物件的話,就返回nil。返回標量的就是預設值,比如CGRect就為(0,0,0,0)。返回值是結構體的話,就返回結構體體的初始值。

相關文章