前两天整理旧资料,翻出一份之前整理过的金山办公2020校招iOS开发工程师笔试题(二)的复盘笔记。笔记第一行写着一段话:“这套卷二比卷一更集中,把Objective-C底层、内存管理和并发调度串成了一整条线,不是背几道八股就能过的。”我重新对照手头的考生回忆、公开面经和不完整的题面梳理了一遍,觉得这类老题对现在准备校招的人仍然很有参考价值。当时金山办公的产品线里WPS、金山文档、稻壳儿这些工具类App对性能、文档渲染和稳定性的要求很高,所以iOS方向笔试试卷的题目风格和纯互联网C端产品不太一样,它不沉迷于炫技,而是不断追问“你知不知道这个机制在真实场景里是怎么工作的”。
这篇文章我打算按这套卷二暴露出来的核心考点来拆:先讲整张卷子的出题逻辑,再按语言底层、内存、并发、UI系统、网络存储、架构开放题逐一展开作答思路,最后补一段考后复盘的方法。适合正在准备iOS校招、想把基础概念串成体系的人阅读,也适合面试前拿来查漏补缺。
1. 这套“卷二”在考什么:从卷面结构看金山办公对iOS新人的定位
1.1 “二”字背后的卷子设计思路
这里有个很实际的观察点:标注“(二)”说明公司把笔试拆成了两套甚至多套卷子,或者同一场考试分了前半场和后半场。卷一通常偏向通用编程能力、数据结构和逻辑题,卷二则明显收窄到iOS专项。这也是很多工具类大厂惯用的考核策略——先用通用题筛掉编程基本功不合格的人,再用专项题筛掉对平台理解停留在API调用层的人。
从考生的回忆看,这套卷二的主干大致集中在三块:Objective-C语言特性、内存与并发、UIKit与系统框架。这三块恰好对应iOS开发日常排障过程中最容易出问题的三个层面。你没有必要背下某个API的全部参数,但必须知道对象消息发送的完整链路,知道Block在捕获变量时编译器替你做了什么,知道RunLoop的Mode切换和UI卡顿之间的关系。这些内容有一个共同点:它们都不会以“请背诵”的形式出现,而是藏在某段代码或某个运行现象里。
1.2 工具类产品对iOS工程师的隐性要求
金山办公做的是文档办公软件,这类产品对iOS工程师的能力诉求,和做社交、电商App有明显区别。文档App本身就是重渲染、重I/O、重交互的客户端,用户会在上面打开几十兆的PPT,会在弱网环境里同步文件,会在老机型上滚动长文档。所以卷二里出现的考点,其实都能映射到这些业务场景:内存问题对应大文档加载和图片缓存,RunLoop问题对应列表滚动流畅度,本地存储问题对应文件同步的落地策略。
备考这类公司时,不能只刷LeetCode,也不能只看iOS面试题合集。你需要带着“这个知识点如果让我来解决WPS的一个具体问题,我会怎么用”的意识去看待每一条理论。面试官并不指望校招生有几年大型项目经验,但他希望看到你具备“把系统机制迁移到业务问题”的思维方式。
1.3 这套卷子的三道隐形坎
基于反馈较多的错题,我总结出三道隐形坎,后面几章都会围绕它们展开。
第一道坎是“知道但讲不透”。很多考生能说出isa指针、消息转发、ARC这些名词,但被追问“一个NSObject对象占多少字节”“weak变量在对象释放时发生了什么”就支支吾吾。第二道坎是“懂API但不懂调度语义”。GCD几乎是必考,但死锁、队列优先级、信号量这些细节经常被忽略。第三道坎是“会写界面但不懂系统流程”。Auto Layout约束冲突怎么排查、离屏渲染是什么触发的,这些难点恰恰是客户端性能优化的重灾区。
把这三道坎迈过去,这份卷二也就拿下了大半。
2. OC对象与消息机制:几道看似送分的题,怎么答才够深
2.1 对象的内存布局:从isa指针到类对象与元类
卷二如果出现“简述Objective-C对象的本质”这类题,别只答“对象是结构体指针”。一个合格的回答至少要覆盖三条链。第一条是实例对象的内存布局,每个对象内部第一个成员是isa指针,指向它的类对象;类对象里存储着方法列表、属性列表、协议列表和实例变量布局信息;类对象本身也有isa指针,指向元类对象,元类对象里面存的是类方法的实现。第二条是继承关系,子类的isa链会沿着父类向上找,方法的查找也沿着这条继承链进行。第三条是根类,NSObject的父类为nil,但它自己的isa指针指向NSObject的元类,元类的父类又指向NSObject类,形成一个闭环。
笔试作答时,建议画一张“实例对象 -> 类对象 -> 元类对象 -> 根类”的示意图,比纯文字说明清楚得多。如果题目问“类方法存在哪里”,答案不是“存在类对象里”,而是“存在元类对象里”。这个细节最能区分你是背过概念还是真懂。
2.2 对象大小的计算逻辑
有一类题会给你一个类,问你实例对象占用多少内存。比如:
@interface Student : NSObject { NSString *_name; int _age; char _flag; } @end这类题目的标准计算过程分两步。第一步先看成员变量,NSString指针占8字节,int占4字节,char占1字节;第二步用Objective-C的字节对齐规则,系统调用class_getInstanceSize时按8字节对齐,最终分配时按16字节对齐。按上面的成员算下来,实际大小是16字节(ISA指针8字节+字符串指针8字节+年龄4字节+字符1字节,对齐后为24字节,再按16字节对齐为32字节,具体需结合64位系统计算,关键是解释对齐规则)。
回答时一定要把“成员变量大小”和“系统分配大小”分开说,并解释malloc的16字节对齐规则。这是面试官判断你有没有真正读过runtime源码的分水岭。
2.3 属性关键字:strong、weak、copy、assign选型的底层理由
属性关键字几乎是必考,但作答只背“strong是强引用,weak是弱引用”显然不够。比较好的作答思路是:先解释每个关键字的引用计数语义,再说编译器在生成setter方法时用了什么函数,最后给出选型建议。
strong对应objc_storeStrong,会保留新值并释放旧值;weak对应objc_storeWeak,除了赋值以外还会在对象销毁时自动把指针置为nil;assign主要用于基本数据类型,不参与引用计数管理;copy会先拷贝一份再强引用,避免可变对象在外部被修改。这里最经典的坑是“把NSMutableString赋值给NSString属性”,如果属性声明为strong而不是copy,外部修改字符串后属性值会跟着变,这在业务上经常造成数据错乱。你回答时如果能主动举出这个例子,会显得你踩过真实项目里的坑。
2.4 消息发送与转发:方法找不到时的完整处理链
OC的方法调用本质是objc_msgSend。对象收到消息后,runtime会沿着isa指针找到类对象,再从类对象的方法缓存和方法列表里查找对应的IMP;如果当前类找不到,就沿superclass链向上找;直到NSObject还找不到,才进入消息转发流程。
消息转发分为三个阶段:第一阶段调用resolveInstanceMethod,允许你动态添加方法;第二阶段调用forwardingTargetForSelector,允许你把消息转给其他对象处理;第三阶段调用methodSignatureForSelector和forwardInvocation,允许你用NSInvocation封装消息做最后处理。三个阶段全都没处理,才会触发unrecognized selector crash。
笔试里要尽量答出“动态方法解析可以与消息转发区别开”“forwardInvocation是最后一次兜底”这两个点。如果题目给了崩溃日志,你要能通过堆栈判断崩溃是发生在objc_msgSend阶段还是消息转发阶段。
3. 内存管理答卷:ARC边界、Block捕获和循环引用的标准答法
3.1 ARC到底是自动垃圾回收还是编译器插桩
卷二里出现“ARC运行时做了什么”这类题时,很多应届生会随口说“自动释放内存”,这是错误的。ARC全称Automatic Reference Counting,是编译器和运行时配合完成的引用计数管理。编译器在编译期分析对象的 retain/release 调用位置,自动插入合适的引用计数操作;运行时提供objc_retain、objc_release、objc_autorelease等函数支持。它和Java、Go的垃圾回收完全不是一回事,不存在“周期性扫描内存”的机制。
补充一个高频追问:@autoreleasepool在循环里有什么作用。如果循环内大量创建临时对象,这些对象不会立刻释放,而是进入当前自动释放池,池子销毁时才统一释放;不写@autoreleasepool的话,临时对象会积压到RunLoop休眠前才释放,导致内存峰值过高。这个细节在图片批量处理、数据批量解析时非常实用。
3.2 Block的三种类型与捕获变量的本质
Block相关题目需要从“类型”和“捕获”两个维度去答。按存储位置分,Block有三种类型:不捕获外部变量的全局Block存储在全局区;捕获外部变量且未在堆上持有的栈Block存储在栈区;栈Block被copy后变成堆Block。在ARC下,大多数情况下编译器会自动帮我们copy,所以开发者直接接触到的多是堆Block。
捕获变量的规则是考察重点。局部变量被Block捕获时是值拷贝,外部修改变量不会影响Block内部的值;使用__block修饰后,变量会被封装成结构体,Block内外共享这个存储,所以能实现修改外部变量的效果;静态变量和全局变量不需要__block,Block直接访问原存储。回答时能解释“__block为什么会把变量包装成一个结构体”会加分,因为这说明你看到过编译器源码层面的处理。
3.3 循环引用的现场还原与四类检测手段
循环引用属于送分题,但也属于丢分题。最典型的场景是控制器持有一个Block,Block内部又使用self,二者互相强引用,导致控制器无法释放。作答时应当先写出错误代码,再给出修改方案:使用__weak typeof(self) weakSelf = self,在Block内部使用weakSelf。
不过只答这个还不够,展开时我会补充三个高频变体。第一个变体是NSTimer,控制器强持有Timer,Timer的target又强持有控制器,必须在dealloc里invalidate;iOS 10之后的新API可以使用block回调配合__weak self来解决。第二个变体是delegate,delegate属性应该声明为weak,避免代理对象无法释放。第三个变体是Block内直接访问成员变量,有些人以为用了weakSelf就安全了,结果在Block里写_name,编译器仍然会通过self去访问成员变量,等于又强引用了self。
我记得当时的考生圈里流传一个经验:笔试题里凡是出现“为什么这里会闪退”“为什么控制器迟迟不走dealloc”的题干,十有八九是在考察循环引用。实际答题时可以补充Xcode Memory Graph的操作方法:在Debug导航栏点击内存图标的第三个标签,选中对象后可以看到所有引用关系,红色箭头就是循环引用路径。工具层面的回答虽然不能替代原理,但能体现你的排障经验。
4. 并发题的得分点:GCD队列调度、死锁与RunLoop联动
4.1 四类队列和两种派发方式,有多少种组合结果
并发题在iOS笔试里几乎占据20%以上的分值,而最基础的得分点就是队列与派发方式的组合。队列分四种:主队列是串行队列,串行队列按FIFO顺序执行,并发队列允许多个任务同时执行;全局并发队列又区分了服务质量等级。派发方式分两种:同步派发会阻塞当前线程,直到任务结束;异步派发立即返回,不会阻塞。
这里有一个非常经典的必问题:在主队列上同步派发一个任务,会发生什么?答案是死锁。因为主队列是串行队列,当前正在执行的任务没有结束,新派发的同步任务在等待当前任务结束,而当前任务又在等待新任务完成,两个任务互相等待。在串行队列内部同步派发任务也会出现同样的死锁。答出这个例子,并发题基本能拿一半分。
4.2 怎么把“异步加载图片”答出层次感
另一个高频题是“有一个图片加载需求,要求不阻塞主线程,加载完成后回到主线程更新UI,你会怎么做”。初级回答是“用dispatch_async放到后台队列,再用dispatch_async回主队列”。这种回答没毛病,但太单薄,不足以在笔试里突出。
比较好的回答是分三部分。第一部分说明图片解码是CPU密集操作,适合放到全局并发队列,同时要注意并发线程数量,可以用信号量控制最大并发数,避免同时发起大量网络请求;第二部分说明加载结果需要缓存,可以考虑NSCache,同时要注意NSCache在内存紧张时自动清理的特性;第三部分说明回调主线程更新UI必须用主队列,但在真实工程里还要考虑页面已经销毁的情况,所以要用weakSelf并做nil判断。这样一个答案就把并发、内存、UI线程安全全串起来了。
4.3 RunLoop的Mode和自动释放池,与卡顿监控的关系
RunLoop在卷二里通常会和“卡顿”“定时器不准”这类问题绑定。作答核心是四件事。
第一个是RunLoop的基本构成:它有多个Mode,App大部分时间运行在kCFRunLoopDefaultMode和UITrackingRunLoopMode两个Mode下,前者处理普通事件,后者处理滚动事件。第二个是Mode切换:当用户滑动界面时,RunLoop会切换到UITrackingRunLoopMode,此时DefaultMode里的Timer事件不会执行,这就是“TableView滚动时定时器不触发”的原因。第三个是Source:Source0处理触摸等事件,Source1处理系统事件和端口消息,Timer是定时器,Observer是观察者,用来监听RunLoop状态变化。第四个是自动释放池:RunLoop在进入休眠前会销毁一次自动释放池,所以在RunLoop循环中创建的大量临时对象能周期性释放。
卡顿监控的原理也是基于Observer:往主线程RunLoop注册Observer,监听kCFRunLoopBeforeSources和kCFRunLoopAfterWaiting两个状态之间的耗时,如果超过阈值就认为主线程卡顿。答出这个机制,能说明你不只懂概念,还知道它在性能监控里的落地方式。
5. UIKit与布局题:生命周期顺序、约束冲突和卡顿归因
5.1 从main函数到页面显示,生命周期方法的完整顺序
UIKit相关记忆题中,最容易被考到的就是生命周期调用顺序。完整链路一句话总结:main函数调用UIApplicationMain,启动RunLoop,AppDelegate收到didFinishLaunching通知,然后加载window和rootViewController,viewController先调用loadView创建视图层级,再调用viewDidLoad表示视图加载完成,之后viewWillAppear、viewWillLayoutSubviews、viewDidLayoutSubviews、viewDidAppear依次触发。
这里有两个高频追问。第一个是“viewDidLoad里拿到的view.frame是最终值吗”,答案不是,此时的frame只是初始值,布局可能还没完成,真正准确的是viewDidLayoutSubviews里。第二个是layoutSubviews的调用时机:首次布局、约束变化、旋转屏幕、调整frame、滚动内容变化时都可能触发。答题时能说出“布局约束会触发layoutSubviews,而不是每改一次frame都会立即重绘”这个层次,就能超过大多数考生。
5.2 约束冲突排查:从“红色警告”到定位到具体约束
Auto Layout题目通常会给你一段约束代码,问你“界面为什么不显示预期效果,怎么排查”。我在实际项目里遇到最多的问题是约束冲突和约束缺失。
约束缺失会导致Ambiguous layout,系统随机选择一种布局方案,界面看起来完全不可控;约束冲突则是多个约束互相矛盾,控制台会打印“Unable to simultaneously satisfy constraints”,默认表现为其中某条约束被系统随机打破。排查步骤可以这样写:先在Debug菜单打开“View Debugging”,选择“Show View Frames”,看哪个控件的frame变成橙色虚线;再用lldb命令执行po [[UIWindow keyWindow] _autolayoutTrace],查看被标记为AMBIGUOUS的视图;最后在需要检查的视图上调用hasAmbiguousLayout和exerciseAmbiguityInLayout方法,系统会随机切换布局方案帮你快速发现问题区域。这些操作在真机调试里同样有效。
5.3 离屏渲染与列表卡顿:一道题串起整个UIKit优化
“UITableView滑动卡顿”是一个常驻题目,卷二后半场爱考。回答时可以直接把原因分成四类,并对每一类给出优化方案。
第一类是主线程做了耗时操作,比如在cellForRowAtIndexPath里同步读取大文件或做复杂计算,优化方向是异步执行预加载、缓存行高、提前计算;第二类是离屏渲染,常见触发条件是cornerRadius加masksToBounds、设置shadowPath不明确、使用组透明度等,优化方向是使用Core Graphics绘制圆角、设置shadowPath、避免在列表滚动过程中频繁触发离屏渲染;第三类是图层混合,当多个半透明图层叠加时GPU需要混合计算,如果cell背景不透明,尽量给backgroundColor,减少混合层级;第四类是自动布局计算开销过大,可以用frame布局代替约束,或尽量缓存计算好的约束结果。
作答时如果能主动补充“用Instruments的Core Animation工具勾选Color Offscreen-Rendered Yellow把离屏渲染区域标黄”,会明显提升答案的实战说服力。
6. 网络与本地存储:从连接建立到WPS文档同步的完整链路
6.1 一个HTTPS请求的完整旅程
网络题在客户端笔试卷里不会只问HTTP状态码,更常见的是“你点击WPS的打开云文档按钮后,数据是怎么回来的”。完整链路可以按顺序答:URL解析和DNS解析,拿到目标服务器IP,建立TCP连接,三次握手,如果是HTTPS则继续TLS握手,交换证书并协商对称密钥,然后发送HTTP请求,服务端处理,返回响应,客户端解析报文,关闭或复用连接。
笔试卷特别爱考HTTPS握手过程中的性能问题。回答时可以提一句“TLS握手通常比TCP握手更耗时,所以很多客户端会做会话恢复,使用Session ID或Session Ticket来减少重复握手”。这能体现你对弱网体验的敏感度,恰好也是工具类产品比较关注的点。
6.2 NSURLSession的设计边界和后台任务
iOS网络开发里,NSURLSession已经取代NSURLConnection,这是基础,但有几个关键点值得展开。
首先是会话类型:默认会话、临时会话、后台会话三者的区别,后台会话专门用来处理App进入后台后的上传下载任务,系统会在任务完成时唤醒App,适合文档同步这类场景。其次是请求优先级和取消:每个任务都有taskIdentifier,可以通过cancelByProducingResignationData来取消请求并保留可恢复数据,断点续传就是基于这个能力做的。最后是请求之间的依赖关系:可以通过NSOperation封装请求,或者用DispatchGroup等待多个请求完成。如果题目问“怎么实现一个带重试机制的下载器”,你需要答出“根据错误码判断是网络错误还是服务端错误,设置重试次数上限,并采用指数退避策略避免雪崩”,这比只写一个AFNetworking调用更有含金量。
6.3 沙盒目录选型与本地存储方案对比
本地存储题通常会给出一个场景:“离线缓存一份文档和用户设置,存储在哪里”。回答的框架是:先说明沙盒目录的四个区域。Documents目录用于用户可见的重要文件,会被iCloud备份;Library/Caches目录用于缓存临时文件,系统可能在空间不足时清空,不可存放不可再生的数据;Library/Preferences目录存放偏好设置,实际通过NSUserDefaults读写;tmp目录存放临时文件,随时可能被清理。然后说明选型逻辑:用户设置放NSUserDefaults,文档缓存放Caches并做分级清理,用户手动导入的文档放Documents,下载中的临时片段放tmp。
接着可以补充数据库选型对比:NSUserDefaults适合小数据量的无序键值保存,但数据量大时读写性能较差;SQLite适合结构化数据和复杂查询,需要自己管理数据库升级;Core Data在早期版本有迁移问题但现在已经成熟;WCDB是腾讯开源方案,支持加密和跨平台,但从学习成本来看,考题更希望你讲清楚“在什么场景下用哪个”,而不是罗列所有数据库特性。回答时能提一句“WPS这类文档App通常会把文件本体和元数据分开存储,文件按目录写在沙盒里,元数据用数据库做索引,便于批量查询和同步”,会非常加分。
6.4 断点续传和增量同步的作答框架
卷子里很有可能追加一个开放问题:“弱网环境下,一个上百兆的Office文档怎么同步”。答这道题需要给出三层结构。
第一层是任务拆分:把大文件拆成多个分片,每个分片独立上传,记录每个分片的上传状态。第二层是断点续传:上传中断后,客户端向服务端查询已收到的分片,只上传缺失部分,避免重传全部数据。第三层是增量同步:文档编辑保存时,不是整文件上传,而是计算变更块,只提交变更和操作指令。如果题目再深一层问“多端同时编辑同一个文档怎么处理冲突”,可以提到“版本链或操作转换”“服务端用版本向量判断冲突”“WPS这类产品还需要做复杂文档格式的差异合并”。这一层回答能直接展示你思考过真实办公场景的核心难题。
7. 架构与开放题:把“文档App优化”答出面试官想要的结构
7.1 从MVC到MVVM,工具类App怎么选架构
架构题是笔试卷后半段的压轴角色,通常不会直接问“什么是MVC”,而是问“一个大型文档App,怎么组织代码更合理”。回答时应当先承认MVC在iOS平台会带来控制器臃肿问题,然后给出MVVM的思路:View只负责展示和事件传递,ViewModel负责把Model的数据加工成View可展示的格式,Model负责业务数据模型,控制器只做绑定和导航。在Swift项目里还可以补充Combine或RxSwift做响应式绑定,在OC项目里则可以通过RAC或者自己实现KVO回调。
如果笔试时间充裕,还可以提一句“组件化对一个多模块办公套件的重要性”,说明通过私有Pod把文档编辑、云同步、登录、会员模块拆成独立组件,每个组件独立测试,通过路由进行模块间跳转。不过注意不要为了炫技而堆砌术语,要落到具体业务上,比如“为WPS新增一个PDF导出功能,应该把导出逻辑放在ViewModel层而不是放在控制器里,这样便于复用和单元测试”。
7.2 开放题“如何优化文档打开速度”的得分结构
这类开放题最怕考生只给一堆名词。比较好的作答结构是“先测量,再定位,再优化,再验证”。
先测量:打开一个10MB的文档,用Time Profiler看主线程耗时分布,用Memory Graph看内存占用,用os_signpost打点记录各阶段耗时。再定位:把打开链路拆成文件读取、数据解析、文档模型构建、首屏渲染四个阶段,找出耗时最大的阶段。再优化:如果是文件读取慢,考虑使用mmap做内存映射代替一次性读入;如果是解析慢,考虑增量解析、异步解析、预解析;如果是首屏渲染慢,考虑分页渲染,只绘制当前屏幕可见的页面,用异步绘制把渲染从主线程挪走;如果是内存高导致被系统杀掉,考虑复用文本布局对象,避免重复创建昂贵的排版对象。最后验证:对比优化前后的启动时间、FPS、内存峰值和卡顿率,用数据说话。
这个回答模式相当于把一份性能优化工作汇报浓缩在几分钟内,面试官很容易从中判断你是否有过大项目排障经验。
7.3 架构与开放题的“反套路”提醒
这类题有一个常见的扣分点:只说“我会用GCD做异步加载”或“我会用缓存”,而不说“在哪个环节、解决哪个指标、怎么验证”。另一类扣分点是把题目变小,比如问“打开慢”,你只答“图片异步加载”,没有覆盖文档解析和渲染层面。开放题的本质是考察系统分析能力,所以答案一定要有宽度和闭环。我建议备考时养成一个习惯:任何一个优化方案都按照“现状-目标-方案-验证”四步来说,这不仅是面试技巧,也是工程习惯。
8. 考后复盘:用这套题的标准去刷下一份iOS笔试题
8.1 把知识点整理成“底层原理-API-场景”三件套
刷完这份卷二之后,不要急着找下一套题。我发现很多人的问题是“做了很多题,但知识是碎的”。正确的整理方法是在笔记本里为每个核心概念建三列:底层原理、常用API、真实场景。比如RunLoop这一行,底层原理写Source/Timer/Observer和Mode切换,常用API写CFRunLoopAddObserver、performSelectorOnMainThread,真实场景写卡顿监控、Timer在滚动时不触发、AFNetworking的常驻线程。这样遇到新题时,你可以快速从场景找到原理再推导出答案。
8.2 用“追问法”自测,而不是只看对错
笔试复盘时,每道题都要对自己追问两次:第一问“如果面试官在这个答案的基础上再追一句为什么,我能不能接住”,比如你写了weakSelf,是否理解为什么用__weak而不是__unsafe_unretained;第二问“这个题背后的知识点还能套到别的什么题里”,比如你搞懂了Block的循环引用,那么在NSTimer、delegate、通知中心是否也有类似风险。这样练完一份卷子,相当于掌握了三类题型的解法。
8.3 动手写小Demo验证,比背一百道题都有效
以“对象大小”这一题为例,你光看答案很难记住字节对齐细节,但如果你新建一个命令行工程,定义一个类,用class_getInstanceSize和malloc_size打印出来,再比照runtime源码里的align16函数,印象会非常深。同理,Block的类型可以通过class打印出来;死锁可以通过在主队列同步派发一个空Block亲眼验证;循环引用可以通过dealloc是否打印来判断。我见过太多只背概念、从不动手验证的备考者,一到追问环节就露馅。笔试题的终极考点其实是“你有没有真的去理解过”。
翻出这份旧笔记时,我最大的感受是:2020年的考题放到现在依然不过时,因为iOS底层的内存管理、消息转发、RunLoop并发模型这些机制没有根本性变化,变化的只是Swift占比更高、新的UI框架不断出现。如果你正在准备下一场笔试,不妨把这份卷子当作一面镜子,先照出自己还有哪些底层概念是模糊的,再顺着那些模糊点去深挖源码,而不是急着刷完一个又一个题库。等你把对象、内存、并发、UI、网络这几件事真正串成一条线,你一定会发现,所有校招笔试题考到最后,看的不是你会多少API,而是你对一个系统从底层到上层有没有完整的认知框架。