1. 程式人生 > >iOS 面試題--轉自唐巧

iOS 面試題--轉自唐巧

iOS 面試題(一)尋找最近公共 View

題目:找出兩個 UIView 的最近的公共 View,如果不存在,則輸出 nil 。
分析:這其實是資料結構裡面的找最近公共祖先的問題。

一個UIViewController中的所有view之間的關係其實可以看成一顆樹,UIViewController的view變數是這顆樹的根節點,其它的view都是根節點的直接或間接子節點。

所以我們可以通過 view 的 superview 屬性,一直找到根節點。需要注意的是,在程式碼中,我們還需要考慮各種非法輸入,如果輸入了 nil,則也需要處理,避免異常。以下是找到指定 view 到根 view 的路徑程式碼:

+ (NSArray *)superViews:(UIView *)view {
    if (view == nil) {
        return @[];
    }
    NSMutableArray *result = [NSMutableArray array];
    while (view != nil) {
        [result addObject:view];
        view = view.superview;
    }
    return [result copy];
}

然後對於兩個 view A 和 view B,我們可以得到兩個路徑,而本題中我們要找的是這裡面最近的一個公共節點。

一個簡單直接的辦法:拿第一個路徑中的所有節點,去第二個節點中查詢。假設路徑的平均長度是 N,因為每個節點都要找 N 次,一共有 N 個節點,所以這個辦法的時間複雜度是 O(N^2)。

+ (UIView *)commonView_1:(UIView *)viewA andView:(UIView *)viewB {
    NSArray *arr1 = [self superViews:viewA];
    NSArray *arr2 = [self superViews:viewB];
    for (NSUInteger i = 0; i < arr1.count; ++i) {
        UIView
*targetView = arr1[i]; for (NSUInteger j = 0; j < arr2.count; ++j) { if (targetView == arr2[j]) { return targetView; } } } return nil; }

一個改進的辦法:我們將一個路徑中的所有點先放進 NSSet 中。因為 NSSet 的內部實現是一個 hash 表,所以查詢元素的時間複雜度變成了 O(1),我們一共有 N 個節點,所以總時間複雜度優化到了 O(N)。

+ (UIView *)commonView_2:(UIView *)viewA andView:(UIView *)viewB {
    NSArray *arr1 = [self superViews:viewA];
    NSArray *arr2 = [self superViews:viewB];
    NSSet *set = [NSSet setWithArray:arr2];
    for (NSUInteger i = 0; i < arr1.count; ++i) {
        UIView *targetView = arr1[i];
        if ([set containsObject:targetView]) {
            return targetView;
        }
    }
    return nil;
}

除了使用 NSSet 外,我們還可以使用類似歸併排序的思想,用兩個「指標」,分別指向兩個路徑的根節點,然後從根節點開始,找第一個不同的節點,第一個不同節點的上一個公共節點,就是我們的答案。程式碼如下:

/* O(N) Solution */
+ (UIView *)commonView_3:(UIView *)viewA andView:(UIView *)viewB {
    NSArray *arr1 = [self superViews:viewA];
    NSArray *arr2 = [self superViews:viewB];
    NSInteger p1 = arr1.count - 1;
    NSInteger p2 = arr2.count - 1;
    UIView *answer = nil;
    while (p1 >= 0 && p2 >= 0) {
        if (arr1[p1] == arr2[p2]) {
            answer = arr1[p1];
        }
        p1--;
        p2--;
    }
    return answer;
}

我們還可以使用 UIView 的 isDescendant 方法來簡化我們的程式碼,不過這樣寫的話,時間複雜度應該也是 O(N^2) 的。lexrus 提供瞭如下的 Swift 版本的程式碼:

/// without flatMap
extension UIView {    
func commonSuperview(of view: UIView) -> UIView? {        
    if let s = superview {            
       if view.isDescendant(of: s) {                
                 return s
            } else {                
                 return s.commonSuperview(of: view)
            }
        }        
       return nil
    }
}

特別地,如果我們利用 Optinal 的 flatMap 方法,可以將上面的程式碼簡化得更短,基本上算是一行程式碼搞定。怎麼樣,你學會了嗎?

