
又到了一年校招季不少读者在后台问我关于iOS开发笔试面试准备的事情。翻出一份网易2018校园招聘iOS开发工程师的笔试卷来看虽然年份有些久远但里面考察的核心知识点到现在依然有很强的参考价值。相比现在很多公司拿LeetCode原题或者纯Swift语法细节来筛人这套卷子更侧重考察iOS开发者的基本功底、内存管理思维和日常开发中踩坑的积累。这篇文章我将以这套笔试卷为切入点把里面涉及的核心知识点、我当时做题时的思考过程以及多年后回看这些考点在工作中的实际应用一并拆解给大家。不管是准备校招、社招还是想系统梳理一遍iOS基础知识这篇都能帮你有针对性地查漏补缺。1. 网易这套笔试卷的整体风格与考点分布复盘先说结论这套卷子整体难度中等偏上客观题部分覆盖了Objective-C语言特性、内存管理、运行时机制、多线程、网络和UIKit几个大方向没有偏题怪题但有几道题专门挖了看似简单、实则容易错的坑。主观题部分则是典型的场景设计题考察的不只是你会不会写代码而是面对真实业务需求时有没有全局架构思维。从考点分布来看可以大致分为这样几个模块模块占比估算典型考察方式Objective-C语言特性25%分类、关联对象、block、KVC/KVO内存管理20%ARC、循环引用、weak底层原理Runtime与消息机制20%Method Swizzling、消息转发、动态添加方法多线程与并发15%GCD、NSOperation、死锁条件UIKit与渲染10%布局、自动布局、视图生命周期网络与系统框架10%HTTP/HTTPS、数据持久化方案选择有意思的是Swift在这套卷子中占比很低这符合2018年网易iOS团队仍以Objective-C为主力开发语言的背景。当然放到现在准备面试的话Swift和SwiftUI的相关准备肯定是必须的但OC底层原理依然是理解iOS开发的基石很多大厂面试官仍然非常看重这块。这套卷子还体现了一个特点对原理理解深度要求很高。比如不直接问你block怎么用而是问你block捕获外部变量时__block修饰符的底层实现是什么不直接问KVO怎么用而是问系统如何实现KVO的自动通知。这种出题思路本质上是在筛选那些写过很多代码且愿意深入底层验证的候选人而不是只会调用API的面向文档开发者。接下来我按照试卷的实际结构逐块拆解核心题目的解题思路以及这些知识点在工作中的真实使用场景。2. 看似送分实则暗藏杀机的OC语言特性题2.1 分类Category到底能不能添加属性这套卷子有一道判断题类似分类中能否声明属性如果能能否自动生成成员变量和getter/setter不少候选人想都不想就回答能或者不能其实这题要分三个层次来看。第一层分类中能不能声明属性语法上当然可以property写在分类的接口声明里完全合法。第二层系统会不会自动为你合成_成员变量和对应的getter/setter实现不会。普通类中的property会自动合成但分类中不会因为分类的底层结构体_category_t里没有存储成员变量的能力它只有类名、方法列表、协议列表和属性列表。第三层那怎么让分类的属性真正可用这就需要通过关联对象Associated Object来模拟核心是objc_setAssociatedObject和objc_getAssociatedObject这两个运行时函数。这里给大家一个我实际项目中封装关联对象属性的常用写法#import objc/runtime.h interface UIView (TapAction) property (nonatomic, copy) void (^tapActionBlock)(void); end implementation UIView (TapAction) - (void)setTapActionBlock:(void (^)(void))tapActionBlock { objc_setAssociatedObject(self, selector(tapActionBlock), tapActionBlock, OBJC_ASSOCIATION_COPY_NONATOMIC); } - (void (^)(void))tapActionBlock { return objc_getAssociatedObject(self, selector(tapActionBlock)); } end注意关联策略的选择block属性要用OBJC_ASSOCIATION_COPY_NONATOMIC因为block本质上是一个对象需要copy到堆上才能安全地在作用域外使用assign/retain策略要慎重用错了会出现访问野指针的问题。key的选择上用selector(tapActionBlock)比字符串字面量更安全因为编译器会检查方法名是否存在。这个知识点对应聘者来说几乎是必考的而且面试官还会进一步追问关联对象在什么时候释放答案是在dealloc时由objc_removeAssociatedObjects处理所以不需要手动置nil但要注意关联对象不会自动置空如果业务上要提前解绑需要手动调用objc_setAssociatedObject传nil。2.2 block的三兄弟栈上、堆上、全局区block是OC里绕不开的话题网易这套卷子中有一道题给了三段代码让考生判断block的类型以及打印结果本质上考察的是block的存储位置。先给结论没有捕获外部变量的block是_NSConcreteGlobalBlock存储在全局区捕获了外部变量且没有做copy操作的block是_NSConcreteStackBlock存储在栈上对栈上block执行copy后得到_NSConcreteMallocBlock存储在堆上。在ARC环境下编译器会自动对block做copy所以在日常开发中我们直接使用block一般不会遇到栈上block失效的问题但理解底层的存储机制对解释循环引用、截获变量等概念至关重要。我给大家出一道类似的经典变体题void (^block)(void); if (YES) { int x 10; block ^{ NSLog(%d, x); }; } block();在MRC环境下这段代码在if作用域结束后再调用block()会访问野指针因为block在栈上随着作用域弹出已经销毁了。但在ARC环境下block被赋值给强引用时会自动被copy到堆上所以运行结果正常。这也能解释为什么ARC时代我们很少遇到block野指针崩溃但面试中要能说清这背后的机制差异。再补充一个容易忽略的点在block内部修改捕获的外部变量必须用__block修饰。原因在于block捕获外部变量本质上是把变量的值或对象指针拷贝了一份而没有__block时拷贝到block内部的是变量的副本修改副本当然不影响原变量。加上__block后编译器会将变量包装成一个结构体block内外共享这个结构体的指针从而能修改原变量。这是面试官最爱追问的点也是区分会用block和理解block的分水岭。2.3 KVO的底层实现细节KVOKey-Value Observing这套卷子也考了但问的不是API怎么用而是系统在调用addObserver:forKeyPath:options:context:后底层做了什么。标准答案分几步运行时为被观察对象的类动态生成一个子类类名通常叫NSKVONotifying_原类名重写被观察属性的setter方法在setter内部先调用willChangeValueForKey:再调用父类的setter最后调用didChangeValueForKey:然后将对象的isa指针指向这个新生成的子类。这也是为什么KVO机制看起来什么都没改却能拦截属性赋值的根本原因。工作中有一个很实际的坑和这个原理相关如果某个类实现了 (BOOL)automaticallyNotifiesObserversForKey:返回NO手动管理KVO通知那么你用KVO观察这个类的属性时不调用willChangeValueForKey和didChangeValueForKey就永远不会触发回调。另外KVO子类的class方法被重写了所以在KVO生效后调用class仍然返回原类名面试中这也是个高频追问点。我还见过不少候选人在回答KVO时把动态生成子类这个点答对了但解释不了为什么KVC的setValue:forKey:也能触发KVO通知。这其实是因为setValue:forKey:内部走的是setter路径或成员变量路径而KVO重写了setter所以KVC设置值也会触发通知。这是一条隐藏链路建议大家记忆时把KVO和KVC打通来理解。3. 内存管理与循环引用网易面试官最爱深挖的主线3.1 从ARC到weak底层实现这套笔试卷中单选题有一道直接考了ARC下对象释放时机另外还有一道问weak修饰符的底层原理。前者算是基础后者则有一定区分度。先说ARC的规则一句口诀是谁强引用谁负责保持对象存活系统在编译期根据对象的强引用数量自动插入retain和release代码运行时则由autoreleasepool配合管理延迟释放。很多人对ARC的理解停留在编译器自动加引用计数代码这个层面但面试官如果继续追问那autoreleasepool底层是什么能答上来的人就少一些。autoreleasepool实际上是一个栈结构objc_autoreleasePoolPush和objc_autoreleasePoolPop对应入栈和出栈出栈时会对池内所有对象执行release。一个for循环如果创建了大量临时对象在循环体内加一个autoreleasepool能及时释放内存这个优化在图片处理、数据解析等场景中特别实用。再说weak的底层实现。weak表本质上是一个哈希表key是对象地址value是弱引用变量的地址数组。当一个对象即将释放时系统会从weak表中找到所有指向它的弱引用把它们全部置为nil然后才执行dealloc。这也解释了为什么__weak修饰的变量在对象释放后访问是nil而不是野指针。这里我给大家分享一个实际崩溃场景充分说明理解weak底层的重要性- (void)testWeak { NSObject *obj [NSObject new]; __weak NSObject *weakObj obj; NSLog(%, weakObj); }这段代码运行完testWeak后weakObj就已经是nil了因为obj是局部变量出了作用域后强引用消失对象被释放。但如果把obj改成static NSObject *obj或者全局变量weakObj就能在方法外继续访问因为对象没有被释放。这些细节在内存优化和对象生命周期管理时经常遇到理解原理后排查起来才顺手。3.2 循环引用全家桶Block、Delegate、NSTimer循环引用是iOS面试必考题网易这份卷子自然没有放过。常见的有三种场景每一道都能扩展成一道完整的面试讨论题。第一种是block循环引用。经典场景是控制器持有block属性block内部又使用了self。在ARC下编译器会警告Capturing self strongly in this block is likely to lead to a retain cycle。解决办法是使用__weak声明一个弱引用在block中使用弱引用对象。但要注意如果block内部有异步延迟执行直接用弱引用会导致执行时对象已经释放需要视业务决定是否在block内部先用强引用持有weakSelf保证执行期间对象存活。标准写法是这样__weak __typeof(self) weakSelf self; self.completionBlock ^{ __strong __typeof(weakSelf) strongSelf weakSelf; if (strongSelf) { [strongSelf doSomething]; } };这里if (strongSelf)既保证了非空也保证了在block执行周期内self不会被释放这是很多面试官期待的完整答案。第二种是delegate循环引用。如果delegate属性用strong修饰同时业务对象持有delegate对象那么两者会互相强引用导致永远无法释放。正确的做法是delegate一律用weak或assign修饰且协议本身没有强引用语义。但assign有一个坑delegate对象释放后assign修饰的指针不会自动置nil后续调用会产生野指针崩溃而weak会自动置nil所以现代iOS开发中一律建议用weak。第三种是NSTimer循环引用。因为Timer会强持有它的target而控制器又持有Timer这就形成了timer-target和self-timer的双向强引用。常见解决方案有使用block形式的 scheduledTimerWithTimeInterval:repeats:block:在block中使用weakSelf或者在viewWillDisappear时主动invalidate并置nil。我还见过一个比较优雅的做法是用一个中间对象Proxy作为target让Timer持有ProxyProxy弱引用真正的target这样既能保证消息转发又能打破循环引用。这个思路如果面试时能提到会很加分。3.3 从一道选择题看内存泄漏排查流程这套卷子里有一道关于NSTimer是否需要在dealloc中invalidate的判断题正确答案是需要。但真正有价值的不是答案本身而是你是否在实际开发中形成了一套内存泄漏排查的方法论。我在实际项目中排查内存泄漏通常按以下几步走先跑一遍App进入相关页面反复进出几次用Xcode Memory Graph观察是否有控制器对象未释放。若发现控制器未释放先看该对象有没有被全局容器、单例、NotificationCenter强持有。再检查block、delegate、NSTimer、CADisplayLink这些最常见的循环引用源。用Instruments的Leaks模板做一次全量内存扫描确认是否存在系统层面的泄漏。这里分享一个真实案例。之前接手一个直播项目用户反馈退出直播间后内存持续上涨用Instruments排查发现是CADisplayLink没有停止引起的。CADisplayLink和NSTimer一样会强持有target而且它和屏幕刷新频率同步如果忘记invalidate每一帧都在执行回调内存和CPU双重上涨。当时在代码里加了这样的逻辑- (void)viewDidDisappear:(BOOL)animated { [super viewDidDisappear:animated]; [self.displayLink invalidate]; self.displayLink nil; }并且把displayLink的target改成了一个代理对象。处理完后再跑内存检测曲线终于稳定下来了。这些血的教训其实都源于笔试中那道看似送分的Timer题所以基本功扎实的人写代码时会有意识地规避这些问题。4. Runtime与消息机制从笔试难题到线上疑难杂症排查4.1 消息发送、消息转发和动态方法解析全链路Runtime部分网易这套卷子的主观题里有一道是让考生叙述OC的消息机制这是所有iOS面试的必问题。完整的答案链路包括消息发送阶段调用对象方法时编译器将[obj doSomething]转成objc_msgSend(obj, selector(doSomething))运行时通过对象的isa指针找到类再在类的方法缓存和方法列表中查找对应的IMP找到就跳转执行。动态方法解析阶段如果没找到会先调用 resolveInstanceMethod:或 resolveClassMethod:允许开发者动态添加方法实现。消息转发阶段如果动态解析也不处理则进入- forwardingTargetForSelector:可以指定另一个对象代为处理如果还是没处理则进入- methodSignatureForSelector:和- forwardInvocation:完整的事件分发流程。最后如果都没处理就会抛出经典的unrecognized selector sent to instance崩溃。笔试中有一道选择题专门考了这个假如给一个不存在的对象发消息会发生什么很多候选人直接选崩溃但正确选项是先走完整消息转发流程最后在兜底阶段崩溃。这之间的区别恰恰是很多人可以实现JS与OC交互、AOP埋点等高级功能的理论基础。4.2 Method Swizzling的正确打开方式和致命陷阱网易这套卷子在Runtime部分还考了Method Swizzling问的是能否在分类中用Swizzling替换系统方法。这里不仅要答能还得能把坑说清楚。先看一个标准的Swizzling写法 (void)load { static dispatch_once_t onceToken; dispatch_once(onceToken, ^{ Class class [self class]; SEL originalSelector selector(viewDidAppear:); SEL swizzledSelector selector(swizzled_viewDidAppear:); 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); } }); }有几个关键细节必须强调一下。第一Swizzling应该写在load里而不是initialize里因为前者只调用一次且时机确定。第二必须用dispatch_once保证线程安全防止Swizzling被执行多次导致方法调换被交换两次等于没有交换。第三这里用class_addMethod做了一层保护如果原类有viewDidAppear:实现class_addMethod会返回NO走method_exchangeImplementations正常交换如果原类没有实现这个方法比如只是想给所有类添加一个自定义实现就先把swizzled方法添加为原selector的实现再替换掉swizzled selector的实现避免子类继承和父类实现互相干扰。第四注意在旧版本SDK上交换viewDidAppear这种系统方法时viewDidAppear方法可能定义在父类或分类中直接method_exchangeImplementations在某些情况下会失效上面的保护逻辑能覆盖这种情况。我在实际项目中用Swizzling做过全局的页面访问统计、点击事件埋点、异常日志采集效果很明显不用在每个页面重复写统计代码。但也踩过一个大坑某个第三方SDK也在load里对同一个方法做了Swizzling两个库的交换顺序不确定结果导致某些页面无法响应部分触摸事件排查了很长时间。后来我们的做法是在Swizzling前先检查方法IMP是否已经被换过或者干脆把统计逻辑全部迁移到AOP框架中统一管理避免和第三方库冲突。4.3 一个真实案例利用消息转发实现方法兜底讲一个工作中真实遇到过的场景。我们App在旧版本中发布了某个API后来新版本接口变更后台重新定义了一系列新方法。为了让老版本用户拿到新数据模版时不会因为方法不存在而崩溃我们实现了一个方法兜底方案。大致思路是定义一个基类重写forwardInvocation:将无法识别的方法归档为日志上报然后统一返回一个安全默认值- (void)forwardInvocation:(NSInvocation *)anInvocation { NSString *selectorName NSStringFromSelector(anInvocation.selector); // 记录到日志系统 [LogManager reportCrash:[NSString stringWithFormat:unrecognized selector: %, selectorName]]; // 根据返回类型设置默认值 const char *returnType anInvocation.methodSignature.methodReturnType; if (strcmp(returnType, encode(void)) ! 0) { NSUInteger length [anInvocation.methodSignature methodReturnLength]; void *buffer calloc(1, length); [anInvocation setReturnValue:buffer]; free(buffer); } }配合- methodSignatureForSelector:返回一个临时的类型签名就能让App在遇到新方法缺失时不至于崩溃同时把错误信息上报到后台。这个思路在大型App的灰度流程、组件化动态下发场景中都很实用也和这套笔试卷中Runtime部分的考察目标一脉相承理解底层机制才能灵活解决线上问题。5. 多线程与并发从GCD死锁到实际业务中的竞态处理5.1 GCD死锁的四种典型场景多线程部分网易这套卷子有一道非常经典的题在主线程同步执行一个主队列任务问会死锁吗答案是会。这是GCD死锁最经典的场景。我在面试时也常拿这道题作为开场因为能答对的人不少但能把这个过程一步一步讲清楚的人不多。主队列是串行队列主线程当前正在执行viewDidLoad还没有执行到dispatch_sync之后的代码这时候再往主队列提交一个同步任务这个任务排到队尾等待执行而队列此时被当前任务占用需要等当前任务结束才能执行新任务形成互相等待死锁。除了主队列同步死锁外还有几种很容易踩坑的死锁场景串行队列自己创建的中嵌套执行dispatch_sync提交到同一个串行队列。使用dispatch_semaphore_wait和dispatch_group_wait时设置超时时间为DISPATCH_TIME_FOREVER如果信号量永远不会被消耗线程就会一直卡死。多个队列相互依赖任务A依赖任务B任务B依赖任务A。对于第2种场景我遇到过线上一个事故某个数据上报模块用信号量等待网络请求的完成结果网络层因为某种原因一直没有回调dispatch_semaphore_wait把工作线程卡死了导致执行该任务的线程池被耗尽。修复方案是设置合理的超时时间不要用永久等待long result dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC)); if (result ! 0) { // 超时处理本次上报失败但不会卡死线程 }5.2 NSOperation与GCD的选型对比网易这套卷子主观题里还有一道什么时候用NSOperation什么时候用GCD的对比题。这题本身不难但如果只是背出前者更高级、后者更轻量就太平淡了。应该从实际业务出发来对比维度GCDNSOperationQueue任务取消不支持直接取消支持cancel依赖关系需要借助dispatch_barrier或手动管理原生支持addDependency最大并发数控制可用信号量/group间接控制原生支持maxConcurrentOperationCountKVO监听不支持支持operation状态KVO重试机制需自行封装可自定义NSOperation实现所以在实际项目里如果是简单的异步任务用GCD就够了如果是图片下载队列、多任务依赖编排、需要取消或暂停的复杂业务用NSOperationQueue会更省心。我一个做电商项目的朋友在实现商品列表图片懒加载时就是用NSOperationQueue把每个图片下载包装成Operation配合maxConcurrentOperationCount限制同时下载数然后通过依赖关系保证同一Section的图片按顺序加载这比用GCD裸加信号量优雅得多。5.3 数据竞争的排查与加锁方案还有一道题考了多线程同时读写一个可变字典问会不会有问题。这题考的是数据竞争的概念。如果你答不会面试官会让步深入如果你答会接下来就会问你打算怎么解决。答案其实很明确NSMutableDictionary不是线程安全的多个线程同时读写可能导致崩溃或数据错乱。解决方案有几种需要根据业务场景选择使用dispatch_barrier_async配合并发队列实现多读单写模式这是性能与安全兼顾的最佳方案之一。使用NSLock或synchronized进行加锁保护简单但可能影响性能。使用os_unfair_lock或pthread_mutex做底层锁性能好、复杂度高。使用原子属性但要注意原子性只保证单次读写的安全不保证复合操作的原子性。我在项目里写过一套高性能字典的封装核心就是dispatch_barrier_async模式interface SafeDictionary : NSObject - (instancetype)init; - (nullable id)objectForKey:(NSString *)key; - (void)setObject:(id)object forKey:(NSString *)key; end implementation SafeDictionary { NSMutableDictionary *_storage; dispatch_queue_t _queue; } - (instancetype)init { self [super init]; if (self) { _storage [NSMutableDictionary dictionary]; _queue dispatch_queue_create(com.example.safeDict, DISPATCH_QUEUE_CONCURRENT); } return self; } - (id)objectForKey:(NSString *)key { __block id result nil; dispatch_sync(_queue, ^{ result _storage[key]; }); return result; } - (void)setObject:(id)object forKey:(NSString *)key { dispatch_barrier_async(_queue, ^{ _storage[key] object; }); } end这种写法的优势在于多个读操作可以并发执行写操作独占执行且等前面所有读操作完成后续的读操作也会等写操作完成保证了安全性的同时又最大化读性能。如果面试时能在多线程话题里给出这样的方案面试官通常会很认可。6. UIKit与系统框架布局、生命周期和持久化方案的选择逻辑6.1 关于UIView和CALayer的关系网易这套卷子在UIKit部分有一道比较基础的题问UIView和CALayer的关系。简单答法是UIView是视图的载体负责交互和布局CALayer是UIView的底层渲染层负责绘制和动画。但如果只答到这里只能得一半分。我习惯这样扩展UIView的layer属性本质就是CALayerUIView本身并没有直接绘制内容的能力绘制和显示完全由layer完成。UIView负责的是事件响应、层级管理、约束处理CALayer负责的是内容展示、动画、阴影、圆角、边框等渲染属性。两者的关系和壳与核很像UIView是外层壳CALayer是内层内核。这个知识点在工作中有很实际的应用。比如只要设置cornerRadius和masksToBounds其实改变的是layer的属性想要更高效的圆角处理可以使用CAShapeLayer配合UIBezierPath想要实现阴影效果可以不触发离屏渲染通过shadowPath指定阴影路径来提升性能。这些细节面试官不一定直接问但遇到列表滚动卡顿优化这类问题时会用到。比如下面这段代码是我在项目里封装的一个高性能圆角头像方案- (UIImageView *)roundedImageView { UIImageView *imageView [[UIImageView alloc] init]; imageView.layer.cornerRadius 20; imageView.layer.masksToBounds YES; return imageView; }如果头像图片尺寸较大且数量较多masksToBounds YES会触发离屏渲染导致滚动掉帧。更优做法是用Core Graphics绘制圆角- (UIImage *)drawRoundedImage:(UIImage *)image withRadius:(CGFloat)radius { CGRect bounds CGRectMake(0, 0, image.size.width, image.size.height); UIGraphicsBeginImageContextWithOptions(image.size, NO, image.scale); CGContextRef context UIGraphicsGetCurrentContext(); UIBezierPath *path [UIBezierPath bezierPathWithRoundedRect:bounds byRoundingCorners:UIRectCornerAllCorners cornerRadii:CGSizeMake(radius, radius)]; CGContextAddPath(context, path.CGPath); CGContextClip(context); [image drawInRect:bounds]; UIImage *roundedImage UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return roundedImage; }这类细节在笔试中不会直接考但在如何优化UITableView滑动流畅度这种开放题里提出来会非常有说服力。6.2 视图控制器生命周期在不同场景下的触发顺序关于viewDidLoad、viewWillAppear:、viewWillLayoutSubviews、viewDidAppear:这些方法网易这套卷子中也有涉及。不是直接考点但在多道题目中都有隐含关联。我当时做题时总结了几个容易出错的场景从A页面push到B页面再返回A页面的viewWillAppear:会再次调用但viewDidLoad不会再调用。B页面的viewDidLoad是在A页面viewWillDisappear:之前触发还是之后触发取决于导航控制器的实现多数情况下B的viewDidLoad会先于A的viewWillDisappear:执行。使用present方式弹出C页面时A页面的viewWillDisappear:和viewDidDisappear:都会调用Dismiss时A页面的viewWillAppear:和viewDidAppear:都会重新调用。在viewDidLoad中获取视图的frame是拿不到最终值的因为此时视图还没有布局完成要获取准确frame需要在viewDidLayoutSubviews或viewWillLayoutSubviews中获取。这里有一个真实踩坑经历可以分享。早期开发时我在viewDidLoad里根据屏幕宽度设置了某个控件的frame在iPhone 8上测试正常但在iPhone X上就发现控件偏移了。排查很久才意识到viewDidLoad执行时安全区域可能还没有计算完成正确方式是在viewSafeAreaInsetsDidChange或viewDidLayoutSubviews重新布局时调整。后来项目全面改用Auto LayoutMasonry或SnapKit这种问题就根治了。所谓自动布局能解决大部分frame计算问题核心原因就在这里系统会在布局阶段统一计算而不是你手算。6.3 数据持久化方案的选择逻辑这套卷子的网络与框架部分有一道你会选择哪种数据持久化方案的题。很多考生第一反应就是UserDefaults但这题考察的是按场景选方案的能力。正确的分析思路应该是少量配置项比如用户开关、登录状态用NSUserDefaultsAPI简单、存储轻量。结构化数据比如商品列表、消息记录用SQLite需要用到FMDB或WCDB这类封装库。对象归档用NSKeyedArchiver适合保存单个对象。文件存储用沙盒目录适合图片、音频等大文件。复杂查询和跨端需求可以考虑用Realm性能好、API友好。面试官如果继续追问SQLite的主键和索引如何设计就需要你具备一定的数据库知识。我记得网易社招面试时还问过在SQLite中如何实现一个简单的全文搜索答FTS3/FTS4模块或者用外部索引都能过关。校招笔试卷虽然没到这个深度但理解没有最好的存储方案只有最适合业务场景的方案这种思维本身就是一个加分项。7. 从真题反推备考方法论校招iOS开发到底该怎么准备7.1 真题暴露的几类常见薄弱点做完这套卷子我总结了几个考生最容易暴露的薄弱点准备校招的读者可以对照检查一下。第一类是API熟、原理浅。很多人天天用NSMutableArray、NSDictionary但连它们底层的数据结构是什么都说不清楚。数组是连续内存还是链表字典的哈希表如何解决冲突这些问题看似偏底层面试官却很喜欢绕到这儿来考察候选人的计算机基础。第二类是只知概念、不会验证。比如问到ARC和MRC的区别能倒背如流但问到ARC下__bridge、__bridge_retained和__bridge_transfer的具体使用场景就懵了。这类桥接和内存管理的理解需要在工程实践中反复验证才能形成感觉。第三类是知其然不知其所以然。比如会用GCD的常用API但不理解队列和线程的映射关系会写KVO回调但不清楚观察机制底层是如何实现的。这套笔试卷主观题部分就是专门设计来筛选这一类考生的。7.2 我看过的一份高分答题思路之前有位读者把自己备考时写的网易笔试卷答题框架发给我看我觉得很有参考价值。他拿到一道场景设计题时会这样组织答案先确定需求边界这个功能解决什么问题不是单纯地罗列用什么API。画出模块结构涉及哪些对象对象之间如何交互。分析关键难点比如数据不一致、线程安全、性能瓶颈。给出方案对比为什么选A不选B优缺点都列出来。补充异常处理网络失败、数据为空、重复点击等边界情况如何处理。附带一句可扩展性将来需求变化时如何调整。这种结构化答题思路适用于分析题、设计题也适用于面试中的系统设计问答。我建议大家平时刷题或者练手时不要只满足于能跑通要有意识地用这个框架去组织思路。7.3 校招准备的时间线与资料清单结合这几年带新人的经验我给准备校招的同学一条比较高效的学习路线第一阶段基础夯实2-3周系统过一遍Objective-C语法、Foundation框架、内存管理推荐《Effective Objective-C 2.0》和《Objective-C高级编程iOS与OS X多线程和内存管理》这两本书配合看Apple官方文档。同时把这些知识点整理成自己的笔记手动画一下引用计数、weak表、消息转发的示意图。第二阶段原理深入3-4周重点啃Runtime、RunLoop、多线程底层、网络协议。可以跟着源码objc4、libdispatch读关键部分不需要全读抓住核心流程即可。这个阶段可以尝试自己写一些验证Demo比如KVO触发顺序打印、Method Swizzling前后IMP变化、GCD不同队列的各种组合实验。第三阶段项目实践长期参加一些开源项目、做个人App或者把课程设计完善成可展示的作品。注意在简历里写清楚你解决了什么问题不要只写使用了XX技术栈。第四阶段刷题冲刺考前2-3周把牛客网的历年iOS真题刷一遍重点整理错题和不确定的知识点。LeetCode的算法题也要保持手感校招笔试题中常考链表、树、字符串、动态规划这几个大类每天保持1-2道。我当时准备的时候用了一个markdown文档专门记录所有踩到的坑每天睡前复盘一遍效果很好。尤其是Runtime相关的知识点自己写Demo验证一遍比看十篇教程都管用。8. 面试之外的加分项聊聊项目复盘和表达方式尽管笔试卷本身分数很重要但网易这类大厂的招聘通常笔试只是第一关后面还有技术面试、HR面试。根据我带过候选人的经验有几个非技术的加分项很容易被忽视。第一个是项目复盘的表达能力。很多候选人简历上写了好几个项目但被问到这个项目中你遇到的最大难点是什么时说不清楚或者答得很散。这里我建议用痛点-方案-思考三步法来组织语言。比如当时的问题是列表滑动卡顿痛点我通过Instruments定位到是离屏渲染导致于是用圆角绘制优化和缓存机制解决方案后续我得知这种优化思路同样适用于地图标注、信息流列表所以我在新项目中直接就规避了这个问题思考。这样讲故事不仅有细节还能体现你的总结和举一反三能力。第二个是代码规范与注释习惯。笔试和面试中如果现场写代码注意变量命名规范、边界条件检查和防御式编程。我见过一些候选人算法题能写出来但代码里全是a、b、c这种无意义命名也没有处理空数组、空字符串的边界条件。这会给人留下工程素养不够的印象。第三个是对业务和产品有认知。大厂面试官很看重候选人能不能从产品角度思考技术方案。比如问到直播App的聊天弹幕如何实现除了说技术方案以外如果能提到弹幕算法要考虑用户互动率、屏幕防遮挡、性能平衡这些产品层面的思考面试官会觉得你有整体思维不是纯执行型的工具人。回到这份网易2018校招笔试卷它虽然没有直接考这些软技能但能被笔试筛出来进入面试的候选人技术基础往往都不差最后比拼的反而就是这种综合表达和思维方式。每次回看这套题我都会有一个感受真正扎实的iOS开发功底不是靠背题背出来的而是靠持续的实践、踩坑和总结堆出来的。很多笔试中的知识点在多年后的某次线上事故排查、某个性能优化中会突然产生共鸣。如果你正在准备校招看到这篇文章后不妨把文中的每个考点都自己动手验证一遍把思考过程写下来这个过程本身比答案更重要。