我后来重新翻看这套 2018 年京东 iOS 工程师秋招笔试题的回忆版时,第一个念头是:出题人相当克制,但也相当凶。整卷没有去追逐当时刚流行起来的 ARKit、Core ML 这类新东西,而是把镜头死死对准 Objective-C 运行时、GCD、内存管理、UI 事件链、架构设计这些日常工作中天天都会碰、却很少有人能讲透的基础设施。今天我把这套题涉及的核心知识点重新整理了一遍,补上了参考答案、代码示例和个人踩坑记录。这份内容既适合当年错过秋招、现在补基础的 iOS 开发,也适合所有准备一线大厂面试的人在半个月内做一次快速检索。
1. 这套笔试题到底想考察什么能力
1.1 题型结构与答题时间分配参考
先说题型。网上能找到的回忆版基本是四类:单选/多选、填空、简答题、手写代码题。选择题大概 15 到 20 道,覆盖 weak 和 assign 的区别、copy 关键字、KVC/KVO 触发时机、GCD 队列类型、App 生命周期、Auto Layout 优先级等。这部分拼的是基础扎实程度,很少出现偏怪题,但会在容易混淆的概念上反复挖坑。
简答题通常 3 到 5 道,常见方向包括:消息转发流程是怎么样的、循环引用怎么排查、TableView 滑动卡顿怎么定位、HTTPS 的握手过程、架构上为什么选 MVVM 不选 MVC。手写代码题则比较直接,我见过的高频题有:实现一个 LRU 缓存、用 GCD 实现多线程下载、数组去重、字符串反转、两个有序数组合并。整场笔试的时间压在 90 分钟左右,选择题不能恋战,简答题要把关键点写出来,代码题最好先确认边界条件再落笔。
1.2 从题目反推岗位画像
京东这套笔试题最大的特点,是不太考“背下来就能答”的框架 API,而是反复考察“框架底层发生了什么”。比如问你addObserver:forKeyPath:options:context:之后系统做了什么,表面上是 KVO 使用题,实际上是想看你对运行时动态子类、isa 指针、方法 override 有没有系统认识。这种考法说明团队希望招的不是“会拼界面的熟练工”,而是能解决线上疑难杂症、能优化性能、能设计公共组件的人。
2018 年这个时间点也很有意思。iOS 11 刚普及,安全区、Masonry 改自动布局、刘海屏适配,全都是新问题。工程上又流行组件化、热修复、动态化。笔试里加入证书签名、混合开发、蓝牙权限这些问题,其实是在筛选“见过线上坑”的人。如果只是背题,不把这些知识点串成一个完整的系统,很容易在追问环节被拆穿。这个问题无论放在哪个年份,逻辑都一样:面试官想找一个“以后能放心把线上模块交给你”的人,而笔试是成本最低的筛子。
2. 高频考点逐个过:Objective-C 运行时与内存管理
2.1 消息发送机制:把 objc_msgSend 讲到肌肉记忆
Objective-C 的方法调用本质是发送消息。[self doSomething]在编译期会被改写成objc_msgSend(self, @selector(doSomething))。笔试里最典型的问题不是要你写出函数名,而是要你说清查找过程。
完整流程是这样:第一步,在接收对象的 isa 指向的类对象里查找方法缓存,如果命中,直接拿到函数指针调用。第二步,缓存没有命中,就去当前类的方法列表里遍历,找到就执行,并写入缓存。第三步,当前类没有,按继承链依次向上找,直到 NSObject。第四步,整个继承链都找不到,触发动态方法解析,允许你用resolveInstanceMethod:补一个实现。第五步,如果动态解析没有解决问题,进入快速转发流程,调用forwardingTargetForSelector:把消息转给备用对象。第六步,还没人接收,就进入完整转发,走methodSignatureForSelector:和forwardInvocation:,把消息封装成 NSInvocation 分发。
回答问题的时候,不要只答“找方法”,要答出“缓存 → 方法列表 → 继承链 → 动态解析 → 转发”这条完整链路。面试官追问“为什么消息转发能防止崩溃”时,你就能顺便提一句:只要在forwardInvocation:里把 invocation 转给一个能响应该 selector 的对象,App 就不会因为 unrecognized selector 而 crash。笔试中还有一个常见变形题:为什么objc_msgSend的返回值在 ARM64 上有时候放在 x0、有时候放在浮点寄存器。这个问题是加分项,能说清楚“参数和返回值根据 ABI 约定走不同寄存器”就很好了。
2.2 KVC 与 KVO:别只答“能监听属性变化”
KVC 的实现相对直观:valueForKey:会先找getKey、key、isKey这些 getter;没有 getter 就找实例变量_key、_isKey、key、isKey;再找不到就调用valueForUndefinedKey:。写入一侧同理,优先找setKey:,找不到就找_key或key成员变量。笔试会考的点是setValue:forKey:中传 nil 会怎样。默认会调用setNilValueForKey:,而 NSObject 的实现是抛异常NSInvalidArgumentException。如果你有业务需要传 nil,应该 override 这个方法。
KVO 的实现则是运行时 + 动态子类。添加观察者后,系统会动态创建一个当前类的子类,类名会变成类似NSKVONotifying_ClassName,然后把对象的 isa 指针指向这个子类。重写对应属性的 setter,在赋值前后分别调用willChangeValueForKey:和didChangeValueForKey:。这样做能保证自动触发通知,也能保证监听回调发生在正确的时机。
笔试里常考一个问题:“在 block 里修改触发 KVO 的属性,会收到通知吗?” 答案是会,只要走的是 setter。但如果你直接修改成员变量_name = ...,则不会触发。另一个常见追问是“KVO 能监听数组的 addObject 吗?” 直接对NSMutableArray属性调用addObject:不会触发,需要调用mutableArrayValueForKey:拿到代理数组,再往代理数组里添加元素。这是我实际开发中踩过很多次的坑,包括聊天列表消息插入、购物车商品数量变化,全都有这个问题。
2.3 Block 循环引用:笔试和线上故障的高发区
循环引用在笔试中几乎必考,而且通常不是简单问“为什么”,而是给你一段代码,让你挑错。最常见的错误写法是这样:
self.name = @"JD"; self.didTapBlock = ^{ NSLog(@"%@", self.name); };self 持有 didTapBlock,block 捕获了 self,于是 self → block → self,谁也释放不了。修正的第一反应是使用__weak:
__weak typeof(self) weakSelf = self; self.didTapBlock = ^{ __strong typeof(weakSelf) strongSelf = weakSelf; if (strongSelf == nil) { return; } NSLog(@"%@", strongSelf.name); };这里用__strong承接 weakSelf 是有讲究的。如果 block 内部是一个多行异步操作,只用 weakSelf,在执行过程中 self 被释放,后面的代码会拿到 nil,可能引发逻辑错误。先让strongSelf在 block 生命周期内保持住,操作完成后再释放,这个处理更安全。
还有一道关于NSTimer的题也很典型:如果NSTimer的 target 是 self,selector 里又执行了invalidate,是否安全?答案是不一定。timer 会强持有 target,你不手动invalidate就会出现 target 无法释放。即使你在dealloc里写了invalidate,问题更大:因为 target 被持有,dealloc 根本不会执行。正确做法是在合适的时机invalidate,比如viewWillDisappear:,同时配合 block-based timer 让 block 里使用 weakSelf。
3. 多线程与网络:并发题怎么答到点子上
3.1 GCD 的队列、栅栏和信号量
多线程部分,京东这套题很偏爱 GCD。核心问题一般有三个:sync和async的区别、串行队列和并发队列的区别、dispatch_barrier_async的作用。
先说sync和async。同步提交会阻塞当前线程,直到 block 执行完毕;异步提交不会阻塞,当前线程继续往下走。这里的坑在于死锁。最常见的死锁代码是:
dispatch_sync(dispatch_get_main_queue(), ^{ // 死锁 });这段代码在主线程执行,向主队列提交了一个同步 block,主队列等待 block 执行,block 等待主线程空闲,于是互相等。同理,在任意串行队列里向自身提交dispatch_sync也会死锁。如果你负责的模块里出现了卡死,优先检查是否有往主队列同步提交的代码。
dispatch_barrier_async常被用来做读写分离:读可以并发,写必须独占。写法是用一个自定义并发队列:
dispatch_queue_t queue = dispatch_queue_create("com.jd.rwqueue", DISPATCH_QUEUE_CONCURRENT); - (id)readData { __block id result = nil; dispatch_sync(queue, ^{ result = [self.data copy]; }); return result; } - (void)writeData:(id)newData { dispatch_barrier_async(queue, ^{ self.data = newData; }); }栅栏 block 会等待它之前的所有 block 执行完,再单独执行,执行完后再让后续并发 block 开始。但要注意:只有使用自己创建的并发队列,dispatch_barrier_async才会体现“栅栏”语义;如果对全局并发队列使用,效果会有问题。这是我在一次项目代码 review 中发现过的坑。
3.2 线程安全:atomic 解决不了业务并发
线程安全题目,标准答案是四个层次:@synchronized、NSLock、pthread_mutex、os_unfair_lock。让我逐个说清楚。
@synchronized写法最方便,编译器会把它转成一个objc_sync_enter/exit的递归锁。因为支持重入,同一个线程可重复加锁,但成本偏高,适合低频保护。NSLock是普通互斥锁,不能重入,使用时要注意先加锁、后解锁,任何提前 return 都可能导致锁没释放。pthread_mutex是程序员可控性最强的锁,具备正常/递归/条件等多种类型。os_unfair_lock是后来为了替代有问题的OSSpinLock推出的,低优先级线程持有锁时,高优先级线程会主动休眠,避免优先级反转。
还有一个高频概念题:属性用atomic修饰就线程安全吗?不是。atomic只保证 getter/setter 内部是原子的,也就是读写属性值本身不会出现半初始化状态。但对于NSMutableArray这类容器,你getter拿到一个数组后,再多线程往数组里 addObject,atomic管不了。所以面试答题一定要强调“atomic 保证赋值原子性,不保证容器操作原子性”。
网络层常见的并发场景是“多个请求完成后统一刷新 UI”。很多人用dispatch_group_notify,我建议掌握得再深一点。比如用信号量控制最大并发数,同时提交 20 个下载任务,限制同时最多下载 5 个:
dispatch_semaphore_t sem = dispatch_semaphore_create(5); dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); for (NSInteger i = 0; i < 20; i++) { dispatch_async(queue, ^{ dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER); // 执行下载 dispatch_semaphore_signal(sem); }); }信号量初始为 5,相当于只有 5 个并发坑位;一个任务结束就signal,释放一个坑位。这是控制并发度最直接的办法,比自定义 NSOperation 并发数要轻量得多。
3.3 网络层考点:HTTPS 握手、超时与重试
网络层笔试一般分两块:一块是理论,一块是工程方案。理论题最常问 HTTPS 握手,我会按“非对称加密传递密钥、对称加密传输数据”这个主线来答。客户端发请求后,服务端返回证书;客户端验证证书的签名链、域名、有效期,通过后用证书里的公钥加密一个临时会话密钥发给服务端;服务端用私钥解密得到会话密钥,后续数据用会话密钥进行对称加密。这样兼顾了安全性(私钥不经过网络)和性能(对称加密更快)。
工程方案题最喜欢问“弱网下怎么做网络请求”。2018 年很多 App 已开始用 WatchDog 和超时管理。我的答案一般拆成四层:层级一是合理的超时时间,一般 GET 请求设 15 秒到 20 秒,POST 上传可以更长;层级二是自动重试,只在明确网络失败和超时时重试,并且用指数退避加抖动,避免重试风暴;层级三是缓存策略,对 GET 请求做按 URL 缓存,网络错误时先返回缓存再重新拉取;层级四是客户端的网络状态监听,切换 Wi-Fi 到 4G 时取消不必要请求、重新建立长连接。
如果你在笔试时能提到“用 Charles 抓包看接口响应,确认超时时间是客户端预设导致的还是服务端响应慢导致的”,这道题会加分不少。这代表你有真实的线上问题排查经验,而不只是背概念。要注意的是,抓包工具在正式实际工作中要合理使用,仅用于自己负责的应用和调试环境。
4. UIKit 与生命周期:一拿分就能拉开差距的界面题
4.1 视图控制器生命周期
界面层题目最大众化的是“ViewController 生命周期”。标准流程是init→loadView→viewDidLoad→viewWillAppear→viewDidAppear→viewWillDisappear→viewDidDisappear→dealloc。笔试要拿高分,就要说出每个阶段适合干什么。
loadView一般不要手动调用,除非你要完全用代码创建根视图。正确做法是在viewDidLoad里配置子视图,在viewWillAppear里做数据刷新和导航栏变化,在viewDidAppear里做打点统计或开始动画,在viewWillDisappear里取消网络请求、暂停定时器。很多人把数据请求放在viewDidLoad,如果每次进入页面都要刷新,放在viewWillAppear更合适。
还有一个高频变体:两个 ViewController 之间用 push 和 present 切换时,生命周期回调顺序有什么差别。Push 是 A.viewWillDisappear → B.viewWillAppear → A.viewDidDisappear → B.viewDidAppear。Present 的前半段和 push 类似,但 A 的 view 不一定离开窗口,所以viewDidDisappear的触发时机和 dismiss 动画有关。能把这些差异说细,说明你处理过多页面交互和转场动画。
4.2 响应链与事件传递:hitTest 才是底层答案
“点击屏幕后,系统如何找到目标 View”是 iOS 笔试题库里的经典题。入口方法是hitTest:withEvent:,它从视图层级根部开始,逆序遍历子视图,返回最终命中的视图。系统默认实现大概是这样:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) { return nil; } if (![self pointInside:point withEvent:event]) { return nil; } for (UIView *subview in self.subviews) { CGPoint convertedPoint = [self convertPoint:point toView:subview]; UIView *hitView = [subview hitTest:convertedPoint withEvent:event]; if (hitView) { return hitView; } } return self; }只要理解了这段代码,很多面试题都能解。比如子 View 超出了父 View 的 bounds,点击超出部分不响应,因为pointInside:在父视图这层就返回 false。比如父视图关闭了userInteractionEnabled,子视图再设置也点不中。笔试中我见过一个很刁钻的题目:“如何让一个超出父视图范围的子按钮仍然可以点击?”答案很简单,重写父视图的pointInside:或hitTest:withEvent:,当 point 落在子视图范围内时返回 true。
事件找到 target view 之后,会沿着响应链向上传递:view → superview → viewController → window → UIApplication → app delegate。如果某个手势和 button 同时存在,会优先触发手势识别器。这个点经常被问“为什么 button 上加了 UITapGestureRecognizer 后点击没反应”,答案就是手势识别器拦截了 button 的事件。
4.3 TableView 复用与滑动卡顿排查
2018 年大厂笔试特别喜欢考 TableView 复用原理。标准回答是:dequeueReusableCellWithIdentifier:会从复用池取 cell,如果没有,再走注册或代码创建的路径。手动创建 cell 时,一定要先判断返回是否为空,不能每次都重新 alloc。iOS 15 之后虽然有UICollectionView.selfSizingInvalidation相关的 API,但核心复用思想没变。
另一道必问的题是“TableView 卡顿怎么排查”。我通常会从 CPU 和 GPU 两条线答。CPU 侧看 cell 里有没有大量圆角裁剪、离屏渲染、动态计算文字高度、同步加载图片;GPU 侧看有没有透明图层叠加、过度绘制、大图解码。优化手段包括:提前算好并缓存行高、图片异步解码后缓存、用 layer 的cornerRadius配合masksToBounds时避免太频繁、减少主线程工作量。
我记得京东这套题里有一道相似题,给了几种错误代码,让你判断哪些会导致卡顿。我当时看到“在tableView:cellForRowAtIndexPath:里用NSDateFormatter做时间格式化”这个选项,立刻意识到这是最常见的隐藏性能问题。NSDateFormatter初始化成本较高,正确做法是全局复用,或者用 timestamp 缓存文案。这种题考的不是你会不会写 TableView,而是你有没有做过真实优化。
4.4 iOS 11 适配:安全区与导航栏新变化
2018 年的电话面试和笔试总绕不开 iOS 11 适配,放到现在也依然有借鉴意义。iOS 11 引入了safeAreaLayoutGuide,原先用 frame 写死 64 导航栏高度的代码很容易踩坑。正确做法是使用view.safeAreaInsets和约束来布局。笔试中会问“iPhone X 底部 home indicator 如何适配”,答案很简单:不要让你的关键操作按钮放在安全区之外,底部留出safeAreaInsets.bottom的距离,避免用户误触。
导航栏还有个坑是prefersLargeTitles和UISearchController一起使用时,navigation bar 高度会动态变化,导致 scrollView contentInset 异常。这个问题适合结合 Auto Layout 来答:直接依赖安全区约束,而不是依赖固定导航栏高度。笔试时把这些适配点写出来,说明你是有真实机型测试经验的,而不是只在模拟器里写代码。
5. 架构、签名与工程化:笔试题里的“加分项”
5.1 架构选型:MVC 到 MVVM 再到组件化
京东的笔试对架构题的设置很有意思,不让你直接说哪种架构最好,而是给一个业务场景,问你会怎么组织代码。如果只是回答“用 MVC,Controller 太胖就用 MVVM”,是拿不到高分的。你得说清楚 MVC 的不足:ViewController 容易积累 UI 逻辑、网络回调、数据解析、导航跳转,最后变成几千行。
MVVM 的核心区别是把数据加工逻辑搬进 ViewModel。Controller 只负责绑定和转发:
// ViewModel - (void)loadData { [self.apiClient requestWithCompletion:^(NSArray *items) { self.cellModels = [self buildCellModelsFromItems:items]; self.onDataUpdated(); }]; }ViewModel 不 import UIKit,方便单测,这是 MVVM 最大的优势。组件化则是在更大规模下,把业务模块拆成独立 target,通过中间层通信。2018 年比较流行的是路由 + 协议注册,通过 URL 或者 protocol 调用,解耦业务模块。答题时如果能提到“组件化不是一开始就做,而是业务膨胀到多人协作成本很高时再拆分”,会显得务实得多。
还有个常被拿来做对比的考点是“跨平台方案怎么选”。2018 年 React Native、Weex、Flutter 各有拥趸,笔试一般不会深入源码,而是问“原生和跨平台共存时如何设计”。我的建议是:核心流量、强交互页面用原生;活动页、运营页用动态化方案;跨平台页面通过 WebView/JSCore bridge 和原生能力打通。把“混合开发不是二选一,而是分层选择”这个思路写进去,会让答题层次更高。
5.2 证书、签名与自动化打包的易错点
签名和上架是整个 iOS 工程化里最容易出问题、也最容易被笔试忽略的模块。我见过好几道题都在问“开发证书和发布证书的区别”“描述文件过期怎么处理”“真机调试时提示未签名怎么办”。
开发证书需要一个 Development 证书加一个 Development 描述文件,描述文件里绑定你的 App ID、设备 UDID、权限 entitlement。发布证书一般分 App Store、Ad Hoc、Enterprise。App Store 发布只能通过 TestFlight 和 App Store Connect;Ad Hoc 可以装到指定设备;Enterprise 是企业内部使用,不经过商店分发。描述文件的有效期通常是一年,证书过期后不是只换了描述文件就够,还要去开发者后台重新生成相应证书文件。
自动化打包这里,建议掌握 Fastlane 的基本流程。至少要知道fastlane gym负责构建和打包,fastlane match负责同步证书,fastlane pilot负责上传 TestFlight。笔试里有一道高频问法是“打包签名冲突如何排查”,我会先检查Keychain里证书是否安装、私钥是否缺失,再检查描述文件里的 App ID 和项目中的 Bundle ID 是否一致,最后检查entitlements文件里有没有多余权限。这三步排查顺序能解决 90% 的签名问题。
5.3 混合开发与系统能力场景题
工程化笔试偶尔会结合真实业务场景,比如“App 内嵌 WebView 如何和原生互相调用”“iOS 蓝牙开发怎么处理系统权限”。WebView 交互题,我会先说 WKWebView 的WKScriptMessageHandler,再补充 JavaScriptCore 注入。因为 WKWebView 是现代 iOS 首选,它比 UIWebView 内存占用更低、崩溃率更低。笔试题常问的“为什么网页 alert 在 WebView 里弹不出来”,多半是因为没有实现WKUIDelegate的runJavaScriptAlertPanelWithMessage。
蓝牙题则是热点之一。热搜词里那句“系统级蓝牙状态和 app 级蓝牙状态能区分出来吗”很典型。从 API 角度看,CBCentralManager.state反映系统蓝牙适配器开关状态,而 App 是否被允许使用蓝牙,还需要看CBManager.authorization。两者不是一回事。iOS 13 以后有显式权限枚举,通过 Info.plist 里的NSBluetoothAlwaysUsageDescription申请权限。CBCentralManager 在初始化时会回调状态更新,必须先判断.poweredOn,再去做扫描和连接,否则会出现奇怪的问题。答题时把这个“系统开关和 App 授权是两层”的意识写出来,面试官会认为你有真正的蓝牙开发经验。
6. 复盘与冲刺建议
6.1 笔试中最容易丢分的几个点
我把自己做这套题时的错误整理成了一张避坑清单,分享几个印象最深的。
第一,简答题只写结论不写原因。比如问“为什么会产生循环引用”,有人只写一句“block 强持有 self”,但要拿满分,必须画出引用关系链,说清 self → block、block → self 这个环。第二,代码题没考虑边界条件。像“数组去重”很多人第一反应用 Set,但忘了如果要求保持原顺序,需要 NSMutableSet 配合遍历,而不能直接转 NSOrderedSet。第三,HTTP 和 HTTPS 混为一谈。有人答“HTTPS 就是加了证书的 HTTP”,却没有说对称加密和非对称加密的组合方式。面试官追问一句“客户端如何验证证书”,就露馅了。
还有一个特别容易丢分的位置是“多选”里含关键词“下列说法正确的是”。这种题不会让你白捡分,经常四个选项里有两个看起来都对。应对方法是平时把容易混淆的概念主动做成对照表:weak vs assign、strong vs copy、frame vs bounds、sync vs async、串行 vs 并发。我备考时会把每对概念用“一句话定义 + 一个使用场景 + 一个反例”的模板写一遍,效果比反复刷题要好。
6.2 按时间线安排的备考顺序
如果你只有两周时间准备,我的建议是这样的。
第一周先补“地基”:第一天到第三天死磕 Objective-C 运行时和内存管理,把消息发送流程、KVO 动态子类、Block 循环引用各写一遍代码验证。第四天到第五天专门过 GCD 和线程安全,所有锁都手写一遍,至少写一个 signal 和 mutex 的 demo。第六天全天看 UIKit 事件机制和 TableView 优化,把hitTest换成自己的理解写一遍。第七天把 HTTPS 握手、证书验证、缓存策略整理成笔记。
第二周开始做“人和题”的结合:前两天刷历年大厂 iOS 笔试题,不求多,一天两套,但每道题都要追问自己三个为什么。第三天开始练手写代码,直接在纸上或纯文本编辑器里写,不要依赖 Xcode 补全。第四天复习工程化知识点,重点看签名、描述文件、自动打包、混合开发。第五天做整套模拟笔试,计时 90 分钟,模拟完再回头补缺。第六天把自己写的代码整理进一个公开仓库,逼自己把注释写清楚。第七天放松一点,只过错题本和个人笔记。
6.3 一些个人体会
这套 2018 年的题库放到今天,很多知识模块依然适用。我当年第一次做的时候,以为自己基础挺好,结果在 KVO 触发时机、GCD 死锁、签名证书三个地方连续翻车。后来我养成了一个习惯:每学一个 iOS 知识点,都在 demo 工程里写一个最小复现,然后在代码里注释出“底层到底发生了什么”。比如 KVO 的 isa-swizzling,我写了一个子类继承验证,打印出 class 方法前后返回的类名差异,才真正弄明白。
最后分享一个小技巧:备考 iOS 笔试千万不要背“标准答案”,要背“追问链”。把每个考点像洋葱一样剥开一层,比如先问“copy 和 strong 有什么区别”,再问“什么时候 NSString 要用 copy”,再问“深拷贝和浅拷贝对内存有什么影响”,再问“属性修饰符在 Swift 里对应什么”。能连续回答四层以上,笔试和后续面试基本扛得住。这套京东笔试题真正想看到的,不是你会背多少个 API,而是你能不能把一个知识点从表面说到原理,从原理说到应用,再从应用说到边界条件。能做到这一层,不管题库怎么更新,你都能应对。