《iOS面试之道》两篇总结之一:开发技能篇

《iOS 面试之道》是故胤道长和唐巧 2018 年合著的针对面试一些问题的书,买之后虽有翻阅,但是始终未认真通读。现在想把书中对于我来说有价值的知识点,简短的总结一下。这里分两篇来说,算法基础放到下篇吧。

《iOS 面试之道》两篇总结之一:开发技能篇

《iOS 面试之道》两篇总结之二:算法基础篇

# 语言工具

# 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
2
var arrA = [1, 2, 3]
print("arrA 地址:", String(format: "%p", arrA));

值类型每次操作后,如赋值、修改等,其内存地址都会改变。

# 什么是属性观察(Property Observer)

在 Swift 中,属性观察器,即 didSet 和 willSet。

初始化方法的设定、以及在 willSet、didSet 中对属性的再次设定、属性销毁时,都不会触发调用属性观察。

# Swift 面试实战题

# 在结构体中如何修改成员变量

使用 mutating 关键字。

另外,如果设计协议时,协议需要被值类型实现,则需要考虑是否给协议方法或者属性添加关键字 mutating。

# 在 Swift 中如何实现或(||)操作
1
2
3
4
5
6
7
func ||(left: Bool, right: @autoclosure () -> Bool) -> Bool {
if left {
return true
} else {
return right()
}
}

只有左侧为假时,才计算右侧,防止不必要的计算开销。

# 实现一个函数:输入任意一个整数,输出为输入的整数 + 2

这个函数主要考察柯里化。Swift 的柯里化特性是函数式编程思想的体现。

1
2
3
4
5
func add(_ num: Int) ->(Int) -> Int {
return { val in
return num + val
}
}
# 实现一个函数:求 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:

  1. @property 是声明属性的语法。被 @property 声明的属性,系统已经自动生成了实例变量,即下划线变量。
  2. 如果对 @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:

  1. @synthesize 为属性添加一个实例变量名,或者说别名。同时会为该属性生成默认的 setter 和 getter 方法。
  2. 如果属性手动已经实现了自己的 setter 和 getter 方法,可以使用 @dynamic 来阻止 @synthesize 自动生成的 setter/getter 覆盖。
  3. 当在协议 Protocol 中声明属性时,协议中声明的属性不会自动生成 setter 和 getter,需要使用 @synthesize 生成 getter 和 setter。
  4. @property 声明的属性有两个对应的词,一个是 @synthesize,一个是 @dynamic。如果 @synthesize 和 @dynamic 都没写,默认是 @synthesize var = _var;
  5. 如果我们同时写了 getter 和 setter 方法,就需要在 .m 文件中使用 @synthesize。

@dynamic:

  1. @dynamic 告诉编译器:该属性的 setter 和 getter 方法已由用户自己实现,不自动生成。
  2. 加入一个属性被 @dynamic 修饰,但是开发者并没有提供 setter 和 getter 方法,编译时候没有问题,一旦在运行过程中,访问到该属性、或者修改该属性时,都会因为缺少 setter 或者 getter 方法而引发崩溃。
  3. 编译时没问题,运行时才执行相应的方法,这就是所谓的动态绑定。
# 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
2
3
4
5
6
7
8
9
10
11
12
13
14
NSString *fir = @"Hello";
NSString *sec = @"Hello";
NSLog(@"fir 内存地址:%p sec 内存地址:%p", fir,sec);
if (fir == sec) {
NSLog(@"fir == sec");
} else {
NSLog(@"fir != sec");
}

if ([fir isEqualToString:sec]) {
NSLog(@"[fir isEqualToString:sec] == YES");
} else {
NSLog(@"[fir isEqualToString:sec] == NO");
}

输出:

1
2
3
2019-12-25 11:46:44.336661+0800 GCD[58834:2265032] fir 内存地址:0x108ddc2c8   sec 内存地址:0x108ddc2c8
2019-12-25 11:46:44.337619+0800 GCD[58834:2265032] fir == sec
2019-12-25 11:46:44.348583+0800 GCD[58834:2265032] [fir isEqualToString:sec] == YES

