京东2019校招笔试的iOS开发工程师岗位,算是我秋招里印象很深的一场。当年刷了不少题,但到了限定时间的线上笔试里,脑子照样容易卡壳。后来我把整套题复盘了一遍,把考点、陷阱、编程题思路都整理进了笔记。今天把这些内容完整梳理出来,涵盖题型分布、高频考点、典型编程题手写方案,还有从笔试延伸到面试的备考思路,希望能给准备校招的同学一个比较系统的参考。
1. 京东2019校招iOS笔试题整体复盘:题型与考点分布
1.1 试卷结构、时间分配与答题策略
2019京东校招的iOS方向笔试采用线上答题,整体风格偏“稳中有细”。整张卷子由单选题、多选题、简答题和编程题构成,考试时长在120分钟左右。选择题的数量相当大,我印象里单选加多选有三十道上下,涵盖OC语言、Foundation、UIKit、内存管理、多线程、Runloop、Runtime等常规iOS知识面。简答题一般是两道,需要用文字完整描述原理,比如“解释Block的底层结构”或“简述App启动过程”。编程题通常是一道,也可能分成两个小题,用C或者OC上手写,考察数据结构和字符串处理这类基本功。
这里先说一下答题节奏,这是很多人挂在第一关的地方。选择题占的分值大,但因为题目范围广,很容易在几道偏题、怪题上消耗太多时间。我采用的策略是先按第一直觉快速作答,拿不准的先标记跳过,等把整卷能拿的分都拿到之后,再回头推敲。线上笔试一般不允许回头改每一道题这种情况较少见,多数平台支持前后题切换,但为了保险,我建议按顺序答题时也要控制单题时间:单选题不要超过90秒,多选题不要超过120秒。一旦超时,先选一个把握最大的答案,标记稍后回头看。
多选题值得单独提醒:京东这类大厂的校招笔试,多选题计分普遍是“少选得部分分,错选不得分”。也就是说,不确定的选项宁可少选也不要硬凑。很多同学看到选项里有一个自己认识的名词就勾上去,结果整题零分,非常可惜。我在复盘时发现自己也犯过这个毛病——一道关于KVO的题,判断“KVO是否支持对数组元素进行监听”时,我多勾了一个“支持”选项,而这个恰恰是常见的理解误区,白白丢了分。
1.2 考点分布图:从语言基础到系统底层
通过复盘整套卷子,能看出题目命制的逻辑非常清晰,大致按“基础语言与内存管理、系统机制与底层原理、工程化与框架应用”三个层次铺开。
第一层次是基础语言能力,占比约六成。重点集中在OC的对象模型、内存管理原则、Block特性、类与分类、常用容器类的底层行为。这类题目看起来都是“常识”,但往往会在细节上做文章。比如“NSMutableArray用copy修饰会怎样”这种题,实际上考的是可变对象与拷贝语义之间的关系,一字之差就决定了对错。
第二层次是系统机制,占比约三成。包括Runloop的运作逻辑、多线程与GCD、KVO/KVC、NSNotificationCenter、App生命周期等。这一部分的难度明显上升,因为不再问“是什么”,而是让你判断“在某种场景下会发生什么”。比如“在子线程中创建并addObserver后,主线程post通知,观察者能不能收到回调”就是典型的场景判断题,只背定义的人很容易出错。
第三层次是底层原理与工程化,占比约一成,但往往以简答题或编程题形式出现,是区分度最高的部分。比如Runtime消息发送流程、方法交换的正确使用方式、App启动的时间与优化手段、HTTPS握手过程等。这类题不要求你写出一个可运行的框架,但要求你能讲清楚全过程,最好能配合代码说明。
清楚了考点分布,再去看自己复习的进度条,能省很多力。我当时有针对地把知识点按这三个层次做了“树状图”,每复习完一块就在上面打钩,考前的焦虑感会小很多。
2. 高频基础型真题拆解与避坑详解
2.1 Block循环引用与内存管理:笔试常客
Block循环引用是iOS笔试选择题里出现频率极高的话题,京东这套题里也有一道非常典型的多选题,问“以下哪些写法会造成循环引用”。选项分别给了:在ViewController里用self持有block、block内部使用weakSelf、block内部使用strongSelf、在block里直接访问成员变量。标准答案应当是“用self持有block算一个,直接访问成员变量也会造成隐患”,有不少人漏选了最后一项。
这里把原理说清楚。循环引用成立的前提是对象A持有对象B,同时对象B又持有对象A,使得彼此的引用计数都无法归零。在block场景下,如果block被self强持有,而block内部又捕获了self,就形成了self -> block -> self的环。用weakSelf打破环是常规解法,但在异步任务中只使用weakSelf也可能有隐患——如果self在block执行前就被释放了,后续代码就会拿到nil。所以很多团队的做法是“外weak内strong”:block外部先用weak捕获,进入block后立刻将其转成strongSelf,保证整个block执行期间对象不会中途消失。
再说一个非常容易被忽略的细节:在block里直接访问成员变量(比如_name),编译器会直接捕获self,等价于强引用self,一样会造成循环引用。笔试时这个选项的迷惑性极强,因为看起来没有写self,但实际上问题更隐蔽。解决方式是访问成员变量时改成显式使用self->_name,配合weakSelf一起使用,这样一眼就能看出捕获关系。
关于内存管理的另一个高频题是“ARC下strong、weak、unsafe_unretained的区别”。weak和unsafe_unretained差别就在于对象释放后:weak会自动置nil,unsafe_unretained会变成野指针。单选题很喜欢把两者放在一起混淆。这里建议记住一句话:unsafe_unretained是为了兼容旧系统或追求性能才使用的,现代开发基本不会主动选它,新代码一律用weak。
2.2 多线程并发与GCD:区分异步、同步和队列类型
多线程是京东笔试的另一块重头戏。单选题比较常见的是“以下哪个操作会阻塞当前线程”:dispatch_sync在主队列上执行、dispatch_async到全局并发队列、dispatch_after、dispatch_sync到自定义串行队列。答案是“在主队列同步执行新块会死锁”。
这道题的核心在于理解队列类型与执行方式的关系。主队列是串行队列,主线程的任务都在这个队列里排队。当主线程正在执行当前代码时,如果调用dispatch_sync往主队列追加一个block,dispatch_sync会等待block执行完毕才返回,但主队列的调度又要求主线程来执行block,而主线程此刻正卡在等待中,于是形成了互相等待,也就是死锁。这个现象刷题的同学一般都知道,但要能解释清楚“为什么死锁”,才是笔试真正想考的。
还有一个容易混的点是栅栏函数dispatch_barrier_async的使用场景。并发队列里多个读操作可以并行,但写操作必须与其他读写操作分开。栅栏函数就是用来在并发队列里插入一道“墙”,让它之前的任务全部执行完后,再执行栅栏block,之后再继续后面的并发任务。这里有一个小陷阱:如果栅栏函数用在全局并发队列上,效果是无效的,因为全局并发队列是整个系统共享的,不能单独设置栅栏。必须使用自己创建的并发队列才有意义。这个点完全可以作为一道判断题出现,我们复习时一定要把“队列来源”考虑进去。
2.3 Runloop、KVO与通知机制:场景判断是关键
Runloop相关的题在整套卷子里出现的概率极高。常见问题包括:Runloop有哪些运行模式、NSTimer在滑动列表时为什么暂停、如何让NSTimer在滑动时不暂停。
NSTimer暂停的根源在于“滑动列表时主线程Runloop切换到了UITrackingRunLoopMode,默认的Timer注册在NSDefaultRunLoopMode下,所以会被临时挂起”。解决方式是把timer添加到NSRunLoopCommonModes下。简单来说,CommonModes是一个模式集合,系统会将常用的一些模式(包括默认模式和追踪模式)纳入其中,Timer加到CommonModes后,无论是普通状态还是滑动状态都能被触发。这个知识点笔试时更多以判断或选择形式出现,理解了Runloop的“模式切换”概念就不容易丢分。
KVO的高频考点集中在触发机制和线程特性上。KVO是基于Runtime动态生成子类并重写setter方法实现的,这个基本大家都知道。但多选题往往会问“KVO在什么情况下不会被触发”,比如直接对成员变量赋值、对数组进行addObject:等。事实上,KVO无法自动监听直接对数组元素的增删操作,因为数组的addObject:并不会调用对象的setter方法,需要使用mutableArrayValueForKey:来获取代理数组,或者在操作前后手动调用willChangeValueForKey和didChangeValueForKey。这些“手动触发”的细节很值得在复习时写一个小demo验证一下,否则选择题里遇到类似描述很容易踩空。
通知机制则会考察同步还是异步。NSNotificationCenter发送通知时,默认是同步执行的,也就是说postNotificationName:方法会阻塞当前线程,直到所有观察者的处理方法都执行完毕才返回。这一点和很多人的直觉相反。如果通知在子线程发送,观察者也会在子线程执行回调,并不会自动切换到主线程。这引出了另一个笔试高频点:收到通知后如果需要更新UI,必须手动切换到主线程。处理方式无非是dispatch_async到主队列,或者用系统提供的某些包装方法。笔试的简答或者选择都爱在这里设坑。
3. 编程题实战:题目还原与手写方案
3.1 链表反转:迭代与递归两种写法都不能含糊
京东笔试的编程题通常不需要写复杂算法,但很看重代码规范性和边界处理。我印象里出现过一个链表反转的问题,要求使用OC或C语言实现,并在函数入口处格外小心参数校验。
先说迭代写法。核心思路是用三个指针:pre指向前一个节点,cur指向当前节点,next先保存cur的下一个节点避免断链。每次循环里先把next存下来,再让cur的next指回pre,然后把pre和cur整体后移。循环结束后,pre正好指向原链表的尾节点,也就是新链表的头节点。
@interface ListNode : NSObject @property (nonatomic, strong) ListNode *next; @property (nonatomic, assign) NSInteger value; @end + (ListNode *)reverseList:(ListNode *)head { if (!head || !head.next) { return head; } ListNode *pre = nil; ListNode *cur = head; while (cur) { ListNode *next = cur.next; cur.next = pre; pre = cur; cur = next; } return pre; }递归写法稍微绕一点,但代码更短。递归的终止条件是当前节点或下一个节点为空。每次递归时,假设后面的链表已经反转完毕,然后把当前节点的下一节点的next指向当前节点,最后把当前节点的next置空,防止形成环。
+ (ListNode *)reverseListRecursive:(ListNode *)head { if (!head || !head.next) { return head; } ListNode *newHead = [self reverseListRecursive:head.next]; head.next.next = head; head.next = nil; return newHead; }两种写法里,迭代方式在笔试机上测试时更稳,不容易因为递归过深而栈溢出;递归写法胜在简洁,但在面试讲思路时更容易体现对链表指针操作的理解。我的建议是都写一遍,务必做到没有任何编译警告。另外笔试环境一般不会提供自动补全,链表的创建、遍历、打印这些基础方法也要能够直接手写出来,否则连测试用例都没法构造。
3.2 字符串与数组综合题:去重、排序与边界处理
除了链表,数组和字符串处理也是笔试编程题的重灾区。比较典型的一道题是“对一个整数数组去重,并按从大到小输出”。常规解法是先排序再去重,能够保证稳定性;也可以用NSOrderedSet去重,但这样无法自定义排序顺序,需要再配合sort方法。
从笔试阅卷的角度看,排序复杂度是次要的,真正看重的是两个点:第一,是否考虑到空数组和单元素数组的边界;第二,是否用了合适的数据结构。这里给出一个偏工程化的写法,时间复杂度为O(n log n),能兼顾可读性:
- (NSArray<NSNumber *> *)deduplicateAndSort:(NSArray<NSNumber *> *)originArray { if (originArray.count <= 1) { return originArray; } NSSet *set = [NSSet setWithArray:originArray]; NSArray *uniqueArray = [set allObjects]; NSArray *sortedArray = [uniqueArray sortedArrayUsingComparator:^NSComparisonResult(NSNumber *obj1, NSNumber *obj2) { return obj1.integerValue < obj2.integerValue ? NSOrderedDescending : NSOrderedAscending; }]; return sortedArray; }字符串类的编程题里,出现概率很高的是“反转字符串中的单词顺序”和“判断回文串”。这里不要为了炫技直接调用系统的componentsSeparatedByString,因为笔试阅卷更希望你展示对字符串索引遍历和边界判断的基本功。比如判断回文串时,一定要先处理好大小写和特殊字符,否则后面边界case会挂。注意这类题通常不是难在算法本身,而是难在“在限定时间、无IDE提示、还要手写注释”的条件下把代码写干净。
3.3 手写代码的评判标准与常见失分点
有些同学觉得自己算法思路正确,代码也能运行,但最终编程题得分不高。复盘后发现,失分点往往不是解题思路,而是代码风格和测试意识。
第一类失分是参数校验缺失。题目给的链表头节点、数组、字符串都可能为空,函数开头不做空判断,后面必崩。第二类是变量命名混乱。笔试阅卷大概率是人工看,代码里出现a、b、c、tmp1这种命名,即使逻辑正确也非常吃亏。规范命名本身就说明你具备工程意识。第三类是缺少测试用例说明。代码写完后,我会在注释里简单补几个测试样例和期望输出,比如“输入@[@3,@1,@2,@3],输出@[@3,@2,@1]”,这样阅卷人能一眼看懂我验证过边界。
如果你习惯了编译器自动补全,平时练习时一定要刻意切换到“白板模式”:打开记事本或Xcode里的注释区写代码,不依赖自动提示。能默写出常用API,像数组排序、字符串截取、字典遍历这些方法,笔试现场的紧张感会大幅降低。
4. 进阶与系统底层题目解析:从原理到实战
4.1 Runtime消息发送机制与Method Swizzling陷阱
京东笔试的简答题里,Runtime相关的题目出镜率极高。一般会让你简述“OC的消息发送机制”,或者给出一个Method Swizzling的代码片段,让你指出其中的问题。
先说消息发送机制的完整流程。在OC中,调用[obj doSomething]并不会直接找到函数地址执行,而是翻译成objc_msgSend(obj, @selector(doSomething))。运行时系统会根据对象的isa找到所属类,再通过类对象的方法列表查找对应的IMP。如果当前类没有找到,就去父类找,一直到NSObject。如果最终找不到,就会进入动态方法解析阶段,调用resolveInstanceMethod:和resolveClassMethod:,给开发者一个“动态添加方法”的机会。如果再没处理,就会走消息转发流程,依次触发forwardingTargetForSelector:和methodSignatureForSelector:、forwardInvocation:。这就是OC的“消息三救”。
笔试简答时,最好把上述流程按顺序写出来,并注明每个阶段的用途。很多人只写了“通过isa找IMP”,没有补充动态方法解析、快速转发和完整转发,这就是丢分点。
Method Swizzling是另一个高频考点。它利用Runtime在运行时交换两个方法的IMP。一个经典坑是,在分类的load方法里做交换,如果没有使用dispatch_once,会导致多次交换,最终方法调用互相错乱。另一个坑是交换后调用原方法时,不能用[self originalMethod]这种写法,因为IMP已经交换过,会产生递归调用。正确写法是保存一份原IMP,在swizzled方法里调用保存的那个IMP。这些细节在笔试的“对错判断”里特别容易被放大。
4.2 App启动过程与优化方向
简答或论述题里,京东还考过“App启动流程”和“启动时间优化”。说一下我总结的标准答案思路。
冷启动过程大致分为:系统加载Mach-O二进制文件与动态库,执行各类初始化,然后调用main函数,创建UIApplication和AppDelegate,最后启动Runloop并加载根视图。在iOS 13之后,AppDelegate的生命周期职责被拆分给了SceneDelegate,但这个题放到2019年来看,还是以旧的AppDelegate为主,不过可以从新架构角度补充说明,会显得你的知识面更及时。
笔试答题时可以分层去写:pre-main阶段做了什么、main函数之后做了什么、每个阶段可能的耗时点在哪。启动优化的方向就对应着展开:减少动态库加载数量、二进制重排减少虚拟内存page-in、冷启动时不执行多余的网络请求和数据库初始化、延迟加载部分非首屏业务等。这样的答法兼具广度和深度,阅卷人容易给高分。
4.3 网络层、HTTPS与开发调试工具实操
网络层在笔试题中更多以选择题形式出现,比如HTTPS握手时客户端如何验证服务器证书、为什么需要CA证书链、对称加密和非对称加密在握手过程中的分工。这类题难点不在算法细节,而在“协议演进逻辑”:非对称加密性能差但安全,用来交换会话密钥;对称加密性能好,用来加密后面的应用数据。
为了应对这类题目,我在复习时用Charles实际抓过一次自己开发App的HTTPS请求。通过安装调试证书,能够查看请求头、响应体、状态码以及耗时数据,排查“接口为什么没数据”“为什么慢”这类问题。注意,这里的抓包只用于本地开发调试自己写的App和接口,目的就是定位网络问题本身。掌握这个流程后,笔试里只要考到“客户端如何确认连接可信”,你就很容易联想到证书校验和中间人攻击的关系,答题点自然就全了。
5. 工程化与上架链路考点延伸
5.1 证书、签名、描述文件与真机调试
2019年的笔试里,工程化的题目比例不高,但每年都在增加。我印象里有一道多选题问的是“iOS真机调试需要什么”,选项里出现了开发者证书、描述文件、Apple ID、网络权限等。答案是证书和描述文件,但很多同学会把Apple ID也算进去,实际上普通Apple ID只能用于模拟器编译,真机运行必须要签名。
这里把证书和描述文件的关系说清楚。开发者证书本质是一种数字身份,用它来“签署”你的App和调试设备。描述文件则是一个配置文件,里面记录了App的App ID、可用的设备UDID和所需权限。调试时,Xcode会自动生成“开发描述文件”,并把设备UDID加入其中。上架时则需要“发布描述文件”,并使用分发证书签名。所有与签名有关的操作,本质上都在解决两个问题:确认开发者身份合法、确认设备被授权。
与此相关的上架流程,笔试可能考到“App Store审核前需要准备哪些材料”或“App icon和截图规模要求”。这类题记住大方向即可:需要软件著作权(部分地区公司主体)、分级问卷、隐私政策,以及最新的iTC后台信息填写。iOS上架周期一般要预留至少一周,如果遇到审核被拒,还需要额外的沟通时间,这些经验性的内容在面试聊项目经历时反而更容易加分。
5.2 组件化、UIStackView与iOS布局相关
组件化是笔试和面试交界处的高频话题。京东的工程体量非常大,协同开发多、模块多,面试官特别看重你对“如何分模块、模块之间如何通信”的理解。笔试可能会以简答题形式出现:“在一个大型App中,如何做组件化拆分?”常见方案有基于URL路由解耦、基于Target-Action的中间件,以及protocol-class注册方式。
从架构演进角度看,组件化的收益在于并行开发和独立测试,代价是维护成本高、组件间依赖关系复杂。作答时需要把这两方面都说清楚,只说“能提高效率”反而显得浅。另外,组件化往往和CocoaPods私有库绑定在一起,复习时顺手把CocoaPods创建私有库的流程(pod lib create、pod spec文件、repo push)过一遍,笔试答案里如果能提到这些实操细节,会让人感觉你是真做过。
布局方面的考点也有可能出现,尤其是UIStackView已经普及的年代。UIStackView是一种容器视图,可以自动管理子视图的排列,但它毕竟是以AutoLayout为底层支撑的,不能完全替代AutoLayout,也无法处理“需要动态计算宽度”这类复杂场景。我在平时开发里会用UIStackView简化均匀排列的一组控件,减少手写约束的数量;但遇到需要不同比例宽度的布局,仍会用更精确的AutoLayout约束。笔试若考布局题,重点通常不是用法,而是“什么场景下UIStackView不合适”,多思考它的局限性比背API更有用。
5.3 蓝牙、自动化与其他iOS能力拓展
搜索词里出现过蓝牙相关的内容,说明现在的面试和笔试已经逐渐覆盖到CoreBluetooth这类系统能力。如果在考试或面试里考到,最基本的要能区分两个类:CBCentralManager负责扫描连接外围设备,CBPeripheralManager负责让本机作为外围设备被连接。还要知道蓝牙状态分为“系统级蓝牙开关”和“App级权限状态”,两者相互独立,开发时应当分别监听。笔试选择题如果考到“扫描到设备后该使用哪个类的方法获取广播数据”,答案也会落到CBAdvertisementData相关的参数上。
iOS自动化在笔试简答中偶尔出现,问法一般是“如何提高iOS的自动化测试效率”。常见的作答方向是XCTest UI测试、fastlane自动化打包上传、以及用GitLab CI/CD做持续集成。我建议在这个方向上花半天时间跑通一个真实流程:写一个简单的UI测试case,再写一个fastlane lane完成build和upload,把这些脚本截图放在自己的博客或者笔记里,后续跟面试官聊工程化话题时就不会缺素材。
6. 备考通关清单与个人经验总结
6.1 校招笔试的时间规划与资源推荐
如果你从现在开始准备,比较合理的节奏是提前三个月进入状态。第一个月主攻OC语言和内存管理,把《Objective-C高级编程:iOS与OS X多线程和内存管理》啃完,这本书虽然薄,但含金量极高,Block和GCD部分都是笔试硬考点。第二个月主攻系统机制,Runloop、Runtime、KVO、多线程、启动过程这些要能对着白板脱稿讲解。第三个月进入刷题模式,重点刷LeetCode上的链表和字符串题,同时每天整理两三道iOS高频考点题,用自己的话输出成笔记。
时间充裕的话,我建议把App启动优化、组件化、CocoaPods私有库、证书签名流程都亲手操作过一遍。笔试只考记忆的部分靠背,但只要是“场景题”,实操过一次和没实操过,答题时的语感完全不一样。
6.2 从笔试到面试的关键衔接
笔试只是第一关,很多考点会原封不动出现在面试里。比如笔试里考了Runloop模式,面试官可能会追问“如何在滑动时保持定时器触发”“Runloop和线程是什么关系”。所以笔试复盘时不要只对答案,最好把每道错题背后的知识点扩充成面试问答的口头稿。
这里分享一个方法:准备一个“问题卡片”文档,把每道题拆成三栏——题目、我的答案、扩展示例。比如有一道关于KVO的题,我的卡片上会补充“KVO在iOS 11之前能不能监听数组元素变化”“使用KVO后对象什么时候dealloc”这类衍生问题。面试当天只看这个文档,状态好的时候能覆盖到很多原本没准备到的细节。
6.3 我个人考后复盘中的三个体会
第一,基础永远比偏门重要。我当年在笔试前花了很多时间研究一些冷门API,结果卷子里出的全是Block循环引用、Runloop模式、消息转发这一类最核心的问题。把基础原理吃透,比知道一百个API的用法更管用。
第二,手写代码必须练到“闭上眼也能写”的程度。笔试现场的紧张感、线上编辑器的卡顿、时间倒计时的压迫感,都会让手速下降不少。只有把链表反转、字符串去重、数组排序这些高频模板练到肌肉记忆,才有可能留出时间检查边界。
第三,错题复盘比刷题数量更重要。我刷了大概六百道LeetCode题,真正让分数提升的却是每次笔试后的错题整理。建议每场笔试结束,都像这篇文章里这样,把错题按考点分类、把原理写透、把衍生问题列出来。坚持两场之后,你会明显感觉到自己碰到新题时不再发怵,因为万变不离其宗。
笔试只是校招这条长跑中的一段,但它的筛人效率极高。把每一次考试当作一次查漏补缺的机会,认真复盘,后续的面试表现会带给你真正的惊喜。