extension UIView {
    func commonSuperview(of view: UIView) -> UIView? {
        return superview.flatMap { 
           view.isDescendant(of: $0) ? 
             $0 : $0.commonSuperview(of: view) 
        }
    }
}

iOS 面試題(二)什麼時候在 block 中不需要使用 weakSelf

問題:我們知道,在使用 block 的時候,為了避免產生迴圈引用,通常需要使用 weakSelf 與 strongSelf,寫下面這樣的程式碼:

__weak typeof(self) weakSelf = self;
[self doSomeBlockJob:^{
    __strong typeof(weakSelf) strongSelf = weakSelf;
    if (strongSelf) {
        ...
    }
}];

那麼請問:什麼時候在 block裡面用self,不需要使用weakself?

當block本身不被self 持有,而被別的物件持有,同時不產生迴圈引用的時候,就不需要使用weakself了。最常見的程式碼就是UIView的動畫程式碼,我們在使用UIView animateWithDuration:animations方法 做動畫的時候,並不需要使用weakself,因為引用持有關係是:

UIView 的某個負責動畫的物件持有block,block 持有了self因為 self 並不持有 block,所以就沒有迴圈引用產生,因為就不需要使用 weak self 了。

[UIView animateWithDuration:0.2 animations:^{
    self.alpha = 1;
}];

當動畫結束時,UIView會結束持有這個 block,如果沒有別的物件持有block的話,block 物件就會釋放掉,從而 block會釋放掉對於 self 的持有。整個記憶體引用關係被解除。

iOS 面試題(三)什麼時候在 block 中不需要使用 weakSelf

我們知道,在使用 block 的時候,為了避免產生迴圈引用,通常需要使用 weakSelf 與 strongSelf,寫下面這樣的程式碼:

__weak typeof(self) weakSelf = self;
[self doSomeBackgroundJob:^{
    __strong typeof(weakSelf) strongSelf = weakSelf;
    if (strongSelf) {
        ...
    }
}];

那麼請問:為什麼 block 裡面還需要寫一個 strong self,如果不寫會怎麼樣?

在 block 中先寫一個 strong self,其實是為了避免在 block 的執行過程中,突然出現 self 被釋放的尷尬情況。通常情況下,如果不這麼做的話,還是很容易出現一些奇怪的邏輯,甚至閃退。

我們以AFNetworking中的AFNetworkReachabilityManager.m的一段程式碼舉例:

__weak __typeof(self)weakSelf = self;
AFNetworkReachabilityStatusBlock callback = ^(AFNetworkReachabilityStatus status) {
    __strong __typeof(weakSelf)strongSelf = weakSelf;
    strongSelf.networkReachabilityStatus = status;
    if (strongSelf.networkReachabilityStatusBlock) {strongSelf.networkReachabilityStatusBlock(status);
    }
};

如果沒有strongSelf的那行程式碼,那麼後面的每一行程式碼執行時,self都可能被釋放掉了,這樣很可能造成邏輯異常。

特別是當我們正在執行 strongSelf.networkReachabilityStatusBlock(status); 這個 block閉包時,如果這個 block 執行到一半時 self 釋放,那麼多半情況下會 Crash。

這裡有一篇文章詳細解釋了這個問題:點選檢視文章
昨天的讀者中,拓荒者 和 陳祥龍 同學在評論中也正確回答出了本題。

拓荒者:
1.在block裡使用strongSelf是防止在block執行過程中self被釋放。 2.可以通過在執行完block程式碼後手動把block置為nil來打破引用迴圈,AFNetworking就是這樣處理的,避免使用者不瞭解引用迴圈造成記憶體洩露。實際業務中暫時沒遇到這種需求,請巧哥指點什麼情況下會有這種需求。

陳祥龍:
strongSelf 一般是在為了避免 block 回撥時 weak Self變成了nil ,非同步執行一些操作時可能會出現這種情況,不知道我說得對不對。因業務需要不能使用weakSelf 這種情況還真沒遇到過

另外,還有讀者提了兩個有意思的問題,大家可以思考一下:
Yuen 提問:“陣列” 和 “字典” 的 enumeratXXXUsingBlock: 是否要使用 weakSelf 和 strongSelf 呢?
瀟湘雨同學提問:block 裡 strong self 後,block 不是也會持有 self 嗎?而 self 又持有 block ,那不是又迴圈引用了?

