
做iOS开发这些年我陆陆续续帮公司出过笔试题、也批过不少卷子。前阵子整理旧电脑翻出一份2018年网易iOS开发实习生的笔试题备份重看一遍还挺有感触。那年头iPhone X刚出、Swift 4还在跟Swift 3的兼容性较劲但笔试里考的核心东西放到现在来看也一点不过时。这篇内容就围绕这套题的题型和考点聊聊我当时是怎么拆解题目的、哪几类题最容易翻车以及这类笔试背后到底想筛选什么样的人。如果你正在准备iOS实习面试或者想从初级开发往中级跳认真过一遍应该会有点收获。1. 先看整体笔试到底在筛什么样的人1.1 2018年前后的iOS技术背景要理解这套题的出题逻辑得先回到那个时间节点。2018年上半年iOS开发的主流环境大概是这样的系统方面iOS 11是主力iPhone X的刘海屏适配是那段时间的热门话题safeAreaInsets、automaticallyAdjustsScrollViewInsets这些API被反复讨论。语言方面Objective-C依然是绝大多数公司的生产主力Swift 4刚刚发布不久很多团队还在观望笔试基本以OC为主、Swift为辅。工具链方面Xcode 9是主流CocoaPods已经是事实上的依赖管理标准虽然当时也有人在讨论Carthage。架构方面MVC还是绝对主流MVVM开始被更多人接受但远没有到人手一套的地步。这套题就是在这样的环境下出的。回头去看它的考察点非常典型语言基础功底、Runtime和内存管理的理解深度、多线程并发是否真的用过、基础数据结构和算法能不能快速写对外加一些iOS特有的框架细节。说白了这些就是实习生在入职后前三个月最需要的能力——不要求你有丰富的项目经验但底子必须扎实。1.2 笔试和面试的分工逻辑很多同学容易把笔试和面试混为一谈觉得“笔试就是小面试”这个理解其实有偏差。实习生的招聘流程里笔试承担的是第一道筛选它的核心目标不是“选出最优秀的”而是“快速过滤掉基础不行的”。面试可以考察你的表达、你的工程直觉、你遇到问题时的沟通方式这些是软性的、需要人来判断的。但笔试不一样它是硬性的一个空在那里会就是会不会就是不会。网易这套题题量不算特别大但覆盖面很广选择题、简答题、编程题都有。它的逻辑是用选择题快速扫一遍你的知识广度用简答题看你是不是真的理解原理而不是背概念用编程题检验你能不能写出编译通过、边界处理正确的代码。所以准备这类笔试光刷面试题是不够的你得能“写”出来。我在后面会反复强调这一点看懂和写对中间隔着十万八千里。2. 语言基础看着简单实则易错的高频题2.1 属性修饰符一道题能串起一堆知识点OC的属性修饰符是笔试选择题的常客几乎每套题都会出现。网易这套题里有一道非常典型的property (nonatomic, readwrite, copy) NSString *name;问的是为什么字符串属性要用copy而不是strong这道题本质上考的是可变与不可变类的关系。很多人背过答案知道“用copy可以防止外部可变字符串被修改”但没想过底层细节。我建议从这样一个例子入手理解NSMutableString *temp [NSMutableString stringWithFormat:hello]; self.name temp; // 如果用 strong [temp appendString: world]; // self.name 会变成 hello world而这可能不是你期望的如果属性是copy赋值时会对temp做一次不可变拷贝之后temp再怎么变self.name都不受影响。这是copy最核心的价值保证属性值的独立性。还有一个是atomic和nonatomic的经典问题。很多人会说“atomic是线程安全的”这句话严格来说是有问题的。atomic保证的是属性的原子性——也就是读写这个属性时不会被多线程同时访问打断但它并不能保证整个业务逻辑的线程安全。比如if (self.array.count 0) { id obj self.array[0]; // 即使 array 是 atomic这里也可能崩溃 }因为count和objectAtIndex:是两个独立的操作中间随时可能有其他线程修改数组。atomic本质上是给底层加了自旋锁或互斥锁在不同时期实现不一样代价是性能开销而iOS开发里绝大多数属性都在主线程访问用atomic属于白花钱。所以默认写nonatomic是合理的工程选择。我整理了一张表笔试前可以快速过一眼修饰符作用常见坑assign直接赋值常用于基础类型用在对象上会产生悬垂指针对象释放后指针仍指向旧地址weak不持有对象对象释放后自动置nil只能用于iOS 5需要支持旧版本时要注意strong持有对象引用计数1容易造成循环引用copy拷贝一份再持有NSString、NSArray、NSDictionary 常用block属性也要用copyatomic保证读写原子性不保证业务逻辑安全且有性能损耗2.2 Block与循环引用笔试怎么答才能拿分Block这部分的题网易考得非常经典。先是选择题下面的代码会不会造成循环引用self.myBlock ^{ [self doSomething]; };答案当然是会。因为self持有myBlock而myBlock内部捕获了self形成self - block - self的引用环两边都无法释放。标准解法是__weak typeof(self) weakSelf self; self.myBlock ^{ [weakSelf doSomething]; };但这里有个细节如果你在简答题里只写这一行可能只能拿一半分。笔试改卷人更希望看到的是你不仅知道要用__weak还知道为什么有时候需要配合__strong一起用。比如这样一个场景block内部有多行代码中间涉及异步操作。__weak typeof(self) weakSelf self; self.myBlock ^{ dispatch_async(dispatch_get_global_queue(0, 0), ^{ [weakSelf doSomething]; // 如果 weakSelf 在这里被释放了doOtherThing 不会执行 [weakSelf doOtherThing]; }); };weakSelf在block执行过程中随时可能变成nil如果你希望整个block执行期间self不被释放标准的写法是__weak typeof(self) weakSelf self; self.myBlock ^{ __strong typeof(weakSelf) strongSelf weakSelf; if (strongSelf) { [strongSelf doSomething]; [strongSelf doOtherThing]; } };先weak再strong这是面试里最常见的加分写法。笔试答题时把这个逻辑写清楚基本就能跟只会“背答案”的人拉开差距。2.3 Category、Extension与关联对象的隐藏考点网易这套题里有一道简答题问的是Category分类和Extension扩展的区别。这个知识点看起来简单但能答全的人不多。我推荐这样组织答案Category在编译时不改变原有类结构的情况下为类增加方法。运行时会把方法合并到类的方法列表中。但Category不能直接添加实例变量。Extension在编译期就决定了方法一定存在可以添加实例变量但只能写在主类的.m文件里本质上是“匿名分类”。Category中的方法如果和主类方法重名运行时不会调用主类的方法而是调用Category里的实现最后编译的那个Category优先。还有一个隐藏考点是“Category里能不能添加属性”。答案是可以但要用关联对象Associated Object来实现本质上并不是真的给类增加了实例变量而是在运行时通过一个全局字典去维护对象和属性值之间的映射。static const char kAssociatedKey \0; implementation NSObject (Associated) - (void)setMyProperty:(NSString *)myProperty { objc_setAssociatedObject(self, kAssociatedKey, myProperty, OBJC_ASSOCIATION_COPY_NONATOMIC); } - (NSString *)myProperty { return objc_getAssociatedObject(self, kAssociatedKey); } end这类题考察的是你对OC运行时机制的敏感度。我后来面试别人时发现能把这个点讲透彻的候选人对底层原理的理解普遍都不错。3. Runtime与内存管理笔试的分水岭3.1 消息发送与消息转发从objc_msgSend说起OC的方法调用本质上是消息发送这个几乎是iOS笔试必考。网易这套题的选择题里直接问[obj doSomething]在底层被编译成了什么答案是objc_msgSend(obj, selector(doSomething))。更完整一点的说法是消息发送函数会沿着对象的isa指针找到类对象再在类对象的方法列表里查找对应的方法实现。如果找不到就沿着superclass指针一直往上找直到NSObject还找不到就进入消息转发流程。消息转发一般分三步这是简答题的高频考点第一步动态方法解析 (BOOL)resolveInstanceMethod:(SEL)sel可以在这个方法里用class_addMethod动态给类添加方法。第二步快速转发- (id)forwardingTargetForSelector:(SEL)sel把消息转发给另一个能处理这个方法的对象。第三步完整转发- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel - (void)forwardInvocation:(NSInvocation *)invocation这里我踩过一个印象很深的坑。当时我在封装一个统计SDK为了让某些打点方法在底层实现还没就绪时不崩溃想用forwardingTargetForSelector:做一个“静默丢弃”的处理结果发现只实现第三步的methodSignatureForSelector:和forwardInvocation:时方法签名返回不对导致doesNotRecognizeSelector:崩溃。后来才发现forwardInvocation:里必须对invocation做完整的参数包装和invoke处理漏一步都可能出问题。笔试如果考到消息转发我建议把三步的来源_objc_msgForward、触发顺序、每一层适合处理什么场景都写清楚。比如第一步适合做“防崩溃兜底”第二部适合做“多继承的替代方案”第三步适合做“方法签名和参数的完整处理”。这样答出来的简答题分数不会低。3.2 ARC到底在做什么编译器的“补刀”内存管理的题笔试里最常见的问法是“ARC下什么时候需要自己处理内存管理”以及“weak变量在对象释放后发生了什么”ARC不是垃圾回收它是在编译期帮你在合适的位置插入retain、release、autorelease也就是所谓的“引用计数管理自动化”。它的核心规则是谁持有谁负责释放对象在没有强引用时被销毁。但ARC不是万能药它解决不了循环引用也解决不了C语言层面的内存问题比如malloc/free。还有一个高频考点是autoreleasepool。在ARC下每次进入autoreleasepool {}块底层的AutoreleasePoolPage就会记录一次push块结束时执行pop把池子里所有对象执行一次release。这个机制在主线程RunLoop里其实每圈都会自动执行所以很多人疑惑“为什么我的临时对象没占很多内存”——因为主线程RunLoop在每次事件循环后都会自动释放一次。关于weak变量的底层原理笔试简答题想拿高分可以这么答weak表是一张由对象地址作为key的哈希表value是weak指针地址的数组。对象释放时系统会遍历这张表把所有指向该对象的weak指针都置为nil防止悬垂指针。这里有一个容易漏的细节对象dealloc时系统首先会清除所有weak引用然后才执行dealloc方法体里剩余的代码最后释放内存。所以你在dealloc里访问self的weak属性得到的是nil。3.3 KVO与KVC底层实现才是加分点网易这套题考了一道KVO的题系统是怎么做到监听属性变化的这个考点年年都出但很多人只会用、不会答原理。正确的解释是这样的当你第一次对一个对象调用addObserver:forKeyPath:options:context:时Runtime会动态创建一个该对象类的子类比如NSKVONotifying_Person然后把对象的isa指针指向这个子类。之后你修改属性时实际上调用的是子类重写过的setter这个setter会先调用willChangeValueForKey:接着调用父类的setter然后调用didChangeValueForKey:最后触发监听回调。这里有一个很经典的坑如果属性是通过直接访问实例变量修改的比如_name xxx不会触发KVO通知。所以很多面试官会追问“KVO能不能监听直接用_name修改的值”答案是不能因为绕过了setter。KVC的题则喜欢考取值和设值的顺序。比如[obj setValue:value forKey:name]会按什么顺序找先找setName:方法找到了就调用。找不到再看accessInstanceVariablesDirectly是否返回YES默认是YES。如果返回YES则按_name、_isName、name、isName的顺序查找实例变量找到就赋值。都找不到调用setValue:forUndefinedKey:默认抛出异常。取值是类似的逻辑先找getName:、name、isName再找实例变量_name、_isName、name、isName最后走valueForUndefinedKey:。这类题目记住一个原则就行KVO和KVC都不是简单的“调用setter/getter”它们背后是Runtime的“套壳逻辑”。理解了这一点笔试和面试都不慌。4. 多线程与网络并发题从来都是重灾区4.1 GCD经典死锁题主队列同步执行多线程是iOS笔试里翻车最多的部分原因是看似简单、实则细节极多。网易这套题里有这么一道选择题- (void)viewDidLoad { [super viewDidLoad]; dispatch_sync(dispatch_get_main_queue(), ^{ NSLog(执行了吗); }); }问这段代码会发生什么答案是死锁NSLog永远不会打印。原因在于viewDidLoad本身就在主队列上执行当前任务还没结束你又调了dispatch_sync往主队列提交新任务。dispatch_sync的本质是“等这个block执行完再返回”而主队列又是串行队列需要等当前任务结束才能执行下一个block。于是互相等待死锁。这个知识点几乎每年都考但很多人背了答案没理解变体。比如换一个场景dispatch_queue_t queue dispatch_queue_create(com.test.queue, DISPATCH_QUEUE_SERIAL); dispatch_async(queue, ^{ dispatch_sync(queue, ^{ NSLog(执行了吗); }); });同样会死锁。因为串行队列里往自己同步提交任务和主队列那个例子本质是一样的。我建议把GCD的“同步/异步”和“串行/并发”这两个维度彻底弄清楚同步sync只是“等待当前block执行完再继续”它不创建新线程异步async是“提交后立刻返回”由系统决定是否开新线程。而死锁的本质不是“在哪个队列”而是“任务在串行队列上互相等待”。理解到这个层面不管题目怎么换皮都能答对。4.2 线程安全与锁的选择网易的简答题里有一道“请列举iOS中常用的几种线程同步方案并说明它们的区别。”这个题我后来面试别人也喜欢问因为它能很好地反映一个人是真用过还是只背了书。我建议这样组织答案方案特点适用场景synchronized用法简单底层是递归锁但不推荐频繁调用处理单一资源的快速互斥代码量少时用NSLock标准的互斥锁使用灵活保护临界区代码NSRecursiveLock可重入锁同一个线程可以多次加锁递归方法里的加锁dispatch_semaphore信号量控制并发访问数量限制同时执行的任务数比如并发下载数dispatch_queue用串行队列做同步天然避免竞争推荐优先考虑用GCD队列替代锁os_unfair_lock底层高性能锁替代已废弃的OSSpinLock对性能有极致要求的场景这里有一个真实的坑很多人喜欢用synchronized(self)但这种方式如果被多个不同的锁复用同一个对象会导致无关代码互相阻塞。还有一个坑是dispatch_semaphore用不好会信号量泄漏造成永久卡死。我自己的习惯是能优先用串行队列就别用锁锁能少用就少用。4.3 网络层从NSURLConnection到NSURLSession2018年的笔试题里网络层已经全面转向NSURLSession了但偶尔还会出一些关于NSURLConnection的对比题。网易这套题里有一道简答题为什么苹果推荐用NSURLSession替代NSURLConnection答案可以从几个角度展开NSURLSession支持后台下载App退到后台后依然可以继续。请求配置NSURLSessionConfiguration分离可以按会话管理缓存、Cookie、证书策略。支持断点续传通过resumeData实现。可以复用连接多个请求基于同一个会话管理。任务可以取消、暂停、恢复。如果你能再补充一点NSURLSession还有didReceiveChallenge:回调可以处理HTTPS的证书校验、双向认证等场景那就更出彩了。笔试的简答题不怕你多写怕的是你只写官方文档里最表面的那一句。5. 数据结构与算法编程题的“硬碰硬”5.1 链表操作是笔试编程题的常青树网易这套笔试的编程题不算特别难但很考察基本功。链表反转这种题目几乎每次校招笔试都会出现。我当时遇到的是一个单链表的反转要求空间复杂度O(1)。这里给出迭代解法的核心代码- (ListNode *)reverseList:(ListNode *)head { ListNode *prev nil; ListNode *cur head; while (cur) { ListNode *next cur.next; // 先保存下一个节点 cur.next prev; // 反转指针 prev cur; // prev 后移 cur next; // cur 后移 } return prev; }这道题的关键在于理解指针的指向变化以及保存next防止链表断裂。我见过很多人在白板上写递归版本思路是对的但写着写着就忘了终止条件。这里再给一个递归版本参考- (ListNode *)reverseList:(ListNode *)head { // 终止条件空链表或只有一个节点 if (!head || !head.next) return head; ListNode *newHead [self reverseList:head.next]; // 把当前节点的下一个节点的 next 指回当前节点 head.next.next head; head.next nil; return newHead; }笔试改卷时阅卷人一般会先看思路对不对再看边界条件空链表、单节点、两个节点处理没处理。很多人只写了主逻辑忘了判空这道题可能只拿一半分。5.2 字符串与数组检验代码的完整性另一道编程题是字符串类的具体记不太清了但大概率是“字符串反转”“单词反转”“判断回文”这类经典题。这种题看似简单真正写的时候要特别注意中英文混合的情况算不算单词空格怎么处理标点符号算不算字符举个例子把句子里的单词逆序- (NSString *)reverseWords:(NSString *)sentence { NSArrayNSString * *words [sentence componentsSeparatedByString: ]; NSMutableArrayNSString * *reversed [NSMutableArray array]; for (NSString *word in words.reverseObjectEnumerator) { if (word.length 0) { [reversed addObject:word]; } } return [reversed componentsJoinedByString: ]; }这里我用reverseObjectEnumerator做倒序遍历同时过滤空字符串是为了处理多个连续空格的情况。这种细节虽然看起来不起眼但在实际笔试的测试用例里出题人往往会故意留几个空格连续的情况来卡人。我补上这个细节至少能保证用例通过率更高。数组的题还有一类的“两数之和”- (NSArray *)twoSum:(NSArrayNSNumber * *)nums target:(NSInteger)target { NSMutableDictionary *map [NSMutableDictionary dictionary]; for (NSInteger i 0; i nums.count; i) { NSInteger complement target - nums[i].integerValue; if (map[(complement)] ! nil) { return [([map[(complement)] integerValue]), (i)]; } map[nums[i]] (i); } return nil; }这里唯一需要注意的坑是如果数组里有重复元素用字典记录下标时要考虑覆盖问题。写之前先问清楚“数组里有没有重复元素”是笔试现场很重要的沟通能力体现。5.3 笔试现场的答题节奏与边界处理编程题不只是考“能不能写出来”更考“能不能在限定时间内写对”。我的习惯是这样读题后先在草稿纸上写测试用例包括正常情况、边界情况、异常情况。想清楚了再动笔而不是边想边写。写完代码后用一个简单用例在脑子里“跑”一遍检查循环是否越界、指针是否正确。检查对象的判空、数组的越界、整数的溢出比如NSUInteger和NSInteger混用时容易出问题。网易这类公司非常看重代码的鲁棒性宁可多写几行判空逻辑也不要留边界漏洞。6. 笔试里最容易翻车的细节与经验教训6.1 读题不仔细最冤的失分方式每次批改笔试我都发现很多人不是不会而是没看清题。有一类题目要求“写一个函数判断一个字符串是不是合法的IP地址”结果一堆人只写了IPv4的判断漏了IPv6。还有一类是要求“不得使用系统库函数”结果有人直接调了componentsSeparatedByString:或者stringByReplacingOccurrencesOfString:被扣分。我的建议是落笔之前把题目的限制条件用笔圈出来。凡是遇到“不能使用XXX”“要求时间复杂度O(n)”“注意边界条件”这类关键词都要在代码里做出对应的处理。做题快是好事但快不等于急先把题目吃透再下手比什么都强。6.2 内存管理相关的隐性扣分点虽然是笔试但代码里还是能看出一个人对内存管理的敏感度。比如block属性有没有写copydelegate属性有没有写weakNSTimer有没有在dealloc里invalidate有没有在后台线程直接操作UI这些都是平时写代码的习惯问题。笔试代码虽然不要求跑起来但阅卷人会代入“这段代码能不能上线”的标准来看。实习生的题代码规范性和安全意识是很重要的加分项。顺便提醒一个隐藏考点NSTimer的循环引用。如果你在代码里写了这样一段self.timer [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:selector(tick:) userInfo:nil repeats:YES];那么self会被timer持有而timer又被self持有形成循环引用。正确做法是在viewWillDisappear或dealloc里invalidate用weak持有timer或使用block变体timerWithTimeInterval:repeats:block:。6.3 时间分配策略别在选择题上恋战网易这套题我记得题量不算太大但编程题需要留出足够的时间。我的建议是选择题每道不要超过3分钟简答题每道控制在8到10分钟编程题留出至少30到40分钟。如果遇到一道选择题卡住了先凭第一印象选一个做个标记回头有时间再改。很多人因为在一道题上纠结太久导致后面的编程题时间不够这是最可惜的。时间分配的本质是“先易后难、先拿分再做难题”。笔试不是竞赛目的是通过不是满分。说到底准备iOS笔试题最有效的方式就是把语言基础、Runtime、内存管理、多线程这四个模块吃透再把链表、字符串、数组这几种算法题练熟剩下的就是心态和临场发挥了。我在实际带实习生的过程中发现笔试能稳定发挥的人往往不是“背题最多”的而是“思路最清晰”的。下次再有人问我怎么准备这份题我大概率会先让他把objc_msgSend、ARC、GCD、链表反转这四件事讲明白讲不透就继续回来看这篇文章。