内存地址相同。字符串存在数据区。

# 多线程语法考查

视图刷新放到主线程。

吐槽一下,这本书对于这些知识点说明的真的是简单到令人发指啊!!!

# RunLoop Timer

滑动时,ScrollView 视图上的 timer 停止,这里有两种方案解决:

  1. 将 timer 加到 NSRunLoopCommonModes 中。
  2. 将 timer 放到另一个线程中,并开启另一个线程的 RunLoop。

示例如下:

1
2
3
4
5
6
7
// 方法1
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

// 方法2
dispatch_async(dispatch_get_global_queue(0, 0), ^{
timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(repeat:) userInfo:nil repeats:true];
});

# Swift VS Objective-C

Swift 是静态类型语言,Objective-C 是动态类型语言。

这小节从数据结构、编程思路和语言特性三点来作对比。

# Swift 为什么将 String、Array 和 Dictionary 设计成值类型

首要要知道,在 OC 中,这三个都被设计成了引用类型。

  • 值类型相比引用类型,最大的优势就是可以高效的使用内存。值类型在栈上操作,引用类型通常在堆上操作。栈上的操作仅仅是单个指针的上下移动,而堆上的操作则牵涉到合并、移位、重新链接等。也就是说,Swift 这么设计大幅度减少了堆上的内存分配和回收的次数。同时,copy-on-write 又将值传递和复制到开销降到最低。
  • Swift 将它们设计成值类型也是为了线程安全。通过 Swift 的 let 设置,是得这些数据达到真正意义上的 “不变”,也从根本上解决了多线程众内存访问和操作顺序的问题。
  • Swift 将它们设计成值类型可以提高 API 的灵活度。譬如添加协议等,对数据进行操作等。
# 如何用 Swift 将协议 Protocol 中部分方法设计成可选 optional

一共有两种方案:

  1. 在协议和方法前均加上 @objc 关键字,然后在可选方法前加上 optional 关键字。该方案实际上是把协议转化为 OC 的方式,然后进行分可选定义。
  2. 用扩展 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
protocol Chef {
func makeFood()
}
extension Chef {
func makeFood() {
print("Make Food")
}
}
struct SeafoodChef: Chef {
func makeFood() {
print("Cook Seafood")
}
}

let chefOne: Chef = SeafoodChef()
let chefTwo: SeafoodChef = SeafoodChef()
chefOne.makeFood()
chefTwo.makeFood()

输出:

1
2
Cook Seafood
Cook Seafood

在 Swift 中,协议中是动态派发的,而扩展中则是静态派发的。也就是说,协议中如果有方法声明,那么方法会根据对象的实际类型进行调用。

如果上述代码中,将协议中 func makeFood() 方法删除,则输出变为:

1
2
Make Food
Cook Seafood

因为协议中没有声明 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
2
[obj isKindOfClass:[SomeClass class]];
[obj isMemberOfClass:[SomeClass class]];

isKindOfClass 用来判断 obj 是否是 SomeClass 或其子类。isMemberOfClass 用来判断 obj 是否就是 SomeClass(非子类)的实例对象。这两个方法都有个前提:obj 必须是 NSObject 或其子类。

在 Swift 中,由于很多类型并非继承自 NSObject,所以通常用 is 函数来进行判断,相当于 isKindOfClass。is 函数可同时用于值类型和引用类型。

另外,自省通常与动态类型一起使用。动态类型就是 id 类型。

# 能够通过 Category 给已有类添加属性 property

不论对 OC 还是 Swift,都可以添加。如:

1
2
3
4
5
6
7
8
9
10
11
private var middleKey: Void?
extension User {
var middleName: String? {
get {
return objc_getAssociatedObject(self, &middleKey) as? String
}
set {
objc_setAssociatedObject(self, &middleKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_COPY_NONATOMIC)
}
}
}

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 检测代码中的循环引用

两种方案:

  1. Xcode 调试工具栏中的 Memory Debug Graph 工具。
  2. 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
2
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteException = true
# Playground 可视化

导入头文件即可:

1
2
3
4
5
6
7
8
import UIKit
import PlaygroundSupport