iOS 面試題(四):block 什麼時候需要構造迴圈引用

問題:有沒有這樣一個需求場景,block 會產生迴圈引用,但是業務又需要你不能使用 weak self? 如果有,請舉一個例子並且解釋這種情況下如何解決迴圈引用問題。

答案:需要不使用 weak self 的場景是:你需要構造一個迴圈引用,以便保證引用雙方都存在。比如你有一個後臺的任務,希望任務執行完後,通知另外一個例項。在我們開源的 YTKNetwork 網路庫的原始碼中,就有這樣的場景。

在 YTKNetwork 庫中,我們的每一個網路請求 API 會持有回撥的 block,回撥的 block 會持有 self,而如果 self 也持有網路請求 API 的話,我們就構造了一個迴圈引用。雖然我們構造出了迴圈引用,但是因為在網路請求結束時,網路請求 API 會主動釋放對 block 的持有,因此,整個迴圈鏈條被解開,迴圈引用就被打破了,所以不會有記憶體洩漏問題。程式碼其實很簡單,如下所示:

//  YTKBaseRequest.m
- (void)clearCompletionBlock {
    // nil out to break the retain cycle.
    self.successCompletionBlock = nil;
    self.failureCompletionBlock = nil;
}

總結來說,解決迴圈引用問題主要有兩個辦法:

第一個辦法是「事前避免」,我們在會產生迴圈引用的地方使用 weak 弱引用,以避免產生迴圈引用。
第二個辦法是「事後補救」,我們明確知道會存在迴圈引用,但是我們在合理的位置主動斷開環中的一個引用,使得物件得以回收。

iOS 面試題(五):weak 的內部實現原理

問題:weak 變數在引用計數為0時,會被自動設定成 nil,這個特性是如何實現的?
答案:在 Friday QA 上,有一期專門介紹 weak的實現原理。

《Objective-C高階程式設計》一書中也介紹了相關的內容。
簡單來說,系統有一個全域性的 CFMutableDictionary 例項,來儲存每個物件的 weak 指標列表,因為每個物件可能有多個 weak 指標,所以這個例項的值是 CFMutableSet 型別。

剩下我們要做的,就是在引用計數變成 0 的時候,去這個全域性的字典裡面,找到所有的 weak 指標,將其值設定成 nil。如何做到這一點呢?Friday QA 上介紹了一種類似 KVO 實現的方式。當物件存在 weak 指標時,我們可以將這個例項指向一個新建立的子類,然後修改這個子類的 release 方法,在 release 方法中,去從全域性的 CFMutableDictionary 字典中找到所有的 weak 物件,並且設定成 nil。我摘抄了 Friday QA 上的實現的核心程式碼,如下:

Class subclass = objc_allocateClassPair(class, newNameC, 0);
Method release = class_getInstanceMethod(class, @selector(release));
Method dealloc = class_getInstanceMethod(class, @selector(dealloc));
class_addMethod(subclass, @selector(release), (IMP)CustomSubclassRelease, method_getTypeEncoding(release));
class_addMethod(subclass, @selector(dealloc), (IMP)CustomSubclassDealloc, method_getTypeEncoding(dealloc));
objc_registerClassPair(subclass);

當然,這並不代表蘋果官方是這麼實現的,因為蘋果的這部分程式碼並沒有開源。《Objective-C高階程式設計》一書中介紹了 GNUStep 專案中的開原始碼,思想也是類似的。所以我認為雖然實現細節會有差異,但是大致的實現思路應該差別不大。

iOS 面試題(六):自己寫的 view 成員,應該用 weak 還是 strong?

問題:我們知道,從 Storyboard 往編譯器拖出來的 UI 控制元件的屬性是 weak 的,如下所示

@property (weak, nonatomic) IBOutlet UIButton *myButton;

那麼,如果有一些 UI 控制元件我們要用程式碼的方式來建立,那麼它應該用 weak 還是 strong 呢?為什麼?

