轉自:https://blog.csdn.net/david21984/article/details/57451917node
iOS 面試題(一)尋找最近公共 View
題目:找出兩個 UIView 的最近的公共 View,若是不存在,則輸出 nil 。
分析:這實際上是數據結構裏面的找最近公共祖先的問題。git
一個UIViewController中的全部view之間的關係其實能夠當作一顆樹,UIViewController的view變量是這顆樹的根節點,其它的view都是根節點的直接或間接子節點。面試
因此咱們能夠經過 view 的 superview 屬性,一直找到根節點。須要注意的是,在代碼中,咱們還須要考慮各類非法輸入,若是輸入了 nil,則也須要處理,避免異常。如下是找到指定 view 到根 view 的路徑代碼:算法
一個簡單直接的辦法:拿第一個路徑中的全部節點,去第二個節點中查找。假設路徑的平均長度是 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)。swift
(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 */安全
/// 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
第一個辦法是「事前避免」,咱們在會產生循環引用的地方使用 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
@end
因爲數組在遍歷的時候可能產生遞歸,就像咱們實現 allObjects 方法那樣。因此咱們須要處理遞歸時的 NSArrayIteratorCursor 的保存,我在實現的時候,拿數組看成棧,來實現保存遍歷時的狀態。
最終,我實現了一個迭代器類,名字叫 NSArrayIterator,用於最終提供 next 方法的實現。這個類有兩個私有變量,一個是剛剛說的那個棧,另外一個是原數組的引用。
@interface NSArrayIterator : NSObject
@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,你們也能夠試試本身的代碼是否處理好了這種狀況:
判斷棧是否爲空,若是爲空則返回 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 了,則退出執行。
typedef void (^Block)();
@interface CancelableObject : NSObject
@implementation CancelableObject {
BOOL _isCanceled;
Block _block;
}
(id)initWithBlock:(Block)block {
self = [super init];
if (self != nil) {
_isCanceled = NO;
_block = block;
}
return self;
}
(void)start {
__weak typeof(self) weakSelf = self;
dispatch_async(dispatch_get_global_queue(0, 0),
^{
if (weakSelf) {
typeof(self) strongSelf = weakSelf;
if (!strongSelf->_isCanceled) {
(strongSelf->_block)();
}
}
});
}
(void)cancel {
_isCanceled = YES;
}
@end
// 另一種寫法,將要執行的 block 直接放到執行隊列中,可是讓其在執行前檢查另外一個 isCanceled 的變量,而後把這個變量的修改實如今另外一個 block 方法中,以下所示:
typedef void (^CancelableBlock)();
typedef void (^Block)();
(CancelableBlock)dispatch_async_with_cancelable:(Block)block {
__block BOOL isCanceled = NO;
CancelableBlock cb = ^() {
isCanceled = YES;
};
dispatch_async(dispatch_get_global_queue(0, 0), ^{
if (!isCanceled) {
block();
}
});
return cb;
}
以上兩種方法都只能在 block 執行前有效,若是要在 block 執行中有效,只能讓 block 在執行中,有一個機制來按期檢查外部的變量是否有變化,而要作到這一點,須要改 block 執行中的代碼。在本例中,若是 block 執行中的代碼是經過參數傳遞進來的話,彷佛並無什麼辦法能夠修改它了。
iOS 面試題(十):一個 Objective-C 對象的內存結構是怎樣的?
問題:一個 Objective-C 對象的內存結構是怎樣的?
答案:這是一道老題,或許不少人都準備過,其實若是不是被每一個公司都考查的話,這道題能夠看看候選人對於 iOS 背後底層原理的感興趣程度。真正對編程感興趣的同窗,都會對這個多少有一些好奇,進而在網上搜索並學習這方面的資料。
如下是本題的簡單回答:
若是把類的實例當作一個C語言的結構體(struct),它首先包含的是一個 isa 指針,而類的其它成員變量依次排列在結構體中。排列順序以下圖所示:
爲了驗證該說法,咱們在Xcode中新建一個工程,在main.m中運行以下代碼:
@interface Father : NSObject {
int _father;
}
@end@implementation Father
@end
@interface Child : Father {
int _child;
}
@end
@implementation Child
@end
int main(int argc, char * argv[])
{
Child * child = [[Child alloc] init];
@autoreleasepool {
// ...
}
}
// 咱們將斷點下在 @autoreleasepool 處,而後在Console中輸入p *child,則能夠看到Xcode輸出以下內容,這與咱們上面的說法一致。
(lldb) p *child
(Child) $0 = {
(Father) Father = {
(NSObject) NSObject = {
(Class) isa = Child
}
(int) _father = 0
}
(int) _child = 0
}
由於對象在內存中的排布能夠當作一個結構體,該結構體的大小並不能動態變化。因此沒法在運行時動態給對象增長成員變量。
注:須要特別說明一下,經過 objc_setAssociatedObject 和 objc_getAssociatedObject方法能夠變相地給對象增長成員變量,但因爲實現機制不同,因此並非真正改變了對象的內存結構。
iOS 面試題(11):對象內存結構中的 isa 指針是用來作什麼的?
問題:Objective-C 對象內存結構中的 isa 指針是用來作什麼的,有什麼用?
答案:Objective-C 是一門面向對象的編程語言。每個對象都是一個類的實例。在 Objective-C 語言的內部,每個對象都有一個名爲 isa 的指針,指向該對象的類。每個類描述了一系列它的實例的特色,包括成員變量的列表,成員函數的列表等。每個對象均可以接受消息,而對象可以接收的消息列表是保存在它所對應的類中。
在 Xcode 中按Shift + Command + O, 而後輸入 NSObject.h 和 objc.h,能夠打開 NSObject 的定義頭文件,經過頭文件咱們能夠看到,NSObject 就是一個包含 isa 指針的結構體,以下圖所示:
按照面向對象語言的設計原則,全部事物都應該是對象(嚴格來講 Objective-C 並無徹底作到這一點,由於它有象 int, double 這樣的簡單變量類型,而 Swift 語言,連 int 變量也是對象)。在 Objective-C 語言中,每個類實際上也是一個對象。每個類也有一個名爲 isa 的指針。每個類也能夠接受消息,例如代碼[NSObject alloc],就是向 NSObject 這個類發送名爲alloc消息。
在 Xcode 中按Shift + Command + O, 而後輸入 runtime.h,能夠打開 Class 的定義頭文件,經過頭文件咱們能夠看到,Class 也是一個包含 isa 指針的結構體,以下圖所示。(圖中除了 isa 外還有其它成員變量,但那是爲了兼容非 2.0 版的 Objective-C 的遺留邏輯,你們能夠忽略它。)
由於類也是一個對象,那它也必須是另外一個類的實列,這個類就是元類 (metaclass)。元類保存了類方法的列表。當一個類方法被調用時,元類會首先查找它自己是否有該類方法的實現,若是沒有,則該元類會向它的父類查找該方法,直到一直找到繼承鏈的頭。
元類 (metaclass) 也是一個對象,那麼元類的 isa 指針又指向哪裏呢?爲了設計上的完整,全部的元類的 isa 指針都會指向一個根元類 (root metaclass)。根元類 (root metaclass) 自己的 isa 指針指向本身,這樣就行成了一個閉環。上面提到,一個對象可以接收的消息列表是保存在它所對應的類中的。在實際編程中,咱們幾乎不會遇到向元類發消息的狀況,那它的 isa 指針在實際上不多用到。不過這麼設計保證了面向對象概念在 Objective-C 語言中的完整,即語言中的全部事物都是對象,都有 isa 指針。
咱們再來看看繼承關係,因爲類方法的定義是保存在元類 (metaclass) 中,而方法調用的規則是,若是該類沒有一個方法的實現,則向它的父類繼續查找。因此,爲了保證父類的類方法能夠在子類中能夠被調用,因此子類的元類會繼承父類的元類,換而言之,類對象和元類對象有着一樣的繼承關係。
我很想把關係說清楚一些,可是這塊兒確實有點繞,咱們仍是來看圖吧,不少時候圖象比文字表達起來更爲直觀。下面這張圖或許可以讓你們對 isa 和繼承的關係清楚一些:
咱們能夠從圖中看出:
NSObject 的類中定義了實例方法,例如 -(id)init 方法 和 - (void)dealloc 方法。
NSObject 的元類中定義了類方法,例如 +(id)alloc 方法 和 + (void)load 、+ (void)initialize 方法。
NSObject 的元類繼承自 NSObject 類,因此 NSObject 類是全部類的根,所以 NSObject 中定義的實例方法能夠被全部對象調用,例如 - (id)init 方法 和 - (void)dealloc 方法。
NSObject 的元類的 isa 指向本身。
isa swizzling 的應用
系統提供的 KVO 的實現,就利用了動態地修改 isa 指針的值的技術。在 蘋果的文檔
中能夠看到以下描述:
Key-Value Observing Implementation Details
Automatic key-value observing is implemented using a technique called isa-swizzling.
The isa pointer, as the name suggests, points to the object’s class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.
When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.
You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.
iOS 面試題(12):按層遍歷二叉樹的節點
解題代碼都是使用 Swift 完成的,我也儘可能在代碼中使用了 Swift 語言的一些特性,你們能夠順便學學 Swift。
問題:給你一棵二叉樹,請按層輸出其的節點值,即:按從上到下,從左到右的順序。
例如,若是給你以下一棵二叉樹:
3
/ 9 20
/ 15 7
輸出結果應該是:
[
[3],
[9,20],
[15,7]]
本題的 Swift 代碼模版以下:
private class TreeNode {
public var val: Int
public var left: TreeNode?
public var right: TreeNode?
public init(_ val: Int) {
self.val = val
self.left = nil
self.right = nil
}
}
class Solution {
func levelOrder(_ root: TreeNode?) -> [[Int]] {
}
}
解答:本題出自 LeetCode 第 102 題,是一個典型的有關遍歷的題目。爲了按層遍歷,咱們須要使用「隊列」,來將每一層的節點先保存下來,而後再依次處理。
由於咱們不但須要按層來遍歷,還須要按層來輸出結果,因此我在代碼中使用了兩個隊列,分別名爲 level 和 nextLevel,用於保存不一樣層的節點。
最終,整個算法邏輯是:
判斷輸入參數是不是爲空。
將根節點加入到隊列 level 中。
若是 level 不爲空,則:
3.1 將 level 加入到結果 ans 中。
3.2 遍歷 level 的左子節點和右子節點,將其加入到 nextLevel 中。
3.3 將 nextLevel 賦值給 level,重複第 3 步的判斷。
將 ans 中的節點換成節點的值,返回結果。
由於咱們是用 Swift 來實現代碼,因此我使用了一些 Swift 語言的特性。例如:隊列中咱們保存的是節點的數據結構,可是最終輸出的時候,咱們須要輸出的是值,在代碼中,我使用了 Swift 的函數式的鏈式調用,將嵌套數組中的元素類型作了一次變換,以下所示:
let ans = result.map { $0.map { $0.val }}
另外,咱們也使用了 Swift 特有的 guard 關鍵字,來處理參數的特殊狀況。
完整的參考代碼以下:
//
// Binary Tree Level Order Traversal.swift
//
// Created by Tang Qiao.
//
import Foundation
private class TreeNode {
public var val: Int
public var left: TreeNode?
public var right: TreeNode?
public init(_ val: Int) {
self.val = val
self.left = nil
self.right = nil
}
}
private class Solution {
func levelOrder(_ root: TreeNode?) -> [[Int]] {
guard let root = root else {
return []
}
var result = [TreeNode]
var level = TreeNode
level.append(root) while level.count != 0 { result.append(level) var nextLevel = [TreeNode]() for node in level { if let leftNode = node.left { nextLevel.append(leftNode) } if let rightNode = node.right { nextLevel.append(rightNode) } } level = nextLevel } let ans = result.map { $0.map { $0.val }} return ans }
}
微信中排版代碼很是不便,因此上述代碼也能夠從個人 Gist 中找到:代碼Gist地址
完成這道題的同窗,能夠試着練習一下 LeetCode的第 107 題,看看能不能只改動一行代碼,就把 107 題也解決掉。
iOS 面試題(13):求兩個鏈表表示的數的和
問題:給你兩個鏈表,分別表示兩個非負的整數。每一個鏈表的節點表示一個整數位。
爲了方便計算,整數的低位在鏈表頭,例如:123 在鏈表中的表示方式是:
3 -> 2 -> 1
如今給你兩個這樣結構的鏈表,請輸出它們求和以後的結果。例如:
輸入: (2 -> 4 -> 1) + (5 -> 6 -> 1)
輸出: 7 -> 0 -> 3
本題的 Swift 代碼模版以下:
private class ListNode {
public var val: Int
public var next: ListNode?
public init(_ val: Int) {
self.val = val
self.next = nil
}
}
class Solution {
func addTwoNumbers(_ l1: ListNode?, _ l2: ListNode?)
-> ListNode? {
}
}
考查點:本題出自 LeetCode 上的第 2 題。
這是我高中學習編程時最先接觸的一類題目,咱們把這類題目叫作「高精度計算」,其實就是在計算機計算精度不夠時,模擬咱們在紙上演算的方式來計算答案,而後得到足夠精度的解。
我還記得我 7 年前第一次去網易有道面試的時候,就考查的是一道相似的高精度計算題目,比這道題複雜得多,我當時用了一個比較笨的辦法,加上當時仍是用 C++ 寫的,內存分配和釋放寫起來也比較麻煩,最後寫了兩頁 A4 紙才寫完。
這道題其實徹底不考查什麼「算法」,人人都知道怎麼計算,可是它考察了「將想法轉換成代碼」的能力,新手一般犯的毛病就是:意思都明白,可是寫不出來代碼。因此,這類題目用來過濾菜鳥確實是挺有效的辦法。
答案:本題的作法其實沒什麼特別,就是直接計算。計算的時候須要考慮到如下這些狀況:
兩個整數長度不一致的狀況。
進位的狀況。當進位產生時,咱們須要保存一個標誌位,以便在計算下一位的和的時候,加上進位。
當計算完後,若是還有進位,須要處理最後結果加一位的狀況。
如下是完整的代碼,我使用了一些 Swift 語言的特性,好比用 flatMap 來減小對於 Optional 類型值爲 nil 的判斷。
private class ListNode {
public var val: Int
public var next: ListNode?
public init(_ val: Int) {
self.val = val
self.next = nil
}
}
private class Solution {
private func getNodeValue(_ node: ListNode?) -> Int { return node.flatMap { $0.val } ?? 0 } func addTwoNumbers(_ l1: ListNode?, _ l2: ListNode?) -> ListNode? { if l1 == nil || l2 == nil { return l1 ?? l2 } var p1 = l1 var p2 = l2 let result: ListNode? = ListNode(0) var current = result var extra = 0 while p1 != nil || p2 != nil || extra != 0 { var tot = getNodeValue(p1) + getNodeValue(p2) + extra extra = tot / 10 tot = tot % 10 let sum:ListNode? = ListNode(tot) current!.next = sum current = sum p1 = p1.flatMap { $0.next } p2 = p2.flatMap { $0.next } } return result!.next }
}
以上代碼也能夠從個人 Gist 中找到:Gist
偷偷告訴你一個小祕密,Gist 裏面的代碼我稍微修改了兩行,最終性能就從戰勝 LeetCode 10% 的提交變成了戰勝 LeetCode 50% 的提交,若是你感興趣,能夠本身仔細對比一下。
iOS 面試題(14):計算有多少個島嶼
問題:在一個地圖中,找出一共有多少個島嶼。
咱們用一個二維數組表示這個地圖,地圖中的 1 表示陸地,0 表示水域。一個島嶼是指由上下左右相連的陸地,而且被水域包圍的區域。
你能夠假設地圖的四周都是水域。
例一:一共有 1 個島嶼。
11110
11010
11000
00000
例二:一共有 3 個島嶼。
11000
11000
00100
00011
答案:這是 LeetCode 上的 第 200 題,咱們能夠用一種被稱爲「種子填充」(floodfill)的辦法來解決此題。
具體的作法是:
遍歷整個地圖,找到一個未被標記過的,值爲 1 的座標。
從這個座標開始,從上下左右四個方向,標記相鄰的 1 。
把這些相鄰的座標,都標記下來,遞歸的進行標記,以便把相鄰的相鄰塊也能標記上。
待標記所有完成以後,將島嶼的計數 +1。
回到第 1 步。若是第 1 步沒法找到未標記的座標,則結束。
雖然思路簡單,可是實現起來代碼量也不算小。這裏有一些小技巧:
咱們能夠將上下左右四個方向的偏移量保存在數組中,這樣在計算位置的時候,寫起來更簡單一些。
遞歸的標記過程能夠用深度優先搜索(DFS)或者寬度優先搜索(BFS)。
如下是完整的參考代碼:
private class Solution {
private var flag: [[Int]]
private var answer: Int
private var movex : [Int] {
return [-1, 1, 0, 0]
}
private var movey : [Int] { return [0, 0, -1, 1] } init() { flag = [[Int]]() answer = 0 } func dfs(_ grid: [[Character]] ,_ x: Int,_ y: Int) { for i in 0..<4 { let tox = x + movex[i] let toy = y + movey[i] if tox >= 0 && tox < grid.count && toy >= 0 && toy < grid[0].count && grid[tox][toy] == "1" && flag[tox][toy] == 0 { flag[tox][toy] = 1 dfs(grid, tox, toy) } } } func numIslands(_ grid: [[Character]]) -> Int { answer = 0 flag = grid.map { $0.map { _ in return 0 }} for i in 0..<grid.count { for j in 0..<grid[i].count { if grid[i][j] == "1" && flag[i][j] == 0 { flag[i][j] = 1 // print("find in \(i), \(j)") dfs(grid, i, j) answer += 1 } } } return answer }
}
Swift 的參數默認是不能修改值的,可是若是是 C++ 語言的話,咱們能夠直接在地圖上作標記。由於地圖只有 0 和 1 兩種值,咱們能夠用 2 表示「標記過的陸地」,這樣就省略了額外的標記數組。如下是我寫的一個 C++ 的示例程序:
class Solution {
public:
void fillLands(vector<vector
int movex[] = {0, 0, 1, -1};
int movey[] = {-1, 1, 0, 0};
queue<pair<int, int>> q;
q.push(make_pair(px, py));
grid[px][py] = '2';
while (!q.empty()) {
pair<int, int> item = q.front();
q.pop();
int tox, toy;
for (int i = 0; i < 4; ++i) {
tox = item.first + movex[i];
toy = item.second + movey[i];
if (tox >= 0 && tox < grid.size()
&& toy >=0 && toy < grid[0].size()
&& grid[tox][toy] == '1') {
grid[tox][toy] = '2';
q.push(make_pair(tox, toy));
}
}
}
}
int numIslands(vector<vector
int ans = 0;
for (int i = 0; i < grid.size(); ++i) {
for (int j = 0; j < grid[0].size(); ++j) {
if (grid[i][j] == '1') {
fillLands(grid, i, j);
ans++;
}
}
}
return ans;
}
};
iOS 面試題(16):解釋垃圾回收的原理
摘要: 問題 咱們知道,Android 手機一般使用 Java 來開發,而 Java 是使用垃圾回收這種內存管理方式。 那麼,ARC 和垃圾回收對比,有什麼優勢和缺點? 考查點 此題實際上是考查你們的知識面,雖然作 iOS 開發並不須要用到垃圾回收這種內存管理…
問題
咱們知道,Android 手機一般使用 Java 來開發,而 Java 是使用垃圾回收這種內存管理方式。 那麼,ARC 和垃圾回收對比,有什麼優勢和缺點?
考查點
此題實際上是考查你們的知識面,雖然作 iOS 開發並不須要用到垃圾回收這種內存管理機制。可是垃圾回收被使用得很是廣泛,不但有 Java,還包括 JavaScript, C#,Go 等語言。
若是兩個候選人,一我的只會 iOS 開發,另外一我的不但會 iOS 開發,對別的語言或技術也有興趣瞭解,那我一般更傾向於後者。並且事實經常是,因爲後者對計算機興趣更濃,他在 iOS 上也一般專研得比前者更多。
垃圾回收簡介
做爲 iOS 開發者,瞭解一下這個世界上除了 ARC 以外最流行的內存管理方式,仍是挺有價值的。因此我儘可能簡單給你們介紹一下。
垃圾回收(Garbage Collection,簡稱 GC)這種內存管理機制最先由圖靈獎得到者 John McCarthy 在 1959 年提出,垃圾回收的理論主要基於一個事實:大部分的對象的生命期都很短。
因此,GC 將內存中的對象主要分紅兩個區域:Young 區和 Old 區。對象先在 Young 區被建立,而後若是通過一段時間還存活着,則被移動到 Old 區。(其實還有一個 Perm 區,可是內存回收算法一般不涉及這個區域)
Young 區和 Old 區由於對象的特色不同,因此採用了兩種徹底不一樣的內存回收算法。
Young 區的對象由於大部分生命期都很短,每次回收以後只有少部分可以存活,因此採用的算法叫 Copying 算法,簡單說來就是直接把活着的對象複製到另外一個地方。Young 區內部又分紅了三塊區域:Eden 區 , From 區 , To 區。每次執行 Copying 算法時,即將存活的對象從 Eden 區和 From 區複製到 To 區,而後交換 From 區和 To 區的名字(即 From 區變成 To 區,To 區變成 From 區)。
Old 區的對象由於都是存活下來的老司機了,因此若是用 Copying 算法的話,極可能 90% 的對象都得複製一遍了,不划算啊!因此 Old 區的回收算法叫 Mark-Sweep 算法。簡單來講,就是隻是把不用的對象先標記(Mark)出來,而後回收(Sweep),活着的對象就不動它了。由於大部分對象都活着,因此回收下來的對象並很少。可是這個算法會有一個問題:它會產生內存碎片,因此它通常還會帶有整理內存碎片的邏輯,在算法中叫作 Compact。如何整理呢?早年用過 Windows 的硬盤碎片整理程序的朋友可能能理解,其實就是把對象插到這些空的位置裏。這裏面還涉及不少優化的細節,我就不一一展開了。
講完主要的算法,接下來 GC 須要解決的問題就只剩下如何找出須要回收的垃圾對象了。爲了不 ARC 解決不了的循環引用問題,GC 引入了一個叫作「可達性」的概念,應用這個概念,即便是有循環引用的垃圾對象,也能夠被回收掉。下面就給你們介紹一下這個概念。
當 GC 工做時,GC 認爲當前的一些對象是有效的,這些對象包括:全局變量,棧裏面的變量等,而後 GC 從這些變量出發,去標記這些變量「可達」的其它變量,這個標記是一個遞歸的過程,最後就像從樹根的內存對象開始,把全部的樹枝和樹葉都記成可達的了。那除了這些「可達」的變量,別的變量就都須要被回收了。
聽起來很牛逼對不對?那爲何蘋果不用呢?實際上蘋果在 OS X 10.5 的時候還真用了,不過在 10.7 的時候把 GC 換成了 ARC。那麼,GC 有什麼問題讓蘋果不能忍,這就是:垃圾回收的時候,整個程序須要暫停,英文把這個過程叫作:Stop the World。因此說,你知道 Android 手機有時候爲何會卡吧,GC 就至關於春運的最後一天返城高峯。當全部的對象都須要一塊兒回收時,那種體驗確定是當時還在世的喬布斯忍受不了的。
看看下面這幅漫畫,真實地展示出 GC 最尷尬的狀況(漫畫中提到的 Full GC,就是指執行 Old 區的內存回收):
固然,事實上通過多年的發展,GC 的回收算法一直在被優化,人們想了各類辦法來優化暫停的時間,因此狀況並無那麼糟糕。
答案
ARC 相對於 GC 的優勢:
ARC 工做在編譯期,在運行時沒有額外開銷。
ARC 的內存回收是平穩進行的,對象不被使用時會當即被回收。而 GC 的內存回收是一陣一陣的,回收時須要暫停程序,會有必定的卡頓。
ARC 相對於 GC 的缺點:
GC 真的是太簡單了,基本上徹底不用處理內存管理問題,而 ARC 仍是須要處理相似循環引用這種內存管理問題。
GC 一類的語言相對來講學習起來更簡單。