ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

京东2018秋招iOS笔试题复盘:从内存管理到性能优化的高频考点解析

京东2018秋招iOS笔试题复盘:从内存管理到性能优化的高频考点解析 吃过2018年京东iOS笔试题的亏的人应该不少我前阵子重新翻出这份题目来复盘最大的感受是这套题放在今天依然是很好的面试复习提纲。那年正值iOS 11/12交替、iPhone X全面屏刚普及Swift还没在社区里形成绝对统治力OC依然是电商App的主力语言。所以整张卷子非常“工程向”不考炫技、不抠偏门多数题都是你平时写业务就绕不开的东西。无论你是准备秋招的在校生还是想跳槽的客户端工程师把这份卷子对应的知识点吃透基本等于把iOS面试最常考的底层框架完整过了一遍。这篇文章我就按自己的理解把这份笔试题背后的考察逻辑、核心考点、答题思路和避坑方法逐一拆开讲。1. 京东2018秋招iOS笔试题整体画像与命题逻辑拆解1.1 电商客户端面试为什么偏爱“工程落地”题京东APP是典型的电商业务形态首页信息流、商品详情、购物车、订单状态流转、秒杀活动每一个模块都对列表流畅度、图片加载、弱网处理、复杂状态同步有很高要求。这就决定了它的技术面试不会像某些公司那样抱着纯数据结构不放而是特别爱把底层机制和业务场景绑在一起考。举个例子同样是考Runtime理论型公司可能让你默写消息转发流程京东这类工程型公司更可能问“线上有个埋点需求要在不侵入业务代码的情况下统计所有按钮点击你会怎么设计”。你会发现前者考的是记忆后者考的是用底层知识解决实际问题的能力。2018年秋招的题就是这么个风格表面在问原理实际在暗示你“以后来了要能扛业务”。所以看这份卷子不能只看单题怎么答要先看懂它的命题偏好——考点集中在iOS日常开发的高频区域并且特别强调“能不能落地上线”。你在复习时如果只顾着刷概念题不结合场景做推导笔试很容易翻车。1.2 从解题中总结出的能力分布我复盘时把这份卷子的主要题型按知识域做了个粗略归类比例大致如下考察模块典型考点估计占比OC语言与内存管理对象内存结构、ARC、循环引用、weak原理、Block30%Runtime与底层机制消息发送与转发、Method Swizzling、KVO原理20%多线程与并发GCD队列、NSOperation依赖、线程安全15%网络与数据持久化HTTPS、AFNetworking封装、SQLite、缓存策略15%UI与性能优化列表卡顿、离屏渲染、RunLoop10%架构与代码设计MVC/MVVM、模块化、组件通信10%这套分布是有道理的。对iOS工程师来说OC语言和内存管理是基本功连引用计数都讲不清楚的人上线后很容易制造内存泄漏Runtime是iOS动态性的灵魂大厂App普遍用它做AOP埋点、防崩溃、动态化多线程和UI性能直接决定用户体感网络和存储是电商业务的数据血管。它看起来是一张卷子其实就是一个大厂iOS工程师的日常能力清单。1.3 知识点迭代慢所以老试卷不过时很多人一看到“2018年”就觉得过期了其实iOS底层核心机制迭代得非常慢。ARC引用计数规则没变、RunLoop的源码到今天还是那套逻辑、Runtime的消息转发流程更是十几年稳如泰山。真正变化大的是Swift语法和上层框架但那份卷子偏偏重点考变化最小的部分。换句话说这份题考的不是“最新技术”而是“iOS开发者的内功”。内功是不分年份的你今天去面一些大厂把这份卷子拿出来当模拟题做命中率依然很可观。这也是我把这些老题重新拿出来逐题聊的原因。2. 语言底层与内存管理先把OC的根刨清楚2.1 一个对象占多少内存怎么算才是对的这类题在2018年卷子里几乎是必出的而且往往以“一个NSObject实例占用多少内存”开头。很多人的第一反应是8字节因为NSObject只有一个isa指针。这个答案只能拿一半分因为它忽略了系统的内存分配机制。我们来看底层逻辑。OC对象在运行时对应的是objc_object结构体NSObject本质上就是objc_object加一些方法。一个NSObject实例的大小可以用class_getInstanceSize([NSObject class])拿到实际返回的是8也就是isa指针的大小。但系统通过malloc分配内存时会遵循16字节对齐规则所以malloc_size((__bridge void *)obj)返回的通常是16。也就是说“对象实际占多少内存”在不同层面有不同答案objc层面是8字节堆上实际是16字节。这个考点延伸出去就是带成员变量的对象怎么算内存。比如一个类有两个int属性结构体大小是16字节isa8 int4 int4但经过对齐后仍然分配16字节。如果再加一个double结构体是24字节按16字节对齐就变成32字节。面试官想听的不是你记得对齐规则而是你能说出为什么系统要按16字节对齐——为了CPU访问效率同时也方便内存池管理。顺带说一个容易踩的坑很多人会把class_getInstanceSize和malloc_size混为一谈。前者是对象本身需要的内存后者是系统实际给的内存两者有差异是正常的不需要纠结。答题时按这个思路展开比背结论要显得有深度。2.2 weak、delegate、block、NSTimer循环引用全家桶这份卷子考循环引用从来不手软一般会给你几段代码让你判断会不会泄漏并说明理由。我印象比较深的是结合NSTimer出的题因为NSTimer对target是强引用的很容易构造循环。先说delegate为什么用weak。delegate关系里控制器持有tableViewtableView的delegate指向控制器。如果delegate是strong就会形成控制器→tableView→控制器的循环用weak之后tableView只是弱引用它的代理不增加引用计数循环就断了。这和__weak局部变量原理一样都是通过“不参与引用计数”来打破环。Block的循环引用要更隐晦。最常见的情况是控制器持有block属性block内部又用了self。只要block被self持有block里的self就会被强引用形成self→block→self的环。解法是常见的__weak typeof(self) weakSelf self在block里用weakSelf。但这里有个高阶细节——如果block内部存在异步延迟而weakSelf已经被释放了业务逻辑就会中断。所以更稳妥的写法是进block后先__strong typeof(weakSelf) strongSelf weakSelf再做判断__weak __typeof(self) weakSelf self; self.block ^{ __strong __typeof(weakSelf) strongSelf weakSelf; if (!strongSelf) return; // 使用strongSelf避免执行过程中self被释放 };NSTimer的循环更经典因为timerWithTimeInterval:target:selector:...会对target做强引用。如果你在控制器里创建timercontroller持有了timertimer又强持有controller就把控制器死死锁住。2018年那会儿官方建议用block方式创建timer把target换成一个不持有外部对象的中间对象这样就切断了引用链。我实际开发中用得更多的是封装一个WeakProxy把timer的target指向这个代理代理对controller做弱引用。还有一个高频题是“weak底层怎么实现”。这题要答到SideTable和散列表weak引用会被注册到SideTable中的弱引用表对象释放时会通过dealloc找到这个表把所有weak引用置为nil。知道这层才能解释为什么weak只能用于对象、不能用于非OC对象以及为什么__unsafe_unretained不自动置空。2.3 Runtime消息发送和Method SwizzlingRuntime在2018年秋招里的考法已经从“默写流程”升级到“讲清楚每一步会在什么场景触发”。最典型的题是向一个对象发送一个不存在的方法会发生什么它的完整链路是编译器把[obj foo]翻译成objc_msgSend(obj, selector(foo))。运行时先在obj的类对象里查找方法缓存缓存没命中就查方法列表。都找不到时先走resolveInstanceMethod:你可以在这里用class_addMethod动态添加实现。如果没处理再走forwardingTargetForSelector:可以把消息转发给其他对象。如果还是没处理走methodSignatureForSelector:和forwardInvocation:实现完整的消息转发。都没有最终触发doesNotRecognizeSelector:程序崩溃。这道题想拿高分就得多说一句消息转发机制是OC动态性的根基也是很多防崩溃和AOP方案的基石。比如有些人会在第三步动态添加方法有些人会在第四步把某个不存在的selector转发给一个“防崩溃对象”让app不至于闪退。Method Swizzling也是必考。常见考法是“如何在不改变原有实现的情况下给所有viewController的viewWillAppear加日志”。标准做法是交换两个方法的实现 (void)load { static dispatch_once_t onceToken; dispatch_once(onceToken, ^{ Class cls [UIViewController class]; SEL originalSel selector(viewWillAppear:); SEL swizzledSel selector(xxx_viewWillAppear:); Method originalMethod class_getInstanceMethod(cls, originalSel); Method swizzledMethod class_getInstanceMethod(cls, swizzledSel); method_exchangeImplementations(originalMethod, swizzledMethod); }); }这题的坑很多比如为什么要在load里做、为什么要用dispatch_once、如果父类子类都实现了Swizzling会不会导致父类实现被覆盖。我在项目里踩过最深的就是在initialize里做Swizzling因为initialize可能被触发多次导致交换多次方法实现直接让调用进入到无限递归。所以记住一个原则Swizzling尽量在load里做而且要保证只执行一次并且要在方法调用发生之前。2.4 KVO的实现原理别只背“用了isa切换”这份卷子大概率会带一道KVO题因为KVO和KVC在日常开发里用得实在太频繁。大多数人的回答是“KVO基于Runtime会自动生成一个子类重写setter方法”。这个答案能及格但很难出彩。完整链条是这样的调用addObserver:forKeyPath:时Runtime会动态创建一个当前类的子类名字通常是NSKVONotifying_XXX然后把对象的isa指针指向这个子类。之后对属性赋值时实际调用的是子类重写过的setter方法这个setter先调用willChangeValueForKey:再调用super的setter最后调用didChangeValueForKey:并在didChangeValueForKey:里通知观察者。这里有个值得深挖的细节KVO重写setter后如何手动触发和自动触发。如果我在业务代码里直接操作成员变量而不走setterKVO是不会触发的这就是为什么KVO要求属性必须通过setter赋值。有些团队想批量刷新就会手动调用willChangeValueForKey:和didChangeValueForKey:这也能解释为什么很多老面试官爱问“手动触发KVO的方式”。我自己的体会是KVO在2018年那会儿用得很多后来很多团队转向了Block回调或者RxSwift这类方案来减少KVO的不确定性。但底层原理没变答KVO的时候把isa-swizzling和setter重写讲透就已经赢过一半的候选人。3. 多线程、UI与性能优化把卡顿和并发掰扯清楚3.1 GCD和NSOperation如何选不要再说“随便用”2018年这份卷子里有一类题我非常喜欢几个网络请求并发执行全部结束后统一刷新UI你怎么实现。多数人会直接答dispatch_group但这题它的意图其实想问你能不能分清GCD与NSOperation的适用边界。用GCD的dispatch_group是最直接的解法dispatch_group_t group dispatch_group_create(); dispatch_queue_t queue dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); for (NSURLRequest *request in requests) { dispatch_group_enter(group); dispatch_async(queue, ^{ // 发起请求... dispatch_group_leave(group); }); } dispatch_group_notify(group, dispatch_get_main_queue(), ^{ // 全部完成刷新UI });这里有个隐藏坑enter和leave必须成对出现。如果网络请求回调里因为异常提前return没走到leavegroup的计数就永远不会归零notify也不会执行。所以写生产代码时最好在请求回调里用defer或者兜底逻辑保证leave一定被调用。GCD虽然好用但遇到“请求A之后做请求BB之后再并发做C和D”这类依赖关系时就比较吃力。NSOperation可以通过addDependency:精确控制依赖还可以通过maxConcurrentOperationCount设置并发数这对控制同时联网的请求数很有帮助。我自己的经验是简单并发用GCD复杂依赖和需要取消操作时用NSOperation。笔试里把这句话写出来面试官会知道你真的在工程上做过取舍。3.2 列表快速滑动依然掉帧排查思路是什么关于UITableView流畅度的题在电商系面试里出现频率极高。2018年京东的首页结构非常复杂图片多、模块多、接口多滑动掉帧是纯业务驱动的痛点。所以卷子里就出现了类似的问题列表滚动卡顿你如何定位和解决。从排查路径看核心思路是区分“主线程CPU耗时”和“渲染层耗时”。第一步打开Instruments的Time Profiler看主线程上哪些方法占用时间长。常见元凶排名是图片的解码和缩放UIImage从网络数据到可显示是需要解码的如果直接在cellForRowAtIndexPath里做主线程会卡。复杂的AutoLayout计算约束越多计算越复杂。动态行高计算每个cell都现场算高度滚动时会频繁触发计算。第二步用模拟器或真机的Debug选项看离屏渲染。这里最常见的坑是给UIImageView加cornerRadius和masksToBounds以及给view加阴影。这些操作会触发离屏渲染在列表快速滑动时极其费性能。解决方案是预裁剪圆角图片或用UIBezierPath绘制圆角而不是直接依赖系统属性。第三步检查cell的复用是否合理。比如不同样式的cell混在一个table里应该注册多个不同的cell类而不是在同一个cell里动态添加子视图。动态添加子视图不仅影响复用还会让约束计算反复触发。放一个我当时做项目时的优化清单当滑动仍然掉帧时按顺序排查排查项操作验证方式图片解码使用YYImage或SDWebImage预解码缩略图尺寸与服务端约定Time Profiler观察解码耗时cell高度提前缓存高度避免重复计算看heightForRowAtIndexPath调用次数离屏渲染删除阴影和圆角操作改用预绘制Core Animation的Debug开关标红区域减少主线程耗时把非UI计算放到子线程观察主线程占用率这道题答得好不好关键在于有没有“排查”思维而不仅仅是背优化手段。面试官想看到的是你拿到一个真实问题以后按什么顺序快速定位和验证而不是一上来就堆优化术语。3.3 RunLoop到底在循环什么怎么用它做监工RunLoop在题库里属于“看似简单、实际最能拉差距”的一题。2018年很多候选人都会说“RunLoop是事件循环用来保证线程不退出”但这只是第一层。真正有深度的回答至少要覆盖线程和RunLoop是一一对应的主线程的RunLoop默认开启子线程默认不开启。RunLoop有Source0、Source1、Timer、Observer四类事件源。Mode的作用是隔离事件源主要场景是kCFRunLoopDefaultMode和UITrackingRunLoopMode滚动时主线程切换Mode导致定时器不执行。关于定时器这个点典型考题是“滑动页面时NSTimer不回调怎么办”。原因是滑动时RunLoop切换到TrackingModeTimer不被调度。解决方案是将Timer加入NSRunLoopCommonModes或改用GCD定时器因为GCD是单独队列不依赖RunLoop Mode。RunLoop还有一个高频用处是卡顿监控。思路是往主线程RunLoop注册一个Observer每次进入kCFRunLoopBeforeSources或kCFRunLoopAfterWaiting时记录时间戳等RunLoop从其他状态回来时计算本轮的耗时。如果超过阈值比如50ms就认为有卡顿发生再把线程的调用栈上报。这个方案后来成了很多APM SDK的雏形面试时能说出这层会让人觉得你真的用过。3.4 iOS分屏、多窗口对布局的挑战笔试题背后的新场景2018年正好是iPad分屏能力普及的阶段卷子里偶尔会夹带一道关于屏幕适配的题本质上你要处理的不只是屏幕尺寸不同而是同一时刻可能有多个不同宽度的窗口同时存在。这意味着AutoLayout不能假设“我的view肯定是全屏的”。典型解法是用UIViewController的traitCollection.horizontalSizeClass判断当前环境是紧凑型还是常规型不同尺寸下切换布局。还有一个细节分屏下viewSafeAreaInsetsDidChange会被频繁调用不能把insets相关计算写在viewDidLoad里必须在布局方法里重新计算。这个点在笔试里作为“延伸讨论”提一嘴会显得你对新场景有敏感度。4. 网络层、数据存储与架构设计从单点功能到整体设计4.1 HTTPS证书校验和中间人攻击到底在问什么网络层的题京东这套卷子基本绕不开HTTPS尤其喜欢问“客户端怎么确认服务器身份”。表面考察的是TLS流程实际想看你有没有真正接入过。简单捋一下HTTPS通过非对称加密协商出一个对称密钥之后用对称加密传输数据。客户端要确认自己连的服务器是真的就需要验证服务器返回的证书。证书由CA签发客户端内置了受信任的CA根证书。服务端证书会形成一个证书链客户端逐级验证最终如果证书链能追溯到信任的根证书才认为服务器身份可信。在iOS工程里这个逻辑体现在AFNetworking的securityPolicy上。默认模式是AFSSLPinningModeNone只校验证书链不做公钥绑定。更安全的是AFSSLPinningModeCertificate或AFSSLPinningModePublicKey把服务器公钥或证书内置到App里防中间人抓包。很多金融和电商类App为了安全会使用公钥绑定因为测试证书可以被替换但公钥一般不变。这里有一个比较细的考点中间人的危害到底是什么。如果客户端不做证书校验攻击者在自己机器上装一个伪造证书就有可能在用户和服务器之间建立双向代理看到所有明文流量。所以“HTTPS是不是安全”的关键不在于加密算法本身而在于“是否验证了证书”。我当时复习时把AFNetworking的源码看了一遍着重看AFSecurityPolicy的几个方法笔试里碰到这题就把“校验证书链”和“选择pin模式”这两点展开很快就和只会背TLS握手流程的人拉开了距离。4.2 登录态、商品缓存、头像分别用什么存储这类存储选型题非常贴合电商业务。2018年卷子里有一道典型的登录状态、用户头像、商品列表缓存你怎么选型。首先要分清三层NSUserDefaults适合存小规模的配置项比如主题色、开关设置、用户ID。它底层其实就是plist文件读写是整体加载的不适合存大数据。登录token这种小字符串放在这里没问题。SQLite适合存结构化、可查询的数据。商品列表缓存用SQLite很合适因为商品字段多、数量大、还需要按条件查询和分页。iOS里常用FMDB做封装注意打开WAL模式能大幅减少读写的锁冲突。用户头像这种图片类内容一般放在文件系统里路径存到数据库。不要直接往NSUserDefaults里塞NSData分分钟撑爆。深层一点面试官可能追问“为什么不用CoreData”。CoreData也不是不行但从工程可控性来说SQLite的SQL语义更明确、排查问题更直观。很多大厂App最终都选择了SQLite系的ORM或自定义存储层不是因为它最潮而是因为可控。你答题时提到“数据库版本迁移”也很加分因为表结构变更是一切本地存储绕不开的坎2018年那会儿主流做法是用FMDB的user_version配合自定义迁移脚本。4.3 京东这种体量的App代码架构应该怎么组织架构题几乎是所有大厂笔试的压轴戏。2018年京东卷子里有一题大约是面对几百人的客户端团队你怎么设计App的架构让它能并行开发、互不干扰。答题思路不能只停留在MVC和MVVM。面试官想听的是模块化和组件化。至少在思路上提四点基础层包含网络、存储、日志、埋点等基础设施用CocoaPods私有仓库管理每个库独立版本号。业务模块层按业务域划分比如首页、商品详情、购物车、订单模块之间不能直接引用对方的头文件。组件通信模块之间通过路由或协议通信。iOS生态里常见方案有基于URL的路由以及基于Protocol的依赖注入。CTMediator这种基于Target-Action的方案在一个阶段也比较流行它的核心是减少模块间的编译依赖。壳工程只负责组装模块、配置启动流程不写业务。再深一层可以提一下“组件化过程中容易翻车的点”。比如模块划分如果没有和业务边界对齐拆着拆着就变成“互相依赖的地狱”比如基础库升级会影响所有业务模块所以必须建立灰度机制。这些都是工程落地才会遇到的问题笔试里能主动说出来比单纯列“我用了MVVM”要有说服力。顺带可以联系一下“混合开发方案”这个延伸考点。电商App经常要做活动页这里就涉及WKWebView与原生交互以及React Native、Flutter等跨端方案。2018年团队里比较常见的是URL拦截和JavaScriptCore注入后续才慢慢演化为更成熟的桥接方案。笔试里即便不要求你写具体实现至少要把“为什么需要混合开发”“混合开发的风险”讲清楚因为这是大厂客户端团队无法回避的现实。5. 常见问题与备考避坑指南5.1 笔试答题最容易出现的4个扣分点我看了很多人的复盘发现扣分往往不是知识不够而是“答题方式”出了问题。第一只写答案不写理由。比如问“运行时会崩溃吗”直接就答“会”。面试官看不到你的推理过程他就无法判断你是真懂还是蒙的。任何判断都要带走代码逻辑比如“因为weak不会增加引用计数所以……”第二底层机制只会说到“好像”。这个特别常见比如能说出“Block会截获变量”但说不清__block变量被包装成结构体后如何在堆上共享。这种半碗水的水平在笔试里会露出马脚因为人家会顺着你的话往下追问。第三代码不编译就交。笔试里写OC代码的语法其实不难难的是在纸上保证每行都不出错。我的建议是写完后在脑子里模拟执行一遍特别检查循环引用、代理是否weak、字符串为空等边界。第四不按题目限定环境回答。题目问“在iOS 11及以上”你却拿iOS 14的新API作答方向就歪了。答题时先确认系统版本、线程、内存环境再给结论。5.2 高频问题排查工具速查表这份卷子虽然不直接考工具但如果你能在相关题目里顺带提到工具会显得你是实战派。我整理了一张速查表问题现象推荐工具关键操作列表卡顿Instruments - Time Profiler查看主线程耗时函数离屏渲染模拟器Debug - Core Animation打开Color Offscreen-Rendered标红检查内存泄漏Instruments - Leaks / MLeaksFinder巡检泄漏对象网络请求异常Charles / 代理工具查看请求参数、响应体、证书信息崩溃定位崩溃日志 symbolicatecrash符号化还原调用栈Charts这种抓包工具平时调试网络特别方便。有时候后端说“接口返回正常”你拿代理一看发现请求体多了个字段或者证书在多环境切换时失效问题一下就定位了。笔试里解释网络问题时顺手提一句“用Charles验证过请求链路”比空谈理论扎实得多。5.3 基于这套题的复习路线我踩过坑后总结的时间表如果你准备面试我建议按这个节奏来第一周专攻OC语言和内存管理。每天过一遍引用计数、weak原理、Block、循环引用然后刷对应的手写题。不要背题要把每个问题整理成“原理场景解法”三段式。第二周多线程和UI性能。多线程重点做分组并发、依赖、线程安全的代码题UI性能重点理解自动布局、cell复用、离屏渲染。最好打开Instruments把卡顿Demo真的跑一遍。第三周网络和架构。把AFNetworking的源码过一遍理解HTTPS下的安全策略架构部分去找一篇组件化落地的工程文章结合自己项目画出模块关系图。第四周冲刺模拟。用这份2018年的题目做一次全真模拟限定时间写出答案然后逐题对照查漏。我当年的教训是只读不写。看了很多Runtime源码但一到手写题就卡壳。从第二次复习开始我强制自己每天手写两道题一周后手感和表达能力都明显提升。写在最后的经验这套2018年的笔试题我反复看了很多遍每看一遍都有新收获。早年我自己面试也挂过很多次后来发现一个规律只要能把这些高频问题做一次“从概念到源码再到业务场景”的完整整理面试时表达出来就完全不慌。别把笔试当成考试把它当成一次自己项目的“故障排查练习”你会发现那些底层机制其实都是你平时写代码时踩过的坑。最后再分享一个小技巧每个考点都尝试写成一篇自己能讲10分钟的文章用“讲得出”代替“记得住”这会让你在技术面试里的表现发生质变。
返回列表