答案:這是一道有意思的問題,這個問題是我當時和 Lancy 一起寫猿題庫 App 時產生的一次小爭論。簡單來說,這道題並沒有標準答案,但是答案背後的解釋卻非常有價值,能夠看出一個人對於引用計數,對於 view 的生命週期的理解是否到位。
從昨天的評論上,我們就能看到一些理解非常不到位的解釋,例如:

@spume 說:Storyboard 拖線使用 weak 是為了規避出現迴圈引用的問題。

這個理解是錯誤的,Storyboard 拖出來的控制元件即使是 strong 的,也不會有迴圈引用問題。
我認為 UI 控制元件用預設用 weak,根源還是蘋果希望只有這些 UI 控制元件的父 View 來強引用它們,而 ViewController 只需要強引用 ViewController.view 成員,則可以間接持有所有的 UI 控制元件。這樣有一個好處是:在以前,當系統收到 Memory Warning 時,會觸發 ViewController 的 viewDidUnload 方法,這樣的弱引用方式,可以讓整個 view 整體都得到釋放,也更方便重建時整體重新構造。
但是首先 viewDidUnload 方法在 iOS 6 開始就被廢棄掉了,蘋果用了更簡單有效地方式來解決記憶體警告時的檢視資源釋放,具體如何做的呢?嗯,這個可以當作某一期的面試題展開介紹。總之就是,除非你特殊地操作 view 成員,ViewController.view 的生命期和 ViewController 是一樣的了。
所以在這種情況下,其實 UI 控制元件是不是 weak 其實關係並不大。當 UI 控制元件是 weak 時,它的引用計數是 1,持有它的是它的 superview,當 UI 控制元件是 strong 時,它的引用計數是 2,持有它的有兩個地方,一個是它的 superview,另一個是這個 strong 的指標。UI 控制元件並不會持有別的物件,所以,不管是手寫程式碼還是 Storyboard,UI 控制元件是 strong 都不會有迴圈引用的。
那麼回到我們的最初的問題,自己寫的 view 成員,應該用 weak 還是 strong?我個人覺得應該用 strong,因為用 weak 並沒有什麼特別的優勢,加上上一篇面試題文章中,我們還看到,其實 weak 變數會有額外的系統維護開銷的,如果你沒有使用它的特別的理由,那麼用 strong 的話應該更好。
另外有讀者也提到,如果你要做 Lazy 載入,那麼你也只能選擇用 strong。
當然,如果你非要用 weak,其實也沒什麼問題,只需要注意在賦值前,先把這個物件用 addSubView 加到父 view 上,否則可能剛剛建立完,它就被釋放了。
在我心目中,這才是我喜歡的面試題,沒有標準答案,每種方案各有各的特點,面試者能夠足夠分清楚每種方案的優缺點,結合具體的場景做選擇,這才是優秀的面試者。

1.懶載入的物件必須用strong的原因在於,如果使用weak,物件沒有被沒有被強引用,過了懶載入物件就會被釋放掉。

iOS 面試題(七):為什麼 Objective-C 的方法呼叫要用方括號?

問題:為什麼 Objective-C 的方法呼叫要用方括號 [obj foo],而不是別的語言常常使用的點 obj.foo ?

答案:
首先要說的是,Objective-C 的歷史相當久遠,如果你查 wiki 的話,你會發現:Objective-C 和 C++ 這兩種語言的發行年份都是 1983 年。在設計之初,二者都是作為 C 語言的面向物件的接班人,希望成為事實上的標準。最後結果大家都知道了,C++ 最終勝利了,而 Objective-C 在之後的幾十年中,基本上變成了蘋果自己家玩的玩具。不過最終,由於 iPhone 的出現,Objective-C 迎來了第二春,在 TOBIE 語言排行榜上,從 20 名開外一路上升,排名曾經超越過 C++,達到了第三名(下圖),但是隨著 Swift 的出現,Objective-C 的排名則一路下滑。


TOBIE排行版

