☰
爱奇艺iOS笔试题深度解析:从Runtime到内存管理的工程实战
2026/10/20 10:01:00 网站建设 项目流程

爱奇艺2019秋招iOS方向笔试题(B)这个话题,我在收藏夹里存了四年多。当年这一套卷子让不少人栽了跟头,现在回头再看,反而觉得它比市面上一堆“iOS八股文题库”更有参考价值。iOS方向的笔试题通常不追求硬核算法,而是看你对工程细节敏不敏感。这套B卷基本覆盖了Objective-C运行时、内存管理、多线程、网络、UI布局、音视频相关的基础能力,适合正在准备秋招的应届生、想跳槽的初中级iOS开发,以及想出题但不知道从哪下手的团队负责人参考。爱奇艺的业务是长视频,移动端要处理播放器、缓存、清晰度切换、App启动速度、弹幕互动等场景,这些都会直接进到考题里。换句话说,这套卷子不是在考你会不会写for循环,而是在看你有没有能力在一个大规模客户端里扛住线上问题。

1. 这套笔试题到底在考什么:从爱奇艺的业务场景反推考点

1.1 技术栈判定:为什么是OC而不是Swift全家桶

2019年爱奇艺客户端主体还是Objective-C,Swift在部分新模块逐步接入,所以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和@strongify;NSTimer为什么会造成循环引用,怎么解决。很多同学能答出“用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读到nil,strongSelf就是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很常见的需求,比如视频缓存列表、播放器配置表,读多写少。正确姿势是自定义并发队列加barrier:

@property (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: bytes=start-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表。这个表以对象的地址为key,value是一个指向所有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_once
NSMutableString用copy修饰copy 后变成不可变,调用 append 崩溃用strong
block 里只写weakSelfblock 执行中 self 可能已被释放必要时转strongSelf并判空
NSTimer 的 block 里仍然强引用 self定时器持有 block,block 持有 self,仍循环引用block 内部也要 weak
dispatch_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的人,而是能主动分析问题、有系统化思路的人。如果你现在正在准备面试,别只看题本身,把题里涉及的内存、线程、运行时串成一张网,比盲目刷十套题有用。我后来自己带团队出题,也一直沿用这套思路:先考基础,再考工程,最后考应变。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询