《iOS面试之道》两篇总结之一:开发技能篇
《iOS 面试之道》是故胤道长和唐巧 2018 年合著的针对面试一些问题的书,买之后虽有翻阅,但是始终未认真通读。现在想把书中对于我来说有价值的知识点,简短的总结一下。这里分两篇来说,算法基础放到下篇吧。
# 语言工具
# Swfit
# Swift 面试理论题
# 类(Class)和结构体(struct)有什么区别
在 Swift 中,class 是引用类型,struct 是值类型。值类型在传递和赋值时进行复制,引用类型则只会引用对象的一个 “指向”。其实,两个的区别,也是 class、struct 两种类型的区别。
在内存中,引用类型,诸如类,是在堆(Heap)上进行存储和操作的;而值类型,诸如结构体,是在栈上进行存储和操作的。相比栈上操作,堆上操作更加耗时和复杂,所以,苹果公司也推荐使用结构体,可以提高 App 的运行效率。
class 的如下功能是 struct 没有的:
- 可以继承,这样子类可用父类的特性和方法。
- 类型转化可以在运行时检查和解释一个实例的类型。
- 可以用 deinit 来释放资源。
- 一个类可以被多次引用。
struct 也有如下优势:
- 结构较小,适用于复制操作,相比一个 class 被多次引用,struct 更加安全。
- 无需担心内存泄漏或者多线程冲突问题。
# Swift 是面向对象还是函数式编程语言
Swift 即是面向对象的编程语言,也是函数式的编程语言。
说 Swift 是面向对象的语言,因为 Swift 支持类的封装、继承和多态,从这点来说,Swift 和 Java 这类纯面向对象的编程语言几乎毫无差别。
说 Swift 是函数式的编程语言,是因为 Swift 支持 map、reduce、filter、flatmap 这类去除中间状态、数学函数式的方法,更加强调运算结果而不是中间过程。
# 在 Swift 中,什么是可选型
在 Swift 中,可选型是为了表达一个变量值为空的情况。无论变量时值类型,还是引用类型,都可以是可选变量。
在 Objective-C 中没有明确提出可选型的概念,而引用类型可以为 nil,来标识其变量值为空的情况。值类型并不可以。
# 在 Swift 中,什么是泛型(Generics)
在 Swift 中,泛型是为增加代码的灵活性而生的:它可以使对应的代码满足任意类型的变量或方法。
# 说明并比较关键词:open,public,internal,fileprivate 和 private
访问级别从高到低依次为:open > public > internal > fileprivate > private。
它们遵循的基本原则是:高级别的变量不允许被定义为低级别变量的成员变量,反之可以。
public 与 open 的唯一区别在于:它修饰的对象可以在任意 Module 中被访问,但不能重写。
# 说明并比较关键词:strong, weak 和 unowned
Swift 的管理机制与 OC 一样都是 ARC。
unowned 与 weak 本质是一样的,唯一不同的是:对象被释放后,仍然有一个无效的引用指向对象。它不是 Optional,也不指向 nil。如果继续访问,则会引起崩溃。
weak 与 unowned 都可以用来解决循环引用。但是更推荐使用 weak,防止意外引发崩溃。
# 在 Swift 中,如何理解 copy-on-write
当值类型在复制时,复制的对象和原对象实质上在内存中指向同一个对象。当且仅当修改复制的对象时,才会在内存中创建一个新的对象。因此可使得值类型被多次复制而无需耗费太多的内存,只有变化时才会增加开销,使内存的使用更加高效。
另外,可通过下面方式简单查看对象内存地址:
1 | var arrA = [1, 2, 3] |
值类型每次操作后,如赋值、修改等,其内存地址都会改变。
# 什么是属性观察(Property Observer)
在 Swift 中,属性观察器,即 didSet 和 willSet。
初始化方法的设定、以及在 willSet、didSet 中对属性的再次设定、属性销毁时,都不会触发调用属性观察。
# Swift 面试实战题
# 在结构体中如何修改成员变量
使用 mutating 关键字。
另外,如果设计协议时,协议需要被值类型实现,则需要考虑是否给协议方法或者属性添加关键字 mutating。
# 在 Swift 中如何实现或(||)操作
1 | func ||(left: Bool, right: @autoclosure () -> Bool) -> Bool { |
只有左侧为假时,才计算右侧,防止不必要的计算开销。
# 实现一个函数:输入任意一个整数,输出为输入的整数 + 2
这个函数主要考察柯里化。Swift 的柯里化特性是函数式编程思想的体现。
1 | func add(_ num: Int) ->(Int) -> Int { |
# 实现一个函数:求 0 ~ 100(包括 0 和 100)中为偶数并且恰好是其他数字平方的数字
考察函数式编程思想。如下:
1 | let arr = (0 ... 10).map { $0 * $0 }.filter { $0 % 2 == 0} |
# Objective-C
# Objective-C 面试理论题
# 什么是 ARC
自动引用计数,更多详细信息可看:《Objective-C 高级编程》三篇总结之一:引用计数篇
# 什么情况下会出现循环引用
多个对象相互强引用,导致无法释放,造成内存泄漏。
可使用 weak 或者 __block 来解决循环引用。
Xcode 中的 Debug Memory Graph 可检查内存泄漏。
# 说明并比较关键字:strong, weak, assign 和 copy
在上面那篇文章中有过详细说明。这里再补充几点:
- weak 一般用来修饰对象,assign 用来修饰基本数据类型。因为 assign 修饰的对象被释放后,指针地址依然存在,造成 “野指针”,在堆上容易造成崩溃。而栈上的内存系统会自动处理,不会造成 “野指针”。
- 在 Objective-C 中,基本数据类型的默认关键字是 atomic、readwrite 和 assign;普通属性的默认关键字是 atomic、readwrite 和 strong。
# 说明并比较关键字:atomic 和 nonatomic
- atomic 修饰的对象会保证 getter 和 setter 的完整性,任何线程访问它都可以得到一个完整的初始化对象,因为要保证操作完成,所以速度比较慢。atomic 比 nonatomic 安全,但也不是绝对的线程安全,例如多个线程同时调用 get 和 set 时,就会导致获得的对象值不一致。想要线程绝对安全,就要用 @synthesize。
- nonatomic 修饰的对象不保证 getter 和 setter 的完整性,所以当多个线程访问它时,它可能返回未初始化的对象。正因为如此,nonatomic 比 atomic 速度快,但是线程也是不安全的。
# 说明并比较关键字:@property, @synthesize, @dynamic
参考: iOS - @property 与 @synthesize 与 @dynamic
上面文章说的非常详细,这里我只做个简单总结。
@property:
- @property 是声明属性的语法。被 @property 声明的属性,系统已经自动生成了实例变量,即下划线变量。
- 如果对 @property 声明的属性单独重写了 setter 或者 getter 方法,都可以使用该属性的实例变量。一旦同时重写了 setter 和 getter 方法,再使用实例变量时就会报错,此时需要使用 @synthesize。如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18@interface ViewController ()
@property(nonatomic, copy) NSString *name;
@end
@implementation ViewController
@synthesize name = _name;
- (NSString *)name {
if (_name == nil) {
}
return _name;
}
- (void)setName:(NSString *)name {
_name = name;
}
@end
@synthesize:
- @synthesize 为属性添加一个实例变量名,或者说别名。同时会为该属性生成默认的 setter 和 getter 方法。
- 如果属性手动已经实现了自己的 setter 和 getter 方法,可以使用 @dynamic 来阻止 @synthesize 自动生成的 setter/getter 覆盖。
- 当在协议 Protocol 中声明属性时,协议中声明的属性不会自动生成 setter 和 getter,需要使用 @synthesize 生成 getter 和 setter。
- @property 声明的属性有两个对应的词,一个是 @synthesize,一个是 @dynamic。如果 @synthesize 和 @dynamic 都没写,默认是 @synthesize var = _var;
- 如果我们同时写了 getter 和 setter 方法,就需要在 .m 文件中使用 @synthesize。
@dynamic:
- @dynamic 告诉编译器:该属性的 setter 和 getter 方法已由用户自己实现,不自动生成。
- 加入一个属性被 @dynamic 修饰,但是开发者并没有提供 setter 和 getter 方法,编译时候没有问题,一旦在运行过程中,访问到该属性、或者修改该属性时,都会因为缺少 setter 或者 getter 方法而引发崩溃。
- 编译时没问题,运行时才执行相应的方法,这就是所谓的动态绑定。
# RunLoop 和线程有什么关系
详细可参考:Runloop 分析
RunLoop 是每一个线程一直运行的一个对象,它主要用来负责响应需要处理的各种事件和消息。每个线程都有且仅有一个 RunLoop 与其对应,没有线程,就没有 RunLoop。
在所有线程中,只有主线程的 RunLoop 是默认启动的,main 函数会设置一个 NSRunLoop 对象。而其他线程的 RunLoop 是默认没有启动的,可以通过 [NSRunLoop currentRunLoop]
来启动。
# 说明并比较关键词:__weak 和 __block
详细可参考:《Objective-C 高级编程》三篇总结之二:Block 篇
- __weak 与 weak 基本相同,前者修饰变量,后者修饰属性。__weak 主要用于防止 Block 中的循环引用。
- __block 也用于修饰变量。它是引用修饰,所以其修饰的值是动态变化的,可以被重新赋值。
# 什么是 Block?它和代理的区别是什么
Block 是带有自动变量的匿名函数。详细可参考 《Objective-C 高级编程》三篇总结之二:Block 篇。
这里再简单总结下它们的主要区别:
Block 和代理的首要区别在于 Block 集中代码块,而代理分散代码块。所以 Block 更适合轻便、简单的回调。如网络传输。而代理适用公共接口较多的情况。这样做也更易于解耦代码结构。
两者的另一个区别在于,Block 运行成本高。Block 出栈时,需要将使用的数据从栈内存复制到堆内存,如果是对象,则引用计数 +1,使用完或者 Block 置为 nil 后才消除。delegate 只是保留了一个对象指针,直接回调,并没有额外消耗。并且 Block 更易造成循环引用。
# Objective-C 面试实战题
# 属性声明代码风格考查
具有 mutable 的对象应该用 copy 修饰,防止被动态修改。应该多用 NSInteger、CGFloat 等。
另外,如果可变类型如 NSMutableString 用 copy 修饰,那么对其修改时,程序会崩溃,报错:
1 | [NSTaggedPointerString appendString:]: unrecognized selector sent to instance 0x824f62a252296794 |
# 架构解耦代码考查
OC 的 enum 应该带有 全名 + case 名,方便与 Swift 混编,如 SexBoy。
Model 应与 View 划清界限。
# 内存管理语法考查
1 | NSString *fir = @"Hello"; |
输出:
1 | 2019-12-25 11:46:44.336661+0800 GCD[58834:2265032] fir 内存地址:0x108ddc2c8 sec 内存地址:0x108ddc2c8 |
内存地址相同。字符串存在数据区。
# 多线程语法考查
视图刷新放到主线程。
吐槽一下,这本书对于这些知识点说明的真的是简单到令人发指啊!!!
# RunLoop Timer
滑动时,ScrollView 视图上的 timer 停止,这里有两种方案解决:
- 将 timer 加到 NSRunLoopCommonModes 中。
- 将 timer 放到另一个线程中,并开启另一个线程的 RunLoop。
示例如下:
1 | // 方法1 |
# Swift VS Objective-C
Swift 是静态类型语言,Objective-C 是动态类型语言。
这小节从数据结构、编程思路和语言特性三点来作对比。
# Swift 为什么将 String、Array 和 Dictionary 设计成值类型
首要要知道,在 OC 中,这三个都被设计成了引用类型。
- 值类型相比引用类型,最大的优势就是可以高效的使用内存。值类型在栈上操作,引用类型通常在堆上操作。栈上的操作仅仅是单个指针的上下移动,而堆上的操作则牵涉到合并、移位、重新链接等。也就是说,Swift 这么设计大幅度减少了堆上的内存分配和回收的次数。同时,copy-on-write 又将值传递和复制到开销降到最低。
- Swift 将它们设计成值类型也是为了线程安全。通过 Swift 的 let 设置,是得这些数据达到真正意义上的 “不变”,也从根本上解决了多线程众内存访问和操作顺序的问题。
- Swift 将它们设计成值类型可以提高 API 的灵活度。譬如添加协议等,对数据进行操作等。
# 如何用 Swift 将协议 Protocol 中部分方法设计成可选 optional
一共有两种方案:
- 在协议和方法前均加上 @objc 关键字,然后在可选方法前加上 optional 关键字。该方案实际上是把协议转化为 OC 的方式,然后进行分可选定义。
- 用扩展 extension 来规定可选方法。
# 协议的代码实战
记着一点就行,weak 只能为引用类型提供内存管理。所以协议有时候需要继承自 class。
# 编程思路
# 混编时,方法如何互调?
- Swift 调用 OC 方法时,使用 bridging 桥接头文件。
- OC 调用 Swift ,则导入 Swift 文件生成的头文件。Swift 文件中对外暴露的属性或方法需加上 @objc 关键字。
# 比较 Swift 和 Objective-C 中初始化方法 init 有什么异同
一言以蔽之,Swift 中初始化方法更加严谨和准确。
- 在 Objective-C 中,初始化方法无法保证所有成员变量都完成初始化;编译器对属性设置并无警告,但是实际操作会出现初始化不完全的情况。初始化方法和普通方法并无差异,可以多次调用。
- 在 Swift 中,初始化方法必须保证所有非 optional 的成员变量都完成初始化;同时,新增 convenience 和 required 两个修饰初始化方法的关键词。convenience 只是提供了一种方便的初始化方法(便利构造器),必须通过调用同一个类中的 designated 初始化方法(指定构造器)来完成。required 是强制子类重写父类中所修饰的初始化方法。
# 比较 Swift 和 Objective-C 中的协议有什么异同
相同点:都可以被用作代理。在实际开发中多用于适配器模式(Adapter Pattern)。
不同点:Swift 中的 protocol 还可以对接口进行抽象,例如 Sequence,配合扩展、泛型、关联类型等实现面向协议编程,从而大大提高代码灵活性。同时,Swift 中的 protocol 还能用于值类型,如结构体和枚举。
# 语言特性
# 谈谈对 Objective-C 和 Swift 对动态性的理解
runtime 其实就是 Objective-C 语言的动态机制。runtime 执行的是编译后的代码,这时它可以动态的添加对象、添加方法、修改属性、传递信息等。具体过程是,在 Objective-C 中,对象调用方法时,如 [self.tableview reload];
,经历了两个阶段:
- 编译阶段:编译器(compiler)会把这句话翻译成
objc_msgSend(self.tableview, @selector(reload)
,把消息发送给 self.tableview。 - 运行阶段:接收者 self.tableview 会响应这个消息,其间可能直接执行、转发消息,也可能找不到方法而导致程序崩溃。崩溃过程以及预防措施,可参考 iOS:消息转发机制、响应者链、App 启动前后。
所以,整个流程是:编译器翻译 -> 给接收者发送消息 -> 接收者响应消息。
其中,接收者如何响应消息,就发生在运行时(runtime)。runtime 的运行时机制就是 Objective-C 的语言特性。
Swift 目前被公认为一门静态语言。它的动态特性都是通过桥接 OC 来实现的。
# 语言特性的代码实战
这里牵扯到协议的派发。通下下面一段代码来说明:
1 | protocol Chef { |
输出:
1 | Cook Seafood |
在 Swift 中,协议中是动态派发的,而扩展中则是静态派发的。也就是说,协议中如果有方法声明,那么方法会根据对象的实际类型进行调用。
如果上述代码中,将协议中 func makeFood()
方法删除,则输出变为:
1 | Make Food |
因为协议中没有声明 makeFood () 方法,所以此时需要按照扩展中的协议静态派发。也就是说,会根据对象的声明类型进行调用,而非实际类型。
# message send 如果找不到对象,则会如何进行后续处理
message send 找不到对象分两种情况:对象为空(nil);对象不为空,却找不到对应的方法。
- 对象为空时,Objective-C 向 nil 发送消息是有效的,在 runtime 中不会产生任何效果。
- 对象不为空,却找不到对应的方法时,程序运行异常,引发 unrecognized selector 错误。
# 什么是 method swizzling
每个类都维护一个方法列表,其中方法名与其实现是一一对应的关系,即 SEL (方法名) 和 IMP (指向实现的指针) 的对应关系。method swizzling 可以在 runtime 期间将 SEL 和 IMP 进行更换。更换时需注意:
- 方法交换应该保证唯一性和原子性。唯一性是指应该尽可能的在 +load () 方法中实现,这样可以保证方法一定会被调用且不会异常。原子性是指需要使用 dispatch_once 来执行方法交换,这样可以保证只运行一次。
- 不要轻易使用 method swizzling。因为动态交换方法的实现并没有编译器的安全保障,可能会在运行时造成奇怪的问题。
# Swift 和 Objective-C 的自省(Introspection)有什么不同
自省在 Objective-C 中就是:判断一个对象是否属于某个类的操作、它有以下两种形式:
1 | [obj isKindOfClass:[SomeClass class]]; |
isKindOfClass 用来判断 obj 是否是 SomeClass 或其子类。isMemberOfClass 用来判断 obj 是否就是 SomeClass(非子类)的实例对象。这两个方法都有个前提:obj 必须是 NSObject 或其子类。
在 Swift 中,由于很多类型并非继承自 NSObject,所以通常用 is 函数来进行判断,相当于 isKindOfClass。is 函数可同时用于值类型和引用类型。
另外,自省通常与动态类型一起使用。动态类型就是 id 类型。
# 能够通过 Category 给已有类添加属性 property
不论对 OC 还是 Swift,都可以添加。如:
1 | private var middleKey: Void? |
setter 方法使用 objc_setAssociatedObject,getter 方法使用 objc_getAssociatedObject 即可。
# Xcode 使用
# Xcode 调试
# LLDB 中 p 和 po 有什么区别
- p 是 expr- 的缩写。它的工作是把收到的参数在当前环境下进行编译,然后打印出对应的值。
- po 即 expr-o-。会打印出比 p 更加详细的内容。
# 分析与优化
# App 启动时间过长,该怎样优化
iOS:消息转发机制、响应者链、App 启动前后。这篇文章里也说过这个问题。这里只重复说一点:
通过添加环境变量可以打印出 App 的启动时间分析:
Edit scheme -> Run -> Arguments -> Environment Variables:
- 添加:DYLD_PRINT_STATISTICS,设置为 1。
- 如果需要更详细的信息,那就添加:DYLD_PRINT_STATISTICS_DETAILS,设置为 1。
# 如何用 Xcode 检测代码中的循环引用
两种方案:
- Xcode 调试工具栏中的 Memory Debug Graph 工具。
- Instruments 里面的 leak,一个专门检测内存泄露的工具。
# 怎样解决 EXC_BAD_ACCESS
产生 EXC_BAD_ACCESS 的主要原因就是访问了已经释放的对象,或者访问他们已经释放了的成员变量或者方法,解决方法主要有以下几种:
- 设置全局断点,快速定位缺陷所在:这种方法效果一般。
- 重写 Object 的 repondsToSelector 方法:这种方法效果一般,并且要在每个 class 进行定点排查,并不推荐。
- 使用 Zombie 和 Address Sanitizer: 可以在绝大部分情况下定位到问题代码。开启方式:Edit scheme -> Run -> Diagnostics -> Address Sanitizer 和 Zombie Objects 选项。
# Playground 技巧
# 如何在 Playground 中执行异步操作
要让 Playground 具备延时运行的特性,可以在 Playground 文件中加入以下代码:
1 | import PlaygroundSupport |
# Playground 可视化
导入头文件即可:
1 | import UIKit |
# 系统框架
# UI 相关知识
# UI 控件和基本布局
# stroyboard/xib 和纯代码构建的 UI 相比,有哪些优点和缺点
优点:
- 简单直接快速。
- 跳转关系清除
缺点:
- 多人协作易冲突。
- 很难做到界面继承和重用。
- 不便进行模块化管理。
- 影响性能。
PS. 吐个槽,我自己经常用 xib,看到这里的对比,,,自己看着办吧。
# Auto Layout 和 Frame 在 UI 布局和渲染上有什么区别
- Auto Layout 是针对多尺寸屏幕的设计。其本质是通过线性不等式设置 UI 控件的相对位置,从而适配多种屏幕尺寸。
- Frame 是基于 X、Y 坐标轴的布局机制。是开发中最底层、最基本的页面布局。
- Auto Layout 的性能比 Frame 差很多。(其实最近两年 Xcode 升级对这个已经进行过大幅度优化了)。
- 优化 Auto Layout 的方案是减少视图层级,减少计算量,缓存计算结果等。
# UIView 和 CALayer 有什么区别
- UIView 和 CALayer 都是 UI 操作的对象。两者都是 NSObject 的子类,发生在 UIView 上的操作,本质上发生在对应的 CALayer 上。
- UIView 是 CALayer 用户交互的对象。UIView 是 UIResponder 的子类,其中提供了很多 CALayer 所没有的交互上的接口,主要负责处理用户触发的各种操作。
- CALayer 在图像和动画渲染上性能更好。这是因为 UIView 有冗余的交互接口,而且相比 CALayer,有层级之分。CALayer 无需处理交互时进行渲染,可以节省大量时间。
# 说明比较关键词: frame,bounds 和 center
- frame 是指当前视图(View)相对于父视图的平面坐标系统中的位置和大小。
- bounds 是指当前视图相对于自己的平面坐标系统中的位置和大小。
- center 是一个 CGPoint,指当前视图在父视图的平面坐标系统中,中间的位置。
另外,frame 和 bounds 的 size 并非一直相等,如下:
1 | let oneV = UIView(frame: CGRect(x: 30, y: 100, width: 100, height: 180)) |
输出:
1 | oneV frame = (-18.520467338700882, 136.31870596839138, 197.04093467740176, 107.36258806321723) |
# 说明并比较方法:layoutIfNeeded,layoutSubviews 和 setNeedsLayout
- layoutIfNeeded 一旦被调用,主线程会立即强制重新布局,它从当前视图开始,一直到完成所有子视图的布局。
- layoutSubviews 用来自定义视图尺寸。它是系统自动调用的,开发者不能手动调用。我们能做的就是重写该方法,让系统调整尺寸时按照我们期望的效果进行布局。这个方法主要用在屏幕旋转、滑动或者触摸界面、修改子视图时被触发。
- setNeedsLayout 与 layoutIfNeeded 非常相似,唯一不同就是它不会立即刷新布局,而是在下一个布局周期才会触发刷新。
# 说明并比较关键词: Safe Area, SafeAreaLayoutGuide 和 SafeAreaInsets
由于 iPhone X 采用了全新的 “刘海” 设计,所以 iOS11 中引入了安全区域(Safe Area)的概念。
- Safe Area 是指 App 合理显示内容的区域。它不包括 status bar, navigation bar, tab bar 和 tool bar 等。在 iPhoneX 系列中,一般是指扣除了顶部的 status bar(高度 44 像素)和底部的 home indicator(高度 34 像素)的区域。
- SafeAreaLayoutGuide 是指 Safe Area 的区域范围和限制。在布局设置中可取其上下左右进行设置。
- SafeAreaInsets 限定了 Safe Area 区域和整个屏幕之间的布局关系。
# 动画
# iOS 中动画实现方式有几种
主要有以下三种:
- UIView Animation 可以实现基于 UIView 的简单动画。它是 CALayer Animation 的封装。它实现的动画无法回撤、暂定、与手势交互。
- CALayer Animation 是在更底层 CALayer 上的动画接口。可以实现 UIView Animation 以及更多自定义效果。支持动画的回撤、暂停与手势交互。
- UIViewPropertyAnimator 是 iOS10 中引入的处理交互式动画的接口。相比 UIView Animation,更加方便,且支持手势交互。
# 控制屏幕上小球,使其水平右移 200 个 point
嗯,,,真的问了这个问题,就要追问更多的细节再去编码。实现方案就是动画位移。
# 多任务开发
# 在 iOS 开发中,如何保证 App 的 UI 在 iPhone、iPad 以及 iPad 分屏情况下依然适用
为适应各种机型,苹果公司在 iOS8 中引入了 Adaptive UI 的概念,需注意以下几点:
- 采用 Auto Layout。
- 采用 Size Class。
- 关注多屏情况。
# 如何用 drag & drop 实现图片拖动功能
iOS11 中最新引入的 Drag and Drop 功能。
# UIScrollView 及其子类
# UIScrollView 及其子类理论面试题
# 说明并比较关键词:contentView, contentInset, contentSize 和 contentOffset
- UIScrollView 上显示内容的区域被称为 contentView。
- contentInset 是指 contentView 与 UIScrollView 的边界。具体属性包括 top、bottom、left 和 right 四个。
- contentSize 值 contentView 的大小。
- contentOffset 是指当前 contentView 浏览位置左上角点的坐标。它是相对于整个 UIScrollView 左上角为原点而言的。
# 说明 UITableViewCell 的重用机制
相同类型的 UITableViewCell 标记为相同的 Identifier,然后用 reuseIdentifier 进行构建。不用重复生成新的 Cell。
# 说明并比较协议 UITableViewDataSource 和 UITableviewDelegate
- UITableViewDataSource 用来管控 UITableView 的实际数据。例如多少行、每行多高等。
- UITableviewDelegate 用来处理 UITableView 的 UI 交互,如设置 header 和 footer、点击、推动、删除等。
# 说明并比较协议:UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout
- UICollectionViewDataSource 管控 UICollectionView 的实际数据。
- UICollectionViewDelegate 用来处理交互。
- UICollectionViewDelegateFlowLayout 用来处理 UICollectionView 的布局及其行为,如滚动方向。
# UICollectionView 中的 Supplementary Views 和 Decoration Views 分别指什么
Cells,Supplementary Views 和 Decoration Views 共同构成了整个 UICollectionView 的视图。 Cells 是最基本的,并且必须由用户实现和配置。而 Supplementary Views 和 Decoration Views 有默认实现,用来美化 UICollectionView。
# 优化进阶
# 如果一个列表视图滑动很慢,那么该怎样优化
遇到此问题,第一步要分析原因。列表视图滑动不流畅,肯定是 UI 或者数据除了问题,可能的原因是:
- 类表渲染时间较长。可能因为某些 UI 控件比较复杂,或者图层过多。
- 界面渲染延后。可能是大量的操作或者耗时计算阻塞的主线程。
- 数据源问题。可能因为网络请求太慢,不能及时得到响应数据。也有可能需要数据太多,主线程不能及时处理。
针对上面三个问题,分别优化:
- 对于第一个问题,首先检查 Cell 是否复用,是否有复杂图层,也可使用惰性加载来推迟创建时间。也可采用 Facebook 推出的 ComponentKit 进行优化。
- 对于第二个问题,可采用 GCD 将耗时操作放到子线程处理,并进行缓存。如果 LinkedIn 推出的 LayoutKit 就是很好的例子。
- 对于第三个问题,可以缓存后端数据,或者和后端协调优化网络请求。
对于界面渲染和优化,Facebook 和 Pinterest 维护的 ASDK 是目前功能最全、效果最好、使用最广的第三方解决方案。
# 说一说实现预加载的方法
即滑动过程中请求新的数据。简单实现方案如下:
1 | func scrollViewDidScroll(_ scrollView: UIScrollView) { |
也可以参考 ASDK 做更进一步的优化。
# 如何用 UICollectionView 实现瀑布流界面
创建一个 UICollectionViewlayout 的子类,并对以下四个属性或方法一一设定:
- collectionViewContentSize,瀑布流的尺寸变化,必然要重写这个属性。
- prepare ()。该方法发生在 UICollectionView 数据准备好,但界面还未布局之时。在这里进行位置计算。
- layoutAttributesForElements (in:)。prepare () 完成布局后,调用该方法,决定展示哪些 item。
- layoutAttributesForItem (at:)。该方法对每一个 item 设定 layoutAttributes。
实现一个瀑布流需要复杂的计算和测试,这里仅仅是提供思路。
# 网络、推送与数据处理
# 网络、推送与数据处理相关理论
# 说一下 HTTP 中 GET 和 POST 的区别
- 从方向上看,GET 是从服务器获取信息的,POST 是想服务器发送信息的。实质上,他们都能够获取、发送信息。
- 从类型看,GET 处理静态和动态内容,POST 只处理动态内容。
- 从参数位置看,GET 参数才 URI 里,POST 参数在其包体里。从这个角度来看,POST 比 GET 更安全、隐秘。
- GET 可以被缓存,可以被存储在浏览器的浏览历史里,其内容理论上有长度限制,而 POST 在这三方面恰恰相反。
PS. 感觉这里说的宽泛而不严谨,关于 HTPP 更多信息,可以参考书籍《图解 HTTP》。
# 说一说 Session 和 Cookie 的概念
- Session 是服务器用来认证、追踪用户的数据结构。它通过判断客户端传来的消息确定用户。确定用户的唯一标志就是客户端传来的 Session ID。
- Cookie 是客户端用来保存用户信息的机制。初次会话时,HTTP 协议会在 Cookie 里记录一个 Session ID,之后每次会话都把 Session ID 发给服务器。
- Session 一般用于用户验证。它默认存储在服务器的一个文件里,当然也可以存储在内存、数据库里。
- 若客户端禁用了 Cookie,则客户端会用 URL 重写技术,即绘画板时在 URL 的末尾加上 Session ID,并发送给服务器。
# 说明并比较网络通信协议:Ajax Polling, Long Polling, WebSockets 和 Server-Sent Event。
。。。
# 在一个 HTTPS 连接的网站中,输入账号和密码,并单击登录按钮后,到服务器返回这个请求前,这期间经历了什么
具体经历以下 8 步:
- 客户端打包请求。其中包括 URL、端口、账号密码等。注意,HTTPS,即 HTTP + SSL/TLS,在 HTTP 上又加了一层处理加密信息的模板。这个过程相当于客户端请求钥匙。
- 服务端接受请求。这个过程中,DNS 把网络地址解析成 IP 地址,在寻找到对应的计算机。这个过程相当于服务器分析是否要向客户端发送钥匙模板。
- 服务器返回数字证书。这个过程相当于服务器想客户端发送钥匙模板。
- 客户端生成加密信息。此时信息已被加密,这个过程相当客户端生成钥匙并锁上请求。
- 客户端发送加密信息。即客户端发送请求。
- 服务器加锁加密信息。
- 服务器向客户端返回信息。
- 客户端解锁返回信息。
# iOS 网络请求
# 说明并比较类:URLSessionTask, URLSessionDataTask, URLSessionUploadTask, URLSessionDownloadTask
- URLSessionTask 是一个抽象类。通过实现它,可以实现网络的任务传输任务。诸如请求、上传、下载任务。它的取消、继续、暂停方法有默认实现。
- URLSessionDataTask 负责 HTTP GET 请求,一般用户获取服务器数据。
- URLSessionUploadTask 负责 HTTP POST/PUT 请求,一般用于上传数据。
- URLSessionDownloadTask 负责下载数据,如断点下载功能。
# 什么是 Completion Handler
Completion Handler 一般用于处理 API 请求后的返回数据。
# 消息推送
# 在 iOS 开发中,本地消息推送的流程是怎样的
UserNotification 框架是针对远程和本队消息的框架,其流程主要有以下 4 步:
- 注册。通过调用 requestAuthiruzatuion,让用户在 Alert 中进行选择。
- 创建。
- 推送。
- 响应。
远程推送与本地推送的差异在于第二步,推送信息的创建。
# 说一说 iOS 开发中,远程推送的原理
这个问题主要是理清 iOS 系统、App、APNs 服务器以及 App 对应的客户端的关系,主要包括以下几方面:
- App 向 iOS 系统申请远程推送消息的权限。这与本地消息推送的注册是一致的。
- iOS 系统想 APNs 服务器请求手机端的 deviceToken,并告诉 App,允许接收推送的通知。
- App 接收到手机端的 deviceToken。
- App 将收到的 deviceToken 传给 App 的服务器端。
- 远程消息由 App 对应的服务器端产生吗,它会先经过 APNs 服务器。
- APNs 服务器将远程通知推送给响应的手机。
- 根据对应的 deviceToken,通知会推送到指定的手机。
# 数据处理
# 在 iOS 开发中,如何实现编码和解码
在 Swift4 中,编码和解码引入了 Encodable 和 Decodable 这两个协议,而 Codable 是这两个协议的合集,在 Swift 中,Enum、Struct 和 Class 都支持 Codable。
# 说一说 iOS 开发中数据持久化方案
- Plist。一般用于保存 App 的基本参数。
- Preference。即使用 UserDefaults 来保存,本质是相关数据保存到同一个 plist 文件下。
- NSKeyedArchiver。序列化方案,即归档和解档。
- CoreData。以上三种都是覆盖存储。CoreData 则是数据库存储,此外还有 SQLite3、FMDB、Realm 等。
# 并发编程
# 在 iOS 开发中,并发操作有哪 3 种方式
- NSThread: 可以最大限度的掌握每一个线程的生命周期。但需要开发者手段管理以及加锁操作。使用场景较小,基本是在开发底层的开源软件或者测试时调用。
- GCD(Grand Central Dispatch): 苹果公司推荐,为了追求高效处理大量并发数据。
- Operation: 与 GCD 类似,但是更加灵活。
# 比较关键词: Serial、Concurrent、Sync、Async
串行、并行、同步、异步,更多详细信息,可参考 《Objective-C 高级编程》三篇总结之三:GCD 篇。
另外,并发编程三大问题、GCD 信号量、栅栏等,都可参考这篇文章。