Objective-C 在設計之初參考了不少 Smalltalk 的設計,而訊息傳送則是向 Smalltalk 學來的。Objective-C 當時採用了方括號的形式來表示傳送訊息,為什麼沒有選擇用點呢?我個人覺得是,當時市面上並沒有別的面嚮物件語言的設計參考,而 Objective-C 「發明」了方括號的形式來給物件發訊息,而 C++ 則「發明」了用點的方式來 “發訊息”。有人可能會爭論說 C++ 的「點」並不是真正的發訊息,但是其實二者都是表示「呼叫物件所屬的成員函式」。
另外,有讀者評論說使用方括號的形式是為了向下相容 C 語言,我並不覺得中括號是唯一選擇,C++ 不也相容了 C 語言麼?Swift 不也可以呼叫 C 函式麼?
最終,其實是 C++ 的「發明」顯得更舒服一些,所以後來的各種語言都借鑑了 C++ 的這種設計,也包括 Objective-C 在內。Objective-C 2.0 版本中,引入了 dot syntax,即:
a = obj.foo 等價於 a = [obj foo]
obj.foo = 1 則等價於 [obj setFoo:1]
Objective-C 其實在設計之中確實是比較特立獨行的,除了方括號的函式呼叫方式外,還包括比較長的,可讀性很強的函式命名風格。
我個人並不討厭 Objective-C 的這種設計,但是從 Swift 語言的設計來看,蘋果也開始放棄一些 Objective-C 的特點了,比如就去掉了方括號這種函式呼叫方式。

所以,回到我們的問題,我個人認為,答案就是:Objective-C 在 1983 年設計的時候,並沒有什麼有效的效仿物件,於是就發明了一種有特點的函式呼叫方式,現在看起來,這種方式比點操作符還是略遜一籌。

大多數語言一旦被設計好,就很難被再次修改,應該說 Objective-C 發明在 30 年前,還是非常優秀的,它的面向物件化設計得非常純粹,比 C++ 要全面得多,也比 C++ 要簡單得多。

iOS 面試題(八):實現一個巢狀陣列的迭代器

問題:
給你一個巢狀的 NSArray 資料,實現一個迭代器類,該類提供一個 next() 方法,可以依次的取出這個 NSArray 中的資料。
比如 NSArray 如果是 [1,[4,3],6,[5,[1,0]]], 則最終應該輸出:1, 4, 3, 6, 5, 1, 0 。
另外,實現一個 allObjects 方法,可以一次性取出所有元素。
給你一個巢狀的 NSArray 資料,實現一個迭代器類,該類提供一個 next() 方法,可以依次的取出這個 NSArray 中的資料。

解答:
本題的程式碼稍長,完整的程式碼我放在git上了,以下是講解。

先說第二問吧,第二問比較簡單:實現一個 allObjects 方法,可以一次性取出所有元素。
對於此問,我們可以實現一個遞迴函式,在函式中判斷陣列中的元素是否又是陣列,如果是的話,就遞迴呼叫自己,如果不是陣列,則加入到一個 NSMutableArray 中即可。下面是示例程式碼:

- (NSArray *)allObjects {
    NSMutableArray *result = [NSMutableArray array];
    [self fillArray:_originArray into:result];
    return result;
}

- (void)fillArray:(NSArray *)array into:(NSMutableArray *)result {
    for (NSUInteger i = 0; i < array.count; ++i) {
        if ([array[i] isKindOfClass:[NSArray class]]) {
            [self fillArray:array[i] into:result];
        } else {
            [result addObject:array[i]];
        }
    }
}

如果你還在糾結掌握遞迴有什麼意義的話,歡迎翻翻我半年前寫的另一篇文章:遞迴的故事(上)遞迴的故事(下)

接下來讓我們來看第一問,在同學的回覆中,我看到很多人用第二問的辦法,把陣列整個另外儲存一份,然後再記錄一個下標,每次返回其中一個。這個方法當然是可行的,但是大部分的迭代器通常都不會這麼實現。因為這麼實現的話,陣列需要整個複製一遍,空間複雜度是 O(N)。

所以,我個人認為本題第一問更好的解法是:
記錄下遍歷的位置,然後每次遍歷時更新位置。由於本題中元素是一個巢狀陣列,所以我們為了記錄下位置,就需要兩個變數:一個是當前正在遍歷的子陣列,另一個是這個陣列遍歷到的位置。

我在實現的時候,定義了一個名為 NSArrayIteratorCursor 的類來記錄這些內容,NSArrayIteratorCursor 的定義和實現如下:

