转自: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 一类的语言相对来讲学习起来更简单。