爱奇艺iOS校招笔试题复盘:内存管理、Runtime与多线程核心考点 这几天整理旧笔记又把爱奇艺2020校招iOS方向笔试题第一场翻出来做了一遍。说实话这种老试卷比现在很多平台上流传的“高频面试题”更有参考价值。它没有堆偏题怪题而是老老实实地考了一个iOS开发者在日常工程里必须过关的基本功内存管理、Runtime、多线程、UIKit事件传递、网络协议。一句话总结如果你能把这套题吃透基本等于把iOS开发的地基重新夯实了一遍。对于正在准备iOS面试题、或者需要给团队新人做技术摸底的同学它是一份很适合当成自测清单的资料。所以这篇文章我不打算只贴几个“标准答案”。我想顺着这套卷子的出题逻辑把每一类考点背后的原理、常见答法、以及我这些年带人和被面试时踩过的坑一起拆开讲这样你回头再去看任何一套iOS笔试题都会从容不少。1. 整张卷子的命题逻辑一份笔试到底在筛什么1.1 从考察模块看爱奇艺的出题偏好先解决一个根本问题命题人到底想筛出什么样的人很多人拿到试卷第一反应是刷题但那样效率不高。我自己复盘这套题时的感觉是爱奇艺在iOS校招上的定位是“基础扎实、能快速落地”不是找考据型选手。所以整张试卷反复出现的是从具体使用场景出发考察原理的题而不是冷门API背诵大赛。考察模块最常见的出题角度想看到的能力内存管理strong/weak/copy的区别block循环引用是否踩过真机内存问题并理解底层机制Runtimeisa指针、消息转发、method swizzling对Objective-C动态特性的掌握程度多线程GCD队列、死锁、线程安全同步处理界面卡顿和并发崩溃的经验UIKit响应链、hitTest、UIStackView、Auto Layout对事件分发和布局体系的理解网络TCP/UDP、HTTP状态码、HTTPS握手移动端网络优化的基本功这个表基本就是我复盘时整理的“考点地图”。你发现没有这里面几乎没有纯记忆性的东西。比如问到copy和strong的区别好我可以答“copy会拷贝一份”但这只是第一步。真正能拉开差距的是第二个问题“NSMutableString赋值给NSString的copy属性会怎样strong属性呢”如果只会背概念到这里就卡住了。而这类追问在笔试题里经常以选择题形式出现选项里会放几个看似对但缺少前提的表述。1.2 难度阶梯与作答策略大部分校招笔试卷子的结构是选择题在前、问答题居中、方案设计题压轴。选择题大概占三到四成属于送分题主要筛掉基础概念不清晰的人中间的问答题重点看逻辑表达最后的方案设计题是加分项比如“如果让你优化一个图片列表的流畅度你会怎么做”。这类题目没有标准答案考察的是你平时代码里有没有思考过性能问题。我的作答策略一直很简单先把会做的做完把分数拿到手再回头啃模糊题。千万别在一道选择题上卡五分钟笔试的时间通常比看起来更紧张尤其是后面的问答题需要手写大量文字非常费时间。另外遇到不会的题尽量把能想到的相关知识点写上去哪怕是推导过程也有分不要留空白。1.3 为什么2020年的题现在依然值得看有同学会问iOS每年都有新版本、新API这套2020年的题会不会过时了我认为不会。因为底层机制是很稳定的东西对象内存管理从MRR到ARC核心思想没变Runtime的objc_msgSend流程几乎没有大改RunLoop的机制在苹果文档里躺了十几年响应链的hitTest逻辑至今适用。新出的SwiftUI、Swift并发框架确实改变了部分写法但面试官依然默认你懂这些“老地基”。尤其是做iOS维护和性能优化离开这些底层知识寸步难行。所以这套题不是过时而是“压舱石”。2. 内存管理与对象生命周期最容易被扣分的部分2.1 属性修饰符strong、weak、copy、assign怎么选卷子里一定有关于属性修饰符的选择题。我先说一个最容易翻车的点很多人把copy理解为“任何情况下都复制一份”这不对。比如字符串对象如果原对象本身是不可变的发送copy消息往往只是把引用计数加一返回的还是同一个对象因为没必要复制。如果原对象是可变的NSMutableStringcopy才会生成一个不可变副本。这就是为什么NSString属性几乎都用copy防止外部用一个NSMutableString不断修改导致属性值跟着变出现难以排查的“幽灵数据”。给你看一段我常拿来当题眼的代码property (nonatomic, copy) NSString *title; - (void)setup { NSMutableString *temp [NSMutableString stringWithFormat:hello]; self.title temp; [temp appendString: world]; NSLog(%, self.title); }如果title声明成strong打印结果是“hello world”如果声明成copy打印结果是“hello”。原因是strong只是持有同一个对象外部修改原对象会直接影响属性copy则是赋值瞬间生成了一个不可变副本和原对象再无关系。这就是为什么要用copy不是教条而是真正避免共享可变状态。同理NSArray、NSDictionary这类容器属性如果不想让外部通过NSMutableArray修改内容也要用copy。weak和assign的区别也是常客。weak修饰对象对象被释放后自动置nil不会产生野指针assign通常用于基础类型如果强行用assign修饰对象对象释放后指针依然指向旧地址下次访问就会出现EXC_BAD_ACCESS。面试里如果出“weak和assign能不能互换使用”这种题直接回答“不能assign不会参与对象生命周期管理释放后指针不置空有崩溃风险”就清晰了。2.2 block循环引用的标准答法block循环引用是问答题的“必考题”没有悬念。考法通常是给你一段代码问会不会泄漏为什么怎么改。标准答法分三步先找出引用环再说明是谁持有谁最后给出方案。最常见的情况是self持有blockblock内部又用了self。比如self.timer [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer *timer) { [self doSomething]; }];这里self持有timertimer持有blockblock捕获了self形成self - timer - block - self的环。在NSTimer repeats为YES的情况下这个环会让self无法释放。解决办法是在block外用weakSelf或者在不需要定时器时把timer invalidate掉打断引用环。但这里还有一个进阶考点为什么有时候在block里把weakSelf转回strongSelf因为weakSelf可能在block执行过程中被释放如果block里有多行代码第一次访问weakSelf不为空第二次访问可能已经为空导致后续逻辑错乱。所以常见写法是__weak typeof(self) weakSelf self; [self someAsyncWorkWithCompletion:^{ __strong typeof(weakSelf) strongSelf weakSelf; if (!strongSelf) return; // 后续所有操作都用strongSelf }];这样既避免循环引用又能保证block一旦开始执行self在本次执行期间不会消失。这个细节很多人不知道写上去会明显加分。2.3 autorelease pool与对象生命周期除了ARCautorelease pool也是经常被拿出来问的点。很多人以为ARC时代已经没有autorelease了其实是有的。ARC只是在编译期帮你插入了retain/release/autorelease调用底层机制没变。主线程的RunLoop在每个循环结束时会自动pop自动释放池所以日常开发里我们很少手动管。真正需要手动写autoreleasepool的场景是在循环里大量创建临时对象的时候。举例来说从一个很大的数组里读取数据并生成临时对象如果不用自动释放池这些临时对象会一直堆积到当前RunLoop循环结束才释放峰值内存会很高。包一层autoreleasepool后每次循环结束就释放了一批for (NSData *data in dataArray) { autoreleasepool { UIImage *image [UIImage imageWithData:data]; // 处理image } }这个考点在笔试里多以选择题出现问“以下哪段代码能降低峰值内存”识别出循环内大量创建对象并包自动释放池基本就是正确项。2.4 常见内存陷阱这里分享几个我在实际项目中踩过的坑也是试卷里容易埋的陷阱。第一代理属性要用weak而不是assign。assign修饰代理对象对象释放后delegate变成野指针回调时崩溃用weak则自动变成nil。第二block中用到了成员变量即使不写self也会强引用self。因为访问成员变量本质是访问self-ivar。这个隐蔽点经常被当成“我没用self就不会循环引用”的误区。第三NSTimer单独说说它不只是循环引用问题在UIScrollView滚动时还会被RunLoop“卡住”我更喜欢用dispatch_source_t或者CADisplayLink来做高频刷新避开Timer在tracking模式下的调度延迟。3. Runtime与消息机制考的是Objective-C的“为什么”3.1 类、元类与isa的三角关系Runtime是iOS笔试里区分度最高的一块。选择题最常见的是问“对象的isa指针指向谁”。标准答案实例对象的isa指向类对象类对象的isa指向元类对象元类对象的isa指向根元类对象根元类对象的isa指向本身。这么一串很多人背不下来我建议用一种工程化的理解方式类对象存储实例方法列表、属性、协议元类对象存储类方法列表。当你调用一个类方法时编译器会去元类里找方法。为什么要知道这个因为KVO的实现就依赖这个体系的扩展。系统在运行时创建了一个子类把原对象的isa指针“偷换”成子类重写被观察属性的setter从而在setter里发出通知。你如果了解isa和类对象的关系KVO原理就迎刃而解。在笔试题里凡是遇到“KVO的底层实现”“对象怎么找到方法实现”这类题都可以从isa和类对象结构出发作答。3.2 消息发送与消息转发的完整流程OC方法调用不是直接找函数指针而是编译成objc_msgSend。完整流程可以分为两段。第一段是消息发送从当前类的method list找selector找不到就去父类找一路找到NSObject再到根类找到就跳转IMP并缓存到cache。第二段是消息转发如果整个继承链都找不到实现系统不会直接崩溃而是给你三次补救机会。我先说完整流程动态方法解析调用resolveInstanceMethod:或resolveClassMethod:允许你用class_addMethod动态添加实现。快速转发调用forwardingTargetForSelector:允许你把消息转发给另一个对象。完整转发调用methodSignatureForSelector:返回方法签名再走forwardInvocation:把消息封装成NSInvocation交给别人处理。这个流程在笔试题里最好的答法是把这六步按顺序写出来每一步写清楚触发条件。我当年带新人时发现能一口气说清这三个阶段的人非常少大多数人只知道“找不到方法会崩溃”。其实系统给了这么多机会这恰恰体现了OC动态语言的灵活性。举个例子你们做防崩溃SDK时经常用消息转发来拦截未实现方法的调用避免线上崩溃。我贴一段动态方法解析的标准代码笔试如果写伪代码也可以照这个思路void dynamicMethodIMP(id self, SEL _cmd, id value) { NSLog(动态添加的方法被调用); } (BOOL)resolveInstanceMethod:(SEL)sel { if (sel selector(dynamicMethod:)) { return class_addMethod([self class], sel, (IMP)dynamicMethodIMP, v:); } return [super resolveInstanceMethod:sel]; }答题时提到“v:”是方法编码也能体现你有方法签名和类型编码的意识这是加分项。3.3 method swizzling的正确打开方式Runtime的工程应用里出镜率最高的就是method swizzling。笔试题通常问“怎么安全地交换方法实现”。记住一个版本就够了在load里做用dispatch_once保证只执行一次原则是永远调用原始实现。为什么用load而不是initialize因为load是类加载进runtime时调用的时机确定且在main函数之前而initialize是惰性的第一次收到消息时才触发如果一个类一直没被使用initialize就不会执行。还有initialize存在线程安全问题多个线程同时发送第一条消息时可能重复调用。所以swizzling的标准姿势是 (void)load { static dispatch_once_t onceToken; dispatch_once(onceToken, ^{ 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); method_exchangeImplementations(originalMethod, swizzledMethod); }); } - (void)swizzled_viewWillAppear:(BOOL)animated { [self swizzled_viewWillAppear:animated]; // 实际调用原实现 // 统计、埋点等额外逻辑 }这里有个隐藏坑交换后调用swizzled_viewWillAppear:其实是调用原始实现。这行代码不是递归而是通过交换后的IMP跳转。我见过很多人考试时没想明白以为会死循环。笔试如果让你判断“这行代码会不会递归”答案是“不会”因为方法名的IMP已经变了。另一个坑是如果子类没有实现viewWillAppear:class_getInstanceMethod拿到的可能是父类的方法直接exchange会影响父类。所以更安全的做法是先用class_addMethod判断子类是否需要添加实现再决定是否exchange。这个细节说得出来面试官会立刻觉得你有实战经验。4. 多线程与RunLoop答好并发题的关键4.1 队列模型与死锁的成因多线程这一块GCD是主力考点而且重点不是API而是队列模型。要分清楚三种队列主队列是串行队列全局并发队列是并发队列自己创建的queue默认也是串行。任务派发用dispatch_sync还是dispatch_async会带来完全不同的执行效果。死锁的经典题长这样- (void)viewDidLoad { dispatch_sync(dispatch_get_main_queue(), ^{ NSLog(我还能执行吗); }); }答案是“不会执行”并且程序直接死锁。原因是dispatch_sync会阻塞当前线程等待block在主队列执行但主队列本身就在串行执行viewDidLoad它不可能转过来执行这个新block于是互相等待谁也没法往前走。在实际工程里这个问题的常见变体是“在回调里不小心回到主队列又用dispatch_sync”尤其是网络回调可能在子线程时大家容易无脑加主队列同步。我的经验是凡是要回到主线程更新UI统一用dispatch_async除非你能明确当前不在主队列否则不要用sync。笔试还有一种考法向串行队列里异步嵌套同步任务问会不会死锁。比如在自定义串行队列的block里再dispatch_sync同一个串行队列也会死锁因为队列串行只有一个线程在执行它又被自己前面派发的任务卡住了。理解了“同步任务会阻塞当前线程直到对应队列执行完这个block”这条核心原则几乎可以推演出所有死锁场景。4.2 线程安全同步机制的选择问到线程安全时很多人第一反应是“加锁”。但锁有很多种各自适用的场景不一样。笔试一般是给一个场景让你选合适方案或者让你比较锁的优缺点。我把常用方案整理成了一个对照表方案特点适用场景synchronized自动加解锁支持递归性能一般简单临界区保护代码可读性好NSLock互斥锁需要手动lock/unlock中小临界区dispatch_semaphore信号量可以控制并发线程数限流、资源池、并发下载控制os_unfair_lock底层锁性能高不可递归高频小临界区需要底层优化NSRecursiveLock支持递归加锁递归函数内部需要保护一个容易被考到的思路是锁不是保护整个方法而是保护“共享可变资源”。如果只是局部变量每个线程有独立栈空间根本不需要加锁。加了锁反而降低性能。另外属性atomic并不是万能的它只保证“读取和写入这个属性本身”是原子的不保证属性背后的业务逻辑是原子的。比如一个可变数组就算数组属性是atomic两个线程同时往数组里addObject依然会崩溃。因为addObject和读取数组内容是独立操作atomic管不到这几个操作组成的事务。正确做法是使用串行队列或者加锁来保护数组的整段操作。4.3 RunLoop与界面卡顿RunLoop在试卷里和内存管理一样属于“必考不衰”的知识点。选择题常问RunLoop的mode有哪些或者“NSTimer在滚动时为什么不触发”。原因是scrollView滚动时主线程RunLoop切换到了UITrackingRunLoopMode默认的timer不是添加到NSRunLoopCommonModes所以timer不被调度。把timer加入到NSRunLoopCommonModes或者在滚动模式也注册timersource就能解决。问答题里更常见的是“如何监控主线程卡顿”。这也是RunLoop的经典应用用CFRunLoopObserver观察主线程RunLoop的状态。卡顿通常表现为在kCFRunLoopBeforeSources和kCFRunLoopAfterWaiting之间耗时长或者在kCFRunLoopBeforeWaiting之前一直忙。子线程每隔一段时间检查主线程是否还停在某个状态超过阈值如果超过就判定卡顿并记录调用栈。很多APM SDK的基本原理就是这个只不过做了更细致的符号化和堆栈聚类。这套思路写在简历项目里很加分笔试时能讲出来也很能体现技术深度。5. UIKit事件传递与网络基础工程题的最后两道关5.1 hitTest与响应链UIKit里必考的事件分发问题核心就是两个方法hitTest:withEvent:和pointInside:withEvent:。触摸发生后系统会从UIWindow开始向子视图递归调用hitTest找到最深的一个能够响应该触摸的视图。默认流程是先判断self.userInteractionEnabled、hidden、alpha是否满足条件不满足直接返回nil。调用pointInside:withEvent:判断触摸点是否在当前视图范围内。倒序遍历subviews对每个子视图递归调用hitTest。如果所有子视图都返回nil则返回self否则返回子视图返回的那个视图。笔试题喜欢考“为什么后添加的子视图会优先响应”。因为倒序遍历数组里最末尾的subview最先被遍历也就是视觉上最上层、最后添加的视图优先响应。自定义hitTest还可以实现“把超出父视图范围的按钮也能点击”这种效果。比如一个全屏透明父容器只希望子按钮能点击需要在父容器重写pointInside返回NO让触摸事件穿透到下层。回答完hitTest紧接着就是响应链。如果hitTest返回nil事件会沿着响应链向上传递当前视图 - 父视图 - 控制器view - 视图控制器 - UIWindow - UIApplication - UIApplicationDelegate。如果都不处理事件被丢弃。这里有个隐藏加分点手势识别器与touch事件的关系。手势识别器会优先于视图的touch事件进行判定当手势被识别后默认会取消发送给视图的触摸序列。所以一个UIButton同时挂一个单击手势手势识别成功后按钮可能收不到响应的touchUpInside。回答这个知识点证明你不只是背了hitTest还知道手势系统对事件链的干扰。5.2 UIStackView与布局题的补充考点爱奇艺这套题的年份是2020那会儿UIStackView已经普及但很多人还是习惯用frame写死布局。所以试卷里出现和布局相关的题并不只是考UIStackView的API而是考“如何做一套可适配不同屏幕尺寸的界面”。这其实是一个思路题答案要包含三层基于约束的Auto Layout、UIStackView对层级的管理、以及动态高度的自适应。UIStackView的核心价值是管理子视图的排列关系它不需要设置每一条约束只需配置axis、spacing、distribution、alignment。很多人一开始不习惯但用熟了之后会发现stack view对“增删子视图后自动重新布局”支持得很好。笔试里如果让手写一个用Auto Layout实现顶部水平排列若干控件的效果用UIStackView会简洁很多而且不容易在约束冲突上出错。我再补一个容易被问到的动态高度题UITableViewCell高度自适应。从iOS 8之后就只需要设置estimatedRowHeight和rowHeight为UITableViewAutomaticDimension然后在cell内部用Auto Layout把上下约束固定完整让内部内容把cell撑起来。这里的关键是约束要形成从上到下的完整链条某个label的垂直约束如果缺失高度计算就会失败。5.3 网络与HTTPS的答题框架网络题在校招笔试里占比不低而且问法很固定。第一类是TCP和UDP的区别第二类是HTTP状态码第三类是HTTPS的握手过程。我直接用答题框架说明你照着写基本不会丢分。TCP是面向连接的可靠传输需要三次握手建立连接保证数据顺序和完整性UDP是无连接、不可靠、但延迟低适合音视频通话、实时游戏这类能容忍少量丢包的场景。三次握手的过程要能说出来客户端发SYN服务端回SYNACK客户端再回ACK。为什么不是两次因为两次握手无法确认客户端的接收能力也无法防止历史连接请求的乱序问题导致服务端建立无效连接。HTTP类题目至少要把常见的状态码分清楚200表示成功301是永久重定向302临时重定向304走缓存400客户端请求错误401未认证403无权限404资源不存在500服务器内部错误502网关错误503服务暂时不可用。这些状态码在移动端联调时经常遇到答不上来基本说明实战经验不够。HTTPS的答题框架可以这样组织HTTPS HTTP TLS。HTTP报文是明文TLS负责加密和证书校验。握手过程大致是客户端发送支持的加密套件和随机数服务端返回证书和随机数客户端验证证书合法性然后用服务端公钥加密一个预主密钥发给服务端双方通过预主密钥生成对称会话密钥之后通信都用对称加密。这里有个易错点为什么不用非对称加密加密全部数据因为非对称加密性能差不适合长报文。所以只用非对称加密交换密钥再用对称加密传输数据。移动端还会考证书校验客户端要检查证书签发机构是否可信、域名是否匹配、证书是否过期。这一套下来网络部分拿高分不难。6. 带着这套题去做一轮系统复盘6.1 分类整理错题形成自己的知识索引我不太建议反复整套刷题边际收益很低。更有效的方式是把错题按主题归类然后为每个主题建一个“知识索引”。比如内存管理类错题下面挂三个子项属性修饰符、循环引用、自动释放池。每次看到相关考题就强制自己把子项里的点都过一遍。这个习惯看起来简单但能逼迫你把零散知识点串成体系。举个实际例子。如果你在“copy与strong”上翻车不要只看一个答案就结束了。要接着问自己三个问题NSMutableString copy之后是什么对象NSArray的copy和mutableCopy差别在哪自定义对象要实现哪些方法才能支持copy每追一个问题你的知识树就长出一截。这套题里面很多点都是可以用“再问三层为什么”来扩展的因为出题人本来就是在考察你有没有往深处想的能力。6.2 动手实验验证原理校招笔试很少要求你写复杂工程但很多原理光靠背是记不牢的。我自己带新人的时候要求他们把卷子里没把握的题改成小demo跑一遍。比如想知道copy属性到底复不复用原对象写两行代码断点打印地址一目了然。想验证RunLoop模式对timer的影响做一个带UIScrollView的页面放一个timer滚动起来观察log。想理解消息转发用一个不存在的selector触发崩溃看系统打印的转发调用栈。这种动手验证比任何面经都有效。因为笔试的题目往往是从真实修改中提炼出来的你如果能亲手复现就已经领先很多人。而且真机调试时看到的内存增长曲线、线程调用栈、RunLoop状态日志这些都是面试时可以随口说出的“真实经验”比干巴巴背原理可信得多。6.3 回答面试题时的三个常见误区最后我总结几个我在面试候选人时反复遇到的答题误区希望你别踩。第一个误区是只给结论不给过程。比如问“weak为什么能自动置nil”有人只回答“因为weak不持有对象”这不够。更好的答法是系统维护了一个全局的weak表对象释放时根据地址找到所有指向它的weak指针把它们置为nil。这个“表”的概念才是关键。第二个误区是混淆相似概念但说不清边界。比如Objective-C的“消息转发”和“方法查找”很多人混着说。其实查找是沿着继承链找IMP转发是查找失败后的补救机制两者阶段不同。答题时先界定范围再说细节可信度会高很多。第三个误区是忽略前提条件。比如“NSOperationQueue和GCD哪个好”很多人直接说NSOperationQueue支持取消、依赖所以更好。但在笔试场景里问题往往有隐藏前提如果只是简单异步执行GCD更轻量如果需要复杂任务编排NSOperationQueue更合适。没有前提的比较答案很难站住脚。遇到这类题先补充“在什么场景下”再给出结论分数会稳得多。我自己的习惯是每年校招季都把这套题拿出来给团队新人做一次摸底做完再面对面过一遍错题。几次下来效果比直接让他们看文档好很多。如果你也在准备面试或者负责带人不妨把这份考卷从头到尾过一遍把每一个“知道”变成“能讲清、能实现、能排查”那通过笔试只是顺手的事。