@interface NSArrayIteratorCursor : NSObject

@property (nonatomic) NSArray *array;
@property (nonatomic) NSUInteger index;

@end

@implementation NSArrayIteratorCursor

- (id)initWithArray:(NSArray *)array {
    self = [super init];
    if (self) {
        _array = array;
        _index = 0;
    }
    return self;
}

@end

由於陣列在遍歷的時候可能產生遞迴,就像我們實現 allObjects 方法那樣。所以我們需要處理遞迴時的 NSArrayIteratorCursor 的儲存,我在實現的時候,拿陣列當作棧,來實現儲存遍歷時的狀態。

最終,我實現了一個迭代器類,名字叫 NSArrayIterator,用於最終提供 next 方法的實現。這個類有兩個私有變數,一個是剛剛說的那個棧,另一個是原陣列的引用。

@interface NSArrayIterator : NSObject

- (id)initWithArray:(NSArray *)array;
- (id)next;
- (NSArray *)allObjects;

@end

@implementation NSArrayIterator {
    NSMutableArray *_stack;
    NSArray *_originArray;
}

在初使化的時候,我們初始化遍歷位置的程式碼如下:

- (id)initWithArray:(NSArray *)array {
    self = [super init];
    if (self) {
        _originArray = array;
        _stack = [NSMutableArray array];
        [self setupStack];
    }
    return self;
}

- (void)setupStack {
    NSArrayIteratorCursor *c = [[NSArrayIteratorCursor alloc] initWithArray:_originArray];
    [_stack addObject:c];
}

接下來就是最關鍵的程式碼了,即實現 next 方法,在 next 方法的實現邏輯中,我們需要:

判斷棧是否為空,如果為空則返回 nil。
從棧中取出元素,看是否遍歷到了結尾,如果是的話,則出棧。
判斷第 2 步是否使棧為空,如果為空,則返回 nil。
終於拿到元素了,這一步判斷拿到的元素是否是陣列。
如果是陣列,則重新生成一個遍歷的 NSArrayIteratorCursor 物件,放到棧中。
重新從棧中拿出第一個元素,迴圈回到第 4 步的判斷。
如果到了這一步,說明拿到了一個非陣列的元素,這樣就可以把元素返回,同時更新索引到下一個位置。
以下是相關的程式碼,對於沒有演算法基礎的同學,可能讀起來還是比較累,其實我寫起來也不快,所以希望你能多理解一下,其實核心思想就是手工操作棧的入棧和出棧:

- (id)next {
    //  1. 判斷棧是否為空,如果為空則返回 nil。
    if ([_stack count] == 0) {
        return nil;
    }
    // 2. 從棧中取出元素,看是否遍歷到了結尾,如果是的話,則出棧。
    NSArrayIteratorCursor *c;
    c = [_stack lastObject];
    while (c.index == c.array.count && _stack.count > 0) {
        [_stack removeLastObject];
        c = [_stack lastObject];
    }
    // 3. 判斷第 2 步是否使棧為空,如果為空,則返回 nil。
    if (_stack.count == 0) {
        return nil;
    }
    // 4. 終於拿到元素了,這一步判斷拿到的元素是否是陣列。
    id item = c.array[c.index];
    while ([item isKindOfClass:[NSArray class]]) {
        c.index++;
        // 5. 如果是陣列,則重新生成一個遍歷的 NSArrayIteratorCursor 物件,放到棧中。
        NSArrayIteratorCursor *nc = [[NSArrayIteratorCursor alloc] initWithArray:item];
        [_stack addObject:nc];
        // 6. 重新從棧中拿出第一個元素,迴圈回到第 4 步的判斷。
        c = nc;
        item = c.array[c.index];
    }

    // 7. 如果到了這一步,說明拿到了一個非陣列的元素,這樣就可以把元素返回,同時更新索引到下一個位置。
    c.index++;
    return item;
}

在讀者回復中,聽榆大叔 和 yiplee 同學用了類似的做法,他們的程式碼在:
聽榆大叔 、yiplee

最終,我想說這個只是我個人想出來的解法,很可能不是最優的,甚至可能也有很多問題,比如,這個程式碼有很多可以進一步 challenge 的地方:

