ARTICLE DETAIL

资讯详情

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

iOS面试核心考点:内存管理、RunLoop与runtime实战解析

iOS面试核心考点:内存管理、RunLoop与runtime实战解析 2017年秋招那阵子我在广州跑了不少笔试场欢聚时代的iOS工程师B卷给我的印象很特别它不像有些公司那样堆一堆API背诵题而是把大量篇幅放在底层原理和直播场景的业务结合上。当时YY直播已经是国内直播领域的第一梯队“高并发消息”“实时互动”这些词对iOS开发来说不是宣传噱头而是每天都要面对的线上压力。也正因为这样这份B卷里的核心考点放到现在准备iOS面试依然很有参考价值内存管理、多线程、RunLoop、runtime外加一道性能设计题基本就是iOS中高级面试的骨架。这篇文章不会去逐题背诵原卷毕竟完整的B卷原题不一定还留存在网上而是按当时的技术栈和面经共识把高概率出现的核心考点、正确答题思路、以及我后来自己做面试官时最看重的回答方式一次性讲透。适合正在准备iOS面试的应届生也适合工作两三年的开发者回来查漏补缺。1. 先拆B卷的出题逻辑直播大厂最在意哪几项基本功1.1 从业务场景反推考纲直播App的iOS端一天要支撑海量在线用户同时弹幕、送礼、连麦。这种业务场景决定了一个iOS工程师不能只会写页面还要能回答三个问题内存会不会爆主线程会不会卡异常情况下会不会崩溃所以笔试题目基本都绕着这三个问题展开。考点分布一般集中在Autorelease、weak/strong/copy、Block的循环引用、RunLoop运行模式、GCD队列、GCD死锁、runtime消息转发、KVO实现、Category、App启动流程、崩溃类型、性能优化。这些东西看起来是“基础题”但组合起来就是直播App每天都可能踩的线上坑。我用一个表格把这层对应关系理清楚方便你按业务痛点去记考点业务痛点对应考点笔试常见的提问方式直播间消息量太大导致内存暴涨内存管理、自动释放池for循环里大量临时对象如何优化滚动时动画卡顿RunLoop、CADisplayLinkTimer在滚动中为什么失效房间内并发任务调度GCD、NSOperation多个下载任务如何控制并发动态下发热点功能runtime消息转发给对象动态添加方法线上崩溃难以定位异常类型、符号化如何拿到并解读崩溃日志手机型号适配iOS 11安全区iPhone X布局怎么适配1.2 题型配比与精力分配参加过当年笔试的同学回忆B卷大致由选择、填空、简答/代码、开放性设计题组成。选择题覆盖很杂每道题的分量都不低不光是单选偶尔还会出现多选漏选错选都扣分简答题通常要求手写代码或者完整解释原理不是写个大概就能过关开放题没有标准答案但最拉分。给一个精力分配建议如果离笔试还有两周把50%时间花在内存管理、多线程、RunLoop、runtime这四块20%花在UIKit与Autolayout适配20%花在网络层与崩溃处理10%用来练手写题速度。不要花太多时间去背各种枚举和API拼写笔试不是查文档比赛。更重要的是很多题会“换皮”出现今天考Timer在滚动中失效明天可能考AVPlayer在滑动时暂停底层还是同一个RunLoop模式问题。1.3 关于“B卷”这个细节“B卷”意味着和A卷是平行卷题目难度未必低但命题风格往往一致。如果你身边有人先考了A卷不要只盯着原题要把A卷出现过的所有考点横向扩展一遍。同一个知识点的问法很可能会换角度——比如A卷考weak能不能解决Block循环引用B卷可能考__block和__weak的区别也可能考strongSelf在Block里的作用。所以我一直觉得刷往年题的重点不是“背住答案”而是把相关知识点织成一张网。2. 内存管理题weak 变量到底会不会被置空以及 ARC 背后的引用计数2.1 一道高频选择题的完整解法当年几乎每份iOS笔试题里都有这样一道题一个weak修饰的UIViewController属性在对象释放后这个属性指向什么选项有野指针、nil、原地址、未定义。正确答案是nil。但面试官真正想听的不是这个结论而是“为什么”。网上很多答案会背一句“weak会自动置空”但不会解释背后的存储结构。我建议你至少理解到这一层所有weak修饰的对象注册时会在运行时的一个全局映射表里记录这个对象的地址和所有weak指针的地址对象dealloc时运行时通过这个映射关系找到所有指向该对象的weak指针统一置为nil再清理表项。这个过程不需要遍历整个内存代价很小所以苹果在ARC里推荐使用weak而不是unsafe_unretained。我试过用下面的代码验证笔试前可以跑一遍Person *person [[Person alloc] init]; __weak Person *weakPerson person; NSLog(%, weakPerson); person nil; NSLog(%, weakPerson); // 输出 (null)面试手写解释的时候只要能把“全局weak表中存了weak指针地址dealloc时统一置空”这层意思说出来就已经超过大部分候选人了。如果还能补一句“unsafe_unretained不会自动置空释放后访问是野指针所以被weak替代”那这一题的印象分会更高。2.2 strong、weak、copy 在属性修饰里的“语言陷阱”笔试里经常给出一段属性声明问哪行写法有问题。例如property (nonatomic, strong) NSString *name; property (nonatomic, copy) NSString *alias;strong修饰NSString到底行不行很多老代码里确实这么写但坑在于如果外部传入一个NSMutableStringstrong只是保留了同一个可变对象后面原可变字符串一旦被修改属性值也会跟着变。而copy修饰会生成一份不可变拷贝避免外部修改影响内部状态。这就是为什么NSString属性通常建议用copy。NSArray、NSDictionary也一样容器类属性用copy比strong安全面试时可以顺口带一句显得你有全局意识。再补充一个容易混淆的点copy修饰NSMutableArray反而是不合理的因为copy返回的是不可变对象运行时再对它调用addObject:会直接崩溃。所以可变容器一般用strong不可变容器用copy。这种细节特别喜欢出成判断对错题。2.3 自动释放池为什么能“拯救”内存峰值有一道很经典的填空/代码题在for循环中创建大量临时对象如何优化内存峰值。标准做法是加autoreleasepoolfor (NSInteger i 0; i 100000; i) { autoreleasepool { NSString *str [NSString stringWithFormat:%ld, (long)i]; // 使用str } }原理是非自己持有的对象通常进入自动释放池如果在循环内一直累积要等到当前RunLoop周期结束才会释放内存峰值会被拉得很高。加autoreleasepool后每个循环迭代结束时都会立刻释放这部分临时对象。这题在直播场景里特别常见——直播间弹幕解析、消息列表构造到处都是高频循环创建对象。补充一个容易被忽略的点ARC时代手动写autoreleasepool依然有效因为ARC只管引用计数平衡不管具体释放时机。很多人以为有了ARC就没有内存管理了这理解是错的。ARC解决的是“什么时候retain/release”的问题而autoreleasepool解决的是“对象什么时候真正销毁”的问题两者是不同维度。2.4 Block 循环引用笔试里出现率最高的代码题还有一种必问题Block中使用self为什么会循环引用。我当时笔试选择题里基本每次都会出现。答案要点是self持有blockblock又捕获并持有self形成引用环。解决办法是__weak typeof(self) weakSelf self; self.block ^{ __strong typeof(weakSelf) strongSelf weakSelf; [strongSelf doSomething]; };很多人只写weakSelf没解释里面为什么又转了一次strongSelf。这里的关键是Block执行期间weakSelf可能随时变成nil如果连续多次调用weakSelf的方法中间对象可能已经释放。转成strongSelf后在Block执行体内持有对象保证整段逻辑执行期间对象不会半路消失。笔试时如果能答出这一层说明你真的写过异步代码不是只会背面试题。3. RunLoop 和多线程那道“Timer 在滑动时卡住了”的场景题3.1 先讲清楚 RunLoop 和“模式”到底是什么RunLoop可以理解为一个“等待事件的循环”没有事件就睡觉有事件就起来处理。每个线程的RunLoop默认不启动只有主线程的会随着App启动自动运行。而RunLoop里有一个很关键的概念叫模式mode每次只能运行一个模式。NSTimer默认添加在kCFRunLoopDefaultMode也就是default模式下当用户滑动列表时主线程的RunLoop会切换到UITrackingRunLoopMode来处理滚动事件default模式下的Timer事件暂时不执行。所以你会看到滑动时Timer不触发滑动结束后Timer又立刻连续触发甚至可能出现积压回调。这不是网络问题不是系统bug是运行模式的优先级和切换机制导致的。3.2 解决办法与陷阱当年笔试里经常要求写一个不被滑动打断的定时器。常见方案有三种把Timer加到NSRunLoopCommonModes、用dispatch_source_t、用CADisplayLink。我一般推荐第二种因为更可控也方便指定执行队列dispatch_source_t timer dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue()); dispatch_source_set_timer(timer, dispatch_time(DISPATCH_TIME_NOW, 0), 1 * NSEC_PER_SEC, 0); dispatch_source_set_event_handler(timer, ^{ // 定时任务 }); dispatch_resume(timer);注意dispatch_source_t需要强持有我见过不少人把这个timer变量写在函数局部结果一离开作用域定时器就被释放任务根本不执行。正确做法是把它作为属性或者静态变量持有。另外CADisplayLink方案虽然不依赖RunLoop模式但它和屏幕刷新频率绑定如果只是做倒计时精度反而不如GCD Timer因为屏幕刷新率和真实时间不是完全一一对应的。能说出这几种方案的局限比单纯背一个解法更能打动面试官。3.3 常驻线程直播心跳任务是怎么做的还有一个高频简答题如何在子线程中执行一个长期存活的任务而不被销毁。经典做法是启动子线程的RunLoop并添加一个port或其他source防止RunLoop因为没有事件源而退出- (void)keepThreadAlive { NSThread *thread [[NSThread alloc] initWithBlock:^{ [[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode]; [[NSRunLoop currentRunLoop] run]; }]; [thread start]; }直播客户端里心跳包、一些轻量级任务经常在独立常驻线程上执行避免影响主线程。如果面试官追问“这个线程会不会常驻占资源”你要能回答RunLoop在没事做时会休眠不会空转消耗CPU。这个知识点也能解释为什么很多App的主线程在做完启动任务后会“闲着”但依然能响应点击和事件——因为它睡在RunLoop里。3.4 GCD 的必考死锁题多线程题目里出现率最高的就是在主线程执行dispatch_sync(dispatch_get_main_queue(), block)会怎样答案是死锁。因为sync会等待block执行完才返回而主线程正在执行sync这一行block被派发到主队列时只能等主线程空闲两边互相等待谁也走不动。同理在任何串行队列里如果当前正在执行这个队列的任务同时又sync提交新任务到这个队列也会死锁。这个考点要记住不只是主队列会崩。我见过不少人只记住主队列场景换一个自定义串行队列就答不上来这就是典型的背题没理解。可以这样自查只要当前线程正在执行某个串行队列的任务再对这个队列做同步提交就会死锁改成async就不会。4. Runtime 与消息转发动态添加方法、KVO 的 isa-swizzling 和交换方法的坑4.1 消息发送和动态解析的三件套OC的方法调用本质上是消息发送编译后变成objc_msgSend(receiver, selector, args)。运行时在找不到实现时会依次走三步动态方法解析、快速转发、完整转发。笔试代码题里经常给一个场景某个对象被调用了不存在的方法问怎么做到不崩溃且能处理。最直接的方式是用class_addMethod在动态解析阶段给类加上实现void myMethodIMP(id self, SEL _cmd) { NSLog(动态添加的方法被调用了); } (BOOL)resolveInstanceMethod:(SEL)sel { if (sel selector(doSomething)) { class_addMethod(self, sel, (IMP)myMethodIMP, v:); return YES; } return [super resolveInstanceMethod:sel]; }这里的v:是类型编码不是随便写的。v代表void代表id:代表SEL。手写题时很容易漏掉类型编码如果写错方法调用结果不可预期甚至是崩溃。这个小坑值得专门提醒。笔试时能把类型编码写对通常说明是真的了解runtime的实现细节而不是背了class_addMethod的拼写。4.2 KVO 底层到底做了什么KVO几乎每年笔试都会出现而且问得很细。一个对象注册KVO后runtime会动态生成它的子类NSKVONotifying_XXX重写被监听属性的setter方法再把对象的isa指针指向这个子类。所以外部看起来对象没变但实际已经是子类实例。关于isa-swizzling有个经典追问KVO触发时机在setter前还是后正确答案是先调用willChangeValueForKey:然后真正赋值最后调用didChangeValueForKey:。手动触发KVO可以这样写[obj willChangeValueForKey:name]; [obj didChangeValueForKey:name];如果面试官再追问为什么KVO监听的属性用成员变量直接赋值时不会触发因为成员变量赋值不会走重写后的setter也就不会调用willChange/didChange。这个细节非常能区分候选人是背过答案还是真正调试过。我自己是踩过一次坑才彻底记住的当时给一个模型直接赋值成员变量KVO回调死活不触发查了半天才发现问题出在这里。4.3 方法交换黑魔法在笔试中的正确答法方法交换Method Swizzling的题目通常考察两点一是在哪里交换二是怎么避免重复交换。规范做法是在load方法中执行用dispatch_once保证只执行一次。有个很隐蔽的坑如果对父类方法做交换会影响子类因为子类可能并没有实现这个方法调用时走的是父类实现。处理方式一般是先判断当前类是否实现了要交换的方法如果没实现就把目标方法添加为当前类的方法再交换。下面这段是当时比较通用的安全写法Class class [self class]; SEL originalSelector selector(viewWillAppear:); SEL swizzledSelector selector(swizzled_viewWillAppear:); Method originalMethod class_getInstanceMethod(class, originalSelector); Method swizzledMethod class_getInstanceMethod(class, swizzledSelector); BOOL didAddMethod class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); if (didAddMethod) { class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); } else { method_exchangeImplementations(originalMethod, swizzledMethod); }笔试不要求一字不差背出来但要知道为什么不直接method_exchangeImplementations而是先class_addMethod。核心原因是避免子类没有实现父类方法而误交换。方法交换在生产环境里要慎用它会影响全局所有对象所以做题时可以把“换完用dispatch_once保护”作为加分项写出来。4.4 关联对象是不是“真属性”还有一道和runtime相关的选择Category中能否添加成员变量答案是不能直接添加但可以用objc_setAssociatedObject实现“关联对象”。关联对象和真正的实例变量不一样它不会自动参与归档序列化也不支持KVO自动监听。如果笔试题问“Category里加属性有什么坑”这几个点就是主要回答方向。顺带说一个容易混淆的地方关联对象虽然可以通过setter/getter访问但它并不在对象的ivar列表中所以用class_copyIvarList是看不到的。这个特性可以用来区分你是“见过用法”还是“理解原理”。在直播业务里关联对象经常用来在UIView上挂额外状态但用多了会显得代码不够清晰我自己是尽量不用的。5. iPhone X 与 iOS 11 时代适配、崩溃符号化和网络抓包5.1 Safe Area 改变了什么2017年这份B卷出现在iPhone X发布前后iOS 11成为首个强推Safe Area的版本。笔试里相关的题目通常是如何让页面内容不被刘海遮挡。在iOS 11之前很多App用autoresizingMask和frame手算导航栏高度到iOS 11开始官方推荐使用safeAreaLayoutGuide。UIScreen.main.bounds.size.height在某些机型上也不再等于可视区域高度因为底部多了Home Indicator区域。如果是storyboard布局可以直接让约束指向safeArea如果是纯代码则用view.safeAreaLayoutGuide的锚点来创建约束。适配时最容易漏的是底部按钮、全屏图片、tableView的contentInset。这几个位置如果不按safe area处理就会出现按钮被Home Indicator遮挡或者TableView最后一行被裁掉的问题。我当时踩过的坑是以为只要把背景色延伸到全屏就完事了结果按钮还是被裁了一截。后来才明白背景延伸到边缘是视觉效果可交互控件必须保持在safe area内。这个思路在面试里也可以作为回答案例先区分视觉延伸和交互安全区再分层面讲适配方案。5.2 崩溃日志从符号化到定位笔试的简答题里偶尔会贴一段崩溃日志问属于哪类崩溃。常见类型有EXC_BAD_ACCESS访问了已释放对象、SIGABRT一般是异常退出比如unrecognized selector、SIGSEGV段错误、SIGPIPE管道破裂常见于socket写入失败。遇到这种题先看异常类型再看崩溃线程的调用栈最后看是否是主线程。如果调用栈全部是十六进制地址说明没有符号化需要把崩溃地址和dSYM文件对应起来用atos命令或者Xcode的symbolicatecrash脚本符号化。有一点要注意符号化必须使用归档时上传的dSYM版本对不上符号化结果就是乱码。我实际处理的线上崩溃案例里很大一部分是KVO观察者未移除、Block循环引用导致延迟释放、以及多线程访问可变数组。笔试如果问“什么原因最可能导致dealloc不调用”这几个方向是需要优先回答的。回答时可以提一句“除了分析崩溃日志还会结合线上监控统计设备分布和系统版本分布”这样显得有工程化思维。5.3 Charles 抓包笔试之外的实战技能热词里出现“charles ios抓包”说明到现在这依然是iOS问题排查的高频动作。当年这份B卷虽然没有强制考Charles操作流程但网络层的题绕不开HTTPS抓包思路。基本流程是Mac上装Charles启动代理iPhone连同一个WiFi并设置代理为Mac的IP和8888端口。如果要看HTTPS明文需要安装Charles根证书并到“设置-通用-关于本机-证书信任设置”里开启完全信任。iOS 10.3之后多了一步手动信任很多人抓不到HTTPS包就是因为没做这一步。抓包是开发调试技能要注意遵守公司的安全规定。但从排障角度说学会看请求耗时、DNS耗时、TLS握手耗时对回答“线上接口慢怎么排查”这类开放题很有帮助。这里可以先用Charles分阶段定位是DNS慢还是建立连接慢还是服务端响应慢再针对性优化。这个过程本身就是面试官想看到的排查思路。6. 开放设计题弹幕不卡、礼物连击不闪会怎么设计6.1 面试官出这道题的潜台词直播场景的开放题是“业务驱动型试卷”的高潮。常见题目是弹幕滚动在人多时卡顿你会如何优化礼物连击数字变化时CPU飙升怎么解决这类题没有标准答案但面试官心里其实有一套判分逻辑看你能不能把模糊的产品体验问题翻译成具体的技术指标并给出有取舍的方案。如果只答“用异步绘制”“用离屏渲染”这种关键词分数不会太高。因为异步绘制不是万能药离屏渲染也不是越少越好关键是你能不能解释清楚为什么这些方案能解决当前场景的瓶颈。这需要你先判断瓶颈在CPU还是GPU、在主线程还是子线程然后再给出对应的方案。6.2 一套可以直接套用的答题框架我把当时经常用的“场景-指标-方案-验证”四层框架分享出来笔试和面试都能用。场景直播间人数多时弹幕批量进入多条弹幕同时渲染。指标用FPS衡量流畅度用内存分配次数衡量卡顿来源用CPU占用率衡量计算量。方案用对象池复用弹幕视图减少频繁创建和销毁。提前计算文案宽高避免布局时重复计算。把透明背景和圆角等效果尽量通过CGContext一次绘制减少离屏渲染。同一时刻只显示有限条弹幕超出部分进入等待队列。动画时间轴放在独立时钟上统一驱动而不是每条弹幕单独创建Timer。验证压测工具模拟高并发弹幕记录FPS和内存抖动线上灰度观察。这个框架对礼物连击也很适用本质都是“高频UI更新怎么降低主线程压力”。礼物连击数字变化时核心在于避免每帧都重建整个UILabel的内容可以复用文本图层或者把数字滚动动画放到图层层面处理减少UIView层面的重复布局。6.3 笔试作答时容易丢分的三个细节没有说明取舍。任何方案都有代价。比如对象池能降低创建开销但会增加内存常驻所以需要控制池大小防止闲置对象太多。只谈优化不谈验证。至少要提到用Instruments的主线程监控、Allocations、Leaks工具来看内存再结合FPS数据判断是否达标。回答太“框架化”。面试官想听到你结合实际场景的思考比如弹幕是否在一个layer上合成、是否考虑过按轨道并行动画、如果同时有10条弹幕进入如何分批处理。开放题最忌讳的是上来就堆名词。比较稳的做法是先说一句“我会先明确当前卡顿是在主线程还是渲染层”然后按排查顺序讲下去。这样哪怕你给出的方案不是最优解至少说明你的思考路径是清晰的。这些内容写下来可能比一张当年的B卷本身还要长。归根结底iOS笔试题的套路变来变去内核就那几块内存、线程、运行时、界面优化、快速定位线上问题。我这些年做面试官也发现能把weak置空原理讲清楚、能把Timer在滑动时失效讲清楚、能对开放题给出“有取舍方案”的候选人不管哪一年都是少数。最后再分享一个小技巧笔试前不要只看题解把这些知识点各写一个小Demo跑一遍得到的结果比背十篇面经都管用。拿RunLoop场景题来说你亲手在模拟器里滑一下列表看到Timer停了又恢复比什么抽象解释都记得牢。这种“亲手验证过的结论”在面试时说出来语气都会不一样。
返回列表