class ViewController: UIViewConrollwe {
override func viewDidLoad() {
super.viewDidLoad()
}
}

# 系统框架

# 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
2
3
4
5
let oneV = UIView(frame: CGRect(x: 30, y: 100, width: 100, height: 180))
oneV.transform3D = CATransform3DMakeRotation(30, 1, 1, 1)
oneV.backgroundColor = UIColor.red
view.addSubview(oneV)
print("oneV frame = \(oneV.frame)\noneV bounds = \(oneV.bounds)")

输出:

1
2
oneV frame = (-18.520467338700882, 136.31870596839138, 197.04093467740176, 107.36258806321723)
oneV bounds = (0.0, 0.0, 100.0, 180.0)
# 说明并比较方法: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
2
3
4
5
6
7
8
9
10
11
12
13
14
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let current = scrollView.contentOffset.y + scrollView.frame.size.height
let totol = scrollView.contentSize.height
let ratio = current / totol

let needRead = cellsPerPage * threshold + currentPage * cellPerPage
let totolCells = cellsPerPage * (currentPage + 1)
let newTheshold = needRead / totolCells

if ratio >= newTheshold {
currentPage += 1
requestNewPage()
}
}

也可以参考 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 是服务器用来认证、追踪用户的数据结构。它通过判断客户端传来的消息确定用户。确定用户的唯一标志就是客户端传来的 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 步:

  1. 客户端打包请求。其中包括 URL、端口、账号密码等。注意,HTTPS,即 HTTP + SSL/TLS,在 HTTP 上又加了一层处理加密信息的模板。这个过程相当于客户端请求钥匙。
  2. 服务端接受请求。这个过程中,DNS 把网络地址解析成 IP 地址,在寻找到对应的计算机。这个过程相当于服务器分析是否要向客户端发送钥匙模板
  3. 服务器返回数字证书。这个过程相当于服务器想客户端发送钥匙模板。
  4. 客户端生成加密信息。此时信息已被加密,这个过程相当客户端生成钥匙并锁上请求。
  5. 客户端发送加密信息。即客户端发送请求。
  6. 服务器加锁加密信息
  7. 服务器向客户端返回信息
  8. 客户端解锁返回信息

# iOS 网络请求

# 说明并比较类:URLSessionTask, URLSessionDataTask, URLSessionUploadTask, URLSessionDownloadTask
  • URLSessionTask 是一个抽象类。通过实现它,可以实现网络的任务传输任务。诸如请求、上传、下载任务。它的取消、继续、暂停方法有默认实现。
  • URLSessionDataTask 负责 HTTP GET 请求,一般用户获取服务器数据。
  • URLSessionUploadTask 负责 HTTP POST/PUT 请求,一般用于上传数据。
  • URLSessionDownloadTask 负责下载数据,如断点下载功能。
# 什么是 Completion Handler

Completion Handler 一般用于处理 API 请求后的返回数据。

# 消息推送

# 在 iOS 开发中,本地消息推送的流程是怎样的

UserNotification 框架是针对远程和本队消息的框架,其流程主要有以下 4 步:

  1. 注册。通过调用 requestAuthiruzatuion,让用户在 Alert 中进行选择。
  2. 创建。
  3. 推送。
  4. 响应。

远程推送与本地推送的差异在于第二步,推送信息的创建。

# 说一说 iOS 开发中,远程推送的原理

这个问题主要是理清 iOS 系统、App、APNs 服务器以及 App 对应的客户端的关系,主要包括以下几方面:

  1. App 向 iOS 系统申请远程推送消息的权限。这与本地消息推送的注册是一致的。
  2. iOS 系统想 APNs 服务器请求手机端的 deviceToken,并告诉 App,允许接收推送的通知。
  3. App 接收到手机端的 deviceToken。
  4. App 将收到的 deviceToken 传给 App 的服务器端。
  5. 远程消息由 App 对应的服务器端产生吗,它会先经过 APNs 服务器。
  6. APNs 服务器将远程通知推送给响应的手机。
  7. 根据对应的 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 信号量、栅栏等,都可参考这篇文章。