├── README.md ├── 系统框架-设计模式.md ├── 语言工具-Xcode使用.md ├── 经验之谈-架构的选择.md ├── 经验之谈- App的测试和上架.md ├── 代码考查到offer的比较和选择.md ├── 语言工具-Swift.md ├── 语言工具-Objective-C.md ├── 系统框架-网络、推送与数据处理.md ├── 语言工具-Swift vs. Objective-C.md ├── 简历的准备到面试流程.md ├── 经验之谈-面向协议的编程.md ├── 系统框架-UIScrollView及其子类.md ├── 系统框架-UIKit.md ├── 算法基础6-7节.md ├── 系统框架-并发编程 ├── 算法基础4-5节.md ├── 算法基础1-3节.md └── LICENSE /README.md: -------------------------------------------------------------------------------- 1 | # iOS面试大全从面试的准备和流程到算法和数据结构以及计算机基础知识 2 | 3 | 读到这里,本系列的内容也就告一段落了。全系列主要涉及三大部分内容:面试的准备和流程;算法和数据结构相关的计算机基础知识;以及 iOS 相关的面试题问答。至此,你已经具备了系统的 iOS 知识体系,对 iOS 面试也有了足够的认识和理解。 4 | # 面试策略 5 | * [简历的准备到面试流程](https://github.com/iOS-Mayday/iOS-Interview-Strategy/blob/main/%E7%AE%80%E5%8E%86%E7%9A%84%E5%87%86%E5%A4%87%E5%88%B0%E9%9D%A2%E8%AF%95%E6%B5%81%E7%A8%8B.md) 6 | * [代码考查到offer的比较和选择](https://github.com/iOS-Mayday/iOS-Interview-Strategy/blob/main/%E4%BB%A3%E7%A0%81%E8%80%83%E6%9F%A5%E5%88%B0offer%E7%9A%84%E6%AF%94%E8%BE%83%E5%92%8C%E9%80%89%E6%8B%A9.md) 7 | 8 | # 算法基础 9 | * [算法基础1-3节(基本数据结构、链表、栈和队列)](https://github.com/iOS-Mayday/iOS-Interview-Strategy/blob/main/%E7%AE%97%E6%B3%95%E5%9F%BA%E7%A1%801-3%E8%8A%82.md) 10 | * [算法基础4-5节(二叉树、排序和搜索)](https://github.com/iOS-Mayday/iOS-Interview-Strategy/blob/main/%E7%AE%97%E6%B3%95%E5%9F%BA%E7%A1%804-5%E8%8A%82.md) 11 | * [算法基础6-7节( 深度优先和广度优先、动态规划)](https://github.com/iOS-Mayday/iOS-Interview-Strategy/blob/main/%E7%AE%97%E6%B3%95%E5%9F%BA%E7%A1%806-7%E8%8A%82.md) 12 | 13 | # 语言工具 14 | * [Swift](https://github.com/iOS-Mayday/iOS-Interview-Strategy/blob/main/%E8%AF%AD%E8%A8%80%E5%B7%A5%E5%85%B7-Swift.md) 15 | * [Objective-C](https://github.com/iOS-Mayday/iOS-Interview-Strategy/blob/main/%E8%AF%AD%E8%A8%80%E5%B7%A5%E5%85%B7-Objective-C.md) 16 | * [Swift vs. Objective-C](https://github.com/iOS-Mayday/iOS-Interview-Strategy/blob/main/%E8%AF%AD%E8%A8%80%E5%B7%A5%E5%85%B7-Swift%20vs.%20Objective-C.md) 17 | * [Xcode使用](https://github.com/iOS-Mayday/iOS-Interview-Strategy/blob/main/%E8%AF%AD%E8%A8%80%E5%B7%A5%E5%85%B7-Xcode%E4%BD%BF%E7%94%A8.md) 18 | 19 | # 系统框架 20 | * [UIKit](https://github.com/iOS-Mayday/iOS-Interview-Strategy/blob/main/%E7%B3%BB%E7%BB%9F%E6%A1%86%E6%9E%B6-UIKit.md) 21 | * [UIScrollView及其子类](https://github.com/iOS-Mayday/iOS-Interview-Strategy/blob/main/%E7%B3%BB%E7%BB%9F%E6%A1%86%E6%9E%B6-UIScrollView%E5%8F%8A%E5%85%B6%E5%AD%90%E7%B1%BB.md) 22 | * [网络、推送与数据处理](https://github.com/iOS-Mayday/iOS-Interview-Strategy/blob/main/%E7%B3%BB%E7%BB%9F%E6%A1%86%E6%9E%B6-%E7%BD%91%E7%BB%9C%E3%80%81%E6%8E%A8%E9%80%81%E4%B8%8E%E6%95%B0%E6%8D%AE%E5%A4%84%E7%90%86.md) 23 | * [并发编程](https://github.com/iOS-Mayday/iOS-Interview-Strategy/blob/main/%E7%B3%BB%E7%BB%9F%E6%A1%86%E6%9E%B6-%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B) 24 | * [设计模式](https://github.com/iOS-Mayday/iOS-Interview-Strategy/blob/main/%E7%B3%BB%E7%BB%9F%E6%A1%86%E6%9E%B6-%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F.md) 25 | 26 | # 经验之谈 27 | * [架构的选择](https://github.com/iOS-Mayday/iOS-Interview-Strategy/blob/main/%E7%BB%8F%E9%AA%8C%E4%B9%8B%E8%B0%88-%E6%9E%B6%E6%9E%84%E7%9A%84%E9%80%89%E6%8B%A9.md) 28 | * [面向协议的编程](https://github.com/iOS-Mayday/iOS-Interview-Strategy/blob/main/%E7%BB%8F%E9%AA%8C%E4%B9%8B%E8%B0%88-%E9%9D%A2%E5%90%91%E5%8D%8F%E8%AE%AE%E7%9A%84%E7%BC%96%E7%A8%8B.md) 29 | * [App的测试和上架](https://github.com/iOS-Mayday/iOS-Interview-Strategy/blob/main/%E7%BB%8F%E9%AA%8C%E4%B9%8B%E8%B0%88-%20App%E7%9A%84%E6%B5%8B%E8%AF%95%E5%92%8C%E4%B8%8A%E6%9E%B6.md) 30 | 31 | 但是,我们并不希望读者死记硬背书中的答案。因为本书并不是解决一切 iOS 问题的灵丹妙药,也不能保证读完此书的读者就一定能通过面试:一方面,iOS 开发本身博大精深。有很多开发中的经验和问题并不适合放在面试中进行提问。另一方面,iOS 相关的技术日新月异。每年苹果都会推出新的编程框架和思路,随之而来的是过去成熟的解决方案被淘汰,在面试中则是新的面试题或者旧有答案甚至是思路的不适用。 32 | 33 | 所以,我们只是希望读者能够凭借此书而一窥 iOS 开发的整个世界,能够学会在开发中不断追问的好奇态度。对于面试中的问题,能够举一反三加以理解,并应用在日常开发中;而在日常开发中,我们也希望读者能够知其所以然,对开发的经验做到提炼和反思。 34 | 35 | 36 | # 2021年最新iOS进阶技术视频+BAT面试专题PDF+学习路线图 37 | 38 | 给大家分享的资料包括 iOS进阶技术视频+面试文档+学习路线图,希望能帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习,也是可以分享给身边好友一起学习的! 39 | # 进阶视频 40 | ![视频](https://user-images.githubusercontent.com/70994474/117925973-97b2ae00-b32a-11eb-8140-50fbca5def2f.png) 41 | 资料获取方式: 加2000人BAT技术交流群;点击群号:[1012951431](https://jq.qq.com/?_wv=1027&k=ZFrhuFFf), 请备注得知渠道(如知乎、简书、CSDN等),联系群主或者管理员,群主管理员上班时间(下午1点到晚上10点),前往免费领取也可以直接加我QQ:**3432968801** 更方便第一时间领取哦 ] 42 | # BAT面试题 43 | ![面试题](https://user-images.githubusercontent.com/70994474/117925960-8ff30980-b32a-11eb-8064-77442c161386.png) 44 | 资料获取方式: 加2000人BAT技术交流群;点击群号:[1012951431](https://jq.qq.com/?_wv=1027&k=ZFrhuFFf), 请备注得知渠道(如知乎、简书、CSDN等),联系群主或者管理员,群主管理员上班时间(下午1点到晚上10点),前往免费领取也可以直接加我QQ:**3432968801** 更方便第一时间领取哦 45 | -------------------------------------------------------------------------------- /系统框架-设计模式.md: -------------------------------------------------------------------------------- 1 | 很多刚入门的 iOS 开发者经过短期训练,可以熟练的调用各种 API。这时候写一个 tableView、实现一个小动画、独立完成一个交互的功能已经不在话下,但同时 iOS 开发者也就到了技术上的第一个瓶颈——即拥有独立开发一个功能的水平,却似乎并未达到独立开发一个 App 的水准;看似什么都会做、什么都能做,却似乎总是不能在第一时间想到最佳方案。功能是完成了,然而效率上不是很高,代码逻辑在日后也可能需要返工重构。 2 | 3 | ![](https://upload-images.jianshu.io/upload_images/22877992-a06c0f0b7aa121cd.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 4 | 5 | 我个人认为,突破这个瓶颈的捷径就是掌握设计模式。设计模式是前人总结的、面对开发中常见问题的解决方案——它们行之有效、便于理解、适合举一反三。简单点说,设计模式就是开发中的套路和模板。熟练掌握设计模式,可以提高开发效率,节省开发时间。这样,我们就可以站在前人的肩膀上,去研究解决那些具有挑战性和未曾解决过的问题。 6 | 7 | ### 1.说说你平常开发中用到的设计模式? 8 | 9 | **关键词:#创建型 #结构型 #行为型** 10 | 11 | iOS 开发中的设计模式有很多,一般最常见的有这 7 种: 12 | 13 | * **MVC:**是应用的一种基本架构,主要目的是将不同的代码归于不同的模块,做到低耦合、代码分配合理、易于扩展维护的目的。 14 | 15 | * **装饰模式(Decorator):**它可以在不修改原代码的机场上进行拓展。注意它与继承最大的区别是:继承时,子类可以修改父类的行为,而装饰模式不希望如此。 16 | 17 | * **适配器模式(Adapter):**将一个类的接口转化为另一个类的接口,使得原本互不兼容的类可以通过接口一起工作。 18 | 19 | * **外观模式(Façade):**用一个公共接口来连接多个类或其他数据类型。公共接口让多个类互相之间保持独立,解耦性良好。同时使用接口时,外部无需理解其背后复杂的逻辑。另外就算接口背后的逻辑改变也不影响接口的使用。 20 | 21 | * **单例模式(Singleton):**单例模式保证对于一个特有的类,只有一个公共的实例存在。它一般与懒加载一起出现,只有被需要时才会创建。单例模式的例子有 UserDefaults.standard,UIApplication.shared,UIScreen.main。 22 | 23 | * **观察者模式(Observer):**定义对象间的一种一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新。在 iOS 中的典型实现是 NotificationCenter 和 KVO。 24 | 25 | * **备忘录模式(Memento):**在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可以将该对象回复到保存之前的状态。 26 | 27 | 如果你正在跳槽或者正准备跳槽不妨动动小手,添加一下咱们的交流群[**931 542 608**](https://jq.qq.com/?_wv=1027&k=cfVxrMAH)来获取一份详细的大厂面试资料为你的跳槽多添一份保障。 28 | 29 | 我们可以把上面7种模式归类为以下3类: 30 | 31 | * **创建型 (Creational):**单例模式 (Singleton) 32 | 33 | * **结构型 (Structural):**MVC、装饰模式 (Decorator)、适配器模式 (Adapter)、外观模式 (Facade) 34 | 35 | * **行为型 (Behavioral):**观察者模式 (Observer)、备忘录模式 (Memento) 36 | 37 | ### 2.什么是MVC? 38 | 39 | **关键词:#model #view #controller** 40 | 41 | MVC 是 Model-View-Controller 的简称。它是苹果官方推荐的 App 开发架构,也是一般开发者最先遇到的、最经典的架构。它把整个 App 分成了三个部分:Model 负责处理数据;View 负责处理 UI;Controller 是 View 和 Model 的桥梁,它将数据从 Model 传送到 View 层展示出来,同时将 View 层的交互传到 Model 层以改变数据。相比于传统的 MVC,苹果的 MVC 的特点是 Model 和 View 层是相互独立的。下图是苹果 MVC 架构的示意图: 42 | ![image](https://upload-images.jianshu.io/upload_images/22877992-7806e2247b62d9ab.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 43 | 44 | 由于 Controller 承担的任务相对较重,实际开发中很多初级的开发者直接将 View 和 Controller 部分的代码全部塞到了 ViewController 类中,造成了它们的高度耦合。如何解耦 View 和 Controller,在 iOS 开发中是一个热门的话题。下图是实际开发中的 MVC 架构: 45 | ![image](https://upload-images.jianshu.io/upload_images/22877992-314bf44f1e72b42a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 46 | 47 | ### 3\. Objective-C 和 Swift 在单例模式的创建上有什么区别? 48 | 49 | **关键词:#线程安全** 50 | 51 | 单例模式在创建过程中,要保重实例变量只被创建一次。整个开发中需要特别注意线程安全,即使在多线程情况下,依然只初始化一次变量。 52 | 53 | Objective-C 中,是用 GCD 来保证这一点的。示例代码如下: 54 | 55 | ``` 56 | + (instanceType)sharedManager { 57 | static Manager *sharedManager = nil; 58 | static dispatch_once_t onceToken; 59 | dispatch_once(&onceToken, ^{ 60 | sharedManager = [[Manager alloc] init]; 61 | }); 62 | return sharedManager; 63 | } 64 | 65 | ``` 66 | 如果你正在跳槽或者正准备跳槽不妨动动小手,添加一下咱们的交流群[**931 542 608**](https://jq.qq.com/?_wv=1027&k=cfVxrMAH)来获取一份详细的大厂面试资料为你的跳槽多添一份保障。 67 | 68 | 在 swift 中,let 关键词已经保证了实例变量不会被修改,所以单例的创建就简单很多: 69 | 70 | ``` 71 | class Manager { 72 | static let shared = Manager() 73 | private init() {} 74 | } 75 | 76 | ``` 77 | 78 | ### 4\. 什么是装饰模式(Decorator)? 79 | 80 | **关键词:#Category #Extension #Delegation** 81 | 82 | 装饰模式是在不改变原封装的前提下,为对象动态添加新功能的模式。在 Objective-C 中,它的实现形式为 Category 和 Delegation;在 Swift 中,它的表现形式是 Extension 和 Delegation。 83 | 84 | * Category 的好处之一是可以给类增加新的方法,它也可以利用动态特性增加新的变量。同时,Category的出现也减轻了类的负担,我们可以利用它将代码分散开来。它的文件名一般为“类名+扩展名” 85 | 86 | * Extension 在 Swift 中的地位等同于 Category 在 Objective-C 中的地位。它更强大的地方在于可以为 Protocol 扩展完成默认实现。 87 | 88 | * Delegation 是程序中一个对象代表另一个对象,或者一个对象与另外一个对象协同工作的模式。一般配合 protocol 使用,例如 tableView 的 UITableViewDataSource 和 UITableViewDelegate 就是典型的 Delegation 模式。注意,delegate 一般声明为 weak 以防止循环引用。 89 | 90 | ### 5\. 什么是观察者模式(Observer)? 91 | 92 | **关键词:#通知 #KVO** 93 | 94 | 观察者模是定义对象间的一种一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新。在 iOS 开发中典型的推模型实现方式为通知和 KVO。 95 | 96 | * **通知(Notifications)** 97 | ![image](https://upload-images.jianshu.io/upload_images/22877992-e623f71f5ff60b4f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 98 | 99 | 1) 观察者 Observer,通过 NotificationCenter 的 addObserver:selector:name:object 接口来注册对某一类型通知感兴趣。在注册时候一定要注意,NotificationCenter 不会对观察者进行引用计数 +1 的操作。 100 | 101 | 2) 通知中心 NotificationCenter,通知的枢纽。 102 | 103 | 3) 被观察的对象,通过 postNotificationName:object:userInfo: 发送某一类型通知,广播改变。 104 | 105 | 4) 通知对象 Notification,当有通知来的时候,Center 会调用观察者注册的接口来广播通知,同时传递存储着更改内容的 Notification 对象。 106 | 107 | * **KVO** 108 | 109 | KVO 的全称是 Key-Value Observer,即键值观察。是一种没有中心枢纽的观察者模式的实现方式。一个主体对象管理所有依赖于它的观察者对象,并且在自身状态发生改变的时候主动通知观察者对象。KVO 是一个纯 Objective-C 的概念,Swift 当前没有很好的动态机制。而且目前只有 NSObject 才支持 KVO。它的具体步骤如下: 110 | 1) 注册观察者 111 | 2) 更改主题对象属性的值,即触发发送更改的通知。 112 | 3) 在制定的回调函数中,处理收到的更改通知。 113 | 114 | 在 Swift 4 中,我们不需要再手动的回收 observer 了。同时配合 NSKeyValueObservation 我们可以更简单的使用 KVO 了,下面是示例代码: 115 | 116 | ``` 117 | // 在 Swift 4 中,NSObject 的类不再自动被推断为 @objc,需要用 @objcMembers 来声明其Objective-c 特性 118 | @objcMembers class User: NSObject { 119 | // dynamic关键词对于observe的闭包来讲是必须的 120 | dynamic var email: String 121 | 122 | init(email: String) { 123 | self.email = email 124 | } 125 | } 126 | 127 | let user = User(email:"user@hotmail.com") 128 | 129 | // 注册观察email属性值,闭包中为若发生变化做出的相应处理 130 | let observation = user.observe(\.email) { (user, change) in 131 | print("User's new email: \(user.email)" ) 132 | } 133 | 134 | user.email = "user@outlook.com" 135 | 136 | ``` 137 | 138 | ### 6\. 什么是备忘录模式(Memento)? 139 | 140 | **关键词:#保存 #回复** 141 | 142 | 备忘录模式是一种保存对象当前的状态,并在日后可以回复的模式。注意,它不会破坏对象的封装;也就是说,私有数据也能被保存下来。 143 | 144 | 其最经典的使用方法就是用 UserDefaults 来读写,同时配合栈可以存储一系列状态。它经常用于初始化、重启、App 前后台状态改变等地方。 145 | # 推荐👇: 146 | 147 | 如果你正在跳槽或者正准备跳槽不妨动动小手,添加一下咱们的交流群[**931 542 608**](https://jq.qq.com/?_wv=1027&k=cfVxrMAH)来获取一份详细的大厂面试资料为你的跳槽多添一份保障。 148 | 149 | ![](https://upload-images.jianshu.io/upload_images/22877992-0bfc037cc50cae7d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 150 | -------------------------------------------------------------------------------- /语言工具-Xcode使用.md: -------------------------------------------------------------------------------- 1 | iOS 开发的官方 IDE 是 Xcode,它也是 Apple 平台最主流的开发工具。目前 Xcode 已经更新到第 9 个版本,功能也是涵盖开发、测试、性能分析、文档查询、源代码管理等多个方面,可谓是 App 开发一站式的平台。 2 | 3 | ![](https://upload-images.jianshu.io/upload_images/22877992-f05c385e0b9a2e37.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 4 | 5 | Xcode 诞生于 2003 年,发展至今,已经可以支持除 Objective-C 和 Swift 之外其他 6 种语言:C、C++与 Objective-C 密不可分;自动化方面则多用 Ruby,例如我们熟知的 fastlane 和 cocoapods;Automation 工具的脚本大多采用 Javascript; 刚刚发布的 CoreML 采用的模型工具则是用 Python 编成。最新的 Xcode 采用完全由 Swift 重写的 Souce Editor,在代码修改、补全、模拟器运行方面有了很大提升。目前最大的缺点是稳定性不够。 6 | 7 | 对于 iOS 工程师而言,熟练运用 Xcode 是必备技能 ,而对 Xcode 的理解深浅亦是工程师水平的分水岭。本节将从基本的 Xcode 开发知识开始,逐渐深入到 Intruments 性能分析和 LLDB 调试,针对 Swift 专门设计的 Playground 也将有所涉及。 8 | 9 | ## Xcode 调试 10 | 11 | ### 1\. LLDB 中 p 和 po 有什么区别? 12 | 13 | **关键词:#调试 #命令** 14 | 15 | * p 是 expr – 的缩写。它做的工作是把接收到的参数在当前环境下编译,然后打印出对应的值。 16 | * po 是 expr –o– 的缩写。它所做的操作与 p 相同。如果接收到的参数是个指针,它会调用对象的 description 方法,并进行打印;如果是个 core foundation 对象,那么会调用 CFShow 方法,并进行打印。如果这两个方法都调用失败,po 打印出和 p 相同的内容。 17 | * 总的来说 po 相对于 p 会打印出更多内容。一般工作中,用 p 即可,因为 p 操作较少效率较高。 18 | 19 | ### 2.Xcode 中的 Runtime issues 和 Buildtime issues 指什么? 20 | 21 | ![image](https://upload-images.jianshu.io/upload_images/22877992-5beaab3d4d2067d1.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 22 | 23 | **关键词:#调试 #编译器** 24 | 25 | * Buildtime issues 有三类:编译器识别出的警告(Warning),错误(Error),以及静态分析(Static Code Analysis)。前两者无须赘述,静态分析错误一般有这几类:未初始化的变量,未使用数据,API 使用错误。比如下面一段代码: 26 | 27 | ``` 28 | class SampleViewController: UIViewController { 29 | override func viewDidLoad() { 30 | let numList: [Int] 31 | let otherNumList = numList 32 | let anotherNumList: [Int]? 33 | } 34 | } 35 | 36 | ``` 37 | 38 | 这段代码中有三个错误。首先 numList 未初始化就赋值给 otherNumList ;其次 anotherNumList 并未使用;最后是 API 使用错误,没有调用 super.viewDidLoad() 方法。 39 | 40 | * Runtime issues 有三类:线程问题,UI 布局和渲染问题,以及内存问题。线程相关问题有很多,最常见的就是数据竞争(data race)。比如下面这段代码: 41 | 42 | ``` 43 | var balance = 0 44 | let fullTimeSalary = 1000, partTimeSalary = 1000 45 | DispatchQueue.global().async { 46 | for _ in 1...12 { 47 | balance += partTimeSalary 48 | } 49 | } 50 | for _ in 1...12 { 51 | balance += fullTimeSalary 52 | } 53 | 54 | ``` 55 | 56 | 这段代码中两个线程同时对 balance 进行写操作,谁先写、balance 值为多少就会变成一个两个线程角力的情况。这种多线程对同一个值进行写操作的行为就是数据竞争。 57 | 58 | UI 布局问题就是诸如尺寸设定没给全或者设定模糊,autolayout 引擎无法渲染的问题。内存问题最常见的就是内存泄漏,比如循环引用就是一个经典的错误。 59 | 60 | ## 分析与优化 61 | 62 | ### 3\. App 启动时间过长,该怎样优化? 63 | 64 | **关键词:#调试 #启动优化** 65 | 66 | App 启动时间过长,可能有多个原因造成。理论上 App 的启动时间是由 main() 函数之前的加载时间(t1)和 main() 函数之后的加载时间(t2)。 67 | 68 | 关于 t1 我们需要分析 App 的启动日志,具体方法是在 Xcode 中添加 DYLD_PRINT_STATISTICS 69 | 环境变量,并将其值设置为 1,这样就可以得到如下的启动日志: 70 | 71 | ``` 72 | Total pre-main time: 1.3 seconds (100.0%) 73 | dylib loading time: 107.45 milliseconds (8.0%) 74 | rebase/binding time: 376.56 milliseconds (28.2%) 75 | ObjC setup time: 166.96 milliseconds (12.5%) 76 | initializer time: 684.01 milliseconds (51.2%) 77 | slowest intializers : 78 | libSystem.dylib : 297.56 milliseconds (22.2%) 79 | libMainThreadChecker.dylib : 33.00 milliseconds (2.4%) 80 | libLLVMContainer.dylib : 113.09 milliseconds (8.4%) 81 | ModelIO : 189.45 milliseconds (14.1%) 82 | 83 | ``` 84 | 85 | 然后我们就可以知道,App 启动主要在这三个方面耗费时间,动态库加载,重定位和绑定,以及对象的初始化。所以优化的手段也有了,简单来说就是: 86 | 87 | * 减少动态库数量,dylib loading time 会下降,苹果的推荐是动态库不要多于 6 个 88 | * 减少 Objective-C 的类数量,例如合并或者删除,这样可以加快动态链接,rebase/binding time 会下降 89 | * 使用 initialize 方法替换 load 方法,或是尽量将 load 方法中的代码延后调用,initializer time 会下降 90 | 91 | 关于 t2,主要是构建第一个界面并完成渲染的时间。所以这个需要在具体的界面布局和渲染代码中进行打点观察,诸如 viewDidLoad 和 viewWillAppear 这两个函数就很值得关注。 92 | 93 | ### 4.如何用 Xcode 检测代码中的循环引用? 94 | 95 | **关键词:#调试 #内存检测** 96 | 97 | 有两种方法可以检测。 98 | 99 | 其一是使用 Xcode 中的 Memory Debug Graph。点击下图所示的调试工具栏中的按钮,Xcode 会自动检测内存相关的 memory runtime issue。点击相关问题处 Xcode 就会给出详细的循环引用示意图。 100 | 101 | ![image](https://upload-images.jianshu.io/upload_images/22877992-a450fa65290976bc.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 102 | 103 | 另一种解决方法是用 Instruments 里面的 Leak 选项——这是一个专门检测内存泄漏的工具。进入页面后发现 Leak Checks 中出现内存泄漏时,我们可以将导航栏切换到 call tree 模式下,强烈建议在 Display Settings 中勾选 Separate by Thread 和 Hide System Libraries 两个选项,这样可以隐藏掉系统和应用本身的调用路径,帮助我们更方便的找出 retain cycle 位置。 104 | 105 | ![image](https://upload-images.jianshu.io/upload_images/22877992-5ca9bff4d6bc9913.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 106 | 107 | ### 5\. 该怎样解决 EXC_BAD_ACCESS? 108 | 109 | **关键词:#调试** 110 | 111 | EXC_BAD_ACCESS 主要原因是访问了某些已经释放的对象,或者访问了它们已经释放的成员变量或方法。解决方法主要有以下几种: 112 | 113 | * 设置全局断点快速定位 bug 所在,这种方法效果一般; 114 | 115 | * 重写 object 的 respondsToSelector 方法,这种方法效果一般且要在每个 class 上进行定点排查,不推荐; 116 | 117 | * 使用 Zombie 和 Address Sanitizer,可以在绝大多数情况下定位问题代码,如下图: 118 | 119 | ![image](https://upload-images.jianshu.io/upload_images/22877992-97579882275e48f9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 120 | 121 | ## Playground 技巧 122 | 123 | ### 6.在实际开发中,我们会测试网络请求收到的数据。要调试 api.org/get 是否工作,工程师在 Playground 中写下了以下代码。假设 API 和网络正常工作,请问这段程序将会打印出什么内容? 124 | 125 | **关键词:#调试 #延时运行** 126 | 127 | ``` 128 | let url = URL(string: “api.org/get”) 129 | let task = URLSession.shared.dataTask(with: url!) { (data, response, error) in 130 | do { 131 | let dictionary = try JSONSerialization.jsonObject(with: data!, options: []) 132 | print(dictionary) 133 | } catch { 134 | print(“error!”) 135 | } 136 | } 137 | 138 | ``` 139 | 140 | 答案是:什么内容都不会打印出来。原因是 Playground 执行完了所有语句,自动退出。如果要让 Playground 具备延时运行的特性,可以在 Playground 中加上一下代码: 141 | 142 | ``` 143 | import PlaygroundSupport 144 | PlaygroundPage.current.needsIndefiniteExecution = true 145 | 146 | ``` 147 | 148 | 这样我们就可以打印出返回的 dictionary 中的内容了。 149 | 150 | ### 7\. 代码实现:请在 playground 中实现一个 10 行的列表,每行随机显示一个 0 – 100 之间的整数。 151 | 152 | **关键词:#调试 #可视化开发** 153 | 154 | 本题主要考察面试者的基本编程能力,对于 API 的熟悉程度和 Playground 可视化编程的了解。完整代码如下: 155 | 156 | ``` 157 | import UIKit 158 | import PlaygroundSupport 159 | 160 | class ViewController: UIViewController { 161 | lazy var tableView: UITableView = { 162 | return UITableView() 163 | }() 164 | lazy var nums: [Int] = { 165 | var array = Array(0...99) 166 | array.shuffle() 167 | return array 168 | }() 169 | 170 | struct Constants { 171 | static let CellIndentifier = "defaultCell" 172 | } 173 | 174 | override func viewDidLoad() { 175 | super.viewDidLoad() 176 | 177 | // autolayout for tableView 178 | tableView.translatesAutoresizingMaskIntoConstraints = false 179 | 180 | view.addSubview(tableView) 181 | 182 | let tableViewContraints = [ 183 | tableView.topAnchor.constraint(equalTo: view.topAnchor), 184 | tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), 185 | tableView.widthAnchor.constraint(equalTo: view.widthAnchor), 186 | tableView.heightAnchor.constraint(equalTo: view.heightAnchor) 187 | ] 188 | NSLayoutConstraint.activate(tableViewContraints) 189 | 190 | // set up tableView 191 | tableView.dataSource = self 192 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: Constants.CellIndentifier) 193 | } 194 | } 195 | 196 | extension ViewController: UITableViewDataSource { 197 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 198 | return nums.count 199 | } 200 | 201 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 202 | let cell = tableView.dequeueReusableCell(withIdentifier: Constants.CellIndentifier, for: indexPath) 203 | 204 | cell.textLabel?.text = String(nums[indexPath.row]) 205 | 206 | return cell 207 | } 208 | } 209 | 210 | PlaygroundPage.current.liveView = ViewController() 211 | 212 | ``` 213 | 214 | # 推荐👇: 215 | 216 | 如果你正在跳槽或者正准备跳槽不妨动动小手,添加一下咱们的交流群[**931 542 608**](https://jq.qq.com/?_wv=1027&k=cfVxrMAH)来获取一份详细的大厂面试资料为你的跳槽多添一份保障。 217 | 218 | ![](https://upload-images.jianshu.io/upload_images/22877992-0bfc037cc50cae7d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 219 | -------------------------------------------------------------------------------- /经验之谈-架构的选择.md: -------------------------------------------------------------------------------- 1 | 这是本系列最后一个章节,主要是一些进阶内容的提问和解答,考察的是开发者功力的深厚 2 | ![](https://upload-images.jianshu.io/upload_images/22877992-90ca6b9fecd6cf98.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 3 | 之前说一个 iOS 开发者成长到一定阶段,就会遇到瓶颈,解决的方法是熟悉设计模式。接触到 App 的架构App 的架构就类似于现代建筑的脚手架或是地基——一旦确定,App 的骨架和结构就已经定型,剩下的工作就是在现成的架构中舔砖加瓦。那么具体来说,我们为什么要关心 App 的架构?有三点原因。 4 | 5 | 首先就是代码均摊。试想如果所有代码都集中在一个 UIViewController 中,App 理论上确实能够运行,然而当调试时面对拥有庞大代码的单个文件,我们需要花大量的时间去找到发生问题的源头。同时在修改代码的同时,又因为所有代码都集中在一处,我们必须格外小心,防止一处修改、他处崩溃这种牵一发而动全身的情况出现。这种就像很多团电路交错在一起,即使是熟练的电工也因为过于复杂而觉得无从下手。真正的架构应该合理分配代码,每个类、结构体、方法、变量的存在都应该遵循单一职责原则。 6 | 7 | 其次是便于测试。测试确保了代码的质量。我们熟知的单元测试、性能测试、UI 测试都是针对单个方法或界面进行测试。架构的合理分配决定了各个测试能够各司其职,不重复、不遗漏,做到最大的测试效率和覆盖率。 8 | 9 | 最后就是易用性。好的架构确保了日后开发中可以轻松应对各种新需求;即使是新人也可以快速学习并适应现有的架构并进行开发。 10 | 11 | 本节将围绕目前流行的 MVC,MVP,MVCS,MVVM,VIPER 等架构来展开。由于绝大多数开发者对于部分架构并不熟悉,本节将着重对架构进行特点分析,并在其之间进行横向比较。 12 | 13 | ### 1.说说苹果官方的 MVC 架构的优缺点? 14 | 15 | **关键词:#耦合** 16 | 17 | MVC 的优点有 2 个: 18 | 19 | * **代码总量少。**基本上大量的逻辑和视图代码都集中在 ViewController 里,View 和 Model 也严格区分,代码分配遵循一定规则。 20 | 21 | * **简单易懂。**新人可以快速上手;修改和增加新的功能也没有明显障碍;即使是没有经验的开发者也可以很好维护。 22 | 23 | 缺点主要由视图层 和控制器层高度耦合造成,其负面影响主要为: 24 | 25 | * **代码过于集中。**ViewController 因为将两部分高度耦合,它将处理交互、视图更新、布局、Model 数据获取和修改、导航等几乎所有操作。 26 | 27 | * **难以进行测试。**由于高度耦合,使得用于检测功能为主的单元测试需要配合特定视图才能进行,测试难度陡增。所以经常在 MVC 架构中,开发者一般只对 Model 进行测试。 28 | 29 | * **难以扩展。**在 ViewController 里添加新功能需要格外小心,高度耦合的逻辑结构增加了出错的风险;同时由于 View 和 Controller 部分由于互相依赖,增加新功能不仅可能需要大量修改原有代码,也会使 ViewController 愈发笨重。 30 | 31 | * **Model 层过于简单。**相比于 ViewController 的庞大代码,Model 层只是定义几个属性。在 Objective-C 的 “.m” 实现文件中,更是几乎看不到代码。 32 | 33 | * **网络请求逻辑无从安放。**网络层放在 Model 中,其异步调用的 API 请求会使得整个 Model 层变得复杂。若是将网络层 放在 ViewController 中,则耦合进一步加剧,以上缺点更加放大。 34 | 35 | 其实 MVC 的缺点一言以蔽之,就是过于笼统的代码分配。任何一个类或者结构体,只要不是数据或是视图,就被放在了控制器一层,而 ViewController 类耦合了视图和控制器,可以说这是 MVC 架构天生的缺点。 36 | 37 | 如果你正在跳槽或者正准备跳槽不妨动动小手,添加一下咱们的交流群[**101 295 1431**](https://jq.qq.com/?_wv=1027&k=SSQAVXir)来获取一份详细的大厂面试资料为你的跳槽多添一份保障。 38 | 39 | ### 2.代码实战:以下代码实现的 MVC 架构有什么缺点? 40 | 41 | **关键词:#view #model** 42 | 43 | ``` 44 | class User { 45 | var name: String 46 | var avatar: UIImage 47 | 48 | init(_ name: String, _ avatar: UIImage) { 49 | self.name = name 50 | self.avatar = avatar 51 | } 52 | } 53 | 54 | extension UIImageView { 55 | func configure(with user: User) { 56 | ... 57 | } 58 | } 59 | 60 | class ViewController: UIViewController { 61 | var user: User? 62 | var userImageView: UIImageView? 63 | 64 | override func viewDidLoad() { 65 | super.viewDidLoad() 66 | 67 | userImageView = UIImageView() 68 | userImageView.configure(with: user) 69 | } 70 | } 71 | 72 | ``` 73 | 74 | 以上代码是经典的 MVC 架构,然而却在两个地方将 View 和 Model 层耦合在了一起。 75 | 76 | 首先,User 类作为 Model,其内部是不应该有 UIImage 这种视图属性的,可以将其改为 NSData。 77 | 78 | 其次,userImageView 作为 View 层,是不应该与 Model 层直接接触的。而在 viewDidLoad 中,我们却发现 userImageView 直接可以调用做为 Model 的 User 去进行配置。这个操作应该由 ViewController 去完成 79 | 80 | ``` 81 | 修改后的代码如下: 82 | class User { 83 | var name: String 84 | var avatarData: Data 85 | 86 | init(_ name: String, _ avatarData: Data) { 87 | self.name = name 88 | self.avatarData = avatarData 89 | } 90 | } 91 | 92 | class ViewController: UIViewController { 93 | var user: User? 94 | var userImageView: UIImageView? 95 | 96 | override func viewDidLoad() { 97 | super.viewDidLoad() 98 | 99 | userImageView = UIImageView() 100 | configure(userImageView, with: user) 101 | } 102 | 103 | func configure(_ imageView: UIImageView?, with user: User) { 104 | ... 105 | } 106 | } 107 | 108 | ``` 109 | 110 | ### 3.MVCS 中的 S 为什么要单独拆分出来? 111 | 112 | **关键词:#数据层 #网络层** 113 | 114 | MVCS 架构其实就是针对 MVC 的优化。S 是 Store 的缩写,意为存储。一般数据持续化层(例如 Core Data )就是 Store,我们把这部分代码单独从 Model 或是 ViewController 里拆分出来构成单独的文件,这就是所谓的数据层。 115 | 116 | 之前我们提到,MVC 的缺点之一就是网络层无处安放。其实根据 MVCS 这个思路,我们我们也可以把网络层放在 S 这一层中。毕竟网络请求也是获得数据,而且一般 API 请求之后数据都要做缓存和持久化处理,所以放在 S(数据层)来说也比较合理。 117 | 118 | 拆分出来之后,整个代码分配更加均衡。同时以往在 ViewController 里面难以进行的单元测试也可以根据单独的数据层文件进行测试,总体来讲测试覆盖率会有所提高。整个拆分之后对于整体架构的维护和扩展也起到了促进作用。 119 | 120 | ### 4.说说 MVP 和 MVC 相比有什么异同? 121 | 122 | **关键词:#解耦 #代码量** 123 | 124 | MVP 的全称是 Model-View-Presenter。它和 MVC 的相同点在于:两者的 Model 功能一样,理论上来讲两者的 Model 层应该完全一样。 125 | 126 | 而不同点在于,MVC 中 View 和 Controller 耦合在 ViewController 类里;而 MVP 的 View 是单独的 UIView/UIViewController,Presenter 也是单独的类。我们来看下 MVP 的结构: 127 | 128 | ![image](https://upload-images.jianshu.io/upload_images/22877992-5f8e90f9daddd174.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 129 | 130 | 如图,MVP 中的 View 是单独的 Class(在 MVP 中,UIView 或是 UIViewController 都属于 View 层),它持有 Presenter 作为变量。当接收到用户交互时,它会调用 Presenter 进行处理。也就是说,View 层不包含任何的业务逻辑代码,它只会将交互交给 Presenter,并从 Presenter 那里接受结果来更新自己。 131 | 132 | 如果你正在跳槽或者正准备跳槽不妨动动小手,添加一下咱们的交流群[**101 295 1431**](https://jq.qq.com/?_wv=1027&k=SSQAVXir)来获取一份详细的大厂面试资料为你的跳槽多添一份保障。 133 | 134 | 而 MVP 中的 Presenter 则负责业务逻辑,它是 View 和 Model 的桥接。它会根据 View 中的交互去修改 Model,或根据 Model 的变化去修改 View。 135 | 136 | 这里要注意,因为 View 持有 Presenter,所以 Presenter 中的 View 应该声明为 weak 或 unowned,以避免循环引用。 137 | 138 | 相比于 MVC,MVP 的耦合度大大降低,代码分配更加合理,测试起来非常方便,整个架构理解和上手难度也并不高。 139 | 140 | 但是它的缺点在于,View 的所有交互都要传给 Presenter 去处理,这样就项目功能一旦增加,View 的代码和 Presenter 的代码都会增加。相比于 MVC 在 ViewController 一个文件里面直接解决,MVP 的总代码量可能会翻倍,这样 App 的维护成本和文件大小都会增大。 141 | 142 | ### 5.MVVM 中的 ViewModel 的作用是什么? 143 | 144 | **关键词:#数据提供 #交互响应** 145 | 146 | ViewModel 一般来扮演两个重要角色: 147 | 148 | * **视图层的真正数据提供者。**一般视图层展示的数据经常是当个或是多个模型的属性组合。例如微博数据流界面,可能一个微博用户模型有 firstName, lastName, status, post 多个属性,ViewModel 就会将这些数据整合在一起,使得视图可以直接调用单个数据就展示所要的效果。简单来说,ViewModel 就是为了视图展示,而对模型层的数据包装。 149 | 150 | * **视图层的交互响应者。**所有用户的交互都会传递给 ViewModel,ViewModel 会依次更新视图层需要的属性,同时相应修改模型层的数据。这里依靠的是属性观察或响应式架构。 151 | 152 | 注意 ViewModel 类中绝对不能包含视图层的任何类或结构体。MVVM 的示意图如下: 153 | ![image](https://upload-images.jianshu.io/upload_images/22877992-7a85a6a86b808189.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 154 | 155 | ### 6\. 试比较 MVC,MVP,MVVM 三种架构。 156 | 157 | **关键词:#模型层 #中间层 #视图层** 158 | 159 | MVC、MVP、MVVM 三种架构皆由模型层(M - Model),视图层(V - View),中间层(C/P/VM - Controller/Presenter/View Model)构成。我们的比较就先从局部开始——分别比较这三个部分,再到整体差异。 160 | 161 | * **模型层几乎相同。**三种架构的模型理论来说都是数据来源,没有什么不同。 162 | 163 | * **视图层理论上都设计为被动,但是实际上略有不同。**实际开发中 MVC 中视图层与中间层高度耦合,几乎所有的操作都统一由 ViewController 包办。 164 | 165 | 但理论上来说,MVC 是希望视图层就是单纯的 UIView,或者 UIViewController 只负责 UI 更新交互,不涉及业务逻辑和模型更新。 166 | 167 | MVP 和 MVVM 在实际开发中视图层实现了 MVC 理论期望,即与中间层严格分离。 168 | 169 | MVP 中视图层是完全被动,单纯的把交互和更新传递给中间层;而 MVVM 中视图层并不是完全被动——它会监视中间层的变化,一旦产生变化,则视图层也会相应变化。 170 | 171 | * **中间层的设计是三种架构的核心的差异。**逻辑上讲,中间层的作用就是连接视图层和模型层。它处理交互、接受通知、完成数据更新。 172 | 173 | MVC 的中间层 Controller 持有视图和模型,主要起到一个组装和连接的作用,通过传递参数和实例变量来直接完成所有操作。 174 | 175 | MVP 的中间层 Presenter 持有模型,在更新模型上与 MVC 的 Controller 角色一样。但它不拥有视图,视图拥有中间层,中间层的工作流程是:从视图层接收交互传递->响应->向视图层传递响应指令->视图进行更新。全部操作必须手动书写代码完成。 176 | 177 | MVVM 的中间层 View Model 持有模型,在更新模型上与前两者相同。它完全独立于视图,视图拥有中间层,通过绑定属性,自动进行更新。全部操作由响应式逻辑框架自动完成。 178 | 179 | * **MVC 耦合度很高,代码分配最不合理,维护和扩展成本最高。**但因为无需层级传递,所以代码总量最少,适合初学者理解和应用。 180 | 181 | * **MVP 和 MVVM 相似,耦合度和代码分配都比较合理,较易实现高测试覆盖率。**MVP 的缺点是视图层需要将所有的交互传递给中间层,且要手动实现响应和更新,所以总代码量远超 MVVM。MVVM 在响应和更新上通过响应式框架自动操作,大大精简了代码量;但是需要引入第三方响应式框架,同时因为属性观察环环相扣,调用栈很大,debug 起来尤为痛苦。 182 | 183 | * **MVC,MVP,MVVM 这三种结构都是以视图为驱动的架构,三种皆为用户交互和视图更新为主要服务目标。**它们一个共同的缺点是没有涉及界面之间的跳转——即路由的设计。 184 | 185 | ### 7\. VIPER 之间的各个组件是如何交互的? 186 | 187 | **关键词:#路由 #Interactor** 188 | 189 | VIPER 架构分别由 5 部分组成:View, Interactor, Presenter, Entity, Router。它的示意图如下,我们从左向右依次来看: 190 | ![image](https://upload-images.jianshu.io/upload_images/22877992-41e193a13fdc421a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 191 | 192 | * **视图层(View)。**与 MVP 或者 MVVM 的视图层类似。它包含与 UI 相关的一切操作。它接收用户的交互信息单并不处理,而是传递给展示层(Presenter)。 193 | 194 | * **展示层(Presenter)。**与 MVP 的 Presenter 或是 MVVM 的 ViewModel 功能类似,更像 Presenter 还是 ViewModel,取决于是否引入响应式编程框架。Presenter 这里只响应并处理视图层传来的交互操作请求,并不直接对数据源进行修改,这是与 MVX 中中间层最大的不同。若要修改数据,展示层会向其持有的数据管理层(Interactor)发送请求,Interactor 会处理一切有关数据源的操作。此外它还连接了路由层(Router)。 195 | 196 | * **路由层(Router)。**专门负责界面跳转和组件之间切换。当 App 较小时,Router 负责页面跳转。当 App 比较大时,不同功能和业务会拆分成不同模块或组件,Router 的作用就是在不同组件之间进行链接。这是之前 MVX 架构所忽略的部分。 197 | 198 | * **数据管理层(Interactor)。**专门负责处理数据源信息。包括网络请求、数据传输、缓存、存储、生成实例等操作。实际上之前中间层和模型层的一些逻辑被进一步剥离至此,整个架构的逻辑也显得更加清晰。 199 | 200 | * **模型层(Entity)。**只拥有初始化方法和属性相关 set/get 方法,与之前的 Model 大同小异。 201 | 202 | 由于分工明确,VIPER 层在代码分配、测试覆盖率上为所有架构之冠。 203 | 204 | 缺点在于,它依然与 MVX 架构一样,是个视图驱动的架构。同时,由于分工精细,不同层级之间交互的代码很多,总体代码量很大,不适宜用在小型 App 中。 205 | # 推荐👇: 206 | 207 | 如果你正在跳槽或者正准备跳槽不妨动动小手,添加一下咱们的交流群[**931 542 608**](https://jq.qq.com/?_wv=1027&k=cfVxrMAH)来获取一份详细的大厂面试资料为你的跳槽多添一份保障。 208 | 209 | ![](https://upload-images.jianshu.io/upload_images/22877992-0bfc037cc50cae7d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 210 | -------------------------------------------------------------------------------- /经验之谈- App的测试和上架.md: -------------------------------------------------------------------------------- 1 | 很多程序员在完成开发后,最期待的就是模拟器上一遍跑通,然后就可以交差了。其实专业的 iOS 开发者除了在开发前十分周全的计划,开发中考虑各种细节问题和边界情况,开发后还会做大量的测试。 2 | 3 | ![](https://upload-images.jianshu.io/upload_images/22877992-7145c0fad0e71aed.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 4 | 5 | 测试在我看来分为三种。第一种是普通的单元测试、UI 测试、性能测试,对于某个模块甚至会做大量的集成测试,这类测试基本上检验了软件上所有可能的逻辑漏洞。第二种测试是真机测试,一般大公司会配备专业的 QA 去手动测试各种情况,之前第一种测试难以覆盖的情况以及一些必须要硬件真机去测试的情况属于此种测试。第三种测试就是将 App 的 beta 版本放在 Testflight 上进行内测,这种测试将邀请特定用户进行体验,以做最后的功能校验。在硅谷,测试一直被看做工程师日常工作的一部分,甚至某些公司在开发上采用了依据测试来写代码的 TDD(Test Driven Development)模式。遗憾的是,因为各种原因,目前国内的互联网公司在测试产品上主要依靠 QA 完成。 6 | 7 | 我们作为专业的 iOS 开发者,虽然无需深度掌握测试技能,但至少应该明白测试的重要性,并能独立完成基本的测试操作。在确保 App 安全无虞的上架、日后类似的 bug 不再重犯,测试的效果无可替代。 8 | 9 | 在完成测试后,也并不代表着 App 就一定可以通过苹果的审核进入商店。苹果官方有明确的审核指南。本节亦会挑选常见的 App Store 相关的上传、下载、审核问题进行探讨。 10 | 11 | ## 测试相关 12 | 13 | ### 1.一个 App 崩溃了,可能是什么原因造成的? 14 | 15 | **关键词:#代码 #内存 #网络 #第三方** 16 | 17 | * **代码出错。**利用了 Objective-C 的动态性能,编译时不会报错,结果运行之后程序找不到对应的实现,产生崩溃。比如下面这个例子。 18 | 19 | ``` 20 | // o1 和 o2 有实现方法 myMethod,但是 o3 没有 21 | MyObject *o1 = ... 22 | MyObject *o2 = ... 23 | NSObject *o3 = ... 24 | 25 | NSArray *array = @[o1, o2, o3]; 26 | for (id obj in array) { 27 | [obj meMethod]; 28 | } 29 | 30 | // Runtime error: unrecognized selector sent to instance XXX 31 | 32 | ``` 33 | 34 | * **内存不够。**比如 App 在运行时占用了手机大量的内存,此时App就会崩溃。经常发生在低配或内存容量很少的手机。这个问题可以通过 Xcode Instruments 调试判断出来。 35 | 36 | * **网络原因。**当网络不佳时,App 的请求得不到即时的响应而导致的超时;或是用户数量太多,服务器端过载而影响到手机端崩溃。其实这些都可以在优化服务器端配置和处理手机端异常中改进用户体验。 37 | 38 | * **第三方。**开发中使用了第三方的工具有可能有病毒或是 bug。另外广告的弹出也可能很阻塞线程或侵占内存,导致 App 崩溃。 39 | 40 | 一般解决 App 崩溃的方式是检查对应的机器日志。国外主流的检测工具是 twitter 开发、google 维护的 Fabric。国内主流的工具是腾讯的 Bugly。 41 | 42 | ### 2.在模拟机上完成所有测试之后,是否就不需要在实机上再进行测试了? 43 | 44 | **关键词:#功能 #硬件** 45 | 46 | 答案是,需不需要实际测验要看具体情况。模拟机可以完成绝大多数的功能检测。但是真机和模拟机的差别还是存在的,主要集中在功能和硬件上: 47 | 48 | * **功能方面。**模拟器不支持 Email、通话、短信等功能,同时也不支持 Accessibility 的 VoiceOver功能,如果 App 是支持残疾人使用的,请务必在真机上测试。 49 | 50 | * **硬件方面。**模拟器不支持相机、音频输入、蓝牙等硬件功能。如果 App 支持手环诸如 Apple Watch 联动,请务必在真机上测试。 51 | 52 | 如果 App 不会涉及到这些差异,那理论上无需用真机进行测试。当然谨慎起见,如果时间充裕是一定要将主要功能在真机上测试的。 53 | 54 | 如果你正在跳槽或者正准备跳槽不妨动动小手,添加一下咱们的交流群[**101 295 1431**](https://jq.qq.com/?_wv=1027&k=SSQAVXir)来获取一份详细的大厂面试资料为你的跳槽多添一份保障。 55 | 56 | ### 3.为什么在单元测试中引入代码模块要用 `@testable` 关键词? 57 | 58 | **关键词:#internal** 59 | 60 | 测试时,我们经常需要导入开发中的 module。普通的 import module 虽然完成了导入,但是只能调用 module 的 public 变量和方法。 61 | 62 | 单元测试和UI测试中,很多 public 的方法是多个内部方法的整合,与其测试复杂的 public 方法,不如单独测试其组成的一个个小的 internal 方法。 63 | 64 | 此时如果能够调用 module 中的 internal 变量和方法,那么将会大大方便测试。`@testable import` 就表示,module 中的 internal 变量和方法也可以在测试中被使用。 65 | 66 | ### 4.代码实战:试着对下面的方法写出对应的单元测试 67 | 68 | **关键词:#异步 #mock** 69 | 70 | ``` 71 | func loadContent() { 72 | let url = "https://app-info.rtf" 73 | let session = URLSession.shared 74 | let client = HTTPClient(session: session) 75 | 76 | client.get(url: url) {(data, error) in 77 | if let error = error { 78 | print("Error: \(error)") 79 | return 80 | } 81 | 82 | if let data = data { 83 | print("Data is successfully fetched from server") 84 | } 85 | } 86 | } 87 | 88 | ``` 89 | 90 | 上面这段代码,是一段访问服务器端返回数据的方法。这道题如果用来测试,涉及到两个知识点:第一个是如何测试异步访问,第二个是使用 mock。我们来分别解释。 91 | 92 | 首先,如何测试异步访问。用 expectation 。本题中我们设定好 expectation 中网络端会返回 data,然后在异步的线程中调用 fulfill() 方法,即表示异步成功结束时会触发。接着我们等待异步结束,当然我们会设定超时的阈值。 93 | 94 | 其次,为什么要使用 mock。测试中, 访问服务器端并接收到数据返回是不切实际的举动:首先如果测试时真的调用服务器接口,你无法保证服务器返回的数据是什么,会不会报错,也就无法准确的测试各种情况;其次,调用接口牵扯到真实的服务器逻辑,会修改服务器数据,对于测试来讲这显然没有必要;最后,每次访问服务器端再返回数据比较耗时,这样整个测试效率很差。所以我们可以模拟服务器返回数据的过程,用一个假的 client 去“装模作样”地访问服务器端,并且从本地直接返回确定好的数据。至此整个操作就无需真的依赖网络,并且我们可以就各种返回情况进行模拟测试。 95 | 96 | 下面是示例代码: 97 | 98 | ``` 99 | var dataLoaded: Data? 100 | 101 | func test_loadContent_shouldReturnData() { 102 | let url = "https://app-info.rtf" 103 | let session = MockSession() 104 | let client = HTTPClient(session: session) 105 | 106 | // 用NSPredicate来过滤条件,只有dataLoaded不为nil才会被记录 107 | let pred = NSPredicate(format: "dataLoaded != nil") 108 | let exp = expectation(for: pred, evaluateWith: self, handler: nil) 109 | 110 | client.get(url: url) { [weak self] (data, error) in 111 | self?.dataLoaded = data 112 | // 当异步成功结束时触发expectation 113 | exp.fulfill() 114 | } 115 | // 等待expectation被触发,超时时间设定为5秒 116 | wait(for: [exp], timeout: 5.0) 117 | // 判断expectation出发后dataLoaded是否不为nil,否则测试失败 118 | XCTAssertNotNil(dataLoaded, "No data is received!") 119 | } 120 | 121 | ``` 122 | 123 | ### 5.谈谈 iOS 中的性能测试(performance test)? 124 | 125 | **关键词:#耗时 #scheme** 126 | 127 | 所谓性能测试,就是检测一个方法快慢的测试。我们一般设定一个基础值,比如 0.01s,然后运行性能测试,测试后会显示本次测试耗时以及平均运行耗时。你可以跟基础值进行比较,并且设定最大上限,比如 10%。这样如果测试超过最大上限耗时,比如 0.01s * 1.1 = 0.011s,那么此次测试就失败了。性能测试的示例图如下: 128 | 129 | ![image](https://upload-images.jianshu.io/upload_images/22877992-954fe8aa252457be.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 130 | 131 | 性能测试一般用在分析那些可能会很耗时的方法上。比如在设备上存取操作、网络端的请求、复杂的计算等等。 132 | 133 | 注意性能测试和 Instruments 的性能优化不同,前者是 App 的性能的底线:如果不满足性能测试的时间标准,那么用户体验将会受到极大影响,甚至被苹果拒绝上架。后者则是在性能上锦上添花的优化操作,是一个可以提高用户体验的任务。性能优化有时就算不做,也无伤大雅。性能测试则是要求方法必须满足指定的耗时要求。 134 | 135 | 一般情况下,建议单独开一个专门的 scheme 来运行性能测试。这样可以清晰得将其和单元测试或是 UI 测试区分开来,借用快捷键 cmd+U 来单独运行性能测试也更加方便。 136 | 137 | 如果你正在跳槽或者正准备跳槽不妨动动小手,添加一下咱们的交流群[**101 295 1431**](https://jq.qq.com/?_wv=1027&k=SSQAVXir)来获取一份详细的大厂面试资料为你的跳槽多添一份保障。 138 | 139 | ### 6.谈谈 iOS 中的 UI 测试? 140 | 141 | **关键词:#record #XCUIElement #Identifier #iPhone vs. iPad** 142 | 143 | 首先 UI 测试特殊的地方在于。我们并不需要完全的手写代码,Xcode 的 record 功能可以自动生成 UI 测试代码。我们只需给出判断条件和代码优化即可。 144 | 145 | 其次 UI 测试的 API 中有这几个值得注意。XCUIApplication 对应的实例是应用的入口,其次所有的UI控件都是 XCUIElement。UI 测试是根据它们对应的 title 属性进行指定(如果有 title 重名的情况,则选择 XCTest 框架搜索到的第一个对应 UI 控间)。我们当然还可以通过 accessibility Identifier 来指定一个 UI 控件。所以我们一般 UI 测试都是通过具体行动(点击、滑动)之后比较不同 UI 控件的状态,异或是寻找指定页面出现的 UI 控件来进行测试。 146 | 147 | 最后 UI 测试会牵涉不同机器不同尺寸的问题。比如 iPhone 用的是 tableView 而 iPad 用的是 splitView,由于 UI 布局不同,UI 控件的位置差异也是需要特殊处理的。 148 | 149 | UI 测试更关注的是用户行为/体验,而单元测试则关注单个方法的逻辑正确。**UI测试能覆盖到单元测试都无法覆盖到的部分**,例如: 150 | 151 | 1. 在给定输入时,输出通过了单元测试;但实际上输出的格式并不满足要求,在屏幕上也会因为尺寸问题被缩进。这时就需要 UI 测试来检查。 152 | 2. 键盘在某界面会无故弹出却无法收起。此时程序在逻辑上正确,单元测试毫无问题;然而 UI 测试却可以检测出屏幕上某些 UI 控件因为被键盘遮挡而无法点击。 153 | 154 | ### 7.如何检查测试覆盖率? 155 | 156 | **关键词:#coverage** 157 | 158 | 运行完测试之后,切换到日志导航,点击刚刚测试的结果,在导航栏上点击 Coverage 即可得到如下测试覆盖率示意图: 159 | ![image](https://upload-images.jianshu.io/upload_images/22877992-b80d7f1c295c45d1.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 160 | 161 | 我们不仅可以查看整个 App 的测试覆盖率,也可以查看每个文件的测试覆盖率。单独点击一个文件进入其中,红色部分表示测试没有覆盖到的地方。 162 | 163 | 代码覆盖率越高说明测试越完善。当然我们不必追求 100% 的代码覆盖率。注意测试覆盖率一般以运行完所有单元、性能、UI 测试之后的数据为准。 164 | 165 | ## App Store相关 166 | 167 | ### 8.什么是 iOS 中的 App ID? 168 | 169 | **关键词:#teamid #bundleid** 170 | 171 | 每一个 App 都有独立的 ID 来唯一确定,这就是 App ID。它由两部分构成:Team ID 和 Bundle ID。两者以点区分,组合在一起就是 App ID 的形式:Team ID.Bundle ID。 172 | 173 | Team ID 指定 App 是由某个具体的开发者或团队开发。Bundle ID 指定 App 或与之相关的一系列 App。Bundle ID 可以唯一确定 App。 174 | 175 | Bundle ID 是在 Xcode 项目中确定的。一个单独的 Xcode 项目可能有多个目标文件,对应也可能产生多个 App。比如 beta 版和 pro 版,付费版和免费版等等。 176 | 177 | ### 9.什么是 iOS 中的 Code Signing? 178 | 179 | **关键词:#密匙 #安全** 180 | 181 | 为了确定 App 是谁开发,开发之后有没有被修改,Apple 引进了 Code Signing 的机制。 182 | 183 | 有了它,在从 App Store 下载 App 后,iOS 和 MacOS 系统可以通过签名确认是谁开发了 App,以及签名是否有效。 184 | 185 | 只要 App 对应的可执行的文件被修改,签名就认定为无效。对于无效的签名系统将拒绝运行 App,以保证整个系统的安全性和用户体验。 186 | 187 | Code Signing 对应的签名是由一对公共和私有的密匙,以及一个由 Apple 签发的证书构成。其中私有的密匙用来产生签名;证书则包含了公共密匙并由此认定 App 的开发者。 188 | 189 | ### 10.什么是 iOS 中的 App Thinning? 190 | 191 | **关键词:#最小** 192 | 193 | App Thinning ,中文翻译为“应用瘦身”,指的是 App store 和操作系统在安装 iOS 或者 watchOS 的 App 的时候通过一些列的优化,尽可能减少安装包的大小,使得 App 以最节省资源的、最合适的大小被安装到你的设备上。其中有三种类型:slicing, bitcode, and on-demand resources。 194 | 195 | * Slicing 指的是根据不同的设备,App 对应产生相应的版本。如 iPad 版本只包含 iPad 版本的图片资源和布局代码,iPhone 版本则类似。此时下载 App 的时候,只需要下载对应版本的 App 即可。 196 | 197 | * Bitcode 是一个 llvm 编译 App 时生成的中间形式。上传或下载新版本的 App 时,苹果会针对 Bitcode 包含的信息进行针对性地添加或筛选,而不是完整地提交或下载一个新的 App。在 iOS 中它是可选的,在 WatchOS 中 Bitcode 则是必须的。 198 | 199 | * On-Demand Resources 是只提供部分的 App 内容,只要足以满足其基本运行即可。比如某些游戏 App,一开始下载之后只能运行最初的内容,而不是全部的内容。如果玩家有兴趣继续探索,App Store 就会解锁后续内容,将其下载更新到游戏中。 200 | 201 | ### 11.向 App Store 提交 App 有哪些可能被拒的原因? 202 | 203 | **关键词:#崩溃 #第三方 #版权 #材料不全** 204 | 205 | App Store 的审核虽然现在越来越快,被拒绝的成本越来越低,但是做到在提交 App 之前仔细检查,争取一次性通过,依然是 iOS 开发者的基本素养。 206 | 207 | 被拒绝的原因有很多,最主要的有以下几种: 208 | 209 | * **崩溃。**程序本身有 bug、第三方服务器出错都有可能。注意我们平常测试是在线下环境中跑 App,而App Store 是在线上环境运行。所以提交审核的时候,还是应该在线上环境运行以防万一。 210 | 211 | * **第三方。**如 App 需要安装第三方应用,比如需要 QQ 登录,而测试员的手机中又没有装 QQ,如果出现提示安装 QQ,就可能被拒;另外使用第三方的广告,也有可能因为违规被拒。 212 | 213 | * **版权。**比如第三方客户端套用某平台的名字;App 描述或命名中为了点击和排名硬塞某些无关的关键词;亦或是山寨现成 App 的行为;App 中包含没有授权的内容也是被拒的理由。注意苹果对某些关键词(比如 Android)非常敏感,绝对不要出现在 App 的提交中。 214 | 215 | * **材料不全。**有时 App 会因为缺少材料导致 App Store 无法审核。比如缺少截图或者使用错误的截图;与硬件相关的 App 提交时,官方没有相关硬件,此时需要开发者提供相关视频。 216 | 217 | 上面只是部分案例。苹果官方有专门的审核导读文件(App Store Review Guidelines) 218 | ,建议开发者在上传 App 前应该仔细研读,并一一检查。 219 | 220 | # 推荐👇: 221 | 222 | 如果你正在跳槽或者正准备跳槽不妨动动小手,添加一下咱们的交流群[**931 542 608**](https://jq.qq.com/?_wv=1027&k=cfVxrMAH)来获取一份详细的大厂面试资料为你的跳槽多添一份保障。 223 | 224 | ![](https://upload-images.jianshu.io/upload_images/22877992-0bfc037cc50cae7d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 225 | -------------------------------------------------------------------------------- /代码考查到offer的比较和选择.md: -------------------------------------------------------------------------------- 1 | ![](https://upload-images.jianshu.io/upload_images/22877992-77df7c4f1a5fb5bc.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 2 | 3 | 4 | # 4.代码考查和系统设计的准备 5 | ## 1.如何准备代码考查 6 | 7 | 很多面试的能力都不是突击可以获得的。项目经历不是,代码能力也不是。如果说项目经历的获取还需要环境支持的话,代码能力的提高基本只需要自己投入就可以了。 8 | 9 | 在网上有很多练习编程的网站,特别是像面向求职者的 LeetCode 一类的网站,提供了各大公司的代码考察题目,并且大部分题目还有标准解答和示意代码。你可以在上面一遍一遍地练习,以提高自己的代码转换能力和逻辑思维能力。我建议大家至少做 100 道 LeetCode 里面 Facebook、Apple 这些大公司的代码题目,很多题目都设计得非常好,既是很好的练习题,又可能在实际工作中用到。Google 的面试题通常还要更难一些,对自己要求更高的同学也可以挑战一下。 10 | 11 | 我曾经面试过一个清华的应届生,考查他各种面试题目都解决得非常快,我就好奇他是怎么做到的。他回答说:他花了一年时间,把 LeetCode 上面的所有题目做了三遍。当时 LeetCode 上面大概有 700 道题目。你看,即使是清华这样厉害学校的学生,同样在代码练习量上非常努力。我自己觉得,这个应届生在努力程度上,就可以超过 95% 的别的候选人了。 12 | 13 | 如果是非科班的学生,在写代码前你还需要学习数据结构和基本的算法知识。相关的学习资料可以选择国外的公开课,或者国内 985 大学计算机专业的教材。麦克道尔著的《程序员面试金典》也是非常不错的学习资料。 14 | 15 | 除此之外,在纸或白板上写代码的能力也需要好好练习。纸上写代码麻烦的地方在于不方便及时涂改,所以需要思考得比较清楚再动手写。准备一些 A4 纸,然后拿 LeetCode 题目多练习几次,慢慢就会有感觉。 16 | 17 | 我自己在纸上写代码的经验来着早年在大学时参加的 ACM 国际大学生程序设计竞赛,这是一个 3 人组队参加的比赛。比赛时 3 个人可以合作答题,但是只有一台电脑可以使用。所以为了最大限度地提高电脑的利用率,我们同时做 3 道题,然后会现在纸上写代码,等自己在纸上完成代码的编写后,再使用电脑来录入和调试。为了备战 ACM 比赛,我差不多有几百道在纸上手写代码的经验。 18 | 19 | 我最后总结出来在纸上写代码的要诀是:一定要先把整体逻辑框架梳理清楚,然后再填充细节。所以你可以用文字、流程图或任何你喜欢的方式先把代码整体逻辑描述在纸上,然后检查没有边界问题后,再在纸上细化成具体的代码。 20 | 21 | ## 2.写代码之外的沟通 22 | 23 | 即使是做代码题目,必要的沟通交流也是必须的。我见过很多候选人听完题目就埋头写代码,完全不和面试官交流,这其实是非常错误得做法。如果写代码完全不需要交流,那么为什么不当做笔试题,而要耽误面试官的时间坐在你旁边?难道就只是为了监督吗? 24 | 25 | 其实,解决一道代码题目的思考过程是非常有价值的,面试官问你一道代码题目,其实是希望和你一起沟通交流,了解你的思路,帮助你找到最好的解法,最后才是把代码完成的事情。 26 | 27 | 所以,当面试官给你一道题目,你首先要做的是和面试官足够地交流。你可以首先确保你完整地理解了题意,这可以通过询问题目的一些细节来达到,比如问输入的数据范围,输出的具体要求,一些异常的情况是否要考虑等等。 28 | 29 | 等你完全理解题意之后,下一步就是将你的想法说出来。你完全不必担心没有一下子说出最好、最完美的解法,大部分好的代码题目都可以一题多解,你可以先说一个最简单直接的方法,然后说出这种方法的时间复杂度、空间复杂度。一般面试官都会问你有没有更好的做法,或者你也可以直接说想思考有没有更好的做法。接着你可以试试看能不能想出一些办法,即使一些办法没有完全想清楚所有细节,也可以说出来。好的面试官如果发现你的方法完全方向不对,还可以及时干预。 30 | 31 | 你如果在思路上有卡住,你甚至可以请求面试官给你一些 “提示”。虽然这可能使得你面试表现稍微减分,但是比起完全没有写出代码来说也要好很多。 32 | 33 | 除了写代码之前和面试官交流、确认解法,写完代码之后,你也需要和面试官讨论你的代码细节问题。通常代码中多多少少会出现一些问题,面试官会给你一些引导,帮助你发现并且修改有问题的代码。 34 | 35 | ## 3.如何准备系统设计 36 | 37 | 如果你是一个应届生,通常考查的系统设计题都不太难,你只需要有一些系统设计的基础,都不至于完全答不上来。在准备资料上,可以看看《设计模式》相关的书。如果有机会实习,可以多尝试一些不同的职位,如果你同时尝试过客户端和服务器端开发,在系统设计上就可以更加综合考虑设计方案在多端的实现难度,以便做出权衡。 38 | 39 | 另外,你可以通过学习分析一些开源项目的代码,来学习架构设计。在网上,你通常也可以搜索到一些常见的系统设计题目,在本书的上一节中,我也提供了好多系统设计题。把这些系统设计题目仔细研究,尝试自己实现一下,通过实践并且和同学讨论,相信你也会有不错的成长。 40 | 41 | 系统设计题目通常都不会有特别标准的解答,其实考查过程更看重一个人分析解决问题的思路。这样的思路可以保证即使未来遇到没有见过的问题,也可以从容地系统性思考和判断。所以,你需要给面试官展现你思考的过程。在面试中,了解需求细节,解释设计思路,讨论和判断一个设计的优缺点,都能够让面试官体会到你这方面的素质。 42 | 43 | 虽然没有标准的答案,但是系统设计还是有一些解题套路,下面我就给大家介绍一下。 44 | 45 | 首先系统设计题都非常考查一个人知识的全面性。所以大家应该平时多了解一些 iOS 之外的技术,比如适度了解一下 Android 端、Web 端以及服务器端的各种技术方案背后的原理。你可以不写别的平台的代码,但是一定要理解它们在技术上能做到什么,不能做到什么。你也不必过于担心,面试官在考查的时候,还是会重点考查 iOS 相关的部分。 46 | 47 | 在知识足够宽泛的情况下,你需要首先和面试官明确问题的各种细节,比如假如题目是“设计一个类似微博的信息流应用”,你需要了解清楚这个信息流应用更多的技术要求,比如: 48 | 49 | * 信息流的内容是否包括图片,文字,语音。 50 | * 平均每个用户每天有多少的信息流更新量。 51 | * 是否需要做图文混排。 52 | * 是否需要做图片的缓存,历史信息的缓存。 53 | * 断网情况下是否需要显示离线内容。 54 | * 发送失败情况下是否需要暂存内容。 55 | * 系统对核心功能的性能(例如发送,刷新)的要求是多少。 56 | 57 | 有一些技术细节可能是面试官想考查的,你问的时候他就会要求多一些;有一些技术方案明显很复杂的,你提出来,他即使不考查你,也会觉得你的考虑是足够周全的。 58 | 59 | 在确定技术细节要求后,你就可以开始讲你的系统架构设计了,这个时候讲的要诀是先框架,再细节。你需要先把各个模块的层次画出来,比如刚刚那道题目,你先介绍一下整体 App 是怎么和服务器通讯的,服务器端的信息流大概是如何存储的,然后你就需要详细介绍 App 的部分。 60 | 61 | 在介绍 App 的框架时,先画出 Model 层,Controller 层,View 层。然后再进一步细化,比如把 Model 层细化到本地存储,图片缓存,网络请求等模块。View 层如何处理图文混排,Controller 层如何与其它层通讯。 62 | 63 | 当框架介绍得差不多的时候,你需要把后续的选择交给面试官。面试官可能会选其中某一个模块,让你做更细一步的设计。比如让你设计网络通讯的 RESTful 接口,细化缓存相关的 API 名字。面试官甚至可能选一两个具体的函数,让你写写。面试官也可能进一步挑战你的一些设计细节,这个过程中,你可能需要修正自己的设计,也可能需要解释你的设计。 64 | 65 | 下图就是回答系统设计题的框架。 66 | 67 | ![系统设计题的框架](https://upload-images.jianshu.io/upload_images/22877992-c08f18b4f1a93104.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 68 | 69 | 一道系统设计题的考查方式可以千变万化,需要的是你和面试官之间密切地交流,性格内向的程序员很可能在这方面吃亏。所以,除了有方泛的知识面以及扎实的架构设计基本功外,多和面试官讨论交流也是面试过程中的关键因素。 70 | 71 | ## 5.复盘 72 | 73 | 复盘是一个人持续提高和进步的源泉。也许你觉得你的面试表现很好,但是为什么面试没有通过呢?当你被拒的时候,与其抱怨面试官或者面试流程不公正,倒不如静下心来想一想,看看是不是自己忽视了某些细节或者关键点。如下图所示: 74 | ![复盘流程](https://upload-images.jianshu.io/upload_images/22877992-ac2c20b4a8f8520d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 75 | 76 | 例如,当你的算法或系统设计题解决方案不太好时,面试官通常不会直接纠正你,他只会稍微引导一下。面试结束后,他更不可能给你做教学,教你应该怎么做答这些题目。所以,你的解决方案好不好,你其实只是凭感觉来的。你可能觉得你已经解决了这个问题,但是面试官可能心里想,这个解决方案虽然可以工作,但是是他见到过的最最愚蠢低效的办法。 77 | 78 | 所有的这一切,你都无法直接知晓。你只能在面试之后,特别是被拒之后,仔细复盘分析一下,看看自己可能哪些问题回答得不好。这个时候,找人请教和讨论也显得特别重要,你可以把你怀疑的一些面试糟糕表现描述出来,然后找人一起分析一下,通常情况下都会有一些收获的。 79 | 80 | 除了复盘算法和系统设计题,也需要复盘一下自己整体的面试流程是否表现正常。例如: 81 | 82 | * 我的自我介绍是否流利? 83 | * 我的项目沟通是否介绍清楚了? 84 | * 面试官有没有完全理解我介绍的项目挑战? 85 | * 我的时间控制是否到位? 86 | * 我有没有迟到,中途接电话,或者任何被认为不礼貌的行为? 87 | * 我做得不好的地方,有没有短期可以改善的? 88 | * 我做得不好的地方,短期不能改善的,我能不能用别的方式适当弥补?比如面试中强调自己的强项。 89 | 90 | 每次面试后,做一个小结,可以使得自己每次都会有一点点进步,几十场面试下来,相信大家都会有不小的成长。 91 | 92 | # 5. 如何提问 93 | 94 | 有些时候前面的环节占用了太多的时间,面试官可能就不会给你提问的机会。但如果面试官说:“我的问题问完了,你有什么问题吗?” 那么恭喜你,你基本上已经完成了整个面试,而且还有一点时间可以交流一下。通常这个提问环节留给双方的时间不会特别多,所以可以就你关心的问题来提一到两个问题即可。 95 | 96 | 很多候选人会说:“我没有什么问题”。这是可以的,但是这个环节最好还是提个问题稍好一点,它能够显示出你对目标公司的关注和兴趣。提问的内容可以围绕着技术氛围、技术分享、公司的业务、未来的方向等等。比如: 97 | 98 | 公司在 iOS 端主要使用了哪些技术框架? 99 | 公司内部有技术分享或者别的学习交流机会吗? 100 | 公司当前团队有多大,希望我进去参与哪方面的业务? 101 | 公司当前有没有什么大的竞争对手? 102 | 未来公司希望在哪些产品上重点发力? 103 | 但是切忌不要问以下这些问题,否则会显得自己很不专业。 104 | 105 | 1.询问面试表现 106 | 很多人在面试结束后,都忍不住问:“我刚刚的面试表现怎么样?”面试官通常都不会当面评价候选人,因为这可能引起冲突。大部分候选人都不能冷静地面对面试批评。而即使你面试表现很好,是否录用也取决于你的竞争者的表现是否在你之上,所以面试官真的很难回答这个问题。 107 | 108 | 通常如果你问了这个问题,面试官要么糊弄地回答:“还不错”,要么就诚恳地回答:“我们通常不当面反馈面试表现”。 109 | 110 | 2.询问面试题答案 111 | 有人会在面试结束前问:“刚刚那道题目应该怎么做?”面试官听到这种问题通常只能苦笑一下。你要知道,这是面试,不是面试培训。面试官没有义务给你解答题目,而且通常为了保证面试题目资源的保密,面试官也不愿意把一道题目的最优解告诉你。如果你回去告诉了你的朋友,这道题目就没有考察的功能了。 112 | 113 | 如果你特别想知道面试题答案,还是应该自己在面试结束后仔细钻研。大部分公司也会要求候选人对面试题保密,所以如果你把题目放到网上共享,通常也是不被允许的,严重的情况下,可能造成通过的面试 offer 因此被取消。 114 | 115 | 3.询问薪资 116 | 大部分公司的薪资都是非常保密的,能知道薪资的人除了 HR 外,只有很高级的主管才有可能知道。大部分的面试官可能都不知道你的职位薪资。所以如果你有这方面的需求,应该直接找 HR 咨询,而不应该问面试官。 117 | 118 | 一般你的最后一轮面试官有可能是你未来的主管,他可能会询问你的薪资和期望,遇到这种情况,你可以趁机反馈出自己的意愿,但如果面试官没有提,最好你也就不要问这方面的信息,以免面试官拒绝回答造成自己过于尴尬。 119 | 120 | 小结 121 | 整体来说,面试的打分和提问环节相关性不大,所以大家只要别问敏感问题即可。 122 | 123 | # 6. offer的比较和选择 124 | 恭喜你!经过努力,你最后拿到了好几家公司的 offer!这些公司有的规模很小,是成立不久的创业公司;有的已经是纳斯达克的上市公司,员工数量过万;有的是外企,有着复杂但是规范的流程;有的是国企事业单位,工资虽然不太高但是福利好并且工作压力不大。你该如何选择? 125 | 126 | 很多人都会纠结,我也曾经纠结过。现在回过头来,我觉得要做好选择,核心还是对自己的情况有一个清晰的判断。每个人不一样,所以最后的选择也不一定一样。 127 | 128 | 1.工作 vs 生活 129 | 这辈子打算怎么过,其实很多人都没有想得特别清楚明白。每个人的选择没有什么高低贵贱之分。也许你希望新工作能够尽量兼顾工作与生活,也许你希望新工作能够帮助自己快速在职业上成长。生活的目标不一样,答案也就不一样。想清楚了,答案就明显了很多。 130 | 131 | 如果你希望尽量兼顾工作与生活,那么那些工作强度大的 offer 自然应该就排在低优先级。哪些公司工作强度大?询问一下对方公司的 HR 或者面试官,平常的上下班时间,加班的频率,通常都可以得到比较客观的回答。如果你恰好有在目标公司上班的朋友,那么你应该可以获得更多这方面的信息。你也可以在网上搜索相关的关键词,获得信息。 132 | 133 | 其实在很多国家,人们并不是那么看重工作的,很多人更多的看重生活以及家庭。只是国内的环境以及我们从小的教育,使得我们很看重工作,以及工作的职业成长。 134 | 135 | 2.职业成长 136 | 大部分人看重职业成长,这一点非常好理解:程序员是一个需要持续学习和积累的工作,如果不能在工作中持续学习成长,几年之后如何在职场获得立足之地呢?但追求快速的职业成长不一定表示一定要去工作强度特别大的公司,其实核心还是看: 137 | 138 | 工作的内容 139 | 指导你的导师(我们行业内常常叫 Mentor) 140 | 一个有职业成长的 offer,其工作内容应该可以让你建立起对相关专业领域的完整知识,并且该工作内容很可能具有成长性和发展性。比如,如果是偏研究的工作,基于深度学习的研究就比别的人工智能研究更加具有成长性。又如,做用户产品就比做内部系统更具有成长性。 141 | 142 | 如果你的 offer 是去维护一个已经开发了 10 多年的老旧系统,除非是要你花大力气重构它,否则你的工作成长性也会较差,因为难的问题都已经被别人早解决过了。 143 | 144 | 行业的成长也很重要,比如互联网行业就比传统的软件行业好,移动互联网行业又比传统互联网行业好。选了一个好的行业,你的能力和待遇会随着行业的发展而发展。选错了一个行业,即使你做到这个行业的冠军,整个行业不挣钱,你的日子也不会好过。 145 | 146 | 除了工作本身的吸引力外,指导你的导师也很重要。一个领域的知识架构是什么样的,重要的观点和讨论,都是一个好的导师能够指引的。当然,你不要抱有什么事情都是导师手把手教这种幻想,这在公司里面既不现实,也不利于你自学能力的发展。 147 | 148 | 3.职业背景 149 | 有一些人,特别是学校不太好的人,会特别看重第一份工作的公司背景。因为大部分公司在挑选简历的时候,差不多主要就看两条:毕业院校和工作过的公司。所以,我建议学校不太好的同学,可以在考虑 offer 的时候选择名气比较大的公司。这样对你未来的简历,会有一定的加分效果。 150 | 151 | 但如果你已经是非常好的学校(比如 985、211 院校)的毕业生了,那么我建议你不用太考虑公司的名气。因为对于你来说,学校背景这一条已经够你用作未来面试的敲门砖了。 152 | 153 | 4.薪资的考虑 154 | 通常情况下,你的薪资是符合市场上相同工作年限的薪资范围的。在薪资范围内,你能拿到多少取决于新公司的内部薪资标准、你的面试表现,以及你以前的工作经历。 155 | 156 | 大部分人的直觉想法都是:「谁给的钱多去谁那儿」,但是我建议大家也综合考虑上面提到的职业成长以及公司的背景。如果一个工作没有职业成长性,那么给得钱再多,我也不建议大家去。 157 | 158 | 应届生的薪资通常都是不能谈的,但是非应届生因为每个候选人情况都不一样,很可能可以谈。 159 | 160 | 5.考虑创业公司 161 | 如果你的学校背景特别好,又在比较好的大公司实习过,那么如果有一些上升期的创业公司,其实也是不错的选择。 162 | 163 | 首先,在这些创业公司里面,你可能获得更多的锻炼机会。然后,你的学校背景和实习经历,使得你即使换工作,也不会因为在创业公司很受影响。最后,也是最重要的是,如果这家公司最终上市,你有可能因此获得极大的期权回报。 164 | 165 | 6.讨论与决策 166 | 以上我提供了一个基于工作和生活侧重点不同的考虑方式,考虑到每个人自身的具体情况,其实你还可以有别的考虑方式。比如有一些人可能父母身体不好,希望更多的离家近;又可能有一些人身体不太好,不希望太累。在你有了你的一套考虑逻辑以及结论之后,我还是建议你找你欣赏的人讨论商量一下,分享一下你的决策思路,也听听他的意见是什么。这些人可能是你的同学,朋友或者是导师,长辈。 167 | 168 | 一个好的决策应该是充分收集信息,并且独立决策的。所以你和少数人的讨论过程,有助于进一步判断你的思路是否有漏洞。一个人的 offer 还是会影响他好几年的职业生涯,所以在这个阶段多收集信息,多思考是非常必要的。但是我们也要切忌人云亦云地做决策,一定要自己独立的做判断,不要简单的盲从权威或者大众,因为没有任何人比你自己掌握的信息更多。 169 | 170 | 7.反馈 171 | 在你做完决定之后,你就需要给公司反馈你的结果了。对于拒绝的 offer,你应该礼貌地表示歉意,并且希望以后可以保持联络。谁知道你未来会不会再次考虑他们的 offer 呢? 172 | 173 | 对于你接受的 offer,你应该更加积极地获取更多的信息。比如看看能否有机会和未来的团队 Leader 见面,了解一下未来工作可能用到的技术栈,提前体验一下公司的产品或者做一些相关技术上的学习。这些都可以帮助你更好更快地融入新的公司和团队。 174 | 175 | 8.心态的调整 176 | 鱼和熊掌不可兼得。很多同学可能在 offer 选择的时候患得患失,最后即使做了决定,也会有后悔的时候。我想说大家还是应该调整好心态,没有哪个公司是完美的,在我们 IT 行业,也很少有人在一个公司待一辈子。所以调整好心态,决定了 offer 之后,就把该放的放下,努力把自己的思想放在未来的工作本身上。因为只有自己的能力提升了,才可能有更大的职业成长空间。 177 | 178 | 小结 179 | 总结一下,你首先要对自己的未来生活有一个规划,是更偏重于家庭和生活,还是更偏重于工作。 180 | 181 | 然后,如果你希望偏重于工作,那么你需要考虑:职业成长性、职业背景、薪资。如果你特别优秀,我也建议你考虑一下创业公司的机会。 182 | 183 | 最后,你的决策过程应该和重要的人讨论,但是决策本身仍然应该由你自己做出。做出决策后,你应该礼貌地拒绝掉不接受的offer,然后对于接受的offer,做进一步的信息沟通和入职准备。 184 | 185 | 如果你正在跳槽或者正准备跳槽不妨动动小手,添加一下咱们的交流群[**101 295 1431**](https://links.jianshu.com/go?to=https%3A%2F%2Fjq.qq.com%2F%3F_wv%3D1027%26k%3D5JFjujE)来获取一份详细的大厂面试资料为你的跳槽多添一份保障。 186 | 187 | ![](https://upload-images.jianshu.io/upload_images/22877992-0bfc037cc50cae7d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 188 | -------------------------------------------------------------------------------- /语言工具-Swift.md: -------------------------------------------------------------------------------- 1 | 本章节主要针对 iOS 的主流开发语言 Objective-C 和 Swift 进行分析和对比,同时也整理了 Xcode 编辑器的使用技巧和经验。 2 | 3 | 正所谓工欲善其事必先利其器,说的就是考察的是开发者对自己手头工具和语言特性的掌握。 4 | 5 | ![](https://upload-images.jianshu.io/upload_images/22877992-f41f6443b65c112b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 6 | 7 | 8 | 在 iOS 开发中,语言的选择是最初的一步。现在苹果主推的编程语言是 Swift。 9 | 10 | Swift 自 2014 年发布以来,已经历经 4 个版本的迭代。在 TIOBE 编程语言排行榜上的目前位列 12 位,超过 Ruby 并远远甩开其上代语言 Objective-C。从性能上来说,它的速度是 Objective-C 的 2.6 倍,Python 的 8.4 倍。更重要的是,Swift 是一门开源的语言,它的质量和进步接受着整个业界的建议、监督、关注。无论从哪个角度讲,Swift 都将取代 Objective-C,成为 iOS 开发的主流语言。 11 | 12 | 所以在面试中,我们会看到关于 Swift 的问题越来越多。对于这门语言的熟悉度,成为判断一个工程师水准的重要标准之一。下面的题目将会从实战和理论两个角度来探讨 Swift 的特性。 13 | 14 | ## 理论题 15 | 16 | ### 1\. 类(class)和结构体(struct)有什么区别? 17 | 18 | **关键词:#引用类型 #值类型** 19 | 20 | 在 Swift 中,类是引用类型,结构体是值类型。值类型在传递和赋值时将进行复制,而引用类型则只会使用引用对象的一个"指向"。所以他们两者之间的区别就是两个类型的区别。 21 | 举个简单的例子,代码如下: 22 | 23 | ``` 24 | class Temperature { 25 | var value: Float = 37.0 26 | } 27 | 28 | class Person { 29 | var temp: Temperature? 30 | 31 | func sick() { 32 | temp?.value = 41.0 33 | } 34 | } 35 | 36 | let A = Person() 37 | let B = Person() 38 | let temp = Temperature() 39 | 40 | A.temp = temp 41 | B.temp = temp 42 | 43 | A.sick() 44 | 45 | ``` 46 | 47 | 上面这段代码,由于 Temperature 是 class ,为引用类型,故 A 的 temp 和 B 的 temp指向同一个对象。A 的 temp修改了,B 的 temp 也随之修改。这样 A 和 B 的 temp 的值都被改成了 41.0。如果将 Temperature 改为 struct,为值类型,则 A 的 temp 修改不影响 B 的 temp。 48 | 49 | 内存中,引用类型诸如类是在堆(heap)上,而值类型诸如结构体是在栈(stack)上进行存储和操作。相比于栈上的操作,堆上的操作更加复杂耗时,所以苹果官方推荐使用结构体,这样可以提高 App 运行的效率。 50 | 51 | 加分回答: 52 | 53 | class 有这几个功能 struct 没有的: 54 | 55 | * class 可以继承,这样子类可以使用父类的特性和方法; 56 | * 类型转换可以在 runtime 的时候检查和解释一个实例的类型; 57 | * 可以用 deinit 来释放资源; 58 | * 一个类可以被多次引用。 59 | 60 | struct 也有这样几个优势: 61 | 62 | * 结构较小,适用于复制操作,相比于一个 class 的实例被多次引用更加安全; 63 | * 无须担心内存 memory leak 或者多线程冲突问题。 64 | 65 | **类似问题:** 66 | 67 | > 引用类型和值类型有什么区别? Struct 相比 class 在使用上有什么优势? 68 | 69 | ### 2\. Swift 是面向对象还是函数式的编程语言? 70 | 71 | **关键词:#面向对象 #函数式编程** 72 | 73 | Swift 既是面向对象的,又是函数式的编程语言。 74 | 75 | 说 Swift 是面向对象的语言,是因为 Swift 支持类的封装、继承、和多态,从这点上来看与 Java 这类纯面向对象的语言几乎毫无差别。 76 | 77 | 说 Swift 是函数式编程语言,是因为 Swift 支持 map, reduce, filter, flatmap 这类去除中间状态、数学函数式的方法,更加强调运算结果而不是中间过程。 78 | 79 | 类似问题: 80 | **为什么说 Swift 是函数式的编程语言?** 81 | 82 | ### 3\. 在 Swift 中,什么是可选型(optional) ? 83 | 84 | **关键词:#Optional #nil** 85 | 86 | 在 Swift 中,可选型是为了表达当一个变量值为空的情况。当一个值为空时,它就是 nil。Swift 中无论是引用类型或是值类型的变量,都可以是可选型变量。举个例子: 87 | 88 | ``` 89 | // 值类型Float,value 默认值为37.0 90 | var value: Float? = 37.0 91 | // 值类型String,key 默认值为 nil 92 | var key: String? = nil 93 | // 引用类型 UIImage,image 默认值为 nil 94 | let image: UIImage? 95 | 96 | ``` 97 | 98 | 加分回答: 99 | 100 | Objective-C 中没有明确提出可选型的概念,然而其引用类型却可以为 nil,以此来标识其变量值为空的情况。Swift 将这一理念扩大到值类型,并且明确提出了可选型的概念。 101 | 102 | ### 4.在 Swift 中,什么是泛型(Generics)? 103 | 104 | **关键词:#泛型** 105 | 106 | 泛型在 Swift 中主要为增加代码的灵活性而生:它可以使得对应的代码满足任意类型的变量或方法。 107 | 108 | 举个简单的例子。我们要写出一个方法,可以交换两个 Int 值,一种写法如下: 109 | 110 | ``` 111 | func swap(_ a: inout Int, _ b: inout Int) { 112 | (a, b) = (b, a) 113 | } 114 | 115 | ``` 116 | 117 | 上面这种写法正确但并不高效。因为假如要再实现一个方法去交换两个 Float 值,那又得重写一遍。泛型就是为了解决这类问题而来,我们希望有一个一般性的方法,可以交换任意类型的变量: 118 | 119 | ``` 120 | func swap(_ a: inout T, _ b: inout T) { 121 | (a, b) = (b, a) 122 | } 123 | 124 | ``` 125 | 126 | 注意,Swift 是类型安全的语言,所以这里交换的两个变量其类型必须一致。 127 | 128 | ### 5\. 请说明并比较以下关键词:Open, Public, Internal, File-private, Private 129 | 130 | **关键词:#访问控制权限** 131 | 132 | Swift 有五个级别的访问控制权限,从高到底依次为比如 Open, Public, Internal, File-private, Private。 133 | 134 | 他们遵循的基本原则是:高级别的变量不允许被定义为低级别变量的成员变量。比如一个 private 的 class 中不能含有 public 的 String。反之,低级别的变量却可以定义在高级别的变量中。比如 public 的 class 中可以含有 private 的 Int。 135 | 136 | * **Open** 具备最高的访问权限。其修饰的类和方法可以在任意 Module 中被访问和重写;它是 Swift 3 中新添加的访问权限。 137 | 138 | * **Public** 的权限仅次于 Open。与 Open 唯一的区别在于它修饰的对象可以在任意 Module 中被访问,但不能重写。 139 | 140 | * **Internal** 是默认的权限。它表示只能在当前定义的 Module 中访问和重写,它可以被一个 Module 中的多个文件访问,但不可以被其他的 Module 中被访问。 141 | 142 | * **File-private** 也是 Swift 3 新添加的权限。其被修饰的对象只能在当前文件中被使用。例如它可以被一个文件中的不同 class,extension,struct 共同使用。 143 | 144 | * **Private** 是最低的访问权限。它的对象只能在定义的作用域内及其对应的扩展内使用。离开了这个对象,即使是同一个文件中的对象,也无法访问。 145 | 146 | 类似问题: 147 | **Swift 3 中新引入的 Open 和 File-private 关键词有什么用?** 148 | 149 | ### 6\. 请说明并比较以下关键词:strong, weak, unowned 150 | 151 | **关键词:#引用类型 #内存管理** 152 | 153 | Swift 的内存管理机制与 Objective-C一样为 ARC(Automatic Reference Counting)。它的基本原理是,一个对象在没有任何强引用指向它时,其占用的内存会被回收。反之,只要有任何一个强引用指向该对象,它就会一直存在于内存中。 154 | 155 | * **strong** 代表着强引用,是默认属性。当一个对象被声明为 strong 时,就表示父层级对该对象有一个强引用的指向。此时该对象的引用计数会增加1。 156 | 157 | * **weak** 代表着弱引用。当对象被声明为 weak 时,父层级对此对象没有指向,该对象的引用计数不会增加1。它在对象释放后弱引用也随即消失。继续访问该对象,程序会得到 nil,不会崩溃。 158 | 159 | * **unowned** 与弱引用本质上一样。唯一不同的是,对象在释放后,依然有一个无效的引用指向对象,它不是 Optional 也不指向 nil。如果继续访问该对象,程序就会崩溃。 160 | 161 | **加分回答:** 162 | 163 | weak 和 unowned 的引入是为了解决由 strong 带来的循环引用问题。简单来说,就是当两个对象互相有一个强指向去指向对方,这样导致两个对象在内存中无法释放(详情请参考第3章第3节第8题)。 164 | 165 | weak 和 unowned 的使用场景有如下差别: 166 | 167 | * 当访问对象时该对象可能已经被释放了,则用 weak。比如 delegate 的修饰。 168 | 169 | * 当访问对象确定不可能被释放,则用 unowned。比如 self 的引用。 170 | 171 | * 实际上为了安全起见,很多公司规定任何时候都使用 weak 去修饰。 172 | 173 | ### 7\. 在 Swift 中,怎样理解是 copy-on-write? 174 | 175 | **关键词:#内存管理** 176 | 177 | 当值类型比如 struct 在复制时,复制的对象和原对象实际上在内存中指向同一个对象。当且仅当复制后的对象进行修改的时候,才会在内存中重新创建一个新的对象。举个例子: 178 | 179 | ``` 180 | // arrayA 是一个数组,为值类型 181 | let arrayA = [1, 2, 3] 182 | // arrayB 这个时候与 arrayA 在内存中是同一个东西,内存中并没有生成新的数组 183 | var arrayB = arrayA 184 | // arrayB 被修改了,此时 arrayB 在内存中变成了一个新的数组,而不是原来的 arrayA 185 | arrayB.append(4) 186 | 187 | ``` 188 | 189 | 上面的代码中我们可以看出,复制的数组和原数组共享同一个地址直到其中之一发生改变。这样设计使得值类型可以多次复制而无需耗费多余内存,只有变化的时候才会增加开销。**因此内存的使用更加高效**。 190 | 191 | ### 8\. 什么是属性观察(Property Observer)? 192 | 193 | **关键词:#willSet #didSet** 194 | 195 | 属性观察是指在当前类型内对特定属性进行监视,并作出响应的行为。它是 Swift 的特性,有两种,为 willSet 和 didSet。举个例子: 196 | 197 | ``` 198 | var title: String { 199 | willSet { 200 | print("将标题从\(title)设置到\(newValue)") 201 | } 202 | didSet { 203 | print("已将标题从\(oldValue)设置到\(title)") 204 | } 205 | } 206 | 207 | ``` 208 | 209 | 上面这段代码对于 title 做了监听。当 title 发生改变前,willSet 对应的作用域将被执行,新的值是 newValue;当 title 发生改变之后,didSet 对应的作用域将被执行,原来的值为 oldValue。这就是属性观察。 210 | 211 | **加分回答:** 212 | 213 | 初始化方法对属性的设定,以及在 willSet 和 didSet 中对属性的再次设定都不会触发属性观察的调用。 214 | 215 | ## Swift 面试实战题 216 | 217 | ### 9\. 结构体中修改成员变量的方法 218 | 219 | **关键词:#mutating** 220 | 请问下面代码有什么问题? 221 | 222 | ``` 223 | protocol Pet { 224 | var name: String { get set } 225 | } 226 | 227 | struct MyDog: Pet { 228 | var name: String 229 | 230 | func changeName(name: String) { 231 | self.name = name 232 | } 233 | } 234 | 235 | ``` 236 | 237 | 应该在 func changeName(name: String) 前加上关键词 mutating,表示该方法将会修改结构体中自己的成员变量。 238 | 239 | > 注意,在设计协议的时候,由于protocol 可以被 class 和 struct 或者 enum 实现,故而要考虑是否用 mutating 来修饰方法。 240 | 241 | 类(class)中不存在这个问题,因为类可以随意修改自己的成员变量。 242 | 243 | ### 10\. 用 Swift 实现或(||)操作 244 | 245 | **关键词:#autoclosure** 246 | 247 | 这题解法很多,下面给出一种最直接的解法: 248 | 249 | ``` 250 | func ||(left: Bool, right: Bool) –> Bool { 251 | if left { 252 | return true 253 | } else { 254 | return right 255 | } 256 | } 257 | 258 | ``` 259 | 260 | 上面这种解法勉强正确,但是并不高效。或(||)操作的本质是当左边为真的时候,我们无需计算右边。而上面这种事先,是将右边默认值预先准备好,再传入进行操作。当右边值的计算十分复杂时会 造成了性能上的浪费。所以,上面这种做法违反了或(||)操作的本质。正确的实现方法如下: 261 | 262 | ``` 263 | func ||(left: Bool, right: @autoclosure () -> Bool) –> Bool { 264 | if left { 265 | return true 266 | } else { 267 | return right() 268 | } 269 | } 270 | 271 | ``` 272 | 273 | autoclosure 可以将右边值的计算推迟到判定 left 为 false 的时候,这样就可以避免第一种方法带来的不必要开销了。 274 | 275 | ### 11\. 实现一个函数。输入是任一整数,输出要返回输入的整数+ 2 276 | 277 | **关键词:#柯里化** 278 | 279 | 这道题看似简单,直接这样写: 280 | 281 | ``` 282 | func addTwo(_ num: Int) -> Int { 283 | return num + 2 284 | } 285 | 286 | ``` 287 | 288 | 接下来面试官会说,那假如我要实现 + 4 呢?程序员想了一想,又定义了另一个方法: 289 | 290 | ``` 291 | func addFour(_ num: Int) -> Int { 292 | return num + 4 293 | } 294 | 295 | ``` 296 | 297 | 这时面试官会问,假如我要实现返回 + 6, + 8 的操作呢?能不能只定义一次方法呢?正确的写法是利用 298 | Swift 的 Currying 特性: 299 | 300 | ``` 301 | func add(_ num: Int) -> (Int) -> Int { 302 | return { val in 303 | return num + val 304 | } 305 | } 306 | 307 | let addTwo = add(2), addFour = add(4), addSix = add(6), addEight = add(8) 308 | 309 | ``` 310 | 311 | Swift 中的柯里化(柯里化) 特性是函数式编程思想的体现。它将接受多个参数的方法进行变形,并用高阶函数的方式进行处理,使整个代码更加灵活。 312 | 313 | ### 12\. 实现一个函数。求 0 到 100(包括0和100)以内是偶数并且恰好是其他数字平方的数。 314 | 315 | **关键词:#函数式编程** 316 | 317 | 这道题十分简单。最简单粗暴的写法如下: 318 | 319 | ``` 320 | func evenSquareNums(from: Int, to: Int) -> [Int] { 321 | var res = [Int]() 322 | 323 | for num in from...to where num % 2 == 0{ 324 | if (from...to).contains(num * num) { 325 | res.append(num * num) 326 | } 327 | } 328 | 329 | return res 330 | } 331 | 332 | evenSquareNums(from: 0, to: 100) 333 | 334 | ``` 335 | 336 | 上面的写法正确,但不够优雅。首先这个方法完全可以利用泛型进行优化,同时可以在创建 res 数组时加上 reserveCapacity 以保证其性能。其实面对这个题目,最简单直接的写法是用函数式编程的思路,一行既可以解决问题: 337 | 338 | ``` 339 | (0...10).map { $0 * $0 }.filter { $0 % 2 == 0 } 340 | 341 | ``` 342 | 343 | Swift 有函数式编程的思想。其中 flatMap, map, reduce, filter 是其代表的方法。本题中考察了 map 344 | 和 filter 的组合使用。相比于一般的 for 循环,这样的写法要更加得简洁漂亮。 345 | 346 | # 推荐👇: 347 | 348 | 如果你正在跳槽或者正准备跳槽不妨动动小手,添加一下咱们的交流群[**931 542 608**](https://jq.qq.com/?_wv=1027&k=cfVxrMAH)来获取一份详细的大厂面试资料为你的跳槽多添一份保障。 349 | 350 | ![](https://upload-images.jianshu.io/upload_images/22877992-0bfc037cc50cae7d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 351 | -------------------------------------------------------------------------------- /语言工具-Objective-C.md: -------------------------------------------------------------------------------- 1 | Objective-C 是苹果为 iOS 和 Mac 开发量身定制的语言。它随着 iPhone 的出现而大火,直到今天国内外大多数的 App 依然是用 Objective-C 在写。 2 | 3 | ![](https://upload-images.jianshu.io/upload_images/22877992-79869bcea6d496de.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 4 | 5 | Objective-C 一度在 TIOBE 排行榜上位列第 3 名,仅次于 Java 和 C。其市场占有份额也远超其他语言。看名字我们可以知道,它与 C 语言有千丝万缕的联系,事实上也确实如此:Objective-C 是 C 语言的超集,它在 C 语言主体上加上了面向对象的特性。这是为了 App 开发的方便,同时也兼顾了语言的整体性能。 6 | 7 | 现在的面试中,传统大厂如 BAT 对 Objective-C 的语言进行较多考察,日常开发也是以 Objective-C 为主。 本章将探讨 Objective-C 的基本语言特性,其动态特性将与 Swift 比较中设计。 8 | 9 | ## Objective-C 面试理论题 10 | 11 | ### 1.什么是 ARC? 12 | 13 | **关键词:#内存管理** 14 | 15 | ARC全称是Automatic Reference Counting,是Objective-C和Swift的内存管理机制。它是根据对象的引用计数来判断当前对象的生命周期:当有一个新的指针指向这个对象时,我们将其引用计数加 1,当某个指针不再指向这个对象时,我们将其引用计数减 1,当对象的引用计数变为 0 时,说明这个对象不再被任何指针指向了,这个时候我们就可以将对象销毁,回收内存。 16 | 17 | 简单地来说,就是代码中自动加入了 retain/release,原先需要手动添加的用来处理内存管理的引用计数的代码可以自动地由编译器完成了。 18 | 19 | ARC 的使用是为了解决对象 retain 和 release 匹配的问题。以前手动管理造成内存泄漏或者重复释放的问题将不复存在。 20 | 21 | **加分回答:** 22 | 23 | 以前需要手动的通过 retain 去为对象获取内存,并用 release 释放内存。所以以前的操作称为 MRC (Manual Reference Counting)。 24 | 25 | ARC 与 Garbage Collection 的区别在于 Garbage Collection 在 runtime 时管理内存,可以解决 retain cycle,而 ARC 在 compile time 时管理内存。 26 | 27 | **类似问题:** 28 | 29 | > Objective-C 的内存管理机制是什么? 30 | 31 | ### 2.什么情况下会出现循环引用? 32 | 33 | **关键词:#内存管理** 34 | 35 | 循环引用是指 2 个或以上对象互相强引用,导致所有对象无法释放的现象。这是内存泄漏的一种情况。举个例子: 36 | 37 | ``` 38 | === class Father === 39 | @interface Father: NSObject 40 | @property (strong, nonatomic) Son *son; 41 | @end 42 | === class Son === 43 | @interface Son: NSObject 44 | @property (strong, nonatomic) Father *father; 45 | @end 46 | 47 | ``` 48 | 49 | 上述代码有两个类,分别为爸爸和儿子。爸爸对儿子强引用,儿子对爸爸强引用。这样释放儿子必须先释放爸爸,要释放爸爸必须先释放儿子。如此一来,两个对象都无法释放。 50 | 51 | 解决方法是将 Father 中的 Son 对象属性从 strong 改为 weak。 52 | 53 | **加分回答:** 54 | 55 | 内存泄漏可以用 Xcode 中的 Debug Memory Graph 去检查: 56 | ![image](https://upload-images.jianshu.io/upload_images/22877992-06bc21a2aae0dd1b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 57 | 58 | 同时 Xcode 也会在 runtime 中自动汇报内存泄漏的问题: 59 | ![image](https://upload-images.jianshu.io/upload_images/22877992-659e793fa8098a72.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 60 | 61 | ### 3.请说明并比较以下关键词:strong, weak, assign, copy 62 | 63 | **关键词:#内存管理 #引用类型** 64 | 65 | * `strong` 表示指向并拥有该对象。其修饰的对象引用计数会增加 1。该对象只要引用计数不为 0 则不会被销毁。当然强行将其设为 nil 可以销毁它。 66 | * `weak` 表示指向但不拥有该对象。其修饰的对象引用计数不会增加。无需手动设置,该对象会自行在内存中销毁。 67 | * `assign` 主要用于修饰基本数据类型,如 NSInteger 和 CGFloat ,这些数值主要存在于栈上。 68 | * `weak` 一般用来修饰对象,`assign` 一般用来修饰基本数据类型。原因是 assign 修饰的对象被释放后,指针的地址依然存在,造成野指针,在堆上容易造成崩溃。而栈上的内存系统会自动处理,不会造成野指针。 69 | * `copy` 与 `strong` 类似。不同之处是 `strong` 的复制是多个指针指向同一个地址,而 `copy` 的复制每次会在内存中拷贝一份对象,指针指向不同地址。`copy` 一般用在修饰有可变对应类型的不可变对象上,如 NSString, NSArray, NSDictionary。 70 | 71 | **加分回答:** 72 | 73 | Objective-C 中,基本数据类型的默认关键字是 atomic, readwrite, assign;普通属性的默认关键字是 atomic, readwrite, strong。 74 | 75 | ### 4.请说明并比较以下关键词:`atomic`, `nonatomic` 76 | 77 | **关键词:#线程** 78 | 79 | * `atomic` 修饰的对象会保证 setter 和 getter 的完整性,任何线程对其访问都可以得到一个完整的初始化后的对象。因为要保证操作完成,所以速度慢。它比 `nonatomic` 安全,但也并不是绝对的线程安全,例如多个线程同时调用 set 和 get 就会导致获得的对象值不一样。绝对的线程安全就要用 `@synchronized`。 80 | 81 | * `nonatomic` 修饰的对象不保证 setter 和 getter 的完整性,所以多个线程对它进行访问,它可能会返回未初始化的对象。正因为如此,它比 `atomic` 快,但也是线程不安全的。 82 | 83 | **类似问题:** 84 | 85 | > `atomic` 是百分之百线程安全的吗? 86 | 87 | ### 5.runloop 和线程有什么关系? 88 | 89 | **关键词:#线程 #runloop** 90 | 91 | runloop 是每一个线程一直运行的一个对象,它主要用来负责响应需要处理的各种事件和消息。每一个线程都有且仅有一个 runloop 与其对应,没有线程,就没有 runloop。 92 | 93 | 其中所有线程中,只有主线程的 runloop 是默认启动的,main 函数会设置一个 NSRunLoop 对象。其他线程,runloop 默认是没有启动的,我们可以通过 `[NSRunLoop currentRunLoop]` 来获得。 94 | 95 | ### 6.请说明并比较以下关键词:`__weak`,`__block` 96 | 97 | **关键词:#变量修改 #block** 98 | 99 | `__weak` 与 `weak` 基本相同。前者用于修饰变量(variable),后者用于修饰属性(property)。`__weak` 主要用于防止 block 中的循环引用。 100 | `__block` 也用于修饰变量。它是引用修饰,所以其修饰的值是动态变化的,即可以被重新赋值的。`__block`用于修饰某些 block 内部将要修改的外部变量。 101 | 102 | **加分回答:** 103 | 104 | `__weak` 和 `__block` 的使用场景几乎与 block 息息相关。而所谓 block,就是 Objective-C 对于闭包的实现。闭包就是没有名字的函数,或者理解为指向函数的指针。 105 | 106 | ### 7.什么是 block?它和代理的区别是什么? 107 | 108 | **关键词:#回调** 109 | 110 | 在 iOS 开发中,block 和代理都是回调的方式。Block 是一段封装好的代码,例如下面这个最简单的例子: 111 | 112 | ``` 113 | // 动画结束后的内容就是一个 block 114 | [UIView animateWithDuration:1.0 animations:^{ 115 | NSLog(@”动画已经完成”); 116 | }]; 117 | 118 | // 声明一个名为 sum 的 block,返回两个整型数值之和 119 | NSInteger (^sumOfNumbers)(NSInteger a, NSInteger b) = ^( NSInteger a, NSInteger b) { 120 | return a + b; 121 | }; 122 | 123 | ``` 124 | 125 | 而代理的声明和实现一般分开,比如我们熟悉的 UITableViewDelegate 就是代理声明在 UITableView 中,实现在某个 UIViewController 中。 126 | 127 | 两者的区别首先在于 block 集中代码块而代理分散代码块,所以 block 更适用于轻便、简单的回调,如网络传输。而代理适用于公共接口较多,这样做也更易于解耦代码架构。 128 | 129 | 另一个区别在于 block 运行成本高。block 出栈需要将使用的数据从栈内存拷贝到堆内存,当然对象的话就是加计数,使用完或者 block 置 nil 后才消除;delegate 只是保存了一个对象指针,直接回调,没有额外消耗。相对 C 的函数指针,只多做了一个查表动作。 130 | 131 | 注意 `block` 容易造成循环引用,解决方法是用 `__weak` 关键词修饰变量构成弱引用。 132 | 133 | ## Objective-C 面试实战题 134 | 135 | ### 8.属性声明代码风格考查 136 | 137 | **关键词:#属性声明** 138 | 139 | ``` 140 | @property (nonatomic, strong) NSString *title; 141 | @property (assign, nonatomic) int workID; 142 | 143 | ``` 144 | 145 | * title 不应该用 strong 来修饰,应该用 copy。因为 NSString 是不可变的数据类型,它有对应的 NSMutableString 的数据类型,用 strong 来修饰会有 NSString 被修改的可能性。举个例子: 146 | 147 | ``` 148 | self.title = @”title”; 149 | NSMutableString *mutableTitle = @”mutableTitle”; 150 | self.title = mutableTitle; 151 | 152 | ``` 153 | 154 | 原来 title 的值是 ”title”,结果后来被改成了 ”mutableTitle”。 155 | 有对应可变数据类型的不可变数据类型都应该修饰为 copy。copy 表示该属性不保留新值,而是将其拷贝。这样一来,属性的封装性就可以得到保护,其对应的值是不会无意间被修改的。如果对可变类型如 NSMutableString 用 copy 来修饰,那么当对其进行修改时,程序会崩溃。 156 | 157 | * workID 不应该用 int,而应该用 NSInteger。Int 只表示 32 位的整型数,而 NSInteger 在 32 位机器上与 int 一样,在 64 位机器上则是 64 位的整型数。对于不同类型机器,NSInteger 更加灵活和准确。同理请用 NSUInteger 替代 unsigned,CGFloat 替代 float。 158 | * 属性声明时,最好遵循原子性,读写,内存管理的顺序。这样可读性更高。正确的写法如下: 159 | 160 | ``` 161 | @property (nonatomic, copy) NSString *title; 162 | @property (nonatomic, assign) NSInteger workID; 163 | 164 | ``` 165 | 166 | ### 9.架构解耦代码考查 167 | 168 | **关键词:#属性声明 #架构解耦** 169 | 170 | ``` 171 | typedef enum { 172 | Normal; 173 | VIP; 174 | } CustomerType; 175 | 176 | @Interface Customer: NSObject 177 | 178 | @property (nonatomic, copy) NSString *name; 179 | @property (nonatomic, strong) UIImage *profileImage; 180 | @property (nonatomic, assign) CustomerType customerType; 181 | 182 | @end 183 | 184 | ``` 185 | 186 | * enum 定义的写法不够好。苹果官方推荐使用 NS_ENUM 来定义枚举。同时枚举的每个类型前应加上 enum 的名称,这样方便混编时直接在 Swift 中调用。 187 | 188 | * UIImage 不应该出现在 Customer 中。Customer 明显是一个 Model 类,UIImage 应该归属于 View 部分,无论是 MVC 还是 MVVM 亦或是 VIPER,Model 都应该和 View 划清界限,避免整个架构耦合。下面是正确的代码: 189 | 190 | ``` 191 | typedef NS_ENUM(NSInteger, CustomerType){ 192 | CustomerTypeNormal; 193 | CustomerTypeVIP; 194 | }; 195 | 196 | @Interface Customer: NSObject 197 | 198 | @property (nonatomic, copy) NSString *name; 199 | @property (nonatomic, strong) NSData *profileImageData; 200 | @property (nonatomic, assign) CustomerType customerType; 201 | 202 | @end 203 | 204 | ``` 205 | 206 | ### 10.语法考察:请问下面代码打印出什么? 207 | 208 | **关键词:#内存管理 #引用类型** 209 | 210 | ``` 211 | NSString *firstStr = @”helloworld”; 212 | NSString *secondStr = @”helloworld”; 213 | 214 | if (firstStr == secondStr) { 215 | NSLog(@”Equal”); 216 | } else { 217 | NSLog(@”Not Equal”); 218 | } 219 | 220 | ``` 221 | 222 | 打印出Equal。 223 | 224 | * == 这个符号判断的不是这两个值是否相等,而是这两个指针是否指向同一个对象。如果要判断两个 NSString 是否值相同,平时开发应该用 isEqualToString 这个方法。 225 | * 上面的代码中,两个指针指向不同的对象,尽管它们的值相同。但是 iOS 的编译器优化了内存分配,当两个指针指向两个值一样的 NSString 时,两者指向同一个内存地址。所以这道题会进入 if 的判断,打印出 "Equal" 字符串。 226 | 227 | ### 11.语法考察:下面代码中有什么 bug? 228 | 229 | **关键词:#多线程** 230 | 231 | ``` 232 | - (void)viewDidLoad { 233 | UILabel *alertLabel = [[UILabel alloc] initWithFrame:CGRectMake(100,100,100,100)]; 234 | alertLabel.text = @"Wait 4 seconds..."; 235 | [self.view addSubview:alertLabel]; 236 | 237 | NSOperationQueue *backgroundQueue = [[NSOperationQueue alloc] init]; 238 | [backgroundQueue addOperationWithBlock:^{ 239 | [NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:4]]; 240 | alertLabel.text = @"Ready to go!” 241 | }]; 242 | } 243 | 244 | ``` 245 | 246 | Bug 在于,在等了 4 秒之后,alertLabel 并不会更新为 Ready to Go。 247 | 248 | 原因是,所有 UI 的相关操作应该在主线程进行。当我们可以在一个后台线程中等待 4 秒,但是一定要在主线程中更新 alertLabel。 249 | 250 | 最简单的修正如下: 251 | 252 | ``` 253 | - (void)viewDidLoad { 254 | UILabel *alertLabel = [[UILabel alloc] initWithFrame:CGRectMake(100,100,100,100)]; 255 | alertLabel.text = @"Wait 4 seconds..."; 256 | [self.view addSubview:alertLabel]; 257 | 258 | NSOperationQueue *backgroundQueue = [[NSOperationQueue alloc] init]; 259 | [backgroundQueue addOperationWithBlock:^{ 260 | [NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:4]]; 261 | [[NSOperationQueue mainQueue] addOperationWithBlock:^{ 262 | alertLabel.text = @"Ready to go!” 263 | }]; 264 | }]; 265 | } 266 | 267 | ``` 268 | 269 | ### 12.以 scheduledTimerWithTimeInterval 的方式触发的 timer,在滑动页面上的列表时,timer 会暂停,为什么?该如何解决? 270 | 271 | **关键词:#线程 #runloop** 272 | 273 | 原因在于滑动时当前线程的 runloop 切换了 mode 用于列表滑动,导致 timer 暂停。 274 | 275 | runloop 中的 mode 主要用来指定事件在 runloop 中的优先级,有以下几种: 276 | 277 | * Default(NSDefaultRunLoopMode):默认,一般情况下使用; 278 | * Connection(NSConnectionReplyMode):一般系统用来处理 NSConnection 相关事件,开发者一般用不到; 279 | * Modal(NSModalPanelRunLoopMode):处理 modal panels 事件; 280 | * Event Tracking(NSEventTrackingRunLoopMode):用于处理拖拽和用户交互的模式。 281 | * Common(NSRunloopCommonModes):模式合集。默认包括 Default,Modal,Event Tracking 三大模式,可以处理几乎所有事件。 282 | 283 | 回到题中的情境。滑动列表时,runloop 的 mode 由原来的 Default 模式切换到了 Event Tracking 模式,timer 原来好好的运行在 Default 模式中,被关闭后自然就停止工作了。 284 | 285 | 解决方法其一是将 timer 加入到 NSRunloopCommonModes 中。其二是将 timer 放到另一个线程中,然后开启另一个线程的 runloop,这样可以保证与主线程互不干扰,而现在主线程正在处理页面滑动。示例代码如下: 286 | 287 | ``` 288 | // 方法1 289 | [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; 290 | 291 | // 方法2 292 | dispatch_async(dispatch_get_global_queue(0, 0), ^{ 293 | timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(repeat:) userInfo:nil repeats:true]; 294 | [[NSRunLoop currentRunLoop] run]; 295 | }); 296 | 297 | ``` 298 | # 推荐👇: 299 | 300 | 如果你正在跳槽或者正准备跳槽不妨动动小手,添加一下咱们的交流群[**931 542 608**](https://jq.qq.com/?_wv=1027&k=cfVxrMAH)来获取一份详细的大厂面试资料为你的跳槽多添一份保障。 301 | 302 | ![](https://upload-images.jianshu.io/upload_images/22877992-0bfc037cc50cae7d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 303 | 304 | -------------------------------------------------------------------------------- /系统框架-网络、推送与数据处理.md: -------------------------------------------------------------------------------- 1 | 如果说移动时代的前身是什么,我想一个可能的答案就是网络时代。网络的兴起,让所有设备相连成为了可能,也催生了电商、社交、搜索等多个领域的商业巨头。而移动时代,则是网络时代的必然延伸,它代表着更便捷、更广阔、更深入的连接。 2 | 3 | ![](https://upload-images.jianshu.io/upload_images/22877992-9335ea7f41cfd8f9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 4 | 5 | 在这个背景之下,我们所开发的 App 或多或少会与网络相连。或是拉取服务器端数据来更新 UI,或是通过网络推送自己的消息,或是在手机端删除自己曾经的照片,或是打开音乐播放应用下载自己喜欢的歌曲。如何请求、接收、处理、发送数据,就是我们这节要讨论的内容。 6 | 7 | ## 计算机理论 8 | 9 | ### 1.谈谈 HTTP 中 GET 与 POST 的区别 10 | 11 | **关键词:#方向 #类型 #参数位置** 12 | 13 | * 从方向上来看,GET 是从服务器端获取信息,POST 是向服务器端发送信息。 14 | 15 | * 从类型上来看,GET 处理静态和动态内容,POST 只处理动态内容。 16 | 17 | * 从参数位置来看,GET 的参数在其 URI 里,POST 的参数在它的包体里:从这个角度来看,POST 比 GET 更加安全隐秘。 18 | 19 | * GET 可以被缓存,可以被储存在浏览器历史中,其内容理论上有长度限制;POST 在这 3 点上恰恰相反。 20 | 21 | ### 2.谈谈 Session,Token,Cookie 的概念 22 | 23 | **关键词:#用户认证 #客户端 #服务器端** 24 | 25 | * Session 是服务器端用来认证、追踪用户的数据结构。它通过判断客户端传来的信息确定用户,确定用户的唯一标识是客户端传来的 Session ID。 26 | 27 | * Token 是服务器端生成的一串字符串,是客户端进行请求的令牌、服务器端用以确定用户的唯一标识。Session ID 就经常被用作 Token 来使用。Token的出现避免了服务器频繁的查询用户名和密码,降低了数据库的查询压力。 28 | 29 | * Cookie 是客户端保存用户信息的机制。初次会话 HTTP 协议会在 Cookie 里记录一个 Session ID ,之后每次把 Session ID 发给服务器端。 30 | 31 | * Session 一般用于用户验证。它默认存在服务器的一个文件里,当然内存、数据库里也可以存储。 32 | 33 | * 若是客户端禁用了 Cookie,客户端会用 URL 重写技术,即会话时在 URL 的末尾加上 Session ID,并发送给服务器端。 34 | 35 | ### 3.在一个 HTTPS 连接的网站里,输入账号密码点击登录后,到服务器返回这个请求前,中间经历了什么 36 | 37 | **关键词:#锁 #客户端 #服务器端** 38 | 39 | **1) 客户端打包请求。**包括 url,端口啊,你的账号密码等等。账号密码登陆应该用的是 Post 方式,所以相关的用户信息会被加载到 body 里面。这个请求应该包含三个方面:网络地址,协议,资源路径。注意,这里是 HTTPS,就是 HTTP + SSL / TLS,在 HTTP 上又加了一层处理加密信息的模块(相当于是个锁)。这个过程相当于是客户端请求钥匙。 40 | 41 | **2) 服务器接受请求。**一般客户端的请求会先发送到 DNS 服务器。 DNS 服务器负责将你的网络地址解析成 IP 地址,这个 IP 地址对应网上一台机器。这其中可能发生 Hosts Hijack 和 ISP failure 的问题。过了 DNS 这一关,信息就到了服务器端,此时客户端会和服务器的端口之间建立一个 socket 连接,socket 一般都是以 file descriptor 的方式解析请求。这个过程相当于是服务器端分析是否要向客户端发送钥匙模板。 42 | 43 | **3) 服务器端返回数字证书。**服务器端会有一套数字证书(相当于是个钥匙模板),这个证书会先发送给客户端。这个过程相当于是服务器端向客户端发送钥匙模板。 44 | 45 | **4) 客户端生成加密信息。**根据收到的数字证书(钥匙模板),客户端会生成钥匙,并把内容锁上,此时信息已经加密。这个过程相当于客户端生成钥匙并锁上请求。 46 | 47 | **5) 客户端发送加密信息。**服务器端会收到由自己发送出去的数字证书加锁的信息。 这个时候生成的钥匙也一并被发送到服务器端。这个过程是相当于客户端发送请求。 48 | 49 | **6) 服务器端解锁加密信息。**服务器端收到加密信息后,会根据得到的钥匙进行解密,并把要返回的数据进行对称加密。这个过程相当于服务器端解锁请求、生成、加锁回应信息。 50 | 51 | **7) 服务器端向客户端返回信息。**客户端会收到相应的加密信息。这个过程相当于服务器端向客户端发送回应。 52 | 53 | **8) 客户端解锁返回信息。**客户端会用刚刚生成的钥匙进行解密,将内容显示在浏览器上。 54 | 55 | 如果你正在跳槽或者正准备跳槽不妨动动小手,添加一下咱们的交流群[**931 542 608**](https://jq.qq.com/?_wv=1027&k=cfVxrMAH)来获取一份详细的大厂面试资料为你的跳槽多添一份保障。 56 | 57 | 整个过程的流程图如下: 58 | ![image](https://upload-images.jianshu.io/upload_images/22877992-542b2a415bed31d0.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 59 | 60 | ## iOS 网络请求 61 | 62 | ### 4.请说明并比较以下类:URLSessionTask,URLSessionDataTask,URLSessionUploadTask,URLSessionDownloadTask 63 | 64 | **关键词:#URLSession** 65 | 66 | * URLSessionTask 是个抽象类。通过实现它可以实例化任意网络传输任务,诸如请求、上传、下载任务。它的暂停(cancel)、继续(resume)、终止(suspend)方法有默认实现 67 | 68 | * URLSessionDataTask 负责 HTTP GET 请求。它是 URLSessionTask 的具体实现。一般用于从服务器端获取数据,并存放在内存中。 69 | 70 | * URLSessionUploadTask 负责 HTTP Post/Put 请求。它继承了 URLSessionDataTask。一般用于上传数据。 71 | 72 | * URLSessionDownloadTask 负责下载数据。它是 URLSessionTask 的具体实现。它一般将下载的数据保存在一个临时的文件中;在 cancel 后可将数据保存,并之后继续下载。 73 | 74 | 它们之间的关系如下图: 75 | ![image](https://upload-images.jianshu.io/upload_images/22877992-4046cf6f66dd1412.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 76 | 77 | ### 5\. 什么是 Completion Handler? 78 | 79 | **关键词:#闭包** 80 | 81 | Completion Handler 一般用于处理 API 请求之后的返回数据。 82 | 83 | 当URLSessionTask 结束之后,无论成功或是报错,Completion Handler 一般都会接受 3 个参数:Data, URLResponse,Error,注意这 3 个参数都是 Optional。 84 | 85 | 在 Swift 中,Completion Handler 必须标明 `@escaping`。因为它总是在 API 请求之后才执行,也就是说方法已经返回才会涉及 Completion Handler,是个经典的逃逸闭包情况。 86 | 87 | ### 6\. 代码实战:设计一个方法,给定 API 的网址,返回用户数据 88 | 89 | **关键词:#URLSessionDataTask** 90 | 91 | 这道题目考察的是 URLSessionDataTask 的基本用法。下面是一种最简单粗暴的写法: 92 | 93 | ``` 94 | func queryUser(url: String, completion: @escaping (_ user: User?, _ error : Error?) -> Void)) { 95 | 96 | guard let url = URL(string: url) else { 97 | return 98 | } 99 | 100 | URLSession.shared.dataTask(with: url) { data, response, error in 101 | if let error = error { 102 | DispatchQueue.main.async { 103 | completion(nil, error) 104 | } 105 | return 106 | } else if let data = data, let response = response as? HTTPURLResponse { 107 | DispatchQueue.main.async { 108 | completion(convertDataToUser(data), nil) 109 | } 110 | } else { 111 | DispatchQueue.main.async { 112 | completion(nil, NSError(“invalid response”)) 113 | } 114 | } 115 | }.resume() 116 | } 117 | 118 | ``` 119 | 120 | 上面的写法有很多问题,其中最主要的是: 121 | 122 | * url 出错处理不当。应该返回错误信息以方便日后调试,而不是应该 return 123 | 124 | * 用 URLSession 的单例不妥。这样每次请求创建一个 dataTask 是一种浪费,同时短时间内多次请求会不必要的造成服务器压力。正确的处理方法应该是每次请求都取消上一次请求(无论有无完成)。 125 | 126 | * 代码重复冗余。代码中多次用到了切换至主线程并调用闭包的过程。实际上我们可以将整个方法扩展为一个类,然后将返回值与成员变量结合起来使用。 127 | 128 | 除了以上 3 点,我们还可以进一步修正代码,增强其可读性,并完善其逻辑。修改后的代码如下: 129 | 130 | ``` 131 | enum QueryError: String { 132 | case InvaldURL = “Invalid URL”, 133 | case InvalidResponse = “Invalid response” 134 | } 135 | 136 | class QueryService { 137 | typealias QueryResult = (User?, String?) -> Void 138 | 139 | var user: User? 140 | var errorMessage: String? 141 | 142 | let defaultSession = URLSession(configuration: .default) 143 | var dataTask: URLSessionDataTask? 144 | 145 | func queryUsers(url: String, completion: @escaping QueryResult) { 146 | dataTask?.cancel() 147 | 148 | guard let url = URL(string: url) else { 149 | DispatchQueue.main.async { 150 | completion(user, QueryError.InvalidURL) 151 | } 152 | return 153 | } 154 | 155 | dataTask = defaultSession.dataTask(with: url) { [weak self] data, response, error in 156 | defer { 157 | self?.dataTask = nil 158 | } 159 | if let error = error { 160 | self?.errorMessage = error.localizedDescription 161 | } else if let data = data, 162 | let response = response as? HTTPURLResponse, 163 | response.statusCode == 200 { 164 | self?.user = convertDataToUser(data) 165 | } else { 166 | self?.errorMessage = QueryError.InvalidResponse 167 | } 168 | 169 | DispatchQueue.main.async { 170 | completion(self?.user,self?.errorMessage) 171 | } 172 | }.resume() 173 | } 174 | } 175 | 176 | ``` 177 | 178 | 上面的修改方法主要针对一些硬伤。如果配合 Swift 的面向协议的编程来实现该 API,整个代码会更加灵活。 179 | 180 | ## 信息推送 181 | 182 | ### 7\. iOS 开发中本地消息通知的流程是怎样的? 183 | 184 | **关键词:#UserNotifications** 185 | 186 | UserNotifications 框架是苹果针对远程和本地消息通知的框架。其流程主要分 4 步: 187 | 188 | **1) 注册。**通过调用 requestAuthorization 这个方法,通知中心会向用户发送通知许可请求。在弹出的 Alert 中点击同意,即可完成注册。 189 | **2) 创建。**首先设置信息内容 UNMutableNotificationContent 和触发机制 UNNotificationTrigger ;然后用这两个值来创建 UNNotificationRequest;最后将 request 加入到当前通知中心 UNUserNotificationCenter.current() 中。 190 | **3) 推送。**这一步就是系统或者远程服务器推送通知。伴随着一声清脆的响声(或自定义的声音),通知对应的 UI 显示到手机界面的过程。 191 | **4) 响应。**当用户看到通知后,点击进去会有相应的响应选项。设置响应选项是 UNNotificationAction 和 UNNotificationCategory。 192 | 193 | 如果你正在跳槽或者正准备跳槽不妨动动小手,添加一下咱们的交流群[**931 542 608**](https://jq.qq.com/?_wv=1027&k=cfVxrMAH)来获取一份详细的大厂面试资料为你的跳槽多添一份保障。 194 | 195 | **加分回答:** 196 | 远程推送的流程与本地推送大同小异,不同的是第 2 步创建,参数内容和消息创建都在服务器端完成,而不是在本地完成。 197 | 198 | ### 8.iOS 开发中远程消息推送的原理是怎样的? 199 | 200 | **关键词: #APNs Server** 201 | 202 | 回答这道题目的关键在于理清 iOS 系统,App,APNs 服务器,以及 App 对应的客户端之间的关系。具体来说就是: 203 | 204 | 1) App 向 iOS 系统申请远程消息推送权限。这与本地消息推送的注册是一样的; 205 | 2) iOS 系统向 APNs(Apple Push Notification Service) 服务器请求手机的 device token,并告诉 App,允许接受推送的通知; 206 | 3) App 将手机的 device token 传给 App 对应的服务器端; 207 | 4) 远程消息由 App 对应的服务器端产生,它会先经过 APNs; 208 | 5) APNs 将远程通知推送给响应手机。 209 | 210 | 具体的流程图如下: 211 | 212 | ![image](https://upload-images.jianshu.io/upload_images/22877992-26ab4f13ae6a6421.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 213 | 214 | ## 数据处理 215 | 216 | ### 9.iOS 开发中如何实现编码和解码? 217 | 218 | **关键词: #Encodable #Decodable** 219 | 220 | 编码和解码在 Swift 4 中引入了 Encodable 和 Decodable 这两个协议,而 Codable 是 Encodable 和 Decodable 的合集。在 Swift 中,Enum,Struct,Class 都支持 Codable。一个最简单的使用如下: 221 | 222 | ``` 223 | enum Gender: String, Codable { 224 | case Male = “Male” 225 | case Female = “Female” 226 | } 227 | 228 | class User: Codable { 229 | let name: String 230 | let age: Int 231 | let gender: Gender 232 | 233 | init(name: String, age: Int, gender: Gender) { 234 | (self.name, self.age, self.gender) = (name, age, gender) 235 | } 236 | } 237 | 238 | ``` 239 | 240 | 这样定义完成之后,我们就可以轻易的在 User 及其对应 JSON 数据进行编码和解码,示范代码如下: 241 | 242 | ``` 243 | let userJsonString = """ 244 | { 245 | "name": "Cook", 246 | "age": 58, 247 | "gender": "Male" 248 | } 249 | """ 250 | 251 | // 从JSON解码到实例 252 | if let userJSONData = userJsonString.data(using: .utf8) { 253 | let userDecode = try? JSONDecoder().decode(User.self, from: userJSONData) 254 | } 255 | 256 | //从实例编码到JSON 257 | let userEncode = User(name: "Cook", age: 58, gender: Gender.Male) 258 | let userEncodedData = try? JSONEncoder().encode(userEncode) 259 | 260 | ``` 261 | 262 | **追问:假如 JSON 的键值和对象的属性名不匹配该怎么办?** 263 | 264 | 可以在对象中定义一个枚举(enum CodingKeys: String, CodingKey),然后将属性和 JSON 中的键值进行关联。 265 | 266 | **追问:假如 class 中某些属性不支持 Codable 该怎么办?** 267 | 268 | 将支持 Codable 的属性抽离出来定义在父类中,然后在子类中配合枚举(enum CodingKeys),将不支持的 Codable 的属性单独处理。 269 | 270 | ### 10.谈谈 iOS 开发中数据持久化的方案 271 | 272 | **关键词: #plist #Preference #NSKeyedArchiver #CoreData** 273 | 274 | 数据持久化就是将数据保存在硬盘中,这样无论是断网还是重启,我们都可以访问到之前保存的数据。iOS 开发中有以下几种方案: 275 | 276 | * **plist。**它是一个 XML 文件,会将某些固定类型的数据存放于其中,读写分别通过 contentsOfFile 和 writeToFile 来完成。一般用于保存 App 的基本参数。 277 | 278 | * **Preference。**它通过 UserDefaults 来完成 key-value 配对保存。如果需要立刻保存,需要调用 synchronize 方法。它会将相关数据保存在同一个 plist 文件下,同样是用于保存 App 的基本参数信息。 279 | 280 | * **NSKeyedArchiver。**遵循 NSCoding 协议的对象就就可以实现序列化。NSCoding 有两个必须要实现的方法,即父类的归档 initWithCoder 和解档 encodeWithCoder 方法。存储数据通过 NSKeyedArchiver 的工厂方法 archiveRootObject:toFile: 来实现;读取数据通过 NSKeyedUnarchiver 的工厂方法 unarchiveObjectwithFile:来实现。相比于前两者, NSKeyedArchiver 可以任意指定存储的位置和文件名。 281 | 282 | * **CoreData。**前面几种方法,都是覆盖存储。修改数据要读取整个文件,修改后再覆盖写入,十分不适合大量数据存储。CoreData 就是苹果官方推出的大规模数据持久化的方案。它的基本逻辑类似于 SQL 数据库,每个表为 Entity,然后我们可以添加、读取、修改、删除对象实例。它可以像 SQL 一样提供模糊搜索、过滤搜索、表关联等各种复杂操作。尽管功能强大,它的缺点是学习曲线高,操作复杂。 283 | 284 | 以上几种方法是 iOS 开发中最为常见的数据持久化方案。除了这些以外,针对大规模数据持久化,我们还可以用 SQLite3、FMDB、Realm 等方法。相比于 CoreData 和其他方案,Realm 以其简便的操作和丰富的功能广受很多开发者青睐。同时大公司诸如 Google 的 Firebase 也有离线数据库功能。其实没有最佳的方案,只有最合适的方案,应该根据实际开发的 App 来挑选合适的持久化方案。 285 | 286 | # 推荐👇: 287 | 288 | 如果你正在跳槽或者正准备跳槽不妨动动小手,添加一下咱们的交流群[**931 542 608**](https://jq.qq.com/?_wv=1027&k=cfVxrMAH)来获取一份详细的大厂面试资料为你的跳槽多添一份保障。 289 | 290 | ![](https://upload-images.jianshu.io/upload_images/22877992-0bfc037cc50cae7d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 291 | -------------------------------------------------------------------------------- /语言工具-Swift vs. Objective-C.md: -------------------------------------------------------------------------------- 1 | 我曾经一度在想苹果为什么要大费周章的出一门新语言,而不是去把同样的精力和时间放在优化 Objective-C 上?后来 Chris Lattner 在他的访谈中说,因为 Objective-C 是一门以 C 语言为基础的语言,所以天生具备 C 的缺点;况且这门语言历经多年,各种弊病也是积重难返。所以,苹果决定,重新开发一门语言,名为 Swift。 2 | 3 | ![](https://upload-images.jianshu.io/upload_images/22877992-7c18b06a4d66401e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 4 | 5 | 6 | 所以,Swift 从一开始就要和 Objective-C 语言分道扬镳。我们会发现 Swift 注重安全性,Objective-C 注重灵活性;Swift 有函数式编程、面向对象编程、面向协议编程,Objective-C 几乎只有面向对象编程;Swift 更注重值类型的数据结构,而 Objective-C 遵循 C 的老一套,注重指针和索引; Swift 是静态类型语言,Objective-C 却是动态类型语言。 7 | 8 | 本章将从数据结构、编程思路、语言特性三个角度来回答两种语言的异同。从比较当中我们也更能体会,尽管两者都是为 iOS 开发而定制的语言,Objective-C 和 Swift 依然有着天壤之别。 9 | 10 | ## 数据结构 11 | 12 | ### 1.说说 Swift 为什么将 String,Array,Dictionary设计成值类型? 13 | 14 | **关键词:#引用类型 #值类型 #多线程 #协议** 15 | 16 | 要解答这个问题,就要和 Objective-C 中相同的数据结构设计进行比较。Objective-C 中,字符串,数组,字典,皆被设计为引用类型。 17 | 18 | * 值类型相比引用类型,最大的优势在于内存使用的高效。值类型在栈上操作,引用类型在堆上操作。栈上的操作仅仅是单个指针的上下移动,而堆上的操作则牵涉到合并、移位、重新链接等。也就是说 Swift 这样设计,大幅减少了堆上的内存分配和回收的次数。同时 copy-on-write 又将值传递和复制的开销降到了最低。 19 | 20 | * String,Array,Dictionary 设计成值类型,也是为了线程安全考虑。通过 Swift 的 let 设置,使得这些数据达到了真正意义上的“不变”,它也从根本上解决了多线程中内存访问和操作顺序的问题。 21 | 22 | ### 2.用 Swift 将协议(protocol)中的部分方法设计成可选(optional),该怎样实现? 23 | 24 | **关键词:#协议 #协议扩展** 25 | 26 | `@optional`和 `@required` 是 Objective-C 中特有的关键字。 27 | 28 | 在 Swift 中,默认所有方法在协议中都是必须实现的。而且,协议里方法不可以直接定义 optional。先给出两种解决方案: 29 | 30 | * 在协议和方法前都加上 `@objc` 关键字,然后再在方法前加上 optional 关键字。该方法实际上是把协议转化为 Objective-C 的方式然后进行可选定义。示例如下: 31 | 32 | ``` 33 | @objc protocol SomeProtocol { 34 | func requiredFunc() 35 | @objc optional func optionalFunc() 36 | } 37 | 38 | ``` 39 | 40 | * 用扩展(extension)来规定可选方法。在 Swift 中,协议扩展(protocol extension)可以定义部分方法的默认实现,这样这些方法在实际调用中就是可选实现的了。示例如下: 41 | 42 | ``` 43 | protocol SomeProtocol { 44 | func requiredFunc() 45 | func optionalFunc() 46 | } 47 | 48 | extension SomeProtocol { 49 | func optionalFunc() { 50 | print(“Dumb Implementation”) 51 | } 52 | } 53 | 54 | Class SomeClass: SomeProtocol { 55 | func requiredFunc() { 56 | print(“Only need to implement the required”) 57 | } 58 | } 59 | 60 | ``` 61 | 62 | ### 3.下面代码有什么问题? 63 | 64 | **关键词:#协议** 65 | 66 | ``` 67 | protocol SomeProtocolDelegate { 68 | func doSomething() 69 | } 70 | 71 | class SomeClass { 72 | weak var delegate: SomeProtocolDelegate? 73 | } 74 | 75 | ``` 76 | 77 | SomeClass 中的 delegate 那行会报错。 78 | 79 | Swift 中的协议不仅可以被 class 这样的引用类型来实现,也可能被 struct 或者 enum 这样的值类型实现(这是和 Objective-C 最大的不同)。weak 关键词是用于 ARC 环境下,为引用类型提供引用计数这般的内存管理。它是不能被用来修饰值类型的。 80 | 81 | 有两种修正方法。 82 | 83 | * 在protocol前加上 `@objc`。Objective-C 中协议只能由 class 来实现,这样一来 weak 修饰的对象就与Objective-C 一样,只是 class 类型。修正如下: 84 | 85 | ``` 86 | @objc protocol SomeProtocolDelegate { 87 | func doSomething() 88 | } 89 | 90 | ``` 91 | 92 | * 在 SomeProtocolDelegate 后添加关键词 class 。如此一来声明了该协议只能由 class 来实现。修正如下: 93 | 94 | ``` 95 | protocol SomeProtocolDelegate: class { 96 | func doSomething() 97 | } 98 | 99 | ``` 100 | 101 | ## 编程思路 102 | 103 | ### 4.在 Swift 和 Objective-C 的混编项目中,如何在 Swift 文件中调用 Objective-C 文件中已经定义的方法?如何在 Objective-C 文件中调用 Swift 文件中定义的方法? 104 | 105 | **关键词:#头文件 #@objc** 106 | 107 | * 在 Swift 中,若要使用 Objective-C 代码,可以在 ProjectName-Bridging-Header.h 里添加 Objective-C 的头文件名称,这样在 Swift 文件中即可调用相应的 Objective-C 代码。一般情况 Xcode 会在 Swift 项目中第一次创建 Objective-C 文件时自动创建 ProjectName-Bridging-Header.h 文件。 108 | 109 | * Objective-C 中若要调用 Swift 代码,可以导入 Swift 生成的头函数 ProjectName-Swift.h 来实现。 110 | 111 | **加分回答:** 112 | 113 | * 在 Swift 文件中,若要规定固定的方法或属性暴露给 Objective-C 使用,可以在方法或属性前加上 `@objc` 来声明。如果该类是 NSObject 子类,那么 Swift 会在非 private 的方法或属性前自动加上 `@objc` 。 114 | 115 | ### 5.试比较 Swift 和 Objective-C 中的初始化方法(init)有什么异同? 116 | 117 | **关键词:#初始化** 118 | 119 | 一言以蔽之,在 Swift 中的初始化方法更加严格和准确。 120 | 121 | * Objective-C 中,初始化方法无法保证所有成员变量都完成初始化;编译器对属性设置并无警告,但是实际操作中会出现初始化不完全的问题;初始化方法与普通方法并无实际差别,可以多次调用。 122 | 123 | * 在 Swift 中,初始化方法必须保证所有非 optional 的成员变量都完成初始化。同时新增 convenience 和 required 两个修饰初始化方法的关键词。convenience 只是提供一种方便的初始化方法,必须通过调用同一个类中 designated 初始化方法来完成。required 是强制子类重写父类中所修饰的初始化方法。 124 | 125 | ### 6.试比较 Swift 和 Objective-C 中的协议(Protocol)有什么异同? 126 | 127 | **关键词:#协议** 128 | 129 | 相同点在于,Swift 和 Objective-C 中的 Protocol 都可以被用作代理。Objective-C 中的 Protocol 类似于 Java 中的 Interface,实际开发中主要用于适配器模式(Adapter Pattern,详见第3章第4节设计模式)。 130 | 131 | 不同点在于,Swift 的 Protocol 还可以对接口进行抽象,例如 Sequence,配合拓展(extension)、泛型、关联类型等可以实现面向协议的编程,从而大大提高整个代码的灵活性。同时 Swift 的 Protocol 还可以用于值类型,如结构体和枚举。 132 | 133 | ## 语言特性 134 | 135 | ### 7.谈谈对 Objective-C 和 Swift 动态特性的理解 136 | 137 | **关键词:#动态特性 #@runtime #面向协议编程** 138 | 139 | runtime 其实就是 Objective-C 的动态机制。runtime 执行的是编译后的代码,这时它可以动态加载对象、添加方法、修改属性、传递信息等等。具体过程是在 Objective-C 中对象调用方法时,如 [self.tableview reload],发生了两件事。 140 | 141 | * 编译阶段,编译器(compiler)会把这句话翻译成 `objc_msgSend(self.tableview,[@selector](https://xiaozhuanlan.com/u/undefined)(reload))`,把消息发送给 self.tableview。 142 | 143 | * 运行阶段,接收者 self.tableview 会响应这个消息,期间可能会直接执行、转发消息,也可能会找不到方法崩溃。 144 | 145 | 所以整个流程是编译器翻译–> 给接收者发送消息 –> 接收者响应消息三个流程。 146 | 147 | 如 [self.tableview reload] 中,self.tableview 就是接收者,reload 就是消息,所以方法调用的格式在编译器看来是 [receiver message]。 148 | 149 | 其中接收者如何响应代码,就发生在运行时(runtime)。runtime 执行的是编译后的代码,这时它可以动态加载对象、添加方法、修改属性、传递信息等等,runtime 的运行机制就是 Objective-C 的动态特性。 150 | 151 | Swift 目前被公认为是一门静态语言。它的动态特性都是通过桥接 OC 来实现。如果要把动态特性写得更Swift 一点,可以用protocol来处理,比如 OC 中的 reflection 这样写: 152 | 153 | ``` 154 | if ([someImage respondsToSelector:@selector(shake)]) { 155 | [someImage performSelector:shake]; 156 | } 157 | 158 | ``` 159 | 160 | Swift中可以这样写: 161 | 162 | ``` 163 | if let shakeableImage = someImage as? Shakeable { 164 | shakeableImage.shake() 165 | } 166 | 167 | ``` 168 | 169 | ### 8.以下代码输出什么? 170 | 171 | **关键词:#动态特性 #协议 #扩展** 172 | 173 | ``` 174 | protocol Chef { 175 | func makeFood() 176 | } 177 | 178 | extension Chef { 179 | func makeFood() { 180 | print("Make Food") 181 | } 182 | } 183 | 184 | struct SeafoodChef: Chef { 185 | func makeFood() { 186 | print("Cook Seafood") 187 | } 188 | } 189 | 190 | let chefOne: Chef = SeafoodChef() 191 | let chefTwo: SeafoodChef = SeafoodChef() 192 | chefOne.makeFood() 193 | chefTwo.makeFood() 194 | 195 | ``` 196 | 197 | 会打印出两行 Cook Seafood。 198 | 199 | 原因在于 Swift 中,协议中是动态派发,扩展中是静态派发。也就是说,协议中如果有方法声明,那么方法在调用中,会根据对象的实际类型进行调用。 200 | 201 | 此题中 makeFood() 方法在 Chef 协议中已经声明,而 chefOne 虽然声明为 Chef,但实际实现为 SeaFoodChef 。所以根据实际情况,makeFood() 会调用 SeaFoodChef 中的实现。chefTwo 也是同样的道理。 202 | 203 | > 追问:如果 protocol 中没有声明 makeFood() 方法,代码又会输出什么? 204 | 205 | 会打印出两行,第一行为 Make Food,第二行为 Cook Seafood。 206 | 207 | 因为协议中没有声明 makeFood() 方法,所以此时只会按照扩展中进行静态派发。也就是说,会根据对象的声明类型进行调用。chefOne 声明为 Chef,所以调用扩展中的实现,chefTwo 声明为 SeafoodChef,所以调用 SeafoodChef 中的实现。 208 | 209 | ### 9\. message send 如果找不到对象,会如何进行后续处理? 210 | 211 | **关键词:#动态特性** 212 | 213 | 找不到对象分 2 种情况:对象为空(nil);对象不为空,却找不到对应的方法。 214 | 215 | * 对象为空时,Objective-C 在向 nil 发送消息是有效的,在 runtime 中不会产生任何效果。如果信息中的方法返回值是对象,那么给 nil 发送消息返回 nil;如果方法返回值是结构体,那么给 nil 发送消息返回 0 。 216 | 217 | * 对象不为空却找不到对应的方法时,程序异常,引发 unrecognized selector。 218 | 219 | ### 10\. 什么是method swizzling? 220 | 221 | **关键词:#动态特性** 222 | 223 | 每个类都维护一个方法列表,其中方法名与其实现是一一对应的关系,即 SEL(方法名)和 IMP(指向实现的指针)的对应关系。method swizzling 可以在 runtime 时将 SEL 和 IMP 进行更换。比如 SELa 原来对应 IMPa,SELb 原来对应 IMPb,method swizzling 之后,SELa 就可以对应 IMPb,Selb 就对应 IMPa。下面是一个封装好的实现示范: 224 | 225 | ``` 226 | //方法一的 SEL 和 Method SEL 227 | oneSEL = @selector(methodOne:); 228 | Method oneMethod = class_getInstanceMethod(selfClass, oneSEL); 229 | 230 | //方法二的 SEL 和 Method 231 | SEL twoSEL = @selector(methodTwo:); 232 | Method twoMethod = class_getInstanceMethod(selfClass, twoSEL); / 233 | 234 | //給方法一添加实现,可以避免方法一没有实现 235 | BOOL addSucc = class_addMethod(selfClass, oneSEL, method_getImplementation(twoMethod), method_getTypeEncoding(twoMethod)); 236 | 237 | if (addSucc) { //添加成功:将方法一的实现替换到方法二 238 | class_replaceMethod(selfClass, twoSEL, method_getImplementation(oneMethod), method_getTypeEncoding(oneMethod)); 239 | }else { //添加失败:方法一已经有实现,直接将方法一和方法二的实现交换 240 | method_exchangeImplementations(oneMethod, twoMethod); 241 | } 242 | 243 | ``` 244 | 245 | **加分回答:** 246 | 247 | * 方法交换应该保证唯一性和原子性。唯一性是指应该尽可能在 +load 方法中实现,这样可以保证方法一定会调用且不会出现异常。原子性是指使用 dispatch_once 来执行方法交换,这样可以保证只运行一次。 248 | 249 | * 不要轻易使用 method swizzling。因为动态交换方法实现并没有编译器的安全保证,可能会在运行时造成奇怪的 bug。 250 | 251 | ### 11\. Swift 和 Objective-C 的自省(Introspection)有什么不同? 252 | 253 | **关键词:#动态特性** 254 | 255 | 自省在 Objective-C 中就是:判断一个对象是不是属于某个类的操作。它有两种形式: 256 | 257 | ``` 258 | [obj isKindOfClass:[SomeClass class]]; 259 | [obj isMemberOfClass:[SomeClass class]]; 260 | 261 | ``` 262 | 263 | 第一句话,isKindOfClass 用来判断 obj 是否为 SomeClass 或其子类的实例对象;第二句话,isMemberOfClass 则对 obj 做出判断,当且仅当 obj 是 SomeClass(非子类)的实例对象时才返回真。这两个方法的使用有个前提,即 obj 必须是 NSObject 或其子类。 264 | 265 | Swift 中由于很多 class 并非继承自 NSObject,故而 Swift 用 is 来进行判断。它相当于 isKindOfClass。相比之下优点是 is 不仅可以用于任何 class 类型上,也可以用来判断 enum 和 struct 类型。 266 | 267 | **加分回答:** 268 | 269 | 自省经常与动态类型一起运用。动态类型就是 id 类型,任何类型的对象都可以用 id 来代指。这个时候我们常常用自省来判断对象的实际所属类。示例代码如下: 270 | 271 | ``` 272 | id vehicle = someCarInstance; 273 | 274 | if ([vehicle isKindOfClass: [Car class]]) { 275 | NSLog(“vehicle is a car”); 276 | if ([vehicle isMemberOfClass: [Tesla class]]) { 277 | NSLog(“vehicle is a Tesla”); 278 | } 279 | } else if ([vehicle isKindOfClass: [Truck class]]) { 280 | NSLog(“vehicle is a a truck”); 281 | } 282 | 283 | ``` 284 | 285 | ### 12\. 能否通过 Category 给已有的类添加属性(property)? 286 | 287 | **关键词:#动态特性** 288 | 289 | 答案是可以。无论是对于 Objective-C 还是 Swift 而言。 290 | 291 | Objective-C 中,正常情况下在 Category 中添加属性会报错,说是找不到 getter 和 setter 方法,这是因为 Category 不会自动生成这两个方法。解决方法是引入运行时头文件,配合关联对象的方法来实现。主要的两个函数是 objc_getAssociatedObject 和 objc_setAssociatedObject 。Swift 中,解决方法与 Objective-C 中相同。只是在写法上更加 Swift 化。 292 | 293 | 假如我们有个 class 叫 User。由于我们 App 要打开国际化市场了,所以 PM 要求我们 User 能满足有中间名字的老外。于是我们会想在它的 Category 里给它添加 middleName 这个属性。示例的 Objective-C 代码如下: 294 | 295 | ``` 296 | // .h 297 | @interface User (Foreign) 298 | @property (nonatomic, copy) NSString *middleName; 299 | @end 300 | 301 | // .m 302 | #import "User + Foreign.h" 303 | #import 304 | 305 | static void *middleNameKey = &middleNameKey; 306 | 307 | @implementation User (Foreign) 308 | - (void)setMiddleName:(NSString *)middleName 309 | { 310 | objc_setAssociatedObject(self, &middleNameKey, middleName, OBJC_ASSOCIATION_COPY_NONATOMIC); 311 | } 312 | - (NSString *)middleName 313 | { 314 | return objc_getAssociatedObject(self, &middleNameKey); 315 | } 316 | @end 317 | 318 | ``` 319 | 320 | 这段代码解释一下是这样的: 321 | 322 | 1. 在 ".h" 文件中添加属性。此属性是私有属性。 323 | 2. 在".m" 文件中引入运行时头文件 `` ,接着设置关联属性的 Key,最后实现 setter 和 getter。 324 | 325 | 其中 objc_setAssociatedObject 这个方法的四个参数分别为原对象,关联属性 key,关联属性,关联策略。具体细节可参考苹果官方 API,这里不做展开。用 Swift 实现类似功能是这样的: 326 | 327 | ``` 328 | private var middleNameKey: Void? 329 | 330 | extension User { 331 | var middleName: String? { 332 | get { 333 | return objc_getAssociatedObject(self, &middleNameKey) as? String 334 | } 335 | 336 | set { 337 | objc_setAssociatedObject(self, &middleNameKey, newValue,. OBJC_ASSOCIATION_COPY_NONATOMIC) 338 | } 339 | } 340 | } 341 | 342 | ``` 343 | 344 | # 推荐👇: 345 | 346 | 如果你正在跳槽或者正准备跳槽不妨动动小手,添加一下咱们的交流群[**931 542 608**](https://jq.qq.com/?_wv=1027&k=cfVxrMAH)来获取一份详细的大厂面试资料为你的跳槽多添一份保障。 347 | 348 | ![](https://upload-images.jianshu.io/upload_images/22877992-0bfc037cc50cae7d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 349 | -------------------------------------------------------------------------------- /简历的准备到面试流程.md: -------------------------------------------------------------------------------- 1 | ![](https://upload-images.jianshu.io/upload_images/22877992-5f5c86c20445f86c.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 2 | # 1.简历的准备 3 | 在面试中,我发现很多人都不能写好一份求职简历,所以我们首先谈谈如何写一份针对互联网公司的求职简历。 4 | 5 | ## 1.简洁的艺术 6 | 7 | 互联网公司和传统企业有着很大的区别,通常情况下,创新和效率是互联网公司比较追求的公司文化,所以体现在简历上,就是超过一页的简历通常会被认为不够专业。 8 | 9 | 更麻烦的是,多数超过一页的简历很可能在 HR 手中就被过滤掉了。因为 HR 每天会收到大量的简历,一般情况下每份简历在手中的停留时间也就 10 秒钟左右。而超过一页的简历会需要更多的时间去寻找简历中的有价值部分,对于 HR 来说,她更倾向于认为这种人通常是不靠谱的,因为写个简历都不懂行规,为什么还要给他面试机会呢? 10 | 11 | 那么我们应该如何精简简历呢? 简单说来就是一个字:删! 12 | 13 | **删掉不必要的自我介绍信息**。很多求职者会将自己在学校所学的课程罗列上去,例如:C 语言,数据结构,数学分析⋯⋯好家伙,一写就是几十门,还放在简历的最上面,就怕面试官看不见。对于这类信息,一个字:删!面试官不关心你上了哪些课程,而且在全中国,大家上的课程也都大同小异,所以没必要写出来。 14 | 15 | **删除不必要的工作或实习、实践经历**。如果你找一份程序员的工作,那么你参加了奥运会的志愿者活动,并且拿到了奖励或者你参加学校的辩论队,获得了最佳辩手这些经历通常是不相关的。诸如此类的还有你帮导师代课,讲了和工作不相关的某某专业课,或者你在学生会工作等等。删除不相关的工作、实习或实践内容可以保证你的简历干净。当然,如果你实在没得可写,比如你是应届生,一点实习经历都没有,那可以适当写一两条,保证你能写够一页的简历,但是那两条也要注意是强调你的团队合作能力或者执行力之类的技能,因为这些才是面试官感兴趣的。 16 | 17 | **删除不必要的证书**。最多写个 4、6 级的证书,什么教师资格证,中高级程序员证,还有国内的各种什么认证,都是没有人关心的。 18 | 19 | **删除不必要的细节**。作为 iOS 开发的面试官,很多求职者在介绍自己的 iOS 项目经历的时候,介绍了这个工程用的工作环境是 Mac OS,使用的机器是 Mac Mini,编译器是 Xcode,能够运行在 iOS 什么版本的环境。还有一些人,把这个项目用到的开源库都写上啦,什么 AFNetworking, CocoaPods 啥的。这些其实都不是重点,请删掉。后面我会讲,你应该如何介绍你的 iOS 项目经历。 20 | 21 | 自我评价,这个部分是应届生最喜欢写的,各种有没有的优点都写上,例如: 22 | 23 | > 性格开朗、稳重、有活力,待人热情、真诚;工作认真负责,积极主动,能吃苦耐劳,勇于承受压力,勇于创新;有很强的组织能力和团队协作精神,具有较强的适应能力;纪律性强,工作积极配合;意志坚强,具有较强的无私奉献精神。对待工作认真负责,善于沟通、协调有较强的组织能力与团队精神;活泼开朗、乐观上进、有爱心并善于施教并行;上进心强、勤于学习能不断提高自身的能力与综合素质。 24 | 25 | 这些内容在面试的时候不太好考查,都可以删掉。通常如果有 HR 面的话,HR 自然会考查一些你的沟通,抗压,性格等软实力。 26 | 27 | 我相信,不管你是刚毕业的学生,还是工作十年的老手,你都可以把你的简历精简到一页 A4 纸上。记住,简洁是一种美,一种效率,也是一种艺术。 28 | 29 | ## 2.重要的信息写在最前面 30 | 31 | 将你觉得最吸引人的地方写在最前面。如果你有牛逼公司的实习,那就把实习经历写在最前面,如果你在一个牛逼的实验室里面做科研,就把研究成果和论文写出来,如果你有获得过比较牛逼的比赛名次(例如 Google code jam, ACM 比赛之类),写上绝对吸引眼球。 32 | 33 | 所以,每个人的简历的介绍顺序应该都是不一样的,不要在网上下载一个模板,然后就一项一项地填:教育经历,实习经历,得奖经历,个人爱好,这样的简历毫无吸引力,也无法突出你的特点。 34 | 35 | 除了你的个人特点是重要信息外,你的手机号、邮箱、毕业院校、专业以及毕业时间这些也都是非常重要的,一定要写在简历最上面。 36 | 37 | ## 3.不要简单地罗列工作经历 38 | 39 | 不要简单地说你开发了某某 iOS 客户端。这样简单的罗列你的作品集并不能让面试官很好地了解你的能力,当然,真正在面试时面试官可能会仔细询问,但是一份好的简历,应该省去一些面试官额外询问你的工作细节的时间。 40 | 41 | 具体的做法是:详细的描述你对于某某 iOS 客户端的贡献。主要包括:你参与了多少比例功能的开发? 你解决了哪些开发中的有挑战的问题? 你是不是技术负责人? 42 | 43 | 而且,通过你反思这些贡献,你也可以达到自我审视,如果你发现这个项目你根本什么有价值的贡献都没做,就打了打酱油,那你最好不要写在简历上,否则当面试官在面试时问起时,你会很难回答,最终让他发现你的这个项目经历根本一文不值时,肯定会给一个负面的印象。 44 | 45 | ## 4.不要写任何虚假或夸大的信息 46 | 47 | 刚刚毕业的学生都喜欢写精通 Java,精通 C/C++,其实代码可能写了不到 1 万行。我觉得你要精通某个语言,至少得写 50 万行这个语言的代码才行,而且要对语言的各种内部机制和原理有了解。那些宣称精通 Java 的同学,连 Java 如何做内存回收,如何做泛型支持,如何做自动 boxing 和 unboxing 的都不知道,真不知道为什么要写精通二字。 48 | 49 | 任何夸大或虚假的信息,在面试时被发现,会造成极差的面试印象。所以你如果对某个知识一知半解,要么就写 “使用过” 某某,要么就干脆不写。如果你简历实在太单薄,没办法写上了一些自己打酱油的项目,被问起来怎么办? 请看看下面的故事: 50 | 51 | > 我面试过一个同学,他在面试时非常诚实。我问他一些简历上的东西,他如果不会,就会老实说,这个我只是使用了一下,确实不清楚细节。对于一些没有技术含量的项目,他也会老实说,这个项目他做的工作比较少,主要是别人在做。最后他还会补充说,“我自认为自己数据结构和算法还不错,要不你问我这方面的知识吧。” 52 | 53 | 这倒是一个不错的办法,对于一个没有项目经验,但是聪明并且数据结构和算法基础知识扎实的应届生,其实我们是非常愿意培养的。很多人以为公司面试是看经验,希望招进来就能干活,其实不是的,至少我们现在以及我以前在网易招人,面试的是对方的潜力,潜力越大,可塑性好,未来进步得也更快;一些资质平庸,却经验稍微丰富一点的开发者,相比聪明好学的面试者,后劲是不足的。 54 | 55 | 总之,不要写任何虚假或夸大的信息,即使你最终骗得过面试官,进了某公司,如果能力不够,在最初的试用期内,也很可能因为能力不足而被开掉。 56 | 57 | ## 5.留下更多信息 58 | 59 | 刚刚说到,简历最好写够一张 A4 纸即可,那么你如果想留下更多可供面试官参考的信息怎么办呢?其实你可以附上更多的参考链接,这样如果面试官对你感兴趣,自然会仔细去查阅这些链接。对于 iOS 面试来说,GitHub 上面的开源项目地址、博客地址都是不错的参考信息。如果你在微博上也频繁讨论技术,也可以附上微博地址。 60 | 61 | 我特别建议大家如果有精力,可以好好维护一下自己的博客或者 GitHub 上的开源代码。因为如果你打算把这些写到简历上,让面试官去上面仔细评价你的水平,你就应该对上面的内容做到足够认真的准备。否则,本来面试完面试官还挺感兴趣的,结果一看你的博客和开源代码,评价立刻降低,就得不偿失了。 62 | 63 | ## 6.不要附加任何可能带来负面印象的信息 64 | 65 | 任何与招聘工作无关的东西,尽量不要提。有些信息提了可能有加分,也可能有减分,取决于具体的面试官。下面我罗列一下我认为是减分的信息。 66 | 67 | ### 1)个人照片 68 | 69 | 不要在简历中附加个人照片。个人长相属于与工作能力不相关的信息,也许你觉得你长得很帅,那你怎么知道你的样子不和面试官的情敌长得一样? 也许你长得很漂亮,那么你怎么知道 HR 是否被你长得一样的小三把男朋友抢了? 我说得有点极端,那人们对于长相的评价标准确实千差万别,萝卜青菜各有所爱,加上可能有一些潜在的极端情况,所以没必要附加这部分信息。这属于加了可能有加分,也可能有减分的情况。 70 | 71 | ### 2)有风险的爱好 72 | 73 | 不要写各种奇怪的爱好。喜欢打游戏、抽烟、喝酒,这类可能带来负面印象的爱好最好不要写。的确有些公司会有这种一起联机玩游戏或者喝酒的文化,不过除非你明确清楚对于目标公司,写上会是加分项,否则还是不写为妙。 74 | 75 | ### 3)使用 PDF 格式 76 | 77 | 不要使用 Word 格式的简历,要使用 PDF 的格式。我在招 iOS 程序员时,好多人的简历都是 Word 格式的,我都怀疑这些人是否有 Mac 电脑。因为 Mac 下的 office 那么难用,公司好多人机器上都没有 Mac 版 office。我真怀疑这些人真是的想投简历么? PDF 格式的简历通常能展现出简历的专业性。 78 | 79 | ### 4)QQ号码邮箱 80 | 81 | 不要使用 QQ 号开头的 QQ 邮箱,例如 `12345@qq.com` ,邮箱的事情我之前简单说过,有些人很在乎这个,有些人觉得无所谓,我个人对用数字开头的 QQ 邮箱的求职者不会有加分,但是对使用 Gmail 邮箱的求职者有加分。因为这涉及到个人的工作效率,使用 Gmail 的人通常会使用邮件组,过滤器,IMAP 协议,标签,这些都有助于提高工作效率。如果你非要使用 QQ 邮箱,也应该申请一个有意义的邮箱名,例如 `tangqiaoboy@qq.com` 。 82 | 83 | ## 7.职业培训信息 84 | 85 | 不要写参加过某某培训公司的 iOS 培训,特别是那种一、两个月的速成培训。这对于我和身边很多面试官来说,绝对是负分。 86 | 87 | 这个道理似乎有点奇怪,因为我们从小都是由老师教授新知识的。我自己也实验过,掌握同样的高中课本上的知识,自己自学的速度通常比老师讲授的速度要慢一倍的时间。即一个知识点,如果你自己要看 2 小时的书才能理解的话,有好的老师给你讲解的话,只需要一个小时就够了。所以,我一直希望在学习各种东西的时候都能去听一些课程,因为我认为这样节省了我学习的时间。 88 | 89 | 但是这个道理在程序员这个领域行不通,为什么这么说呢?原因有两点: 90 | 91 | 1. 计算机编程相关的知识更新速度很快。同时,国内的 IT 类资料的翻译质量相当差,原创的优秀书籍也很少。所以,我们通常需要靠阅读英文才能掌握最新的资料。拿 iOS 来说,每年 WWDC 的资料都非常重要,而这些内容涉及版权,国内培训机构很难快速整理成教材。 92 | 93 | 2. 计算机编程知识需要较多的专业知识积累和实践。学校的老师更多只能做入门性的教学工作。 94 | 95 | 如果一个培训机构有一个老师,他强到能够通过自己做一些项目来积累很多专业知识和实践,并且不断地从国外资料上学习最新的技术。那么这个人在企业里面会比在国内的培训机构更有施展自己能力的空间。国内的培训机构因为受众面的原因,基本上还是培养那种初级的程序员新手,所以对老师的新技术学习速度要求不会那么高,自然老师也不会花那么时间在新技术研究上。但是企业就不一样了,企业需要不停地利用新技术来增强自己的产品竞争力,所以对于 IT 企业来说,产品的竞争就是人才的竞争,所以给优秀的人能够开出很高的薪水。 96 | 97 | 所以,我们不能期望从 IT 类培训机构中学习到最新的技术,一切只能通过我们自学。当然,自学之后在同行之间相互交流,对于我们的技术成长也是很有用的。 98 | 99 | ## 小结 100 | 101 | ![](https://upload-images.jianshu.io/upload_images/22877992-524b32eb9200dc94.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 102 | 103 | 上图是本节讨论的总结,在简历准备上,我们需要考虑简历的简洁性等各种注意事项。 104 | 105 | # 2.寻找机会 106 | 1.寻找内推机会 107 | 其实,最好的面试机会都不是公开渠道的。好的机会都隐藏于各种内部推荐之中。通过内部推荐,你可以更加了解目标工作的团队和内容,另外内部推荐通常也可以跳过简历筛选环节,直接参加笔试或面试。我所在的猿辅导公司为内推设立了非常高的奖金激励,因为我们发现,综合各种渠道来看,内推的渠道的人才的简历质量最高,面试通过率最高的。 108 | 109 | 所以,如果你是学生,找找你在各大公司的师兄师姐内推;如果你已经工作了,你可以找前同事或者通过一些社交活动认识的技术同行内推。 110 | 111 | 大部分情况下,如果在目标公司你完全没有认识的人,你也可以找机会来认识几个。比如你可以通过微博、知乎、Twitter、GitHub 来结交新的朋友。然后双方聊天如果愉快的话,我相信内推这种举手之劳的事情对方不会拒绝的。 112 | 113 | 如果你都工作 5 年以上,还是没有建立足够好的社交圈子帮助你内推,那可能你需要做很多的社交活动交一些朋友。 114 | 115 | 2.其它常见的渠道 116 | 内推之外,其它的公开招聘渠道通常都要差一些。现在也有一些专门针对互联网行业的招聘网站,例如拉勾、100offer 这类,它们也是不错的渠道,可以找到相关的招聘信息。 117 | 118 | 但因为这类公开渠道简历投放数量巨大,通常 HR 那边就会比较严格地筛选简历,拿我们公司来说,通常在这些渠道看 20 份简历,才会有 1 份愿意约面的简历。而且 HR 会只挑比较好的学校或者公司的候选人,也不排除还有例如笔试这种更多的面试流程。但是面试经验都是慢慢积累的,建议你也可以尝试这些渠道。 119 | 120 | # 3.面试流程 121 | ## 1.流程简述 122 | 123 | 就我所知,大部分的 iOS 公司的面试流程都大同小异。我们先简述一下大体的流程,然后再详细讨论。 124 | 125 | 在面试的刚开始,面试官通常会要求你做一个简短的自我介绍。然后面试官可能会和你聊聊你过去的实习项目或者工作内容。接着面试官可能会问你一些具体的技术问题,有经验的面试官会尽量找一些和你过去工作相关的技术问题。最后,有些公司会选择让你当场写写代码,Facebook 会让你在白板上写代码,国内的更多是让你在 A4 纸上写。有一些公司也会问一些系统设计方面的问题,考查你的整体架构能力。在面试快要结束时,通常面试官还会问你有没有别的什么问题。 126 | 127 | 以上这些流程,不同公司可能会跳过某些环节。比如有一些公司就不会考察当场在白板或 A4 纸上写代码。有些公司可能跳过问简历的环节直接写代码,特别是校园招聘的时候,因为应届生通常项目经验较少。面试流程图如下所示: 128 | 129 | ![面试流程图](https://upload-images.jianshu.io/upload_images/22877992-d2ed6a806ceaa66a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 130 | 131 | ## 2.自我介绍 132 | 133 | 自我介绍通常是面试中最简单、最好准备的环节。 134 | 135 | 一个好的自我介绍应该结合公司的招聘职位来做定制。比如公司有硬件的背景,就应该介绍一下在硬件上的经验或者兴趣;公司如果注重算法能力,则介绍自己的算法练习;公司如果注重团队合作,那么你介绍一下自己的社会活动都是可以的。 136 | 137 | 一个好的自我介绍应该提前写下来,并且背熟。因为候选人通常的紧张感都是来自于面试刚开始的几分钟,如果你刚开始的几分钟讲的结结巴巴,那么这种负面情绪会加剧你面试时的紧张感,从而影响你正常发挥。如果你提前把自我介绍准备得特别流利,那么开始几分钟的紧张感过去之后,你很可能就会很快进入状态,而忘记紧张这个事情了。 138 | 139 | 即使做到了以上这些仍然是不够的,候选者常见的问题还包括: 140 | 141 | * 太简短 142 | * 没有重点 143 | * 太拖沓 144 | * 不熟练 145 | 146 | 我们在展开讨论上面这些问题之前,我们可以站在面试官的立场考虑一下:如果你是面试官,你为什么要让候选人做自我介绍?你希望在自我介绍环节考察哪些信息? 147 | 148 | 在我看来,自我介绍环节相当重要,因为: 149 | 150 | * 首先它考察了候选人的表达能力。大部分的程序员表达能力可能都一般,但是如果连自我介绍都说不清楚,通常就说明表达沟通能力稍微有点问题了。面试官通过这个环节可以初步考察到候选人的表达能力。 151 | * 它同样也考察了候选人对于面试的重视程度。一般情况下,表达再差的程序员,也可以通过事先拟稿的方式,把自我介绍内容背下来。如果一个人自我介绍太差,说明他不但表达差,而且不重视这次面试。 152 | * 最后,自我介绍对之后的面试环节起到了支撑作用。因为自我介绍中通常会涉及自己的项目经历,自己擅长的技术等。这些都很可能吸引面试官追问下去。好的候选人会通过自我介绍 “引导” 面试官问到自己擅长的领域知识。如果面试官最后对问题答案很满意,通过面试的几率就会大大增加。 153 | 154 | 所以我如果是面试官,我希望能得到一个清晰流畅的自我介绍。下面我们来看看候选人在面试中的常见问题。 155 | 156 | ### 1)太简短 157 | 158 | 一个好的自我介绍大概是 3~5 分钟。过短的自我介绍没法让面试官了解你的大致情况,也不足以看出来你的基本表达能力。 159 | 160 | 如果你发现自己没法说出足够时间的自我介绍。可以考虑在介绍中加入:自己的简单的求学经历,项目经历,项目中有亮点的地方,参与或研究过的一些开源项目,写过的博客,其它兴趣爱好,自己对新工作的期望和目标公司的理解。 161 | 162 | 我相信每个人经过准备,都可以做到一个 5 分钟长度的自我介绍。 163 | 164 | ### 2)没有重点 165 | 166 | 突破了时间的问题,接下来就需要掌握介绍的重点。通常一个技术面试,技术相关的介绍才是重点。所以求学经历,兴趣爱好之类的内容可以简单提到即可。 167 | 168 | 对于一个工作过的开发者,你过去做的项目哪个最有挑战,最能展示出你的水平其实自己应该是最清楚的。所以大家可以花时间把这个内容稍微强调一下。 169 | 170 | 当然你也没必要介绍得太细致,面试官如果感兴趣,自然会在之后的面试过程中和你讨论。 171 | 172 | ### 3)太拖沓 173 | 174 | 有些工作了几年的人,做过的项目差不多有个 3~5 个,面试的时候就忍不住一个一个说。单不说这么多项目在自我介绍环节不够介绍。就是之后的详细讨论环节,面试官也不可能讨论完你的所有项目经历。 175 | 176 | 所以千万不要做这种 “罗列经历” 的事情,你要做的就是挑一个或者最多两个项目,介绍一下项目大致的背景和你解决的主要问题即可。至于具体的解决过程,可以不必介绍。 177 | 178 | ### 4)不熟练 179 | 180 | 即便你的内容完全合适,时间长度完全合理,你也需要保证一个流利的陈述过程。适当在面试前多排练几次,所有人都可以做到一个流利的自我介绍。 181 | 182 | 还有一点非常神奇,就是一个人在做一件事情的时候,通常都是开始的前以及刚开始几分钟特别紧张。比如考试,演讲或者面试,通常这几分钟之后,人们进入 “状态” 了,就会忘记紧张了。 183 | 184 | 将自己的自我介绍背下来,可以保证一个流利顺畅的面试开局,这可以极大舒缓候选人的紧张情绪。相反,一开始自我介绍就结结巴巴,会加剧候选人的紧张情绪,而技术面试如果不能冷静的话,是很难在写代码环节保证逻辑清晰正确的。 185 | 186 | 所以,请大家务必把这个小环节重视起来,做出一个完美的开局。 187 | 188 | ## 3.项目介绍 189 | 190 | 自我介绍之后,就轮到讨论更加具体的内容环节了,通常面试官都会根据自我介绍或者你的简历,选一个他感兴趣的项目来详细讨论。 191 | 192 | 这个时候,大家务必需要提前整理出自己参与的项目的具体挑战,以及自己做得比较好的地方。切忌不要说:“这个项目很简单,没什么挑战,那个项目也很简单,没什么好说的”。再简单的事情,都可以做到极致的,就看你有没有一个追求完美的心。 193 | 194 | 比如你的项目可能在你看来就是摆放几个 iOS 控件。但是,这些控件各自有什么使用上的技巧,有什么优化技巧?其实如果专心研究,还是有很多可以学习的。拿 UITableView 来说,一个人如果能够做到把它的滑动流程度优化好,是非常不容易的。这里面涉及网络资源的异步加载、图片资源的缓存、后台线程的渲染、CALayer 层的优化等等。 195 | 196 | 这其实也要求我们做工作要精益求精,不求甚解。所以一场成功的面试最最本质上,看得还是自己平时的积累。如果自己平时只是糊弄工作,那么面试时就很容易被看穿。 197 | 198 | 在这一点上,我奉劝大家在自己的简历上一定要老实。不要在建简历上弄虚作假,把自己没有做过的项目写在里面。 199 | 200 | 顺便我在这里也教一下大家如何面试别人。如果你是面试官,考察简历的真假最简单的方法就是问细节。一个项目的细节如果问得很深入,候选人有没有做过很容易可以看出来。 201 | 202 | 举个例子,如果候选人说他在某公司就职期间做了某某项目。你就可以问他: 203 | 204 | * 这个工作具体的产品需求是什么样的? 205 | * 大概做了多长时间? 206 | * 整体的软件架构是什么样的? 207 | * 涉及哪些人合作?几个开发和测试? 208 | * 项目的时间排期是怎么定的? 209 | * 你主要负责的部分和合作的人? 210 | * 项目进行中有没有遇到什么问题? 211 | * 这个项目最后最大的收获是什么?遗憾是什么? 212 | * 项目最困难的一个需求是什么?具体什么实现的? 213 | 214 | 面试官如果做过类似项目,还可以问问通常这个项目常见的坑,看看候选人是什么解决的。 215 | 216 | ## 4.写代码 217 | 218 | 编程能力,说到底还是一个实践的能力,所以说大部分公司都会考察当场写代码。我面试过上百人,见到过很多候选人在自我介绍和项目讨论时都滔滔不绝,侃侃而谈,感觉非常好。但是一到写代码环节就怂了,要么写出来各种逻辑问题和细节问题没处理好,要么就是根本写不出来。 219 | 220 | 由于人最终招进来就是干活写代码的,所以如果候选人当场写代码表现很差的话,基本上面试就挂掉了。 221 | 222 | 程序员这个行业,说到底就是一个翻译的工作,把产品经理的产品文档和设计师的 UI 设计,翻译成计算机能够理解的形式,这个形式通常就是一行一行的源码。 223 | 224 | 当面试官考察你写代码的时候,他其实在考察: 225 | 226 | * 你对语言的熟悉程度。如果候选人连常见的变量定义和系统函数都不熟悉,说明他肯定经验还是非常欠缺。 227 | * 你对逻辑的处理能力。产品经理关注的是用户场景和核心需求,而程序员关注的是逻辑边界和异常情况。程序的 bug 往往就是边界和特殊情况没有处理好。虽然说写出没有 bug 的程序几乎不可能,但是逻辑清晰的程序员能够把思路理清楚,减少 bug 发生的概率。 228 | * 设计和架构能力。好的代码需要保证易于维护和修改。这里面涉及很多知识,从简单的 “单一职责” 原则,到复杂的 “好的组合优于继承” 原则,其中设计模式相关的知识最多。写代码的时候多少还是能够看出这方面的能力。另外有些公司,例如 Facebook,会有专门的系统设计(System Design)面试环节,专注于考察设计能力。 229 | 230 | ## 5.系统设计 231 | 232 | 有一些公司喜欢考查一些系统设计的问题,简单来说,就是让你解决一个具体的业务需求,看看你是否能够将业务逻辑梳理清楚,并且拆解成各个模块,设计好模块间的关系。举几个例子,面试官可能让你: 233 | 234 | * 设计一个类似微博的信息流应用。 235 | * 设计一个本地数据缓存架构。 236 | * 设计一个埋点分析系统。 237 | * 设计一个直播答题系统。 238 | * 设计一个多端的数据同步系统。 239 | * 设计一个动态补丁的方案。 240 | 241 | 这些系统的设计都非常考查一个人知识的全面性。通常情况下,如果一个人只知道 iOS 开发的知识,是很难做出相关的设计的。为了能够解决这些系统设计题,我们首先需要有足够的知识面,适度了解一下 Android 端、Web 端以及服务器端的各种技术方案背后的原理。你可以不写别的平台的代码,但是一定要理解它们在技术上能做到什么,不能做到什么。你也不必过于担心,面试官在考查的时候,还是会重点考查 iOS 相关的部分。 242 | 243 | 我们将在下一小节,展开讨论如何准备和回答系统设计题。 244 | 245 | ## 6.提问 246 | 247 | 提问环节通常在面试结束前,取决于前面的部分是否按时结束,有些时候前面的环节占用了太多时间,也可能没有提问环节了。在后面的章节,我们会展开讨论一下如何提问。 248 | # 资料推荐 249 | 250 | 如果你正在跳槽或者正准备跳槽不妨动动小手,添加一下咱们的交流群[**101 295 1431**](https://links.jianshu.com/go?to=https%3A%2F%2Fjq.qq.com%2F%3F_wv%3D1027%26k%3D5JFjujE)来获取一份详细的大厂面试资料为你的跳槽多添一份保障。 251 | 252 | ![](https://upload-images.jianshu.io/upload_images/22877992-0bfc037cc50cae7d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 253 | 254 | -------------------------------------------------------------------------------- /经验之谈-面向协议的编程.md: -------------------------------------------------------------------------------- 1 | 2015 年 WWDC,苹果第一次提出了 Swift 的面向协议编程(Protocol Oriented Programming,以下简称 POP ),这是计算机历史上一个全新的编程范式。在此之前,相对应的面向对象的编程(Object Oriented Programming,以下简称 OOP )已经大行其道 50 年,它几乎完美的解决函数式编程(Functional Programming)的缺点,并且出现在从大型系统到小型应用、从服务器端到前端的各个方面。它的优点被无数程序员称颂,它解决了诸多开发中的大小问题。那么问题来了,既然 OOP 如此万能,为什么 Swift 要弄出全新的 POP ? 2 | 3 | ![](https://upload-images.jianshu.io/upload_images/22877992-e59c370779cad098.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 4 | 5 | 笔者认为,原因有三。其一,OOP 有自身的缺点。在继承、代码复用等方面,其灵活度不高。而 POP 恰好可以优雅得解决这些问题;其二,POP 可以保证 Swift 作为静态语言的安全性,而彼时 Objective-C 时代的 OOP,其动态特性经常会导致异常;其三,OOP 无法应用于值类型,而 POP 却可以将其优势拓展到结构体(struct)和枚举(enum)类型上。 6 | 7 | 本节将通过问题串联的形式,说明 POP 相比于 OOP 的优势,同时展示 POP 在实际开发中的运用。 8 | 9 | ## POP vs OOP 10 | 11 | ### 1.什么是 OOP ?它在 iOS 开发中有哪些优点? 12 | 13 | **关键词:#面向对象编程** 14 | 15 | OOP 全称是 Object Oriented Programming,即面向对象的编程,是目前最主流的编程范式。在 iOS 开发中,绝大多数的部分运用的都是 OOP。 16 | 17 | 在 iOS 开发中,它有如下优点: 18 | 19 | * **封装和权限控制。**相关的属性和方法被放入一个类中,Objective-C 中 ".h" 文件负责声明公共变量和方法,".m" 文件负责声明私有变量,并实现所有方法。Swift 中也有 public/internal/fileprivate/private 等权限控制。 20 | 21 | * **命名空间。**在 Swift 中,不同的 class 即使命名相同,在不同的 bundle 中由于命名空间不同,它们依然可以和谐共存毫无冲突。这在 App 很大、bundle 很多的时候特别有用。Objective-C 没有命名空间,所以很多类在命名时都加入了驼峰式的前缀。 22 | 23 | * **扩展性。**在 Swift 中,class 可以通过 extension 来进行增加新方法,通过动态特性亦可以增加新变量。这样我们可以保证在不破坏原来代码封装的情况下实现新的功能。Objective-C 中,我们可以用 category 来实现类似功能。另外,Swift 和 Objective-C 中还可以通过 protocol 和代理模式来实现更加灵活的扩展。 24 | 25 | * **继承和多态。**同其他语言一样,iOS 开发中我们可以将共同的方法和变量定义在父类中,在子类继承时再各自实现对应功能,做到代码复用的高效运作。同时针对不同情况可以调用不同子类,大大增加代码的灵活性。 26 | 27 | 如果你正在跳槽或者正准备跳槽不妨动动小手,添加一下咱们的交流群[**101 295 1431**](https://jq.qq.com/?_wv=1027&k=SSQAVXir)来获取一份详细的大厂面试资料为你的跳槽多添一份保障。 28 | 29 | 30 | ### 2.请谈谈 OOP 在 iOS 开发中的缺点 31 | 32 | **关键词:#内存 #继承** 33 | 34 | 一般面试官这样问,我们不仅要回答出缺点,还要说出一个比较成熟的解决方案。一个专业的程序员不仅要知道问题出在哪里,更要知道该怎么修正问题。 35 | 36 | OOP 有以下几个缺点: 37 | 38 | * **隐式共享。**class 是引用类型,在代码中某处改变某个实例变量的时候,另一处在调用此变量时就会受此修改影响。示例代码如下: 39 | 40 | ``` 41 | class People { var name = “”} 42 | // 创建张三,设置其名字为张三 43 | let zhangSan = People() 44 | zhangSan.name = “张三” 45 | 46 | // 创建李四,设置其名字为李四 47 | let liSi = zhangSan 48 | Lisi.name = “李四” 49 | 50 | print(zhangSan.name) // 李四 51 | print(Lisi.name) // 李四 52 | 53 | ``` 54 | 55 | 这很容易就造成异常。尤其是在多线程时,我们经常遇到的资源竞速(Race Condition)就是这个情况。解决方案是在多线程时枷锁,当然这个方案会引入死锁和代码复杂度剧增的问题。最好的解决这个问题是尽可能用诸如 struct 这样的值类型取代 class。 56 | 57 | * **冗杂的父类。**试想这样一种场景,一个 UIViewController 的子类和一个 UITableViewController 中都需要加入 handleSomething() 这种方法。OOP 的解决方案是直接在 UIViewController 的 extension 中加入 handleSomething()。但是随着新方法越加越多,以后 UIVIewController 会越变越冗杂。当然我们也可以引入一个专门的父类或工具类,但是依然有职权不明确、依赖、冗杂等多种问题。 58 | 59 | 另一方面,父类中的 handleSomething() 方法必须由具体实现,它不能根据子类做出灵活调整。子类如果要做特定操作,必须要重写方法来实现。既然子类要重写,那么在父类中的实现在这种时候就显得多此一举。解决方案使用 protocol,这样它的方法就不需要用具体实现了,交给服从它的类或结构体即可。 60 | 61 | * **多继承。** Swift 和 Objective-C 是不支持多继承的,因为这会造成菱形问题,即多个父类实现了同一个方法,子类无法判断继承哪个父类的情况。在 Java 中,有 interface 的解决方案,Swift 中有类似 protocol 的解决方案。 62 | 63 | ### 2.说说 POP 相比于 OOP 的优势 64 | 65 | **关键词:#灵活 #安全** 66 | 67 | 这道题是一个开放性的问题。在面试中一个很好的回答方式是理论+举例。POP 相比 OOP 具有如下优势。 68 | 69 | * **更加灵活。**比如上题中我们提到的冗杂的父类的例子。我们可以用协议和其扩展来让所有服从此协议的 class 都可以用到默认的 handleSomething() 方法,同时服从了该协议的同时也增加了代码的可读性。具体代码如下: 70 | 71 | ``` 72 | protocol SomethingHandleable { 73 | func handleSomething() 74 | } 75 | 76 | extension SomethingHandleable { 77 | func handleSomething() { 78 | // 实现 79 | } 80 | } 81 | 82 | class ViewController: UIViewController, SomethingHandleable { } 83 | class TableViewController: UITableViewController, SomethingHandleable { } 84 | 85 | ``` 86 | 87 | * **减少依赖。**相对于传入具体的实例变量,我们可以传入 protocol 来实现多态。同时测试时也可以利用 protocol 来 mock 真实的实例,减少对于对象及其实现的依赖。比如下面这个实例: 88 | 89 | 如果你正在跳槽或者正准备跳槽不妨动动小手,添加一下咱们的交流群[**101 295 1431**](https://jq.qq.com/?_wv=1027&k=SSQAVXir)来获取一份详细的大厂面试资料为你的跳槽多添一份保障。 90 | 91 | 92 | ``` 93 | protocol Request { 94 | func send(request: Info) 95 | } 96 | 97 | protocol Info {} 98 | 99 | class UserRequest: Request { 100 | // 注意这里我们传入了Info这个protocol,它无需是具体的UserInfo,这方便了我们之后测试和扩展 101 | func send(info: Info) { 102 | // 实际实现,一般是把info发给server 103 | } 104 | } 105 | 106 | class UserInfo: Info {} 107 | 108 | class MockUserRequest: Request { 109 | func send(info: Info) { // 这里我们就可以为测试方便来自定义实现 } 110 | } 111 | 112 | func testUserRequest() { 113 | let userRequest = MockUserRequest() 114 | userRequest.send(info: UserInfo()) 115 | } 116 | 117 | ``` 118 | 119 | * **消除动态分发的风险。**对于服从了 protocol 的类或结构体来说,它必须实现 protocol 声明的所有方法。否则编译时就会报错,这根本上杜绝了 runtime 时程序的风险,下面就是 POP 和 OOP 在动态派发时的对比: 120 | 121 | ``` 122 | // Objective-C下动态派发runtime报错实例 123 | ViewController *vc = ... 124 | [vc handleSomething]; 125 | 126 | TableViewController *tvc = ... 127 | [tvc handleSomething]; 128 | 129 | NSObject *ob = ... // ob 没有实现handleSomething 130 | NSArray *array = @[vc, tvc, ob]; 131 | for (id obj in array) { 132 | [obj handleSomething]; // 能通过编译,但运行到ob时程序会崩溃 133 | } 134 | 135 | // Swift中使用了POP 136 | let vc = ... 137 | let tvc = ... 138 | let ob = ... 139 | 140 | let array: [SomethingHandleable] = [vc, tvc, ob] // 这里直接会报错,因为ob没有实现SomethingHandleable协议 141 | 142 | ``` 143 | 144 | * **协议可以用于值类型。**相比于 OOP 只能用于 class,POP 可以用于 struct 和 enum 这样的值类型上。比如下面这个例子: 145 | 146 | ``` 147 | protocol Flyable { } 148 | 149 | protocol Bird { 150 | var name: String { get } 151 | var canFly: Bool { get } 152 | } 153 | 154 | extension Bird { 155 | var canFly: Bool { return self is Flyable } 156 | } 157 | 158 | struct ButterFly: Flyable {} 159 | 160 | struct Penguin: Bird { 161 | var name = "Penguin" 162 | } 163 | 164 | struct Eagle: Bird, Flyable { 165 | var name = “Eagle” 166 | } 167 | 168 | enum FlyablePokemon: Flyable { 169 | case Pidgey 170 | case Duduo 171 | } 172 | 173 | ``` 174 | 175 | ## POP 面试实战 176 | 177 | 如果你正在跳槽或者正准备跳槽不妨动动小手,添加一下咱们的交流群[**101 295 1431**](https://jq.qq.com/?_wv=1027&k=SSQAVXir)来获取一份详细的大厂面试资料为你的跳槽多添一份保障。 178 | 179 | 180 | ### 4.要给一个 UIButton 增加一个点击后抖动的效果,该怎样实现? 181 | 182 | **关键词:#扩展 #协议** 183 | 184 | 解决方案有三种。个人推荐用 protocol 来解决。 185 | 186 | * 实现一个自定义的 UIButton 类,在其中添加点击抖动效果的方法(shake 方法); 187 | 188 | * 写一个 UIButton 或者 UIView 的拓展(extension),然后在其中增加 shake 方法; 189 | 190 | * 定义一个 protocol,然后在协议扩展(protocol extension)中添加 shake 方法; 191 | 192 | 分析这三种方法: 193 | 194 | * 在自定义的类中添加 shake 方法扩展性不好。如果 shake 方法被用在其他地方,又要在其他类中再添加一遍 shake 方法,这样代码复用性差。 195 | 196 | * 在 extension 中实现虽然解决了代码复用性问题,但是可读性比较差。团队开发中并不是所有人都知道这个 extension 中存在 shake 方法,同时随着功能的扩展,extension 中新增的方法会层出不穷,它们很难归类管理。 197 | 198 | * 用协议定义解决了复用性、可读性、维护性三个难题。协议的命名(例如 Shakeable)直接可以确定其实现的 UIButton 拥有相应 shake 功能;通过协议扩展,可以针对不同类实现特定的方法,可维护性也大大提高;因为协议扩展通用于所有实现对象,所以代码复用性也很高。 199 | 200 | ### 5.优化以下代码 201 | 202 | **关键词:#Self #关联类型** 203 | 204 | ``` 205 | protocol Food {} 206 | struct Fish: Food {} 207 | struct Bone: Food {} 208 | 209 | protocol Animal { 210 | func eat(food: Food) 211 | func greet(other: Animal) 212 | } 213 | 214 | struct Cat: Animal { 215 | func eat(food: Food) { 216 | guard let _ = food as? Fish else { 217 | print("猫只吃鱼!") 218 | return 219 | } 220 | } 221 | 222 | func greet(other: Animal) { 223 | if let _ = other as? Cat { 224 | print("喵~") 225 | } else { 226 | print("猫很傲娇,不会对其他动物打招呼!") 227 | } 228 | } 229 | } 230 | 231 | struct Dog: Animal { 232 | func eat(food: Food) { 233 | guard let _ = food as? Bone else { 234 | print("狗只啃骨头!") 235 | return 236 | } 237 | } 238 | 239 | func greet(other: Animal) { 240 | if let _ = other as? Cat { 241 | print("汪~") 242 | } else { 243 | print("狗很骄傲,不会像其他动物打招呼!") 244 | } 245 | } 246 | } 247 | 248 | ``` 249 | 250 | 首先理清这道题目的基本逻辑,有 2 个协议,分别是 Food 和 Animal,然后两个结构体 fish 和 bone 分别服从 food 协议,cat 和 dog 分别服从 animal 协议。 251 | 252 | 如果你正在跳槽或者正准备跳槽不妨动动小手,添加一下咱们的交流群[**101 295 1431**](https://jq.qq.com/?_wv=1027&k=SSQAVXir)来获取一份详细的大厂面试资料为你的跳槽多添一份保障。 253 | 254 | 255 | 其中又有两个方法为 eat 和 greet,我们发现分别在 cat 和 dog 中,eat 方法有对应类型的参数,同时 greet 也对应类型的参数。所以假如 cat 和 dog 中能在服从 Animal 协议的同时,又写出对应自己类型的函数,那就可以省掉 if else 这类判断了。比如下面这样: 256 | 257 | ``` 258 | struct Dog: Animal { 259 | func eat(food: Bone) {} 260 | func greet(other: Dog) { print("汪~") } 261 | } 262 | 263 | struct Cat: Animal { 264 | func eat(food: Fish) {} 265 | func greet(other: Cat) { print("喵~") } 266 | } 267 | 268 | ``` 269 | 270 | 很遗憾直接写成这样,程序是通不过编译的,Xcode 会提示,Cat 和 Dog 没有服从 Animal 协议,因为协议中要求的 food 必须是 Food,不能是 Bone 或者 Fish ,同理 greet 也是同样要求。但是我们可以用 Self 和关联类型去改进 Animal 协议,这样 Cat 和 Dog 这样写就没问题了。代码如下: 271 | 272 | ``` 273 | protocol Animal { 274 | associatedtype FoodType: Food 275 | 276 | func greet(other: Self) 277 | func eat(food: FoodType) 278 | } 279 | 280 | ``` 281 | 282 | Self 相当于是 protocol 的占位符,它表示任意只要满足 Animal 的类型皆可。associatedType 就是关联类型,它实际上是一个类型的占位符,这样我们可以让 Dog 和 Cat 来指定 FoodType 到底是什么类型。而根据 greet 方法中对 FoodType 的使用,Swift 可以自动推断,FoodType 在 Cat 中是 Fish,在 Dog 中是 Bone。 283 | 284 | ### 6.试用 Swift 实现二分搜索算法 285 | 286 | **关键词:#Self #泛型** 287 | 288 | 首先要审题,二分搜索算法,那么输入的对象是什么?是整型数组还是浮点型数组?如果输入不是排序过的数组该如何抛出异常?这些都是要在写答案之前与面试官探讨的问题。 289 | 290 | 我们先来热个身,假如面试官要求写出对于整型排序数组的二分搜索算法,则代码如下: 291 | 292 | ``` 293 | func binarySearch(sortedElements: [Int], for element: Int) -> Bool { 294 | var lo = 0, hi = sortedElements.count - 1 295 | 296 | while lo <= hi { 297 | let mid = lo + (hi - lo) / 2 298 | if sortedElements[mid] == element { 299 | return true 300 | } else if sortedElements[mid] < element { 301 | lo = mid + 1 302 | } else { 303 | hi = mid - 1 304 | } 305 | } 306 | 307 | return false 308 | } 309 | 310 | ``` 311 | 312 | 如果你正在跳槽或者正准备跳槽不妨动动小手,添加一下咱们的交流群[**101 295 1431**](https://jq.qq.com/?_wv=1027&k=SSQAVXir)来获取一份详细的大厂面试资料为你的跳槽多添一份保障。 313 | 314 | 315 | 上面的方法完成了面试官的要求,但是有如下几个问题。首先,这个方法只适用于整型数组;其次,虽然变量名为 sortedElements,但是我们无法保证输入的数组就一定是按序排列的。我们来看看用面向协议的编程来实现二分法: 316 | 317 | ``` 318 | extension Array where Element: Comparable { 319 | public var isSorted: Bool { 320 | 321 | var previousIndex = startIndex 322 | var currentIndex = startIndex + 1 323 | 324 | while currentIndex != endIndex { 325 | if self[previousIndex] > self[currentIndex] { 326 | return false 327 | } 328 | 329 | previousIndex = currentIndex 330 | currentIndex = currentIndex + 1 331 | } 332 | 333 | return true 334 | } 335 | } 336 | 337 | func binarySearch(sortedElements: [T], for element: T) -> Bool { 338 | // 确保输入数组是按序排列的 339 | assert(sortedElements.isSorted) 340 | 341 | var lo = 0, hi = sortedElements.count - 1 342 | 343 | while lo <= hi { 344 | let mid = lo + (hi - lo) / 2 345 | 346 | if sortedElements[mid] == element { 347 | return true 348 | } else if sortedElements[mid] < element { 349 | lo = mid + 1 350 | } else { 351 | hi = mid - 1 352 | } 353 | } 354 | 355 | return false 356 | } 357 | 358 | ``` 359 | 360 | 上面解法首先在 Array 的扩展中加入了新变量 isSorted 用于判断输入的数组是否按序排列。之后在 binarySearch 的方法中运用了泛型,保证其中每一个元素都遵循 Comparable 协议,并且所有元素都是一个类型。有了上面的写法,我们可以将二分搜索法运用到各种类型的数组中,灵活性大大提高,例如: 361 | 362 | ``` 363 | binarySearch(sortedElements: [1,4,7], for: 4) // true 364 | binarySearch(sortedElements: [1.0,3.2,9.23], for: 3.2) // true 365 | binarySearch(sortedElements: ["1","2","3"], for: "4") // false 366 | binarySearch(sortedElements: ["4","2","3"], for: "4") // assert failure 367 | 368 | ``` 369 | 370 | 当然,上面方法还可以进一步优化。例如 Array 的扩展可以放到 Collection 之中;isSorted 可以接受数学符号进行正反向排序查询;binarySearch 方法可以直接写到 Collection 的扩展中进行调用。总之,运用 POP 的思路,可以写出严谨、灵活的代码,其实用性和可读性也非常之好。 371 | # 推荐👇: 372 | 373 | 如果你正在跳槽或者正准备跳槽不妨动动小手,添加一下咱们的交流群[**931 542 608**](https://jq.qq.com/?_wv=1027&k=cfVxrMAH)来获取一份详细的大厂面试资料为你的跳槽多添一份保障。 374 | 375 | ![](https://upload-images.jianshu.io/upload_images/22877992-0bfc037cc50cae7d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 376 | -------------------------------------------------------------------------------- /系统框架-UIScrollView及其子类.md: -------------------------------------------------------------------------------- 1 | UIScrollView 恐怕是所有 App 都绕不过去的类——尤其是它的子类 UITableView 和 UICollectionView。看看我们日常常见的 App,新闻类的今日头条,社交类的微博和微信,电商类的淘宝、腾讯,日常管理用的备忘录和图片 App 的缩放功能,都或多或少得使用了 UIScrollView 及其子类。 2 | 3 | ![](https://upload-images.jianshu.io/upload_images/22877992-841bdcd834a1cc4e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 4 | 5 | 当一个屏幕无法展示 App 需要展示的所有内容时,就是 UIScrollView 大展拳脚的时候:通过使用 UIScrollView,用户可以滑动或是缩放屏幕,来看单个屏幕无法展示的内容。如何定制不同 Cell 的 UI、如何与用户交互、如何与服务器端数据同步、如何在滑动时最大限度保证界面的流畅,这些都是考察的要点,是一个 iOS 工程师必备的基本技能。 6 | 7 | ## 基本理论 8 | 9 | ### 1.请说明并比较以下关键词:contentView,contentInset,contentSize,contentOffset。 10 | 11 | **关键词:#UIScrollView** 12 | 13 | * **UIScrollView 上显示内容的区域被称为 contentView。**一般情况下我们对 UIScrollView 的操作,例如 addSubview 这样的操作都是在 contentView 上进行。 14 | 15 | * **contentInset 是指 contentView 与 UIScrollView 的边界。**与网页开发的 padding 类似,分别指 contentView 的四条边到 UIScrollView 的对应边的距离,分别为 top,bottom,left,right。 16 | 17 | * **contentSize 是指 contentView 的大小。**它一般超过屏幕大小,是整个 UIScrollView 实际内容的大小。比如一张图片有四个屏幕之大,我们在缩放的时候只能看到其 1/4 的内容,那么它的 contentSize 就是四个屏幕合起来的尺寸大小。 18 | 19 | * **contentOffset 是当前 contentView 浏览位置左上角点的坐标。**它是相对于整个 UIScrollView 左上角为左边原点而言。默认为 CGPointZero。 20 | 21 | 下图是对这几个关键词的说明: 22 | ![image](https://upload-images.jianshu.io/upload_images/22877992-44cd32767da40d28.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 23 | 24 | ### 2\. 请说明 UITableViewCell 的重用机制 25 | 26 | **关键词:#UITableViewCell #reuseIdentifier** 27 | 28 | UITableView 的每一行就是 UITableViewCell。绝大多数 UITableViewCell 的构图都一样,只是内容不同而已。所以我们将同一类型的 UITableViewCell 标记为相同的 Identifier,然后用reuseIdentifier 去进行构建,配合不同内容进行批量使用。 29 | 30 | 当用户滑动列表的时候,如果 reuseIdentifier 不为 nil,UITableView 会自动去调用已经生成好的UITableViewCell 来展示内容。否则每次滑动,UITableView 都会重新生成一个新的 UITableViewCell,这样极其浪费资源,而且容易造成主线程卡顿。 31 | 32 | ### 3\. 请说明并比较以下协议:UITableViewDelegate,UITableViewDataSource 33 | 34 | **关键词:#数据 #UI** 35 | 36 | * 一般在 UIViewController 上配置 UITableView,都会用到这 2 个协议,这 2 个协议由当前 UIVIewController 实现。 37 | 38 | * UITableViewDataSource 用来管控 UITableView 的实际数据:例如有多少 section,每个 section 有多少行,每行用哪种 UITableViewCell。其中 numOfRows 和 cellForRowAtIndexPath 这两个方法必须被实现,numOfSections 默认为 1。另外UITableViewDataSource还负责拖拽、修改、删除列表操作,因为这会对数据源进行修改。 39 | 40 | * UITableViewDelegate 用来处理 UITableView 的 UI 和交互:例如设置 UITableView 的 header 和 footer,点击、高亮某个 UITableViewCell 对应的操作。它所有的方法都是可选方法,有默认实现。 41 | 42 | ### 4\. 请说明并比较以下协议:UICollectionViewDelegate,UICollectionViewDataSource,UICollectionViewDelegateFlowLayout 43 | 44 | **关键词:#数据 #UI #Layout** 45 | 46 | * 一般在 UIViewController 上配置 UICollectionView,都会用到这 3 个协议,这 3 个协议由当前 UIVIewController 实现。 47 | 48 | * UICollectionViewDataSource 用来管控 UICollectionView 的实际数据:例如有多少 section,每个 section 有多少个 item,每个 item 对应的 UI 如何 。其中 numOfItems 和 cellForItemAtIndexPath 这两个方法必须被实现,numOfSections 默认为 1。另外UICollectionViewDataSource还负责拖拽、修改、删除列表操作,因为这会对数据源进行修改。 49 | 50 | * UICollectionViewDelegate 用来处理交互:例如设置点击、高亮某个 item 对应的操作。它所有的方法都是可选方法,有默认实现。 51 | 52 | * UICollectionViewDelegateFlowLayout 用来处理 UICollectionView 的布局及其行为。比如具体 item 的尺寸大小, item 之间的间距,header 和 footer 的大小和间距,以及 UICollectionView 的滚动方向。这个协议的所有方法也都是可选方法,有默认实现。 53 | 54 | 如果你正在跳槽或者正准备跳槽不妨动动小手,添加一下咱们的交流群[**931 542 608**](https://jq.qq.com/?_wv=1027&k=cfVxrMAH)来获取一份详细的大厂面试资料为你的跳槽多添一份保障。 55 | 56 | ## 拓展知识 57 | 58 | ### 5.代码实现:实现一个 10 行的列表,每行随机显示一个 0 – 100 之间的整数。用户可以删除、移动任何一行,下拉则列表中的数字重新刷新。 59 | 60 | **关键词:#UITableViewDataSource #UITableViewDelegate #refreshControl** 61 | 62 | 本题主要考察 UITableView 最基本的用法:主要涉及 UITableViewDataSource,UITableViewDelegate 这两个协议的使用和 refreshControl 的我们将这道题拆解为 3 个步骤。第一步,实现一个 10 行列表,每行随机显示 0 到 100 之间的整数。示例代码如下: 63 | 64 | ``` 65 | class ViewController: UIViewController { 66 | @IBOutlet weak var tableView: UITableView! 67 | 68 | fileprivate var nums = [Int]() 69 | fileprivate let numOfRows = 10 70 | fileprivate let maxNum = 100 71 | 72 | override func viewDidLoad() { 73 | super.viewDidLoad() 74 | configureTableViewDataSource() 75 | } 76 | 77 | func configureTableViewDataSource() { 78 | generateRandomNums() 79 | tableView.reloadData() 80 | } 81 | 82 | func generateRandomNums() { 83 | nums.removeAll() 84 | 85 | for _ in 0.. Int { 93 | return nums.count 94 | } 95 | 96 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 97 | let cell = UITableViewCell() 98 | cell.textLabel?.text = "\(nums[indexPath.row])" 99 | return cell 100 | } 101 | } 102 | 103 | ``` 104 | 105 | 第二步,实现下拉刷新的效果。主要就是给 tableView 添加 refreshControl,它能够重新生成随机数并加载 tableView。相关代码如下: 106 | 107 | ``` 108 | override func viewDidLoad() { 109 | … 110 | let refreshControl = UIRefreshControl() 111 | refreshControl.addTarget(self, action: Selector.handleRefresh, for: .valueChanged) 112 | tableView.addSubview(refreshControl) 113 | } 114 | 115 | @objc func handleRefresh() { 116 | configureTableViewDataSource() 117 | refreshControl.endRefreshing() 118 | } 119 | 120 | extension Selector { 121 | static let handleRefresh = #selector(ViewController.handleRefresh) 122 | } 123 | 124 | ``` 125 | 126 | 第三步,实现列表删除和移动功能。主要就是用 UITableViewDelegate 实现 move 和 delete 的操作,相关代码如下: 127 | 128 | ``` 129 | // MARK: - UITableViewDataSource 130 | extension ViewController: UITableViewDataSource { 131 | func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) { 132 | 133 | let moveNum = nums[sourceIndexPath.row] 134 | nums.remove(at: sourceIndexPath.row) 135 | nums.insert(moveNum, at: destinationIndexPath.row) 136 | } 137 | 138 | func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) { 139 | 140 | switch editingStyle { 141 | case .delete: 142 | nums.remove(at: indexPath.row) 143 | tableView.deleteRows(at: [indexPath], with: .automatic) 144 | default: 145 | break 146 | } 147 | } 148 | } 149 | 150 | ``` 151 | 152 | 注意,移动和删除操作必须在 tableView 进入编辑模式时才能进行操作。最简单的做法是直接在 viewDidLoad 里设置 tableView 的 isEditing 属性为 true。一般为了用户体验,我们会引入 navigationController,然后在导航栏的右上角添加 edit 按钮来让用户在普通和编辑模式中切换。 153 | 154 | ### 6\. UICollectionView 中的 Supplementary Views 和 Decoration Views 分别指什么? 155 | 156 | **关键词:#补充 #装饰** 157 | 158 | * **Cells,Supplementary Views,Decoration Views 共同构成了整个 UICollectionView 的视图。**Cells 是最基本的、必须用户自己实现并配置的。而 Supplementary Views 和 Decoration Views 有默认实现,主要是用来美化 UICollecctionView 的。 159 | 160 | * **Supplementary Views 是补充视图。**一般用来设置每个 Seciton 的 Header View 或者Footer View,用来标记 Section 的 View。 161 | 162 | * **Decoration Views 是装饰视图。**完全跟数据没有关系的视图,负责给 cell 或者 supplementary Views 添加辅助视图用的,例如给单个 section 或整个 UICollectionView 的背景(background)设置就属于 Decoration Views。 163 | 164 | * Supplementary Views 的布局一般可以在 UICollectionViewFlowLayout 中实现完成。如果要定制化实现 Supplementary Views 和 Decoration Views,那就要实现 UICollectionViewLayout 抽象类中。 165 | 166 | 下图是 Cells、Supplementary Views、Decoration Views 的说明: 167 | ![image](https://upload-images.jianshu.io/upload_images/22877992-2cece327916fba67.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 168 | 169 | ## 优化进阶 170 | 171 | ### 7.UITableViewCell如何根据其内容自动设置其布局? 172 | 173 | **关键词:#auto layout #UITableViewAutomaticDimension #estimatedRowHeight 174 | 175 | 主要有以下三步: 176 | 177 | 1. 用auto layout对UITableViewCell中所有子视图的位置和大小进行定义; 178 | 2. 将`rowHeight`设置为`UITableViewAutomaticDimension` 179 | 3. 给`estimatedRowHeight`赋值(随意值,不要太离谱即可) 180 | 181 | 示例代码: 182 | 183 | ``` 184 | tableView.rowHeight = UITableView.automaticDimension 185 | tableView.estimatedRowHeight = 600 186 | 187 | ``` 188 | 189 | ### 8.一个tableView滑动很慢,该怎样优化? 190 | 191 | **关键词:#渲染 #多线程 #网络传输** 192 | 193 | 拿到问题第一步要分析原因,列表视图滑动很慢,肯定是 UI 或是数据上出了问题,它们可能是: 194 | 195 | * 列表渲染时间较长。可能原因是某些 UI 控件比较复杂,或者图层过多。 196 | * 界面渲染延后。可能原因是大量的操作或耗时的计算阻塞主线程。 197 | * 数据源问题。可能原因是网络请求太慢,不能及时得到相应数据;也有可能是需要更新的数据太多,主线程一时处理不过来。 198 | 199 | 然后我们针对三个问题,分别去进行优化。 200 | 201 | 如果你正在跳槽或者正准备跳槽不妨动动小手,添加一下咱们的交流群[**931 542 608**](https://jq.qq.com/?_wv=1027&k=cfVxrMAH)来获取一份详细的大厂面试资料为你的跳槽多添一份保障。 202 | 203 | * 第一个问题。首先检查 UITableViewCell 是否进行了复用。对于复杂视图的创建,可以采用惰性加载来推迟创建时间。尽量减少视图层级也是很好的优化方法。Facebook 推出的 ComponentKit 就是很好的解决方案。 204 | 205 | * 第二个问题。可以用 GCD 多线程操作将复杂的计算放到后端线程,并进行缓存。例如布局计算或是非 UI 对象的创建和调整就可以如此操作。Linkedin 推出的 LayoutKit 就是很好的例子。 206 | 207 | * 第三个问题。建议将网络端数据缓存并存储在手机端,将取得部分数据根据优先级进行顺序渲染,还可以优化服务器端的实现来优化网络请求。 208 | 209 | * 另外对于界面渲染和优化其实 Facebook 和 Pinterest 维护的 ASDK 是目前为止功能最全、效果最好、使用最广的第三方解决方案。 210 | 211 | ### 9.说说实现预加载的方法 212 | 213 | **关键词:#网络传输 #无限滚动 #Threshold** 214 | 215 | 在实际开发中,列表经常需要随着滑动而不停的展示新的内容。在滑动到一定程度后,我们就需要发送网络请求,以获得新的数据。网络请求是一种耗时且昂贵的操作,为了提高用户体验,开发者经常运用预加载的方式提前请求,这样可以在用户滑动到列表最底部之前提前获得最新数据,无需让用户等待。这就是无限滚动列表。 216 | 217 | 预加载的原理就是,根据当前 UITableView 所在位置,除以目前整个 contentView 的高度,来判断当前位置是否超过 Threshold,如果超过,就发起网络请求,获得数据。以下是示范代码: 218 | 219 | ``` 220 | override func scrollViewDidScroll(_ scrollView: UIScrollView) { 221 | let current = scrollView.contentOffset.y + scrollView.frame.size.height 222 | let total = scrollView.contentSize.height 223 | let ratio = current / total 224 | 225 | if ratio >= threshold { 226 | requestNewPage() 227 | } 228 | } 229 | 230 | ``` 231 | 232 | 以上就是一种最简单的预加载方法。它的缺点十分明显,就是当列表很长时,会出现新加载的页面还没看,应用就会发出另一次请求的情况。 233 | 234 | 举个例子,假设 Threshold 是 0.7,每个屏幕展示 10 个 cell,每次加载 10 个 cell 的数据,当浏览到第 28 个 cell 时,由于会加载第 40 到第 50 个 cell 的数据,可是我们之前加载的第 30 到第 40 个 cell 的数据还没有被访问。此时发出网络请求,就造成了浪费。 235 | 236 | 解决方法是将 Threshold 变成一个动态的值,随着数据的增长而增长。示范代码如下: 237 | 238 | ``` 239 | override func scrollViewDidScroll(_ scrollView: UIScrollView) { 240 | let current = scrollView.contentOffset.y + scrollView.frame.size.height 241 | let total = scrollView.contentSize.height 242 | let ratio = current / total 243 | 244 | let needRead = cellsPerPage * threshold + currentPage * cellsPerPage 245 | let totalCells = cellsPerPage * (currentPage + 1) 246 | let newThreshold = needRead / totalCells 247 | 248 | if ratio >= newThreshold { 249 | currentPage += 1 250 | requestNewPage() 251 | } 252 | } 253 | 254 | ``` 255 | 256 | 当然我们还可以进一步优化。例如用惰性加载只处理用户想看到的内容,或是用 ASDK 进行智能预加载。这样可以进一步提高用户体验,并使整个滑动的性能效率最大化。 257 | 258 | ### 10.如何用 UICollectionView 实现瀑布流界面? 259 | 260 | **关键词:#UICollectionViewLayout** 261 | 262 | 面试中当场实现一个瀑布流,在不允许上网查询的情况下算是十分困难的了。而且代码量很大,所以我们这道题重在分析思路。假设我们已经有了 UICollectionView,现在要做的就是定制化每一个 cell,让他们的高度根据其实际内容设定,从而实现瀑布流。 263 | 264 | 我们知道要定制化 UICollectionView 的 layout 就一定要使用 UICollectionViewLayout。所以我们首先要做的就是创建一个该抽象类的子类,然后将其设定为当前 UICollectionView 的 Layout。 265 | 266 | 之后我们需要设计我们的 UICollectionViewLayout 子类,有 4 样东西我们需要一一设定: 267 | 268 | * collectionViewContentSize。由于瀑布流导致的尺寸变化我们重写 contentSize。其中宽度一般情况我们是可以确定的,它取决于每个item的宽度,一行几个 item,以及 contentInset 值。高度我们可以先设定为 0,之后在 prepare() 里进行更新。 269 | 270 | * prepare()。该方法发生在 UICollectionView 数据准备好,但界面还未布局之时。它用于计算各种布局信息,并设定每个 item 的相关属性。这里我们用横纵坐标轴分别进行计算每个 cell 的 xOffset 和 yOffset,然后将其转化为相应的 frame 并缓存起来。 271 | 272 | * layoutAttributesForElements(in:)。prepare() 完成布局之后该方法被调用,它决定了哪些 item 在 CollectionView 给定的区域内可见。我们只要取交集(intersect)即可。 273 | 274 | * layoutAttributesForItem(at:)。该方法需要我们针对每一个 item 设定 layoutAttribute。由于我们在 prepare() 中已经完成相应计算,此时只需返回对应 indexPath 的特定属性即可。 275 | 276 | 完成这些设定之后,我们发现 UICollectionView 里每个 item 里的高度需要从含有 UICollectionView 的 ViewController 里获得。为了避免循环引用,最好的方法就是在我们的 UICollectionViewLayout 子类中定义一个 protocol,然后让 ViewController 实现这个protocol,来完成高度的获得。Delelgate 这种模式的运用让整个设计的扩展度和灵活度变高。 277 | 278 | 至此我们就完成了 UICollectionView 实现瀑布流的全过程。以上只是一种比较直接的实现,最复杂的部分在于 prepare() 中运用 xOffset 和 yOffset 构建 LayoutAttributes 的过程,其中含有大量的数学计算。网上对于瀑布流有很多实现,大家不妨借鉴的同时,亲自动手,以加深对 UICollectionView 的理解。 279 | # 推荐👇: 280 | 281 | 如果你正在跳槽或者正准备跳槽不妨动动小手,添加一下咱们的交流群[**931 542 608**](https://jq.qq.com/?_wv=1027&k=cfVxrMAH)来获取一份详细的大厂面试资料为你的跳槽多添一份保障。 282 | 283 | ![](https://upload-images.jianshu.io/upload_images/22877992-0bfc037cc50cae7d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 284 | -------------------------------------------------------------------------------- /系统框架-UIKit.md: -------------------------------------------------------------------------------- 1 | 本章节主要从视图、网络、设计模式几个方面考察开发者的开发水准,这是任何一个合格的 iOS 开发者都应该具备的基本素养。 2 | ![](https://upload-images.jianshu.io/upload_images/22877992-62362c3afb69b44f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 3 | iOS 开发中最重要的 API 就是 UIKit。它是苹果官方提供的管理界面和交互的最基本的 API。UIKit 被用在所有的 iPhone 和 iPad 开发中,它涵盖的内容包括触摸和交互处理、视图布局、图形绘制中。可以说 UIKit 相关知识点的考察是所有面试中最基本、最必不可少、最重要的一环。 4 | 5 | 本节将从用户界面聊起,回答开发中常见的布局和交互问题;之后将重点集中在动画渲染上,最后的问答题将集中在 iPad 的多屏开发上。对于 iOS 11 中最新的 drag and drop 和安全区域亦有涉及。 6 | 7 | ## UI 控件和基本布局 8 | 9 | ### 1.要在 UIView 上定义一个 Label有 哪几种方式? 10 | 11 | **关键词:#storyboard #xib #Frame #Auto Layout** 12 | 13 | 这道题本身问法十分模糊。定义一个 Label,指的是创建一个,还是说给它做相应的布局,亦或是设置它的属性值?这都是要和面试官进行进一步沟通确定的。 14 | 15 | 假如我们要从零创建一个 label,配置它在页面上的布局,并设置属性值,有以下几种方式。 16 | 17 | * 用 storyboard 或 xib 完成。直接在库面板中拖拽一个 label 完成创建,然后设置相应的 constraint 进行布局,最后在属性检查器面板对相应属性进行设置。这是苹果推荐的做法。 18 | 19 | * 用纯代码的方式来做。在 ViewController 中新建一个 label,然后用 frame 或是 auto layout(可以用 anchor 或 NSLayoutConstraint )来布局,最后再一个个属性进行手动设置。 20 | 21 | ### 2.storyboard/xib,和纯代码构建 UI 相比,有什么优缺点? 22 | 23 | **关键词:#可视化 #多人协作 #性能** 24 | 25 | storyboard/xib 的开发方式优点和缺点都十分明显。优点是: 26 | 27 | * **简单直接。**直接拖拽和点选即可配置 UI,界面所见即所得。 28 | 29 | * **跳转关系清楚。**Storyboards 中可以清楚的区分 View Controller 界面之间的跳转关系。而且在代码中,通过实现 prepare(for segue: UIStoryboardSegue, sender: Any?),我们可以统一管理界面跳转和数据管理。 30 | 31 | 缺点是: 32 | 33 | * **协作冲突。**多人编辑时很容易产生冲突,且冲突很难解决。因为自带 Xcode 和系统的版本号,协作时 storyboard/xib 会在相同位置做同样修改,这样代码冲突几乎是不可避免的。解决方法是细分 storyboard 以及对应工程师的职责,但是这样同样带来了维护成本。 34 | 35 | * **很难做到界面继承和重用。**代码中实现要容易和明确得多,然而 storyboard/xib 却很难做到。 36 | 37 | * **不便于进行模块化管理。**storyboard/xib 中搜索起来很不方便,且统一修改多个 UI 控件的属性值不可能,必须一个一个改。在代码中一个工厂模式就可以搞定。 38 | 39 | * **性能影响。**storyboard/xib 在界面渲染上有时会成为性能杀手。例如首页 UI 构造时,代码书写和优化就会比 storyboard 多图层的渲染要好很多。 40 | 41 | ### 3.Auto Layout 和 Frame 在 UI 布局和渲染上有什么区别? 42 | 43 | **关键词: #性能** 44 | 45 | * **Auto Layout 是针对多尺寸屏幕的设计。**其本质是通过线性不等式对 UI 控件的相对位置进行设定,从而适配多种 iPhone/iPad 屏幕的尺寸。 46 | 47 | * **Frame 是基于 xy 坐标轴系统的布局机制。**它从数学上限定了 UI 控件的具体位置,是 iOS 开发中最底层、最基本的界面布局机制。 48 | 49 | * **Auto Layout 的性能比 Frame 差很多。**Auto Layout 的布局过程首先求解线性不等式,然后再转化为 Frame 去进行布局。其中求解的计算量非常大,通常 Auto Layout 的性能损耗是 Frame 布局的 10 倍左右。 50 | 51 | **加分回答:** 52 | 53 | 解决方法是尽量压缩视图层级减少计算量;同时 Layout 的计算也可以通过后台线程来处理,这样就可以不阻塞主线程操作。计算结果亦可以缓存起来,加速之后界面布局渲染。成熟的解决方案有 Facebook 的 ComponentKit,Pinterest 的 Texture(前身是 ASDK ),以及 LinkdedIn 的 LayoutKit。 54 | 55 | ### 4.UIView 和 CALayer 有什么区别? 56 | 57 | **关键词: #性能 #交互** 58 | 59 | * **UIView 和 CALayer 都是 UI 操作的对象。**两者都是 NSObject 的子类,发生在 UIView 上的操作本质上也发生在对应的 CALayer 上。 60 | 61 | * **UIView 是 CALayer 用于交互的抽象。**UIView 是 UIResponder 的子类( UIResponder 是 NSObject 的子类),提供了很多 CALayer 所没有的交互上的接口,主要负责处理用户触发的种种操作。 62 | 63 | * **CALayer 在图像和动画渲染上性能更好。**这是因为 UIView 有冗余的交互接口,而且相比 CALayer 还有层级之分。CALayer 在无需处理交互时进行渲染可以节省大量时间。 64 | 65 | ### 5.请说明并比较以下关键词:Frame, Bounds, Center 66 | 67 | **关键词: #坐标 #父视图** 68 | 69 | * Frame 是指当前视图(View)相对于父视图的平面坐标系统中的位置和大小。 70 | 71 | * Bounds 是指当前视图相对于自己的平面坐标系统中的位置和大小。 72 | 73 | * Center 是一个 CGPoint,指当前视图在父视图的平面坐标系统中最中间位置点 。 74 | 75 | **加分回答:** 76 | 77 | 介绍完上述概念后,下面用画图的方式来讲解这三个词的区别。如下图: 78 | 79 | ![image](https://upload-images.jianshu.io/upload_images/22877992-fd7efc8696625243.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 80 | 81 | 其中 View B 左上角的点的frame 值是(200, 100),bounds 值是(0, 0),center 所对应点的值是 82 | (275, 200)。 83 | 84 | ### 6.请说明并比较以下方法:layoutIfNeeded, layoutSubviews, setNeedsLayout 85 | 86 | **关键词: #布局 #周期** 87 | 88 | * layoutIfNeeded 方法一旦调用,主线程会立即强制重新布局,它从当前视图开始,一直到完成所有子视图的布局。 89 | 90 | * layoutSubviews 是用来自定义视图尺寸调整的。它是系统自动调用的,开发者不能手动调用。我们能做的就是重写该方法,让系统在尺寸调整时能按照希望的效果去进行布局。这个方法主要在屏幕旋转、滑动或触摸界面、子视图修改时被触发。 91 | 92 | * setNeedsLayout 与 layoutIfNeeded 相似,唯一不同的就是它不会立刻强制视图重新布局,而是在下一个布局周期才会触发更新。它主要用在多个 view 布局先后更新的场景下。例如我们要在两个布局不停变化的点之间连一条线,这个线的布局就可以调用 setNeedsLayout 方法。 93 | 94 | ### 7.请说明并比较以下关键词:Safe Area, SafeAreaLayoutGuide, SafeAreaInsets 95 | 96 | **关键词: #安全区域** 97 | 98 | 由于 iPhone X 全新的刘海设计,iOS 11 中引入了安全区域(Safe Area)这套概念。如下图: 99 | 100 | ![image](https://upload-images.jianshu.io/upload_images/22877992-3ad7ff38b7efd9ad.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 101 | 102 | * **Safe Area 是指应用合理显示内容的区域。**它不包括 status bar, navigation bar, tab bar , tool bar 等。iPhone X 中一般是指扣除了顶部的 status bar(高度为20)、navigation bar(高度为44)和底部的 home indicator 区域(高度为34),这样应用的内容不会被刘海挡住或是影响底部手势操作。 103 | 104 | * **SafeAreaLayoutGuide 是指 SafeArea 的区域范围和限制 。**在布局设置中,我们可以分别取得它的上下左右 4 个边界的位置进行相应布局处理。 105 | 106 | * **SafeAreaInsets 限定了 SafeArea 区域与整个屏幕之间的布局关系。**一般我们用上下左右 4 个值来获取 SafeArea 与屏幕边缘之间的距离。 107 | 108 | ## 动画 109 | 110 | ### 8.iOS 中实现动画的方式有几种? 111 | 112 | **关键词: #UIViewPropertyAnimator #UIView Animation #CALayer Animation** 113 | 114 | 最主要的实现动画方式有 3 种,UIView Animation、CALayer Animation、UIViewPropertyAnimator。 115 | 116 | * **UIView Animation 可以实现基于 UIView 的简单动画。**它是 CALayer Animation 的封装,主要可以实现移动、旋转、缩放、变色等基本操作。其基本函数为`+ animateWithDuration:animations:`,其中持续时间(duration)为基本参数,block 中对 UIView 属性的调整就是动画结束后的最终效果。除此之外他还有关键帧动画和两个 view 转化等接口。它实现的动画无法回撤、暂停、与手势交互。 117 | 118 | * **CALayer Animation 是更在底层 CALayer 上的动画接口。**除了 UIView Animation 可以实现的效果。它可以修改更多的属性以实现各种复杂的动画效果。其实现的动画可以回撤、暂停、与手势交互。 119 | 120 | * **UIViewPropertyAnimator 是 iOS 10 引进的处理交互式动画的接口。**它也是基于 UIView 实现,可以实现所有的 UIView Animation 效果。它最大的优点在于 timing function 以及与手势配合的交互式动画设置相比 CALayer Animation 十分简便,可以说是为交互而生的动画接口。 121 | 122 | ### 9.代码实现:控制屏幕上的圆形小球,使其水平向右滑动 200 个 point。 123 | 124 | **关键词: #UIViewPropertyAnimator #交互式动画** 125 | 126 | 这道题很明显是要求实现动画。然而,题目中对于动画的各种参数(持续时间,延时,速度控制等)都没有要求。我们在做这道题目的时候一定要就相关细节向面试官询问清楚,切忌上来就写——实际开发中最怕用户需求都不明白就写代码,最终也只会是南辕北辙。 127 | 128 | 假设圆形小球已经在屏幕上,面试官没有参数要求,只是要实现水平移动的效果。那么实现代码如下: 129 | 130 | ``` 131 | // UIViewPropertyAnimator实现 132 | let animator = UIViewPropertyAnimator(duration: 2, curve: .linear) { 133 | circle.frame = circle.frame.offsetBy(dx: 200, dy: 0) 134 | } 135 | animator.startAnimation() 136 | 137 | // UIView Animation实现 138 | UIView.animate(withDuration: 2) { 139 | circle.frame = circle.frame.offsetBy(dx: 200, dy: 0) 140 | } 141 | 142 | // CALayer实现 143 | let animation = CABasicAnimation.init(keyPath: "position.x") 144 | animation.fromValue = circle.center.x 145 | animation.toValue = circle.center.x + 200 146 | animation.duration = 2 147 | self.circle.layer.add(animation, forKey: nil) 148 | 149 | ``` 150 | 151 | **追问:假如需要根据手势来控制小球的水平移动,该怎么操作?** 152 | 153 | 这次考察的是交互式动画,那么交互式动画用 UIViewPropertyAnimator 来做最为方便。关于手势具体如何控制球的移动,请向面试官询问。我们假设面试官给出如下要求: 154 | 155 | * 一开始小球静止,除非用户触摸屏幕,否则小球不动 156 | * 按住屏幕并左右滑动,此时小球随手势线性左右滑动 157 | * 松开手,小球从当前位置滑动到水平初始距离向右 200 points 处,整个移动过程是先快后慢的效果 158 | * 当再次触摸屏幕时,如果小球未滑动到终点,则小球将暂停滑动,再次随手势线性滑动 159 | * 当到达终点后,无论用户如何触摸屏幕,小球在终点静止不动 160 | 161 | 从上述要求中我们知道:timing function 是 ease out,开始时暂停动画。随着手势的移动,我们记录动画的完成度 fractionComplete。当手势释放时,我们继续动画,让其自动完成。注意手势操控动画进行交互的时候,Animator 会自动将 timing function 从 ease out 转为 linear。代码如下: 162 | 163 | ``` 164 | var progress: CGFloat = 0 165 | var animator: UIViewPropertyAnimator! 166 | 167 | override func viewDidLoad() { 168 | let gesture = UIPanGestureRecognizer(target: self, action: Selector.handlePan) 169 | view.addGestureRecognizer(gesture) 170 | 171 | animator = UIViewPropertyAnimator.init(duration: 2, curve: .easeOut, animations: { 172 | self.circle.frame = self.circle.frame.offsetBy(dx: 200, dy: 0) 173 | }) 174 | } 175 | 176 | func handlePan(recognizer:UIPanGestureRecognizer) { 177 | switch recognizer.state { 178 | case .began: 179 | animator.pauseAnimation() 180 | progress = animator.fractionComplete 181 | case .changed: 182 | let translation = recognizer.translation(in: self.circle) 183 | animator.fractionComplete = translation.x / 200 + progress 184 | case .ended: 185 | animator.continueAnimation(withTimingParameters: nil, durationFactor: 0) 186 | default: 187 | break 188 | } 189 | 190 | ``` 191 | 192 | ## 多任务开发 193 | 194 | ### 10.iOS 开发中,如何保证应用的 UI 在 iPhone、iPad 以及 iPad 分屏情况下依然适用? 195 | 196 | **关键词:#Adaptive UI #Size Class #Auto Layout** 197 | 198 | 为了针对各种机型,苹果在 iOS 8 中引入了 Adaptive UI 的概念。所以要保证应用的 UI 在各种情况下依然良好,主要注意以下几个点: 199 | 200 | * **采用 Auto Layout。**与 frame 设置绝对位置不同,所有的 UI 控件将保持相对位置。例如将 label 设置成对应屏幕 center X, center Y,此时无论是 iPhone 还是 iPad,此 label 都将相对于屏幕居中。 201 | 202 | * **采用 Size Class。**很多时候 UI 控件可能在 iPhone 上大小刚好,但在 iPad 上可能偏小,位置也有可能有偏移。此时用 Size Class,可以分别在不同的机型上进行安装/卸载对应的 constraint,并且可以方便的进行预览。苹果将自家设备按照横纵两个尺寸进行区别,不同的情况对应的 Regular 和Compact 组合。比如竖屏的 iPhone 宽度是 Compact,高度是 Regular。具体分类请看下图: 203 | 204 | ![image](https://upload-images.jianshu.io/upload_images/22877992-b738667fdea3506f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 205 | 206 | * **关注多屏情况。**iPad 上引进的多屏情况主要分三种:Slide Over,Split View,Picture in Picture。苹果明确指出应用应该支持 Slide Over 和 Split View。这时候作为工程师,应该多多与设计师交流针对这两种情况的 UI 设计,并配合 Size Class 进行分类适配。下图详尽说明了 iPad 上多任务的尺寸分类: 207 | 208 | ![image](https://upload-images.jianshu.io/upload_images/22877992-6816b11c3ecf4fac.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 209 | 210 | ### 11.代码实现:将 UIImageView 上的图片直接拖拽到另一个 UIImageView 上。 211 | 212 | **关键词:#Drag and Drop** 213 | 214 | 这道题考察的是 iOS 11 最新引入的 Drag and Drop 功能。跟很多面试题一样,它没有说明起始和终止的 UIImageView 是否在一个应用之内。如果在同一个应用之内,那么无论是 iPhone 还是 iPad 都能实现这样的功能;如果是把图片从一个应用拖拽到另一个应用之上,那么只能是 iPad 实现。 215 | 216 | 我们假设面试官考察的是在同一个应用中,将一张图片从一个 UIImageView 中拖拽到另一个 UIImageView 。 217 | 218 | Drag and Drop 一般实现起来分3步: 219 | 220 | **1\. 对相应的 UIImageView 分别添加 drag 和 drop delegate** 221 | 222 | ``` 223 | dragImageView.addInteraction(UIDragInteraction(delegate: self)) 224 | dropImageView.addInteraction(UIDropInteraction(delegate: self)) 225 | 226 | ``` 227 | 228 | 注意,dragImageView 和 dropImageView 的 userInteractionEnabled 必须是 true。 229 | 230 | **2.实现 drag delegate 规定的方法** 231 | 232 | ``` 233 | extension ViewController: UIDragInteractionDelegate { 234 | func dragInteraction(_ interaction: UIDragInteraction, itemsForBeginning session: UIDragSession) -> [UIDragItem] { 235 | if interaction.view == dragImageView { 236 | let dragImage = dragImageView.image 237 | let itemProvider = NSItemProvider(object: dragImage!) 238 | let dragItem = UIDragItem(itemProvider: itemProvider) 239 | return [dragItem] 240 | } else { 241 | return [] 242 | } 243 | } 244 | } 245 | 246 | ``` 247 | 248 | 这个方法的功能就是告诉系统,我们要拖动的对象。 249 | 250 | 方法里面的 NSItemProvider 简单来说就是用来在 Drag and Drop,或者 Extension app 和 Host app 之间传输数据的类。 251 | 252 | UIDragItem 则是像对 NSItemProvider 的进一步封装,除了包含传输数据外,还可以自定义一些数据。 253 | 254 | 实现完该方法后,图片就可以从 dragImageView 里拖动出来了。 255 | 256 | **3.实现 drop delegate 对应的方法** 257 | 258 | 一般来讲,需要实现 3 个方法: 259 | 260 | ``` 261 | // 询问是否可以处理 drag 的数据,默认是 true,所以并不一定要实现 262 | func dropInteraction(_ interaction: UIDropInteraction, canHandle session: UIDropSession) -> Bool 263 | 264 | // 询问系统当 drop 之时,以何种方式处理 drag 的数据 265 | // UIDropProposal对应的操作是 cancel, forbidden, copy, move 266 | func dropInteraction(_ interaction: UIDropInteraction, sessionDidUpdate session: UIDropSession) -> UIDropProposal 267 | 268 | // drop 已经发生,取出 drag 中的数据并进行处理 269 | func dropInteraction(_ interaction: UIDropInteraction, performDrop session: UIDropSession) 270 | 271 | ``` 272 | 273 | 具体的代码如下: 274 | 275 | ``` 276 | func dropInteraction(_ interaction: UIDropInteraction, sessionDidUpdate session: UIDropSession) -> UIDropProposal { 277 | let dropLocation = session.location(in: view) 278 | let operation: UIDropOperation 279 | 280 | if dropImageView.frame.contains(dropLocation) { 281 | operation = .copy 282 | } else { 283 | operation = .cancel 284 | } 285 | return UIDropProposal(operation: operation) 286 | } 287 | 288 | func dropInteraction(_ interaction: UIDropInteraction, performDrop session: UIDropSession) { 289 | session.loadObjects(ofClass: UIImage.self) { [weak self] (imageItems) in 290 | self?.dragImageView.image = nil 291 | self?.dropImageView.image = imageItems.first as? UIImage 292 | } 293 | 294 | ``` 295 | 296 | 以上是最简单直接的实现方法。Drag and Drop 中还有很多可以定制的方法和属性,例如支持多点触摸的 preview 方法。工作中,你可能只需要实现 drag 功能,也可能只需要支持 drop 功能。 297 | 298 | # 推荐👇: 299 | 300 | 如果你正在跳槽或者正准备跳槽不妨动动小手,添加一下咱们的交流群[**931 542 608**](https://jq.qq.com/?_wv=1027&k=cfVxrMAH)来获取一份详细的大厂面试资料为你的跳槽多添一份保障。 301 | 302 | ![](https://upload-images.jianshu.io/upload_images/22877992-0bfc037cc50cae7d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 303 | -------------------------------------------------------------------------------- /算法基础6-7节.md: -------------------------------------------------------------------------------- 1 | # 6. 深度优先和广度优先 2 | 之前介绍了最简单的搜索法:二分搜索。虽然它的算法复杂度非常低只有 O(logn),但使用起来也有局限:只有在输入是排序的情况下才能使用。这次讲解两个更复杂的搜索算法: 3 | 4 | ![](https://upload-images.jianshu.io/upload_images/22877992-fd3b752f889d6d2c.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 5 | 6 | 7 | * **深度优先搜索(Depth-First-Search,以下简称DFS)** 8 | * **广度优先搜索(Breadth-First-Search,以下简称BFS)** 9 | 10 | ## 基本概念 11 | 12 | DFS 和 BFS 的具体定义这里不做赘述。笔者谈谈自己对此的形象理解:假如你在家中发现钥匙不见了,为了找到钥匙,你有两种选择: 13 | 14 | 1. 从当前角落开始,顺着一个方向不停的找。假如这个方向全部搜索完毕依然没有找到钥匙,就回到起始角落,从另一个方向寻找,直到找到钥匙或所有方向都搜索完毕为止。这种方法就是 DFS。 15 | 2. 从当前角落开始,每次把最近所有方向的角落全部搜索一遍,直到找到钥匙或所有方向都搜索完毕为止。这种方法就是 BFS。 16 | 17 | 我们假设共有 10 个角落,起始角落为 1,它的周围有 4 个方向,如下图: 18 | 19 | ![](https://upload-images.jianshu.io/upload_images/22877992-5dd1340a0ae7677b.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 20 | 21 | DFS 的搜索步骤为: 22 | 23 | * 1 24 | * 2 -> 3 -> 4 25 | * 5 26 | * 6 ->7 -> 8 27 | * 9 -> 10 28 | 29 | 即每次把**一个方向彻底搜索完全**后,才返回搜索下一个方向。 30 | 31 | BFS 的搜索步骤为: 32 | 33 | * 1 34 | * 2 -> 5 -> 6 -> 9 35 | * 3 -> 4 36 | * 7 37 | * 10 38 | * 8 39 | 40 | 即每次访问**上一步周围所有方向上的角落**。 41 | 42 | 细心的朋友会记得,我之前在讲二叉树的时候,讲到了**前序遍历**和**层级遍历**,而这两者本质上就是 DFS 和 BFS。 43 | 44 | DFS 的 Swift 实现: 45 | 46 | ``` 47 | func dfs(_ root: Node?) { 48 | guard let root = root else { 49 | return 50 | } 51 | 52 | visit(root) 53 | root.visited = true 54 | 55 | for node in root.neighbors { 56 | if !node.visited { 57 | dfs(node) 58 | } 59 | } 60 | 61 | } 62 | 63 | ``` 64 | 65 | BFS 的 Swift 实现: 66 | 67 | ``` 68 | func bfs(_ root: Node?) { 69 | var queue = [Node]() 70 | 71 | if let root = root { 72 | queue.append(root) 73 | } 74 | 75 | while !queue.isEmpty { 76 | let current = queue.removeFirst() 77 | 78 | visit(current) 79 | current.visited = true 80 | 81 | for node in current.neighbors { 82 | if !node.visited { 83 | queue.append(node) 84 | } 85 | } 86 | } 87 | } 88 | 89 | ``` 90 | 91 | 永远记住:**DFS 的实现用递归,BFS 的实现用循环(配合队列)**。 92 | 93 | ## iOS 实战演练 94 | 95 | 硅谷面试 iOS 工程师,有这样一个环节,给你 1 ~ 1.5 小时,从头开始实现一个小 App。我们来看这样一个题目: 96 | 97 | > 实现一个找单词 App : 给定一个初始的字母矩阵,你可以从任一字母开始,上下左右,任意方向、任意长度,选出其中所有单词。 98 | 99 | ![](https://upload-images.jianshu.io/upload_images/22877992-33c8d50ab4e53e04.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 100 | 101 | 很多人拿到这道题目就懵了,完全不是我们熟悉的 UITableView 或者 UICollectionView 啊,这要咋整。我们来一步步分析。 102 | 103 | **第一步:实现字母矩阵** 104 | 105 | 首先,我们肯定有个字符二阶矩阵作为输入,姑且记做:`matrix: [[Character]]`。现在要把它展现在手机上,那么可行的方法,就是创建一个 UILabel 二维矩阵,记做 `labels: [[UILabel]]`,矩阵中每一个 UILabel 对应的内容就是相应的字母。同时,我们可以维护 2 个全局变量,xOffset 和 yOffset。然后在for循环中创建相应的 UILabel 同时将其添加进 lables 中便于以后使用,代码如下: 106 | 107 | ``` 108 | var xOffset = 0 109 | var yOffset = 0 110 | let cellWidth = UIScreen.mainScreen().bounds.size.width / matrix[0].count 111 | let cellHeight = UIScreen.mainScreen().bounds.size.height / matrix.count 112 | 113 | for i in 0 ..< matrix.count { 114 | for j in 0 ..< matrix[0].count { 115 | let label = UILabel(frame: CGRect(x: xOffset, y: yOffset, width: cellWidth, height: cellHeight)) 116 | label.text = String(matrix[i][j]) 117 | view.addSubView(label) 118 | labels[i][j] = label 119 | xOffset += cellWidth 120 | } 121 | xOffset = 0 122 | yOffset += cellHeight 123 | } 124 | 125 | ``` 126 | 127 | **第二步:用 DFS 实现搜索单词** 128 | 129 | 现在要实现搜索单词的核心算法了。我们先简化要求,假如只在字母矩阵中搜索单词 "crowd" 该怎么做? 130 | 首先我们要找到 "c" 这个字母所在的位置,然后再上下左右找第二个字母 "r" ,接着再找字母 "o" ······ 以此类推,直到找到最后一个字母 "d" 。如果没有找到相应的字母,我们就回头去首字母 "c" 所在的另一个位置,重新搜索。 131 | 132 | 这里要注意一个细节,就是我们不能回头搜索字母。比如我们已经从 "c" 开始向上走搜索到了 "r" ,这个时候就不能从 "r" 向下回头 -- 因为 "c" 已经访问过了。所以这里需要一个 var visited: [[Bool]] 来记录访问记录。代码如下: 133 | 134 | ``` 135 | func searchWord(_ board: [[Character]]) -> Bool { 136 | guard board.count > 0 && board[0].count > 0 else { 137 | return false 138 | } 139 | 140 | let (m, n) = (board.count, board[0].count) 141 | var visited = Array(repeating: Array(repeating: false, count: n), count: m) 142 | 143 | for i in 0 ..< m { 144 | for j in 0 ..< n { 145 | if board[i][j] == "c" && dfs(board, Array("crowd"), m, n, i, j, &visited, 0) { 146 | return true 147 | } 148 | } 149 | } 150 | 151 | return false 152 | } 153 | 154 | func dfs(_ board: [[Character]], _ wordContent: [Character], _ m: Int, _ n: Int, _ i: Int, _ j: Int, _ visited: inout [[Bool]], _ index: Int) -> Bool { 155 | if index == wordContent.count { 156 | return true 157 | } 158 | 159 | guard i >= 0 && i < m && j >= 0 && j < n else { 160 | return false 161 | } 162 | guard !visited[i][j] && board[i][j] == wordContent[index] else { 163 | return false 164 | } 165 | 166 | visited[i][j] = true 167 | 168 | if dfs(board, wordContent, m, n, i + 1, j, &visited, index + 1) || dfs(board, wordContent, m, n, i - 1, j, &visited, index + 1) || dfs(board, wordContent, m, n, i, j + 1, &visited, index + 1) || dfs(board, wordContent, m, n, i, j - 1, &visited, index + 1) { 169 | return true 170 | } 171 | 172 | visited[i][j] = false 173 | return false 174 | } 175 | 176 | ``` 177 | 178 | **第三步:优化算法,进阶** 179 | 180 | 好了现在我们已经知道了怎么搜索一个单词了,那么多个单词怎么搜索?首先题目是要求找出所有的单词,那么肯定事先有个字典,根据这个字典,我们可以知道所选字母是不是可以构成一个单词。所以题目就变成了: 181 | 182 | 已知一个字母构成的二维矩阵,并给定一个字典。选出二维矩阵中所有横向或者纵向的单词。 183 | 也就是实现以下函数: 184 | 185 | ``` 186 | func findWords(_ board: [[Character]], _ dict: Set) -> [String] {} 187 | 188 | ``` 189 | 190 | 我们刚才已经知道如何在矩阵中搜索一个单词了。所以最暴力的做法,就是在矩阵中,搜索所有字典中的单词,如果存在就添加在输出中。 191 | 192 | 这个做法显然复杂度极高:首先,每次 DFS 的复杂度就是 O(n2 )。字母矩阵越大,搜索时间就越长;其次,字典可能会非常大,如果每个单词都搜索一遍,开销太大。这种做法的总复杂度为 O(m· n2),其中m为字典中单词的数量,n 为矩阵的边长。 193 | 194 | 这时就要引入 Trie 树(前缀树) 。前缀树是一种有序树,用于保存关联数组,特别适用于保存字符串。就这道题目而言,首先我们把字典转化为前缀树,这样的好处在于它可以检测矩阵中字母构成的前缀是不是一个单词的前缀,如果不是就没必要继续 DFS 下去了。这样我们就把搜索字典中的每一个单词,转化为了只搜字母矩阵。代码如下: 195 | 196 | ``` 197 | func findWords(_ board: [[Character]], _ dict: Set) -> [String] { 198 | var res = [String]() 199 | 200 | let (m, n) = (board.count, board[0].count) 201 | 202 | let trie = _convertSetToTrie(dict) 203 | var visited = Array(repeating: Array(repeating: false, count: n), count: m) 204 | 205 | for i in 0 ..< m { 206 | for j in 0 ..< n { 207 | _dfs(board, m, n, i, j, &visited, &res, trie, "") 208 | } 209 | } 210 | 211 | return res 212 | } 213 | 214 | private func _dfs(_ board: [[Character]], _ m: Int, _ n: Int, _ i: Int, _ j: Int, inout _ visited: [[Bool]], inout _ res: [String], _ trie: Trie, _ str: String) { 215 | // 越界 216 | guard i >= 0 && i < m && j >= 0 && j < n else { 217 | return 218 | } 219 | 220 | // 已经访问过了 221 | guard !visited[i][j] else { 222 | return 223 | } 224 | 225 | // 搜索目前字母组合是否是单词前缀 226 | var str = str + "\(board[i][j])" 227 | guard trie.prefixWith(str) else { 228 | return 229 | } 230 | 231 | // 确认当前字母组合是否为单词 232 | if trie.isWord(str) && !res.contains(str) { 233 | res.append(str) 234 | } 235 | 236 | // 继续搜索上下左右四个方向 237 | visited[i][j] = true 238 | _dfs(board, m, n, i + 1, j, &visited, &res, trie, str) 239 | _dfs(board, m, n, i - 1, j, &visited, &res, trie, str) 240 | _dfs(board, m, n, i, j + 1, &visited, &res, trie, str) 241 | _dfs(board, m, n, i, j - 1, &visited, &res, trie, str) 242 | visited[i][j] = false 243 | } 244 | 245 | ``` 246 | 247 | 这里对 Trie 不做深入展开,有兴趣的朋友自行研究。 248 | 249 | ## 总结 250 | 251 | 深度优先遍历和广度优先遍历是算法中略微高阶的部分,实际开发中,它也多与地图路径、棋盘游戏相关。虽然不是很常见,但是理解其基本原理并能熟练运用,相信可以使大家的开发功力更上一层楼。 252 | 253 | # 7. 动态规划 254 | 之前的章节中,分析的问题大多比较具体直接 —— 可以直接套用一种方法解决。今天要讲的动态规划,其面对的问题通常是无法一蹴而就,需要把复杂的问题分解成简单具体的小问题,然后通过求解简单问题,去推出复杂问题的最终解。 255 | ![](https://upload-images.jianshu.io/upload_images/22877992-b0aeff2081334787.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 256 | 257 | 形象的理解就是为了推倒一系列纸牌中的第 100 张纸牌,那么我们就要先推倒第 1 张,再依靠多米诺骨牌效应,去推倒第 100 张。 258 | 259 | ## 实例讲解 260 | 261 | > 斐波拉契数列是这样一个数列:1, 1, 2, 3, 5, 8, ... 除了第一个和第二个数字为 1 以外,其他数字都为之前两个数字之和。现在要求第 100 个数字是多少。 262 | 263 | 这道题目乍一看是一个数学题,那么要求第 100 个数字,很简单,一个个数字算下去就是了。假设 F(n) 表示第 n 个斐波拉契数列的数字,那么我们易得公式 F(n) = F(n - 1) + F(n - 2),n >= 2 ,下面就是体力活。当然这道题转化成代码也不是很难,最粗暴的解法如下: 264 | 265 | ``` 266 | // 此方法会因为栈溢出而崩溃 267 | func Fib() -> Int { 268 | var (prev, curr) = (0, 1) 269 | 270 | for _ in 1 ..< 100 { 271 | (curr, prev) = (curr + prev, curr) 272 | } 273 | 274 | return curr 275 | } 276 | 277 | ``` 278 | 279 | 用动态规划怎么写呢?首先要明白动态规划有以下几个专有名词: 280 | 281 | **1)初始状态**,即此问题的最简单子问题的解。在斐波拉契数列里,最简单的问题是,一开始给定的第一个数和第二个数是几?自然我们可以得出是 1; 282 | **2)状态转移方程**,即第n个问题的解和之前的 n - m 个问题解的关系。在这道题目里,我们已经有了状态转移方程 F(n) = F(n - 1) + F(n - 2)。 283 | 284 | 所以这题要求 F(100),那我们只要知道 F(99) 和 F(98) 就行了;想知道 F(99),我们只要知道 F(98) 和 F(97) 就行了;想要知道 F(98),我们需要知道 F(97) 和 F(96) ...... ,以此类推,我们最后只要知道F(2)和F(1)的值,就可以推出 F(100)。而 F(2) 和 F(1) 正是我们所谓的初始状态,即 F(2) = 1,F(1) =1。所以代码如下: 285 | 286 | ``` 287 | func Fib(_ n: Int) -> Int { 288 | // 定义初始状态 289 | guard n > 0 else { 290 | return 0 291 | } 292 | if n == 1 || n == 2 { 293 | return 1 294 | } 295 | 296 | // 调用状态转移方程 297 | return Fib(n - 1) + Fib(n - 2) 298 | } 299 | 300 | print(Fib(100)) 301 | 302 | ``` 303 | 304 | ![斐波拉契数列的动态规划](https://upload-images.jianshu.io/upload_images/22877992-93f6fc62a9f0920e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 305 | 306 | 这种递归的写法看起来简洁明了,但是上面写法有一个问题:我们要求 F(100),那么要计算 F(99) 和 F(98) ;要计算 F(99),我们要计算 F(98) 和 F(97) ...... 大家已经发现到这一步,我们已经重复计算两次F(98) 了。而之后的计算中还会有大量的重复,这使得这个解法的复杂度非常之高。解决方法就是,用一个数组,将计算过的值存起来,这样可以用空间上的牺牲来换取时间上的效率提高,代码如下: 307 | 308 | ``` 309 | var nums = Array(repeating: 0, count: 100) 310 | 311 | func Fib(_ n: Int) -> Int { 312 | // 定义初始状态 313 | guard n > 0 else { 314 | return 0 315 | } 316 | if n == 1 || n == 2 { 317 | return 1 318 | } 319 | // 如果已经计算过,直接调用,无需重复计算 320 | if nums[n - 1] != 0 { 321 | return nums[n - 1] 322 | } 323 | 324 | // 将计算后的值存入数组 325 | nums[n - 1] = Fib(n - 1) + Fib(n - 2) 326 | 327 | return nums[n - 1] 328 | } 329 | 330 | ``` 331 | 332 | 动态转移虽然看上去十分高大上,但在面试中遇到相关问题要注意以下两点: 333 | 334 | * **栈溢出:**每一次递归,程序都会将当前的计算压入栈中。随着递归深度的加深,栈的高度也越来越高,直到超过计算机分配给当前进程的内存容量,程序就会崩溃。 335 | * **数据溢出:**因为动态规划是一种由简至繁的过程,其中积蓄的数据很有可能超过系统 当前数据类型的最大值,从而导致程序抛出异常。 336 | 337 | 这两点,我们在上面这道求解斐波拉契数列第100个数的题目就都遇到了。 338 | 339 | * 首先,递归的次数很多,我们要从 F(100) = F(99) + F(98) ,一直推理到 F(3) = F(2) + F(1),这样很容易造成栈溢出。 340 | * 其次,F(100) 应该是一个很大的数。实际上 F(40) 就已经突破一亿,F(100) 一定会造成整型数据溢出。 341 | 342 | 当然对于这两点我们也有相应的解决方法。对付栈溢出,我们可以把递归写成循环的形式(**所有的递归都可改写成循环**);对付数据溢出,我们可以在程序每次计算中,加入数据溢出的检测,适时终止计算,抛出异常。 343 | 344 | ## 斐波拉契数列问题面试实战题 345 | 346 | 笔者以前在硅谷参加了一个 hackthon 大赛,当时是要做一个扫描英文单词出翻译的 App。它大概长这样: 347 | 348 | ![image](https://upload-images.jianshu.io/upload_images/22877992-cd6fabd5da1244dd.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 349 | 350 | 当时这个 App 其他部分运行非常流畅,就是在打开摄像头扫描单词的时候,会出现误读的情况。比如手写的 “price”,机器会识别成 “pr1ce”,从而无法对其进行正确的翻译。笔者对这种情况进行了相应的优化处理,方法如下: 351 | 352 | **1) 缩小误差范围:**将所有的单词构造成前缀树。然后对于扫描的内容,搜索出相应可能的单词。具体做法可以参考上节《深度优先和广度优先》一文中搜索单词的方法。 353 | **2) 计算出最接近的单词:**假如上一步,我们已经有了10个可能的单词,那么怎么确定最接近真实情况的单词呢?这里我们要定义两个单词的距离 -- 从第一个单词wordA,到第二个单词wordB,有三种操作: 354 | 355 | * 删除一个字符 356 | * 添加一个字符 357 | * 替换一个字符 358 | 359 | 综合上述三种操作,用**最少步骤**将单词wordA变到单词wordB,我们就称这个值为两个单词之间的距离。比如 pr1ce -> price,只需要将 1 替换为 i 即可,所以两个单词之间的距离为1。pr1ce -> prize,要将 1 替换为 i ,再将 c 替换为 z ,所以两个单词之间的距离为2。相比于prize,price更为接近原来的单词。 360 | 361 | 现在问题转变为实现下面这个方法: 362 | 363 | ``` 364 | func wordDistance(_ wordA: String, wordB: String) -> Int { ... } 365 | 366 | ``` 367 | 368 | 要解决这个复杂的问题,我们不如从一个简单的例子出发:求 “abce” 到 “abdf” 之间的距离。它们两之间的距离,无非是下面三种情况中的一种。 369 | 370 | * 删除一个字符:假如已知 `wordDistance("abc", "abdf")` ,那么 “abce” 只需要删除一个字符到达 “abc” ,然后就可以得知 “abce” 到 “abdf” 之间的距离。 371 | * 添加一个字符:假如已知 `wordDistance("abce", "abd")`,那么我们只要让 “abd” 添加一个字符到达 “abdf” 即可求出最终解。 372 | * 替换一个字符:假如已知 `wordDistance("abc", "abd")`,那么就可以依此推出`wordDistance("abce", "abde") = wordDistance("abc", "abd")`。故而只要将末尾的 “e” 替换成 "f",就可以得出 `wordDistance("abce", "abdf")`。 373 | 374 | 这样我们就可以发现,求解任意两个单词之间的距离,只要知道之前单词组合的距离即可。我们用dp[i][j]表示第一个字符串 wordA[0…i] 和第 2 个字符串 wordB[0…j] 的最短编辑距离,那么这个动态规划的两个重要参数分别是: 375 | 376 | * **初始状态:**dp[0][j] = j,dp[i][0] = i 377 | * **状态转移方程:**dp[i][j] = min(dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]) + 1 378 | 379 | 再举例解释一下,"abc" 到 "xyz",dp[2][1] 就是 "ab" 到 "x" 的距离,不难看出是 2;dp[1][2] 就是 "a" 到 "xy" 的距离,是 2;dp[1][1] 也就是 "a" 到 "x" 的距离,很显然就是 1。所以 dp[2][2] 即 "ab" 到 "xy" 的距离是 min(dp[2][1], dp[1][2], dp[1][1]) + 1 就是 2。 380 | 381 | 有了初始状态和状态转移方程,那么动态规划的代码就出来了: 382 | 383 | ``` 384 | func wordDistance(_ wordA: String, _ wordB: String) -> Int { 385 | let aChars = Array(wordA), bChars = Array(wordB) 386 | let aLen = aChars.count, bLen = bChars.count 387 | 388 | var dp = Array(repeating: (Array(repeating: 0, count: bLen + 1)), count: aLen + 1) 389 | 390 | for i in 0 ... aLen { 391 | for j in 0 ... bLen { 392 | // 初始情况 393 | if i == 0 { 394 | dp[i][j] = j 395 | } else if j == 0 { 396 | dp[i][j] = i 397 | // 特殊情况 398 | } else if aChars[i - 1] == bChars[j - 1] { 399 | dp[i][j] = dp[i - 1][j - 1] 400 | } else { 401 | // 状态转移方程 402 | dp[i][j] = min(dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]) + 1 403 | } 404 | } 405 | } 406 | 407 | return dp[aLen][bLen] 408 | } 409 | 410 | ``` 411 | 412 | 用动态规划计算出单词之间的距离之后,在做一些相应的优化,就可以准确的识别出扫描的单词。 413 | 414 | ## 总结 415 | 416 | 动态规划算是算法进阶中比较重要的一环,它的思想就是把复杂问题化为简单具体问题,然后分析出初始状态和状态转移方程,从而推出最终解。也许它在实际编程或是 iOS 开发中出现频率不高,但是这种删繁就简的思路,却可以应用在生活或者工作中的方方面面。 417 | # 推荐👇: 418 | 419 | 如果你正在跳槽或者正准备跳槽不妨动动小手,添加一下咱们的交流群[**931 542 608**](https://jq.qq.com/?_wv=1027&k=cfVxrMAH)来获取一份详细的大厂面试资料为你的跳槽多添一份保障。 420 | 421 | ![](https://upload-images.jianshu.io/upload_images/22877992-0bfc037cc50cae7d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 422 | -------------------------------------------------------------------------------- /系统框架-并发编程: -------------------------------------------------------------------------------- 1 | 所有的语言都会涉及并发编程,并发就是多个任务同时运行,这也几乎是所有语言最难的地方。iOS 开发中,并发编程主要用于提升 App 的运行性能,保证App实时响应用户的操作。其中我们日常操作的 UI 界面就是运行在主线程之上,是一个串行线程。如果我们将所有的代码放在主线程上运行,那么主线程将承担网络请求、数据处理、图像渲染等各种操作,无论是 GPU 还是内存都会性能耗尽,从而影响用户体验。 2 | 3 | ![](https://upload-images.jianshu.io/upload_images/22877992-f318cd42c46c1e81.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 4 | 5 | 本章将从并发编程的理论说起,重点关注 GCD 和 Operations——iOS 开发中最主要的处理并发编程的两套 API。除了理论,实战部分将涉及如何将并发编程运用在实际 App 开发中。最后,本章将涉及如何 debug 并发编程的问题:包括如何观察在 Xcode 中观察线程信息、如何发现并解决编程中的并发问题。 6 | 7 | ### 1.iOS 开发中对于并发操作有哪三种方式? 8 | 9 | 关键词:#NSThread #GCD #Operations 10 | 11 | 在 iOS 开发中,基本有 3 种方式实现多线程: 12 | 13 | * **NSThread** 可以最大限度的掌控每一个线程的生命周期。但是同时也需要开发者手动管理所有的线程活动,比如创建、同步、暂停、取消等等,其中手动加锁操作挑战性很高。总体使用场景很小,基本是造轮子或是测试时使用。 14 | 15 | * **GCD(Grand Central Dispatch)** 是 Apple 推荐的方式,它将线程管理推给了系统,用的是名为 dispatch queue 的队列。开发者只要定义每个线程需要执行的工作即可。所有的工作都是先进先出,每一个 block 运转速度极快(纳秒级别)。使用场景主要是为了追求高效处理大量并发数据,如图片异步加载、网络请求等。 16 | 17 | * **Operations** 与 GCD 类似。虽然是 OperationQueue 队列实现,但是它并不局限于先进先出的队列操作。它提供了多个接口可以实现暂停、继续、终止、优先顺序、依赖等复杂操作,比 GCD 更加灵活。应用场景最广,效率上每个 Operation 处理速度较快(毫秒级别),几乎所有的基本线程操作都可以实现。 18 | 19 | ### 2.试比较以下关键词:Serial, Concurrent, Sync, Async? 20 | 21 | **关键词:#多任务 #阻塞线程** 22 | 23 | 这 4 个关键词,前两个 Serial/Concurrent 构成一对,后两个 Sync/Async 构成一对。我们分别来看: 24 | 25 | * **Serial/Concurrent** 声明队列的属性是串行还是并发。串行队列(Serial Queue)指队列中同一时间只能执行一个任务,当前任务执行完后才能执行下一个任务,在串行队列中只有一个线程。并发队列(Concurrent Queue)允许多个任务在同一个时间同时进行,在并发队列中有多个线程。串行队列的任务一定是按开始的顺序结束,而并发队列的任务并不一定会按照开始的顺序而结束。 26 | 27 | * **Sync/Async** 表明任务是同步还是异步执行。同步(Sync)会把当前的任务加入到队列中,除非等到任务执行完成,线程才会返回继续运行,也就是说同步会阻塞线程。异步(Async)也会把当前的任务加入到队列中,但它会立刻返回,无需等任务执行完成,也就是说异步不会阻塞线程。 28 | 29 | * 无论是串行还是并发队列都可以执行执行同步或异步操作。注意在串行队列上执行同步操作容易造成死锁,在并发队列上则不用担心。异步操作无论实在串行队列还是并发队列上都可能出现竞态条件的问题;同时异步操作经常与逃逸闭包一起出现在 API 的设计当中。 30 | 31 | ### 3.代码实战:以下代码均在串行队列中发生,执行之后会打印出什么? 32 | 33 | **关键词:#串行 #同步 #异步** 34 | 35 | ``` 36 | // 串行同步 37 | serialQueue.sync { 38 | print(1) 39 | } 40 | print(2) 41 | serialQueue.sync { 42 | print(3) 43 | } 44 | print(4) 45 | 46 | // 串行异步 47 | serialQueue.async { 48 | print(1) 49 | } 50 | print(2) 51 | serialQueue.async { 52 | print(3) 53 | } 54 | print(4) 55 | 56 | // 串行异步中嵌套同步 57 | print(1) 58 | serialQueue.async { 59 | print(2) 60 | serialQueue.sync { 61 | print(3) 62 | } 63 | print(4) 64 | } 65 | print(5) 66 | 67 | // 串行同步中嵌套异步 68 | print(1) 69 | serialQueue.sync { 70 | print(2) 71 | serialQueue.async { 72 | print(3) 73 | } 74 | print(4) 75 | } 76 | print(5) 77 | 78 | ``` 79 | 80 | 首先,在串行队列上进行同步操作,所有任务将顺序发生,所以第一段的打印结果一定是 1234; 81 | 82 | 其次,在串行队列上进行异步操作,此时任务完成的顺序并不保证。所以可能会打印出这几种结果:1234 ,2134,1243,2413,2143。注意 1 一定在 3 之前打印出来,因为前者在后者之前派发,串行队列一次只能执行一个任务,所以一旦派发完成就执行。同理 2 一定在 4 之前打印,2 一定在 3 之前打印。 83 | 84 | 接着,对同一个串行队列中进行异步、同步嵌套。这里会构成死锁,所以只会打印出 125 或者 152。 85 | 86 | 最后,在串行队列中进行同步、异步嵌套,不会构成死锁。这里会打印出 3 个结果:12345,12435,12453。这里1一定在最前,2 一定在 4 前,4 一定在 5 前。 87 | 88 | ### 4.代码实战:以下代码均在并发队列中发生,执行之后会打印出什么? 89 | 90 | **关键词:#并发 #同步 #异步** 91 | 92 | ``` 93 | // 并发同步 94 | concurrentQueue.sync { 95 | print(1) 96 | } 97 | print(2) 98 | concurrentQueue.sync { 99 | print(3) 100 | } 101 | print(4) 102 | 103 | // 并发异步 104 | concurrentQueue.async { 105 | print(1) 106 | } 107 | print(2) 108 | concurrentQueue.async { 109 | print(3) 110 | } 111 | print(4) 112 | 113 | // 并发异步中嵌套同步 114 | print(1) 115 | concurrentQueue.async { 116 | print(2) 117 | concurrentQueue.sync { 118 | print(3) 119 | } 120 | print(4) 121 | } 122 | print(5) 123 | 124 | // 并发同步中嵌套异步 125 | print(1) 126 | concurrentQueue.sync { 127 | print(2) 128 | concurrentQueue.async { 129 | print(3) 130 | } 131 | print(4) 132 | } 133 | print(5) 134 | 135 | ``` 136 | 137 | 如果你正在跳槽或者正准备跳槽不妨动动小手,添加一下咱们的交流群[**931 542 608**](https://jq.qq.com/?_wv=1027&k=cfVxrMAH)来获取一份详细的大厂面试资料为你的跳槽多添一份保障。 138 | 139 | 首先,在并发队列上进行同步操作,所有任务将顺序执行、顺序完成,所以第一段的打印结果一定是 1234; 140 | 141 | 其次,在并发队列上进行异步操作,因为并发对列有多个线程 。所以这里只能保证 24 顺序执行,13 乱序,可能插在任意位置:1234,1243,2413 ,2431,2143,2341,2134,2314。 142 | 143 | 接着,对同一个并发队列中进行异步、同步嵌套。这里不会构成死锁,因为同步操作只会阻塞一个线程,而并发队列对应多个线程。这里会打印出 4 个结果:12345,12534,12354,15234。注意同步操作保证了 3 一定会在 4 之前打印出来。 144 | 145 | 最后,在并发队列中进行同步、异步嵌套,不会构成死锁。而且由于是并发队列,所以在运行异步操作时也同时会运行其他操作。这里会打印出 3 个结果:12345,12435,12453。这里同步操作保证了 2 和 4 一定在 3 和 5 之前打印出来。 146 | 147 | ### 5.举例说明 iOS 并发编程中的三大问题? 148 | 149 | **关键词:#竞态条件 #优先倒置 #死锁问题** 150 | 151 | 在并发编程中,一般会面对这样的三个问题:竞态条件、优先倒置、死锁问题。针对 iOS 开发,它们的具体定义为: 152 | 153 | * **竞态条件(Race Condition)**。指两个或两个以上线程对共享的数据进行读写操作时,最终的数据结果不确定的情况。例如以下代码: 154 | 155 | ``` 156 | var num = 0 157 | DispatchQueue.global().async { 158 | for _ in 1…10000 { 159 | num += 1 160 | } 161 | } 162 | 163 | for _ in 1…10000 { 164 | num += 1 165 | } 166 | 167 | ``` 168 | 169 | 最后的计算结果 num 很有可能小于 20000,因为其操作为非原子操作。在上述两个线程对num进行读写时其值会随着进程执行顺序的不同而产生不同结果。 170 | 171 | * **优先倒置(Priority Inverstion)**。指低优先级的任务会因为各种原因先于高优先级任务执行。例如以下代码: 172 | 173 | ``` 174 | var highPriorityQueue = DispatchQueue.global(qos: .userInitiated) 175 | var lowPriorityQueue = DispatchQueue.global(qos: .utility) 176 | 177 | let semaphore = DispatchSemaphore(value: 1) 178 | 179 | lowPriorityQueue.async { 180 | semaphore.wait() 181 | for i in 0...10 { 182 | print(i) 183 | } 184 | semaphore.signal() 185 | } 186 | 187 | highPriorityQueue.async { 188 | semaphore.wait() 189 | for i in 11...20 { 190 | print(i) 191 | } 192 | semaphore.signal() 193 | } 194 | 195 | ``` 196 | 197 | 上述代码如果没有 semaphore,高优先权的 highPriorityQueue 会优先执行,所以程序会优先打印完 11 到 20。而加了 semaphore 之后,低优先权的 lowPriorityQueue 会先挂起 semaphore,高优先权的highPriorityQueue 就只有等 semaphore 被释放才能再执行打印。 198 | 199 | 也就是说,低优先权的线程可以锁上某种高优先权线程需要的资源,从而优于迫使高优先权的线程等待低优先权的线程,这就叫做优先倒置。 200 | 201 | * **死锁问题(Dead Lock)**。指两个或两个以上的线程,它们之间互相等待彼此停止执行,以获得某种资源,但是没有一方会提前退出的情况。iOS 中有个经典的例子就是两个 Operation 互相依赖: 202 | 203 | ``` 204 | let operationA = Operation() 205 | let operationB = Operation() 206 | 207 | operationA.addDependency(operationB) 208 | operationB.addDependency(operationA) 209 | 210 | ``` 211 | 212 | 还有一种经典的情况,就是在对同一个串行队列中进行异步、同步嵌套: 213 | 214 | ``` 215 | serialQueue.async { 216 | serialQueue.sync { 217 | } 218 | } 219 | 220 | ``` 221 | 222 | 因为串行队列一次只能执行一个任务,所以首先它会把异步 block 中的任务派发执行,当进入到 block 中时,同步操作意味着阻塞当前队列 。而此时外部 block 正在等待内部 block 操作完成,而内部block 又阻塞其操作完成,即内部 block 在等待外部 block 操作完成。所以串行队列自己等待自己释放资源,构成死锁。这也提醒了我们,千万不要在主线程中用同步操作。 223 | 224 | 如果你正在跳槽或者正准备跳槽不妨动动小手,添加一下咱们的交流群[**931 542 608**](https://jq.qq.com/?_wv=1027&k=cfVxrMAH)来获取一份详细的大厂面试资料为你的跳槽多添一份保障。 225 | 226 | ### 6.代码实战:以下代码有什么隐患? 227 | 228 | **关键词:#竞态条件 #thread sanitizer** 229 | 230 | ``` 231 | func getUser(id: String) throws -> User { 232 | return try storage.getUser(id) 233 | } 234 | 235 | func setUser(_ user: User) throws { 236 | try storage.setUser(user) 237 | } 238 | 239 | ``` 240 | 241 | 上面这段代码的功能是读写用户信息。乍一看上去没有什么问题,但是一旦多线程涉及到读写,就会产生竞态条件(race condition)。解决方法是打开Xcode中的线程检测工具 thread sanitizer(在 Xcode 的scheme 中勾选 Thread Sanitizer 即可),它会检测出代码中出现竞态条件之处,并提醒我们修改。 242 | 243 | 对于读写问题,一般有 3 种处理方式。 244 | 245 | 第 1 种是用串行队列,无论读写,同一时间只能做一个操作,这样就保证了队列的安全。其缺点是速度慢,尤其是在大量读写发生时,每次只能做单个读或写操作的效率实在太低。修改代码如下: 246 | 247 | ``` 248 | func getUser(id: String) throws -> User { 249 | return serialQueue.sync { 250 | return try storage.getUser(id) 251 | } 252 | } 253 | func setUser(_ user: User) throws { 254 | try serialQueue.sync { 255 | try storage.setUser(user) 256 | } 257 | } 258 | 259 | ``` 260 | 261 | 第 2 种是用并发队列配合异步操作完成。异步操作由于会直接返回,所以必须配合逃逸闭包来保证后续操作的合法性。 262 | 263 | ``` 264 | enum Result { 265 | case value(T) 266 | case error(Error) 267 | } 268 | 269 | func getUser(id: String, completion: @escaping (Result) -> Void) { 270 | try serialQueue.async { 271 | do { 272 | user = try storage.getUser(id) 273 | completion(.value(user)) 274 | } catch { 275 | completion(.error(error)) 276 | } 277 | } 278 | } 279 | 280 | func setUser(_ user: User, completion: @escaping (Result<()>) -> Void) { 281 | try serialQueue.async { 282 | do { 283 | try storage.setUser(user) 284 | completion(.value(()) 285 | } catch { 286 | completion(.error(error)) 287 | } 288 | } 289 | } 290 | 291 | ``` 292 | 293 | 第 3 种方法是用并发队列,读操作时用 sync 直接返回结果,写操作时用 barrier flag 来保证此时并发队列只进行当前的写操作(类似将并发队列暂时转为串行队列),而无视其他操作。示例代码如下: 294 | 295 | ``` 296 | enum Result { 297 | case value(T) 298 | case error(Error) 299 | } 300 | 301 | func getUser(id: String) throws -> User { 302 | var user: User! 303 | try concurrentQueue.sync { 304 | user = try storage.getUser(id) 305 | } 306 | 307 | return user 308 | } 309 | 310 | func setUser(_ user: User, completion: @escaping (Result<()>) -> Void) { 311 | try concurrentQueue.async(flags: .barrier) { 312 | do { 313 | try storage.setUser(user) 314 | completion(.value(()) 315 | } catch { 316 | completion(.error(error)) 317 | } 318 | } 319 | } 320 | 321 | ``` 322 | 323 | ### 7.试比较以下 GCD 中的方法 dispatch_async, dispatch_after, dispatch_once, dispatch_group 324 | 325 | **关键词:#异步 #延时 #单例 #线程同步** 326 | 327 | 首先要明确,这几个关键词都是 Objective-C 编程中出现的。它们分别有如下用法: 328 | 329 | * dispatch_async 用于对某个线程进行异步操作。异步操作可以让我们在不阻塞线程的情况下充分利用不同线程和队列来处理任务。例如我们需要从网络端下载图片,然后将图片赋予某个 UIImageView,就可以用到 dispatch_async: 330 | 331 | ``` 332 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ 333 | UIImage *image = [client fetchImageFromURL: url]; 334 | dispatch_async(dispatch_get_main_queue(), ^{ 335 | self.imageView.image = image; 336 | }); 337 | }); 338 | 339 | ``` 340 | 341 | * dispatch_after 一般用于主线程的延时操作。例如要将一个页面的导航标题由“等待”之后 2 秒改为“完成”,可以用 dispatch_after 来实现: 342 | 343 | ``` 344 | self.title = @”等待”; 345 | double delayInSeconds = 2.0; 346 | dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)); 347 | dispatch_after(popTime, dispatch_get_main_queue(), ^(void){ 348 | self.title = @”完成”; 349 | }); 350 | 351 | ``` 352 | 353 | 如果你正在跳槽或者正准备跳槽不妨动动小手,添加一下咱们的交流群[**931 542 608**](https://jq.qq.com/?_wv=1027&k=cfVxrMAH)来获取一份详细的大厂面试资料为你的跳槽多添一份保障。 354 | 355 | * dispatch_once 用于确保单例的线程安全。它表示修饰的区域只会访问一次,这样多线程情况下类也只会初始化一次,确保了 Objective-C 中单例的原子化。 356 | 357 | ``` 358 | + (instancetype)sharedManager { 359 | static Manager *sharedManager = nil; 360 | static dispatch_once_t onceToken; 361 | dispatch_once(&onceToken, ^{ 362 | sharedManager = [[Manager alloc] init]; 363 | }); 364 | return sharedManager; 365 | } 366 | 367 | ``` 368 | 369 | * dispatch_group 一般用于多个任务同步。一般用法是当多个任务关联到同一个群组(group)后,所有的任务在执行完后我们执行一个统一的后续工作。注意 dispatch_group_wait 是个同步操作,它会阻塞线程。 370 | 371 | ``` 372 | dispatch_group_t group = dispatch_group_create(); 373 | 374 | dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ 375 | NSLog(@”开始做任务1!”); 376 | }); 377 | 378 | dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ 379 | NSLog(@”开始做任务2!”) 380 | }); 381 | 382 | dispatch_group_notify(group, dispatch_get_main_queue(), ^{ 383 | NSLog(@”任务1和任务2都完成了!”) 384 | }); 385 | 386 | ``` 387 | 388 | ### 8.GCD 中全局(global)队列有哪几种优先级? 389 | 390 | **关键词:#QoS** 391 | 392 | 官方链接:[https://developer.apple.com/library/archive/documentation/Performance/Conceptual/EnergyGuide-iOS/PrioritizeWorkWithQoS.html](https://developer.apple.com/library/archive/documentation/Performance/Conceptual/EnergyGuide-iOS/PrioritizeWorkWithQoS.html) 393 | 394 | 首先,全局队列肯定是并发队列。如果不指定优先级,就是默认(default)优先级。另外还有 background,utility,user-Initiated,unspecified,user-Interactive。下面按照优先级顺序从低到高来排列: 395 | 396 | * **Background:**用来处理特别耗时(几分钟甚至上小时)的后台操作,例如同步、备份数据;性能方面优先考虑电量消耗。 397 | 398 | * **Utility:**用来处理需要一点时间(几十秒到几分钟)而又不需要立刻返回结果的操作。特别适用于异步操作,例如下载或是解码编码数据;性能方面会综合考虑电量消耗和即时反馈。 399 | 400 | * **Default:**默认优先级。一般来说开发者应该指定优先级。属于特殊情况。 401 | 402 | * **User-Initiated:**用来处理用户触发的、需要立刻(几秒)返回结果的操作。比如打开用户点击的文件;性能方面优先考虑即时反馈。 403 | 404 | * **User-Interactive:**用来相应用户交互或展现动画。如果不及时响应就可能阻塞主线程的操作;性能方面优先考虑立刻响应。 405 | 406 | * **Unspecified:**未确定优先级,由系统根据不同环境推断。比如使用过时的 API 不支持优先级,此时就可以设定为未确定优先级。属于特殊情况。 407 | 408 | ### 9.试比较以下 Operations 中的关键词:Operation,BlockOperation,OperationQueue 409 | 410 | **关键词:#Operation** 411 | 412 | Operations 是 iOS 中除 GCD 外有一种实现并发编程的方式。它将单个任务算作一个 Operation,然后放在 OperationQueue 队列中进行管理和运行。其中最常用的三个关键词就是 Operation,BlockOperation,OperationQueue。 413 | 414 | * Operation 指代一系列工作或者任务。Operation 本身是个抽象类,我们一般通过集成它来完成自定义的工作。每个单独的 Operation 有个 4 种状态:准备就绪(Ready),执行中(Executing),暂停(cancelled),完成(Finished)。下面就是一个自定义的图片转化的 Operation 子类: 415 | 416 | ``` 417 | class ImageFilterOperation: Operation { 418 | var inputImage: UIImage? 419 | var outputImage: UIImage? 420 | 421 | override func main() { 422 | outputImage = filter(image: inputImage) 423 | } 424 | } 425 | 426 | ``` 427 | 428 | * BlockOperation 是系统定义的一个 Operation 的子类。它本身在默认权限的全局队列上进行,负责并发执行多个任务。其中任务之间互不依赖,同时 BlockOperation 也可以像 dispatch_group 一样同步管理多个任务。例如下面的示例代码: 429 | 430 | ``` 431 | let multiTasks = BlockOperation() 432 | 433 | multiPrinter.completionBlock = { 434 | print("所有任务完成!") 435 | } 436 | 437 | multiTasks.addExecutionBlock { print("任务1开始"); sleep(1) } 438 | multiTasks.addExecutionBlock { print("任务2开始"); sleep(2) } 439 | multiTasks.addExecutionBlock { print("任务3开始"); sleep(3) } 440 | 441 | multiPrinter.start() 442 | 443 | ``` 444 | 445 | * OperationQueue 是负责安排和运行多个 Operation 的队列。但是它并不局限于先进先出的队列操作。它提供了多个接口可以实现暂停、继续、终止、优先顺序、依赖等复杂操作。同时,还可以通过设置其 maxConcurrentOperationCount 属性来区分其是串行还是并发。下面是示例代码: 446 | 447 | ``` 448 | Let multiTaskQueue = OperationQueue() 449 | 450 | multiTaskQueue.addOperation { print("任务1开始"); sleep(1) } 451 | multiTaskQueue.addOperation { print("任务2开始"); sleep(2) } 452 | multiTaskQueue.addOperation { print("任务3开始"); sleep(3) } 453 | 454 | multiTaskQueue.waitUntilAllOperationsAreFinished() 455 | 456 | ``` 457 | 458 | ### 10.如何在 OperationQueue 中取消某个 Operation 操作? 459 | 460 | **关键词:#cancel() #isCancelled** 461 | 462 | 在 Operation 抽象类中,有一个 cancel() 方法,它做的唯一一件事情就是将 Operation 的 isCancelled 属性从 false 改为 true。由于它并不会真正去深入代码将具体执行的工作暂停,所以我们必须利用 isCancelled 属性的变化来暂停 main() 方法中的工作。 463 | 464 | 举个例子,我们有个求和整型数组的操作,其对应 Operation 如下: 465 | 466 | ``` 467 | class ArraySumOperation: Operation { 468 | let nums: [Int] 469 | var sum: Int 470 | 471 | init(nums: [Int]) { 472 | self.nums = nums 473 | self.sum = 0 474 | super.init() 475 | } 476 | 477 | override func main() { 478 | for num in nums { 479 | sum += num 480 | } 481 | } 482 | } 483 | 484 | ``` 485 | 486 | 如果我们要运行该 Operation,就应该将其加入到 OperationQueue 中: 487 | 488 | ``` 489 | let queue = OperationQueue() 490 | let sumOperation = ArraySumOperation(nums: Array(1...1000)) 491 | 492 | // 一旦假如到OperationQueue中,Operation就开始执行 493 | queue.addOperation(sumOperation) 494 | 495 | // 打印出500500(1+2+3+...+1000) 496 | sumOperation.completionBlock = { print(sumOperation.sum) } 497 | 498 | ``` 499 | 500 | 如果要取消,我们应该调用 cancel() 方法,而它只会将 isCancelled 改成 false,而我们要利用 isCancelled 的状态控制 main() 中的操作,所以可将 ArraySumOperation 改成如下: 501 | 502 | ``` 503 | class ArraySumOperation: Operation { 504 | let nums: [Int] 505 | var sum: Int 506 | 507 | init(nums: [Int]) { 508 | self.nums = nums 509 | self.sum = 0 510 | super.init() 511 | } 512 | 513 | override func main() { 514 | for num in nums { 515 | if isCancelled { 516 | return 517 | } 518 | sum += num 519 | } 520 | } 521 | } 522 | 523 | ``` 524 | 525 | 此时运行 Operation,就会得到不同结果: 526 | 527 | ``` 528 | let queue = OperationQueue() 529 | let sumOperation = ArraySumOperation(nums: Array(1...1000)) 530 | 531 | // 一旦假如到OperationQueue中,Operation就开始执行 532 | queue.addOperation(sumOperation) 533 | 534 | sumOperation.cancel() 535 | 536 | // sumOperation在彻底完成前已经暂停,sum值小于500500 537 | sumOperation.completionBlock = { print(sumOperation.sum) } 538 | 539 | ``` 540 | 541 | 同时 OperationQueue 还有 cancelAllOperations() 方法可以调用,它相当于是对于所有在该队列上工作的 Operation,都调用其 cancel() 方法。 542 | 543 | ### 11.说说在实际开发中,主线程和其他线程的使用场景 544 | 545 | **关键词:#UI #耗时** 546 | 547 | 主线程一般用于负责 UI 相关操作,如绘制图层、布局、响应用户响应。很多 UIKit 相关的控件如果不在主线程操作,会产生未知效果。Xcode 中的 Main Thread Checker 可以将相关问题检测出来并报错。 548 | 549 | 其他线程例如后台线程一般用来处理比较耗时的工作。网络请求、数据解析、复杂计算、图片的编码解码管理等都属于耗时的工作,应该放在其他线程处理。如果放在主线程,由于其是串行队列,会直接阻塞主线程的 UI 操作,直接影响用户体验。 550 | 551 | # 推荐👇: 552 | 553 | 如果你正在跳槽或者正准备跳槽不妨动动小手,添加一下咱们的交流群[**931 542 608**](https://jq.qq.com/?_wv=1027&k=cfVxrMAH)来获取一份详细的大厂面试资料为你的跳槽多添一份保障。 554 | 555 | ![](https://upload-images.jianshu.io/upload_images/22877992-0bfc037cc50cae7d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 556 | 557 | -------------------------------------------------------------------------------- /算法基础4-5节.md: -------------------------------------------------------------------------------- 1 | # 4. 二叉树 2 | 前面介绍了数组、字典、字符串、链表、栈、队列的处理和应用方法。本节将会探讨平常相对很少用到、面试中却是老面孔的数据结构:二叉树。本节主要包括以下内容: 3 | 4 | ![](https://upload-images.jianshu.io/upload_images/22877992-a2dcf3591cd23383.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 5 | 6 | 7 | * 基本概念:实现,深度 ,二叉查找树 8 | * 二叉树的遍历 9 | * 苹果公司面试题:在 iOS 中展示二叉树 10 | 11 | ## 二叉树的基本概念 12 | 13 | ![](https://upload-images.jianshu.io/upload_images/22877992-3527805b203c38ec.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 14 | 15 | 首先介绍下二叉树。二叉树中每个节点最多有两个子节点,一般称为左子节点和右子节点,并且二叉树的子树有左右之分,其次序不能任意颠倒。下面是节点的 Swift 实现: 16 | 17 | ``` 18 | public class TreeNode { 19 | public var val: Int 20 | public var left: TreeNode? 21 | public var right: TreeNode? 22 | public init(_val: Int) { 23 | self.val = val 24 | } 25 | } 26 | 27 | ``` 28 | 29 | 一般在面试中,会给定二叉树的根节点。要访问任何其他节点,只要从起始节点开始往左/右走即可。 30 | 31 | 在二叉树中,节点的层次从根开始定义,根为第一层,树中节点的最大层次为树的**深度**。 32 | 33 | ``` 34 | // 计算树的最大深度 35 | func maxDepth(root: TreeNode?) -> Int { 36 | guard let root = root else { 37 | return 0 38 | } 39 | return max(maxDepth(root.left), maxDepth(root.right)) + 1 40 | } 41 | 42 | ``` 43 | 44 | 面试中,最常见的是二叉查找树,它是一种特殊的二叉树。它的特点就是左子树中节点的值都小于根节点的值,右子树中节点的值都大于根节点的值。那么问题来了,给你一棵二叉树,怎么判断它是二叉查找树?我们根据定义,可以写出以下解法: 45 | 46 | ``` 47 | // 判断一颗二叉树是否为二叉查找树 48 | func isValidBST(root: TreeNode?) -> Bool { 49 | return _helper(root, nil, nil) 50 | } 51 | 52 | private func _helper(node: TreeNode?, _ min: Int?, _ max: Int?) -> Bool { 53 | guard let node = node else { 54 | return true 55 | } 56 | // 所有右子节点都必须大于根节点 57 | if let min = min, node.val <= min { 58 | return false 59 | } 60 | // 所有左子节点都必须小于根节点 61 | if let max = max, node.val >= max { 62 | return false 63 | } 64 | 65 | return _helper(node.left, min, node.val) && _helper(node.right, node.val, max) 66 | } 67 | 68 | ``` 69 | 70 | 上面的代码有这几个点值得注意: 71 | 72 | 1. 二叉树本身是由递归定义的,所以原理上所有二叉树的题目都可以用递归来解 73 | 2. 二叉树这类题目很容易就会牵涉到往左往右走,所以写 helper 函数要想到有两个相对应的参数 74 | 3. 记得处理节点为 nil 的情况,尤其要注意根节点为 nil 的情况 75 | 76 | ## 二叉树的遍历 77 | 78 | 最常见的树的遍历有 3 种,前序、中序、后序遍历。这 3 种写法相似,无非是递归的顺序略有不同。面试时候有可能考察的是用非递归的方法写这 3 种遍历:用栈实现。 79 | 80 | ``` 81 | // 用栈实现的前序遍历 82 | func preorderTraversal(root: TreeNode?) -> [Int] { 83 | var res = [Int]() 84 | var stack = [TreeNode]() 85 | var node = root 86 | 87 | while !stack.isEmpty || node != nil { 88 | if node != nil { 89 | res.append(node!.val) 90 | stack.append(node!) 91 | node = node!.left 92 | } else { 93 | node = stack.removeLast().right 94 | } 95 | } 96 | 97 | return res 98 | } 99 | 100 | ``` 101 | 102 | 除了这三种最常见的遍历之外,还有一种遍历是层级遍历(广度优先遍历),请看下图: 103 | 104 | ![](https://upload-images.jianshu.io/upload_images/22877992-61b47da981769e2e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 105 | 106 | 这棵树的层级遍历结果为[[1], [2, 3], [4, 5, 6, 7], [8, 9, 10, 11, 12, 13, 14, 15]]。 107 | 108 | 层级遍历相比于以上 3 种常见遍历的好处在于:如果构建一棵树,那么至少要知道中序遍历和前序/后序遍历中的一种,也就是至少要知道两种遍历方式;但是层级遍历自己便可以唯一确定一棵树。我们来看下面一道苹果公司的面试题。 109 | 110 | ## 二叉树面试实战题 111 | 112 | > 请设计一个应用可以展示一颗二叉树。 113 | 114 | 首先一个简单的 App 是 MVC 架构,所以我们就要想,在 View 的层面上表示一棵二叉树?我们脑海中浮现树的结构是这样的: 115 | 116 | ![](https://upload-images.jianshu.io/upload_images/22877992-4b594de6cc020237.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 117 | 118 | 所以是不是在 View 的界面上,每个节点弄个 UILabel 来表示,然后用数学方法计算每个 UIlabel 对应的位置,从而完美的显示上图的样子? 119 | 120 | 这个想法比较简单粗暴,是最容易想到,实现之后又是最直观展示一棵二叉树的,但是它有以下两个问题: 121 | 122 | * 每个 UILabel 的位置计算起来比较麻烦; 123 | * 如果一棵树有很多节点(比如1000个),那么当前界面就会显示不下了,这时候咋办?就算用 UIScrollView 来处理,整个树也会变得非常不直观,每个节点所对应的 UILabel 位置计算起来就会更费力。 124 | 125 | 要处理大量数据,我们就想到了 UITableView。假如每一个 cell 对应一个节点,以及其左、右节点,那么我们就可以清晰的展示一棵树。比如上图这棵树,用 UITableView 就可以写成这样: 126 | 127 | ![](https://upload-images.jianshu.io/upload_images/22877992-621c0932d2edc6e7.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 128 | 129 | 其中"#"表示空节点。明眼人可以看出,我们是按照层级遍历的方式布局整个 UITableView。这种做法解决了上面两个问题: 130 | 131 | * 无需进行位置计算,UITableView 提供复用 Cell,效率大幅提高 132 | * 面对很多节点的问题,可以先处理一部分数据,然后用处理 infinite scroll 的方式来加载剩余数据 133 | 134 | 接着问题来了,给你一棵二叉树,如何得到它的层级遍历?其实层级遍历就是图的广度优先遍历,而广度优先遍历很自然就会用到队列,所以我们不妨用队列来帮助实现树的层级遍历: 135 | 136 | ``` 137 | func levelOrder(root: TreeNode?) -> [[Int]] { 138 | var res = [[Int]]() 139 | // 用数组来实现队列 140 | var queue = [TreeNode]() 141 | 142 | if let root = root { 143 | queue.append(root) 144 | } 145 | 146 | while queue.count > 0 { 147 | var size = queue.count 148 | var level = [Int]() 149 | 150 | for _ in 0 ..< size { 151 | let node = queue.removeFirst() 152 | 153 | level.append(node.val) 154 | if let left = node.left { 155 | queue.append(left) 156 | } 157 | if let right = node.right { 158 | queue.append(right) 159 | } 160 | } 161 | res.append(level) 162 | } 163 | 164 | return res 165 | } 166 | 167 | ``` 168 | 169 | ## 总结 170 | 171 | 到这里为止,我们已经把重要的数据结构都分析了一遍。要知道,这些数据结构都不是单独存在的,我们在解决二叉树的问题时,用到了队列;解决数组的问题,也会用到字典或是栈。在真正面试或是日常开发中,最低的时间复杂度是首要考虑,接着是优化空间复杂度,其次千万不要忘记考虑边界情况。在Swift 中,用 let 和 var 的地方要区分清楚,该不该定义数据为 optional,有没有处理 nil 的情况都是很容易忽略的。 172 | 173 | # 5. 排序和搜索 174 | 前几节中,我们主要探讨了数据结构:比如数组、链表、队列、树。这些数据结构都是了解 Swift 和算法的基础。从今以后的文章,我们将更多的关注于通用算法,这次我们就来聊聊排序和搜索。 175 | 排序的基本概念 176 | 177 | 说到排序,我们平常用的算法一般就以下几种: 178 | 179 | | 名称 | 时间复杂度 | 空间复杂度 | 是否稳定 | 180 | | --- | --- | --- | --- | 181 | | 冒泡排序 | O(n^2) | O(1) | 是 | 182 | | 插入排序 | O(n^2) | O(1) | 是 | 183 | | 选择排序 | O(n^2) | O(1) | 否 | 184 | | 堆排序 | O(nlogn) | O(1) | 否 | 185 | | 归并排序 | O(nlogn) | O(n) | 是 | 186 | | 快速排序 | O(nlogn) | O(logn) | 否 | 187 | | 桶排序 | O(n) | O(k) | 是 | 188 | 189 | 这些算法具体的定义本文不再赘述。一般情况下,好的排序算法性能是 O(nlogn),坏的性能是 O(n^2)。本文在此用 Swift 示范实现归并排序和快速排序: 190 | 191 | ``` 192 | // 归并排序 193 | func mergeSort(_ array: [Int]) -> [Int] { 194 | guard array.count > 1 else { 195 | return array 196 | } 197 | 198 | // 分割 199 | let middleIndex = array.count / 2 200 | let leftArray = mergeSort(Array(array[0.. [Int] { 209 | var leftIndex = 0, rightIndex = 0 210 | var orderedArray = [Int]() 211 | 212 | while leftIndex < left.count || rightIndex < right.count { 213 | if leftIndex < left.count && rightIndex < right.count { 214 | if left[leftIndex] <= right[rightIndex] { 215 | orderedArray.append(left[leftIndex]) 216 | leftIndex += 1 217 | } else { 218 | orderedArray.append(right[rightIndex]) 219 | rightIndex += 1 220 | } 221 | } else if leftIndex < left.count { 222 | orderedArray.append(left[leftIndex]) 223 | leftIndex += 1 224 | } else { 225 | orderedArray.append(right[rightIndex]) 226 | rightIndex += 1 227 | 228 | } 229 | } 230 | 231 | return orderedArray 232 | } 233 | 234 | // 快速排序 235 | func quicksort(_ array:[Int]) -> [Int] { 236 | guard array.count > 1 else { 237 | return array 238 | } 239 | let pivot = array[array.count / 2] 240 | let left = array.filter { $0 < pivot } 241 | let middle = array.filter { $0 == pivot } 242 | let right = array.filter { $0 > pivot } 243 | 244 | return quicksort(left) + middle + quicksort(right) 245 | } 246 | 247 | ``` 248 | 249 | 表格中有一个特例是桶排序,它是将输入的数组分配到一定数量的空桶中,每个空桶再单独排序。当输入的数组是均匀分配时,桶排序的时间复杂度为 O(n)。举个微软的面试题来当例子: 250 | 251 | > 有三种颜色(红,黄,蓝)的球若干,要求将所有红色的球放在黄色球的前面,最后放上所有的蓝色球。 252 | 253 | 这道题目最直接的解法就是桶排序。首先第一次遍历,统计有多少个红色球(假设 x 个),多少个黄色球(假设 y 个),和多少个蓝色球(假设 z 个)。然后再一次遍历,数组前部 x 个位置填充红色球,中间 y 个位置放上对应数量的黄色球,最后 z 个位置再放上蓝色球。 254 | 255 | 另外解释一下稳定的意思:相等的键值,如果排过序后与原来未排序的次序相同,则称此排序算法为稳定。举个例子: 256 | 257 | ``` 258 | // 原数组 259 | [[2, 1], [1,3], [1,4]] 260 | 261 | // 排序算法一 262 | [[1,3], [1,4], [2, 1]] 263 | // 排序算法二 264 | [[1,4], [1,3], [2, 1]] 265 | 266 | ``` 267 | 268 | 我们注意到排序算法一和二的区别就在于对[1, 3], [1, 4]这两个元素的处理。排序算法一中,这两个元素位置与原数组相同,故称其为稳定算法。而排序算法二则是不稳定算法。 269 | 在 Swift 中,排序的使用如下: 270 | 271 | ``` 272 | // 以升序排列为例,原数组可改变 273 | array.sort() 274 | 275 | // 以降序排列为例,原数组不可改变 276 | newArray = array.sorted(by: >) 277 | 278 | // 字典键值排序示例 279 | let keys = Array(map.keys) 280 | let sortedKeys = keys.sorted() { 281 | return map[$0]! > map[$1]! 282 | } 283 | 284 | ``` 285 | 286 | 在 Java 中,其自带的 sort 函数部分是用归并排序实现的。而在 Swift 源代码中,sort 函数采用的是一种内省算法(IntroSort)。它由堆排序、插入排序、快速排序 3 种算法构成,依据输入的深度选择最佳的算法来完成。本书关注的重点是实战,所以不做展开。对源代码感兴趣的读者可以在 GitHub 上读取苹果公司的 Swift 开源库。 287 | 288 | ## 搜索的基本概念 289 | 290 | 一般最直接的搜索就是遍历集合,然后找到满足条件的元素。这种直接的暴力搜索法实现起来简单,但是当输入数据十分巨大的时候,搜索起来就会很慢(复杂度 O(n))。本节将探讨算法复杂度更低、效果更好的搜索方法 —— 二分搜索。 291 | 292 | 二分搜索,即在有序数组中,查找某一特定元素的搜索。它从中间的元素开始,如果中间的元素是要找的元素,则返回;若中间元素小于要找的元素,则要找的元素一定在大于中间元素的那一部分,那只需搜索那部分即可;反之搜索小于中间元素的部分即可。重复以上步骤,直到找到或确认该元素不存在为止。这样每一次搜索,就能减小一办的搜索范围,所以它的**算法复杂度为 O(logn)**。 293 | 294 | Swift 实现二分搜索 295 | 296 | ``` 297 | // 假设nums是一个升序数组 298 | func binarySearch(_ nums: [Int], _ target: Int) -> Bool { 299 | var left = 0, mid = 0, right = nums.count - 1 300 | 301 | while left <= right { 302 | mid = (right - left) / 2 + left 303 | 304 | if nums[mid] == target { 305 | return true 306 | } else if nums[mid] < target { 307 | left = mid + 1 308 | } else { 309 | right = mid - 1 310 | } 311 | } 312 | 313 | return false 314 | } 315 | 316 | ``` 317 | 318 | 这里要注意两个细节: 319 | 320 | 第一,mid 定义在 while 循环外面,如果定义在里面,则每次循环都要重新给 mid 分配内存空间,造成不必要的浪费;定义在循环之外,每次循环只是重新赋值; 321 | 第二,每次重新给 mid 赋值不能写成 mid = (right + left) / 2。这种写法表面上看没有问题,但当数组的长度非常大、算法又已经搜索到了最右边部分的时候,那么 right + left 就会非常之大,造成溢出导致程序崩溃。所以解决问题的办法是写成 mid = (right - left) / 2 + left。 322 | 323 | 当然,我们可以用递归来实现二分搜索: 324 | 325 | ``` 326 | func binarySearch(nums: [Int], target: Int) -> Bool { 327 | return binarySearch(nums: nums, target: target, left: 0, right: nums.count - 1) 328 | } 329 | 330 | func binarySearch(nums: [Int], target: Int, left: Int, right: Int) -> Bool { 331 | guard left <= right else { 332 | return false 333 | } 334 | 335 | let mid = (right - left) / 2 + left 336 | 337 | if nums[mid] == target { 338 | return true 339 | } else if nums[mid] < target { 340 | return binarySearch(nums: nums, target: target, left: mid + 1, right: right) 341 | } else { 342 | return binarySearch(nums: nums, target: target, left: left, right: mid - 1) 343 | } 344 | } 345 | 346 | ``` 347 | 348 | ## 排序实战 349 | 350 | 直接来看一道 Facebook, Google, Linkedin 都考过的面试题。 351 | 352 | > 已知有很多会议,如果有这些会议时间有重叠,则将它们合并。 353 | > 比如有一个会议的时间为 3 点到 5 点,另一个会议时间为 4 点到 6 点,那么合并之后的会议时间为 3 点到6点 354 | 355 | 解决算法题目第一步永远是把具体问题抽象化。这里每一个会议我们已知开始时间和结束时间,就可以写一个类来定义它: 356 | 357 | ``` 358 | public class MeetingTime { 359 | public var start: Int 360 | public var end: Int 361 | public init(_ start: Int, _ end: Int) { 362 | self.start = start 363 | self.end = end 364 | } 365 | } 366 | 367 | ``` 368 | 369 | 然后题目说已知有很多会议,就是说我们已知有一个 MeetingTime 的数组、所以题目就转化为写一个函数,输入为一个 MeetingTime 的数组,输出为一个将原数组中所有重叠时间都处理过的新数组。 370 | 371 | ``` 372 | func merge(meetingTimes: [MeetingTime]) -> [MeetingTime] {} 373 | 374 | ``` 375 | 376 | 下面来分析一下题目怎么解。最基本的思路是遍历一次数组,然后归并所有重叠时间。举个例子:[[1, 3], [5, 6], [4, 7], [2, 3]]。这里我们可以发现[1, 3]和[2, 3]可以归并为[1, 3],[5, 6]和[4, 7]可以归并为[4, 7]。所以这里就提出一个要求:要将所有可能重叠的时间尽量放在一起,这样遍历的时候可以就可以从前往后一个接着一个的归并。于是很自然的想到 -- 按照会议开始的时间排序。 377 | 378 | 这里我们要对一个 class 进行排序,而且要自定义排序方法,在 Swift 中可以这样写: 379 | 380 | ``` 381 | meetingTimes.sortInPlace() { 382 | if $0.start != $1.start { 383 | return $0.start < $1.start 384 | } else { 385 | return $0.end < $1.end 386 | } 387 | } 388 | 389 | ``` 390 | 391 | 意思就是首先对开始时间进行升序排列,如果它们相同,就比较结束时间。 392 | 393 | 有了排好顺序的数组,要得到新的归并后的结果数组,我们只需要在遍历的时候,每次比较原数组(排序后)当前会议时间与结果数组中当前的会议时间,假如它们有重叠,则归并;如果没有,则直接添加进结果数组之中。所有代码如下: 394 | 395 | ``` 396 | func merge(meetingTimes: [MeetingTime]) -> [MeetingTime] { 397 | // 处理特殊情况 398 | guard meetingTimes.count > 1 else { 399 | return meetingTimes 400 | } 401 | 402 | // 排序 403 | var meetingTimes = meetingTimes.sort() { 404 | if $0.start != $1.start { 405 | return $0.start < $1.start 406 | } else { 407 | return $0.end < $1.end 408 | } 409 | } 410 | 411 | // 新建结果数组 412 | var res = [MeetingTime]() 413 | res.append(meetingTimes[0]) 414 | 415 | // 遍历排序后的原数组,并与结果数组归并 416 | for i in 1.. last.end { 420 | res.append(current) 421 | } else { 422 | last.end = max(last.end, current.end) 423 | } 424 | } 425 | 426 | return res 427 | } 428 | 429 | ``` 430 | 431 | ## 搜索实战 432 | 433 | ### 第一题:版本崩溃 434 | 435 | 一般的二分搜索基本上稍微有点基本功的都能写出来。所以,真正面试的时候,最多也就是问问概念。真正的搜索相关面试题,长下面这个样子: 436 | 437 | > 有一个产品发布了 n 个版本。它遵循以下规律:假如某个版本崩溃了,后面的所有版本都会崩溃。 438 | > 举个例子:一个产品假如有 5 个版本,1,2,3 版都是好的,但是第 4 版崩溃了,那么第 5 个版本(最新版本)一定也崩溃。第 4 版则被称为第一个崩溃的版本。 439 | > 440 | > 现在已知一个产品有 n 个版本,而且有一个检测算法 func isBadVersion(version: Int) -> Bool 可以判断一个版本是否崩溃。假设这个产品的最新版本崩溃了,求第一个崩溃的版本。 441 | 442 | 分析这种题目,同样还是先抽象化。我们可以认为所有版本为一个数组 [1, 2, 3, ..., n],现在我们就是要在这个数组中检测出满足`isBadVersion(version) == true中version` 的最小值。 443 | 444 | 很容易就想到二分搜索,假如中间的版本是好的,第一个崩溃的版本就在右边,否则就在左边。我们来看一下如何实现: 445 | 446 | ``` 447 | func findFirstBadVersion(version: n) -> Int { 448 | // 处理特殊情况 449 | guard n >= 1 else { 450 | return -1 451 | } 452 | 453 | var left = 1, right = n, mid = 0 454 | 455 | while left < right { 456 | mid = (right - left) / 2 + left 457 | if isBadVersion(mid) { 458 | right = mid 459 | } else { 460 | left = mid + 1 461 | } 462 | } 463 | 464 | return left // return right 同样正确 465 | } 466 | 467 | ``` 468 | 469 | 这个实现方法要注意两点 470 | 471 | ``` 472 | 1.当发现中间版本(mid)是崩溃版本的时候,只能说明第一个崩溃的版本小于等于中间版本。所以只能写成 right = mid; 473 | 2.当检测到剩下一个版本的时候,我们已经无需在检测直接返回即可,因为它肯定是崩溃的版本。所以 while 循环不用写成 left <= right。 474 | 475 | ``` 476 | 477 | ### 第二题:搜索旋转有序数组 478 | 479 | 上面的题目是一个简单的二分搜索变种。我们来看一个复杂版本的: 480 | 481 | > 一个有序数组可能在某个位置被旋转。给定一个目标值,查找并返回这个元素在数组中的位置,如果不存在,返回 -1。假设数组中没有重复值。 482 | > 483 | > 举个例子:[0, 1, 2, 4, 5, 6, 7]在4这个数字位置上被旋转后变为[4, 5, 6, 7, 0, 1, 2]。搜索 4 返回 0 。搜索 8 则返回 -1 。 484 | 485 | 假如这个有序数组没有被旋转,那很简单,我们直接采用二分搜索就可以解决。现在被旋转了,还可以采用二分搜索吗? 486 | 487 | 我们先来想一下旋转之后的情况。第一种情况是旋转点选的特别前,这样旋转之后左半部分就是有序的;第二种情况是旋转点选的特别后,这样旋转之后右半部分就是有序的。如下图: 488 | 489 | ![](https://upload-images.jianshu.io/upload_images/22877992-372d88427788a5d3.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 490 | 491 | 那么怎么判断是结果 1 还是结果 2 呢?我们可以选取整个数组中间元素(mid) ,与数组的第1个元素(left)进行比较 -- 如果 mid > left,则是旋转结果1,那么数组的左半部分就是有序数组,我们可以在左半部分进行正常的二分搜索;反之则是结果二,数组的右半部分为有序数组,我们可以在右半部分进行二分搜索。 492 | 493 | 这里要注意一点,即使我们一开始清楚了旋转结果,也要判断一下目标值所落的区间。对于旋转结果1,数组左边最大的值是mid,最小值是left。假如要找的值target落在这个区间内,则使用二分搜索。否则就要在右边的范围内搜索,这个时候相当于回到了一开始的状态,有一个旋转的有序数组,只不过我们已经剔除了一半的搜索范围。对于旋转结果2,也类似处理。全部代码如下: 494 | 495 | ``` 496 | func search(nums: [Int], target: Int) -> Int { 497 | var (left, mid, right) = (0, 0, nums.count - 1) 498 | 499 | while left <= right { 500 | mid = (right - left) / 2 + left 501 | 502 | if nums[mid] == target { 503 | return mid 504 | } 505 | 506 | if nums[mid] >= nums[left] { 507 | if nums[mid] > target && target >= nums[left] { 508 | right = mid - 1 509 | } else { 510 | left = mid + 1 511 | } 512 | } else { 513 | if nums[mid] < target && target <= nums[right] { 514 | left = mid + 1 515 | } else { 516 | right = mid - 1 517 | } 518 | } 519 | } 520 | 521 | return -1 522 | } 523 | 524 | ``` 525 | 526 | 大家可以想一下假如旋转后的数组中有重复值比如[3,4,5,3,3,3]该怎么处理? 527 | 528 | ## iOS中搜索与排序的配合使用 529 | 530 | ![RSS Reader](https://upload-images.jianshu.io/upload_images/22877992-4c616c2c4b1dfe8f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 531 | 532 | 上图是iOS中开发的一个经典案例:新闻聚合阅读器(RSS Reader)。因为新闻内容经常会更新,所以每次下拉刷新这个UITableView或是点击右上角刷新按钮,经常会有新的内容加入到原来的dataSource中。刷新后合理插入新闻,就要运用到搜索和排列。 533 | 534 | 笔者在这里提供一个方法。首先,写一个 ArrayExtensions.swift; 535 | 536 | ``` 537 | extension Array { 538 | func indexForInsertingObject(object: AnyObject, compare: ((a: AnyObject, b: AnyObject) -> Int)) -> Int { 539 | var left = 0 540 | var right = self.count 541 | var mid = 0 542 | 543 | while left < right { 544 | mid = (right - left) / 2 + left 545 | 546 | if compare(a: self[mid] as! AnyObject, b: object) < 0 { 547 | left = mid + 1 548 | } else { 549 | right = mid 550 | } 551 | } 552 | 553 | return left 554 | } 555 | } 556 | 557 | ``` 558 | 559 | 然后在 FeedsViewController (就是显示所有新闻的 tableView 的 controller )里面使用这个方法。一般情况下,FeedsViewController 里面会有一个 dataSource,比如一个装新闻的 array。这个时候,我们调用这个方法,并且让它每次都按照新闻的时间进行排序: 560 | 561 | ``` 562 | let insertIdx = news.indexForInsertingObject(object: singleNews) { (a, b) -> Int in 563 | let newsA = a as! News 564 | let newsB = b as! News 565 | return newsA.compareDate(newsB) 566 | } 567 | 568 | news.insert(singleNews, at: insertIdx) 569 | 570 | ``` 571 | 572 | 这里你需要在 News 这个 model 里实现 compareDate 这个函数。 573 | 574 | ## 总结 575 | 576 | 排序和搜索在 Swift 中的应用场景很多,比如 tableView 中对于 dataSource 的处理。二分搜索是一种十分巧妙和高效的搜索方法,它会经常配合排序出现在各种日常开发中。当然,二分搜索也会出现各种各样的变种,其实万变不离其宗,关键是想方法每次减小一半的搜索范围即可。 577 | 578 | # 推荐👇: 579 | 580 | 如果你想一起进阶,不妨添加一下交流群[**931 542 608**](https://jq.qq.com/?_wv=1027&k=cfVxrMAH) 581 | 582 | 面试题资料或者相关学习资料都在群文件中 进群即可下载! 583 | ![](https://upload-images.jianshu.io/upload_images/22877992-0bfc037cc50cae7d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 584 | -------------------------------------------------------------------------------- /算法基础1-3节.md: -------------------------------------------------------------------------------- 1 | 本章为算法部分,作为对程序员基本功的考察,算法几乎是所有公司、各种水平的程序员都要面对的必考内容。该部分采用 Swift 语言重新审视了多种数据结构和算法原理,可以说是为 iOS 开发者量身打造的算法解答。 2 | 3 | ![](https://upload-images.jianshu.io/upload_images/22877992-750d514b1479a20b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 4 | 5 | 6 | # 1. 基本数据结构 7 | ## 数组 8 | 9 | 数组是最基本的数据结构。在 Swift 中,以前 Objective-C 时代中将 NSMutableArray 和 NSArray 分开的做法,被统一到了唯一的数据结构 —— Array 。虽然看上去就一种数据结构,其实它的实现有三种: 10 | 11 | * ContiguousArray:效率最高,元素分配在连续的内存上。如果数组是值类型(栈上操作),则 Swift 会自动调用 Array 的这种实现;如果注重效率,推荐声明这种类型,尤其是在大量元素是类时,这样做效果会很好。 12 | * Array:会自动桥接到 Objective-C 中的 NSArray 上,如果是值类型,其性能与 ContiguousArray 无差别。 13 | * ArraySlice:它不是一个新的数组,只是一个片段,在内存上与原数组享用同一区域。 14 | 15 | 下面是数组最基本的一些运用。 16 | 17 | ``` 18 | // 声明一个不可修改的数组 19 | let nums = [1, 2, 3] 20 | let nums = [Int](repeating: 0, count: 5) 21 | 22 | // 声明一个可以修改的数组 23 | var nums = [3, 1, 2] 24 | // 增加一个元素 25 | nums.append(4) 26 | // 对原数组进行升序排序 27 | nums.sort() 28 | // 对原数组进行降序排序 29 | nums.sort(by: >) 30 | // 将原数组除了最后一个以外的所有元素赋值给另一个数组 31 | // 注意:nums[0.. { 41 | private var stack: [Element] 42 | var isEmpty: Bool { return stack.isEmpty } 43 | var peek: AnyObject? { return stack.last } 44 | 45 | init() { 46 | stack = [Element]() 47 | } 48 | 49 | mutating func push(_ element: Element) { 50 | stack.append(object) 51 | } 52 | 53 | mutating func pop() -> Element? { 54 | return stack.popLast() 55 | } 56 | } 57 | 58 | // 初始化一个栈 59 | let stack = Stack() 60 | 61 | ``` 62 | 63 | 最后特别强调一个操作:`reserveCapacity()`。它用于为原数组预留空间,防止数组在增加和删除元素时反复申请内存空间或是创建新数组,特别适用于创建和 removeAll() 时候进行调用,为整段代码起到提高性能的作用。 64 | 65 | ## 字典和集合 66 | 67 | 字典和集合(这里专指HashSet)经常被使用的原因在于,查找数据的时间复杂度为 O(1)。 68 | 一般字典和集合要求它们的 Key 都必须遵守 Hashable 协议,Cocoa 中的基本数据类型都 69 | 满足这一点;自定义的 class 需要实现 Hashable,而又因为 Hashable 是对 Equable 的扩展, 70 | 所以还要重载 == 运算符。 71 | 72 | 下面是关于字典和集合的一些实用操作: 73 | 74 | ``` 75 | let primeNums: Set = [3, 5, 7, 11, 13] 76 | let oddNums: Set = [1, 3, 5, 7, 9] 77 | 78 | // 交集、并集、差集 79 | let primeAndOddNum = primeNums.intersection(oddNums) 80 | let primeOrOddNum = primeNums.union(oddNums) 81 | let oddNotPrimNum = oddNums.subtracting(primeNums) 82 | 83 | // 用字典和高阶函数计算字符串中每个字符的出现频率,结果 [“h”:1, “e”:1, “l”:2, “o”:1] 84 | Dictionary("hello".map { ($0, 1) }, uniquingKeysWith: +) 85 | 86 | ``` 87 | 88 | 集合和字典在实战中经常与数组配合使用,请看下面这道算法题: 89 | 90 | > 给一个整型数组和一个目标值,判断数组中是否有两个数字之和等于目标值 91 | 92 | 这道题是传说中经典的 “2Sum”,我们已经有一个数组记为 nums,也有一个目标值记为 target,最后要返回一个 Bool 值。 93 | 94 | 最粗暴的方法就是每次选中一个数,然后遍历整个数组,判断是否有另一个数使两者之和为 target。这种做法时间复杂度为 O(n^2)。 95 | 96 | 采用集合可以优化时间复杂度。在遍历数组的过程中,用集合每次保存当前值。假如集合中已经有了**目标值减去当前值**,则证明在之前的遍历中一定有一个数与当前值之和等于目标值。这种做法时间复杂度为 O(n),代码如下。 97 | 98 | ``` 99 | func twoSum(nums: [Int], _ target: Int) -> Bool { 100 | var set = Set() 101 | 102 | for num in nums { 103 | if set.contains(target - num) { 104 | return true 105 | } 106 | 107 | set.insert(num) 108 | } 109 | 110 | return false 111 | } 112 | 113 | ``` 114 | 115 | 如果把题目稍微修改下,变为 116 | 117 | > 给定一个整型数组中有且仅有两个数字之和等于目标值,求两个数字在数组中的序号 118 | 119 | 思路与上题基本类似,但是为了方便拿到序列号,我们采用字典,时间复杂度依然是 O(n)。代码如下。 120 | 121 | ``` 122 | func twoSum(nums: [Int], _ target: Int) -> [Int] { 123 | var dict = [Int: Int]() 124 | 125 | for (i, num) in nums.enumerated() { 126 | if let lastIndex = dict[target - num] { 127 | return [lastIndex, i] 128 | } else { 129 | dict[num] = i 130 | } 131 | } 132 | 133 | fatalError("No valid output!") 134 | } 135 | 136 | ``` 137 | 138 | ## 字符串和字符 139 | 140 | 字符串在算法实战中极其常见。在 Swift 中,字符串不同于其他语言(包括 Objective-C),它是值类型而非引用类型,它是多个字符构成的序列(并非数组)。首先还是列举一下字符串的通常用法。 141 | 142 | ``` 143 | // 字符串和数字之间的转换 144 | let str = "3" 145 | let num = Int(str) 146 | let number = 3 147 | let string = String(num) 148 | 149 | // 字符串长度 150 | let len = str.count 151 | 152 | // 访问字符串中的单个字符,时间复杂度为O(1) 153 | let char = str[str.index(str.startIndex, offsetBy: n)] 154 | 155 | // 修改字符串 156 | str.remove(at: n) 157 | str.append("c") 158 | str += "hello world" 159 | 160 | // 检测字符串是否是由数字构成 161 | func isStrNum(str: String) -> Bool { 162 | return Int(str) != nil 163 | } 164 | 165 | // 将字符串按字母排序(不考虑大小写) 166 | func sortStr(str: String) -> String { 167 | return String(str.sorted()) 168 | } 169 | 170 | // 判断字符是否为字母 171 | char.isLetter 172 | 173 | // 判断字符是否为数字 174 | char.isNumber 175 | 176 | // 得到字符的 ASCII 数值 177 | char.asciiValue 178 | 179 | ``` 180 | 181 | 关于字符串,我们来一起看一道以前的 Google 面试题。 182 | 183 | > 给一个字符串,将其按照单词顺序进行反转。比如说 s 是 "the sky is blue", 184 | > 那么反转就是 "blue is sky the"。 185 | 186 | 这道题目一看好简单,不就是反转字符串的翻版吗?这种方法有以下两个问题 187 | 188 | * 每个单词长度不一样 189 | * 空格需要特殊处理 190 | 这样一来代码写起来会很繁琐而且容易出错。不如我们先实现一个字符串翻转的方法。 191 | 192 | ``` 193 | fileprivate func reverse(_ chars: inout [T], _ start: Int, _ end: Int) { 194 | var start = start, end = end 195 | 196 | while start < end { 197 | swap(&chars, start, end) 198 | start += 1 199 | end -= 1 200 | } 201 | } 202 | 203 | fileprivate func swap(_ chars: inout [T], _ p: Int, _ q: Int) { 204 | (chars[p], chars[q]) = (chars[q], chars[p]) 205 | } 206 | 207 | ``` 208 | 209 | 有了这个方法,我们就可以实行下面两种字符串翻转: 210 | 211 | * 整个字符串翻转,"the sky is blue" -> "eulb si yks eht" 212 | * 每个单词作为一个字符串单独翻转,"eulb si yks eht" -> "blue is sky the" 213 | 整体思路有了,我们就可以解决这道问题了 214 | 215 | ``` 216 | func reverseWords(s: String?) -> String? { 217 | guard let s = s else { 218 | return nil 219 | } 220 | 221 | var chars = Array(s), start = 0 222 | reverse(&chars, 0, chars.count - 1) 223 | 224 | for i in 0 ..< chars.count { 225 | if i == chars.count - 1 || chars[i + 1] == " " { 226 | reverse(&chars, start, i) 227 | start = i + 2 228 | } 229 | } 230 | 231 | return String(chars) 232 | } 233 | 234 | ``` 235 | 236 | 时间复杂度还是 O(n),整体思路和代码简单很多。 237 | 238 | ## 总结 239 | 240 | 在 Swift 中,数组、字符串、集合以及字典是最基本的数据结构,但是围绕这些数据结构的问题层出不穷。而在日常开发中,它们使用起来也非常高效(栈上运行)和安全(无需顾虑线程问题),因为他们都是值类型。 241 | 242 | # 2. 链表 243 | 244 | 本节我们一起来探讨用 Swift 如何实现链表以及链表相关的技巧。 245 | 246 | ## 基本概念 247 | 248 | 对于链表的概念,实在是基本概念太多,这里不做赘述。我们直接来实现链表节点。 249 | 250 | ``` 251 | class ListNode { 252 | var val: Int 253 | var next: ListNode? 254 | 255 | init(_ val: Int) { 256 | self.val = val 257 | } 258 | } 259 | 260 | ``` 261 | 262 | 有了节点,就可以实现链表了。 263 | 264 | ``` 265 | class LinkedList { 266 | var head: ListNode? 267 | var tail: ListNode? 268 | 269 | // 头插法 270 | func appendToHead(_ val: Int) { 271 | let node = ListNode(val) 272 | 273 | if let _ = head { 274 | node.next = head 275 | } else { 276 | tail = node 277 | } 278 | 279 | head = node 280 | } 281 | 282 | // 头插法 283 | func appendToTail(_ val: Int) { 284 | let node = ListNode(val) 285 | 286 | if let _ = tail { 287 | tail!.next = node 288 | } else { 289 | head = node 290 | } 291 | 292 | tail = node 293 | } 294 | } 295 | 296 | ``` 297 | 298 | 有了上面的基本操作,我们来看如何解决复杂的问题。 299 | 300 | ## Dummy 节点和尾插法 301 | 302 | 话不多说,我们直接先来看下面一道题目。 303 | 304 | > 给一个链表和一个值 x,要求将链表中所有小于 x 的值放到左边,所有大于等于 x 的值放到右边。原链表的节点顺序不能变。 305 | > 306 | > 例:1->5->3->2->4->2,给定x = 3。则我们要返回1->2->2->5->3->4 307 | 308 | 直觉告诉我们,这题要先处理左边(比 x 小的节点),然后再处理右边(比 x 大的节点),最后再把左右两边拼起来。 309 | 310 | 思路有了,再把题目抽象一下,就是要实现这样一个函数: 311 | 312 | ``` 313 | func partition(_ head: ListNode?, _ x: Int) -> ListNode? {} 314 | 315 | ``` 316 | 317 | 即我们有给定链表的头节点,有给定的x值,要求返回新链表的头结点。接下来我们要想:怎么处理左边?怎么处理右边?处理完后怎么拼接? 318 | 319 | 先来看怎么处理左边。我们不妨把这个题目先变简单一点: 320 | 321 | 给一个链表和一个值 x,要求只保留链表中所有小于 x 的值,原链表的节点顺序不能变。 322 | 323 | 例:1->5->3->2->4->2,给定x = 3。则我们要返回 1->2->2 324 | 325 | 我们只要采用**尾插法**,遍历链表,将小于 x 值的节点接入新的链表即可。代码如下: 326 | 327 | ``` 328 | func getLeftList(_ head: ListNode?, _ x: Int) -> ListNode? { 329 | let dummy = ListNode(0) 330 | var pre = dummy, node = head 331 | 332 | while node != nil { 333 | if node!.val < x { 334 | pre.next = node 335 | pre = node! 336 | } 337 | node = node!.next 338 | } 339 | 340 | // 防止构成环 341 | pre.next = nil 342 | return dummy.next 343 | } 344 | 345 | ``` 346 | 347 | > 注意,上面的代码我们引入了 **Dummy** 节点,它的作用就是作为一个虚拟的头前结点。我们引入它的原因是我们**不知道要返回的新链表的头结点是哪一个**,它有可能是原链表的第一个节点,可能在原链表的中间,也可能在最后,甚至可能不存在(nil)。而 Dummy 节点的引入可以巧妙的涵盖所有以上情况,我们可以用 `dummy.next` 方便得返回最终需要的头结点。 348 | 349 | 现在我们解决了左边,右边也是同样处理。接着只要让左边的尾节点指向右边的头结点即可。全部代码如下: 350 | 351 | ``` 352 | func partition(_ head: ListNode?, _ x: Int) -> ListNode? { 353 | // 引入Dummy节点 354 | let prevDummy = ListNode(0), postDummy = ListNode(0) 355 | var prev = prevDummy, post = postDummy 356 | 357 | var node = head 358 | 359 | // 用尾插法处理左边和右边 360 | while node != nil { 361 | if node!.val < x { 362 | prev.next = node 363 | prev = node! 364 | } else { 365 | post.next = node 366 | post = node! 367 | } 368 | node = node!.next 369 | } 370 | 371 | // 防止构成环 372 | post.next = nil 373 | // 左右拼接 374 | prev.next = postDummy.next 375 | 376 | return prevDummy.next 377 | } 378 | 379 | ``` 380 | 381 | 注意这句 `post.next = nil`,这是为了防止链表循环指向构成环,是必须的但是很容易忽略的一步。 382 | 刚才我们提到了环,那么怎么检测链表中是否有环存在呢? 383 | 384 | ![](https://upload-images.jianshu.io/upload_images/22877992-c5d849d0dad5141e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 385 | 386 | ## 快行指针 387 | 388 | 笔者理解快行指针,就是**两个指针访问链表,一个在前一个在后,或者一个移动快另一个移动慢,这就是快行指针**。来看一道简单的面试题: 389 | 390 | > 如何检测一个链表中是否有环? 391 | 392 | 答案是用两个指针同时访问链表,其中一个的速度是另一个的 2 倍,如果他们相等了,那么这个链表就有环了,这就是快行指针的实际使用。代码如下: 393 | 394 | ``` 395 | func hasCycle(_ head: ListNode?) -> Bool { 396 | var slow = head 397 | var fast = head 398 | 399 | while fast != nil && fast!.next != nil { 400 | slow = slow!.next 401 | fast = fast!.next!.next 402 | 403 | if slow === fast { 404 | return true 405 | } 406 | } 407 | 408 | return false 409 | } 410 | 411 | ``` 412 | 413 | 再举一个快行指针一前一后的例子,看下面这道题。 414 | 415 | > 删除链表中倒数第 n 个节点。例:1->2->3->4->5,n = 2。返回1->2->3->5。 416 | > 注意:给定 n 的长度小于等于链表的长度。 417 | 418 | 解题思路依然是快行指针,这次两个指针移动速度相同。但是一开始,第一个指针(指向头结点之前)就落后第二个指针 n 个节点。接着两者同时移动,当第二个移动到尾节点时,第一个节点的下一个节点就是我们要删除的节点。代码如下: 419 | 420 | ``` 421 | func removeNthFromEnd(head: ListNode?, _ n: Int) -> ListNode? { 422 | guard let head = head else { 423 | return nil 424 | } 425 | 426 | let dummy = ListNode(0) 427 | dummy.next = head 428 | var prev: ListNode? = dummy 429 | var post: ListNode? = dummy 430 | 431 | // 设置后一个节点初始位置 432 | for _ in 0 ..< n { 433 | if post == nil { 434 | break 435 | } 436 | post = post!.next 437 | } 438 | 439 | // 同时移动前后节点 440 | while post != nil && post!.next != nil { 441 | prev = prev!.next 442 | post = post!.next 443 | } 444 | 445 | // 删除节点 446 | prev!.next = prev!.next!.next 447 | 448 | return dummy.next 449 | } 450 | 451 | ``` 452 | 453 | 这里还用到了 Dummy 节点,因为有可能我们要删除的是头结点。 454 | 455 | ## 总结 456 | 457 | 这次我们用 Swift 实现了链表的基本结构,并且实战了链表的几个技巧。在结尾处,我还想强调一下 Swift 处理链表问题的两个细节问题: 458 | 459 | * 一定要注意头结点可能就是 nil。所以给定链表,我们要看清楚 head 是不是 optional,在判断是不是要处理这种边界条件。 460 | * 注意每个节点的 next 可能是 nil。如果不为 nil,请用"!"修饰变量。在赋值的时候,也请注意"!"将 optional 节点传给非 optional 节点的情况。 461 | # 3. 栈和队列 462 | 463 | 这期我们来讨论一下栈和队列。在 Swift 中,没有内设的栈和队列,很多扩展库中使用 Generic Type 来实现栈或是队列。正规的做法使用链表来实现,这样可以保证加入和删除的时间复杂度是 O(1)。然而笔者觉得最实用的实现方法是使用数组,因为 Swift 没有现成的链表,而数组又有很多的 API 可以直接使用,非常方便。 464 | 465 | ## 基本概念 466 | 467 | 对于栈来说,我们需要了解以下几点: 468 | 469 | * 栈是**后进先出**的结构。你可以理解成有好几个盘子要垒成一叠,哪个盘子最后叠上去,下次使用的时候它就最先被抽出去。 470 | * 在 iOS 开发中,如果你要在你的 App 中添加撤销操作(比如删除图片,恢复删除图片),那么栈是首选数据结构 471 | * 无论在面试还是写 App 中,只关注栈的这几个基本操作:push, pop, isEmpty, peek, size。 472 | 473 | ``` 474 | protocol Stack { 475 | /// 持有的元素类型 476 | associatedtype Element 477 | 478 | /// 是否为空 479 | var isEmpty: Bool { get } 480 | /// 栈的大小 481 | var size: Int { get } 482 | /// 栈顶元素 483 | var peek: Element? { get } 484 | 485 | /// 进栈 486 | mutating func push(_ newElement: Element) 487 | /// 出栈 488 | mutating func pop() -> Element? 489 | } 490 | 491 | struct IntegerStack: Stack { 492 | typealias Element = Int 493 | 494 | var isEmpty: Bool { return stack.isEmpty } 495 | var size: Int { return stack.count } 496 | var peek: Element? { return stack.last } 497 | 498 | private var stack = [Element]() 499 | 500 | mutating func push(_ newElement: Element) { 501 | stack.append(newElement) 502 | } 503 | 504 | mutating func pop() -> Element? { 505 | return stack.popLast() 506 | } 507 | } 508 | 509 | ``` 510 | 511 | 对于队列来说,我们需要了解以下几点: 512 | 513 | * 队列是**先进先出**的结构。这个正好就像现实生活中排队买票,谁先来排队,谁先买到票。 514 | * iOS 开发中多线程的 GCD 和 NSOperationQueue 就是基于队列实现的。 515 | * 关于队列我们只关注这几个操作:enqueue, dequeue, isEmpty, peek, size。 516 | 517 | ``` 518 | protocol Queue { 519 | /// 持有的元素类型 520 | associatedtype Element 521 | 522 | /// 是否为空 523 | var isEmpty: Bool { get } 524 | /// 队列的大小 525 | var size: Int { get } 526 | /// 队首元素 527 | var peek: Element? { get } 528 | 529 | /// 入队 530 | mutating func enqueue(_ newElement: Element) 531 | /// 出队 532 | mutating func dequeue() -> Element? 533 | } 534 | 535 | struct IntegerQueue: Queue { 536 | typealias Element = Int 537 | 538 | var isEmpty: Bool { return left.isEmpty && right.isEmpty } 539 | var size: Int { return left.count + right.count } 540 | var peek: Element? { return left.isEmpty ? right.first : left.last } 541 | 542 | private var left = [Element]() 543 | private var right = [Element]() 544 | 545 | mutating func enqueue(_ newElement: Element) { 546 | right.append(newElement) 547 | } 548 | 549 | mutating func dequeue() -> Element? { 550 | if left.isEmpty { 551 | left = right.reversed() 552 | right.removeAll() 553 | } 554 | return left.popLast() 555 | } 556 | } 557 | 558 | ``` 559 | 560 | ## 栈和队列互相转化 561 | 562 | 处理栈和队列问题,最经典的一个思路就是使用两个栈/队列来解决问题。也就是说在原栈/队列的基础上,我们用一个协助栈/队列来帮助我们简化算法,这是一种空间换时间的思路。下面是示例代码: 563 | 564 | ``` 565 | // 用栈实现队列 566 | struct MyQueue { 567 | var stackA: Stack 568 | var stackB: Stack 569 | 570 | var isEmpty: Bool { 571 | return stackA.isEmpty 572 | } 573 | 574 | var peek: Any? { 575 | get { 576 | shift() 577 | return stackB.peek 578 | } 579 | } 580 | 581 | var size: Int { 582 | get { 583 | return stackA.size + stackB.size 584 | } 585 | } 586 | 587 | init() { 588 | stackA = Stack() 589 | stackB = Stack() 590 | } 591 | 592 | func enqueue(object: Any) { 593 | stackA.push(object); 594 | } 595 | 596 | func dequeue() -> Any? { 597 | shift() 598 | return stackB.pop(); 599 | } 600 | 601 | fileprivate func shift() { 602 | if stackB.isEmpty { 603 | while !stackA.isEmpty { 604 | stackB.push(stackA.pop()!); 605 | } 606 | } 607 | } 608 | } 609 | 610 | // 用队列实现栈 611 | struct MyStack { 612 | var queueA: Queue 613 | var queueB: Queue 614 | 615 | init() { 616 | queueA = Queue() 617 | queueB = Queue() 618 | } 619 | 620 | var isEmpty: Bool { 621 | return queueA.isEmpty 622 | } 623 | 624 | var peek: Any? { 625 | get { 626 | if isEmpty { 627 | return nil 628 | } 629 | 630 | shift() 631 | let peekObj = queueA.peek 632 | queueB.enqueue(queueA.dequeue()!) 633 | swap() 634 | return peekObj 635 | } 636 | } 637 | 638 | var size: Int { 639 | return queueA.size 640 | } 641 | 642 | func push(object: Any) { 643 | queueA.enqueue(object) 644 | } 645 | 646 | func pop() -> Any? { 647 | if isEmpty { 648 | return nil 649 | } 650 | 651 | shift() 652 | let popObject = queueA.dequeue() 653 | swap() 654 | return popObject 655 | } 656 | 657 | private func shift() { 658 | while queueA.size > 1 { 659 | queueB.enqueue(queueA.dequeue()!) 660 | } 661 | } 662 | 663 | private func swap() { 664 | (queueA, queueB) = (queueB, queueA) 665 | } 666 | } 667 | 668 | ``` 669 | 670 | 上面两种实现方法都是使用两个相同的数据结构,然后将元素由其中一个转向另一个,从而形成一种完全不同的数据。 671 | 672 | ## 面试题实战 673 | 674 | > 给一个文件的绝对路径,将其简化。举个例子,路径是 "/home/",简化后为 "/home";路径是"/a/./b/../../c/",简化后为 "/c"。 675 | 676 | 这是一道 Facebook 的面试题。这道题目其实就是平常在终端里面敲的 cd、pwd 等基本命令所得到的路径。 677 | 678 | 根据常识,我们知道以下规则: 679 | 680 | * “. ” 代表当前路径。比如 “ /a/. ” 实际上就是 “/a”,无论输入多少个 “ . ” 都返回当前目录 681 | * “..”代表上一级目录。比如 “/a/b/.. ” 实际上就是 “ /a”,也就是说先进入 “a” 目录,再进入其下的 “b” 目录,再返回 “b” 目录的上一层,也就是 “a” 目录。 682 | 683 | 然后针对以上信息,我们可以得出以下思路: 684 | 685 | 1. 首先输入是个 String,代表路径。输出要求也是 String, 同样代表路径; 686 | 2. 我们可以把 input 根据 “/” 符号去拆分,比如 "/a/b/./../d/" 就拆成了一个String数组["a", "b", ".", "..", "d"]; 687 | 3. 创立一个栈然后遍历拆分后的 String 数组,对于一般 String ,直接加入到栈中,对于 ".." 那我们就对栈做 pop 操作,其他情况不错处理。 688 | 689 | 思路有了,代码也就有了 690 | 691 | ``` 692 | func simplifyPath(path: String) -> String { 693 | // 用数组来实现栈的功能 694 | var pathStack = [String]() 695 | // 拆分原路径 696 | let paths = path.split(separatedBy: "/") 697 | 698 | for path in paths { 699 | // 对于 "." 我们直接跳过 700 | guard path != "." else { 701 | continue 702 | } 703 | // 对于 ".." 我们使用pop操作 704 | if path == ".." { 705 | if (!pathStack.isEmpty) { 706 | pathStack.removeLast() 707 | } 708 | // 对于太注意空数组的特殊情况 709 | } else if path != "" { 710 | pathStack.append(path) 711 | } 712 | } 713 | // 将栈中的内容转化为优化后的新路径 714 | return "/" + pathStack.joined(separator: "/") 715 | } 716 | 717 | ``` 718 | 719 | 上面代码除了完成了基本思路,还考虑了大量的特殊情况、异常情况。这也是硅谷面试考察的一个方面:面试者思路的严谨,对边界条件的充分考虑,以及代码的风格规范。 720 | 721 | ## 总结 722 | 723 | 在 Swift 中,栈和队列是比较特殊的数据结构,笔者认为最实用的实现和运用方法是利用数组。虽然它们本身比较抽象,却是很多复杂数据结构和 iOS 开发中的功能模块的基础。这也是一个工程师进阶之路理应熟练掌握的两种数据结构。 724 | 725 | # 推荐👇: 726 | 727 | 如果你想一起进阶,不妨添加一下交流群[**931 542 608**](https://jq.qq.com/?_wv=1027&k=cfVxrMAH) 728 | 729 | 面试题资料或者相关学习资料都在群文件中 进群即可下载! 730 | ![](https://upload-images.jianshu.io/upload_images/16899013-6e5da383ff79ac82.png?imageMogr2/auto-orient/strip|imageView2/2/w/596/format/webp) 731 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | --------------------------------------------------------------------------------