尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

金山办公iOS笔试题复盘:从对象内存到并发调度

金山办公iOS笔试题复盘:从对象内存到并发调度 前两天整理旧资料翻出一份之前整理过的金山办公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除了赋值以外还会在对象销毁时自动把指针置为nilassign主要用于基本数据类型不参与引用计数管理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内外共享这个存储所以能实现修改外部变量的效果静态变量和全局变量不需要__blockBlock直接访问原存储。回答时能解释“__block为什么会把变量包装成一个结构体”会加分因为这说明你看到过编译器源码层面的处理。3.3 循环引用的现场还原与四类检测手段循环引用属于送分题但也属于丢分题。最典型的场景是控制器持有一个BlockBlock内部又使用self二者互相强引用导致控制器无法释放。作答时应当先写出错误代码再给出修改方案使用__weak typeof(self) weakSelf self在Block内部使用weakSelf。不过只答这个还不够展开时我会补充三个高频变体。第一个变体是NSTimer控制器强持有TimerTimer的target又强持有控制器必须在dealloc里invalidateiOS 10之后的新API可以使用block回调配合__weak self来解决。第二个变体是delegatedelegate属性应该声明为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的基本构成它有多个ModeApp大部分时间运行在kCFRunLoopDefaultMode和UITrackingRunLoopMode两个Mode下前者处理普通事件后者处理滚动事件。第二个是Mode切换当用户滑动界面时RunLoop会切换到UITrackingRunLoopMode此时DefaultMode里的Timer事件不会执行这就是“TableView滚动时定时器不触发”的原因。第三个是SourceSource0处理触摸等事件Source1处理系统事件和端口消息Timer是定时器Observer是观察者用来监听RunLoop状态变化。第四个是自动释放池RunLoop在进入休眠前会销毁一次自动释放池所以在RunLoop循环中创建的大量临时对象能周期性释放。卡顿监控的原理也是基于Observer往主线程RunLoop注册Observer监听kCFRunLoopBeforeSources和kCFRunLoopAfterWaiting两个状态之间的耗时如果超过阈值就认为主线程卡顿。答出这个机制能说明你不只懂概念还知道它在性能监控里的落地方式。5. UIKit与布局题生命周期顺序、约束冲突和卡顿归因5.1 从main函数到页面显示生命周期方法的完整顺序UIKit相关记忆题中最容易被考到的就是生命周期调用顺序。完整链路一句话总结main函数调用UIApplicationMain启动RunLoopAppDelegate收到didFinishLaunching通知然后加载window和rootViewControllerviewController先调用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而是你对一个系统从底层到上层有没有完整的认知框架。
返回列表