1、notification 是同步还是异步,和 delegate 相⽐有什么区别,效率呢?
通知原理:【www.jianshu.com/p/5bc074ef4… 】
五、KVC、KVO
11pdf-3、通知能不能跨线程
xmind - 文中图片
本文档:
9、谈谈对 KVC(Key-Value-Coding)与 KVO(Key-Value-Observing)(键-值-编码与键-值-监看)的理解?
回答 NSNotification 是同步的,开始发布通知,通知执⾏完毕,执⾏完毕通知
在抛出通知以后,观察者在通知事件处理完成以后(这⾥我们休眠 3 秒),抛出者才会往下继续执⾏,也就是说这个过程默认是同步的;
当发送通知时,通知中⼼会⼀直等待所有的 observer 都收到并且处理了通知才会返回到 poster;
异步处理:
第⼀种⽅法,发布通知的时候搞在⼦线程中,当然了,接收⽅法触发的也是在⼦线程中
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:@"clickBtn" object:nil];
});
第⼆种⽅法,接收⽅法在⼦线程中执⾏
- (void)click111
{
dispatch_async(dispatch_get_global_queue(0, 0), ^{
sleep(3);
NSLog(@"通知执⾏完毕");
});
}
-
iOS 中页面销毁时不移除通知是否会崩溃?
在 iOS 9.0 之前,页面销毁时不移除通知会导致崩溃。 这是因为通知中心对观察者的引用是 unsafe_unretained,当观察者释放时,观察者的指针值可能仍然为非 nil,导致野指针错误。
在 iOS 9.0 之后,页面销毁时不移除通知通常不会导致崩溃。 这是因为通知中心对观察者的引用是 weak,当观察者释放时,观察者的引用会被自动置为 nil,不会造成野指针错误。
但是,页面销毁时不移除通知仍然存在以下问题:
- 可能会导致内存泄漏。观察者会被一直保留在通知中心,即使它们已经不再需要接收通知。
- 可能会导致意外行为。如果观察者在页面销毁后被重新创建,它可能会再次开始接收通知,即使它不应该接收。
因此,建议您在页面销毁时始终移除通知。 您可以使用以下方法来移除通知:
-
使用
removeObserver(_:forName:object:)方法从通知中心移除观察者。 -
使用
NotificationCenter.default.removeObserver(self)方法从通知中心移除所有观察者。 -
多次添加同一个通知会怎样?
多次添加同一个通知不会导致崩溃。 观察者只会收到一次通知,即使它被添加了多次。
这是因为通知中心会根据通知名称和对象来标识观察者。 即使同一个观察者被添加了多次,只要通知名称和对象相同,它就只会收到一次通知。
-
多次移除同一个通知会怎样?
多次移除同一个通知不会导致错误。 观察者只会从通知中心中移除一次。
这是因为通知中心会维护一个观察者列表。 当您移除一个观察者时,它只会从列表中被删除一次,即使它已经被添加了多次。
但是,多次移除同一个通知可能会导致意外行为。 这是因为观察者可能会被意外地从通知中心中移除,即使它应该继续接收通知。
因此,建议您只移除您明确知道要移除的观察者。 您可以使用以下方法来移除观察者:
- 使用
removeObserver(_:forName:object:)方法从通知中心移除观察者。 - 使用
NotificationCenter.default.removeObserver(self)方法从通知中心移除所有观察者。
2、关键字 static const 的作⽤
以下是一些常用的 Swift 关键字类别及其示例:
- **数据类型:** `Int`, `Float`, `Double`, `String`, `Bool`, `Array`, `Dictionary`, `Set` 等。
- **控制流程:** `if`, `else`, `while`, `for`, `switch`, `case`, `break`, `continue` 等。
- **函数:** `func`, `return`, `nil`, `self`, `try`, `catch`, `throw` 等。
- **错误处理:** `try`, `catch`, `throw`, `fatalError` 等。
- **类型转换:** `as`, `is` 等。
- **内存管理:** `strong`, `weak`, `unsafe_unretained`, `copy`, `autoreleasepool` 等。
- **泛型:** `struct`, `enum`, `protocol`, `associatedtype`, `where` 等。
- **并发编程:** `DispatchQueue`, `Thread`, `sync`, `async` 等。
- **面向对象:** `class`, `init`, `deinit`, `super`, `override`, `required` 等。
- **闭包:** `{`, `}`, `in`, `escape`, `@escaping` 等。
- **类型别名:** `typealias` 等。
- **属性包装器:** `@propertyWrapper` 等。
| 关键字 | 描述 | 用法 |
|---|---|---|
defer | 在当前作用域退出后执行一段代码块,无论退出原因如何。 | 适用于资源管理、清理任务和错误处理。 |
guard | 检查条件,如果条件为假则退出当前作用域。 | 用于处理函数或代码块中的前提条件和早期退出。 |
do-catch | 优雅地处理错误和异常。 | 用于捕获和从错误或意外情况中恢复。 |
try | 尝试执行可能抛出错误的操作。 | 与 catch 一起使用来处理潜在错误。 |
throw | 创建并抛出一个错误来指示异常情况。 | 用于指示需要特殊处理的错误或意外情况。 |
// 使用 defer 进行资源管理:
func readFile(path: String) throws -> String {
let fileHandle = try FileHandle(forReadingAtPath: path)
defer {
fileHandle.close()
}
// 读取文件内容
let data = try fileHandle.readToEnd()
return String(data: data, encoding: .utf8)!
}
// 使用 guard 进行早期退出:
func divide(x: Int, y: Int) throws -> Int {
guard y != 0 else {
throw MyError.divisionByZero
}
return x / y
}
// 使用 do-catch 进行错误处理:
func performOperation(data: String) {
do {
// 尝试解析数据
let value = Int(data)!
// 处理值
print("Processed value: (value)")
} catch {
// 处理错误
print("Error parsing data: (error)")
}
}
// 使用 try 处理潜在错误:
func accessResource(path: String) throws -> Data {
let url = URL(fileURLWithPath: path)
let data = try Data(contentsOf: url)
return data
}
// 抛出错误:
func withdrawMoney(amount: Int) throws {
guard balance >= amount else {
throw MyError.insufficientFunds
}
balance -= amount
print("Withdrew (amount) successfully. New balance: (balance)")
}
1)在函数体内,⼀个被声明为静态的变量在这⼀函数被调⽤过程中维持其值不变(该变量存放在静态变量区)。
2)在模块内(但在函数体外),⼀个被声明为静态的变量可以被模块内所⽤函数访问,但不能被模块外其它函数访问。它是⼀个本地的全局变量。
3)在模块内,⼀个被声明为静态的函数只可被这⼀模块内的其它函数调⽤。那就是,这个函数被限制在声明它的模块的本地范围内使⽤。
4)内联函数是 C++中的⼀种特殊函数,它可以像普通函数⼀样被调⽤,但是在调⽤时并不通过函数调⽤的机制⽽是通过将函数体直接插⼊调⽤处来实现的,这样可以⼤⼤减少由函数调⽤带来的开销,从⽽提⾼程序的运⾏效率。
⼤多数应试者能正确回答第⼀部分,⼀部分能正确回答第⼆部分,但是很少的⼈能懂得第三部分。这是⼀个应试者的严重的缺点,因为他显然不懂得本地化数据和代码范围的好处和重要性。
考点:在嵌⼊式系统中,要时刻懂得移植的重要性,程序可能是很多程序员共同协作同时完成,在定义变量及函数的过程,可能会重名,这给系统的集成带来⿇烦,因此保证不冲突的办法是显⽰的表⽰此变量或者函数是本地的,static 即可。
在 Linux 的模块编程中,这⼀条很明显,所有的函数和全局变量都要⽤ static 关键字声明,将其作⽤域限制在本模块内部,与其他模块共享的函数或者变量要 EXPORT 到内核中。
static 关键字⾄少有下列 n 个作⽤:
(1)设置变量的存储域,函数体内 static 变量的作⽤范围为该函数体,不同于 auto 变量,该变量的内存只被分配⼀次,因此其值在下次调⽤时仍维持上次的值;
(2)限制变量的作⽤域,在模块内的 static 全局变量可以被模块内所⽤函数访问,但不能被模块外其它函数访问;
(3)限制函数的作⽤域,在模块内的 static 函数只可被这⼀模块内的其它函数调⽤,这个函数的使⽤范围被限制在声明它的模块内;
(4)在类中的 static 成员变量意味着它为该类的所有实例所共享,也就是说当某个类的实例修改了该静态成员变量,其修改值为该类的其它所有实例所见;
(5)在类中的 static 成员函数属于整个类所拥有,这个函数不接收 this 指针,因⽽只能访问类的 static 成员变量。
1.作⽤于变量:
⽤ static 声明局部变量-------局部变量指在代码块{}内部定义的变量,只在代码块内部有效(作⽤域),其缺省的存储⽅式是⾃动变量或说是动态存储的,即指令执⾏到变量定义处时才给变量分配存储单元,跳出代码块时释放内存单元(⽣命期)。⽤ static 声明局部变量时,则改变变量的存储⽅式(⽣命期),使变量成为静态的局部变量,即编译时就为变量分配内存,直到程序退出才释放存储单元。这样,使得该局部变量有记忆功能,可以记忆上次的数据,不过由于仍是局部变量,因⽽只能在代码块内部使⽤(作⽤域不变)。
⽤ static 声明外部变量-------外部变量指在所有代码块{}之外定义的变量,它缺省为静态变量,编译时分配内存,程序结束时释放内存单元。同时其作⽤域很⼴,整个⽂件都有效甚⾄别的⽂件也能引⽤它。为了限制某些外部变量的作⽤域,使其只在本⽂件中有效,⽽不能被其他⽂件引⽤,可以⽤static 关键字对其作出声明。
总结:⽤ static 声明局部变量,使其变为静态存储⽅式(静态数据区),作⽤域不变;⽤ static 声明外部变量,其本⾝就是静态变量,这只会改变其连接⽅式,使其只在本⽂件内部有效,⽽其他⽂件不可连接或引⽤该变量。
2.作⽤于函数:
使⽤ static ⽤于函数定义时,对函数的连接⽅式产⽣影响,使得函数只在本⽂件内部有效,对其他⽂件是不可见的。这样的函数又叫作静态函数。使⽤静态函数的好处是,不⽤担⼼与其他⽂件的同名函数产⽣⼲扰,另外也是对函数本⾝的⼀种保护机制。
如果想要其他⽂件可以引⽤本地函数,则要在函数定义时使⽤关键字 extern,表⽰该函数是外部函数,可供其他⽂件调⽤。另外在要引⽤别的⽂
件中定义的外部函数的⽂件中,使⽤ extern 声明要⽤的外部函数即可。
关键字 const 有什么含意?
我只要⼀听到被⾯试者说:"const 意味着常数"(不是常数,可以是变量,只是你不能修改它),我就知道我正在和⼀个业余者打交道。去年Dan Saks 已经在他的⽂章⾥完全概括了 const 的所有⽤法,因此 ESP(译者:Embedded Systems Programming)的每⼀位读者应该⾮常熟悉 const 能做什么和不能做什么.如果你从没有读到那篇⽂章,只要能说出 const 意味着"只读"就可以了。尽管这个答案不是完全的答案,但我接受它作为⼀个正确的答案。(如果你想知道更详细的答案,仔细读⼀下 Saks 的⽂章吧。)
如果应试者能正确回答这个问题,我将问他⼀个附加的问题:下⾯的声明都是什么意思?
Const 只是⼀个修饰符,不管怎么样 a 仍然是⼀个 int 型的变量
const int a;
int const a;
const int *a;
int * const a;
int const * a const;
本质:const 在谁后⾯谁就不可修改,const 在最前⾯则将其后移⼀位即可,⼆者等效
const int a; 前两个的作⽤是⼀样,a 是⼀个常整型数。
const int *a;第三个意味着 a 是⼀个指向常整型数的指针(也就是,指向的整型数是不可修改的,但指针可以,此最常见于函数的参数,当你只引⽤传进来指针所指向的值时应该加上 const 修饰符,程序中修改编译就不通过,可以减少程序的 bug)。
int * const a;第四个意思 a 是⼀个指向整型数的常指针(也就是说,指针指向的整型数是可以修改的,但指针是不可修改的)。
int const * a const;
最后⼀个意味着 a 是⼀个指向常整型数的常指针(也就是说,指针指向的整型数是不可修改的,同时指针也是不可修改的)。
如果应试者能正确回答这些问题,那么他就给我留下了⼀个好印象。顺带提⼀句,也许你可能会问,即使不⽤关键字 ,也还是能很容易写出功能正确的程序,
那么我为什么还要如此看重关键字 const 呢?我也如下的⼏下理由:
-
关键字 const 的作⽤是为给读你代码的⼈传达⾮常有⽤的信息,实际上,声明⼀个参数为常量是为了告诉了⽤户这个参数的应⽤⽬的。如果你曾花很多时间清理其它⼈留下的垃圾,你就会很快学会感谢这点多余的信息。(当然,懂得⽤ const 的程序员很少会留下的垃圾让别⼈来清理的。)
-
通过给优化器⼀些附加的信息,使⽤关键字 const 也许能产⽣更紧凑的代码。
-
合理地使⽤关键字 const 可以使编译器很⾃然地保护那些不希望被改变的参数,防⽌其被⽆意的代码修改。简⽽⾔之,这样可以减少 bug 的出现。
const 关键字⾄少有下列 n 个作⽤:
(1)欲阻⽌⼀个变量被改变,可以使⽤ const 关键字。在定义该 const 变量时,通常需要对它进⾏初始化,因为以后就没有机会再去改变它了;
(2)对指针来说,可以指定指针本⾝为 const,也可以指定指针所指的数据为 const,或⼆者同时指定为 const;
(3)在⼀个函数声明中,const 可以修饰形参,表明它是⼀个输⼊参数,在函数内部不能改变其值;
(4)对于类的成员函数,若指定其为 const 类型,则表明其是⼀个常函数,不能修改类的成员变量;
(5)对于类的成员函数,有时候必须指定其返回值为 const 类型,以使得其返回值不为“左值”。
例如: const classA operator*(const classA& a1,const classA& a2);
operator的返回结果必须是⼀个 const 对象。如果不是,这样的变态代码也不会编译出错: classA a, b, c; (a * b) = c; // 对 ab 的结果赋值
操作(a * b) = c 显然不符合编程者的初衷,也没有任何意义
3、⾄少写出五种常⽤的设计模式,说说他们在什么情况下会⽤到?
以下是23种常见设计模式的列表及其简要解释:
**创建型模式:**
1. **工厂方法(Factory Method):** 定义一个创建对象的接口,让子类决定实例化哪一个类。
1. **抽象工厂(Abstract Factory):** 为多个产品家族提供一个创建接口。
1. **建造者(Builder):** 将一个复杂对象的构造过程分解为多个步骤,使得步骤可以独立地进行或重新组合。
1. **单例(Singleton):** 确保一个类只有一个实例,并提供全局访问点。
1. **原型(Prototype):** 通过复制现有的对象来创建新对象。
**结构型模式:**
6. **适配器(Adapter):** 将一个类的接口转换成另一个类需要的接口,使得原本不兼容的类可以协同工作。
6. **桥接(Bridge):** 将一个对象的接口和实现解耦,使得二者可以独立变化。
6. **组合(Composite):** 将对象组成树状结构,并提供处理整个结构或单个组成部分的方法。
6. **装饰器(Decorator):** 向一个对象添加新的功能,而不修改其本身。
6. **外观(Facade):** 为多个子系统提供一个统一的接口,简化对子系统的访问。
6. **享元(Flyweight):** 减少对象数量,以节省内存和提高性能。
6. **代理(Proxy):** 为另一个对象提供一个代理或替身,控制对该对象的访问。
**行为模式:**
13. **观察者(Observer):** 一个对象(发布者)维护一组依赖它的对象(观察者),并在状态发生变化时通知所有观察者。
13. **策略(Strategy):** 将一个算法的行为封装成一个对象,使得算法可以更换而不会影响使用它的客户。
13. **模版方法(Template Method):** 定义一个算法骨架,允许子类在不改变算法结构的情况下添加自己的行为。
13. **命令(Command):** 将一个请求封装成一个对象,使得您可以将请求参数化、队列化或记录日志。
13. **责任链(Chain of Responsibility):** 将多个对象连接成一个链,依次处理请求,直到找到能够处理该请求的对象。
13. **迭代器(Iterator):** 顺序访问一个聚合对象中的元素。
13. **状态(State):** 当一个对象的内部状态发生变化时,允许对象的行为相应地改变。
13. **备忘录(Memento):** 保存一个对象的快照,以便在之后恢复。
13. **访问者(Visitor):** 对一类对象结构中的元素进行不同操作的封装。
13. **解释器(Interpreter):** 将一个语言的文法解释成一个程序。
13. **中介者(Mediator):** 在多个对象之间协调协作。
以下是23种常见设计模式的列表及其详细解释:
**创建型模式**
**1. 工厂方法(Factory Method)**
**优点:**
- 提高代码的可复用性:将对象的创建过程封装在工厂方法中,使得子类可以根据需要创建不同的对象。
- 降低耦合度:客户端无需关心具体对象的创建过程,只需调用工厂方法即可获得所需的对象。
- 方便扩展:可以通过创建新的子类来扩展工厂方法,以创建新的对象类型。
**缺点:**
- 增加了一层抽象,可能会导致代码更加复杂。
- 可能会造成内存泄漏,如果工厂方法没有正确释放对象。
**应用场景:**
- 当需要创建多个同类对象时,并且每个对象都有自己的创建逻辑时。
- 当需要将对象的创建过程与使用它的代码解耦时。
- 当需要扩展对象类型时。
**2. 抽象工厂(Abstract Factory)**
**优点:**
- 提高代码的可复用性:将多个产品家族的创建过程封装在抽象工厂中,使得子类可以创建不同产品家族的对象。
- 降低耦合度:客户端无需关心具体产品家族的创建过程,只需调用抽象工厂方法即可获得所需的产品。
- 方便扩展:可以通过创建新的子类来扩展抽象工厂,以创建新的产品家族。
**缺点:**
- 增加了一层抽象,可能会导致代码更加复杂。
- 可能会造成内存泄漏,如果抽象工厂没有正确释放对象。
**应用场景:**
- 当需要创建多个产品家族的对象时,并且每个产品家族都有自己的创建逻辑时。
- 当需要将对象的创建过程与使用它的代码解耦时。
- 当需要扩展产品家族时。
**3. 建造者(Builder)**
**优点:**
- 将一个复杂对象的构造过程分解为多个步骤,使得步骤可以独立地进行或重新组合。
- 提高代码的可读性和可维护性。
- 方便扩展:可以通过添加新的建造者步骤来扩展建造者模式。
**缺点:**
- 增加了一层抽象,可能会导致代码更加复杂。
- 可能会造成内存泄漏,如果建造者没有正确释放对象。
**应用场景:**
- 当需要构造一个复杂的对象时,并且构造过程包含多个步骤时。
- 当需要将对象的构造过程与使用它的代码解耦时。
- 当需要定制对象的构造过程时。
**4. 单例(Singleton)**
**优点:**
- 确保一个类只有一个实例,并提供全局访问点。
- 方便共享资源:可以通过单例实例来共享资源,避免重复创建资源。
- 简化代码:可以使用单例实例来简化代码,避免在多个地方创建和销毁对象。
**缺点:**
- 难以测试:由于单例类只有一个实例,因此难以测试其行为。
- 难以扩展:由于单例类只有一个实例,因此难以扩展其功能。
- 违反了单一职责原则:单例类通常包含多个职责,违反了单一职责原则。
**应用场景:**
- 当需要确保一个类只有一个实例时,例如日志记录器、配置管理器等。
- 当需要共享资源时,例如数据库连接池、缓存等。
- 当需要简化代码时,例如全局变量等。
**5. 原型(Prototype)**
**优点:**
- 提高效率:通过复制现有的对象来创建新对象,可以提高效率,避免重复创建对象。
- 方便扩展:可以通过原型模式来扩展对象的类型。
**缺点:**
- 可能会造成内存泄漏,如果原型没有正确释放对象。
- 不适用于需要共享状态的对象。
**应用场景:**
- 当需要快速创建对象时。
- 当需要扩展对象的类型时。
- 当需要创建不需要共享状态的对象时。
**结构型模式**
**6. 适配器(Adapter)**
**缺点:**
- 增加了一层抽象,可能会导致代码更加复杂。
- 可能会降低代码的性能。
**应用场景:**
- 当需要将一个类的接口转换成另一个类需要的接口时。
- 当需要重用现有的类时。
- 当需要扩展适配器模式时。
**7. 桥接(Bridge)**
**优点:**
- 将一个对象的接口和实现解耦,使得二者可以独立变化。
- 提高代码的可复用性:可以独立地修改接口和实现,而不会影响对方。
- 提高代码的可扩展性:可以通过创建新的接口和实现来扩展桥接模式。
**缺点:**
- 增加了一层抽象,可能会导致代码更加复杂。
- 可能会降低代码的性能。
**应用场景:**
- 当需要将一个对象的接口和实现解耦时。
- 当需要提高代码的可复用性和可扩展性时。
**8. 组合(Composite)**
**优点:**
- 将对象组成树状结构,并提供处理整个结构或单个组成部分的方法。
- 提高代码的可复用性:可以使用组合模式来重用现有的对象。
- 提高代码的可扩展性:可以通过添加新的组成部分来扩展组合模式。
**缺点:**
- 可能会导致代码更加复杂,尤其是树状结构较深时。
- 可能会降低代码的性能,尤其是处理整个结构时。
**应用场景:**
- 当需要将对象组成树状结构时。
- 当需要处理整个结构或单个组成部分时。
**9. 装饰器(Decorator)**
**优点:**
- 向一个对象添加新的功能,而不修改其本身。
- 提高代码的可复用性:可以使用装饰器来重用现有的对象。
- 提高代码的可扩展性:可以通过创建新的装饰器来扩展装饰器模式。
**缺点:**
- 可能会导致代码更加复杂,尤其是装饰器较多时。
- 可能会降低代码的性能,尤其是装饰器执行昂贵的操作时。
**应用场景:**
- 当需要向一个对象添加新的功能而不修改其本身时。
- 当需要动态地添加或删除功能时。
**10. 外观(Facade)**
**优点:**
- 为多个子系统提供一个统一的接口,简化对子系统的访问。
- 提高代码的可维护性:可以将子系统的复杂性隐藏在外观背后。
- 提高代码的可测试性:可以独立地测试外观和子系统。
**缺点:**
- 增加了一层抽象,可能会导致代码更加复杂。
- 可能会降低代码的性能,因为需要进行额外的调用。
**应用场景:**
- 当需要为多个子系统提供一个统一的接口时。
- 当需要简化对子系统的访问时。
- 当需要提高代码的可维护性和可测试性时。
**11. 享元(Flyweight)**
**优点:**
- 减少对象数量,以节省内存和提高性能。
- 适用于那些具有大量相同状态的对象的情况。
**缺点:**
- 增加了一层抽象,可能会导致代码更加复杂。
- 难以实现共享可变状态的对象。
**应用场景:**
- 当需要减少对象数量以节省内存和提高性能时。
- 适用于那些具有大量相同状态的对象的情况。
**12. 代理(Proxy)**
**优点:**
- 为另一个对象提供一个代理或替身,控制对该对象的访问。
- 提高代码的安全性和可控性:可以通过代理来控制对对象的访问权限。
- 提高代码的性能:可以通过代理来缓存对象或优化对对象的访问。
**缺点:**
- 增加了一层抽象,可能会导致代码更加复杂。
- 可能会降低代码的性能,因为需要进行额外的调用。
**应用场景:**
- 当需要控制对对象的访问时。
- 当需要提高代码的安全性和可控性时。
- 当需要提高代码的性能时。
**行为模式**
**13. 观察者(Observer)**
**优点:**
- 一个对象(发布者)维护一组依赖它的对象(观察者),并在状态发生变化时通知所有观察者。
- 解耦发布者和观察者:发布者和观察者之间没有紧密的耦合,可以独立地开发和维护。
- 提高代码的可扩展性:可以轻松地添加新的观察者。
**缺点:**
- 可能会导致代码更加复杂,尤其是观察者较多时。
- 可能会降低代码的性能,尤其是发布者需要频繁通知观察者时。
**应用场景:**
- 当一个对象的状态发生变化时需要通知多个其他对象时。
- 当需要解耦发布者和观察者时。
- 当需要提高代码的可扩展性时。
**14. 策略(Strategy)**
**优点:**
- 将一个算法的行为封装成一个对象,使得算法可以更换而不会影响使用它的客户。
- 提高代码的可复用性:可以使用策略模式来重用现有的算法。
- 提高代码的可扩展性:可以通过创建新的策略来扩展策略模式。
**缺点:**
- 增加了一层抽象,可能会导致代码更加复杂。
- 可能会降低代码的性能,因为需要进行额外的调用。
**应用场景:**
- 当需要将算法的行为与使用它的代码解耦时。
- 当需要动态地更换算法时。
- 当需要支持多种算法时。
**15. 模版方法(Template Method)**
**优点:**
- 定义一个算法骨架,允许子类在不改变算法结构的情况下添加自己的行为。
- 提高代码的可复用性:可以使用模版方法模式来重用现有的算法骨架。
- 提高代码的可扩展性:子类可以根据需要扩展算法的行为。
**缺点:**
- 可能会导致代码更加复杂,尤其是算法步骤较多时。
- 难以实现递归算法。
**应用场景:**
- 当需要定义一个算法骨架并允许子类扩展其行为时。
- 当需要将算法的公共部分和私有部分分离时。
- 当需要支持多种算法变体时。
**16. 命令(Command)**
**优点:**
- 将一个请求封装成一个对象,使得您可以将请求参数化、队列化或记录日志。
- 解耦请求的发送者和接收者:请求的发送者和接收者之间没有紧密的耦合,可以独立地开发和维护。
- 提高代码的可扩展性:可以轻松地添加新的命令。
**缺点:**
- 可能会导致代码更加复杂,尤其是命令较多时。
- 可能会降低代码的性能,因为需要进行额外的调用。
**应用场景:**
- 当需要将一个请求封装成一个对象时。
- 当需要解耦请求的发送者和接收者时。
- 当需要支持撤销/重做功能时。
**17. 责任链(Chain of Responsibility)**
**优点:**
- 将多个对象连接成一个链,依次处理请求,直到找到能够处理该请求的对象。
- 解耦请求的发送者和接收者:请求的发送者无需知道哪个对象能够处理请求,只需将请求发送给链的第一个对象即可。
- 提高代码的可扩展性:可以轻松地添加新的处理对象。
**缺点:**
- 可能会导致代码更加复杂,尤其是链条较长时。
- 可能会降低代码的性能,因为需要依次处理每个对象。
**应用场景:**
- 当需要将一个请求发送给多个对象时,并且不知道哪个对象能够处理该请求时。
- 当需要动态地添加或删除处理对象时。
- 当需要支持多级授权时。
**行为模式**
**18. 迭代器(Iterator)**
**缺点:**
- 可能会导致代码更加复杂,尤其是聚合对象结构较复杂时。
- 难以实现双向迭代。
**应用场景:**
- 当需要顺序访问一个聚合对象中的元素时。
- 当需要解耦聚合对象和迭代器时。
- 当需要支持不同的迭代方式时。
**19. 状态(State)**
**优点:**
- 当一个对象的内部状态发生变化时,允许对象的行为相应地改变。
- 提高代码的可复用性:可以使用状态模式来重用状态逻辑。
- 提高代码的可扩展性:可以轻松地添加新的状态。
**缺点:**
- 可能会导致代码更加复杂,尤其是状态较多时。
- 难以管理状态之间的转换。
**应用场景:**
- 当一个对象的内部状态会影响其行为时。
- 当需要将对象的行为与状态解耦时。
- 当需要支持状态的演化时。
**20. 备忘录(Memento)**
**优点:**
- 保存一个对象的快照,以便在之后恢复。
- 提高代码的可恢复性:可以将对象恢复到以前的状态。
- 提高代码的安全性:可以避免对象状态的意外修改。
**缺点:**
- 可能会导致代码更加复杂,尤其是需要管理多个备忘录时。
- 可能会降低代码的性能,因为需要保存和恢复对象的状态。
**应用场景:**
- 当需要在之后恢复对象的某个状态时。
- 当需要实现撤销/重做功能时。
- 当需要在多个对象之间共享状态时。
**21. 访问者(Visitor)**
**优点:**
- 对一类对象结构中的元素进行不同操作的封装。
- 解耦访问者和被访问对象:访问者可以独立于被访问对象而存在,可以用于不同的对象结构。
- 提高代码的可扩展性:可以轻松地添加新的访问者操作。
**缺点:**
- 可能会导致代码更加复杂,尤其是访问者操作较多时。
- 难以实现双向访问。
**应用场景:**
- 当需要对一类对象结构中的元素进行不同操作时。
- 当需要解耦访问者和被访问对象时。
- 当需要支持新的访问者操作时。
**22. 解释器(Interpreter)**
**优点:**
- 将一个语言的文法解释成一个程序。
- 提高代码的可扩展性:可以轻松地添加新的语法规则。
- 提高代码的可维护性:可以将语法规则与执行逻辑分离。
**缺点:**
- 可能会导致代码更加复杂,尤其是语法规则较复杂时。
- 可能会降低代码的性能,因为需要解析和解释语法规则。
**应用场景:**
- 当需要解释一个语言的文法时。
- 当需要将语法规则与执行逻辑分离时。
- 当需要支持新的语法规则时。
**23. 中介者(Mediator)**
**优点:**
- 在多个对象之间协调协作。
- 解耦多个对象之间的关系:多个对象之间没有紧密的耦合,可以独立地开发和维护。
- 提高代码的可维护性:可以将对象的协作逻辑集中在中介者中。
**缺点:**
- 可能会导致代码更加复杂,尤其是对象较多时。
- 可能会降低代码的性能,因为需要进行额外的调用。
**应用场景:**
- 当多个对象之间需要相互协作时。
- 当需要解耦多个对象之间的关系时。
- 当需要集中控制对象的协作逻辑时。
两者的不同在于,KVO 是被观察者主动向观察者发送消息;Notification 是被观察者向 NotificationCenter 发送消息,再由 NotificationCenter post 通知到每个注册的观察者。
MVC 和 MVVM 的区别:MVVM 是对胖模型进⾏的拆分,其本质是给控制器减负,将⼀些弱业务逻辑放到 VM 中去处理。MVC 是⼀切设计的基础,所有新的设计模式都是基于 MVC 进⾏的改进。
项⽬中常⽤的设计模式
`1.原型 -Prototype`
客户端知道抽象 Prototype 类。在运⾏时,抽象 Prototype ⼦类的任何对象都可以按照客户端的意图来进⾏复制。因此⽆需⼿⼯创建就可以创造同⼀类型的多个实例.
在以下情形,会考虑使⽤原型模式:需要创建的对象应独⽴于其类型与创建⽅式要实例化的类是在运⾏时决定的不想要⼀个与产品层次相对应的⼯⼚层次(也就是⼯⼚类不需要与产品类⼀⼀对应)不同类实例间的差异仅是状态的若⼲组合。因此复制相应数量的原型⽐⼿⼯实例化更加⽅便。
类不容易创建,⽐如每个组件可把其他组件作为⼦节点的组合对象。复制已有的组合对象并对副本进⾏修改会更加容易
使⽤原型模式的常见误解是“原型对象应该是⼀种象征性对象从来不切实⽤”,从功能的⾓度上来看,不管什么对象,只要复制⾃⾝⽐⼿⼯实例化要好,都可以是原型对象.在以下两种特别常见的情形,我们会考虑使⽤此 Prototype 模式:
有很多相关的类,其⾏为略有不同,⽽且主要差异在于内部属性,如名称,图像等需要使⽤组合(树形)对象作为其他东西的基础,例如,使⽤组合对象作为组件来构建另⼀个组合对象.现实世界中还有许多状况应该应⽤这⼀模式.使⽤设计模式更像艺术⾏为⽽⾮科学⾏为.打破常规,⽤于创新,更聪明地⼯作。此模式的最低限度是⽣成对象的真实副本(对象复制,深/浅复制),以作为同⼀环境下其他相关事物的基础(原型)。原型模式就是通过 copy 对象⽣成新对象。JS ⾥,可以先给⼀个对象上⾯写⼀堆⽅法,搞⼀堆属性,然后通过 copy 这个对象来⽣成新对象,这个对象表现的就像⼀个类⼀样
`2.⼯⼚⽅法-Factory Method`
⼯⼚⽅法也称为虚构造器(virtual constructor).它适⽤的情形是"⼀个类⽆法预期需要⽣成那个类的对象,想让其⼦类来指定所⽣成的对象".所以⼯⼚⽅法可以使得⼀个类的实例化延迟到其⼦类.
使⽤场景
在以下情形,会考虑使⽤⼯⼚⽅法模式:
编译时⽆法准确定预期要创建的对象的类
类想让其⼦类决定在运⾏时创建什么
类有若⼲辅助类为其⼦类,⽽你想将返回哪个⼦类这⼀信息局部化(原⽂:A class has some helper classes as its subclasses and you want to localize the knowledge of which one to return)
使⽤这⼀模式的最低限度是,⼯⼚⽅法能给予类在变更返回哪⼀种对象这⼀点上更多的灵活性.
`3.抽象⼯⼚-Abstract Factory`
定义
抽象⼯⼚模式提供⼀个固定的接⼜,⽤于创建⼀系列有关联或相依存的对象,⽽不必指定其具体类或其创建的细节.客户端与⼯⼚得到的具体对象之间没有耦合.
通过上⾯类图所⽰, Client 只知道 AbstractFactory 和 AbstractProduct.每个⼯⼚类中,结构与实际操作的细节按⿊箱对待.甚⾄产品也不知道谁将负责创建它们.只有具体的⼯⼚知道为客户端创建什么,如何创建.这个模式有⼀个有趣点是,很多时候它都是⽤⼯⼚⽅法模式来实现.⼯⼚⽅法把实际的创建过程推迟到重载它的⼦类中.在类图中,⽅法
createProductA 和 createProductB 是⼯⼚⽅法.最初的抽象⽅法什么也不知道.这种抽象⾮常通⽤,⼴泛⽤于任何需要抽象创建过程的场合.
抽象⼯⼚模式常与原型模式,单例模式,享元模式等其他⼀些设计模式⼀起使⽤.
使⽤场景
抽象⼯⼚与⼯⼚⽅法模式在许多⽅⾯都⾮常相似.很多⼈常常搞不清应该在什么时候⽤哪个. 两个模式都⽤于相同的⽬的:创建对象⽽不让客户端知晓返回了什么确切的具体对象.下表为抽象⼯⼚模式与⼯⼚⽅法模式的对⽐.
抽象⼯⼚
⼯⼚⽅法
通过对象组合创建抽象产品
通过类继承创建抽象产品
创建多系列产品
创建⼀种产品
必须修改⽗类的接⼜才能⽀持新的产品
⼦类化创建者并重载⼯⼚⽅法以创建新产品
`4.⽣成器-Builder`
定义
有时,构建某些对象有多种不同的⽅式.如果这些逻辑包含在构建这些对象的类的单⼀⽅法中,构建的逻辑会⾮常荒唐(列如,针对各种构建需求的⼀⼤⽚嵌套 if-else 或者 switch-case 语句),如果能够把构建过程分解为"客户-指导者-⽣成器"(client-director-builder)的关系,那么过程将更容易管理与复⽤.针对此类关系的设计模式称为⽣成器.
⽣成器模式: 将⼀个复杂对象的构建与它的表现分离,使得同样的构建过程可以创建不同的表现.
在⽣成器模式中,除了要⽣成的产品 product 和客户 client 之外,还要两个关键⾓⾊就是:指导者 Director,和⽣成器 Builder.
这种模式将产品的创建过程(需要⼀个什么样(what)的产品,如何(how)创建这样的产品)中的 how 和 what 分离了Director 知道需要什么样(what)的产品, Builder ⽣成器知道如何(how)创建产品Builder 是个抽象接⼜,声明了⼀个 builderPart ⽅法,该 builder ⽅法由 ConcretBuilder 实现,以构造实际产品(Product).
ConcretBuilder 有个 getResult ⽅法,向客户端返回构造完毕的 Product.
Director 定义了⼀个 construct ⽅法,命令 Builder 的实例去 buildPart.
Director 和 Builder 形成了⼀种聚合关系,这意味着 Builder 是⼀个组成部分,与 Director 结合,以使整个模式运转,但 Director 并不负责 Builder 的⽣存期.
使⽤场景
在以下情形,会考虑使⽤⽣成器模式:
需要创建涉及各种部件的复杂对象.创建对象的算法应该独⽴于部件的装配⽅式.常见列⼦是构建组合对象
构建过程需要以不同的⽅式(列如,部件或变现的不同组合)构建对象.
⽣成器和抽象⼯⼚的对⽐
抽象⼯⼚和⽣成器模式很相似,然后两者还是有很⼤的区别的:
⽣成器关注的是分布创建复杂对象,很多时候同⼀类型的对象可以以不同的⽅式创建.
抽象⼯⼚的重点在于创建简单或复杂产品的套件
⽣成器在多步创建过程的最后⼀步放回产品,⽽抽象⼯⼚则⽴即返回产品
表格:⽣成器模式和抽象⼯⼚模式的主要差异
⽣成器
抽象⼯⼚构建复杂对象
构建简单或复杂对象
以多个步骤创建对象
以单⼀步骤创建对象
以多种⽅式构建对象
以单⼀⽅式构建对象
在构建过程的最后⼀步返回产品
⽴刻返回产品
专注⼀个特定产品
强调⼀套产品
⽣成器模式的整体思想是:分离"什么"和"如何",使得 aDirector 能把同⼀个"什么"(规格)应⽤到不同的 aBuilder,⽽它懂得"如何"按照给定的规格构建
⾃⼰特定的产品.
`5.单例-Singleton`
定义
⾯向对象应⽤程序中的单例类(singleton class)总是返回⾃⼰的同⼀个实例. 它提供了对类的对象的所提供的资源的全局访问点,与这类设计相关的设计模式称为单例模式.
单例模式⼏乎是最简单的设计模式了
这⼀模式意图使得类的⼀个对象成为系统中的唯⼀实例
单例模式:保证⼀个类仅有个实例,并提供⼀个访问它的全局访问点
使⽤场景
在以下情形,会考虑使⽤单例模式:
类只能有⼀个实例,⽽且必须从⼀个为⼈熟知的访问点对其进⾏访问,⽐如⼯⼚⽅法
这个唯⼀的实例只能通过⼦类化进⾏扩展,⽽且扩展的对象不会破坏客户端代码
`6.代理-Proxy`
定义
有以下⼏种代理:
远程代理(remote proxy)为位于不同地址空间或⽹络上的对象提供本地代表
虚拟代理(virtual proxy)根据需要创建重型对象
保护代理(protection proxy)根据各种访问权限控制对原对象的访问
智能引⽤代理(smart-reference proxy)通过对真正对象的引⽤进⾏计数来
管理内存. 也⽤于锁定真正对象,让其他对象不能对其进⾏修改
代理模式:为其他对象提供⼀种代理以控制对这个对象的访问,通常,代理是⼀种替代或者占位,它控制对另⼀些对象的访问,⽽这些对象可能是远程对象,创建开销较⼤的对象,或者是对安全性要求⾼的对象.
当 client 向 Proxy 对象发送 request 消息是,Proxy 对象会把这个消息转发给 Proxy 对象之中的 RequestSubject 对象.RealSubjec 会实施实际的操作间接的满⾜客户端的需求.
在运⾏时,我们可以想象这样⼀个场景:客户端以抽象类型引⽤⼀个对象.这个引⽤实际上是个 Proxy 对象.Proxy 对象本⾝有⼀个对 RealSubject 实例的引⽤,以后如果接到请求,此实例将执⾏⾼强度的⼯作.场景如下图:
使⽤场景
在以下情形,会考虑使⽤代理模式:
需要⼀个远程代理,为位于不同地址空间或者⽹络中的对象提供本地代表
需要⼀个虚拟代理,来根据要求创建重型对象.
需要⼀个保护代理,来根据不同访问权限控制对原对象的访问
需要⼀个只能引⽤代理,通过对实体对象的引⽤进⾏计数来管理内存. 也能⽤于锁定实体对象,让其他对象不能修改它.
`7.命令-Command`
定义
在⾯向对象设计中,我们把指令封装在各种命令对象中.命令对象可以被传递并且在制定时刻被不同的客户端复⽤.从这⼀概念精⼼设计⽽来的设计模式叫做命令模式命令对象封装了如何对⽬标执⾏指令的信息,因此客户端或调⽤者不必了解⽬标的任何细节,却仍可以对它执⾏任何已有的操作.
通过把请求封装成对象,客户端可以把参数化并置⼊队列或⽇志中,也能够⽀持可撤销的操作.
命令对象将⼀个或多个动作绑定到特定的接收器.命令模式消除了作为对象的动作和执⾏它的接收器之间的绑定.
命令模式: 将请求封装为⼀个对象,从⽽可⽤不同的请求对客户进⾏参数化,对请求排对或记录请求⽇志,以及⽀持可撤销的操作Client(客户端) 创建 ConcreteCommand 对象并设定其
receiver(接收器)
Invoker 要求通⽤命令(实际上是 ConcreteCommand)实施请求
Command 是为 Invoker 所知的通⽤接⼜(协议)
ConcreteCommand 起 Receiver 和对它的操作 action 之间的中间⼈的作⽤
Receiver 可以是随着由 Command(ConcreteCommand)对象实施的相应请求,⽽执⾏实际操作的任何对象.
使⽤场景
在以下情形,会考虑使⽤命令模式:
想让应⽤程序⽀持撤销和恢复
想⽤对象参数化⼀个动作以执⾏操作,并⽤不同命令对象来代替回调函数
想要在不同时刻对请求进⾏指定,排列和执⾏.
想要记录修改⽇志,这样在系统故障时,这些修改可在后来重做⼀遍.
想让系统⽀持事务(transaction),事务封装了对数据的⼀系列修改.事务可以建模为命令对象
`8.享元模式`
享元模式主要⽤于减少同⼀类对象的⼤量创建,以减少内存占⽤,提⾼项⽬流畅度,在 iOS 开发中,⼤家肯定都⽤过UITableViewCell,UICollectionViewCell,这两个类在使⽤过程中就使⽤了享元模式,⼯作原理基本就是:利⽤重⽤池重⽤思想,创建页⾯可显⽰的 cell 个数的对象,在页⾯滚动过程中监听每个 cell 的状态,从页⾯消失的 cell 被放回重⽤池,将要显⽰的 cell 先去重⽤池中去取,如果可以取到,则继续使⽤这个 cell,如果没有多余的 cell,就重新创建新的,这样即使你有 100 条数据,也仅仅只会创建页⾯可显⽰个数的 cell 对象,这样就⼤⼤减少了对象的创建,实现了⼤量内存占⽤,导致内存泄露的问题
简述性能优化
APP 的冷启动概括为三⼤阶段
dyld,Apple 的动态链接器,可以⽤来装载 Mach-O ⽂件(可执⾏⽂件、动态库等)
启动 APP 时,dyld 所做的事情有:
1.装载 APP 的可执⾏⽂件,同时会递归加载所有依赖的动态库
2.当 dyld 把可执⾏⽂件、动态库都装载完毕后,会通知 Runtime 进⾏下⼀步的处理 runtime
启动 APP 时,runtime 所做的事情有:
1.调⽤ map_images 进⾏可执⾏⽂件内容的解析和处理
2.在 load_images 中调⽤ call_load_methods,调⽤所有 Class 和 Category 的+load ⽅法
3.进⾏各种 objc 结构的初始化(注册 Objc 类 、初始化类对象等等)
4.调⽤ C++静态初始化器和__attribute__((constructor))修饰的函数
到此为⽌,可执⾏⽂件和动态库中所有的符号(Class,Protocol,Selector,IMP,…)都已经按格式成功加载到内存中,被 runtime 所管理 main
1.APP 的启动由 dyld 主导,将可执⾏⽂件加载到内存,顺便加载所有依赖的动态库
2.并由 runtime 负责加载成 objc 定义的结构
3.所有初始化⼯作结束后,dyld 就会调⽤ main 函数
4.接下来就是 UIApplicationMain 函数,AppDelegate 的 application:didFinishLaunchingWithOptions:⽅法
优化⽅案:
⼀、dyld
减少动态库、合并⼀些动态库(定期清理不必要的动态库)
减少 Objc 类、分类的数量、减少 Selector 数量(定期清理不必要的类、分类)
减少 C++虚函数数量
⼆、runtime
⽤+initialize ⽅法和 dispatch_once 取代所有的__attribute__((constructor))、C++静态构造器、ObjC 的+load
三、main
在不影响⽤户体验的前提下,尽可能将⼀些操作延迟,不要全部都放在 finishLaunching ⽅法中按需加载
4、UIView 和 CALayer 的区别?
每个 UIView 内部都有⼀个 CALayer 在背后提供内容的绘制和显⽰,并且 UIView 的尺⼨样式都由内部的 Layer 所提供。两者都有树状层级结
构,layer 内部有 SubLayers,View 内部有 SubViews.但是 Layer ⽐ View 多了个 AnchorPoint ,UIView 本⾝不具备显⽰的功能,是它内部的层才有显⽰功能。
1. 在 View 显⽰的时候,UIView 做为 Layer 的 CALayerDelegate,View 的显⽰内容由内部的 CALayer 的 display
2. CALayer 是默认修改属性⽀持隐式动画的,在给 UIView 的 Layer 做动画的时候,View 作为 Layer 的代理,Layer 通过
actionForLayer:forKey:向 View 请求相应的 action(动画⾏为)
3. layer 内部维护着三分 layer tree,分别是 presentLayer Tree(动画树),modeLayer Tree(模型树), Render Tree (渲染树),在做 iOS 动画的时候,我
们修改动画的属性,在动画的其实是 Layer 的 presentLayer 的属性值,⽽最终展⽰在界⾯上的其实是提供 View 的 modelLayer
4. 两者最明显的区别是 View 可以接受并处理事件,⽽ Layer 不可以,UIKit 使⽤ UIResponder 作为响应对象,来响应系统传递过来的事件并进⾏处理。UIApplication、UIViewController、UIView、和所有从 UIView 派⽣出来的 UIKit 类(包括 UIWindow)都直接或间接地继承⾃ UIResponder类。
5. 在 UIResponder 中定义了处理各种事件和事件传递的接⼜, ⽽ CALayer 直接继承 NSObject,并没有相应的处理事件的接⼜。
6. UIView 和 CALayer 是相互依赖的关系。UIView 依赖与 calayer 提供的内容,CALayer 依赖 uivew 提供的容器来显⽰绘制的内容。归根到底CALayer 是这⼀切的基础,如果没有 CALayer,UIView ⾃⾝也不会存在,UIView 是⼀个特殊的 CALayer 实现,添加了响应事件的能⼒。
UIView 来⾃ CALayer,⾼于 CALayer,是 CALayer ⾼层实现与封装。UIView 的所有特性来源于 CALayer ⽀持
`UIView+block 动画和 CALayer 动画的区别`
UIView 动画和 CALayer 动画:
1 实际开发中较多⽤ UIKit 动画,⽤ CALayer 开发动画太⿇烦.
2 CALayer 动画结束后有反弹,UIView 动画结束后没有反弹.
3 CALayer 动画结束后没有真正修改控件中的属性值,UIView 动画结束后已经修改了控件中的属性值.
4 使⽤ CALayer 的 delegate 属性,监听动画的执⾏过程,UIView 类⽅法监听动画执⾏过程
5 CALayer 旋转动画⽀持 3D
6 UIView 旋转动画不⽀持 3D
5、简述事件传递和响应者链?
runloop【juejin.cn/post/698351… 】
1. 事件的传递
2. 如果 view 的控制器存在,就传递给控制器;
3. 如果控制器不存在,则将其传递给它的⽗视图,在视图层次结构的最顶级视图,如果也不能处理收到的事件或消息,则其将事件或消息传递给 window 对象进⾏处理,如果 window 对象也不处理,则其将事件或消息传递给UIApplication 对象,如果 UIApplication 也不能处理该事件或消息,则将其丢弃。
4. 在 iOS 开发中会遇到各种操作事件,通过程序可以对这些事件做出响应。
5. ⾸先,当发⽣事件响应时,必须知道由谁来响应事件。在 IOS 中,由响应者链来对事件进⾏响应,所有事件响应的类都是 UIResponder 的⼦类,响应者链是⼀个由不同对象组成的层次结构,其中的每个对象将依次获得响应事件消息的机会。当发⽣事件时,事件⾸先被发送给第⼀响应者,
第⼀响应者往往是事件发⽣的视图,也就是⽤户触摸屏幕的地⽅。事件将沿着响应者链⼀直向下传递,直到被接受并做出处理。
6. ⼀般来说,第⼀响应者是个视图对象或者其⼦类对象,当其被触摸后事件被交由它处理,如果它不处理,事件就会被传递给它的视图控制器对象 viewcontroller(如果存在),然后是它的⽗视图(superview)对象(如果存在),以此类推,直到顶层视图。接下来会沿着顶层视图(top view)到窗⼜(UIWindow 对象)再到程序(UIApplication 对象)。如果整个过程都没有响应这个事件,该事件就被丢弃。⼀般情况下,在响应者链中 只要由对象处理事件,事件就停⽌传递。
7. ⼀个典型的相应路线图如:
8. First Responser -- > The Window -- >TheApplication -- > App Delegate
9. 正常的响应者链流程经常被委托(delegation)打断,⼀个对象(通常是视图)可能将响应⼯作委托给另⼀个对象来完成(通常是视图控制器 ViewController),这就是为什么做事件响应时在 ViewController 中必须实现相应协议来实现事件委托。在 iOS 中,存在 UIResponder 类,它定义了响应者对象的所有⽅法。UIApplication、UIView 等类都继承了 UIResponder 类,UIWindow 和 UIKit 中的控件因为继承了 UIView,所以也间接继承了UIResponder 类,这些类的实例都可以当作响应者.⼀、触摸事件的事件传递 触摸事件的传递⽅向是 由内⾄外(外:指所触摸的最外层视图)触摸开始后,系统会把事件加⼊到 UIApplication 事件队列。⽽后 UIApplication会从事件队列中取出最前⾯的事件分发处理。先分发给程序的主窗⼜,让后主窗⼜调⽤ hitTest: withEvent:⽅法,查找⽤户所点击的视图。
1.1 在最内层视图(key window 视图)上调⽤ pointInside:withEvent: ⽅法来判断事件触摸点是否在当前视图内。如果返回 NO,则hitTest:withEvent: ⽅法返回 nil,表明⽤户的触摸点不再此视图上。
1.2 如果返回 YES,那么当前视图会向所有⼦视图发送 hitTest:withEvent: ⽅法。
1.3 ⼦视图仍然按照 hitTest:withEvent: 和 pointInside:withEvent:结合的⽅法来查找所点击的视图。
1.4 如果最终都没有找到所点击的视图,则 hitTest:withEvent:最后返回的仍然是 nil。事件传递结束。
1.5 如果最终查找到所点击的视图,则 hitTest:withEvent:最后返回的是所点击的视图对象。然后准备进⾏事件响应。
重要事例:当⽤户点击的 point 超出⽗视图(B)的 frame 时。程序最终找不到所点击的视图,hitTest:withEvent:返回是 nil。
当程序查找到⽗视图(B)的时候,因为⽤户所点击的点处于⽗视图(B)的外⾯。pointInside:withEvent: ⽅法判断结果是触摸点不在此⽗视图(B)上,pointInside:withEvent: ⽅法返回的结果始终为 NO,则⽗视图不会再向⼦视图发送 hitTest:withEvent:⽅法。
在iOS中,判断子视图是否响应了触摸事件,**并非仅仅**基于它是否重写了 `touchesBegan:withEvent:` 方法。虽然重写此方法可以表明子视图参与了触摸处理,但它不是决定性的标准。
识别子视图触摸响应的主要机制涉及系统API `hitTest:withEvent:` 和 `pointInside:withEvent:`, 如前一回复中所解释。这些方法遍历视图层级,并根据触摸点的位置和视图的结构确定处理触摸事件的适当视图。
另一方面,重写 `touchesBegan:withEvent:` 方法为子视图提供了一种主动参与触摸事件处理的方式。当触摸发生在子视图的边界内,并且该子视图已重写 `touchesBegan:withEvent:` 时,将调用该方法,允许子视图响应触摸执行自定义操作。
但是,重写 `touchesBegan:withEvent:` 并不保证子视图已经从最终处理的角度“响应”了触摸事件。触摸事件仍可能在视图层级中传播,最终到达其他子视图或视图控制器的视图。
总而言之,虽然重写 `touchesBegan:withEvent:` 可以是子视图触摸处理逻辑的一部分,但它不是唯一决定子视图是否“响应”触摸事件的因素。系统API `hitTest:withEvent:` 和 `pointInside:withEvent:` 在识别触摸交互的目标视图方面发挥着更根本的作用。
⼆、触摸事件的响应者链条
响应者链条的传递⽅向是 由外⾄内
2.1 事件传递查找到⽤户点击的视图(C)后,当前视图(C)是否能够响应触摸事件(处理触摸事件)。
2.2 如果不能响应触摸事件,则交给当前视图(C)的⽗视图来响应。
2.3 如果⽗视图不能响应触摸事件,则由⽗视图的⽗视图来响应。
2.4 若最后谁都不对此触摸事件做处理,则丢弃此事件。
⼀般来说,如果⼦视图响应了触摸事件后,⽗视图就不会再响应此触摸事件。
重要:但是系统 API 判断⼦视图是否响应了触摸事件的⽅法。是由是否重写了 touchesBegan: withEvent:⽅法来判断的。
如果您没有重写 touchBegan:withEvent:⽅法,即使重写了剩余的其他三个 touch ⽅法,系统仍然认为⼦视图没有响应触摸事件⽽会⼀直向⽗视图响应下去。
6、你在项⽬中⽤过哪些数据持久化的⽅法,并说明为什么⽤这个?
- plist ⽂件(属性列表)
- preference(偏好设置)NSUserDefaults
- NSKeyedArchiver(归档)
- SQLite 3
- CoreData
- Documents: 最常⽤的⽬录,iTunes 同步该应⽤时会同步此⽂件夹中的内容,适合存储重要数据。
- Library/Caches: iTunes 不会同步此⽂件夹,适合存储体积⼤,不需要备份的⾮重要数据。
- Library/Preferences: iTunes 同步该应⽤时会同步此⽂件夹中的内容,通常保存应⽤的设置信息。
- tmp: iTunes 不会同步此⽂件夹,系统可能在应⽤没运⾏时就删除该⽬录下的⽂件,所以此⽬录适合保存应⽤中的⼀些临时⽂件,⽤完就删除。
plist ⽂件是将某些特定的类,通过 XML ⽂件的⽅式保存在⽬录中。
只有以上列出的类型才能使⽤ plist ⽂件存储。
存储时使⽤ writeToFile: atomically:⽅法。 其中 atomically 表⽰是否需要先写⼊⼀个辅助⽂件,再把辅助⽂件拷贝到⽬标⽂件地址。这是更安全的写⼊⽂件⽅法,⼀般都写 YES,读取时使⽤ arrayWithContentsOfFile:⽅法。
偏好设置 NSUserDefaults 是专门⽤来保存应⽤程序的配置信息的,⼀般不要在偏好设置中保存其他数据,如果没有调⽤ synchronize ⽅法,系统会根据 I/O 情况不定时刻地保存到⽂件中,所以如果需要⽴即写⼊⽂件的就必须调⽤ synchronize ⽅法,偏好设置会将所有数据保存到同⼀个⽂件中,即preference ⽬录下的⼀个以此应⽤包名来命名的 plist ⽂件。
NSKeyedArchiver:归档在 iOS 中是另⼀种形式的序列化,只要遵循了 NSCoding 协议的对象都可以通过它实现序列化,由于决⼤多数⽀持存储数据的 Foundation 和 Cocoa Touch 类都遵循了 NSCoding 协议,因此,对于⼤多数类来说,归档相对⽽⾔还是⽐较容易实现的。
1.遵循 NSCoding 协议
NSCoding 协议声明了两个⽅法,这两个⽅法都是必须实现的。⼀个⽤来说明如何将对象编码到归档中,另⼀个说明如何进⾏解档来获取⼀个新对象。
SQLite3
之前的所有存储⽅法,都是覆盖存储。如果想要增加⼀条数据就必须把整个⽂件读出来,然后修改数据后再把整个内容覆盖写⼊⽂件。所以它们都不适合存储⼤量的内容。
1.字段类型
表⾯上 SQLite 将数据分为以下⼏种类型:
integer : 整数
real : 实数(浮点数)
text : ⽂本字符串
blob : ⼆进制数据,⽐如⽂件,图⽚之类的
实际上 SQLite 是⽆类型的。即不管你在创表时指定的字段类型是什么,存储是依然可以存储任意类型的数据。⽽且在创表时也可以不指定字段类型。SQLite 之所以什么类型就是为了良好的编程规范和⽅便开发⼈员交流,所以平时在使⽤时最好设置正确的字段类型!主键必须设置成 integer
- 准备⼯作
准备⼯作就是导⼊依赖库啦,在 iOS 中要使⽤ SQLite3,需要添加库⽂件:libsqlite3.dylib 并导⼊主头⽂件,这是⼀个 C 语⾔的库,所以直接使⽤SQLite3 还是⽐较⿇烦的。使⽤ sqlite3_exec() ⽅法可以执⾏任何 SQL 语句 sqlite3_prepare_v2() : 检查 sql 的合法性。
sqlite3_step() : 逐⾏获取查询结果,不断重复,直到最后⼀条记录
sqlite3_coloum_xxx() : 获取对应类型的内容,iCol 对应的就是 SQL 语句中字段的顺序,从 0 开始。根据实际查询字段的属性,使⽤sqlite3_column_xxx 取得对应的内容即可。
sqlite3_finalize() : 释放 stmt
FMDB
1.简介
FMDB 是 iOS 平台的 SQLite 数据库框架,它是以 OC 的⽅式封装了 SQLite 的 C 语⾔ API,它相对于 cocoa ⾃带的 C 语⾔框架有如下的优点:
使⽤起来更加⾯向对象,省去了很多⿇烦、冗余的 C 语⾔代码,对⽐苹果⾃带的 Core Data 框架,更加轻量级和灵活,提供了多线程安全的数据库操作⽅法,有效地防⽌数据混乱。
注:FMDB 的 gitHub 地址
2.核⼼类
FMDB 有三个主要的类:FMDatabase
⼀个 FMDatabase 对象就代表⼀个单独的 SQLite 数据库,⽤来执⾏ SQL 语句
FMResultSet
使⽤ FMDatabase 执⾏查询后的结果集
FMDatabaseQueue
⽤于在多线程中执⾏多个查询或更新,它是线程安全的
7、import 跟#include、@class 有什么区别?#import<> 跟 #import“”又什么区别?
#import 指令是 Object-C 针对@include 的改进版本,能确保引⽤的⽂件只会被引⽤⼀次,不会陷⼊递归包含的问题中;
@import 与@class 的区别: #import 会链⼊该头⽂件的全部信息,包括实体变量和⽅法等;⼆@class 只是告诉编译器,其后⾯声明的名称是类的名称,⾄于这些类如何定义的,暂时不⽤考虑。在头⽂件中,⼀般只需要知道被引⽤的类的名称就可以了,不需要知道其内部的实体变量和⽅法,所以在头⽂件中⼀般使⽤@class 来声明这个名称是类的名称;⽽在实现类⾥⾯,因为会⽤到这个引⽤类的内部的实体变量和⽅法,所以需要使⽤#import 类包含这个被引⽤类的头⽂件:@class 还可以解决循环包含的问题,@class 告诉编译器某个类的声明,当执⾏时,才去查看类的实现⽂件
#import<>跟#import""的区别: #import<>⽤来包含系统⾃带的⽂件,#import""⽤来包含⾃定义的⽂件
8、属性 readwrite,readonly,assign,retain,copy,nonatomic 各是什么作⽤,在那种情况下⽤?
-
readwrite 是可读可写特性;需要⽣成 getter ⽅法和 setter ⽅法时
-
readonly 是只读特性 只会⽣成 getter ⽅法 不会⽣成 setter ⽅法 ;不希望属性在类外改变
-
assign 是赋值特性,setter ⽅法将传⼊参数赋值给实例变量;仅设置变量时;
-
retain 表⽰持有特性,setter ⽅法将传⼊参数先保留,再赋值,传⼊参数的 retaincount 会+1;
-
copy 表⽰赋值特性,setter ⽅法将传⼊对象复制⼀份;需要完全⼀份新的变量时。
-
nonatomic ⾮原⼦操作,决定编译器⽣成的 setter getter 是否是原⼦操作,atomic 表⽰多线程安全,⼀般使⽤ nonatomic
retain:表⽰持有特性,set ⽅法将传⼊参数先保留,再赋值,传⼊参数的 retaincount 会+1;
copy:表⽰拷贝特性,set ⽅法的实现是 release 旧值,copy 新值,⽤于 NSString、block 等类型(set ⽅法将传⼊的对象复制⼀份;需要完全⼀份新的变量时使⽤),copy ⽤于当 a 指向⼀个对象,b 也想指向同样的对象的时候,如果⽤ assign,a 如果释放,再调⽤ b 会 crash,如果⽤ copy 的⽅式, a 和 b 各⾃有⾃⼰的内存,就可以解决这个问题,⽤ strong、copy 修饰不可变字符串仅仅是指针的指向内存变了。
nonatomic:⾮原⼦操作,决定编译器⽣成的 setter getter 是否是原⼦操作,atomic 表⽰多线程安全,atomic 意为操作是原⼦的,意味着只有⼀个线程访问实例变量(⽣成的 setter 和 getter ⽅法是⼀个原⼦操作)。atomic 是线程安全的,⾄少在当前的存取器上是安全的。它是⼀个默认的特性,但是很少使⽤,因为⽐较影响效率
⼀般使⽤ nonatomic,atomic 和 nonatomic ⽤来决定编译器⽣成的 getter 和 setter 是否为原⼦操作。在多线程环境下,原⼦操作是必要的,否则有可能引起错误的结果。加了 atomic,setter 函数会变成下⾯这样:
if (property != newValue) {
[property release];
property = [newValue retain];
}
24、assign 的内存管理语义,MRC 用的时候会有循环引用吗?
assign 主要⽤于修饰数据类型,数据类型变量的内存由编译器⾃动管理。⽐如 NSInteger、CGFloat 等。
assign 修饰对象属性时,其指向⼀个对象之后,不改变该对象的引⽤计数。即只引⽤已创建的对象,⽽不持有对象。
assign 修饰的属性不持有对象,当其指向的对象在别处释放后,该指针变为悬挂指针也叫野指针
assign 修饰对象类型会怎样?
1、对象的内存⼀般被分配到堆上,基本数据类型和 oc 数据类型⼀般被分配在栈上。如果⽤ assign 修饰对象,当对象释放后(因为不存在强引⽤,离开作⽤域对象内存可能被回收),指针的地址还是存在的,也就是说指针并没有被置为 nil, 下次再访问该对象就会造成野指针异常。
2、对象是分配在堆上的,堆上的内存由程序员⼿动释放。assign 修饰基本数据类型或 OC 数据类型,因为基本数据类型是分配在栈上的,由系统分配和释放,所以不会造成野指针。
⽤ strong 修饰 NSString,修改不了,你只能把这个指针指向其他内存地址,你不能修改指针指向的地址,问题在于⽤ strong 修饰 NSString 指向的是可变字符串,那是。NSString,不可变⼀律 copy 修饰,可变字符串是不可变字符串的⼦类,你⽤ strong 修饰不可变字符串,但是我可以传可变字符串,就可以在其他地⽅改。
block 就是封装了函数调⽤,参数的类
9、谈谈对 KVC(Key-Value-Coding)与 KVO(Key-Value-Observing)(键-值-编码与键-值-监看)的理解?
当通过 KVC 调⽤对象时,⽐如:[self valueForKey:@”someKey”]时,程序会⾃动试图通过⼏种不同的⽅式解析这个调⽤。
⾸先查找对象是否带有 someKey 这个⽅法,如果没找到,会继续查找对象是否带有 someKey 这个实例变量(iVar),如果还没有找到,程序会继续试图调⽤ -(id)valueForUndefinedKey:这个⽅法。如果这个⽅法还是没有被实现的话,程序会抛出⼀个 NSUndefinedKeyException 异常错误。(KeyValue Coding 查找⽅法的时候,不仅仅会查找 someKey,这个⽅法,还会查找 getsomeKey 这个⽅法,前⾯加⼀个 get,或者_someKey 以及 _getsomeKey 这⼏种形式。同时,查找实例变量的时候也会不仅仅查找 someKey 这个变量,也会查找_someKey 这个变量是否存在。)设计 valueForUndefinedKey:⽅法的主要⽬的是当你使⽤-(id)valueForKey ⽅法从对象中请求值时,对象能够在错误发⽣前,有最后的机会响应这个请求。
异步请求最⼤数⽬是多⼤,为什么只能这么多?
这个数量是跟 cpu 有关的,并发性取决于 cpu 核数,每个核只能 同时处理⼀个任务.4 核 cpu 理论上可以并发处理 4 个任务,如果按 http 来算就是 4 个请求,但是 cpu 是抢占式资源,所以⼀般来说并发量是要根据任务的 耗时和 cpu 的繁忙度来计算 4 个左右只是个经验值你开 10 个短耗时的任务和⼏个长耗时任务的效率是不同的- -..⼀般来说估算这个量的最⼤效率估算公⽰是 cpu 核数2-1,这个公式是当时对集群进⾏压测得到的结论.cpu 抢占时间跟任务时长…开启这个数量的 线程可以最⼤化的榨⼲ cpu ⼀个道理。cpu 不可能都被抢去做 connection.iOS 是 cpu 密集型的消耗?。这个⼤概知道就⾏了,也不会有⼈特 别在意吧…cpu 核数2-1 那个是做淘宝的 java 团队压测得到的线程最优数?,放在 iOS 上也多少适⽤…⼀般来说不超过这个量就好,线程不是起的越多越好,线程数就是…cpu 来决定的
对 NSOperation 的理解
1、NSOperation 是苹果公司对 GCD 的封装,完全⾯向对象。
NSOperation 和 NSOperationQueue 分别对应 GCD 的 任务 和 队列 。
NSBlockOperation 还有⼀个⽅法:addExecutionBlock: ,这个⽅法必须在 start()⽅法之前执⾏,通过这个⽅法可以给 Operation 添加多个执⾏Block。这样 Operation 中的任务会并发执⾏,它会在 主线程 和 其它的多个线程执⾏这些任务将要执⾏的任务封装到⼀个 NSOperation 对象中,将此任务添加到⼀个 NSOperationQueue 对象中,创建⼀个 Operation 后,需要调⽤ start ⽅法来启动任务,它会 默认在当前队列同步执⾏,NSBlockOperation 还有⼀个⽅法:addExecutionBlock: ,这个⽅法必须在 start()⽅法之前执⾏,通过这个⽅法可以给 Operation 添加多个执⾏ Block。
这样 Operation 中的任务会并发执⾏,它会在 主线程 和 其它的多个线程执⾏这些任务。你还需要实现 cancel() 在内的各种⽅法。、
调⽤⼀个 NSOperation 对象的 start() ⽅法来启动这个任务,但是这样做他们默认是 同步执⾏ 的。
这时就要⽤到队列 NSOperationQueue 了。⽽且,按类型来说的话⼀共有两种类型:主队列、其他队列。只要添加到队列,会⾃动调⽤任务的start() ⽅法。
⼤家将 NSOperationQueue 与 GCD 的队列 相⽐较就会发现,这⾥没有串⾏队列,那如果我想要 10 个任务在其他线程串⾏的执⾏怎么办?
NSOperationQueue 有⼀个参数 maxConcurrentOperationCount 最⼤并发数,⽤来设置最多可以让多少个任务同时执⾏。当你把它设置为 1 的时候,他就是串⾏了!
NSOperationQueue 还有⼀个添加任务的⽅法,- (void)addOperationWithBlock:(void (^)(void))block,这是不是和 GCD 差不多?这样就可以添加⼀个任务到队列中了。
NSOperation 还有⼀个⾮常实⽤的功能,那就是添加依赖。⽐如有 3 个任务:A: 从服务器上下载⼀张图⽚,B:给这张图⽚加个⽔印,C:把图⽚返回给服务器。这时就可以⽤到依赖了。不能添加相互依赖,会死锁,⽐如 A 依赖 B,B 依赖 A。可以使⽤ removeDependency 来解除依赖关系。可以在不同的队列之间依赖,反正就是这个依赖是添加到任务⾝上的,和队列没关系。
加密⽅式有⼏种
11pdf-### 10、加密-算法、http握手流程、双向认证、抓包、中间人攻击
Base64:可进⾏反向解密Md5: 哈希算法之⼀, 把⼀个任意长度的字节串变换成⼀定长度的⼗六进制的⼤整数.字符串的转换过程是不可逆的,不能通过加密结果,反向推导出原始内容
RSA:
sha512:
Des:
token 值: 登录令牌.利⽤ token 值来判断⽤户的登录状态.类似于 MD5 加密之后的长字符串,⽤户登录成功之后,在后端(服务器端)会根据⽤户信息⽣成⼀个唯⼀的值,在服务器端(数据库)会保存这个 token 值,以后利⽤这个 token 值来检索对应的⽤户信息,并且判断⽤户的登录状态,⽤户登录成功之后,服务器会将⽣成的 token 值返回给 客户端,在客户端也会保存这个 token 值.(⼀般可以保存在 cookie 中,也可以⾃⼰⼿动确定保存位置(⽐如偏好 设置.)).以后客户端在发送新的⽹络请求的时候,会默认⾃动附带这个 token 值(作为⼀个参数传递给服务器.).服务器拿到客户端传递的 token 值跟保存在数据库中的 token 值做对⽐,以此来判断⽤户⾝份和登录状态,⼀般的 app ,token 值得失效时间都在 1 年以上.特殊的 app :银⾏类 app /⽀付类 app :token 值失效时间 15 分钟左右.⼀旦⽤户信息改变(密码改变),会在服务器⽣成新的 token 值,原来的 token 值就会失效.需要再次输⼊账号和密码,以得到⽣成的新的 token 值.唯⼀性判断: 每次登录,都会⽣成⼀个新的 token 值.原来的 token 值就会失效.利⽤时间来判断登录的差异性.
SSL/TLS 协议的做法是把这两者结合:
假如 A 与 B 要传输信息,那么 A 可以使⽤⾮对称加密⽣成⼀对密钥 (k1, k2),然后将 k1 发给 B, k2 ⾃⼰保留;B 收到 k1 后先⾃⼰使⽤对称加密⽣成⼀个 key, 然后使⽤ k1 将这个 key 加密传给 A;A 收到密⽂后使⽤ k2 将其解密,⾄此,A 和 B 已经完成了 key 的传输,之后他们就可以使⽤这个 key 按对称加密的⽅式传输信息。因为在传输过程中只暴露了 k1,要解密需要知道 k2,所以就算有⼈窃取到传输的信息,也⽆法解密。但 SSL/TLS 协议不仅仅是做了这些,因为如果这样的⽅式还是有⽅法可以破解的,有⼀种⽅法是“中间⼈攻击”,它分别欺骗 A 和 B,让对⽅误以为它是 A(或者B),这样它就可以⾃⼰定义⼀个 key,然后欺骗 A 和 B,让他们误以为已经完成了 key 的传输,然后使⽤这个 key 来传输信息。为了防⽌这种攻击,又引⼊了⼀个叫 CA 的东西。CA(Certificate Authority) 是⼀些⾮常权威的专门⽤于认证⼀个⽹站合法性的组织。这样 A 和 B 在传输密钥的时候就可以通过 CA 来判断对⽅是否合法,这样“中间⼈攻击”也就⽆法实施。(对称加密:AES,⾮对称加密:RSA)。
10、如何让属性不被监听?
Automaticallynitifiesobserversforkey 和 hook
在iOS开发中,如果你想要让某个属性不被KVO监听,可以使用`AutomaticallyNotifiesObserversForKeys`方法并将其对应的属性设置为`NO`。另外,你可以通过重写`setter`方法来hook属性的设置,从而在设置之前做一些自定义操作。
以下是一个简单的例子:
@interface MyObject : NSObject
@property (nonatomic) BOOL myProperty;
@end
@implementation MyObject
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
if ([key isEqualToString:@"myProperty"]) {
return NO;
}
return [super automaticallyNotifiesObserversForKey:key];
}
- (void)setMyProperty:(BOOL)newValue {
[self willChangeValueForKey:@"myProperty"];
_myProperty = newValue;
[self didChangeValueForKey:@"myProperty"];
}
@end
在这个例子中,我们重写了`+automaticallyNotifiesObserversForKey:`方法,并且将`myProperty`的监听设置为`NO`,这样就不会自动发送通知给KVO。同时,我们重写了`setMyProperty:`方法,在改变属性值之前和之后通过`willChangeValueForKey:`和`didChangeValueForKey:`方法发送通知,这样就可以在改变属性时自定义KVO的行为。
在 iOS 中,KVO(Key-Value Observing)机制允许对象监视其他对象的属性变化,并对其做出相应反应。然而,出于保护敏感数据或提高性能等原因,有时您可能需要限制属性观察。以下是一些常用的方法:
- 使用原子属性(Atomic Properties):
iOS 9 引入了原子属性,它提供了一种内置机制来防止属性值被 KVO 观察。通过将属性声明为原子属性,您可以确保对其值的任何修改都是原子操作,这意味着它们会立即发生,而不会出现中间状态。这有效地阻止了 KVO 观察者检测到属性的更改。
Swift
@atomic var isLoggedIn: Bool = false
请谨慎使用代码。
content_copy
- 采用非 KVO 兼容的数据结构:
与其使用可观察 KVO 的数据结构(例如可变数组或字典),不如考虑使用非 KVO 兼容的替代方案。例如,您可以使用 Swift 的“写时复制”数据结构,例如 CopyOnWriteArray 和 CopyOnWriteDictionary。这些结构提供写时复制行为,确保对底层数据的更改会导致新副本,有效地阻止 KVO 观察者注意到修改。
Swift
var names = CopyOnWriteArray<String>()
请谨慎使用代码。
content_copy
- 实现自定义属性访问器:
如果您需要对属性观察进行更细粒度的控制,可以重写属性的 getter 和 setter 方法。在 setter 方法中,您可以执行所需的属性更新,但避免发送 KVO 通知。这种方法在管理属性更改和防止不需要的观察方面提供了更大的灵活性。
Swift
class User {
private var _isLoggedIn: Bool = false
var isLoggedIn: Bool {
get { return _isLoggedIn }
set {
_isLoggedIn = newValue
// 这里不会发送 KVO 通知
}
}
}
请谨慎使用代码。
content_copy
- 利用 KVO 块级选项:
添加 KVO 观察者时,您可以使用 options 参数指定 NSKeyValueObservingOptionOld 或 NSKeyValueObservingOptionNew。这些选项分别在观察回调期间提供属性的旧值和新值。通过使用这些选项,您可以执行自己的更改检测逻辑,并根据您的条件决定是否通知其他观察者。
Swift
object.addObserver(self, forKeyPath: "property", options: [.old, .new], context: nil)
请谨慎使用代码。
content_copy
- 考虑替代的通信机制:
在某些情况下,KVO 可能不是用于传达属性更改的最合适方法。相反,您可以探索替代机制,例如委托模式或自定义通知系统。这些方法可以提供对象之间更具针对性和可控的通信。
请记住,防止属性观察可能会对依赖 KVO 更改检测的代码产生影响。在实施这些技术之前,请仔细评估您的特定需求和潜在影响。
11、制作.a 静态库 SDK 和使⽤.a 静态库?
Build Active Architecture Only 选项设置,Build Setting ⾥⾯的 Build Active Architecture Only 设置为 NO。Build Active Architecture Only 设置为 YES
时,是为了 debug 的时候编译速度更快,此时它只编译当前的 architecture 版本。设置为 NO 时,会编译所有的版本。
Debug 版本和 Release 版本的切换
Debug 是调试版本,主要让程序员使⽤。在调试的过程中 Debug 会启动更多的服务来监控错误,运⾏速度较慢,⽽且⽐较耗能。
Release 是发布版本,主要让⽤户使⽤,在使⽤的过程中会去掉那些繁琐的监控服务,运⾏速度相对较快,⽽且⽐较节约内存。
这⾥,我们在 Debug 版本和 Release 版本下,使⽤模拟器和真机进⾏编译,⽣成 Debug 和 Release 模拟器和真机版本的静态库, 查看静态库设备
⽀持类型:lipo -info 静态库路径合并静态库:lipo -create 静态库 1 的路径 静态库 2 的路径 -output 要⽣成的静态库路径+静态库名称。
12、谈谈什么是”懒加载”, 应⽤场景是什么?
懒加载的最根本作⽤是需要多次调⽤这个对象的时候使⽤,本质上就是对⼀个实例的 getter ⽅法的重写,⽐如某个 ui 需要多次改变状态,这时候⽤懒加载,弊端倒没有。但是这个实在是需要看情况的,不是所有 getter 都需要写成懒加载的。有些时候很确定那些实例变量需要在某个地⽅初始化,那就不需要重写 getter 了。⼤量的重写 getter 会造成⼤量的版⾯浪费,⽽且增加很多输⼊量。举个例⼦,控制器的 viewDidLoad 后所有布局和视图都需 要初始化好,那么可以将这些控件都放到⼀个独⽴的⽅法⾥初始化和布局,并不需要单独写 getter。提⾼了程序的可读性,也能降低程序的耦合性,不⽤再关⼼数组该在什么时候创建,我们只使⽤他就⾏了,也被成为延迟加载,可以做到⽤到时再加载;加载过了就不会再次加载,节约了系统资源;对于实际开发中可能会遇到的⼀些顺序问题,懒加载也能很好的解决;
懒加载的实现思路:
在类扩展中创建⼀个属性;
重写这个属性对应的 getter,将要实现的逻辑放到这个 getter 中;
考虑到懒加载只加载⼀次,那么在实现逻辑之前应该判断⼀下这个属性是否为空,为空才执⾏逻辑,否则直接返回这个属性;
加载⽹络数据和 UI 初始化⼦控件;
13、如果使⽤ sqlite, ⼤批量的插⼊数据, 需要做哪些优化?
在数据库的 sql 语句前加:“begin;” 结束后加“commit;”;发现了 sqlite 的事务处理问题,在 sqlite 插⼊数据的时候默认⼀条语句就是⼀个事务,
有多少条数据就有多少次磁盘操作,在批量插⼊数据的时候,只开启⼀个事务,这样只会进⾏⼀次磁盘操作。
14、简述 UITableView 的重⽤机制?遇到 tableView 卡顿嘛?会造成卡顿的原因⼤致有哪些?
`复⽤机制⼤体是这样:`
UITableView ⾸先加载⼀屏幕(假设 UITableView 的⼤⼩是整个屏幕的⼤⼩)所需要的 UITableViewCell,具体个数要根据每个cell 的⾼度⽽定,总之肯定要铺满整个屏幕,更准确说当前加载的 cell 的⾼度要⼤于屏幕⾼度。然后你往上滑动,想要查看更多的内容,那么肯定需要⼀个新的 cell 放在已经存在内容的下边。这时候先不去⽣成,⽽是先去 UITableView ⾃⼰的⼀个资源池⾥去获取。这个资源池⾥放了已经⽣成的⽽且能⽤的 cell。如果资源池是空的话才会主动⽣成⼀个新的 cell。
那么这个资源池⾥的 cell 又来⾃哪⾥呢?
当你滑动时视图时,位于最顶部的 cell 会相应的往上滑动,直到它彻底消失在屏幕上,消失的 cell 去了哪⾥呢?你肯定想到了,是的,它被 UITableView 放到资源池⾥了。其他 cell 也是这样,只要⼀滑出屏幕就放⼊资源池。这样,有进有出,总共需要⼤约⼀屏幕多⼀两个的 cell 就够了。相对于 1000 来说节省的资源就是指数级啊,完美解决了性能问题。
设置 Cell 的存在差异性的那些属性(包括样式和内容)时,有了 if 最好就要有 else,要显式的覆盖所有可能性。
设置 Cell 的存在差异性的那些属性时,代码要放在初始化代码块的外部。
查看 UITableView 头⽂件,会找到 NSMutableArray* visiableCells,和 NSMutableDictnery* reusableTableCells 两个结构。
visiableCells 内保存当前显⽰的 cells,reusableTableCells 保存可重 ⽤的 cells。
TableView 显⽰之初,reusableTableCells 为空,那么 tableViewdequeueReusableCellWithIdentifier:CellIdentifier 返回 nil。
开始的 cell 都是 通过[[UITableViewCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier]来创建,⽽且cellForRowAtIndexPath 只是调⽤最⼤显⽰ cell 数的 次数。⽐如:有 100 条数据,iPhone ⼀屏最多显⽰ 10 个 cell。
程序最开始显⽰ TableView 的情况是:
1. ⽤[[UITableViewCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier]创建 10 次 cell,并给 cell 指定同样的重⽤标识
(当然,可以为不同显⽰类型的 cell 指定不同的标识)。并且 10 个 cell 全部都加⼊到 visiableCells 数组, reusableTableCells 为空。
2. 向下拖动 tableView,当 cell1 完全移出屏幕,并且 cell11(它也是 alloc 出来的,原因同上)完全显⽰出来的时候。cell11 加⼊到 visiableCells,
cell1 移出 visiableCells,cell1 加⼊到 reusableTableCells。
3. 接着向下拖动 tableView,因为 reusableTableCells 中已经有值,所以,当需要显⽰新的 cell,cellForRowAtIndexPath 再次被调⽤的候,
tableViewdequeueReusableCellWithIdentifier:CellIdentifier,返回 cell1。 cell1 加⼊到 visiableCells, cell1 移出 reusableTableCells;cell2 移出
visiableCells,cell2 加⼊到 reusableTableCells。之后再需要显⽰的 Cell 就可以正常重⽤了。
**卡顿原因**
减少 cellForRowAtIndexPath 代理中的计算量(cell 的内容计算)
⾸先要提前计算每个 cell 中需要的⼀些基本数据,代理调⽤的时候直接取出;
图⽚要异步加载,加载完成后再根据 cell 内部 UIImageView 的引⽤设置图⽚;图⽚数量多时,图⽚的尺⼨要跟据需要提前经过 transform 矩阵变换压缩好(直接设置图⽚的 contentMode 让其⾃⾏压缩仍然会影响滚动效率),必要的时候要准备好预览图和⾼清图,需要时再加载⾼清图。
图⽚的‘懒加载'⽅法,即延迟加载,当滚动速度很快时避免频繁请求服务器数据。
尽量⼿动 Drawing 视图提升流畅性,⽽不是直接⼦类化 UITableViewCell,然后覆盖 drawRect ⽅法,因为 cell 中不是只有⼀个 contentview。绘制cell 不建议使⽤ UIView,建议使⽤ CALayer。
减少 heightForRowAtIndexPath 代理中的计算量(cell 的⾼度计算)
由于每次 TableView 进⾏ update 更新都会对每⼀个 cell 调⽤ heightForRowAtIndexPath 代理取得最新的 height,会⼤⼤增加计算时间。如果表格的所有 cell ⾼度都是固定的,那么去掉 heightForRowAtIndexPath 代理,直接设置 TableView 的 rowHeight 属性为固定的⾼度;
如果⾼度不固定,应尽量将 cell 的⾼度数据计算好并储存起来,代理调⽤的时候直接取,即 将 height 的计算时间复杂度降到 O(1)。例如:在异步请求服务器数据时,提前将 cell ⾼度计算好并作为 dataSource 的⼀个数据存到数据库供随时取⽤。
优化 UITableviewCell
UITableView 是我们最常⽤来展⽰数据的控件之⼀,并且通常需要 UITableView 在承载较多内容的同时保证交互的流畅性,对 UITableView 的性能优化是我们开发应⽤程序必备的技巧之⼀。
在 UITableView 的复⽤机制⼀节,已经提到了 UITableView 的复⽤机制。现在就来看看 UITableView 在复⽤时最主要的两个回调⽅法:- [UITableView
tableView:cellForRowAtIndexPath:]和- [UITableView tableView:heightForRowAtIndexPath:]。UITableView 是继承⾃ UIScrollView,所以在渲染的过程中它会先确定它的 contentSize 及每个 Cell 的位置,然后才会把复⽤的 Cell 放置到对应的位置。⽐如现在⼀共有 50 个 Cell,当前屏幕上显⽰ 5 个。那么在第⼀次创建或 reloadData 的时候, UITableView 会先调⽤ 50 次- [UITableView tableView:heightForRowAtIndexPath:]
确定 contentSize 及每个 Cell 的位置,然后再调⽤ 5 次 - [UITableView tableView:cellForRowAtIndexPath:]
来渲染当前屏幕的 Cell。在滑动屏幕的时候,每当⼀个 Cell 进⼊屏幕时,都需要调⽤⼀次 - [UITableView tableView:cellForRowAtIndexPath:]和-
[UITableView tableView:heightForRowAtIndexPath:]⽅法。
1.最常⽤的就是 cell 的重⽤, 注册重⽤标识符
如果不重⽤ cell 时,每当⼀个 cell 显⽰到屏幕上时,就会重新创建⼀个新的 cell
如果有很多数据的时候,就会堆积很多 cell。
如果重⽤ cell,为 cell 创建⼀个 ID,每当需要显⽰ cell 的时候,都会先去缓冲池中寻找可循环利⽤的 cell,如果没有再重新创建 cell
2.避免 cell 的重新布局
cell 的布局填充等操作 ⽐较耗时,⼀般创建时就布局好
如可以将 cell 单独放到⼀个⾃定义类,初始化时就布局好
3.提前计算并缓存 cell 的属性及内容
当我们创建 cell 的数据源⽅法时,编译器并不是先创建 cell 再定 cell 的⾼度,⽽是先根据内容⼀次确定每⼀个 cell 的⾼度,⾼度确定后,再创建要显⽰的 cell,滚动时,每当 cell 进⼊凭虚都会计算⾼度,提前估算⾼度告诉编译器,编译器知道⾼度后,紧接着就会创建 cell,这时再调⽤⾼度的具体计算⽅法,这样可以⽅式浪费时间去计算显⽰以外的 cell
4.减少 cell 中控件的数量
尽量使 cell 得布局⼤致相同,不同风格的 cell 可以使⽤不⽤的重⽤标识符,初始化时添加控件,不适⽤的可以先隐藏
5.不要使⽤ ClearColor,⽆背景⾊,透明度也不要设置为 0,渲染耗时⽐较长
6.使⽤局部更新
如果只是更新某组的话,使⽤ reloadSection 进⾏局部更。
7.加载⽹络数据,下载图⽚,使⽤异步加载,并缓存。
8.少使⽤ addView 给 cell 动态添加 view。
9.按需加载 cell,cell 滚动很快时,只加载范围内的 cell。
10.不要实现⽆⽤的代理⽅法,tableView 只遵守两个协议。
11.缓存⾏⾼:estimatedHeightForRow 不能和 HeightForRow ⾥⾯的 layoutIfNeed 同时存在,这两者同时存在才会出现“窜动”的 bug。所以我的建议是:只要是固定⾏⾼就写预估⾏⾼来减少⾏⾼调⽤次数提升性能。如果是动态⾏⾼就不要写预估⽅法了,⽤⼀个⾏⾼的缓存字典来减少代码的调⽤次数即可。
12.不要做多余的绘制⼯作。在实现 drawRect:的时候,它的 rect 参数就是需要绘制的区域,这个区域之外的不需要进⾏绘制。例如上例中,就可
以⽤ CGRectIntersectsRect、CGRectIntersection 或 CGRectContainsRect 判断是否需要绘制 image 和 text,然后再调⽤绘制⽅法。
13.预渲染图像。当新的图像出现时,仍然会有短暂的停顿现象。解决的办法就是在 bitmap context ⾥先将其画⼀遍,导出成 UIImage 对象,然后再绘制到屏幕。
卡顿优化在 CPU 层⾯
尽量⽤轻量级的对象,⽐如⽤不到事件处理的地⽅,可以考虑使⽤ CALayer 取代 UIView
不要频繁地调⽤ UIView 的相关属性,⽐如 frame、bounds、transform 等属性,尽量减少不必要的修改
尽量提前计算好布局,在有需要时⼀次性调整对应的属性,不要多次修改属性
Autolayout 会⽐直接设置 frame 消耗更多的 CPU 资源
图⽚的 size 最好刚好跟 UIImageView 的 size 保持⼀致
控制⼀下线程的最⼤并发数量尽量把耗时的操作放到⼦线程
⽂本处理(尺⼨计算、绘制)
图⽚处理(解码、绘制)
卡顿优化在 GPU 层⾯
尽量避免短时间内⼤量图⽚的显⽰,尽可能将多张图⽚合成⼀张进⾏显⽰
GPU 能处理的最⼤纹理尺⼨是 4096x4096,⼀旦超过这个尺⼨,就会占⽤ CPU 资源进⾏处理,所以纹理尽量不要超过这个尺⼨
尽量减少视图数量和层次
减少透明的视图(alpha<1),不透明的就设置 opaque 为 YES
尽量避免出现离屏渲染
1.预排版,提前计算
在接收到服务端返回的数据后,尽量将 CoreText 排版的结果、单个控件的⾼度、cell 整体的⾼度提前计算好,将其存储在模型的属性中。需要使⽤时,直接从模型中往外取,避免了计算的过程。
尽量少⽤ UILabel,可以使⽤ CALayer 。避免使⽤ AutoLayout 的⾃动布局技术,采取纯代码的⽅式
2.预渲染,提前绘制
例如圆形的图标可以提前在,在接收到⽹络返回数据时,在后台线程进⾏处理,直接存储在模型数据⾥,回到主线程后直接调⽤就可以了
避免使⽤ CALayer 的 Border、corner、shadow、mask 等技术,这些都会触发离屏渲染。
3.异步绘制
4.全局并发线程
5.⾼效的图⽚异步加载
15、什么是沙箱模型?哪些操作是属于私有 api 范畴?
1、应⽤程序可以在⾃⼰的沙盒⾥运作,但是不能访问任何其他应⽤程序的沙盒。
2、应⽤程序间不能共享数据,沙盒⾥的⽂件不能被复制到其他应⽤程序⽂件夹中,也不能把其他应⽤程序⽂件夹中的⽂件复制到沙盒⾥。
3、苹果禁⽌任何读、写沙盒以外的⽂件,禁⽌应⽤程序将内容写到沙盒以外的⽂件夹中。
4、沙盒根⽬录⾥有三个⽂件夹:Documents,⼀般应该把应⽤程序的数据⽂件存到这个⽂件夹⾥,⽤于存储⽤户数据或其他应该定期备份的信息。Library,下有两个⽂件夹,Caches 存储应⽤程序再次启动所需的信息,Preferences 包含应⽤程序偏好设置⽂件,不过不要在这⾥修改偏好设置。
temp,存放临时⽂件,即应⽤程序再次启动不需要的⽂件。
沙盒根⽬录⾥有三个⽂件夹分别是:documents,tmp,Library。
1、Documents ⽬录:您应该将所有的应⽤程序数据⽂件写⼊到这个⽬录下。这个⽬录⽤于存储⽤户数据或其它应该定期备份的信息。
2、AppName.app ⽬录:这是应⽤程序的程序包⽬录,包含应⽤程序的本⾝。由于应⽤程序必须经过签名,所以您在运⾏时不能对这个⽬录中的内容进⾏修改,否则可能会使应⽤程序⽆法启动。
3、Library ⽬录:这个⽬录下有两个⼦⽬录:Caches 和 Preferences
Preferences ⽬录:包含应⽤程序的偏好设置⽂件。您不应该直接创建偏好设置⽂件,⽽是应该使⽤ NSUserDefaults 类来取得和设置应⽤程序的偏好.
Caches ⽬录:⽤于存放应⽤程序专⽤的⽀持⽂件,保存应⽤程序再次启动过程中需要的信息。
4、tmp ⽬录:这个⽬录⽤于存放临时⽂件,保存应⽤程序再次启动过程中不需要的信息。
16、self.跟 self->什么区别?
self. 是调⽤ get ⽅法或者 set ⽅法;self 是当前本⾝,是 1 个指向当前对象的指针;self-> 是直接访问成员变量
self 和 super 区别
- self 调⽤⾃⼰⽅法,super 调⽤⽗类⽅法
self 是类,super 是预编译指令【self class】和【super class】输出是⼀样的
1.当使⽤ self 调⽤⽅法时,会从当前类的⽅法列表中开始找,如果没有,就从⽗类中再找;⽽当使⽤ super 时,则从⽗类的⽅法列表中开始找,然后调⽤⽗类的这个⽅法。
2.当使⽤ self 调⽤时,会使⽤ objc_msgSend 函数: id objc_msgSend(id theReceiver, SEL theSelector, …)。第 ⼀个参数是消息接收者,第⼆个参数是调⽤的具体类⽅法的 selector,后⾯是 selector ⽅法的可变参数。以 [self setName:] 为例,编译器会替换成调⽤ objc_msgSend 的函数调⽤,其中 theReceiver 是 self,theSelector 是 @selector(setName:),这个 selector 是从当前 self 的 class 的⽅法列表开始找的 setName,当找到后把对应的 selector 传递过去。
3.当使⽤ super 调⽤时,会使⽤ objc_msgSendSuper 函数:id objc_msgSendSuper(struct objc_super
当编译器遇到 [super setName:] 时,开始做这⼏个事:1)构 建 objc_super 的结构体,此时这个结构体的第⼀个成员变量 receiver 就是 ⼦类,和 self 相同。⽽第⼆个成员变量 superClass 就是指⽗类
调⽤ objc_msgSendSuper 的⽅法,将这个结构体和 setName 的 sel 传递过去。
2)函数⾥⾯在做的事情类似这样:从 objc_super 结构体指向的 superClass 的⽅法列表开始找 setName 的 selector,找到后再以 objc_super->receiver
去调⽤这个 selector
17、简述@protected ,@private,@public,@package,各有什么含义作⽤?
@protected:其作⽤范围只能在⾃⾝类,⼦类,外界不可以访问,可以继承,如果不加修饰,默认是@protected
@public:作⽤范围在任何地⽅,外界可以访问,又可以继承(不能作为默认的,是因为违反了类的封装特性)
@private:私有的,其作⽤范围只能在⾃⾝类,外界不可以访问,也不能继承(不能作为默认的,是因为违反了类的继承特性)
@package:其作⽤范围是在某个框架内
@dynamic 关键字⽐较熟悉,它告诉编译器不要为属性合成 getter 和 setter ⽅法。
Swift 中也有 dynamic 关键字,它可以⽤于修饰变量或函数,它的意思也与 OC 完全不同。它告诉编译器使⽤动态分发⽽不是静态分发。OC 区别于其他语⾔的⼀个特点在于它的动态性,任何⽅法调⽤实际上都是消息分发,⽽ Swift 则尽可能做到静态分发。因此,标记为 dynamic 的变量/函数会隐式的加上。
18、iOS 平台怎么做数据的持久化?coredata 和 sqlite 有⽆必然联系?coredata 是⼀个关系型数据吗?
数据的持久化本质上都是就是写⽂件,但从逻辑上又分成很多种,⽐如写⼊沙盒,⽐如存到⽹络上,⽐如写⼊数据库。core data 是对 sqlite 的封装,因为 sqlite 是 c 语⾔的 api,然⽽有⼈也需要 obj-c 的 api,所以有了 core data ,另外,core data 不仅仅是把 c 的 api翻译成 oc 的 api,还提供了⼀些管理的功能,使⽤更加⽅便。
App 升级之后数据库字段或者表有更改会导致 crash,CoreData 的版本管理和数据迁移变得⾮常有⽤,⼿动写 sql 语句操作还是⿇烦⼀些。
CoreData 不光能操纵 SQLite,CoreData 和 iCloud 的结合也很好,如果有这⽅⾯需求的话优先考虑 CoreData。
CoreData 并不是直接操纵数据库,⽐如:使⽤ CoreData 时不能设置数据库的主键,⽬前仍需要⼿动操作。
iOS 平台怎么做数据的持久化?coredata 和 sqlite 有⽆必然联系?coredata 是⼀个关系型数据吗?
数据的持久化本质上都是就是写⽂件,但从逻辑上又分成很多种,⽐如写⼊沙盒,⽐如存到⽹络上,⽐如写⼊数据库。
core data 是对 sqlite 的封装,因为 sqlite 是 c 语⾔的 api,然⽽有⼈也需要 obj-c 的 api,所以有了 core data ,另外,core data 不仅仅是把 c 的 api 翻译成 oc 的 api,还提供了⼀些管理的功能,使⽤更加⽅便。
App 升级之后数据库字段或者表有更改会导致 crash,CoreData 的版本管理和数据迁移变得⾮常有⽤,⼿动写 sql 语句操作还是⿇烦⼀些。
CoreData 不光能操纵 SQLite,CoreData 和 iCloud 的结合也很好,如果有这⽅⾯需求的话优先考虑 CoreData。
CoreData 并不是直接操纵数据库,⽐如:使⽤ CoreData 时不能设置数据库的主键,⽬前仍需要⼿动操作
19、Object-c 的类可以多重继承么?可以实现多个接⼜么?category 是什么?重写⼀个类的⽅式⽤继承好还是分类好?为什么?
Object-c 的类不可以多重继承;可以实现多个接⼜,通过实现多个接⼜可以完成 C++的多重继承;Category 是类别,⼀般情况⽤分类好,⽤ Category 去重写类的⽅法,仅对本 Category 有效,不会影响到其他类与原有类的关系,修改原有⽅法当然是继承 增加当然是分类,类别只能扩充⽅法,⽽不能扩充成员变量。
通过协议实现
协议主要是⽤来提出类应遵守的标准,但其特性也可⽤来实现多继承。⼀个类可以遵守多个协议,也即实现多个协议的⽅法,以此来达到多继承的效果。
概念上的单继承和多继承应该是继承⽗类的属性和⽅法,并且不经重写即可使⽤,但通过协议实现多继承有如下不同:
⼦类需要实现协议⽅法;由于协议⽆法定义属性,所以该⽅法只能实现⽅法的多继承
通过类别实现
下⾯就有请 Objective-C 的⼀⼤⿊魔法——Catagory(分类)。
相对于协议,它的 Runtime 特性造就了其⼀定优势:
可以为类添加⽅法
可以为类添加实例(通过 Runtime),这是协议做不到的
分类⽅便管理
通过添加分类,我们可以为程序员添加各种⽅法,同时通过 Runtime 这⼀⿊魔法实现了动态添加属性,我们可以直接通过点语法为程序员设置座右铭。
同时,分类的⽂件在管理上也⽐较⽅便,如果不想⽤直接删除#import 即可,灵活性较强。
关于 Catagory 添加属性的⽅式可以⾃⾏学习 Runtime
通过消息转发
消息转发也是 Runtime 的⿊魔法,其中⼀个⽤处就是可以实现多继承,当发送消息后找不到对应的⽅法实现时,会经过如下过程:
动态⽅法解析: 通过 resolveInstanceMethod:⽅法,检查是否通过@dynamic 动态添加了⽅法。
直接消息转发: 不修改原本⽅法签名,直接检查 forwardingTargetForSelector:是否实现,若返回⾮ nil 且⾮ self,则向该返回对象直接转发消息。
标准消息转发: 先处理⽅法调⽤再转发消息,重写 methodSignatureForSelector:和 forwardInvocation:⽅法,前者⽤于为该消息创建⼀个合适的⽅法签名,后者则是将该消息转发给其他对象。
上述过程均未实现,则程序异常,通过消息转发,我们实现了动态性,及真正的将⽅法交给其他类来实现,⽽⾮协议或者分类所需要⾃⾏实现。
同时,消息转发也给我们了充分的灵活性,如上代码,我们可以在 Programer 类声明 sing 和 draw ⽅法,但也可不暴露这些接⼜⽽通过类型强转来调⽤这两个⽅法
标准消息转发
标准消息转发相对于直接消息转发更加⾼级,可以由程序员控制转发的过程,同时也可以实现对多个对象的转发,直接消息转发仅能把该⽅法直接转发给其他某对象。
分类的属性实现是通过 Runtime 关联对象,⽽消息转发的属性实现也是类似⽅法。
分类和消息转发更接近于真正意义的多继承,也⽅便管理,添加删除⽗类⽅便。
类扩展是在编译阶段被添加到类中,⽽类别是在运⾏时添加到类中。
20、描述⾯向对象的三⼤特征,并作简单的介绍。
⾯向对象的三个基本特征是:封装、继承、多态。
封装是⾯向对象的特征之⼀,是对象和类概念的主要特性。
封装,也就是把客观事物封装成抽象的类,并且类可以把⾃⼰的数据和⽅法只让可信的类或者对象操作,对不可信的进⾏信息隐藏。隐藏对象的属性和实现细节,仅对外公开接⼜,提⾼代码安全性,封装程度越⾼,独⽴性越强,使⽤越⽅便。
继承是指这样⼀种能⼒:它可以使⽤现有类的所有功能,并在⽆需重新编写原来的类的情况下对这些功能进⾏扩展。 通过 继承创建的新类称为“⼦类”或“派⽣类”。 被继承的类称为“基类”、“⽗类”或“超类”多态性:允许你将⽗对象设置成为和⼀个或更多的他的⼦对象相等的技术,赋值之后,⽗对象就可以根据当前赋值给它的⼦对象的特性以不同的⽅式运作。简单的说,就是⼀句话:允许将⼦类类型的指针赋值给⽗类类型的指针编程的实质就是将⼈类的思想转换成机器可以理解的语⾔的过程。
学习的核⼼问题就是掌握这种思维的⽅式。
OC 是⼀种⾯向对象的语⾔。
⾯向对象是相对⾯向过程⽽⾔。⾯向对象和⾯向过程都是⼀种思想。
例如洗⾐服这件事的不同理解。
⾯向过程的⽅式去理解:准备⾐服已经相关的⽤品,打开洗⾐机,放⼊⾐服和洗⾐液,启动洗⾐机。
⾯向 对象的理解⽅法:买个全⾃动洗⾐机,准备要洗的⾐物及⽤品。或者找个⼈帮你洗,呵呵。
⾯向过程:按⼈们认识客观世界的系统思维⽅式,采⽤基于对象(实体)的概念建⽴模型,
模拟客观世界分析、设计、实现软件的办法。通过⾯向对象的理念使计算机软件系统能与现实世界中的系统⼀⼀对应。
⾯向对象:是⼀种解决软件复⽤的设计和编程⽅法。这种⽅法把软件系统中相近相似的操作逻辑和操作应⽤数据、状态,以类的型式描述出来,以对象实例的形式在软件系统中复⽤,以达到提⾼软件开发效率的作⽤。
它的优点是可以⼤幅度提⾼软件项⽬的成功率,减少维护的费⽤,提⾼可移植性和可靠性。
⾯向对象是相对⾯向过程⽽⾔。⾯向对象和⾯向过程都是⼀种思想。
例如洗⾐服这件事的不同理解。
⾯向过程的⽅式去理解:准备⾐服已经相关的⽤品,打开洗⾐机,放⼊⾐服和洗⾐液,启动洗⾐机。
⾯向 对象的理解⽅法:买个全⾃动洗⾐机,准备要洗的⾐物及⽤品。或者找个⼈帮你洗,呵呵。
⾯向过程:按⼈们认识客观世界的系统思维⽅式,采⽤基于对象(实体)的概念建⽴模型,模拟客观世界分析、设计、实现软件的办法。通过⾯向对象的理念使计算机软件系统能与现实世界中的系统⼀⼀对应。
⾯向对象:是⼀种解决软件复⽤的设计和编程⽅法。这种⽅法把软件系统中相近相似的操作逻辑和操作应⽤数据、状态,以类的型式描述出来,以对象实例的形式在软件系统中复⽤,以达到提⾼软件开发效率的作⽤。它的优点是可以⼤幅度提⾼软件项⽬的成功率,减少维护的费⽤,提⾼可移植性和可靠性。⾯向过程是把问题中的数据⽤算法进⾏描述,强调功能性;⾯向对象是把功能封装进对象,强调问题的解决需要哪些对象的使⽤。⾯向对象的编程思想是基于⾯向过程发展⽽来的,
21、如何实现 APP 的本地化?
需要在 Localizable.strings 下对应的⽂件中,分别以 Key-Value 的形式,为代码中每⼀个需要本地化的字符串赋值
// NSLocalizedString(key, comment) 本质
// NSlocalizeString 第⼀个参数是内容,根据第⼀个参数去对应语⾔的⽂件中取对应的字符串,第⼆个参数将会转化为字符串⽂件⾥的注释,可以传 nil,也可以传空字符串@""。
#define NSLocalizedString(key, comment) [[NSBundle mainBundle] localizedStringForKey:(key) value:@"" table:nil]
key = value CFBundleDisplayName = "App 名称"
22、有没有写过⾃定义的控件?
initWithFrame:中添加⼦控件。
layoutSubviews 中设置⼦控件 frame。
对外设置数据接⼜,重写 setter ⽅法给⼦控件设置显⽰数据。
在 ViewController ⾥⾯使⽤ init/initWithFrame:⽅法创建⾃定义类,并且给⾃定义类的 frame 赋值。
对⾃定义类对外暴露的数据接⼜进⾏赋值即可。
RunLoop 运⾏循环机制
RunLoop(运行循环)是 iOS 和 macOS 中 Foundation 框架提供的一个核心概念,用于管理线程的运行和事件处理。它主要体现在以下三个方面:
**1. 保持程序持续运行:**
每个线程都有一个与之关联的 RunLoop。当线程启动时,RunLoop 会自动启动并持续运行。RunLoop 会不断地从事件源(例如计时器、触摸事件、网络请求等)获取事件,并将这些事件分发给相应的处理程序。只要 RunLoop 还在运行,线程就会一直保持活跃状态,并处理接收到的事件。
**2. 处理 App 中的各种事件:**
RunLoop 可以处理各种类型的事件,包括:
- **触摸事件:** 当用户触摸屏幕时,RunLoop 会收到触摸事件,并将这些事件分发给相应的触摸处理程序。
- **计时器事件:** 当计时器触发时,RunLoop 会收到计时器事件,并将这些事件分发给相应的计时器处理程序。
- **网络请求事件:** 当网络请求完成时,RunLoop 会收到网络请求事件,并将这些事件分发给相应的网络请求处理程序。
- **自定义事件:** 您也可以创建自定义事件并使用 RunLoop 来处理它们。
**3. 节省 CPU 资源,提高程序性能:**
RunLoop 采用了一种称为“运行模式”的机制来提高程序性能。运行模式决定了 RunLoop 在没有事件要处理时如何运行。有两种主要的运行模式:
- **默认运行模式:** 在默认运行模式下,RunLoop 会进入休眠状态,直到有事件要处理时才会唤醒。这可以节省 CPU 资源,提高程序性能。
- **手动运行模式:** 在手动运行模式下,RunLoop 会一直运行,即使没有事件要处理。这通常用于需要实时更新的应用程序,例如游戏或音频播放器。
基本概念
进程
进程是指在系统中正在运⾏的⼀个应⽤程序,⽽且每个进程之间是独⽴的,它们都运⾏在其专⽤且受保护的内存空间内,⽐如同时打开迅雷、
Xcode,系统就会分别启动两个进程。
线程
⼀个⼈进程如果想要执⾏任务,必须得有⾄少⼀条线程,进程的所有任务都会在线程中执⾏,⽐如使⽤⽹易云⾳乐播放⾳乐,使⽤迅雷下载电影,都需要在线程中执⾏。
主线程
iOS 程序运⾏后,系统会默认开启⼀条线程,称为“主线程”或者“UI 线程”,主线程是⽤来显⽰/刷新 UI 界⾯,处理 UI 事件的。
简介
运⾏循环、跑圈
RunLoop 内部是⼀个 do-while 循环。
总结下来,RunLoop 的作⽤主要体现在三⽅⾯:
保持程序的持续运⾏
处理 App 中的各种事件(⽐如触摸事件、定时器事件、Selector 事件)
节省 CPU 资源,提⾼程序性能:该做事的时候做事,该休息的时候休息就是说,如果没有 RunLoop 程序⼀运⾏就结束了,你根本不可能看到持续运⾏的 app。
iOS 中有 2 套 API 访问和使⽤ RunLoop
Foundation:NSRunLoop
Core Foundation: CFRunLoopRef
NSRunLoop 是基于 CFRunLoopRef 的⼀层 OC 包装,因此我们需要研究 CFRunLoopRef 层⾯的 API(Core Foundation 层⾯)
关于 RunLoop 的源码请看这⾥
RunLoop 与线程
源码中,关于创建线程的核⼼代码如下:
// should only be called by Foundation
// t==0 is a synonym for "main thread" that always works
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
if (pthread_equal(t, kNilPthreadT)) {
t = pthread_main_thread_np();
}
__CFLock(&loopsLock);
if (!__CFRunLoops) { // 如果没有线程,则要创建线程
__CFUnlock(&loopsLock);
// 创建⼀个可变字典
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
// 将主线程放进去,创建 RunLoop(也就是说,创建哪个线程的 RunLoop 需要将线程作为参数传⼊)
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
// 将主线程的 RunLoop 和主线程以 key/value 的形式保存。
// 因此由此可以看出,⼀条线程和⼀个 RunLoop 是⼀⼀对应的
CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
CFRelease(dict);
}
CFRelease(mainLoop);__CFLock(&loopsLock);
}
// 当你输⼊ cunrrentRunLoop 时,会通过当前线程这个 key,在字典中寻找对应的 RunLoop
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFUnlock(&loopsLock);
// 如果没有在字典中找到
if (!loop) {
// 则重新创建⼀个 RunLoop
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
__CFLock(&loopsLock);
// 然后将 RunLoop 和线程以 key/value 的形式保存
// 再⼀次验证了 RunLoop 和 key 是⼀⼀对应的
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
if (!loop) {
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
loop = newLoop;
}
// don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
__CFUnlock(&loopsLock);
CFRelease(newLoop);
}
if (pthread_equal(t, pthread_self())) {
_CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
_CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
}
}
return loop;
}
程序启动时,系统会⾃动创建主线程的 RunLoop
每⼀条线程都有唯⼀的⼀个与之对应的 RunLoop 对象
主线程的 RunLoop 已经⾃动创建好了,⼦线程的 RunLoop 需要⼿动创建
RunLoop 在第⼀次获取时创建,在线程结束时销毁
代码:
// 获取当前的线程的 RunLoop 对象,注意 RunLoop 是懒加载,currentRunLoop 时会⾃动创建对象
[NSRunLoop currentRunLoop];
// 获取主线程的 RunLoop 对象
[NSRunLoop mainRunLoop];
// 如果是 CF 层⾯
CFRunLoopGetCurrent();
CFRunLoopGetMain();
RunLoop 相关类
通过:
NSLog(@"%@", [NSRunLoop mainRunLoop]);
可以对 RunLoop 内部⼀览⽆余
Core Foundation 中关于 RunLoop 的 5 个类:
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopObserverRef
RunLoop 想要跑起来,必须有 Mode 对象⽀持,⽽ Mode ⾥⾯必须有
(NSSet *)Source、(NSArray *)Timer,源和定时器。
⾄于另外⼀个类(NSArray *)observer 是⽤于监听 RunLoop 的状态,因此不会激活 RunLoop。
CFRunLoopModeRefCFRunLoopModeRef 代表 RunLoop 的运⾏模式
每个 RunLoop 都包含若⼲个 Mode ,每个 Mode 又包含若⼲个 Source/Timer/Observer,每次
RunLoop 启动时,只能指定其中⼀个 Mode,这个 Mode 被称作 CurrentMode,如果需要切换 Mode,只能退出
Loop,再重新指定⼀个 Mode 进⼊,这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响(可以通过切换
Mode,完成不同的 timer/source/observer)。
[NSRunLoop currentRunLoop].currentMode; // 获取当前运⾏模式
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
系统默认注册了 5 个 Mode:
NSDefaultRunLoopMode:App 的默认 Mode,通常主线程是在这个 Mode 下运⾏(默认情况下运⾏)
UITrackingRunLoopMode:界⾯跟踪 Mode,⽤于 ScrollView 追踪触摸滑动,保证界⾯滑动时不受其他 Mode 影响(操作 UI 界⾯的情况下运⾏)
UIInitializationRunLoopMode:在刚启动 App 时进⼊的第⼀个 Mode,启动完成后就不再使⽤
GSEventReceiveRunLoopMode:接受系统事件的内部 Mode,通常⽤不到(绘图服务)
NSRunLoopCommonModes:这是⼀个占位⽤的 Mode,不是⼀种真正的 Mode (RunLoop ⽆法启动该模式,设置这种模式下,默认和操作 UI 界
⾯时线程都可以运⾏,但⽆法改变 RunLoop 同时只能在⼀种模式下运⾏的本质)
下⾯主要区别 NSDefaultRunLoopMode 和 UITrackingRunLoopMode 以及 NSRunLoopCommonModes。请看以下代码:
- (void)viewDidLoad {
[super viewDidLoad];
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
// 在默认模式下添加的 timer 当我们拖拽 textView 的时候,不会运⾏ run ⽅法
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
// 在 UI 跟踪模式下添加 timer 当我们拖拽 textView 的时候,run ⽅法才会运⾏
[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
// timer 可以运⾏在两种模式下,相当于上⾯两句代码写在⼀起
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}
- (void)run
{
NSLog(@"--------run");
}
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
[self performSelectorOnMainThread:@selector(run) withObject:nil waitUntilDone:YES modes:@[UITrackingRunLoopMode]];
[self performSelectorOnMainThread:@selector(run) withObject:nil waitUntilDone:YES modes:@[NSRunLoopCommonModes]];
CFRunLoopTimerRef
CFRunLoopTimerRef 是基于事件的触发器
CFRunLoopTimerRef 基本上就是 NSTimer,它受 RunLoop 的 Mode 影响
创建 Timer 有两种⽅式,下⾯的这种⽅式必须⼿动添加到 RunLoop 中去才会被调⽤
// 这种⽅式创建的 timer 必须⼿动添加到 RunLoop 中去才会被调⽤
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(time) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer
forMode:NSDefaultRunLoopMode];
// 同时让 RunLoop 跑起来
[[NSRunLoop currentRunLoop] run];
⽽通过 scheduledTimer 创建 Timer ⼀开始就会⾃动被添加到当前线程并且以
NSDefaultRunLoopMode 模式运⾏起来,代码如下:
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
/*
注意:调⽤了 scheduledTimer 返回的定时器,已经⾃动被添加到当前
runLoop 中,⽽且是 NSDefaultRunLoopMode ,想让上述⽅法起作⽤,
必须先让添加了上述 timer 的 RunLoop 对象 run 起来,通过
scheduledTimerWithTimeInterval 创建的 timer 可以通过以下⽅法修改 mode
*/
[[NSRunLoop currentRunLoop] addTimer:timer2 forMode:UITrackingRunLoopMode];
注意: GCD 的定时器不受 RunLoop 的 Mode 影响
CADisplayLink *display = [CADisplayLink displayLinkWithTarget:self selector:@selector(run)];/*
注意:CADisplayLink ,也是在 Runloop 下运⾏的,
有⼀个⽅法可以将 CADisplayLink 对象添加到⼀个 Runloop 对象中去
*/
[display addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
CFRunLoopSourceRef
CFRunLoopSourceRef 其实是事件源(输⼊源)
按照官⽅⽂档,Source 的分类
Port-Based Sources:基于端⼜的:跟其他线程进⾏交互的,Mac 内核发过来⼀些消息
Custom Input Sources:⾃定义输⼊源
Cocoa Perform Selector Sources(self performSelector:...)
按照函数调⽤栈,Source 的分类
Source0:⾮基于 Port 的(触摸事件、按钮点击事件),不能触动出发
Source1:基于 Port 的,通过内核和其他线程通信,接收分发系统事件,基于 mach_Port 的,来⾃系统内核或者其他进程或线程的事件
(触摸硬件,通过 Source1 接收和分发系统事件到 Source0 处理)
为了搞清楚,Source 是如何通过函数调⽤栈来传递事件的,我们做如下实验:
我们可以看到,从程序启动 start 开始,函数调⽤栈在监听到事件点击后,会⼀路往下,⼀直到-buttonClick:⽅法,中间会经过
CFRunLoopSource0,这说明我们的按钮点击事件是属于 Source0 的。
⽽ Source1 是基于 Port 的,就是说,Source1 是和硬件交互的,触摸⾸先在屏幕上被包装成⼀个 event 事件,再通过 Source1 进⾏分发到
Source0,最后通过 Source0 进⾏处理。
CFRunLoopObserverRef
CFRunLoopObserverRef 是观察者,能够监听 RunLoop 的状态改变,主要监听以下⼏个时间节点:
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity)
{
kCFRunLoopEntry = (1UL << 0), // 1 即将进⼊ Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 2 即将处理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 4 即将处理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 32 即将进⼊休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 64 刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7), // 128 即将退出 Loop
kCFRunLoopAllActivities = 0x0FFFFFFFU // 监听所有事件
};
// 1.创建观察者 监听 RunLoop
// 参 1: 有个默认值 CFAllocatorRef :CFAllocatorGetDefault()
// 参 2: CFOptionFlags activities 监听 RunLoop 的活动 枚举 见上⾯
// 参 3: 重复监听 Boolean repeats YES
// 参 4: CFIndex order 传 0
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0,
^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
// 该⽅法可以在添加 timer 之前做⼀些事情, 在添加 source 之前做⼀些事情
NSLog(@"%zd", activity);
});
// 2.添加观察者,监听当前的 RunLoop 对象
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
// CF 层⾯的东西 凡是带有 create、copy、retain 等字眼的函数在 CF 中要进⾏内存管理
CFRelease(observer);
通过打印可以观察的 RunLoop 的状态
补充:在进⼊第⼀个阶段前,会先判断当前 RunLoop 空不空, 如果是空的 直接来到 10 阶段!
RunLoop 的应⽤
NSTimer
需求 让定时器 在其他线程开启
NSBlockOperation *block = [NSBlockOperation blockOperationWithBlock:^{
// 这种⽅式创建的 timer 必须⼿动添加到 Runloop 中去才会被调⽤NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(time) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
// 同时让 RunLoop 跑起来
[[NSRunLoop currentRunLoop] run];
}];
[[[NSOperationQueue alloc] init] addOperation:block];
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] run];
[[NSRunLoop currentRunLoop] addTimer:timer2 forMode:UITrackingRunLoopMode];
ImageView:显⽰ performSelector
需求
有时候,⽤户拖拽 scrollView 的时候,mode:UITrackingRunLoopMode,显⽰图⽚,如果图⽚很⼤,会渲染⽐较耗时,造成不好的体验,因此,设置当⽤户停⽌拖拽的时候再显⽰图⽚,进⾏延迟操作
⽅法 1:设置 scrollView 的 delegate 当停⽌拖拽的时候做⼀些事情
⽅法 2:使⽤ performSelector 设置模式为 default 模式 ,则显⽰图⽚这段代码只能在 RunLoop 切换模式之后执⾏
// 加载⽐较⼤的图⽚时,
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
// inModes 传⼊⼀个 mode 数组,这句话的意思是
// 只有在 NSDefaultRunLoopMode 模式下才会执⾏ seletor 的⽅法显⽰图⽚
[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"avater"] afterDelay:3.0
inModes:@[NSDefaultRunLoopMode]];
}
效果为:当⽤户点击之后,下载图⽚,但是图⽚太⼤,不能及时下载。这时⽤户可能会做些其他 UI 操作,⽐如拖拽,但是如果⽤户正在拖拽浏览其他的东西时,图⽚下载完毕了,此时如果要渲染显⽰,会造成不好的⽤户体验,所以当⽤户拖拽完毕后,显⽰图⽚。
这是因为,⽤户拖拽,处于 UITrackingRunLoopMode 模式下,所以图⽚不会显⽰。
常驻线程
需求:
搞⼀个线程⼀直存在,⼀直在后台做⼀些操作 ⽐如监听某个状态, ⽐如监听是否联⽹。
- (void)viewDidLoad {
[super viewDidLoad];
// 需求:搞⼀个线程⼀直不死,⼀直在后台做⼀些操作 ⽐如监听某个状态, ⽐如监听是否联⽹。
// 需要在线程中开启⼀个 RunLoop ⼀个线程对应⼀个 RunLoop 所以获得当前 RunLoop 就会⾃⼰创建 RunLoop
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run2) object:nil];
self.thread = thread;
[thread start];
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
[self performSelector:@selector(run) onThread:self.thread withObject:nil waitUntilDone:NO];
}
- (void)run2
{
NSLog(@"----------");
/*
* 创建 RunLoop,如果 RunLoop 内部没有添加任何 Source Timer
* 会直接退出循环,因此需要⾃⼰添加⼀些 source 才能保持 RunLoop 运转
*/
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
// [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
[[NSRunLoop currentRunLoop] run];
NSLog(@"-----------22222222");}
从 RunLoop 的源码看来,如果⼀个 RunLoop 中没有添加任何的 Source Timer,会直接退出循环。
⾃动释放池
RunLoop 循环时,在进⼊睡眠之前会清掉⾃动释放池,并且创建⼀个新的释放池,⽤于内部变量的销毁。
在⼦线程开 RunLoop 的时候⼀定要⾃⼰写⼀个@autoreleasepool,⼀个 RunLoop 对应⼀条线程,⾃动释放池是针对当前线程⾥⾯的对象。
- (void)viewDidLoad {
[super viewDidLoad];
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(excute) object:nil];
self.thread = thread;
[thread start];
}
- (void)excute
{
@autoreleasepool {
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(text) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:
forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
}
}
这样保证了内存安全。
`谈谈你对于索引计数的理解?`
retain 值 = 索引计数(ReferenceCounting) NSArray 对象会 retain(retain 值加⼀)任何数组中的对象。当 NSArray 被卸载 (dealloc)的时候,所有数组中的对象会被执⾏⼀次释放(retain 值减⼀)。不仅仅是 NSArray,任何收集类 (CollectionClasses)都执⾏类似操作。例如 NSDictionary,甚⾄ UINavigationController。Alloc/init 建⽴的对象,索引计数为 1。⽆需将其再次 retain [NSArray array]和[NSDate date]等“⽅法”建⽴⼀个索引计数为 1 的对象,但是也是⼀个⾃动释放对象所以是本地临时对象,那么⽆所谓了。如果是打算在全 Class 中使⽤的变量(iVar),则必须 retain 它。缺省的类⽅法返回值都被执⾏了“⾃动释放”⽅法。 (*如上中的 NSArray)在类中的卸载⽅法“dealloc”中,release 所有未被平衡的 NS 对象。(*所有未被 autorelease,⽽ retain
值为 1 的)
Runloop 怎么取消?
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self performSelector:@selector(asdf) withObject:nil afterDelay:1];
[[NSRunLoop currentRunLoop] runMode:
][oiuytrew beforeDate:[NSDate distantFuture]];
});
-(void)asdf{
CFRunLoopStop([NSRunLoop currentRunLoop].getCFRunLoop); dispatch_sync(dispatch_get_global_queue(0, 0), ^{
[self performSelector:@selector(asdasd) withObject:nil afterDelay:5];
});
}
-(void)asdasd{
//runloop 已关闭,不会执⾏。
}
正在执⾏中的取消不了
23、runloop 和线程有和关系?
runloop:字⾯意思就是跑圈,其实也就是⼀个循环跑圈,⽤来处理线程⾥⾯的事件和消息。
runloop 和线程的关系:每个线程如果想继续运⾏,不被释放,就必须有⼀个 runloop 来不停的跑圈,以来处理线程⾥⾯的各个事件和消息。主线程默认是开启⼀个 runloop。也就是这个 runloop 才能保证我们程序正常的运⾏。⼦线程是默认没有开始 runloop 的⼀条线程对应⼀个 RunLoop 对象,每条线程都有唯⼀⼀个与之对应的 RunLoop 对象。我们只能在当前线程中操作当前线程的 RunLoop,⽽不能去操作其他线程的 RunLoop。RunLoop 对象在第⼀次获取 RunLoop 时创建,销毁则是在线程结束的时候。主线程的 RunLoop 对象系统⾃动帮助我们创建好了(原理如下),⽽⼦线程的 RunLoop 对象需要我们主动创建。
runloop 是来管理线程的,当线程的 runloop 被开启后,线程会在执⾏完任务后进⼊休眠状态,有了任务就会被唤醒去执⾏任务。
关于这两者的更多关系:
runloop 与线程是⼀⼀对应的,⼀个 runloop 对应⼀个核⼼的线程,为什么说是核⼼的,是因为 runloop 是可以嵌套的,但是核⼼的只能有⼀个,
他们的关系保存在⼀个全局的字典⾥。
runloop 在第⼀次获取时被创建,在线程结束时被销毁。
对于主线程来说,runloop 在程序⼀启动就默认创建好了。
对于⼦线程来说,runloop 是懒加载的,只有当我们使⽤的时候才会创建,所以在⼦线程⽤定时器要注意:确保⼦线程的 runloop 被创建,不然定时器不会回调。
NSTimer为什么不准
1:Runloop Timer 底层使⽤的 timer 精度不⾼;
2:与 Runloop 底层的调⽤机制有关系。
情况产⽣:
1:NSTimer 被添加在 mainRunLoop 中,模式是 NSDefaultRunLoopMode,mainRunLoop 负责所有主线程事件,例如 UI 界⾯的操作,复杂的运
算使当前 RunLoop 持续的时间超过了定时器的间隔时间,那么下⼀次定时就被延后,这样就会造成 timer 的阻塞、
2:模式的切换,当创建的 timer 被加⼊到 NSDefaultRunLoopMode 时,此时如果有滑动 UIScrollView 的操作,runLoop 的 mode 会切换为 TrackingRunLoopMode,这是 timer 会停⽌回调。
解决:
1:在⼦线程中创建 timer,在主线程进⾏定时任务的操作或者在⼦线程中创建 timer,在⼦线程中进⾏定时任务的操作,需要 UI 操作时切换回主
线程进⾏操作。
2:CGD 操作:dispatch_source_create,创建定时器,dispatch_source_set_timer :设置定时器。dispatch_resume:启动。
3:CADisplayLink(频率能达到屏幕刷新率的定时器类):displayLinkWithTarget,addToRunLoop
NSTimer 循环引⽤解决⽅案
timer 作为 VC 的属性,被 VC 强引⽤,创建 timer 对象时 VC 作为 target 被 timer 强
引⽤,即循环引⽤。
既然是强引⽤导致循环引⽤,那么⽤__weak 修饰 self 就好了,想法是对的,但是做
法是⽆效的。因为⽆论是 weak 还是 strong 修饰,在 NSTimer 中都会重新⽣成⼀个新的强引
⽤指针指向 self,导致循环引⽤的。
VC 强引⽤ timer,因为 timer 的 target 是 TempTarget 实例,所以 timer 强引⽤
TempTarget 实例,⽽ TempTarget 实例弱引⽤ VC,解除循环引⽤.
原理跟类⽅法相似,打破循环引⽤的环路。将 timer 的 target 设置为 WeakProxy 实例,利⽤消息转发机制实现执⾏ VC 中的计时⽅法,解决循环引⽤。