爱奇艺iOS笔试题深度解析:从Runtime到内存管理的工程实战 📅 发布时间:2026/8/31 5:00:57 👁 浏览次数: 爱奇艺2019秋招iOS方向笔试题B这个话题我在收藏夹里存了四年多。当年这一套卷子让不少人栽了跟头现在回头再看反而觉得它比市面上一堆“iOS八股文题库”更有参考价值。iOS方向的笔试题通常不追求硬核算法而是看你对工程细节敏不敏感。这套B卷基本覆盖了Objective-C运行时、内存管理、多线程、网络、UI布局、音视频相关的基础能力适合正在准备秋招的应届生、想跳槽的初中级iOS开发以及想出题但不知道从哪下手的团队负责人参考。爱奇艺的业务是长视频移动端要处理播放器、缓存、清晰度切换、App启动速度、弹幕互动等场景这些都会直接进到考题里。换句话说这套卷子不是在考你会不会写for循环而是在看你有没有能力在一个大规模客户端里扛住线上问题。1. 这套笔试题到底在考什么从爱奇艺的业务场景反推考点1.1 技术栈判定为什么是OC而不是Swift全家桶2019年爱奇艺客户端主体还是Objective-CSwift在部分新模块逐步接入所以B卷的考察重点是OC。这和很多同学考前猜的“Swift会占很大比例”不一样。原因是视频类App的底层播放、解码基本是C/C上层UI还是OC大量历史代码不可能推倒重写。Swift当时语法变动快校招考察Swift很容易变成背语法测不出真实工程能力。所以卷子里出现属性关键字、weak原理、分类、关联对象、KVO、Runtime都是围绕OC核心机制在出题重点看你对编译期和运行期的理解。如果你只知道ARC是自动引用计数答不出weak表是怎么维护的这道题基本就拿不到分。1.2 考点分布与出题逻辑从当年考生回忆和网上流传的版本看B卷大致可以分成下面几个模块模块常见考察点背后能力语言基础property关键字、copy/strong、深浅拷贝、Category、Extension基本功是否扎实内存管理ARC、循环引用、weak/strong、autorelease内存问题定位能力多线程GCD、NSOperation、锁、线程安全并发稳定性意识Runtimeisa、objc_msgSend、方法交换、动态方法解析APM、防崩溃能力RunloopMode、Source、卡顿监控性能优化功底UI布局Auto Layout、UIStackView、Safe Area、iOS分屏适配与还原能力网络HTTP缓存、DNS、断点续传、抓包网络问题定位音视频播放器架构、首屏秒开、硬解软解业务匹配度出题逻辑很清晰先确认基础是否扎实再看有没有工程思维。选择题、填空题考记忆简答题考理解编程题考动手设计题考架构。B卷相对A卷加重了Runtime和GCD手写题的分量Swift语法细碎题反而少。如果你只背了几个常用API就去考很容易在“为什么”这里卡住。1.3 B卷和A卷的差异与应对思路网上流传的版本里A卷偏向OC语法细节和简单数据结构B卷更偏向运行时与架构。A卷可能问“Category能不能添加属性”B卷会直接让你手写消息转发的流程。但核心都一样要你具备分析“崩溃发生在哪”“内存为什么会涨”“启动怎么优化”的能力。应对思路很简单不要背题把知识点串成体系。比如内存管理要能从ARC概念串到循环引用、weak表、dealloc时序、NSTimer、block再串到MLeaksFinder这类工具原理。这样不管题目怎么变形底层逻辑都是通的。2. 核心考题逐题拆解拿到题先别急着背答案2.1 内存管理ARC下循环引用怎么破这几乎是iOS笔试的必考题。常见问法有几种解释block循环引用__weak typeof(self) weakSelf self;是否一定安全什么时候要用weakify和strongifyNSTimer为什么会造成循环引用怎么解决。很多同学能答出“用weak”但答不出为什么。先说block。如果你在block里直接用了self而self又持有这个block就会形成self - block - self的闭环。ARC下编译器不一定能自动判断需要手动打破。常规写法是__weak typeof(self) weakSelf self; self.block ^{ __strong typeof(weakSelf) strongSelf weakSelf; if (strongSelf) { [strongSelf doSomething]; } };这里有个关键点weakSelf是弱引用block执行过程中self可能已经被释放所以如果block内部需要保证self存活必须转一次strongSelf。但转完之后也要判空因为weakSelf读到nilstrongSelf就是nil。Swift里面对应的是捕获列表[weak self]然后guard let strongSelf self else { return }。这道题能答到这一层说明你真正处理过block回调的边界问题。再看NSTimer。Timer的target对self是强引用如果self持有timer就构成timer - self - timer的循环。常见方案是用中间对象做weak代理或者用iOS 10之后的block版本Timer把target对象变成block。写block版本时要注意block内部仍然要weak self否则循环引用只是换了个位置而已。爱奇艺这种视频App里定时器用得非常频繁弹幕、进度条、轮播都有如果对生命周期理解不到位很容易出现控制器释放不了、内存持续上涨的问题。2.2 多线程GCD与锁多读单写怎么设计多线程题里出现频率最高的一个是“什么时候用GCD、什么时候用NSOperation”另一个是“怎么设计一个多读单写的缓存”。先说前者。NSOperationQueue是GCD的上层封装适合需要取消、依赖、最大并发数控制的场景GCD更轻量适合发一次性任务和信号量控制。笔试时能说出“GCD底层是C接口NSOperation是OC对象”这个点会很加分。多读单写是爱奇艺这类App很常见的需求比如视频缓存列表、播放器配置表读多写少。正确姿势是自定义并发队列加barrierproperty (nonatomic, strong) NSMutableDictionary *data; property (nonatomic, strong) dispatch_queue_t rwQueue; - (instancetype)init { self [super init]; if (self) { _data [NSMutableDictionary dictionary]; _rwQueue dispatch_queue_create(com.example.rw, DISPATCH_QUEUE_CONCURRENT); } return self; } - (id)objectForKey:(NSString *)key { __block id obj nil; dispatch_sync(self.rwQueue, ^{ obj self.data[key]; }); return obj; } - (void)setObject:(id)obj forKey:(NSString *)key { dispatch_barrier_async(self.rwQueue, ^{ self.data[key] obj; }); }这里有个特别容易踩的坑不能用dispatch_get_global_queue因为barrier在全局并发队列上是“不守规矩”的它不保证独占执行。必须自己创建的并发队列barrier block才会等前面所有block执行完再单独执行然后放行后面的block。如果写操作需要保证完成后立刻读就要用dispatch_barrier_sync而不是async。另外读操作虽然安全但会阻塞调用线程所以在高频率读场景下要评估是否接受或者考虑用其他数据结构代替字典。锁的题目也经常一起出。synchronized简单但性能最差NSLock是互斥锁atomic修饰属性并不能保证业务逻辑线程安全只能保证getter/setter的原子性OSSpinLock因为优先级反转问题已经被废弃iOS 10之后用os_unfair_lock。笔试时能把锁的演进和适用场景讲清楚比只会说“我用synchronized”要好很多。2.3 Runtime消息转发与方法交换Runtime在B卷里属于拉开分差的题目。基础问法是objc_msgSend的流程是什么resolveInstanceMethod、forwardingTargetForSelector、methodSignatureForSelector、forwardInvocation各有什么用高端问法是让你手写一个方法交换并指出要注意什么。消息发送的流程简单说就是先查缓存再查类对象的方法列表找不到就往父类找还找不到就进入动态方法解析。动态方法解析阶段可以调用resolveInstanceMethod动态添加方法比如dynamic属性的实现。之后是快速转发通过forwardingTargetForSelector把消息转发给别的对象。最后是完整转发需要methodSignatureForSelector返回方法签名再调用forwardInvocation处理。方法交换的代码一般是 (void)load { static dispatch_once_t onceToken; dispatch_once(onceToken, ^{ Class cls [self class]; SEL originalSelector selector(viewWillAppear:); SEL swizzledSelector selector(xxx_viewWillAppear:); Method originalMethod class_getInstanceMethod(cls, originalSelector); Method swizzledMethod class_getInstanceMethod(cls, swizzledSelector); method_exchangeImplementations(originalMethod, swizzledMethod); }); }这里要记住几个点不要在load里做太多事情但方法交换为了保证只执行一次必须放dispatch_once交换前最好用class_addMethod判断原方法是否存在避免影响父类实现交换后如果还需要调用原实现记得调用的是xxx_viewWillAppear:不是viewWillAppear:否则会死循环。爱奇艺这种规模客户端很多APM、网络监控、防崩溃SDK都在用方法交换笔试考这个非常贴合实际工作。2.4 Runloop卡顿监控与空闲任务Runloop题表面上是考概念实际上考的是性能优化意识。常见问题有Runloop的Mode有几种kCFRunLoopDefaultMode、UITrackingRunLoopMode、NSRunLoopCommonModes有什么区别为什么滑动ScrollView时NSTimer不触发如何利用Runloop监听卡顿先说Mode。Runloop跑在一种Mode下不是所有Mode都能同时处理事件。DefaultMode处理普通状态TrackingMode处理拖拽滑动当界面滑动时Runloop会切换到TrackingMode之前注册在DefaultMode的Timer就被暂挂起。要解决这个问题可以把Timer加到NSRunLoopCommonModes里它算是一个标记能同时兼容默认和滑动两种场景。卡顿监控的思路是用RunloopObserver监听Runloop状态记录每次kCFRunLoopBeforeSources和kCFRunLoopAfterWaiting的时间差。如果超过阈值比如50ms或100ms就认为主线程卡顿再去抓取当前主线程的调用栈。这个方案不是让你完整实现而是要能说出关键原理。我当时给团队做卡顿监控时还会把卡顿现场的内存、CPU、页面路径一起上报单看Runloop时间差还不够。2.5 UI与布局UIStackView、Safe Area和分屏爱奇艺内容详情页的结构很复杂封面、标题、选集、评论、推荐位层层嵌套用UIStackView能省掉不少约束代码。但笔试考UI布局通常不会让你写一大段约束而是问几个关键点translatesAutoresizingMaskIntoConstraints要不要关UIStackView的spacing在iOS 11之前为什么容易出问题Safe Area和layoutMargins有什么区别第一问是必答的。用Auto Layout时如果translatesAutoresizingMaskIntoConstraints没有设为NO系统会基于autoresizing mask生成冲突约束运行时直接崩溃或约束错乱。UIStackView在iOS 9引入早期对spacing的支持不完整很多人习惯在viewDidLoad里手动设置但iOS 11之后才支持自定义间距之前会有奇怪的表现。Safe Area要突出“刘海屏”适配iPhone X发布之后很多页面底部按钮被Home Indicator挡住根因就是没有用safeAreaLayoutGuide。B卷如果考分屏还会让你处理viewWillTransitionToSize和traitCollection变化核心是不要在布局时写死宽高。2.6 网络层设计缓存、DNS与断点续传网络题在爱奇艺这种视频App里会结合下载场景比普通的“讲讲AFNetworking”要深。常见问法怎么设计网络层HTTP缓存有哪些字段断点续传怎么实现Charles抓HTTPS的原理是什么设计网络层不需要你写出完整代码但要有分层思维。我的习惯是底层用NSURLSession封装上层拆成请求生成器、响应解析器、错误码映射、缓存策略、加密签名、统一回调。请求和响应都要有中间层方便加日志、加埋点、切域名。缓存要区分内存缓存和磁盘缓存视频类App对缓存命中率要求高但缓存策略又和普通接口不一样。断点续传是视频下载的核心原理是HTTP的Range头。客户端发起请求时带上Range: bytesstart-end服务端返回206 Partial Content客户端继续写入文件指定偏移位置。需要考虑的是断网、切换WiFi、服务端不支持Range三种情况。如果服务端返回200而不是206说明不支持断点续传需要重新下载整个文件。这个考点虽然不算难但能把边界情况说全的人不多。2.7 音视频专项播放器架构与首屏秒开音视频是爱奇艺的看家本领B卷一般会有一道开放式大题比如“如何设计一个播放器”。我的建议是不要一上来就谈硬解软解而是先分层播放内核负责解码、渲染业务层负责清晰度切换、倍速、缓存、统计。播放内核和业务层之间用协议隔离业务层不直接依赖具体播放器实现。这样以后从自研播放器切到第三方播放器或者从AVPlayer换到IJKPlayer上层业务代码不用大改。首屏秒开是视频App永恒的优化点。可以做的事很多DNS预解析、提前初始化播放器、预加载首片数据、首帧渲染成功后马上显示、服务端下发合适的清晰度。启动阶段尽量少在主线程做耗时操作把初始化播放器放到子线程但要注意AVPlayer的部分回调必须在主线程。这道题没有唯一答案面试官看的是你能不能把多线程、网络、UI三个维度串起来而不是背一个播放器API。3. 高频编程题与现场手写这部分决定你能不能进二面3.1 手写单例的正确姿势单例题不能只是写个sharedInstance笔试阅卷人会看你的线程安全处理。正确写法是 (instancetype)sharedInstance { static MyClass *instance; static dispatch_once_t onceToken; dispatch_once(onceToken, ^{ instance [[self alloc] init]; }); return instance; }dispatch_once由内核保证只执行一次比synchronized加判空要安全也比“先判空再加锁”性能好。这里有个细节不要在init里调用sharedInstance否则会死锁。单例虽然好用但别滥用因为单例对象常驻内存会一直持有引用。爱奇艺的项目里有很多Manager但都做了内存警告处理把缓存对象在didReceiveMemoryWarning时清掉这个细节如果能在笔试里写出来会显得你很有工程意识。3.2 手写weak属性实现原理weak是iOS面试绕不开的点。笔试不要求你能默写完整源码但要能解释清楚弱引用表。简单说编译器会把__weak变量转成objc_initWeak再通过objc_storeWeak维护一个全局的weak表。这个表以对象的地址为keyvalue是一个指向所有weak引用的数组。对象释放时系统通过objc_clear_deallocating找到对应的weak引用把它们全部置为nil再移除表项。这也是为什么weak属性在对象释放后会自动变成nil。实际工程里有一个性能细节weak变量的读写要访问全局表频繁在循环里写weak会有额外开销。所以block回调里如果频繁访问self最好先用strongSelf转一次而不要一直用weakSelf。手写这个原理能让你在讲到ARC时不只是背定义而是真正理解“弱引用不增加引用计数但也需要runtime来维护”。3.3 用GCD实现一个线程安全的字典多读单写我在2.2已经写了核心代码笔试现场写的时候要额外注意几点队列必须是自己创建的并发队列变量要用__block修饰读操作用dispatch_sync写操作用dispatch_barrier_async。写完代码后面试官通常会追问如果写操作频繁会有什么问题答案是barrier会把写操作前后的并发读隔开导致写操作附近读的并发度下降如果写操作很频繁多读单写模型就不是最优解可能需要考虑用其他结构。另外dispatch_barrier_async的“async”意味着写操作不会阻塞当前调用线程但如果你需要写完之后立刻去读就要考虑加信号量或改用dispatch_barrier_sync。我见过很多人在这个点上翻车只背了“用barrier”却没说清楚同步异步的区别。3.4 KVO的触发时机与依赖键KVO考题分两层。第一层是基本使用注册观察者、实现回调、在dealloc里移除。第二层是机制理解为什么直接给成员变量赋值不触发KVO为什么setter方法没实现也能触发原因是KVO本质上是在运行时生成子类并重写setter在setter里调用willChangeValueForKey和didChangeValueForKey。如果手动调用这两个方法即使没有setter也能触发监听。还有依赖键比如一个对象的fullName依赖firstName和lastName需要实现keyPathsForValuesAffectingFullName。这道题在音视频App里会用在状态同步上比如播放器状态变化要通知多个UI组件刷新。笔试如果能画一个对象关系图说清楚谁监听谁、什么时候触发会比只写代码段更有说服力。4. 答题时最容易翻车的细节与排查技巧4.1 笔试题不是LeetCode要答出“工程感”很多同学把iOS笔试题当成算法题做这是最大的误区。比如问你ViewController生命周期你可以背“loadView、viewDidLoad、viewWillAppear、viewWillDisappear”的顺序但只拿基础分。真正要写的是viewDidLoad里不要加载大量数据viewWillAppear每次页面出现都会调用适合刷新UI但不要做耗时操作viewDidAppear可以做曝光统计和播放埋点dealloc里要移除通知和KVO观察者。把这些和业务场景联系起来答案立刻有了区分度。爱奇艺的视频详情页会频繁push、pop、切前后台生命周期回调每多一次耗时用户感知就多一分卡顿。4.2 代码题最容易错的几个盲点我统计过自己面试过的候选人代码题翻车点高度集中错误写法错误原因正确做法单例里用变量判空加synchronized冗余且性能差直接用dispatch_onceNSMutableString用copy修饰copy 后变成不可变调用 append 崩溃用strongblock 里只写weakSelfblock 执行中 self 可能已被释放必要时转strongSelf并判空NSTimer 的 block 里仍然强引用 self定时器持有 blockblock 持有 self仍循环引用block 内部也要 weakdispatch_barrier_async用在全局队列全局队列无法保证 barrier 效果自定义并发队列方法交换没有用dispatch_once多次交换导致行为混乱放入dispatch_once这些点笔试现场不一定全是原题但绕来绕去都会指向同一个底层概念。如果你能在答题时主动提到“这里容易踩坑”往往会让阅卷人眼前一亮。4.3 排查线上问题常用的三板斧B卷里出现Runtime和Runloop题其实对应的是线上问题排查能力。崩溃了怎么看需要symbolicatecrash解析崩溃日志拿到dSYM文件符号化然后看堆栈。卡顿了怎么查用Runloop监控抓取主线程调用栈再结合当前页面路径。内存泄漏怎么看用MLeaksFinder的思路在对象消失后延迟一段时间检查对象是否还在如果还在就认为泄漏。这套流程在笔试里不会直接考但在面试官追问时会问。比如你说了Runloop卡顿监控他可能会问“抓到的堆栈怎么分析”你如果能答出“先看主线程是否停在系统调用再看是不是同步网络请求、大量图片解码、约束计算”这就把笔试和实际工作串起来了。4.4 时间分配与答题策略个人建议这套B卷总时间如果是一个半小时选择题和填空题控制在20到30分钟简答题不要惜字如金能画图就画图能写伪代码就写伪代码编程题至少留40分钟先写思路再写代码不要一上来就敲容易漏边界设计题留30分钟重点写分层架构和关键模块不要纠结细节。遇到完全不会的题也要把能想到的关键词写上去阅卷是按点给分。空白卷比写错更伤。5. 从笔试题延伸到面试追问爱奇艺式连环问5.1 从“网络层怎么设计”追问到“视频下载失败怎么排查”笔试时你写了网络层设计面试官大概率会继续追问视频下载到一半失败怎么处理这时候不能只回答“重新请求”。要拆开来看先判断是网络断了还是服务端主动断开再看HTTP状态码比如403表示鉴权失败416表示Range越界503表示服务端过载还要看磁盘空间够不够文件写坏没写坏。爱奇艺视频下载场景里弱网状态最常见所以客户端要做分片下载、断点续传、失败重试策略。能把这些讲清楚面试官才会觉得你真正写过下载模块。5.2 从“多读单写”追问到“直播弹幕怎么保证有序”你写了多读单写面试官可能接着问直播弹幕消息频率很高多读单写会不会有延迟如何既保证顺序又保证性能这个问题要分两层答底层数据安全用多读单写没问题但UI层不能每条消息都刷新需要做批量渲染。顺序方面如果从WebSocket拿到的消息本身带序号客户端可以按序号排序而不是依赖底层队列顺序。面试官想听的是你有没有根据业务场景做取舍而不是把barrier当成万能解。5.3 从“Runloop卡顿监控”追问到“首屏优化”Runloop卡顿监控只能定位问题不能解决问题。所以面试官会顺着问怎么优化。你可以答把图片解码放到子线程用后台线程提前创建播放器启动阶段用串行队列减少竞争布局用异步计算视频首帧预渲染。首屏优化的终极目标是让用户尽快看到内容同时避免操作时卡顿。爱奇艺这种App的启动过程涉及渠道配置、登录态拉取、首页接口、缓存读取、播放器预加载每一条链路都可能成为瓶颈。笔试能答到这里已经超出“会写iOS”的层面。整套卷子做下来我最强烈的感受是大部分题目都能在文档里找到答案但如果不理解背后的业务场景很难写出区分度。爱奇艺的iOS岗要的不是只会调API的人而是能主动分析问题、有系统化思路的人。如果你现在正在准备面试别只看题本身把题里涉及的内存、线程、运行时串成一张网比盲目刷十套题有用。我后来自己带团队出题也一直沿用这套思路先考基础再考工程最后考应变。