
2023年度小满春招iOS研发岗第三批笔试是我当年校招季做得最过瘾的一场。120分钟题量不算大但它把iOS开发高频面试题、iOS架构基础、界面渲染机制、工程化打包链路几乎一网打尽。我当时边做题边感慨这套题不像是来筛人的更像是来劝你平时多动手写Demo的。这篇文章就是那份笔试的复盘我把每一类题背后的原理、常见坑和备考建议都摊开来讲适合正在准备iOS春招、秋招的应届生也适合想查漏补缺的初中级iOS开发。先说结论这场笔试的题目风格偏“底层原理工程落地”没有太多偏题怪题但覆盖面很广。从OC的消息机制到GCD的死锁场景从UIStackView的布局逻辑到App上架的证书配置从模拟器调试到真机耗电治理几乎每个高频考点都碰到了。如果你平时只是照着教程写页面、调接口没有系统梳理过知识体系做起来会明显觉得吃力。1. 整场笔试形态速览题型分布与时间分配策略这套卷子我印象里是120分钟题量控制在40道左右整体分为四个板块单选题、多选题、两道编程题、一道开放设计题。单选题约15道每道权重不高但胜在覆盖面广多选题约8道是拉分项选错一个就全错没有部分得分编程题占大头一道属于“必须AC”的保底题一道属于“需要压缩时间复杂度的进阶题”最后那道开放设计题没有标准答案拼的是工程思维和表达条理。先说一下时间分配。我看到不少同学在单选和多选上死磕结果编程题只剩20分钟基本等于放弃。我的策略是先用8到10分钟快速扫完所有客观题会就选不会的先标记绝不停留超过一分钟。然后直接做编程题先把保底题写得干净利落再回头啃进阶题。等两道题都动笔了再回来仔细推敲之前标记的选择题。最后至少留15分钟给开放设计题哪怕只写一个清晰的设计框架也比空白强很多。客观题里确实有几道是“一眼假”的干扰项。比如某道多选问“哪些操作会触发隐式动画”选项里有setHidden:、setAlpha:、setFrame:、setTransform:如果你对Core Animation的动画机制不熟很容易把setFrame:也选上。实际上UIView的frame变更在默认情况下是否触发隐式动画取决于是否在动画事务内、layer的actions是否被禁用并不像alpha和hidden那样自带隐式动画行为。这一类题考的已经不是记忆力了而是你有没有真正在调试中观察过UIView的动画事务。开放设计题在卷尾出现的频率其实很高这场给的是一个偏应用的场景设计一个支持多业务模块复用的iOS组件库。题目没有限定UI层还是工具层所以可以先从分层思路答起基础工具层、UI组件层、业务模块层再讲组件之间如何解耦下面会单独展开。2. 技术基础题考点拆解消息转发、ARC与GCD的底层逻辑这一板块是笔试重头戏几乎每道题都在问你“底层到底怎么实现的”。我挑几个印象最深的展开讲。2.1 OC消息转发机制不只是背出三个方法名有一道题是给定一段代码问调用一个doesNotRecognizeSelector:之前运行时系统具体做了哪几步。标准流程是三段式动态方法解析、快速转发、完整转发。但笔试的陷阱在于很多人能说出resolveInstanceMethod:、forwardingTargetForSelector:、methodSignatureForSelector:这几个方法名却不知道它们的调用顺序和返回值处理。正确链路是先走resolveInstanceMethod:如果你在这里通过class_addMethod动态添加了实现流程直接结束如果没有进入forwardingTargetForSelector:这是快速转发阶段可以把消息转给一个备用对象只要返回非nil、非self消息就由那个对象接收如果返回nil才进入完整的转发流程也就是methodSignatureForSelector:配合forwardInvocation:前者返回方法签名后者负责把消息完整分发出去如果签名也返回nil最终才抛异常。这道题真正想考察的其实是你有没有在热修、防崩溃组件这些场景里真正用过消息转发。比如JSPatch就是通过消息转发机制让OC方法可以调用JS实现的虽然没有明着考JSPatch但原理完全一致。备考时建议自己写一个小Demo动态为一个没有实现的方法兜底你才能分清快速转发和完整转发的边界。2.2 ARC下的属性修饰符weak的底层维护逻辑选择题里有一道是问weak和assign的区别这个太经典了。但今年的题目把问题往深处推了一层一个对象被多个weak指针引用时对象释放的那一刻底层是怎么把这些指针全部置为nil的。答案涉及SideTable。每个对象在运行时对应一个SideTable里面维护一张weak_table_t散列表存储所有指向该对象的weak_entry_t。当对象dealloc时runtime会通过这张表找到所有weak指针逐个置为nil防止野指针。理解了这个机制你就能解释为什么weak只能用于OC对象、不能用于基本数据类型因为这些数据根本不走runtime引用计数管理。顺带提一下我在项目里踩过的坑给一个UI控件用weak修饰然后在viewDidLoad里初始化并添加为子视图之后发现它提前被释放了。原因很典型——只有子视图持有它但子视图的addSubview:其实是强引用这个视图的按理说不会释放。问题出在后来我从父视图移除了它又没保留强引用等下次访问时就变成了nil。所以weak属性一定要在别处有强引用兜底否则就是给自己埋雷。2.3 GCD的死锁不要只背“串行队列里同步执行会死锁”关于GCD的这道编程题也很典型给出一段代码问下面哪个组合会死锁。A选项是主队列里调用dispatch_sync到主队列B选项是串行队列里再调用dispatch_sync到同一个串行队列C选项是并行队列里调用dispatch_syncD选项是并发队列里调用异步任务后再用dispatch_group_notify。实际上A和B都会死锁C不会。原因是dispatch_sync的本质是把一个block提交到队列然后阻塞当前线程等待block执行完。如果当前线程和目标队列是同一个串行队列block永远等不到执行前一个任务还在等它完成于是互相卡死。这个知识点对应到一个真实场景你肯定遇到过dispatch_sync主线程崩溃报错提示“EXC_BAD_INSTRURE”的情况最常见原因就是某个回调线程里同步执行UI刷新任务时没有判断当前是否已在主线程。我的习惯是封装一个安全函数- (void)performOnMainQueue:(void (^)(void))block { if ([NSThread isMainThread]) { block(); } else { dispatch_async(dispatch_get_main_queue(), block); } }既能避免死锁也不影响异步更新UI的流畅度。笔试里光在纸上画图解释死锁不如直接在控制台跑一遍直观考前真的建议自己写一个串行队列 同步执行的崩溃Demo试试。2.4 Runloop与NSTimer滑动时定时器停摆的根因开放题里那道关于NSTimer的题目其实是从一场真实排查里提炼出来的列表页滚动时某个定时器不走了停下来以后又恢复。很多人第一反应是“NSTimer不准”但这个“不准”的根因是Runloop的Mode切换。NSTimer默认添加到当前Runloop的NSDefaultRunLoopMode而你滚动UIScrollView时Runloop会切换到UITrackingRunLoopMode这个模式下不处理默认模式的Timer事件。解法其实是老生常谈把Timer加到NSRunLoopCommonModes或者用dispatch_source_t实现定时器绕开Runloop的Mode限制。但笔试这道题不止问解法还问了一个延伸问题现在都是iOS 15、16了直接用Timer配合RunLoop是不是已经过时这个要认真答。Timer本身没有过时但异步并发环境下更推荐Timer.publish或者DispatchSourceTimer它们不依赖Runloop在后台队列也能稳定触发。如果你在项目里用Swift开发这一块差异要特别清楚。3. 界面与交互考点的实操复盘响应链、UIStackView与分屏适配界面类题目在这套卷子里占比不低但考法很“iOS 16化”不再单纯问你frame和bounds而是往新特性、复杂交互上靠。3.1 UIStackView不要把自动布局的封装当万能答案有一道多选题专门考UIStackView的axis、distribution、alignment三个属性在动态调整间距时的表现。不少同学在这道题上翻车因为平时只在Storyboard里拖拽没有深入理解StackView的约束生成逻辑。UIStackView本质上不是一个渲染视图而是一个负责生成约束的非渲染容器。它的arrangedSubviews会对内部视图做约束管理当其中一个子视图被隐藏或移除时其他子视图会按照distribution的规则重新排列。如果你在StackView里再嵌套StackView系统会自动生成大量约束一旦某个子视图的固有内容大小intrinsicContentSize不明确就会出现约束冲突或布局漂移。我的建议是少于等于3个固定控件的场景不需要StackView直接手动写约束更可控动态列表、横向标签流这类内容StackView配合UIStackView.distribution .fillProportionally能减少大量约束代码。但别把动态增删子视图的逻辑全都交给StackView在动画过程中增删子视图很容易触发约束警告。3.2 事件响应链hitTest与nextResponder的使用场景笔试考了一道点击事件穿透的题一个superview上有两个重叠的子视图上层视图的isUserInteractionEnabled NO点上层区域时事件会落到谁身上。答案是落到下层视图因为系统在hitTest:withEvent:时会遍历subviews的反序只有当子视图的isUserInteractionEnabled NO、alpha 0.01或者hidden YES时才会跳过它。这个问题本身不难但它的变种很多。最常见的是“点击一个圆角按钮点击圆角外的透明区域事件是否还会触发按钮响应”。如果你没有重写pointInside:withEvent:只要点还在按钮的frame范围内即使落在透明圆角外也会触发点击。所以很多设计团队会让开发重写这个判断实现真正抠出形状区域的命中检测- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event { return CGPathContainsPoint(self.roundedPath.CGPath, NULL, point, false); }这个话题背后还牵扯到nextResponder链也就是事件从hitTest选中的view开始向superview、viewController.view、viewController、window、UIApplication逐级上报的过程。笔试里关于Responder Chain的题目只要看到“点击事件怎么传递”“怎么拦截事件上抛”基本就是考这里。3.3 iPad分屏与多窗口UIScene的尺寸适配最近热搜里“iOS分屏”出现得很频繁这其实也在暗示笔试的考点方向。以往问分屏都是iPad上的UIViewController适配现在更多会问UIScene生命周期和size class的变化回调。iPad分屏之后同一个App可能会出现compact和regular两种不同宽度类别开发者需要在viewWillTransitionToSize:withTransitionCoordinator:或traitCollectionDidChange:里调整布局。笔试给出一个场景分屏宽度只有320pt时原来的UICollectionView每行4列变成了2列问你如何适配。常规做法是监听traitCollection.horizontalSizeClass或者是collectionView的layout里根据collectionView.bounds.width动态计算列数。这种题目没有唯一答案但必须把适配逻辑讲清楚。3.4 iOS自动化测试XCUITest能覆盖多大范围说完分屏试卷里还夹带了一道自动化测试的题问XCUITest相比UnitTest的定位差异。XCUITest是UI层测试可以直接模拟用户点击、滑动、输入但它依赖“UI元素当前是否存在”一旦控件的accessibilityIdentifier没设好脚本就非常脆弱。笔试能接受的最佳答案是用UnitTest覆盖核心算法和数据模型用XCUITest做关键路径的回归验证不要指望UI自动化覆盖所有异常分支。4. 系统集成与工程化能力设备调试、App打包与上架链路这部分是整个笔试里最贴近“真实工作”的板块考的是你不只是会写代码还要能把一个App从工程配置到上架的整条链路跑通。4.1 iOS开发者模式与设备模拟真机调试和模拟器的差异单选题问“以下哪种场景必须在真机上验证”选项包括定位权限、后台蓝牙、远程推送、支付模拟。答案是后台蓝牙和远程推送因为你无法在模拟器上模拟系统级蓝牙状态和真实推送链路。模拟器可以用开发者模式模拟很多功能但它不是万能的。备考这块时我建议把模拟器和真机的差异列表背下来功能模拟器真机CPU架构x86_64/arm64(Apple Silicon)arm64摄像头用Mac摄像头不可选分辨率真实摄像头参数蓝牙不支持支持包括BLE推送需要用模拟器APNs模拟完整APNs链路性能测试无参考价值有参考价值存储/权限弹窗部分可用完整可用这道题背后还有个隐藏考点iOS开发者模式如何在真机开启。很多时候模拟器跑得好好的一上真机就崩溃原因大多在证书、描述文件和开发者模式。真机调试前先检查Settings - Privacy Security - Developer Mode是否开启iOS 16之后的真机第一次连接Xcode时会强制要求打开。4.2 证书与描述文件从开发者账号到App签名这里考了一道实操题你接手了一个新项目Bundle Identifier已存在但本机没有该App对应的证书真机编译报错CodeSign error: No codesigning certificate found怎么解决。标准步骤是登录开发者后台确认证书类型是iOS Distribution还是Development在本机钥匙串里申请CertificateSigningRequest然后下载对应描述文件在Xcode的Signing Capabilities里选择对应的Team和Provisioning Profile。有一个特别常见的坑是证书过期。iOS开发者的证书有效期的确有限续期之后本机钥匙串仍然保留旧证书但Xcode会自动匹配新的。如果遇到“Profile doesnt include signing certificate”多半是因为描述文件里的证书不是当前钥匙串里那一个。建议手动下载一次新的Provisioning Profile再清理~/Library/MobileDevice/Provisioning Profiles下的旧文件。4.3 App上架与审核加急地址、版本迭代的注意点上架流程本身不复杂但笔试里考的几个点都很硬核。比如TestFlight外部测试上限是多少审核被拒后怎么加快重新审核版本更新时build号能不能比线上版本低这些都有标准答案。TestFlight外部测试人员上限是10000人审核被拒后可以在App Store Connect里申请加急审核地址是审核拒绝信息里的“Contact App Review”入口必要时附上复现视频和路径能显著提升处理速度。build号必须大于线上版本否则上传时会被拒绝提示The provided build number is invalid。版本号CFBundleShortVersionString和构建号CFBundleVersion是两套体系别搞混。顺便提一句加急审核我见过很多团队一慌就反复点击加急申请反而导致审核队列优先级被降。正确做法是一次申请里把问题描述清晰附上复现路径和录屏一次到位比多次催促有效得多。4.4 UniApp、Flutter、RN混合开发方案的选型对比因为现在中小团队大量使用跨端方案笔试里也出了一道混合开发选型题产品要求在iOS和Android两端快速验证商业模式团队只有2名前端没有原生开发资源选哪种方案。这道题没有标准答案但至少要给出几个维度的对比。我给一个比较完整的参考框架方案性能生态原生桥接难度适合场景WKWebView套壳一般极强较低内容型、H5已有大量沉淀React Native较高强中动态化需求高已有前端团队Flutter高较强中高度定制UI渲染一致性要求高UniApp较高强中国内多端发布小程序复用我当时的回答选的是Flutter理由是对2人小团队来说Dart语言不需要额外学iOS原生就能勉强维护UI渲染引擎能保证两端一致性同时对原生能力有依赖的部分通过MethodChannel做桥接。作答时一定要讲出你的判断依据而不是背一份对比表。如果你用UniApp要额外注意iOS端打包流程生成自定义调试基座时需要配置好Bundle Identifier和Profile而且要确认是否勾选了uni-push模块否则云端打包后运行时容易报“当前应用打包时配置了ios uni-push功能但uni-push未配置”之类的问题。4.5 iOS模拟器的延伸如何高效模拟位置与推送笔试最后有一道简答题问“模拟器上如何模拟定位和推送”很多只做过基础开发的人会愣住。模拟器其实支持在Features - Location里直接设置坐标点或模拟行进路线这在开发地图功能时太常用了。推送则可以在模拟器上用xcrun simctl push命令直接注入一条APNs消息不用连真机也能调试推送UI。如果实际项目中需要模拟后台定位用模拟器导入GPX文件Xcode支持自定义模拟位置比手动输入坐标更可靠。我习惯先在真机上看一次真实定位回调再用模拟器反复跑路线这样能同时验证定位和航向逻辑。5. 性能调优与问题排查场景卡顿、网络抓包与耗电治理笔试最后一个硬核板块基本是把今年热搜里“iOS开发 电池优化”“charles ios抓包”“ios ble连接参数规范”这些关键词一网打尽。下面整理几个高频考点和对应的排查思路。5.1 主线程卡顿监控与离屏渲染有一道题是问“以下哪些操作容易导致列表滑动掉帧”选项里有cellForRowAtIndexPath里同步读大量文件、给多个cell添加带阴影的图层、直接设置cornerRadius加maskToBounds、在子线程更新UI。答案是前三项最后一项会导致崩溃而不是掉帧。核心考点是离屏渲染。当设置cornerRadius masksToBounds时Core Animation需要先在离屏缓冲区渲染出圆角再合成到屏幕虽然现代iOS设备对简单圆角已经做了优化但大量并发使用仍会增加开销。阴影也一样只要设置shadowPathGPU就不必为每帧动态计算阴影形状。建议是列表页的圆角图片用预裁剪阴影用shadowPath固定范围不要在cell里做动态模糊。监控方法上可以基于Runloop的Observer做卡顿检测注册KVO监听Runloop的beforeWaiting和afterWaiting如果两个回调间隔超过阈值比如50ms就认为发生一次卡顿。还有一种常见做法是插桩主线程的CADisplayLink统计FPS曲线低于45时上报AppDelegate上下文。笔试里只要把这套思路讲出来就算踩到点子上。5.2 Charles抓包解决HTTPS解密和iOS系统信任问题网络题里直接问“用Charles抓包时为什么iOS端抓到的HTTPS请求全是乱码”。很明显考的是SSL证书配置的两步先在Mac上安装Charles根证书再在iOS设备上把证书下载到描述文件并开启全员信任Settings - General - About - Certificate Trust Settings。很多新手只做了第一步忘记在手机设置里打开SSL Trust开关所以抓不到明文。进阶一点的考点是抓包时为什么抓不到某些网络请求这往往是因为App开启了证书校验SSL PinningCharles的中间人证书会被拒绝。解法可以是开发期间关闭Pinning或者把Charles根证书加入白名单。另外新版本iOS里如果App用URLSession默认的default配置代理确实会生效但如果用了底层网络库绕过系统代理就抓不到。这种题非常考验有没有真正踩过坑。5.3 电池优化定位、后台唤醒与IO频控有一道问答题是“一个地图类App在后台被频繁杀掉耗电严重可能的原因有哪些怎么优化”。这个问题在招聘JD里出现的频率也很高因为很多App的耗电大头不是UI而是后台任务。绕不开的有三个后台定位CLLocationManager持续请求定位权限、后台音频、BGAppRefreshTask频繁唤醒。定位的优化思路很简单优先使用startMonitoringSignificantLocationChanges重大位置变化而不是持续定位或者改成startUpdatingLocation配合desiredAccuracy kCLLocationAccuracyHundredMeters降低精度同时allowsBackgroundLocationUpdates只在真正需要后台定位时开启。IO方面的耗电则容易被忽略。频繁写NSUserDefaults、不断创建NSCache都会增加CPU占用。高性能IO可以选MMKV它的mmap机制比NSUserDefaults的plist读写更快、更省电这也是很多大厂做性能优化时常用的一招。5.4 BLE连接参数规范与CBManager状态判断由于热搜里“ios ble连接参数规范”“ios cbcentralmanager系统级蓝牙状态和app级蓝牙状态能区分出来吗”都是高频词我估计不少人在准备这块内容。笔试里可能给出一道关于CoreBluetooth的题调用CBCentralManager的state属性时如何区分系统蓝牙关闭和App权限受限。答案在iOS 13之后的CBManagerState里一共包含五个状态.unknown、.resetting、.unsupported、.unauthorized、.poweredOff、.poweredOn。当系统蓝牙关闭时表现为.poweredOff当用户拒绝App使用蓝牙权限时表现为.unauthorized。这个区分对UI提示很关键否则用户会以为只是没开蓝牙其实要去设置里允许权限。BLE连接参数上建议把连接间隔Connection Interval设置得适度大一些太小的间隔虽然延迟低但耗电快外围设备上报数据量不大时推荐12到30之间的ConnectionInterval或者直接使用系统默认参数不要为追求低延迟牺牲电量。5.5 崩溃排查与野指针的常见定位路径除了上面这些笔试里还有一道送分题是“线上App崩溃率突然升高首先应该看哪些维度”。答案是四件事崩溃日志版本分布、崩溃堆栈TOP10、崩溃发生时间与最近发版时间、受影响设备系统版本。围绕这四点你才能排掉“是不是新版本引入的问题”“是不是某类旧机型系统不兼容”“是不是某个服务器接口变更导致的空数据”。崩在OC对象上的野指针就是经典难题因为堆栈往往指向objc_msgSend看不出具体业务。现在通用的解法是开启Address Sanitizer和Zombie Objects做本地复现线上则要依赖数据采集平台把malloc_history和VM Region信息带上。6. 一场笔试背后的备考方法论给下一届校招生的冲刺清单笔试复盘不能只讲题还要讲怎么准备。针对这套试卷反映出来的趋势我整理了一份用过的备考冲刺清单不一定适合所有人但方向不会偏。6.1 知识体系的分层搭建我把iOS复习内容划成三层应用层、系统层、底层。应用层UIKit控件、Auto Layout、手势、动画、网络库、存储方案。系统层Runloop、多线程GCD/NSOperation、内存管理、Runtime、Core Animation、Core Bluetooth、APNs。底层Mach-O、dyld启动流程、arm64汇编基础、静态库动态库差异、符号解析。笔试超过80%的题目都落在系统层。很多人在应用层逛得很熟但一问weak的底层表结构、Runloop的observer回调顺序就卡壳这是典型的“广度有了、深度不足”。6.2 编程题的刷题节奏iOS岗的编程题通常不会太难但要求准确率和代码风格。LeetCode上建议把“字符串处理”“链表”“二叉树”“动态规划”这四个专题刷熟因为笔试编程题基本就这几个方向。对于这套试卷里的保底题大概是链表反转字符串拼接的难度没有DP和复杂图论。写题的技巧是先和面试官确认边界条件再用注释写清楚核心思路最后再动代码。很多同学代码写对了但没考虑空数组、越界、特殊字符用例一挂就直接判失败。6.3 常见备考误区背题、堆工具、忽略调试我见过太多人备考失败不是因为不努力而是方向偏了。误区一是“背题”把网上流传的“iOS面试题大全”背得滚瓜烂熟但一问到变形题就懵。底层原理不能靠背要靠手写代码去验证。误区二是“堆工具”觉得会用Charles、Instruments、Reveal就是高手其实工具只是辅助真正核心的是你知不知道工具背后的原理。误区三是“忽略模拟器和真机的差异”只在模拟器上跑通就以为自己做好了结果公司安排拿真机测试才发现一堆问题。我的建议是备考最后两周做一个“Debug打卡表”每天花30分钟重复一个真实的调试场景内存泄漏排查、Runloop卡顿检测、弱引用失效、BLE连接失败。这些场景看起来不起眼但笔试的开放题甚至技术面考的原型都在里面。最后再分享一个心得校招笔试不完全是“知识点测试”很多题是从真实项目里抽象出来的场景回答时要有“发现现象 - 定位根因 - 给出解决 - 验证效果”的完整路径。答题时不用怕过程冗长把能体现工程思维的细节写上面试官反而更喜欢。这份复盘覆盖的点很多但本质就一句话把iOS开发的每一层都摸一遍笔试就不会有什么意外。