我有一个同事,他既不姓金,也不是司机,但我们都叫他“金司机”。他跟仓鼠一样是一个 iOS 工程师,至于叫司机的原因就不难想到了…… 为了防止博客被封,在此不举例子。
总之,金司机在这周周会上给组里同事展示了好几道他出的“面试题”,成功淘汰了组里所有同事、甚至包括我们老大,给平淡的工作带来了许多欢乐。之所以打引号,是因为这些题只是形式像面试题,其实并不能真的用来面试(而且我们公司绝不会使用这些题来面试),不然恐怕一个人都招不到了。大家有兴趣看看就好,不许喷我同事~
代码是在 command line 环境下执行的,虽然代码是 swift 写的,不过 API 都是一样的,写 Objective-C 的朋友也能一看就懂。我们开始吧~
主线程与主队列
在看这组题之前,先问自己一个问题:主线程和主队列的关系是什么?
第一题
let key = DispatchSpecificKey<String>()
DispatchQueue.main.setSpecific(key: key, value: "main")
func log() {
debugPrint("main thread: \(Thread.isMainThread)")
let value = DispatchQueue.getSpecific(key: key)
debugPrint("main queue: \(value != nil)")
}
DispatchQueue.global().sync(execute: log)
RunLoop.current.run()
执行结果是什么呢?
第二题
let key = DispatchSpecificKey<String>()
DispatchQueue.main.setSpecific(key: key, value: "main")
func log() {
debugPrint("main thread: \(Thread.isMainThread)")
let value = DispatchQueue.getSpecific(key: key)
debugPrint("main queue: \(value != nil)")
}
DispatchQueue.global().async {
DispatchQueue.main.async(execute: log)
}
dispatchMain()
什么情况下输出的结果并不是两个 true
呢?
GCD 与 OperationQueue
第三题
let observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, CFRunLoopActivity.allActivities.rawValue, true, 0) { _, activity in
if activity.contains(.entry) {
debugPrint("entry")
} else if activity.contains(.beforeTimers) {
debugPrint("beforeTimers")
} else if activity.contains(.beforeSources) {
debugPrint("beforeSources")
} else if activity.contains(.beforeWaiting) {
debugPrint("beforeWaiting")
} else if activity.contains(.afterWaiting) {
debugPrint("afterWaiting")
} else if activity.contains(.exit) {
debugPrint("exit")
}
}
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, CFRunLoopMode.commonModes)
// case 1
DispatchQueue.global().async {
(0...999).forEach { idx in
DispatchQueue.main.async {
debugPrint(idx)
}
}
}
// case 2
//DispatchQueue.global().async {
// let operations = (0...999).map { idx in BlockOperation { debugPrint(idx) } }
// OperationQueue.main.addOperations(operations, waitUntilFinished: false)
//}
RunLoop.current.run()
上面 GCD 的写法,和被注释掉的 OperationQueue 的写法,print 出来会有什么不同呢?
线程安全
第四题
这个题 Objective-C 和 swift 会有些不一样,所以我提供了两个版本的代码:
Swift:
let queue1 = DispatchQueue(label: "queue1")
let queue2 = DispatchQueue(label: "queue2")
var list: [Int] = []
queue1.async {
while true {
if list.count < 10 {
list.append(list.count)
} else {
list.removeAll()
}
}
}
queue2.async {
while true {
// case 1
list.forEach { debugPrint($0) }
// case 2
// let value = list
// value.forEach { debugPrint($0) }
// case 3
// var value = list
// value.append(100)
}
}
RunLoop.current.run()
使用 case 1 的代码会 crash 吗?case 2 呢?case 3 呢?
Objective-C:
dispatch_queue_t queue1 = dispatch_queue_create("queue1", 0);
dispatch_queue_t queue2 = dispatch_queue_create("queue2", 0);
NSMutableArray* array = [NSMutableArray array];
dispatch_async(queue1, ^{
while (true) {
if (array.count < 10) {
[array addObject:@(array.count)];
} else {
[array removeAllObjects];
}
}
});
dispatch_async(queue2, ^{
while (true) {
// case 1
// for (NSNumber* number in array) {
// NSLog(@"%@", number);
// }
// case 2
// NSArray* immutableArray = array;
// for (NSNumber* number in immutableArray) {
// NSLog(@"%@", number);
// }
// case 3
NSArray* immutableArray = [array copy];
for (NSNumber* number in immutableArray) {
NSLog(@"%@", number);
}
}
});
[[NSRunLoop currentRunLoop] run];
使用 case 1 的代码会 crash 吗?case 2 呢?case 3 呢?
Runloop
第五题
class Object: NSObject {
@objc
func fun() {
debugPrint("\(self) fun")
}
}
var runloop: CFRunLoop!
let sem = DispatchSemaphore(value: 0)
let thread = Thread {
RunLoop.current.add(NSMachPort(), forMode: .commonModes)
runloop = CFRunLoopGetCurrent()
sem.signal()
CFRunLoopRun()
}
thread.start()
sem.wait()
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
CFRunLoopPerformBlock(runloop, CFRunLoopMode.commonModes.rawValue) {
debugPrint("2")
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: {
debugPrint("1")
let object = Object()
object.fun()
// CFRunLoopWakeUp(runloop)
})
}
RunLoop.current.run()
这样会输出什么呢?
答案
第一题:
"main thread: true"
"main queue: false"
看到主线程上也可以运行其他队列。
第二题:
这道题要想出效果比较不容易。所以放一张截图:
看,主队列居然不在主线程上啦!
这里用的这个 API dispatchMain()
如果改成 RunLoop.current.run()
,结果就会像我们一般预期的那样是两个 true
。而且在 command line 环境下才能出这效果,如果建工程是 iOS app 的话因为有 runloop,所以结果也是两个 true
的。
第三题:
GCD:
"entry"
"beforeTimers"
"beforeSources"
"beforeWaiting"
"afterWaiting"
"exit"
"entry"
"beforeTimers"
"beforeSources"
"beforeWaiting"
"afterWaiting"
0
1
2
3
4
...
996
997
998
999
"exit"
"entry"
"beforeTimers"
"beforeSources"
"beforeWaiting"
"afterWaiting"
"exit"
"entry"
"beforeTimers"
"beforeSources"
"beforeWaiting"
OperationQueue
"entry"
"beforeTimers"
"beforeSources"
"beforeWaiting"
"afterWaiting"
0
"exit"
"entry"
"beforeTimers"
"beforeSources"
"beforeWaiting"
"afterWaiting"
1
"exit"
"entry"
"beforeTimers"
"beforeSources"
"beforeWaiting"
"afterWaiting"
2
"exit"
"entry"
"beforeTimers"
"beforeSources"
"beforeWaiting"
"afterWaiting"
...
这个例子可以看出有大量任务派发时用 OperationQueue 比 GCD 要略微不容易造成卡顿一些。
第四题:
这个题其实还挺实用的,答案是两种语言的每个 case 都会 >< [NSArray copy]
那个概率低一点儿,但是稍微跑一会儿还是很容易触发的。
第五题:
上面的代码直接运行出来是
"1"
"<Runloop.Object: 0x102d05be0> fun"
如果把 object.fun()
改成 object.perform(#selector(Object.fun), on: thread, with: nil, waitUntilDone: false)
的话就能 print 出来 2 了,就是说 runloop 在 sleep 状态下,performSelector 是可以唤醒 runloop 的,而一次单纯的调用不行。有一个细节就是,如果用CFRunLoopWakeUp(runloop)
的话,输出顺序是1 fun 2
而用 performSelector
的话顺序是 1 2 fun
。我的朋友骑神的解释:
perform调用时添加的timer任务会唤醒runloop去处理任务。但因为CFRunLoopPerformBlock的任务更早加入队列中,所以输出优先于fun
题解
仓鼠本来想厚颜无耻地写一篇付费文章,然后把题解部分作为付费部分,估计肯定赚一波小钱:)但是因为仓鼠比较菜,心虚怕会说错,所以我就不提供题解啦~ 欢迎大家在评论区讨论吧,我也会放出朋友们的解答链接~~
后记
本文所有 credit 归我的同事金司机所有。虽然不是付费文章,但是本文所有的打赏我也会转给我的同事金司机~ 如果大家觉得这些题有趣的话欢迎打赏哈哈哈~
另外,仓鼠公司也在招人。因为以前写博客被喷过,至今心有余悸;所以怕公司被喷,我不敢说是哪个公司了(有这么招人的吗?) 总之就是一个外企互联网公司,坐标北京。大部分 swift,很显然我的同事和老大技术水平都非常强,仓鼠在这是最菜的。而且大家都特别 nice,公司福利待遇也是业内顶尖水平的。我们的面试题非常注重实操,主要都是现场写代码实现小功能,100% 是平常工作最常使用的,绝不会使用上面这些奇奇怪怪的题,大家可以放心。要求的话,现阶段只招比较 senior 的人,基本上要求真的有 4 年以上的经验,有大厂的经历或者学校背景好的话会比较好~ 不用太担心对语言的要求,不会 swift 是没问题的,英语也不是大问题。有兴趣的朋友欢迎私信仓鼠,我可以解答关于工作和面试的各种问题~ 如果是因为这篇文章带来的推荐奖,我也会全部转给我的同事金司机,说到做到:)