2024年春招,小红书iOS开发岗第三批笔试我完整走了一轮。说实话,动笔之前我以为是常规的八股题堆砌,真坐到电脑前才发现,这份卷子几乎是把iOS开发者在日常工作中会踩到的坑铺开当考题用。整场笔试涉及OC和Swift的底层原理、内存管理、多线程、网络、UI布局、架构设计,甚至还有蓝牙、证书上架和跨端兼容问题,覆盖面很广。这篇文章我不打算写标准答案,而是把我对这批笔试的题型结构、考点拆解、典型题思路,以及准备过程中踩过的坑完整复盘出来,给后面准备iOS面试和笔试的同学参考。
1. 笔试整体情况与考点分布
1.1 这批笔试的题型结构
小红书这轮第三批笔试整体时间紧张,题量偏大,给我的感觉是出题人没有打算让你慢慢磨。题型大致分了四块:
第一块是基础选择题和判断题,覆盖OC与Swift语法、ARC、GCD、Runloop、网络协议这些常规考点。题量不小,我的策略是每道题控制在半分钟内,遇到模棱两可的先标记跳过,不要在一道题上消耗太多时间。
第二块是简答题,重点集中在Block原理、循环引用、HTTPS握手、线程保活这几个方向上。简答题比较考验表达能力,不是背结论就行,而是要能把底层逻辑说清楚,最好有递进层次。
第三块是代码题,常见的是给定一段有内存问题或并发问题的代码,让候选人改bug。这一块最容易拉开差距,因为光知道理论不够,要落地成可运行的、边界情况处理妥当的代码。
第四块是场景设计题,也是这份卷子最有意思的地方。比如给一个智能硬件App做蓝牙连接优化,给一个混合开发App排查iOS兼容性问题,或者让候选人描述一次从证书过期到上架被拒的完整处理过程。这类题没有标准答案,考的是你平时有没有真的在做项目,有没有在踩坑之后做总结。
1.2 为什么考点集中在这些方向
我在复盘时把网上相关的技术热词筛了一遍,发现小红书这批笔试的考察方向跟内容社区App的业务特点高度绑定。社区类App最怕什么?怕崩溃、怕卡顿、怕用户体验割裂,所以内存管理、多线程安全和UI性能一定是重中之重。
其次,内容社区现在几乎都逃不开混合开发。WKWebView内嵌、小程序跳转、跨端框架接入,这些在面试和笔试里都会以兼容性问题的形态出现。小红书有自己的小程序生态,对这块要求不会低。
还有一个容易被忽视的方向是商业化。广告SDK、分享SDK、推送SDK,这些三方库的接入过程里会碰到签名问题、证书更新问题、甚至加急审核问题。我在笔试里被问到开发者证书相关的内容时,就明显感觉到这类问题不是随便看看文档就能答好的,没有实际遇到过证书过期导致打包失败的人,很难给出有说服力的回答。
另外,IoT和硬件配套在社区类产品里也有应用场景,比如联动智能设备、记录运动数据等,所以BLE连接相关的题出现在试卷里并不意外。
2. 基础考点拆解:三个绕不开的底层问题
2.1 内存管理与Block:循环引用是高频中的高频
笔试里有一道代码题让我印象很深,题目给了这样一个场景:控制器持有NSTimer作为定时器,控制器里用__weak typeof(self) weakSelf = self,然后在Timer的block里使用weakSelf,问这样是否能彻底解决循环引用。
很多人的第一反应是“用了weakSelf就安全了”,但题目恰好挖了坑。NSTimer在被加到Runloop后,Runloop会对Timer做强引用,而Timer的target对控制器也是强引用,形成了Runloop → Timer → 控制器的引用链。即使Block里用的是weakSelf,控制器也被Timer强引用着,控制器在deinit里invalidate定时器这件事永远不会执行,循环引用依然存在。
我在笔试里的回答思路是这样拆解的:先说明循环引用成立的条件是“A持有B,B持有A”,然后分析NSTimer这个场景里谁持有谁,最后给出两个可行方案:一是自定义一个基于Block的Timer封装,让Timer不直接持有target;二是在控制器出现前就在合适时机invalidate,并保证deinit里能执行清理。iOS 10后系统提供了+scheduledTimerWithTimeInterval:repeats:block:,能够避免对target的强引用,但控制器对Timer的持有关系还是需要手动管理。
Block捕获外部变量也是笔试常客。笔试里有一道送分变体:在一个Block内部修改局部变量,直接用会报编译错误,必须加__block。这个考点背后的原理很简单:Block为了能在将来执行时读取正确的变量值,默认会做值捕获,把外部变量拷贝进来,但这样做只能读不能改,加上__block之后变量会变成Block内部和外部的共享存储,这样才能实现修改。
答题时要展示出层次感:先解释Block捕获变量的机制,再讲__block的作用,最后补充一个容易踩的点——捕获OC对象时,Block默认是强引用捕获的,如果捕获的是self,就会循环引用,所以要用__weak。这三层递进讲清楚,基本就能拿满这道题的分数。
2.2 多线程与Runloop:并发条件下的“稳定”从哪里来
第三批笔试的代码题里有一道GCD相关的题目,考察的是“主队列上调用同步任务会怎样”。很多人看到题会凭记忆回答“死锁”,但如果被追问为什么死锁,就语焉不详了。我的理解是:主队列是串行队列,同步任务意味着“等当前任务执行完再执行新任务”,而当前任务正在阻塞等待新任务完成,这个互相等待就构成了死锁。回答时最好能把线程、队列、同步/异步四个概念的关系理清楚。
还有一道简答题问Runloop在AFNetworking旧版里的线程保活原理。这道题如果只背结论肯定露馅,需要顺着思路讲:线程执行完任务后就会退出,如果想要让一个后台线程长期活着等待任务,就要给它一个Runloop,并且往Runloop里添加Source或Timer,让Runloop在没事做时进入休眠而不是退出。当有任务要处理时,唤醒Runloop,处理完继续休眠。这种常驻线程模式不会占用大量CPU,但能让异步任务有稳定的执行环境。
并发环境下的数据安全也是考察重点。笔试里有道问答题问“如何保证多线程下的数组读写安全”,我给出的方案包括加锁(NSLock、os_unfair_lock)、使用串行队列同步访问、以及用读写锁区分读操作和写操作。这里我的体会是:不要一上来就抛技术名词,而是先分析数据竞争会发生什么,再说明为什么需要锁,最后再对比几种方案各自的适用场景。比如读多写少的场景适合用读写锁,高频小资源的保护用os_unfair_lock更轻量,granularity较大的业务逻辑用串行队列反而更不容易出错。
这块我给准备笔试的同学一个建议:GCD那套API名字虽然简单,但实际工程里的坑往往出在队列选型和锁的粒度上。笔试考的是你能不能判断“这个场景用什么并发方案最合适”,而不是让你默写API。
2.3 网络层与HTTPS:从握手到抓包都要能讲清楚
网络部分的简答题问了HTTPS的通信过程。答题要点很清晰:先是TCP三次握手建立连接,然后TLS握手协商加密套件、交换证书、验证身份,最后用对称加密密钥进行业务数据传输。这里要注意把“证书验证”讲透,客户端需要验证服务端证书是否由可信CA签发、域名是否匹配、证书是否过期,验证通过才继续后续流程。
笔试里有一道跟Charles抓包相关的实操题:为什么配置了SSL Proxying之后能在Charles里看到HTTPS明文?这个问题背后的原理是Charles作为一个本地代理,会同时与客户端和服务端建立TLS连接,客户端信任Charles的根证书后,Charles就能解密流量。但要留意,这个能力仅限于调试环境,真实线上App如果只做系统级信任,仍然面临中间人攻击的风险,所以应用层还需要做证书校验或双向认证来防抓包、防篡改。
我在笔试里顺手提了一个实际项目里的做法:对核心接口使用自定义证书校验逻辑,同时把关键请求参数做签名,服务端验签后再返回数据。这样即使流量被截获,攻击者也很难篡改内容。这个补充让答案的实操度提高不少。
另外值得一提的是一定要熟悉ATS(App Transport Security)。iOS 9之后默认要求网络请求走HTTPS,如果确实需要兼容HTTP,必须在Info.plist里配置NSAppTransportSecurity。笔试里问“为什么我的网络请求在iOS真机上突然失败,但模拟器正常”时,大概率就是ATS或证书信任的问题。
3. 场景题与实操题:真正的分水岭
3.1 iOS BLE连接:参数规范与状态机
我记得有一道场景题大概是这样的:一个iOS App需要连接公司自研的BLE设备,设备端固件工程师反馈连接不稳定,数据吞吐率低,问从iOS侧可以做哪些优化。
这道题的考察点很细。首先要能区分系统级蓝牙状态和App内部连接状态,CBCentralManager的centralManagerDidUpdateState:回调里拿到的CBManagerState是系统层面的状态,包括未授权、已关闭、已开启等;而App层面的连接状态,比如是否正在扫描、是否已经连接上某个外设、是否处于重连中,需要自己用属性去管理。线上App最常犯的错误就是只盯着系统状态判断“蓝牙是否可用”,忽略了App内部状态机。
接下来是BLE连接参数规范。默认的BLE 4.0 MTU是23字节,实际可用负载只有20字节,如果一次要传输大量数据,比如固件升级,就需要协商更大的MTU,iOS侧可以通过maximumWriteValueLength(for:)查询当前协商后的最大值,并以此拆分数据包。另外连接间隔(Connection Interval)也关键,间隔越短,实时性越高,但功耗越大;间隔越长,越省电但延迟越明显。做穿戴设备同步时,通常是短间隔传大数据,完成后再退回长间隔保活。
还有个很容易被忽略的点是外设端的广播参数和连接参数协商。iOS在连接外设时会参考外设广播里的连接参数,如果外设端配置不合理,比如超时时间太短,就容易出现连接后马上断开的情况。我当时笔试里直接建议了一个排查思路:先用LightBlue这类工具验证外设广播内容,再在iOS端打印所有系统状态回调,确定是哪一段状态流转出了问题,避免在业务层瞎猜。
3.2 签名、证书与上架全流程
证书相关的内容在笔试里出现的频率超出我的预期。有一道题是:开发证书过期后,团队中多个成员共用一台打包机,最近Xcode突然报Provisioning profile doesn't include signing certificate,怎么排查。
这个问题的核心在于开发者证书和描述文件(Provisioning Profile)的匹配关系。描述文件里记录了允许的App ID、设备UDID和开发团队证书信息,如果证书过期了但描述文件没更新,或者描述文件里包含的设备不是当前真机,就会报这个错。正确的处理流程是:先在钥匙串里确认证书是否有效,再到Apple Developer后台重新生成描述文件,下载后安装到Xcode,同时确认真机UDID在设备列表里。
延伸的考题还包括“iOS浏览器唤起安装App”的实现原理。这道题我回答的是双方案对比:URL Scheme是老方案,简单直接但容易冲突,而且系统对未知Scheme的提示不友好;Universal Links是新方案,基于HTTPS关联域名,没有繁琐的弹窗确认,体验好很多,但要求服务端配置apple-app-site-association文件并确保HTTPS可达。为了稳妥,很多App会在Universal Links失效时降级到URL Scheme,两层兜底。
如果笔试遇到申请加急审核这类问题,核心不是背地址和流程,而是先说明什么情况下才符合加急条件:比如线上发生重大崩溃、涉及到支付流程阻断、或者存在必须紧急下架的安全问题。回答时要体现出“我不能滥用加急渠道,要有理有据”的工程素养,这比单纯告诉考官“去后台申请”要加分得多。
3.3 混合开发与跨端兼容场景
小红书这类App里H5和原生交互非常频繁,所以和混合开发相关的场景题也出现在试卷里。有一道题问:同一个页面,在iOS端用WKWebView加载时偶尔出现白屏,重启App才恢复,可能是什么原因。
我当时的分析思路是这样的:白屏大概率是WKWebView的渲染进程被系统杀掉,但WebView容器还在内存里,页面内容已经丢失。这个问题在内存压力大的设备上尤其常见。解决方案包括在webViewWebContentProcessDidTerminate回调里检测到进程终止后主动reload,同时把页面状态做缓存,必要时做架构上的优化,比如把WKWebView换成复用性更好的异步加载方案,避免同时加载多个WebView。
笔试里还出现了跨端兼容相关的选择题,比如H5弹窗组件在iOS上显示异常,小程序端返回上一页时拿不到extradata。这类问题的答案实际上都指向同一个排查习惯:先复现,再确定是端上WebView引擎的差异,还是原生桥接的通信时序问题,最后再针对不同端写兼容逻辑。比如van-popup在iOS上显示异常,常见原因是position:fixed在iOS的WKWebView里受transform等CSS属性影响的渲染问题,解决时要用absolute定位或调整父级样式结构;小程序拿不到extradata,常见原因是wx.navigateBack传参只在特定时机生效,需要换用全局状态管理或事件总线。
跨端趋势方面有一个考点是问一个团队同时维护iOS/Android/鸿蒙三端时,哪些逻辑适合下沉到跨端层,哪些必须留在原生层。我的回答是:纯UI渲染和强依赖系统能力的逻辑留在原生层,业务状态管理和轻量数据转换可以下沉。现在很多App把验证码校验、埋点上报、灰度开关这类逻辑做成跨端统一处理,效果很好。iOS 11.0及以上、Android 4.0及以上、HarmonyOS NEXT 5.0及以上这几个兼容底线,也要在笔试时能讲清楚自己的选择依据。
4. 笔试中的高频坑与避坑清单
4.1 三处最容易答偏或写错的细节
第一个容易答偏的就是“weakSelf是不是万能解药”。太多人一看到Block就问循环引用,一聊循环引用就写weakSelf。实际情况是weakSelf只能在“self持有了Block,但Block不能强持有self”的场景中生效。如果Block是被第三方对象持有(比如系统的UIView动画Block),或者持有链路上还有其他强引用,weakSelf就不一定能解决问题。笔试答题时最好先明确“谁持有谁”,再动笔写方案。
第二个细节是HTTPS相关,很多人会把“HTTPS已加密”理解成“内容无法被看到”,实际上抓包工具只要安装了根证书并且App信任了用户证书,就能解密流量。所以问题的关键不是“能不能加密”,而是“如何防止中间人攻击”。回答里能提到证书固定(Certificate Pinning)和双向认证,会让答案显得有实战积累。
第三个细节是BLE连接问题,很多人在回答时只谈Central的state,忽略外设端的配置和App内部状态机。笔试阅卷人大概率是个做过实际项目的人,看到这种只讲半截的回答,一眼就知道候选人没有真正做过外设联调。答题时宁可多写几点排查步骤,也不要只给一个结论。
4.2 高频考点避坑清单
我把这次笔试中容易踩坑的考点整理成了表格,方便后面准备的同学快速定位自己的薄弱点。
| 考点类型 | 常见错误 | 正确思路 |
|---|---|---|
| 内存管理 | 以为weak即可一劳永逸 | 先分析持有链,再判断weak是否足够 |
| 并发编程 | 全局队列+加锁解决所有问题 | 根据读写频率和数据规模选择队列或锁 |
| 网络调试 | 认为HTTPS无法被中间人查看 | 说明证书信任机制与防抓包手段 |
| BLE连接 | 只看系统蓝牙开关状态 | 结合系统状态与App状态机一起判断 |
| 签名证书 | 报错后只重新生成描述文件 | 先确认证书链、设备和App ID匹配情况 |
| 混合开发 | 把兼容问题归咎于“系统bug” | 先复现,再定位是WebView引擎还是通信时序问题 |
把这张表里的每一项吃透,笔试里的多数“感觉见过但答不准”的题都能找到方向。
4.3 我在准备阶段用过的两个笨办法
第一,我把简历里每一个技术点都改写成“一段带场景的描述”。比如简历里写了“负责蓝牙设备连接优化”,就强迫自己回答四个问题:为什么会出现连接失败?我当时通过什么手段定位的?最终改了哪些参数?效果如何量化?这种写法在回答场景题时特别有用,因为笔试里的场景题往往就是你简历上某个真实的项目换了个壳。
第二,我花了一个周末专门整理自己的“报错日志”。我把过去一年在开发中遇到过的报错信息按主题分类,包括证书错误、WebView白屏、蓝牙断连、跨端组件渲染异常等,把每个报错的报错信息、现场截图、排查过程和最终根因都写下来。笔试时遇到相似问题,我几乎是把复盘内容直接搬出来用,完全不用临时组织语言。
5. 笔试复盘与后续动作
5.1 考后怎么复盘才有价值
笔试结束后我没有急着对答案,而是先把整张卷子按照“会做但错了”“不会做”“会做但对答案没把握”三类重新过了一遍。这个过程比对答案重要得多,因为“会做但错了”往往意味着某个基础概念有隐性缺陷,而“不会做”则提示某个知识领域存在盲区。
我当时把第三批笔试里所有的错题统一整理成一个知识点表格,每个知识点后面加上“原理、场景、代码/操作、面试话术”四个字段。原理用来打底,场景用来记忆,代码或操作用来验证,话术用来准备下一轮面试。这个方法实践下来非常有效,因为技术面试几乎不直接问“请背诵XX原理”,而是让你在一个具体问题里展示原理。
5.2 给下一批同学的三点实操建议
如果目标是下一次考试有所突破,我的建议很直接:不要背题,要把每个考点连成一个闭环。比如“HTTPS抓包”这个考点,闭环是从握手流程开始,到Charles配置证书,再到App如何防抓包,最后再落到线上事故里如何排查。这四个环节打通了,不管题目怎么换角度考,都能答出东西。
其次,代码题一定要动手写。笔试和面试不同,面试你可以口头讲思路,笔试最终看的是代码。尤其是GCD、Block、Delegate循环引用、蓝牙回调这类高频代码题,建议在本地多敲几遍,熟悉编译错误提示,别等到考试时边想边写浪费时间。
最后一点,不要忽略对自己项目经历的深度复盘。小红书这批笔试的场景设计题,哪怕没有标准答案,考官也希望看到候选人的答题过程是否完整、是否有工程判断力。准备时把“我做过的项目、遇到的问题、怎么解决的、有没有更好的方案”想透,比刷多少道题库都有效。
我个人经历下来最大的感受是:笔试结果并不是终点,过程里暴露出的薄弱点和那些“以为自己知道其实并不知道”的知识漏洞,才是最宝贵的收获。技术栈每年都在变,但底层功底和排查问题的思路是通用的,把这些沉淀下来,不管面哪家公司都不会慌。