轉(zhuǎn)載自:iOS 面試題
28、找出兩個(gè) UIView 的最近的公共 View裹刮,如果不存在音榜,則輸出 nil 。
分析:這其實(shí)是數(shù)據(jù)結(jié)構(gòu)里面的找最近公共祖先的問(wèn)題捧弃。
一個(gè) UIViewController
中的所有 view 之間的關(guān)系其實(shí)可以看成一顆樹(shù)赠叼,UIViewController
的 view 變量是這顆樹(shù)的根節(jié)點(diǎn),其它的 view 都是根節(jié)點(diǎn)的直接或間接子節(jié)點(diǎn)违霞。
所以我們可以通過(guò) view 的 superview
屬性嘴办,一直找到根節(jié)點(diǎn)。需要注意的是葛家,在代碼中户辞,我們還需要考慮各種非法輸入,如果輸入了 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];
}
然后對(duì)于兩個(gè) view A 和 view B弹砚,我們可以得到兩個(gè)路徑,而本題中我們要找的是這里面最近的一個(gè)公共節(jié)點(diǎn)枢希。
一個(gè)簡(jiǎn)單直接的辦法:拿第一個(gè)路徑中的所有節(jié)點(diǎn)桌吃,去第二個(gè)節(jié)點(diǎn)中查找。假設(shè)路徑的平均長(zhǎng)度是 N苞轿,因?yàn)槊總€(gè)節(jié)點(diǎn)都要找 N 次茅诱,一共有 N 個(gè)節(jié)點(diǎn)逗物,所以這個(gè)辦法的時(shí)間復(fù)雜度是 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;
}
一個(gè)改進(jìn)的辦法:我們將一個(gè)路徑中的所有點(diǎn)先放進(jìn) NSSet 中瑟俭。因?yàn)?NSSet 的內(nèi)部實(shí)現(xiàn)是一個(gè) hash 表翎卓,所以查找元素的時(shí)間復(fù)雜度變成了 O(1),我們一共有 N 個(gè)節(jié)點(diǎn)摆寄,所以總時(shí)間復(fù)雜度優(yōu)化到了 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 外,我們還可以使用類似歸并排序的思想微饥,用兩個(gè)「指針」逗扒,分別指向兩個(gè)路徑的根節(jié)點(diǎn),然后從根節(jié)點(diǎn)開(kāi)始欠橘,找第一個(gè)不同的節(jié)點(diǎn)矩肩,第一個(gè)不同節(jié)點(diǎn)的上一個(gè)公共節(jié)點(diǎn),就是我們的答案肃续。代碼如下:
/* 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 方法來(lái)簡(jiǎn)化我們的代碼黍檩,不過(guò)這樣寫(xiě)的話,時(shí)間復(fù)雜度應(yīng)該也是 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 方法,可以將上面的代碼簡(jiǎn)化得更短疼蛾,基本上算是一行代碼搞定肛跌。
extension UIView {
func commonSuperview(of view: UIView) -> UIView? {
return superview.flatMap {
view.isDescendant(of: $0) ?
$0 : $0.commonSuperview(of: view)
}
}
}
29、什么時(shí)候在 block 中不需要使用 weakSelf
問(wèn)題
我們知道察郁,在使用 block 的時(shí)候衍慎,為了避免產(chǎn)生循環(huán)引用,通常需要使用weakSelf
與strongSelf
皮钠,寫(xiě)下面這樣的代碼:
__weak typeof(self) weakSelf = self;
[self doSomeBlockJob:^{
__strong typeof(weakSelf) strongSelf = weakSelf;
if (strongSelf) {
...
}
}];
那么請(qǐng)問(wèn):什么時(shí)候在 block 里面用 self稳捆,不需要使用 weak self?
答案
當(dāng) block 本身不被 self 持有麦轰,而被別的對(duì)象持有乔夯,同時(shí)不產(chǎn)生循環(huán)引用的時(shí)候,就不需要使用 weak self 了款侵。最常見(jiàn)的代碼就是 UIView 的動(dòng)畫(huà)代碼末荐,我們?cè)谑褂?UIView 的 animateWithDuration:animations 方法 做動(dòng)畫(huà)的時(shí)候,并不需要使用 weak self新锈,因?yàn)橐贸钟嘘P(guān)系是:
UIView 的某個(gè)負(fù)責(zé)動(dòng)畫(huà)的對(duì)象持有了 block
block 持有了 self
因?yàn)?self 并不持有 block甲脏,所以就沒(méi)有循環(huán)引用產(chǎn)生,因?yàn)榫筒恍枰褂?weak self 了。
[UIView animateWithDuration:0.2 animations:^{
self.alpha = 1;
}];
當(dāng)動(dòng)畫(huà)結(jié)束時(shí)块请,UIView 會(huì)結(jié)束持有這個(gè) block娜氏,如果沒(méi)有別的對(duì)象持有 block 的話,block 對(duì)象就會(huì)釋放掉墩新,從而 block 會(huì)釋放掉對(duì)于 self 的持有贸弥。整個(gè)內(nèi)存引用關(guān)系被解除。
30抖棘、為什么 weakSelf 需要配合 strong self 使用
在使用 block 的時(shí)候茂腥,為了避免產(chǎn)生循環(huán)引用,通常需要使用 weakSelf 與 strongSelf切省,寫(xiě)下面這樣的代碼:
__weak typeof(self) weakSelf = self;
[self doSomeBackgroundJob:^{
__strong typeof(weakSelf) strongSelf = weakSelf;
if (strongSelf) {
...
}
}];
為什么 block 里面還需要寫(xiě)一個(gè) strong self最岗,如果不寫(xiě)會(huì)怎么樣?
在 block 中先寫(xiě)一個(gè) strong self朝捆,其實(shí)是為了避免在 block 的執(zhí)行過(guò)程中般渡,突然出現(xiàn) self 被釋放的尷尬情況。通常情況下芙盘,如果不這么做的話驯用,還是很容易出現(xiàn)一些奇怪的邏輯,甚至閃退儒老。
我們以 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);
}
};
如果沒(méi)有 strongSelf 的那行代碼蝴乔,那么后面的每一行代碼執(zhí)行時(shí),self 都可能被釋放掉了驮樊,這樣很可能造成邏輯異常薇正。
特別是當(dāng)我們正在執(zhí)行 strongSelf.networkReachabilityStatusBlock(status)
; 這個(gè) block 閉包時(shí),如果這個(gè) block 執(zhí)行到一半時(shí) self 釋放囚衔,那么多半情況下會(huì) Crash挖腰。
這里有一篇文章詳細(xì)解釋了這個(gè)問(wèn)題:https://dhoerl.wordpress.com/2013/04/23/i-finally-figured-out-weakself-and-strongself/
提問(wèn):
1、“數(shù)組” 和 “字典” 的 enumeratXXXUsingBlock: 是否要使用 weakSelf 和 strongSelf 呢练湿?
2猴仑、block 里 strong self 后,block 不是也會(huì)持有 self 嗎肥哎?而 self 又持有 block 辽俗,那不是又循環(huán)引用了?
在block里用strong引用篡诽,保證了持有引用的周期只在 block被執(zhí)行時(shí)榆苞,閉包函數(shù)返回后就釋放了。而直接用強(qiáng)引用霞捡,持有引用的周期則是block的生命周期,就會(huì)循環(huán)引用了薄疚。
31碧信、block 什么時(shí)候需要構(gòu)造循環(huán)引用
有沒(méi)有這樣一個(gè)需求場(chǎng)景赊琳,block 會(huì)產(chǎn)生循環(huán)引用,但是業(yè)務(wù)又需要你不能使用 weak self? 如果有砰碴,請(qǐng)舉一個(gè)例子并且解釋這種情況下如何解決循環(huán)引用問(wèn)題
答案
需要不使用 weak self 的場(chǎng)景是:你需要構(gòu)造一個(gè)循環(huán)引用躏筏,以便保證引用雙方都存在。比如你有一個(gè)后臺(tái)的任務(wù)呈枉,希望任務(wù)執(zhí)行完后趁尼,通知另外一個(gè)實(shí)例。在我們開(kāi)源的 YTKNetwork 網(wǎng)絡(luò)庫(kù)的源碼中猖辫,就有這樣的場(chǎng)景酥泞。
在 YTKNetwork 庫(kù)中,我們的每一個(gè)網(wǎng)絡(luò)請(qǐng)求 API 會(huì)持有回調(diào)的 block啃憎,回調(diào)的 block 會(huì)持有 self芝囤,而如果 self 也持有網(wǎng)絡(luò)請(qǐng)求 API 的話,我們就構(gòu)造了一個(gè)循環(huán)引用辛萍。雖然我們構(gòu)造出了循環(huán)引用悯姊,但是因?yàn)樵诰W(wǎng)絡(luò)請(qǐng)求結(jié)束時(shí)吵血,網(wǎng)絡(luò)請(qǐng)求 API 會(huì)主動(dòng)釋放對(duì) block 的持有琅锻,因此江滨,整個(gè)循環(huán)鏈條被解開(kāi)寿烟,循環(huán)引用就被打破了弃锐,所以不會(huì)有內(nèi)存泄漏問(wèn)題魂仍。代碼其實(shí)很簡(jiǎn)單三热,如下所示:
// YTKBaseRequest.m
- (void)clearCompletionBlock {
// nil out to break the retain cycle.
self.successCompletionBlock = nil;
self.failureCompletionBlock = nil;
}
總結(jié)來(lái)說(shuō)念祭,解決循環(huán)引用問(wèn)題主要有兩個(gè)辦法:
- 第一個(gè)辦法是「事前避免」睛藻,我們?cè)跁?huì)產(chǎn)生循環(huán)引用的地方使用 weak 弱引用启上,以避免產(chǎn)生循環(huán)引用。
- 第二個(gè)辦法是「事后補(bǔ)救」店印,我們明確知道會(huì)存在循環(huán)引用冈在,但是我們?cè)诤侠淼奈恢弥鲃?dòng)斷開(kāi)環(huán)中的一個(gè)引用,使得對(duì)象得以回收按摘。
32包券、weak 的內(nèi)部實(shí)現(xiàn)原理
問(wèn)題
weak 變量在引用計(jì)數(shù)為0時(shí),會(huì)被自動(dòng)設(shè)置成 nil炫贤,這個(gè)特性是如何實(shí)現(xiàn)的溅固?
答案
在 Friday QA 上,有一期專門(mén)介紹 weak 的實(shí)現(xiàn)原理兰珍。https://mikeash.com/pyblog/friday-qa-2010-07-16-zeroing-weak-references-in-objective-c.html
《Objective-C高級(jí)編程》一書(shū)中也介紹了相關(guān)的內(nèi)容侍郭。
簡(jiǎn)單來(lái)說(shuō),系統(tǒng)有一個(gè)全局的 CFMutableDictionary 實(shí)例,來(lái)保存每個(gè)對(duì)象的 weak 指針列表亮元,因?yàn)槊總€(gè)對(duì)象可能有多個(gè) weak 指針猛计,所以這個(gè)實(shí)例的值是 CFMutableSet 類型。
剩下我們要做的爆捞,就是在引用計(jì)數(shù)變成 0 的時(shí)候奉瘤,去這個(gè)全局的字典里面,找到所有的 weak 指針煮甥,將其值設(shè)置成 nil盗温。如何做到這一點(diǎn)呢?Friday QA 上介紹了一種類似 KVO 實(shí)現(xiàn)的方式成肘。當(dāng)對(duì)象存在 weak 指針時(shí)卖局,我們可以將這個(gè)實(shí)例指向一個(gè)新創(chuàng)建的子類,然后修改這個(gè)子類的 release 方法艇劫,在 release 方法中吼驶,去從全局的 CFMutableDictionary 字典中找到所有的 weak 對(duì)象( 這里應(yīng)該是找到所有弱引用本對(duì)象的地方,將其置為nil )店煞,并且設(shè)置成 nil蟹演。我摘抄了 Friday QA 上的實(shí)現(xiàn)的核心代碼,如下:
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);
33顷蟀、自己寫(xiě)的 view 成員酒请,應(yīng)該用 weak 還是 strong?
問(wèn)題
從 Storyboard 往編譯器拖出來(lái)的 UI 控件的屬性是 weak 的鸣个,如下所示
@property (weak, nonatomic) IBOutlet UIButton *myButton;
那么羞反,如果有一些 UI 控件我們要用代碼的方式來(lái)創(chuàng)建,那么它應(yīng)該用 weak 還是 strong 呢囤萤?為什么昼窗?
答案
當(dāng) UI 控件是 weak 時(shí),它的引用計(jì)數(shù)是 1涛舍,持有它的是它的 superview澄惊,當(dāng) UI 控件是 strong 時(shí),它的引用計(jì)數(shù)是 2富雅,持有它的有兩個(gè)地方掸驱,一個(gè)是它的 superview,另一個(gè)是這個(gè) strong 的指針没佑。UI 控件并不會(huì)持有別的對(duì)象毕贼,所以,不管是手寫(xiě)代碼還是 Storyboard蛤奢,UI 控件是 strong 都不會(huì)有循環(huán)引用的鬼癣。
weak 變量會(huì)有額外的系統(tǒng)維護(hù)開(kāi)銷的陶贼,如果你沒(méi)有使用它的特別的理由,那么用 strong 的話應(yīng)該更好扣溺。
如果要做 Lazy 加載骇窍,那么也只能選擇用 strong。
當(dāng)然锥余,如果非要用 weak,其實(shí)也沒(méi)什么問(wèn)題痢掠,只需要注意在賦值前驱犹,先把這個(gè)對(duì)象用 addSubView 加到父 view 上,否則可能剛剛創(chuàng)建完足画,它就被釋放了雄驹。
34、實(shí)現(xiàn)一個(gè)嵌套數(shù)組的迭代器
問(wèn)題
1淹辞、給一個(gè)嵌套的 NSArray 數(shù)據(jù)医舆,實(shí)現(xiàn)一個(gè)迭代器類,該類提供一個(gè) next() 方法象缀,可以依次的取出這個(gè) NSArray 中的數(shù)據(jù)蔬将。比如 NSArray 如果是 [1,[4,3],6,[5,[1,0]]], 則最終應(yīng)該輸出:1, 4, 3, 6, 5, 1, 0 央星。
2霞怀、實(shí)現(xiàn)一個(gè) allObjects 方法,可以一次性取出所有元素莉给。
解答
先看第二個(gè)問(wèn)題:
可以實(shí)現(xiàn)一個(gè)遞歸函數(shù)毙石,在函數(shù)中判斷數(shù)組中的元素是否又是數(shù)組,如果是的話颓遏,就遞歸調(diào)用自己徐矩,如果不是數(shù)組,則加入到一個(gè) 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]];
}
}
}
第一個(gè)問(wèn)題:
記錄下遍歷的位置滤灯,然后每次遍歷時(shí)更新位置。由于本題中元素是一個(gè)嵌套數(shù)組遥皂,所以我們?yōu)榱擞涗浵挛恢昧ε纾托枰獌蓚€(gè)變量:一個(gè)是當(dāng)前正在遍歷的子數(shù)組,另一個(gè)是這個(gè)數(shù)組遍歷到的位置演训。
在實(shí)現(xiàn)的時(shí)候弟孟,定義了一個(gè)名為 NSArrayIteratorCursor
的類來(lái)記錄這些內(nèi)容,NSArrayIteratorCursor
的定義和實(shí)現(xiàn)如下:
@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
由于數(shù)組在遍歷的時(shí)候可能產(chǎn)生遞歸样悟,就像實(shí)現(xiàn) allObjects
方法那樣拂募。所以我們需要處理遞歸時(shí)的 NSArrayIteratorCursor
的保存庭猩,在實(shí)現(xiàn)的時(shí)候,拿數(shù)組當(dāng)作棧陈症,來(lái)實(shí)現(xiàn)保存遍歷時(shí)的狀態(tài)蔼水。
最終,實(shí)現(xiàn)了一個(gè)迭代器類录肯,名字叫 NSArrayIterator
趴腋,用于最終提供 next
方法的實(shí)現(xiàn)。這個(gè)類有兩個(gè)私有變量论咏,一個(gè)是剛剛說(shuō)的那個(gè)棧优炬,另一個(gè)是原數(shù)組的引用。
@interface NSArrayIterator : NSObject
- (id)initWithArray:(NSArray *)array;
- (id)next;
- (NSArray *)allObjects;
@end
@implementation NSArrayIterator {
NSMutableArray *_stack;
NSArray *_originArray;
}
在初使化的時(shí)候厅贪,初始化遍歷位置的代碼如下:
- (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];
}
用遞歸的方式來(lái)處理空數(shù)組的邏輯似乎是寫(xiě)起來(lái)更簡(jiǎn)單的蠢护,邏輯如下:
1、判斷棧是否為空养涮,如果為空則返回 nil葵硕。
2、從棧中取出元素贯吓,看是否遍歷到了結(jié)尾懈凹,如果是的話,則出棧宣决。
3蘸劈、判斷第 2 步是否使棧為空,如果為空尊沸,則返回 nil威沫。
4、終于拿到元素了洼专,這一步判斷拿到的元素是否是數(shù)組棒掠。
a、如果是數(shù)組屁商,則重新生成一個(gè)遍歷的 NSArrayIteratorCursor 對(duì)象烟很,放到棧中,并且遞歸調(diào)用自己蜡镶。
b雾袱、如果不是數(shù)組,就把元素返回官还,同時(shí)更新索引到下一個(gè)位置芹橡。
整個(gè)代碼也變得更短更清楚了一些,如下所示:
- (id)next {
// 1. 判斷棧是否為空望伦,如果為空則返回 nil林说。
if (_stack.count == 0) {
return nil;
}
// 2. 從棧中取出元素煎殷,看是否遍歷到了結(jié)尾,如果是的話腿箩,則出棧豪直。
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. 終于拿到元素了,這一步判斷拿到的元素是否是數(shù)組剑梳。
id item = c.array[c.index];
if ([item isKindOfClass:[NSArray class]]) {
c.index++;
// 5. 如果是數(shù)組唆貌,則重新生成一個(gè)遍歷的
// NSArrayIteratorCursor 對(duì)象,放到棧中, 然后遞歸調(diào)用 next 方法
[self setupStackWithArray:item];
return [self next];
}
// 6. 如果到了這一步垢乙,說(shuō)明拿到了一個(gè)非數(shù)組的元素,這樣就可以把元素返回语卤,
// 同時(shí)更新索引到下一個(gè)位置追逮。
c.index++;
return item;
}