尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

搜狗2015iOS笔试题详解:内存管理、Runtime与GCD核心考点

搜狗2015iOS笔试题详解:内存管理、Runtime与GCD核心考点 搜狗2015年的笔试题放到今天来看依然很有嚼头。那会儿iOS 9刚出Swift 2.0才发布不久Objective-C依然是绝对主力ARC虽然普及但坑还没被完全踩平面试题也确实考得比现在“硬核”不少。我当时刷完这套题的第一感觉是搜狗这家公司招人不是看你会不会用API而是看你到底懂不懂这个系统在背后帮你做了什么。这篇复盘我把当时的题目按知识点重新整理了一遍每道题都给出了详细的解答思路和代码示例并补充了从现在的视角回看依然有效的工程经验希望能帮正在准备iOS面试的朋友少走弯路。1. 笔试的知识面盘点搜狗到底想考什么搜狗2015年这套iOS笔试题覆盖的范围很典型和现在国内大厂的iOS社招笔试相比它更侧重基础原理和内存细节对UI和业务层的考察反而比较少。整套题大致可以拆成下面几个模块Objective-C语言特性属性关键字、Category、Block、消息发送机制。内存管理MRC与ARC、循环引用、autorelease、weak实现原理。运行时的底层逻辑isa指针、方法查找流程、swizzling、关联对象。Foundation框架细节NSString、NSArray、NSDictionary的底层存储方式和拷贝语义。并发与多线程GCD、NSOperation、线程同步与死锁。网络层与系统架构HTTP协议状态码、TCP握手、iOS系统架构分层。设计与工程能力开放性设计题常见的有“如何设计一个图片缓存库”或“如何实现二维码扫码页”。从考点分布能看出来搜狗作为搜索引擎公司对工程师的内存敏感度和性能意识要求很高。毕竟搜索类App的数据处理量大、页面渲染路径长如果工程师对内存管理和延时优化没有概念做出来的功能很容易在低端机上卡顿甚至崩溃。另外我发现一个细节这套题里几乎没有考察Storyboard和Auto Layout说明在当年的工程环境里代码手写UI还是主流而且面试官更看重你能否讲清楚控件的生命周期和视图层级而不是拖拽UI的能力。这也给准备笔试的人提了个醒——不要只会搭界面底层原理才是拉开差距的地方。2. 几道核心基础题的深入拆解2.1 属性关键字copy与strong的区别你真的讲透了吗这道题几乎年年出现但大多数人的回答停留在“copy会拷贝一份strong是引用”这种表面层次。搜狗的题目通常会给出一段代码让你判断最终输出什么比如下面这段interface Person : NSObject property (nonatomic, copy) NSMutableString *name; end NSMutableString *str [NSMutableString stringWithString:搜狗]; Person *p [[Person alloc] init]; p.name str; [str appendString: 2015]; NSLog(%, p.name);如果对copy没有深刻理解很容易回答“搜狗 2015”。实际上p.name被声明为copy而源对象是NSMutableString所以setter内部执行的不是简单指针赋值而是发送copyWithZone:消息。NSMutableString的copy方法返回的是不可变的NSString执行后p.name指向一片新内存和原来的str没有任何关系。因此str后续的appendString:操作不会影响p.name输出应该是“搜狗”。这道题背后有两个关键点值得展开。第一为什么对NSString属性要用copy而不是strong因为外界完全有可能把一个NSMutableString赋值给这个属性如果你只做强引用外部后续修改这个可变字符串属性值就跟着变了。对使用者来说这往往是不可接受的——你拿到一个看似不可变的字符串结果它的内容被悄悄改掉排查起来非常隐蔽。用copy可以保证属性内部持有的对象永远是独立的、不可变的从源头杜绝这个问题。第二copy和mutableCopy的深浅拷贝规则。很多人死记硬背“copy返回不可变、mutableCopy返回可变”却忽略了无拷贝优化immutable对象调用copy时直接返回自身并retain。我在实际项目里遇到过因为滥用copy导致的内存上升问题一个频繁赋值的不可变NSString属性每次setter都走一次深拷贝虽然字符串内容不长但在列表滚动场景下积少成多卡顿就出现了。正确的做法是——只有当你真的需要隔离外部修改时才用copy否则用strong反而性能更好。2.2 NSString、NSArray、NSDictionary类簇与存储语义搜狗有一道经典的类簇题目问的是NSString的真实类型是什么。表面上你用abc创建了一个字符串打印class却发现并不是NSString而是一个叫__NSCFConstantString的东西。这背后就是类簇设计模式NSString只是一个公共接口真正的实现类有好几种包括__NSCFConstantString字符串常量、__NSCFString可变的桥接字符串、NSTaggedPointerString短字符串优化。理解类簇对日常开发的价值在于你不能假设一个对象的具体类型只能依赖公共接口行为。比如说NSString的isEqualToString:内部实现就考虑到了不同子类的对比——如果双方都是NSTaggedPointerString直接比较指针值就够了效率极高只有在类型不同的情况下才会走逐字符比较。同样的思路可以延伸到NSArray和NSDictionary。2015年那会儿面试官已经开始问“NSDictionary的key是用什么修饰的”这类问题了。答案是copy因为NSDictionary需要确保key在存入后不会再被修改否则hash表就乱套了。如果你用NSMutableString当key存进去之后改了内容后续再用原值去查hash值变了就永远查不到了。这也是为什么NSDictionary的key要遵循NSCopying协议。在实际工程中我建议你在设计数据模型时对集合类型的属性也要谨慎选择是copy还是strong。如果一个数组属性是暴露给外部赋值的而你不想让外部后续修改影响内部状态用copy如果这个数组是内部创建并自己维护的strong就够了没必要为每次setter付出拷贝开销。2.3 ARC时代为什么不让你手动调用dealloc搜狗的笔试题里有这么一道“在ARC环境下能否在dealloc中手动调用[super dealloc]”答案是“不能”一旦调用编译器会直接报错。这道题考察的是对ARC编译期的理解。ARC并不是垃圾回收而是编译器在编译阶段自动插入retain、release、autorelease调用。dealloc的方法族归编译器托管你只需要在dealloc中清理非OC对象如CFRelease、free和移除通知监听即可。与这道题相关的还有一个经典误区ARC环境下weak和assign都表示非持有关系有什么区别assign用于基本数据类型weak用于对象类型并且weak会在对象释放后自动置nil。为什么需要自动置nil因为野指针是最难排查的崩溃来源之一你无法确定那块内存是否已经被复用weak机制通过全局的SideTable和weak_entry_t维护了一个引用表对象释放时根据表项把所有的weak指针都置为nil从底层杜绝了野指针访问。但weak不是免费的。每次访问weak变量编译器都会通过objc_loadWeak和objc_storeWeak进行加锁、查表、retain和autorelease一系列操作。如果你的热点路径上频繁读取一个weak属性性能开销会比strong大不少。我的优化习惯是如果某个对象在生命周期内几乎不会被释放优先用strong或直接传指针weak只在有循环引用风险或需要观察对象释放时机的时候用。3. Runtime与内存管理2015年的重头戏3.1 消息机制与objc_msgSend搜狗笔试不会直接让你背objc_msgSend的流程它通常会给一段代码让你判断结果比如往一个对象发送未实现的方法会怎样或者问你对一个nil对象发消息会发生什么。前者会触发unrecognized selector崩溃但在此之前还有三次拯救机会后者在Objective-C里完全合法返回值为空不会崩溃。完整的消息查找流程是这样的获取isa指针找到对应的类对象。在类的method_list_t中查找方法找不到就去父类直到NSObject为止。如果都找不到进入动态方法解析阶段可以调用resolveInstanceMethod:动态添加实现。如果解析阶段没有处理进入快速转发流程调用forwardingTargetForSelector:把消息转发给其他对象。如果快速转发也没处理走完整转发流程通过methodSignatureForSelector:和forwardInvocation:来做自定义处理。全部没有处理最终触发doesNotRecognizeSelector:崩溃。这套机制放到2015年的题目里考察的是你能不能写出动态添加方法的代码。比如面试官让你用Runtime实现一个方法未找到时的动态转发正确的实现是重写resolveInstanceMethod:在里面用class_addMethod动态添加一个函数。我当年在笔试里写的是C函数IMP面试官追问了一句“IMP的参数类型为什么要带self和_cmd”这就要说到Objective-C方法的本质了——任何方法本质上都是一个至少带两个参数self和_cmd的C函数如果你漏掉了这两个参数方法调用时栈帧就会错位轻则数据异常重则直接崩溃。3.2 循环引用场景与解决姿势搜狗对循环引用的考察方式是给一段业务代码让你找出哪里有retain cycle并且写出至少两种解决方案。典型的场景包括Block内直接使用self而self又持有这个Block形成闭环。NSTimer的target持有self而self又持有timer。delegate没有声明为weak导致两个对象互相持有。先看Block的循环引用。在ARC下最常见的解法是__weak typeof(self) weakSelf self; self.block ^{ __strong typeof(weakSelf) strongSelf weakSelf; if (strongSelf) { [strongSelf doSomething]; } };这里有个经常被忽略的细节为什么在Block内部还要再声明一个strongSelf因为从block调用开始到实际使用weakSelf之间这个对象有可能已经被释放了如果直接给weakSelf发消息中途释放的话后面所有消息都会发到nil上行为不可预期。用__strong修饰局部变量可以在block执行期间把弱引用升级为强引用保证对象存活到block执行结束——前提是进入block时对象还活着。NSTimer的循环引用在2015年几乎是必考题。由于NSTimer的target是强引用的如果self又持有timer两者就形成了闭环。当年常规解法是在viewWillDisappear或dealloc里invalidate但timer在dealloc里invalidate本身就有问题——因为循环引用导致dealloc根本不会被调用这就成了鸡生蛋的问题。现在比较稳妥的做法是使用带block的timerWithTimeInterval:repeats:block:block内部用weakSelf。但要注意一个坑NSTimer的block参数只是为了让你方便写回调它并不会改变timer对target的持有关系block形式的timer内部依然会保留一个对block的拷贝。所以在需要兼容iOS 9及以下版本时最好使用一个中间代理对象Proxy来打破循环这个思想我在后面还会展开说。3.3 autorelease与autoreleasepool的时机搜狗那道autorelease的题我记得很清楚它问的是“在一个for循环里创建大量临时对象什么时候会被释放如果不释放会发生什么”标准答案是临时对象会被加入当前线程的autorelease pool直到当前runloop循环结束或pool被drain时统一释放。如果循环内大量创建临时对象内存占用会持续攀升峰值可能压垮内存。最直接的解决方式是用autoreleasepool包裹循环体for (NSInteger i 0; i 100000; i) { autoreleasepool { NSString *temp [NSString stringWithFormat:%ld, (long)i]; // 使用temp } }运行到循环体末尾时autoreleasepool的作用范围结束pool内的对象会被批量释放内存峰值大大降低。这个优化在图片处理、数据解析、文件读取等场景特别明显因为那些操作往往在循环里产生大量临时内容。需要特别提醒的是不要在主线程的每个方法里都包一层autoreleasepool因为主线程的runloop本身会周期性地drain pool过度使用反而会引入无谓的objc_autoreleasePoolPush和Pop开销。我见过有的项目把所有方法体都塞进autoreleasepool里结果性能反而下降了。正确场景是处理大量临时对象、自己创建的次级线程、或者极长的循环体。4. 并发与网络搜狗的高频考点4.1 GCD面试三连同步、异步、死锁GCD几乎是所有iOS笔试的保留节目搜狗2015年考过这样一道题在主线程执行以下代码会发生什么dispatch_sync(dispatch_get_main_queue(), ^{ NSLog(hello); });答案是死锁。因为dispatch_sync是同步提交任务会阻塞当前线程直到任务完成而提交的目标队列是主队列任务需要在主线程执行。当前主线程被dispatch_sync阻塞主队列的任务又等主线程空闲才能执行两者互相等待就死了。这道题延伸出来的知识点是队列和线程的关系。串行队列不意味着只有一个线程它强调的是任务按顺序执行。在dispatch_sync里如果目标队列是串行队列会不会死锁取决于当前线程和目标队列是否有关联。比如你在一个串行队列里用dispatch_sync提交任务到同一个串行队列同样会死锁。所以做题的时候先判断“当前线程是否还等待这个任务完成”再判断“目标任务是否依赖当前线程空闲”。搜狗的另一道高频题是“GCD有哪些控制并发数量的方式”。2015年还没有DispatchSemaphore的流行写法其实已经有了但考得少所以标准答案是dispatch_groupdispatch_semaphore结合使用。用信号量控制并发上限的经典写法是dispatch_semaphore_t semaphore dispatch_semaphore_create(5); for (NSInteger i 0; i 100; i) { dispatch_async(dispatch_get_global_queue(0, 0), ^{ dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); // 并发数为5 dispatch_semaphore_signal(semaphore); }); }这里的原理是信号量初始值为5每进入一个任务wait减1任务结束signal加1等于同时最多只有5个任务在执行。注意dispatch_semaphore_wait不能写在主线程里否则一旦信号量阻塞主线程UI就卡死了。4.2 网络状态码与TCP握手搜狗笔试直接出了一道“什么是HTTP 304”这个状态码在移动端优化里特别重要它表示Not Modified意思是服务器告诉客户端“你缓存的资源还是有效的可以直接用”。在移动端合理利用304可以极大减少流量消耗和加载时间。更深入的问法是客户端如何配合服务器做缓存答案涉及两个HeaderETag资源的唯一标识符客户端请求时带上If-None-Match服务器对比相同就直接返回304。Last-Modified资源最后修改时间客户端请求时带If-Modified-Since如果服务器资源没变就返回304。开发中容易踩的坑是有些服务器的实现不支持304每次都返回200全量数据这时你对图片、接口的缓存策略就要做客户端本地兜底。我通常的做法是在网络层做一个内存加磁盘的二级缓存本地缓存有效期没到就直接返回缓存不去请求服务器这样可以有效规避服务端缓存头不标准的问题。TCP三次握手也是必考。搜狗会顺着网络请求链路往下问为什么是三次而不是两次答案是为了防止失效的连接请求报文段突然又到达服务端两次握手的话服务端会建立一条无效连接而白白浪费资源。三次握手之后再问“为什么断开要四次挥手”因为TCP是全双工的两个方向各自独立关闭发送方关掉自己的发送通道后接收方可能还在发送数据所以需要一个额外的挥手包来关闭接收方的发送通道。4.3 系统架构与常用框架还有一道题是让画出iOS系统架构的分层结构。标准答案是四层从上到下分别是Cocoa Touch Layer负责UI、事件、通知、iAd等。Media Layer负责图像、音频、视频渲染。Core Services Layer负责Core Foundation、CFNetwork、Core Data等。Core OS Layer内核、文件系统、网络底层。这道题本身不难但面试官通常会追加一个问题“你写业务代码时遇到过哪些跨层调用”这是考察你是否理解系统框架的分层设计原则。正常做法是尽量在Cocoa Touch层调用Media和Core Services层的接口不要直接操作Core OS层。如果你在业务代码里用了socket去发自定义协议数据那么你已经跨到了Core OS层对应的错误处理和重连策略都要自己写复杂度会高很多。5. 设计题如何设计一个图片缓存库搜狗笔试的最后一道大题是开放性的要求设计一个图片缓存库并提供核心接口。这类题没有标准答案但面试官可以通过你的设计看出你的工程思维、边界处理能力和架构审美。当年我设计的方案后来也沿用到了实际项目里这里完整还原一下。我的设计分三层内存缓存、磁盘缓存、网络下载。读取图片时先查内存再查磁盘最后才走网络。这个顺序基于一个现实规律——访问速度从快到慢命中率从高到低。第一层内存缓存用NSCache实现第二层磁盘缓存按URL的MD5作为文件名存储第三层网络层负责下载和缓存回写。核心接口定义如下interface ImageCache : NSObject (instancetype)sharedInstance; /// 根据URL取图片优先内存其次磁盘最后网络 - (void)imageWithURL:(NSURL *)url completion:(void (^)(UIImage *image))completion; /// 预加载图片到内存 - (void)preloadImageWithURL:(NSURL *)url; /// 清理过期磁盘缓存 - (void)cleanDiskCacheWithExpireDays:(NSInteger)days; /// 获取当前缓存总大小 - (long long)totalCachedSize; end实现过程中有几个关键点值得展开内存缓存为什么用NSCache而不是NSMutableDictionary因为NSCache自带淘汰策略系统内存紧张时会自动清理部分缓存。更重要的是NSCache是线程安全的不需要额外加锁。如果用NSMutableDictionary你要自己处理多线程访问的同步、内存告警时的清理很容易出bug。磁盘缓存的清除时机要怎么设计常见做法是App启动时和收到内存警告时各清一次。清理策略可以按最近访问时间排序删除超过指定天数的文件或者当总大小超过阈值时删除最久未使用的文件。注意删除操作要放在子线程执行避免阻塞UI。网络下载的并发控制也很关键。同一张图片可能同时被多个控件请求如果每个请求都发起一次下载就会造成流量浪费。解决方案是维护一个NSMutableDictionary把URL映射到一组回调block第一次请求时创建下载任务后续相同URL的请求只追加回调到数组里。下载完成时统一遍历回调然后从字典中移除该URL。这类设计题的答题思路概括成一句话就是先分层再定义接口然后对每个核心方法给出边界处理方案最后补充性能优化点。面试官不在乎你写的每个方法都实现但很在乎你能不能把“缓存一致性、线程安全、淘汰策略”这些点讲清楚。6. 笔试题背后的工程启示与备考建议这套搜狗2015年iOS笔试题放到2025年的今天虽然部分考点的表述变了但底层原理依然是面试考察的核心。比如现在的面试可能会问“iOS 13之后的SceneDelegate生命周期”“Swift Concurrency的Actor模型”但内存管理、消息机制、线程安全的底层逻辑并没有本质变化。你在准备笔试时与其去背大量的API拼写不如把下面几个能力练扎实第一多写小实验验证结论。很多读者看到“copy修饰NSMutableString的属性再修改原对象不会影响属性值”这种结论可能半信半疑。我建议你新建一个Playground或命令行工程花两分钟跑一遍亲眼看到输出再下结论。只有自己验证过的知识面试时才能脱口而出。第二建立“原理到现象”的反射思维。遇到一个bug不要只搜索解决方案先问自己“这个现象说明了什么底层机制”。比如某个对象在释放后访问出现野指针能联想到weak就是用来规避这个问题的进一步能想到__unsafe_unretained为什么危险。这种思维模式比背一百道题都管用。第三如果时间有限优先看Runloop和内存管理这两个专题。因为我观察到的结果是大多数iOS笔试拉开差距的题目都集中在这两块记忆性的API调用大家都会但能把autoreleasepool的释放时机、闭环打破、消息转发这些细节讲出层次感的人还是少数。从搜狗这套题上我也能感觉到2015年移动端招聘的一个趋势企业对工程师的源码阅读能力开始有了期待。笔试里提到的消息转发、isa指针、类簇这些概念如果你只看过文档、没读过Runtime源码的话很容易落入“知其然而不知其所以然”的陷阱。所以如果你想在面试中拿高分我强烈建议花时间精读一遍objc4源码中objc_msgSend、runtime、ARC相关的核心实现哪怕只读懂了三分之一面试表现都会有质的提升。最后再说一点实战层面的经验。当年我复习时把每道错题都整理成一个小的验证工程用Xcode跑通后记录现象。后来发现这个方法对我的帮助远超预期——因为很多面试题不是靠背能解决的你必须亲手复现才能真正理解边界情况。比如在ARC环境下写一个会循环引用的demo然后通过Instruments的Leaks工具看到内存泄漏的橙色标记这种直观的冲击比“循环引用会泄漏内存”这句话有用得多。你如果准备面试不妨也试试这种“亲手做实验”的复习方式。
返回列表