拓冰建站拓冰建站
首页 / 资讯中心 / 正文

小红书iOS秋招笔试复盘:考点拆解与避坑指南

2023年秋招那会儿我印象最深的一场笔试就是小红书的iOS开发岗。倒不是因为它难到离谱而是这套题出得特别“iOS”——基础题不绕弯子但特别细代码题也不玩偏门就是考察你平时有没有真写过、真调过。当时同一批的同学考完普遍反馈题量不小坑位隐蔽代码题看着眼熟但想拿满分需要点真功夫。这篇文章把我当时那场笔试的题目类型、考点分布、手写题思路、系统设计题的答题框架以及考后复盘踩过的坑全部整理出来。如果你是准备iOS校招、或者正在面小红书这类内容社区大厂移动端岗位这篇复盘可以直接拿来当复习提纲用。1. 笔试整体印象与题型分布1.1 三个环节与时间分配小红书的秋招笔试是牛客网线上作答全程摄像头监控总共时长我记得是120分钟对iOS岗单独出题没有和Java、前端共用一套卷子。整体结构分三块选择题、手写代码题、系统设计/简答题。选择题大约20道单选和多选混合覆盖Objective-C、Swift、操作系统、网络协议、数据结构这些方向。手写代码题是2道一道偏数据结构算法一道偏iOS并发场景设计。最后还有一道开放性的系统设计题给一个业务场景让你谈方案。从题型设计就能看出来小红书想要的不是只会刷LeetCode的人而是真正理解iOS运行时机制、能解决实际渲染和性能问题的工程师。我用最终通过的复盘时间线反推了一套时间分配方案选择题25分钟以内拿不准的先标记不要恋战手写代码题50分钟两道题各25分钟如果卡住10分钟没思路就写暴力解保底系统设计题35分钟宁可少写代码也要把方案架构画清楚剩余10分钟检查有没有漏答选择题有没有串行这个分配比例的核心在于选择题再纠结也就1分但系统设计题一写就是20分起步性价比完全不同。1.2 这套笔试题在考什么我考完和几个同期进面的同学交流发现一个共识小红书笔试很看重“应用层之外的底层理解”。比如选择题里会问RunLoop的Source0和Source1区别、ARC下对象什么时候释放、dealloc里能不能访问弱引用这类偏底层的细节。这背后其实反映了小红书App的业务特点。小红书客户端的信息流非常重图片加载、视频播放、笔记编辑、直播互动几乎每条业务线都涉及性能优化和资源调度。如果工程师对内存管理、线程调度、视图渲染周期没有体系化的理解线上问题排查基本靠猜。所以大家复习时如果只刷题库不看苹果官方文档和Runtime源码笔试会非常吃亏。这套题的筛选逻辑很直接你日常写代码的时候到底有没有想过每一行在底层做了什么。2. 核心考点深度拆解iOS基础才是分水岭2.1 内存管理与对象生命周期选择题里至少有4道直接考内存管理这比例相当高了。最典型的一道是给出几段代码判断是否有循环引用以及打印结果。循环引用这个考点平时用ARC写业务可能感受不深但笔试特别爱考Block和Delegate组合的场景。比如一个ViewController持有BlockBlock内部又捕获了self然后self又强持有这个Block的属性。常规的解决方式是weak var weakSelf self但笔试会进一步问如果Block内部是异步执行执行到一半self已经释放了怎么办这就要考虑到weakSelf是否可能为nil以及是否需要strongSelf来保证生命周期。我当时写了一段参考示例class DetailViewController: UIViewController { var onDone: (() - Void)? var data: Data? func loadData() { // 正确写法weak strong onDone { [weak self] in guard let strongSelf self else { return } strongSelf.refreshUI() } } deinit { print(DetailViewController deinit) } }这里有个新手容易犯的错在deinit里访问属性或调用方法。笔试会考一个概念——对象进入dealloc阶段后并不代表完全销毁但如果访问了已经释放的弱引用或正在销毁的属性就会引发不可预知行为。实际上deinit里只应该释放非托管资源比如监听通知的移除、KVO的移除、定时器的invalidate。还有一道题考autoreleasepool。场景是在一个for循环里创建大量临时对象如果不包autoreleasepool这些临时对象会堆积在自动释放池里直到当前RunLoop循环结束才释放。正确写法是在循环体内部加autoreleasepool让每一批临时对象及时释放。这个点平时做图片处理或者批量数据解析时会遇到但笔试很容易忽略。2.2 多线程与RunLoop多线程的考题是另一个重头。印象最深的一道题是在主队列调用DispatchQueue.main.sync会怎样答案是死锁。因为主队列是串行队列sync阻塞当前线程等待任务执行而任务又在主队列里永远排不上队就僵住了。这个考点其实是想考察两个维度一是对GCD队列类型的理解二是对线程死锁的敏感度。实际业务里这个场景不常见但类似的写法会以变形出现比如在viewDidLoad里用信号量等待网络请求返回一旦网络回调在主队列也容易卡死。死锁的本质两个任务互相等待对方释放资源。 GCD中表现为当前线程阻塞等待sync任务完成而sync任务又在当前线程的串行队列中永远无法执行。除了GCD笔试还考了NSOperationQueue和GCD的选择。题目描述大概是有多个图片下载任务要求同时最多只能有3个下载请求下载完成后再通知主线程刷新UI。最优解是用OperationQueue设置maxConcurrentOperationCount 3因为Operation支持依赖关系可以方便地设置任务之间的先后顺序。如果全部用GCD实现需要自己用信号量控制并发数代码会复杂很多。RunLoop那部分考得让我有点意外。题目问在UIScrollView滚动过程中Timer为什么可能不触发这个问题的标准回答是RunLoop在滚动时会切换到UITrackingRunLoopMode默认的Timer注册在kCFRunLoopDefaultMode下滚动模式下不执行。解决办法是把Timer加到NSRunLoopCommonModes或者改用CADisplayLink。所以复习iOS基础的时候不能只盯着UIKitRunLoop、多线程、内存管理这三个点是组合拳笔试经常两三道题联动考。2.3 自动布局与UIStackView细节关于UI的考点里选择题考了UIStackView的distribution、alignment这些属性的含义还有一个Case是在一个水平StackView里放两个Label左边的Label内容长短不固定希望右边的Label始终靠右问怎么设置约束优先级。这道题本质是在考Auto Layout的Content Hugging Priority和Compression Resistance Priority。简单说Content Hugging Priority越高视图越倾向于保持自身内容大小不拉伸Compression Resistance Priority越高视图越倾向于不被压缩。我当时选了给左边Label降低Horizontal Content Hugging Priority默认250让它在空间紧张时被拉伸/压缩而右边Label保持默认751的优先级。但笔试真正的考点在于如果两个Label的优先级都默认StackView会按添加顺序决定拉伸优先级容易导致右边Label被挤出屏幕。经验总结设置StackView内部约束时要主动调整两个优先级而不是依赖默认值。 这是平时做自适应Cell高度最容易忽略的细节。小红书这类图文社区App首页卡片高度会随文本内容和图片数量动态变化iOS开发中对自适应布局的要求极高。笔试考这个点其实反映了他们日常开发中真实遇到的痛点。2.4 性能优化与电池优化有一道多选题问的是App启动时间优化手段选项包括冷启动时减少动态库加载、将不影响首屏的初始化任务延迟到首帧渲染后、使用load方法做配置注入、避免主线程执行大文件IO。前三项都是对的第四项也对但真正需要注意的是减少App启动时加载的动态库数量是硬指标微信这类超级App甚至通过合并动态库、精简二进制来优化。电池优化这个方向考得挺新颖。小红书App视频播放和定位使用场景多考题问在定位功能中连续获取用户位置但精度要求不高时应该怎么设置CLLocationManager参数答案是降低desiredAccuracy、增大distanceFilter因为GPS高精度定位最耗电。另一个小问是后台播放音频时如何配置AVAudioSession的Category让App在后台继续播放视频声音。这说明小红书实际开发中真的有接入CoreLocation和后台任务笔试出题不是凭空想象是结合真实业务场景来的。我当时答这个部分靠的就是平时用Xcode的Energy Log做电量分析的经验——真调过心里才有底。3. 手写题复盘从思路到代码3.1 LRU缓存实现第一道代码题是经典的LRU缓存设计要求实现get和put两个方法容量满时淘汰最久未使用的键值对。这道题在LeetCode上是146题但在小红书笔试中要求只能用Swift写且要求时间复杂度O(1)。Swift实现LRU缓存的标准做法是字典双向链表。字典负责O(1)查找双向链表负责O(1)删除和移动节点我当时的实现思路建立Node类包含key、value、previous、next指针建立LRUCache类持有head和tail哨兵节点get命中时把节点移到链表头部put时如果key存在更新value并移到头部如果key不存在创建节点放到头部并检查容量是否超标超标则删除尾部节点参考代码class Node { let key: Int var value: Int var prev: Node? var next: Node? init(_ key: Int, _ value: Int) { self.key key self.value value } } class LRUCache { private var cache: [Int: Node] [:] private let capacity: Int private let head Node(0, 0) private let tail Node(0, 0) init(_ capacity: Int) { self.capacity capacity head.next tail tail.prev head } func get(_ key: Int) - Int { guard let node cache[key] else { return -1 } moveToHead(node) return node.value } func put(_ key: Int, _ value: Int) { if let node cache[key] { node.value value moveToHead(node) } else { let node Node(key, value) cache[key] node addToHead(node) if cache.count capacity { removeTail() } } } private func addToHead(_ node: Node) { node.prev head node.next head.next head.next?.prev node head.next node } private func removeNode(_ node: Node) { node.prev?.next node.next node.next?.prev node.prev } private func moveToHead(_ node: Node) { removeNode(node) addToHead(node) } private func removeTail() { if let node tail.prev, node ! head { removeNode(node) cache.removeValue(forKey: node.key) } } }这道题想要满分关键不只是实现正确还要注意边界容量为0怎么处理、重复put相同key怎么处理、get未命中返回-1。笔试判分往往是跑测试用例边界漏一个就扣分。3.2 二叉树层序遍历变体第二道手写题是二叉树的层序遍历但有一个变形按“Z字型”输出每一层节点值第一层从左到右第二层从右到左第三层又反过来。这道题本质是LeetCode 103考的是BFS 层号判断。我当时用队列做基础BFS记录当前层的节点个数每次处理完一层就翻转结果数组如果层号是偶数。因为时间紧我选择了最直白的做法——先按正常顺序收集每一层的数组最后根据层号reverse。func zigzagLevelOrder(_ root: TreeNode?) - [[Int]] { guard let root root else { return [] } var result: [[Int]] [] var queue: [TreeNode] [root] var leftToRight true while !queue.isEmpty { var levelCount queue.count var levelValues: [Int] [] for _ in 0..levelCount { let node queue.removeFirst() levelValues.append(node.val) if let left node.left { queue.append(left) } if let right node.right { queue.append(right) } } if !leftToRight { levelValues levelValues.reversed() } result.append(levelValues) leftToRight.toggle() } return result }这道题其实不难但笔试时容易写错一个地方在遍历当前层时如果用for node in queue然后往队列尾部append子节点会死循环。正确写法是先记录levelCount queue.count然后只处理前levelCount个节点新加的节点留给下一轮。这个细节是高频翻车点。3.3 一道并发控制题第三道代码题是简答代码混合的形式题目描述大概有100个图片下载任务要求并发数不超过5每下载完一个就补一个直到全部完成最后统一在主线程更新UI。我被告知这道题是为了考察实际开发中的线程控制能力。我用了信号量方案因为信手拈来let semaphore DispatchSemaphore(value: 5) let queue DispatchQueue.global(qos: .userInitiated) let group DispatchGroup() for url in imageURLs { queue.async(group: group) { semaphore.wait() defer { semaphore.signal() } downloadImage(from: url) } } group.notify(queue: DispatchQueue.main) { print(全部下载完成刷新UI) }这个写法能过但后来复盘时觉得用OperationQueue更符合苹果的推荐方式因为可以设置maxConcurrentOperationCount然后往队列里添加BlockOperation配合completionBlock统一处理完成回调。不过面试官在后续面试中其实更在意的是你有没有解释为什么信号量方案会有死锁风险比如如果信号量初始值小于任务依赖数或者wait放在主线程就很容易卡死。这个点是区分“背答案”和“真懂并发”的关键。4. 系统设计题假如让你做小红书信息流首页4.1 需求拆解与核心矛盾最后一道大题是一道开放设计题给一个近似小红书的双列瀑布流首页要求设计iOS端的整体架构、数据拉取策略、图片/视频加载方案和内存优化方案。这道题一看就是小红书工程师日常工作的缩影。它的核心矛盾是信息流内容多、图文视频混合、用户滑动速度快但iPhone内存有限网络环境不稳定。所以设计方案的优劣取决于能否精准表达这四个问题的解法。我当时的方案框架分三层数据层MVVM中的Repository模式负责网络请求、缓存、数据解析展示层UICollectionView 自定义布局使用cell复用资源层独立的图片加载器和视频预加载器回答时我不只写了结论还解释了为什么用MVVM而不是MVC因为信息流页面的ViewModel可以统一处理分页逻辑、点赞状态、数据映射避免Controller膨胀到几千行。4.2 架构选型为什么是MVVM Repository很多校招生在系统设计题里只会堆名词比如“我用MVVM”、“我用RxSwift”但不说为什么。我建议笔试中把每一个选择都和业务背景挂钩。小红书信息流首页的特点是Feed数据来源多样有推荐流、关注流、搜索流不同数据源的数据结构略有差异但UI表现基本一致。用MVVM的好处是ViewModel对UI层屏蔽数据来源差异多个ViewModel可以组合比如推荐流中插入广告卡片可以复用同一种CellViewModel的输入输出可以方便做单元测试Repository层的设计也很有必要它可以统一管理本地缓存和远端请求。缓存策略我设计的是“先读缓存再发请求成功后更新缓存并刷新UI”。这里要注意一个坑如果缓存和网络请求同时返回会导致UI跳动所以需要比较数据的时间戳或版本号确保只处理最新数据。设计题高分技巧不只给方案还要给出为什么这个方案比另一个方案好。 比如“这里不用MVC是因为Controller会同时处理数据缓存、网络请求、视图刷新测试成本高”。4.3 关键细节预加载、复用、弱网处理系统设计题最加分的地方在于细节。我当时重点写了三个模块第一个是图片预加载策略。只对当前屏幕外一屏到两屏的卡片触发预加载预加载队列用OperationQueue控制并发数避免抢占主线程资源。图片内存缓存用NSCache并设置成本限制因为NSCache在系统内存紧张时会自动清理。第二个是Cell复用机制。注意不要在prepareForReuse里做耗时操作比如清空图片、停止播放器。正确做法是给Cell上的图片设置一个当前任务的标识符如果cell复用后标识符不一致就丢弃上一个加载结果。第三个是弱网处理。下拉刷新时如果网络超时应该给用户一个可点击的重试按钮而不是简单toast一个“网络错误”。我写了超时重试机制第一次超时后间隔2秒重试第二次间隔4秒最多重试3次。这个题的答题时间有限不需要写完整代码重点是逻辑闭环。我当时的回答结构是需求背景 - 整体分层 - 核心模块方案 - 风险和优化方向。面试官后来说这个回答最大的优点是“有取舍判断”他知道你真正想过性能边界在哪。5. 笔试之外的准备与避坑5.1 选择题里的“隐藏陷阱”选择题部分除了iOS基础还混着几道操作系统和网络题比如虚拟内存和物理内存的映射、TCP慢启动、HTTPS握手过程。这些内容虽然不直接属于iOS但大厂笔试题普遍喜欢考因为客户端工程师也要理解网络请求链路。印象最深的一道多选哪些操作会导致离屏渲染选项有设置cornerRadius且masksToBounds true、使用drawRect:绘制文字、给视图添加阴影、使用UIVisualEffectView。正确答案是全部都会。离屏渲染的本质是GPU在当前屏幕缓冲区外另开一块空间进行渲染再合成回屏幕。性能瓶颈在于上下文切换所以圆角、阴影、模糊这些效果虽然好看大量使用会显著增加GPU压力。笔试中遇到这种多选题我的经验是把每个选项当成一道判断题来想不能只凭“好像见过”来选。比如cornerRadius如果只设置圆角但不开masksToBounds实际上不会触发离屏渲染但如果设置了背景色又有可能会触发。5.2 环境与时间管理这里要提醒所有参加线上笔试的同学提前一天调试好浏览器、摄像头、Chrome插件别等到开考前10分钟才发现设备不支持。我当时开考的时候因为摄像头驱动问题监考系统弹窗三次才通过第一次弹窗我还懵了几秒。看起来是小问题实际上浪费了宝贵的考试时间而且白屏那几分钟特别影响做题节奏。建议准备双屏设备的同学直接把副屏关掉。牛客的笔试系统检测到切换窗口可能会标记作弊哪怕你是去看LeetCode都会判违规。我同学当时就是因为切到Xcode调试被系统警告了一次虽然没取消成绩但后续面试时被追问了笔试中的异常行为很尴尬。时间管理上我的原则是选择题不超过总时长的20%手写题目每题不超过25分钟。遇到一道题超过5分钟没有思路先标记跳过与其死磕一道题不如保证后面的题都有分。5.3 复盘清单这些考点可以提前准备结合我在小红书笔试中的经验和后续几场秋招笔试题整理了一份iOS岗笔试高频内容速查模块高频考点复习方式内存管理ARC、循环引用、strong/weak/copy、autoreleasepool结合Instrument的Leaks做一次实战多线程GCD死锁、OperationQueue依赖、信号量手写信号量控制并发RunLoopMode切换、Timer不触发、PerformSelector读RunLoop源码注释界面布局约束优先级、UIStackView、自适应Cell自己写一个动态高度Cell性能优化启动耗时、卡顿检测、离屏渲染用Xcode组织性能测试网络HTTPS、TCP握手、断点续传用Charles抓包实践系统设计列表架构、图片加载、缓存策略画一张架构图并写出优缺点这套清单虽然是围绕小红书这场笔试整理的但后来我去面其他内容社区类公司发现重合度极高。iOS岗的笔试核心逻辑就是你不仅要会写UI还要明白UI背后的系统机制。最后再分享一个小技巧笔试结束后不管觉得自己考得多差一定要趁记忆清晰的时候把题目复盘写下来。我在交卷后的30分钟内把选择题的知识点和代码题的思路全部记在备忘录里后来面试的时候美团、字节的面试官问我简历里有没有遇到过什么难题我直接把小红书笔试里的系统设计题当成项目经验讲反而成为加分项。笔试不只是筛选更是一次高质量的系统复习机会。认真对待每一道错题比多做十套新题都管用。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门