爱奇艺2019秋招iOS笔试B卷:Runtime、内存管理与多线程考点解析 1. 开局这份试卷到底在筛选什么样的人1.1 从题型分布反推考察重心很多人拿到爱奇艺2019秋招iOS方向笔试题B的第一反应是“知识点好散”其实恰恰相反。把整套题摊开看散的是表面聚的是底层。iOS方向笔试的考察范围通常集中在OC语言特性、Runtime运行时、内存管理、多线程、网络与UI渲染这几个大块而B卷在几个关键点上的出题密度明显高于其他同类试卷。先说结论这份卷子不是考“你背了多少API”而是考“你在日常开发中是否真的理解过自己写的每一行代码”。举个典型的例子消息转发、isa指针、Class结构这类Runtime考点在B卷里往往不是单独出一道概念题而是包装在一个“某个方法找不到实现”的代码片段里让你判断调用链会走哪一步、最终会不会崩溃。这种题如果只背过Runtime的流程图没在调试器里实际跟踪过很容易在细节上翻车。另外iOS客户端的岗位特点决定了笔试必须覆盖OC与C的边界。ARC机制、Block捕获变量、内存管理语义这些内容表面上是语言特性本质上是在考察你对系统底层内存分配与释放机制的理解程度。B卷在这块的占比通常不低很多考生在这里失分不是因为不知道答案而是因为答案只写了“是什么”没写清楚“为什么”。1.2 B卷的难度曲线与答题节奏从实际考试反馈来看B卷的难度曲线不是均匀的而是前缓后陡。前面的OC语法和Foundation框架题属于送分题真正的分水岭集中在Runtime、内存循环引用和RunLoop相关题目上。这一段的题目往往题干很短选项或者要求却高度浓缩一道题能牵扯出三四个知识点。我建议的答题策略是先把所有概念题快速过一遍遇到需要大段分析的代码题标记出来最后集中攻克。因为代码题需要完整的上下文思考一旦被一道题卡住后面的时间分配就会全线崩溃。B卷的时间一般按90分钟设计按题型分布来看Runtime和内存管理加起来应该留出至少30分钟多线程和网络各15分钟剩下的时间给基础题和检查。1.3 一份考点全貌对照清单为了方便你自查我把B卷常见的考点和出题形式整理成了一张表。这张表不是我凭空列的而是综合了多次笔试复盘和面试反馈总结出来的高频点集合。考点领域常见出题形式考察深度OC对象本质isa指针、Class结构、对象内存布局要求画出内存分布图、说明类与元类关系Runtime消息机制未实现方法调用、消息转发流程给代码片段判断调用结果、写出转发方法签名KVC/KVO赋值触发条件、手动触发方式判断代码能否触发回调、说明底层实现内存管理MRC/ARC差异、循环引用、weak原理分析Block/Timer/Delegate的引用链Block捕获变量规则、__block修饰符、Block的三种类型判断变量修改结果、分析存储位置多线程GCD死锁、队列任务执行顺序给代码判断输出顺序、找出死锁位置RunLoop线程与RunLoop关系、定时器模式说明EventLoop机制、分析滑动卡顿原因网络HTTP/HTTPS、TCP握手、DNS解析分析请求流程、说明安全机制UI渲染流程、离屏渲染、Auto Layout给出优化方案、描述图层合成过程这张表可以当复习清单用每复习完一个知识点就对照打勾。但你要记住笔试只是第一道槛真正拉开差距的往往是把这些知识点串联起来分析问题的能力。下面几个章节我就挑这块卷子里最容易“看着会、写着错”的几类题目逐一拆透。2. Runtime是重头戏一道消息转发题背后藏了三层考点2.1 先看原型题一个未实现的selector会发生什么B卷里出现了一道非常经典的运行时题目大致是说某个对象调用了一个没有实现的选择子运行时会发生什么最终会不会崩溃。很多考生的第一反应是直接回答“会崩溃抛出unrecognized selector”然后这道题就只拿到了一半分数。实际上这道题考的是三层内容。第一层确认消息发送机制编译器会把[obj doSomething]转换成objc_msgSend(obj, selector(doSomething))然后在对象的类对象中查找方法列表找不到就沿继承链往上找一直找到NSObject都没有就会进入消息转发流程。第二层转发流程有三次机会动态方法解析resolveInstanceMethod:、快速转发forwardingTargetForSelector:、完整转发methodSignatureForSelector:和forwardInvocation:。第三层如果三次机会都错过了系统才抛出doesNotRecognizeSelector:异常。下面这段代码展示了动态方法解析这个阶段可以怎么补救implementation MyClass (BOOL)resolveInstanceMethod:(SEL)sel { if (sel selector(doSomething)) { // 动态添加方法实现 class_addMethod(self, sel, (IMP)dynamicMethodIMP, v:); return YES; } return [super resolveInstanceMethod:sel]; } void dynamicMethodIMP(id self, SEL _cmd) { NSLog(动态添加的方法执行了); } end答这道题的时候你应该把三个阶段的顺序和各自的返回值写清楚然后说明每一阶段的具体触发条件。这样答案就从“会崩溃”升级成了“默认会崩溃但中间有三个可干预的节点”这才是面试官想看到的理解深度。2.2 答题加分点用代码把消息转发全流程串起来如果你能把消息转发全流程写成可运行的代码这道题基本就稳了。完整的完整转发阶段涉及两个方法它们的配合关系是很多人的盲区。// 阶段二快速转发 - (id)forwardingTargetForSelector:(SEL)aSelector { if (aSelector selector(dance)) { return self.backupObject; // 把消息转给备用对象 } return [super forwardingTargetForSelector:aSelector]; } // 阶段三完整转发 - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { if (aSelector selector(sing)) { return [NSMethodSignature signatureWithObjCTypes:v:]; } return [super methodSignatureForSelector:aSelector]; } - (void)forwardInvocation:(NSInvocation *)anInvocation { if (anInvocation.selector selector(sing)) { [anInvocation invokeWithTarget:self.backupObject]; } else { [super forwardInvocation:anInvocation]; } }注意methodSignatureForSelector:和forwardInvocation:必须成对实现而且签名必须一致。签名不一致时方法编号对不上NSInvocation就无法正确解包参数同样会走到异常。从笔试答题的角度我把这三个阶段的关键信息整理成了一个便于记忆的分段动态方法解析对象收到消息后先有机会用class_addMethod动态补上实现。类方法和实例方法要分别实现resolveClassMethod:和resolveInstanceMethod:。快速转发如果动态解析没有处理运行时查找forwardingTargetForSelector:返回一个能处理该选择子的对象消息完整转发过去开销最小。完整转发如果快速转发返回nil运行时需要方法签名和调用封装通过NSInvocation实现参数传递、返回值设置这是最灵活但最重的一条路。2.3 面试官真正想看到的“运行时思维”除了背流程B卷在这类题上特别爱加一个追问什么场景下你会主动使用消息转发这个问题看起来开放其实考察的是你有没有在实际项目中用过Runtime机制。生产环境里消息转发的典型使用场景有三类。第一类是AOP切面逻辑比如统一记录某个类的方法调用耗时可以通过完整转发把消息包一层打点代码。第二类是防崩溃处理对forwardInvocation:做统一兜底把未实现方法丢进日志系统而不是让App直接闪退这在线上SDK里很常见。第三类是Proxy代理模式比如NSProxy配合消息转发实现懒加载、远程调用、数据绑定等能力。如果你在笔试答案里能写出“用NSProxy做远程对象代理调用方只需要持有NSProxy实例方法调用自动转发给远端服务”这道题的区分度立刻就出来了。2.4 容易被追问的method swizzling与类簇问题消息转发聊完之后面试官很容易顺手追问一个著名的Runtime应用method swizzling。别以为笔试不考这个很多B卷的附加题就是给你一段swizzle代码让你指出哪里写得不安全。经典的写法是这样的 (void)load { static dispatch_once_t onceToken; dispatch_once(onceToken, ^{ Class class [self class]; SEL originalSelector selector(viewWillAppear:); SEL swizzledSelector selector(xxx_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); } }); }这里至少有四个考核点。第一为什么在load里做因为load方法在类加载时必然调用一次不会被覆盖或延迟。第二为什么要用dispatch_once防止重复swizzle导致互为交换的死循环。第三为什么先判断class_addMethod因为如果父类没有实现该方法子类通过继承链拿到的是父类的实现直接交换会让子类方法影响父类。第四swizzle后的方法里为什么还要调用原来的实现因为很多场景是“先做自己的事再调用原实现”。答题时能把这四点讲清楚就不只是背代码了而是真正理解了Runtime方法查找的本质。3. 内存管理从来不是背规则循环引用的三种典型考场3.1 考点一Block捕获self什么时候会产生循环引用内存管理是B卷另一个高分值密集区而循环引用是其中最常考的分析题。典型题目是在ViewController里写一个Block属性Block内部用了self问会不会循环引用。这题看似送分但B卷会变着花样考比如加一个dispatch_after加一个__weak修饰或者把捕获变量从self换成self的一个属性。核心分析逻辑永远是谁持有谁。循环引用成立需要两个条件一是两个对象互相强引用二是一个对象通过间接路径最终强引用了自己。ViewController持有BlockBlock又持有self只要Block存在且被self持有这条环就断不掉。实操中最容易踩坑的是你以为加了__weak就安全了但在Block内部如果调用了self.property编译器为了确保弱引用对象在Block执行期间不被释放会在某些条件下把__weak对象再次强引用。所以标准写法是这样的__weak typeof(self) weakSelf self; self.block ^{ __strong typeof(weakSelf) strongSelf weakSelf; if (strongSelf) { [strongSelf doSomething]; } };这里有一个细节值得在笔试里写出来内部再定义一个__strong strongSelf是为了防止Block执行过程中self被提前释放导致后续多次访问self时出现悬垂指针。这种“外弱内强”的写法在SDK开发里非常通用。3.2 考点二NSTimer与Target-Action的隐藏引用NSTimer的循环引用问题在笔试题里常被包装成“一个定时器在ViewController里启动后dealloc一直没有触发为什么”。答案在于NSTimer的timerWithTimeInterval:target:selector:userInfo:repeats:方法中RunLoop会强持有TimerTimer强持有targettarget又是ViewController于是形成了一条“Timer - ViewController”的强引用链。只要Timer没有invalidateViewController的dealloc就不会执行。更隐蔽的是即使用了__weak修饰self再传给Timer也不一定解决问题因为Timer的target参数本身就被Timer强持有weak修饰只是让传参时的临时引用变弱Timer内部依然会按需建立强引用。正确做法是都用Block版API并在Block里访问weakSelf__weak typeof(self) weakSelf self; self.timer [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) { __strong typeof(weakSelf) strongSelf weakSelf; [strongSelf refreshUI]; }];同时在适当的时机比如界面消失、页面销毁必须调用[self.timer invalidate]并把timer置nil这才是真正解除Timer和当前对象关联的操作。在笔试题里你需要在答案中明确区分“invalidate的作用是移除RunLoop中的定时器并释放对target的持有”这一关键点。3.3 考点三delegate为什么用weak代理对象被提前释放怎么办delegate用weak修饰几乎已经成了iOS开发的固定写法但笔试一定会问为什么。标准答案有两层。第一层为了避免代理对象被持有从而出现“持有者持有了代理、代理又引用了持有者”的强引用环。第二层weak的目的是“我不是一定要拥有你你只是过来帮我处理事情你走了我不拦着”。但笔试题会进一步问代理对象被提前释放了怎么办答案是通过respondsToSelector:做安全检查。因为代理被释放后weak属性会自动置nil调用一个nil对象的方法不会崩溃但如果代理对象没被释放但没实现某个方法直接调用就会崩溃。所以正确的调用姿势是这样if (self.delegate [self.delegate respondsToSelector:selector(didFinishTask:)]) { [self.delegate didFinishTask:result]; }这个细节很多人知道但在笔试写答案时容易漏掉。回答“delegate为什么用weak”时主动补充“使用时用respondsToSelector做防护”会让面试官确认你在项目里是真的按规范写的而不是背了结论。3.4 笔试之外的加分项用Instruments验证循环引用如果B卷有线下环节或者面试现场编程能主动提出用Instruments验证循环引用是非常好的加分表现。具体流程是Xcode - Product - Profile选择Leaks模板运行App后进入目标页面再退出观察Leaks面板是否出现红色泄漏对象然后在Xcode顶部选择Memory Graph查看对象的引用链能直观看到是否存在“ViewController - Block - ViewController”的闭环。这个验证手段在笔试里写不出来但在面试谈项目经验时说出来效果远大于“我读过《Objective-C高级编程》”。因为它证明了你有闭环的工程思维而不只是会做题。4. 多线程题目最怕“答套路”从死锁到RunLoop一次性说清4.1 高频题主队列同步任务为什么死锁多线程是B卷的另一个高频失分点而“主队列同步任务死锁”几乎是年年必出。题目往往是在viewDidLoad里写dispatch_sync(dispatch_get_main_queue(), ^{ ... })问会不会死锁为什么。答案不复杂但要讲透。主队列是一个串行队列它已经有一个正在执行的任务就是当前在跑的viewDidLoad。dispatch_sync提交的新任务会被追加到主队列尾部但主队列的任务必须一个一个执行完才能轮到下一个而当前任务又卡在dispatch_sync这行代码上等待新任务完成于是形成了“当前任务等新任务新任务等当前任务结束”的互相等待。如果你在答案里画出这个等待链再补充一句主线程的RunLoop被这个等待卡住导致界面无法刷新、用户交互无响应这就是典型的卡死场景就等于把“背结论”变成了“讲机制”。另外要注意这个问题的变体是换到自定义串行队列里同样的同步串行组合同样死锁原理完全一致。GCD的另一种经典题是判断输出顺序。比如dispatch_queue_t queue dispatch_get_global_queue(0, 0); dispatch_async(queue, ^{ NSLog(1); [self performSelector:selector(printNumber) withObject:nil afterDelay:0]; NSLog(3); });这里performSelector:afterDelay:依赖RunLoop但在全局并发队列的后台线程中RunLoop默认不活跃afterDelay:0的block不会在当前RunLoop中执行所以输出大概率是“1、3”而printNumber可能一直不执行。B卷经常出这类题考的就是你对“GCD异步队列与RunLoop机制之间关系”的理解。4.2 RunLoop与线程的绑定关系是必问的延伸RunLoop是B卷的常客但它很少单独考察往往紧跟在一个GCD题目后面。核心知识点是RunLoop和线程是一一对应的线程启动时并不会自动创建RunLoop只有调用[NSRunLoop currentRunLoop]时才会创建并且存放在全局字典中线程销毁时随之释放。主线程的RunLoop默认启动并运行后台线程默认不启动。所以performSelector:afterDelay:放在主线程有效放在后台线程就可能失效正是因为后台线程没有活跃的RunLoop来执行定时任务。笔试里如果能把“RunLoop的输入源、定时源、Observer观察者、运行模式mode”这几者的协作关系简单描述一遍再指出“滑动ScrollView时RunLoop切换到UITrackingRunLoopMode所以定时器可能暂停”就能覆盖大部分分数点。这个知识点在B卷里往往以“NSTimer在滚动时停止怎么办”的形态出现。4.3 GCD与NSOperation的选型对比题怎么答才显深度B卷里经常会有一道选型题什么时候用GCD什么时候用NSOperation。很多人直接背标准答案GCD轻量、简单NSOperation支持取消、依赖、KVO。但这还不够。我建议从三个层次来答。第一层API层面GCD是C接口基于blockNSOperation是OC对象封装度高。第二层控制能力NSOperation可以设置最大并发数、添加依赖、取消任务配合KVO监听状态GCD的dispatch_group、dispatch_semaphore、dispatch_barrier也能实现类似能力但代码可读性差一些。第三层场景匹配网络请求下载图片、并列执行多个独立任务GCD足够需要控制任务依赖顺序、批量下载暂停恢复NSOperation更合适。笔试里指出“NSOperationQueue底层也是用GCD实现的是基于GCD的更高层封装”这句话非常加分能说明你不是把两者当成对立技术而是理解它们的分层关系。4.4 一道高质量异步题点击事件、自动释放池、主线程执行顺序B卷多线程部分经常会考一道综合性题目用户点击了按钮从触摸事件发生到按钮响应方法执行中间经历了什么。这题表面是RunLoop实则把事件分发、图层渲染、自动释放池全串起来了。大致流程是触摸事件先由系统硬件捕获打包成UIEvent进入当前RunLoop的输入源队列。RunLoop醒来后处理事件通过UIApplication的分发机制找到目标视图再触发touchesBegan:、UIControlEventTouchUpInside等回调。处理完事件后RunLoop进入休眠前会执行一次自动释放池的释放操作同时如果有界面变更需要显示会触发CATransaction的提交最终在渲染周期里完成屏幕更新。笔试里不要写得太啰嗦但要把“事件输入源 - 触摸分发 - 响应链 - 自动释放池释放 - 渲染提交”这几个节点写全。能写出“自动释放池在主线程RunLoop每次循环休眠前统一释放”和“UI更新是在当前事务提交后统一渲染”这两句话这道题基本就锁定了。5. 网络与UI底层iOS基础题里的“显功底”位置5.1 从TCP三次握手到HTTP/HTTPS的取舍B卷网络部分的题不会太深但非常喜欢考“某一层协议的机制”。三次握手基本是必考为什么是三次而不是两次核心在于确认双方收发能力。第一次握手客户端发送SYN服务端确认客户端发送能力正常。第二次握手服务端回SYNACK客户端确认自己发送能力正常、服务端收发能力都正常。第三次握手客户端发送ACK服务端确认客户端能收到自己发出的数据。如果没有第三次握手服务端无法确认客户端的接收能力是否正常也就可能创建一个无用的连接资源。HTTP与HTTPS的区别笔试答题时除了说“HTTPS多了一层SSL/TLS加密”建议补充两个点。第一HTTPS建立连接时先做TCP握手再做TLS握手TLS握手过程中通过证书校验服务器身份、协商对称密钥。第二证书校验依赖CA体系客户端内置了根证书库所以私搭证书的HTTPS接口在真机上默认报错这也是“App无法访问测试环境”的常见原因。如果能提到“对称加密负责业务数据、非对称加密负责密钥协商”这一题就不会丢分。5.2 HTTPS证书校验与中间人攻击一道填空题就能区分水平B卷大概率会把HTTPS的安全性考成一个简答题如何防止中间人攻击以及ATS是什么。中间人攻击的原理是攻击者插在客户端和服务器之间分别与双方建立连接转发数据时解密并篡改。HTTPS能防范中间人核心机制是“服务端证书不可伪造”客户端安装时或每次连接时校验证书链和域名。这里有个容易漏掉的细节证书只保证“这个服务器确实是它声称的服务器”不保证“这个服务器的业务逻辑没有被攻击者利用”。所以答题时讲清楚“证书链校验 公钥校验 域名校验”三个层次比单纯说“加密了就安全”更严谨。开发层面的补充是现在的iOS项目普遍使用NSURLSession的URLSession:didReceiveChallenge:completionHandler:回调做二次校验可以对比服务器证书的指纹与本地备份的指纹是否一致防止私搭CA签发证书被中间人利用。这在面试中属于加分细节。5.3 UI层面离屏渲染、卡顿优化与Auto LayoutUI部分的题B卷很少考死记硬背的布局API而是喜欢考渲染性能优化。离屏渲染是重中之重。面试官如果问“圆角阴影为什么会造成卡顿”你需要答出cornerRadius配合masksToBounds或阴影属性会导致图层无法在普通绘制流程中直接渲染需要先创建一份离屏的上下文绘制完毕后再合成回屏幕。这个过程开销很高尤其在列表滚动时大量cell触发离屏渲染就会引发掉帧。解决方案可以写成几点使用UIBezierPath绘制圆角并配合Core Graphics生成圆角图片将阴影设置到独立容器视图上优先用shadowPath指定阴影形状从设计层面避免大量透明图层叠加。答题时能顺手写出“使用Instruments的Core Animation工具可以查看离屏渲染的黄色标记”会是加分点。Auto Layout的题则要抓住“约束系统的求解过程”。约束本质上是一组线性方程系统通过Cassowary算法求解每个视图的frame。笔试常见问题是“约束写多了会不会崩溃”答案是不会多约束是冗余而非冲突只有约束互相矛盾时系统才会在运行时打印约束冲突日志并按优先级断掉某些约束。能答出“优先级从Required逐渐往下拆解直到找到可行解”的机制说明你真研究过Auto Layout的原理。5.4 真题演练怎么描述iOS的渲染流程这道题在B卷里可能以“iOS为什么卡顿”的形态出现。答题要抓住流程Core Animation提交事务 - Render Server解析图层树 - 生成绘制指令 - GPU渲染 - 显示到屏幕。卡顿的本质是掉帧也就是一帧在16.7毫秒内没有完成上述流程。延伸的考点是哪些操作会阻塞主线程导致掉帧。大图解码、复杂视图层级布局、同步网络数据解析、大量cell离屏渲染都是典型元凶。对每个点能给出一句对应的优化方案预解码和缓存图片、简化布局分支、异步解析数据、避免圆角阴影叠加。笔试时能用“CPU和GPU负载不均衡导致整条流水线变慢”这个角度切入比零散给优化技巧更显功力。6. 笔试之外这些高频考点如何反哺日常开发6.1 复盘错题时先做“考点迁移”很多人在笔试结束后只对答案不看考点结构这是最大的浪费。B卷里的每一类题都能迁移到开发中消息转发可以用来做动态埋点循环引用分析可以迁移到日常Code ReviewRunLoop机制能帮你定位“定时器在页面滑动时不触发”的线上问题。我个人的习惯是把错题按“考点 - 表象 - 根因 - 工程影响”四栏重新整理一遍效果远好于反复刷同一套题。6.2 把面试题库变成代码评审清单笔试刷完后你会发现很多题目其实就是代码评审中常见问题的抽象。比如Block循环引用是不是对应着“某个页面退出后dealloc不执行”GCD死锁是不是对应着“某处代码在刷新主UI时卡死”HTTPS证书校验是不是对应着“线上接口被第三方篡改”如果能把题库整理成一个评审清单每写一个ViewController或一个网络层工具都对着清单自查一遍面试能力会稳许多。6.3 最后说点实在的2019真题对今天仍然有效的复习策略爱奇艺2019秋招这套题虽然年份较早但考点并没有过时。iOS底层的东西变化其实不大Runtime、RunLoop、内存管理这些基础机制至今仍在。复习时我建议优先掌握这套卷子里出现过的“为什么类”问题而不是背诵大量API细节。API会随版本演进但机制是稳定的。另外提醒一句笔试只是筛选的第一环很多公司会在面试中让你现场分析一道类似的题重点是看你的思维路径。你要主动说出“我先看谁的引用关系”“我先确认队列是串行还是并行”“我先看方法是否真的在类里实现”这种分析过程的展示比直接给结论更能说明你的工程能力。按这个思路复盘B卷不仅仅是为了过笔试更是为后续的每一轮技术面打底子。