這個程式碼是執行緒安全的嗎?如果我們要實現一個執行緒安全的迭代器,應該怎麼做?
如果在使用迭代器的時候,陣列被修改了,會怎麼樣?
如何檢測在遍歷元素的時候,陣列被修改了?
如何避免在遍歷元素的時候,陣列被修改?
如果大家有想出更好的解法,歡迎留言告訴我。

【續】iOS 面試題(八):實現一個巢狀陣列的迭代器

昨天我的程式碼,有一個 Bug,就是我沒有處理好巢狀的陣列元素為空的情況,我寫了一個簡單的 TestCase,大家也可以試試自己的程式碼是否處理好了這種情況:

- (void)testEmptyArray {
    NSArray *arr = @[ @[ @[ ]], @[@[ @[ @[ ]]]]];
    NSArrayIterator *c = [[NSArrayIterator alloc] initWithArray:arr];
    XCTAssertEqualObjects(nil, [c next]);
    XCTAssertEqualObjects(nil, [c next]);
}

於是乎,我發現我的程式碼可以再優化一下,用遞迴的方式來處理空陣列的邏輯似乎是寫起來更簡單的,於是我優化之後的邏輯如下:

判斷棧是否為空,如果為空則返回 nil。
從棧中取出元素,看是否遍歷到了結尾,如果是的話,則出棧。
判斷第 2 步是否使棧為空,如果為空,則返回 nil。
終於拿到元素了,這一步判斷拿到的元素是否是陣列。
如果是陣列,則重新生成一個遍歷的 NSArrayIteratorCursor 物件,放到棧中,並且遞迴呼叫自己。
如果不是陣列,就把元素返回,同時更新索引到下一個位置。
整個程式碼也變得更短更清楚了一些,如下所示:

next 方法的實現:

- (id)next {
    //  1. 判斷棧是否為空,如果為空則返回 nil。
    if (_stack.count == 0) {
        return nil;
    }
    // 2. 從棧中取出元素,看是否遍歷到了結尾,如果是的話,則出棧。
    NSArrayIteratorCursor *c;
    c = [_stack lastObject];
    while (c.index == c.array.count && _stack.count > 0) {
        [_stack removeLastObject];
        c = [_stack lastObject];
    }
    // 3. 判斷第2步是否使棧為空,如果為空,則返回 nil。
    if (_stack.count == 0) {
        return nil;
    }
    // 4. 終於拿到元素了,這一步判斷拿到的元素是否是陣列。
    id item = c.array[c.index];
    if ([item isKindOfClass:[NSArray class]]) {
        c.index++;
        // 5. 如果是陣列,則重新生成一個遍歷的
        //    NSArrayIteratorCursor 物件,放到棧中, 然後遞迴呼叫 next 方法
        [self setupStackWithArray:item];
        return [self next];
    }

    // 6. 如果到了這一步,說明拿到了一個非陣列的元素,這樣就可以把元素返回,
    //    同時更新索引到下一個位置。
    c.index++;
    return item;
}

初使化部分:
- (id)initWithArray:(NSArray *)array {
    self = [super init];
    if (self) {
        _originArray = array;
        _stack = [NSMutableArray array];
        [self setupStackWithArray:array];
    }
    return self;
}

- (void)setupStackWithArray:(NSArray *)array {
    NSArrayIteratorCursor *c = [[NSArrayIteratorCursor alloc] initWithArray:array];
    [_stack addObject:c];
}

iOS 面試題(九):建立一個可以被取消執行的 block

問題:
我們知道 block 預設是不能被取消掉的,請你封裝一個可以被取消執行的 block wrapper 類,它的定義如下:

typedef void (^Block)();
@interface CancelableObject : NSObject

- (id)initWithBlock:(Block)block;

- (void)start;

- (void)cancel;

@end

答案:這道題是從網上看到的,原題是建立一個可以取消執行的 block,我想到兩種寫法。

// 方法一:建立一個類,將要執行的 block 封裝起來,然後類的內部有一個 _isCanceled 變數,在執行的時候,檢查這個變數,如果 _isCanceled 被設定成 YES 了,則退出執