ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

京东校招iOS笔试题复盘:高频考点、内存管理与手写编程题全解析

京东校招iOS笔试题复盘:高频考点、内存管理与手写编程题全解析 京东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会自动置nilunsafe_unretained会变成野指针。单选题很喜欢把两者放在一起混淆。这里建议记住一句话unsafe_unretained是为了兼容旧系统或追求性能才使用的现代开发基本不会主动选它新代码一律用weak。2.2 多线程并发与GCD区分异步、同步和队列类型多线程是京东笔试的另一块重头戏。单选题比较常见的是“以下哪个操作会阻塞当前线程”dispatch_sync在主队列上执行、dispatch_async到全局并发队列、dispatch_after、dispatch_sync到自定义串行队列。答案是“在主队列同步执行新块会死锁”。这道题的核心在于理解队列类型与执行方式的关系。主队列是串行队列主线程的任务都在这个队列里排队。当主线程正在执行当前代码时如果调用dispatch_sync往主队列追加一个blockdispatch_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)能兼顾可读性- (NSArrayNSNumber * *)deduplicateAndSort:(NSArrayNSNumber * *)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题真正让分数提升的却是每次笔试后的错题整理。建议每场笔试结束都像这篇文章里这样把错题按考点分类、把原理写透、把衍生问题列出来。坚持两场之后你会明显感觉到自己碰到新题时不再发怵因为万变不离其宗。笔试只是校招这条长跑中的一段但它的筛人效率极高。把每一次考试当作一次查漏补缺的机会认真复盘后续的面试表现会带给你真正的惊喜。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进