ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

2018京东iOS秋招笔试题复盘:Runtime、GCD与内存管理

2018京东iOS秋招笔试题复盘:Runtime、GCD与内存管理 我后来重新翻看这套 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 持有 didTapBlockblock 捕获了 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 是 selfselector 里又执行了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拿到一个数组后再多线程往数组里 addObjectatomic管不了。所以面试答题一定要强调“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 再到组件化京东的笔试对架构题的设置很有意思不让你直接说哪种架构最好而是给一个业务场景问你会怎么组织代码。如果只是回答“用 MVCController 太胖就用 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 ConnectAd 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而是你能不能把一个知识点从表面说到原理从原理说到应用再从应用说到边界条件。能做到这一层不管题库怎么更新你都能应对。
返回列表