swift和OC的区别
- Swift 是强类型语言,编译时提供类型安全保证。Swift 具有类型推断功能,可以根据上下文自动推断变量的类型,从而减少代码中的类型声明。
- OC 中类型检查较宽松,且不支持类型推断,开发者需要手动指定类型。另外,OC 中大部分对象是动态类型的(
id类型),使得在运行时进行类型检查。
| 特性 | Swift | Objective-C |
|---|---|---|
| 语法简洁性 | 现代、简洁 | 语法复杂,带有方括号和分号 |
| 类型系统 | 强类型,类型安全,支持类型推断 | 动态类型检查,类型较宽松 |
| 内存管理 | ARC 自动内存管理,支持闭包 | ARC 自动内存管理,支持对象 |
| 错误处理 | do-catch,基于 throw | 使用 NSError |
| 闭包 | 简洁的闭包语法,支持捕获外部变量 | Block 语法繁琐 |
| 扩展与协议 | 支持扩展和协议,协议可用于结构体和枚举 | 支持分类和协议,协议仅限类实现 |
| 编程范式 | 面向对象和函数式编程 | 主要是面向对象编程 |
| 可选类型 | 可选类型(Optional),避免 nil 错误 | 对象可以为 nil,没有明确的可选类型 |
| 枚举 | 支持关联值和方法,功能强大 | 只支持简单的枚举类型 |
OC和Swift枚举
-
功能性:
- Objective-C:枚举基本上是带有命名的整数值,功能较为简单。
- Swift:枚举支持关联值、原始值、方法和计算属性,功能强大,灵活性高。
-
类型安全:
- Objective-C:使用
NS_ENUM和NS_OPTIONS宏可以提高类型安全性,但仍然有限。 - Swift:枚举是第一类类型,提供强类型检查,减少错误。
- Objective-C:使用
-
模式匹配:
- Objective-C:没有原生的模式匹配功能。
- Swift:通过
switch语句和模式匹配,可以非常简洁地处理不同的枚举情况。
-
定义方法和属性:
- Objective-C:枚举不能定义方法和属性。
- Swift:枚举可以定义实例方法、类型方法和计算属性。
Swift 中 类(class) 和 结构体(struct) 的区别,以及各自优缺点
一、类 (Class) 和 结构体 (Struct) 的主要区别
-
引用类型 vs. 值类型:
- 类(Class) :引用类型。对象通过引用传递,多个变量可以引用同一个实例,更改一个变量会影响所有引用该实例的变量。
- 结构体(Struct) :值类型。实例通过值传递,每次赋值或传递都会创建一个新的副本,更改副本不会影响原始实例。
-
继承:
- 类(Class) :支持继承,可以从另一个类继承方法、属性和其他特性。
- 结构体(Struct) :不支持继承。
-
内存管理:
- 类(Class) :使用引用计数(ARC)来管理内存,涉及到强引用、弱引用和无主引用等概念。
- 结构体(Struct) :由于是值类型,不需要引用计数,内存管理相对简单。
-
构造器:
- 类(Class) :必须为所有属性初始化提供构造器,支持指定构造器和便利构造器。
- 结构体(Struct) :自动生成成员逐一构造器,可以手动定义自定义构造器。
-
方法可变性:
- 类(Class) :方法默认是可变的,可以直接修改属性。
- 结构体(Struct) :方法默认是不可变的,只有标记为
mutating的方法才能修改属性。
OC能调用swift的结构体模型吗
OC 可以调用 Swift 的类和协议,但调用 Swift 的结构体和枚举有一些限制,因为它们的内存布局和调用机制在底层实现上有所不同。然而,你可以通过一些间接的方法来实现这一点。以下是一些方法和技巧:
-
将 Swift 结构体封装在 Swift 类中
- 你可以创建一个 Swift 类来封装 Swift 结构体,然后在 OC 中调用这个 Swift 类。
知识扩散:
-
暴露给OC的成员用@objc修饰
-
@objcMembers
- 默认行为:
@objcMembers会将类中所有的属性和方法自动标记为@objc,使它们可以被 Objective-C 代码调用。 - 继承:子类会继承
@objcMembers的行为,如果父类标记了@objcMembers,则子类中的属性和方法也会自动标记为@objc。 - NSObject 子类:
@objcMembers通常与继承自NSObject的类一起使用,因为只有NSObject子类才能被完全暴露给 Objective-C。
- 默认行为:
一、为什么Swift 暴露给 OC 的类 要最终 继承 NSObject
1. 兼容性和互操作性
OC 是基于 C 语言的面向对象扩展,而 NSObject 是 OC 类的基类,提供了许多基础设施,使得 OC 代码可以正常工作。通过继承 NSObject,Swift 类可以:
- 参与 OC 的运行时系统,包括动态方法调度、消息传递等。
- 使用 OC 的反射(reflection)功能,例如
respondsToSelector:和performSelector:withObject:afterDelay:等方法。 - 处理 OC 的内存管理,包括引用计数(reference counting)。
2. 动态特性支持
OC 提供了许多动态特性,这些特性依赖于 NSObject 类。通过继承 NSObject,Swift 类可以:
- 支持 OC 的 KVC(键值编码)和 KVO(键值观察)。
- 支持动态添加和替换方法(method swizzling)。
- 参与 OC 的分类(category)和协议(protocol)扩展。
3. 集成现有的 OC API
很多现有的 OC API 都要求对象是 NSObject 的子类。为了使 Swift 类能够无缝地使用这些 API,它们需要继承 NSObject。例如,Foundation 框架中的许多类和方法都依赖于 NSObject。
4. Interface Builder 和 Storyboards 支持
在使用 Interface Builder 和 Storyboards 时,很多 UI 组件和控件依赖于 NSObject 作为它们的基类。通过继承 NSObject,Swift 类可以更容易地与这些工具集成,支持 UI 设计和布局。
怎么理解 Swift中的泛型约束
泛型约束是对泛型类型参数施加的限制,确保这些类型参数满足某些条件,例如遵循特定协议或继承自特定类。泛型约束使用 where 子句或直接在类型参数后面指定。
一、使用泛型约束的原因
- 保证类型安全:确保泛型类型参数满足特定条件,避免运行时错误。
- 增强代码可读性:明确泛型类型参数的要求,使代码更易于理解。
- 提高代码的灵活性:允许泛型类型参数在不同的上下文中使用,但仍然满足特定条件。
二、泛型约束的语法
泛型约束可以通过以下两种方式指定:
-
在类型参数声明时指定:
- 语法:
<T: SomeProtocol> - 示例:
func someFunction<T: Equatable>(param: T) { }
- 语法:
-
使用
where子句指定:- 语法:
where T: SomeProtocol - 示例:
func someFunction<T>(param: T) where T: Equatable { }
- 语法:
三、关联类型和泛型约束
在定义协议时,可以使用关联类型来定义协议中的泛型类型,并在关联类型上添加约束:
protocol Container {
associatedtype Item: Equatable
var count: Int { get }
subscript(i: Int) -> Item { get }
}
struct Stack<Element: Equatable>: Container {
private var items = [Element]()
var count: Int {
return items.count
}
subscript(i: Int) -> Element {
return items[i]
}
mutating func push(_ item: Element) {
items.append(item)
}
mutating func pop() -> Element {
return items.removeLast()
}
}
Swift 和OC中的初始化方法
-
Objective-C:初始化方法通过
init系列方法定义,分配内存和初始化分为两步。指定初始化方法调用[super init]初始化父类,便捷初始化方法调用其他初始化方法。 -
Swift:初始化方法通过
init定义,具有更严格的初始化规则,包括两段式初始化过程和安全检查。便捷初始化方法使用convenience关键字。
一、Objective-C 中的初始化方法
在 Objective-C 中,初始化方法通常分为两步:
- 分配内存:通过调用
alloc方法。 - 初始化对象:通过调用
init方法或自定义的初始化方法。
1. 重要概念
- 返回类型:初始化方法的返回类型是
instancetype,表示返回与接收者类型相同的实例。这在继承时非常有用。 - 调用父类初始化方法:在自定义初始化方法中,通常先调用
[super init]来初始化父类部分。 - self 检查:初始化方法通常会检查
self是否为nil,以确保父类初始化成功。
二、Swift 中的初始化方法
Swift 中的初始化方法分为指定初始化方法和便捷初始化方法。Swift 的初始化方法有更严格的规则,以确保对象在使用前完全初始化。在 Swift 中,初始化方法使用 init 关键字定义。
1.重要概念
- 指定初始化方法(Designated Initializers) :是类的主要初始化方法,确保所有属性被初始化,并且调用父类的初始化方法。
- 便捷初始化方法(Convenience Initializers) :用
convenience关键字标识,必须调用同一个类中的另一个初始化方法,最终调用到指定初始化方法。
2.初始化规则
-
初始化的两段式过程:
- 第一阶段:类及其所有父类的存储属性被分配内存,但未初始化。
- 第二阶段:每个类的存储属性被初始化,之后可以使用其他初始化方法或实例方法。
-
安全检查:
- 所有属性必须在初始化方法中设置初值。
- 在调用父类初始化方法之前,子类的属性必须先初始化。
- 便捷初始化方法必须最终调用一个指定初始化方法。
三、 Convenience Initializers(便捷初始化方法)
便捷初始化方法通常是调用一个或多个指定初始化方法来完成初始化。它们的返回类型通常是 instancetype,并且不能直接调用 super。
四、 UIViewcontroller生命周期
init(nibName:bundle:)或init(coder:)- 控制器初始化时被调用。使用代码或通过
storyboard进行初始化时调用不同的初始化方法。
- 控制器初始化时被调用。使用代码或通过
loadView- 当视图控制器的视图被访问,但尚未加载时,
loadView被调用。 - 如果你没有重写该方法,系统会默认从
nib文件或storyboard中加载视图。
- 当视图控制器的视图被访问,但尚未加载时,
viewDidLoad- 当视图控制器的视图已经加载到内存中时调用,此时视图层次结构已经建立,但尚未显示到屏幕上。
viewWillAppear(_:)- 每当视图控制器的视图即将显示在屏幕上时调用。在视图出现之前调用,无论是首次显示还是每次重新显示。
updateViewConstraints- 视图控制器的view开始更新AutoLayout约束。
viewWillLayoutSubviews- 视图控制器的view将要更新内容视图的位置。
viewDidAppear(_:)- 当视图控制器的视图已经完全显示在屏幕上时调用。
viewWillDisappear(_:)- 当视图控制器的视图即将从屏幕上消失时调用。
viewDidDisappear(_:)- 当视图控制器的视图已经完全从屏幕上消失时调用。
deinit- 视图控制器被释放时调用。
Swift和OC中的 protocol 有什么不同
Swift 和 OC 中的 protocol 在语法和功能上有许多相似之处,但 Swift 提供了更多高级特性,如关联类型、泛型、协议扩展和更强大的类型系统,使得 Swift 的协议编程更加灵活和强大。理解这些区别有助于在两种语言中编写更加健壮和可维护的代码。
-
可选方法
- 在 OC 中,协议可以定义可选方法和必需方法。可选方法通过
@optional关键字标记 - 在 Swift 中,协议默认只有必需方法。如果需要定义可选方法,协议必须继承自
NSObjectProtocol,并且可选方法需要使用@objc标记
- 在 OC 中,协议可以定义可选方法和必需方法。可选方法通过
-
属性和方法要求
- 在 OC 中,协议可以定义方法和属性要求,但属性通常通过方法来表示
- 在 Swift 中,协议可以直接定义属性要求,并且可以指定属性是只读还是读写
-
关联类型和泛型
- Objective-C 中的协议不支持关联类型和泛型。协议只能定义方法和属性的要求,而不能对类型进行参数化
- Swift 中的协议支持关联类型和泛型,这使得协议更加灵活和强大。关联类型使用
associatedtype关键字定义
-
扩展
- 在 Objective-C 中,无法直接为协议添加扩展实现,只能通过类别(category)为类添加方法
- Swift 支持为协议添加默认实现(扩展),使得协议更强大和灵活
线程
一、进程与线程
- 进程
进程是系统进行资源分配和调度的基本单位,在iOS上,一个App运行起来的实例就是一个进程,每个进程在内存中都有自己独立的地址段。
- 线程
线程是进程的基本执行单元,进程中的所有任务都在线程中执行,因此,一个进程中至少要有一个线程。iOS程序启动后会默认开启一个主线程,也叫UI线程。
-
进程与线程的关系
-
地址空间:同一进程中的地址空间可以被本进程中的多个线程共享,但进程与进程之间的地址空间是独立的
-
资源拥有:同一进程中的资源可以被本进程中的所有线程共享,如内存、I/O、CUP等等,但进程与进程之间的资源是相互独立的
-
一个进程中的任一线程崩溃后,都会导致整个进程崩溃,但进程奔溃后不会影响另一个进程
-
进程可以看做是线程的容器,每个进程都有一个程序运行的入口,但线程不能独立运行,必须依存于进程
-
二、线程如何通信
1. 官方文档方法:
-
Direct messaging:-perform Selector:系列。 -
Global variables,shared memory and objects:直接通过全局变量、共享内存等方式,但这种方式会造成资源抢夺,涉及到线程安全问题。 -
Conditions:一种特殊的锁--条件锁,当使用条件锁使一个线程等待(wait)时,该线程会被阻塞并进入休眠状态,在另一个线程中对同一个条件锁发送信号(single),则等待中的线程会被唤醒继续执行任务。 -
Run loop sources:通过自定义Run loop sources来实现,后面有Run loop。 -
Ports and sockets:通过端口和套接字来实现线程间通讯。
2. 实际方法主要包括以下几种:
-
Grand Central Dispatch (GCD):
- GCD是一种低级别的API,用于管理并发任务,允许轻松在不同队列之间切换。
// 后台任务
DispatchQueue.global(qos: .background).async {
let result = performSomeBackgroundTask()
// 切换到主线程更新UI
DispatchQueue.main.async {
updateUI(with: result)
}
}
-
OperationQueue 和 Operation:
OperationQueue是一个高级并发API,可以管理和控制操作的依赖关系、优先级等。
let backgroundQueue = OperationQueue()
backgroundQueue.addOperation {
let result = performSomeBackgroundTask()
OperationQueue.main.addOperation {
updateUI(with: result)
}
}
-
NSNotificationCenter:
- 使用通知中心在应用的不同部分之间传递信息,不论它们是否在同一线程上运行。
// 在后台线程发布通知
NotificationCenter.default.post(name: .customNotification, object: nil)
// 在主线程观察通知
NotificationCenter.default.addObserver(forName: .customNotification, object: nil, queue: .main) { notification in
updateUI()
}
-
Perform Selector:
- 在需要在主线程上调用某个方法时,可以使用
performSelector方法。
- 在需要在主线程上调用某个方法时,可以使用
self.performSelector(onMainThread: #selector(updateUI), with: result, waitUntilDone: false)
-
Key-Value Observing (KVO) :
- KVO允许一个对象监听另一个对象属性的变化,可以在主线程上监听后台线程修改的属性。
class MyClass: NSObject {
@objc dynamic var value: String = ""
}
let myObject = MyClass()
// 观察属性变化
myObject.addObserver(self, forKeyPath: "value", options: .new, context: nil)
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "value" {
// 更新UI
updateUI(with: change?[.newKey] as? String)
}
}
DispatchQueue.global(qos: .background).async {
myObject.value = "newValue"
}
-
Async/Await (Swift Concurrency) :
- Swift 5.5引入了新的并发特性,通过async/await语法可以更方便地进行异步编程和线程间通信。
func fetchData() async {
let result = await performSomeBackgroundTask()
await MainActor.run {
updateUI(with: result)
}
}
Task {
await fetchData()
}
-
RunLoop:
- 可以使用RunLoop在主线程和后台线程之间进行通信,特别是在涉及到长时间运行的任务时。
class MyWorker {
var shouldKeepRunning = true
func startWorker() {
DispatchQueue.global(qos: .background).async {
while self.shouldKeepRunning {
// 处理任务
performSomeBackgroundTask()
// 通过RunLoop切换到主线程更新UI
RunLoop.main.perform {
updateUI()
}
// 让RunLoop休眠
Thread.sleep(forTimeInterval: 1)
}
}
}
func stopWorker() {
shouldKeepRunning = false
}
}
三、GCD栅栏函数
1. 什么是 GCD 栅栏?
- GCD 栅栏通过
dispatch_barrier_async或dispatch_barrier_sync方法来实现。它允许在一个并发队列中执行栅栏任务,确保栅栏前的所有任务都执行完毕后,才开始执行栅栏任务,并且在栅栏任务执行完成后,才继续执行栅栏后的任务。
2. 工作原理
-
并发队列中的任务执行顺序:
- 在 GCD 并发队列中,任务通常是并发执行的,彼此之间没有严格的顺序。
-
栅栏任务的行为:
- 当栅栏任务到达时,GCD 会确保栅栏任务前面的所有任务都已经执行完毕,才开始执行栅栏任务。
- 栅栏任务执行时,队列中不会并发执行其他任务(即使是并发队列)。
- 栅栏任务执行完成后,队列中的其他任务才会继续执行,并恢复并发行为。
// 创建并发队列
let concurrentQueue = DispatchQueue(label: "com.example.concurrentQueue", attributes: .concurrent)
// 添加并发任务
concurrentQueue.async {
print("任务 1")
}
concurrentQueue.async {
print("任务 2")
}
// 添加栅栏任务
concurrentQueue.async(flags: .barrier) {
print("栅栏任务 - 等待前面的任务完成后执行")
}
// 栅栏后的任务
concurrentQueue.async {
print("任务 3")
}
concurrentQueue.async {
print("任务 4")
}
四、线程锁
- 互斥锁(Mutex) :
- 互斥锁是最常见的线程锁,用于确保多个线程互斥地访问共享资源。
- 只有一个线程可以持有互斥锁,当一个线程持有锁时,其他线程必须等待直到该线程释放锁。
//OC写法
@synchronized(self) {
// 线程安全的代码块
}
-------------------------------------
//swift写法
let mutex = NSLock()
mutex.lock()
// 线程安全的代码块
mutex.unlock()
-
递归锁(Recursive Lock) :
- 递归锁允许同一线程多次获取同一把锁,而不会导致死锁。每次获取锁时,锁的计数器加一,释放锁时计数器减一,直到计数器为零时,锁才真正释放。
- 适用于递归函数或需要多次锁定的场景。
let recursiveLock = NSRecursiveLock()
recursiveLock.lock()
// 线程安全的代码块
recursiveLock.unlock()
-
读写锁(Read-Write Lock) :
- 读写锁允许多个线程同时读取共享资源(读操作),但写操作是互斥的。当一个线程在写入时,其他线程(无论是读还是写)都必须等待。
- 适用于读多写少的场景,提高了并发性能。
let rwLock = DispatchQueue(label: "com.example.rwlock", attributes: .concurrent)
// 读操作
rwLock.sync {
// 线程安全的读操作
}
// 写操作
rwLock.async(flags: .barrier) {
// 线程安全的写操作
}
-
旋锁(Spin Lock) :(注意:自旋锁在 iOS 中较少使用,容易导致高 CPU 占用)
- 自旋锁是一种忙等待的锁,线程在获取锁时会一直循环检查锁的状态,直到获取锁为止。
- 适用于锁定时间非常短的场景,因为自旋锁不涉及线程的睡眠和唤醒操作,开销较低。
-
条件锁(Condition Lock) :
- 条件锁是一种特殊的锁,线程可以在特定的条件满足时获取锁。它允许线程在等待某个条件时释放锁,当条件满足后重新获取锁并继续执行。
let conditionLock = NSConditionLock(condition: 0)
conditionLock.lock(whenCondition: 1)
// 线程安全的代码块
conditionLock.unlock(withCondition: 2)
五、死锁
iOS线程死锁是一种在多线程编程中常见的问题,当两个或多个线程互相等待对方释放资源时,就会发生死锁,从而导致程序无法继续执行。以下是关于iOS线程死锁原理的详细介绍:
-
死锁产生的条件
- 互斥条件:指共享资源的互斥使用,即同一时刻只能有一个线程访问该资源。
- 持有并等待条件:一个线程已经持有了至少一个资源,但又在等待其他线程持有的资源。
- 不可剥夺条件:线程所获得的资源在未使用完毕之前不能被其他线程强行夺走,只能由持有该资源的线程主动释放。
- 环路等待条件:存在一个线程等待序列{P1,P2,...,Pn},其中P1等待P2持有的资源,P2等待P3持有的资源,依此类推,直到Pn等待P1持有的资源,形成一个闭环。
-
死锁发生的示例
- 主线程同步调用自身:在主线程中尝试同步调用自己,从而导致主线程被阻塞,无法完成当前的任务,这将导致死锁的发生。例如,在
viewDidLoad方法中,如果主线程同步调用自己,将会导致程序崩溃。 - 串行队列中的同步操作:在串行队列(如主队列)中,如果某个线程A已经持有了该队列的同步锁,并且在这个锁还未释放的情况下,又试图在该队列中进行同步操作,那么就会形成死锁。
- 主线程同步调用自身:在主线程中尝试同步调用自己,从而导致主线程被阻塞,无法完成当前的任务,这将导致死锁的发生。例如,在
Runloop的理解
RunLoop 是一个事件处理循环,用于调度和处理各种事件和消息。它是 iOS 和 macOS 应用程序的核心机制之一,负责管理输入源(如触摸事件、定时器、网络事件等)并保持应用的响应性。一个 RunLoop 只会在有事件需要处理时运行,否则它会进入休眠状态以节省资源。每个线程都有一个与之关联的 RunLoop,主线程的 RunLoop 在应用启动时自动启动,而其他线程的 RunLoop 需要手动配置和启动。
一、RunLoop 的工作原理
RunLoop 的工作原理可以分为以下几个步骤:
- 进入循环:RunLoop 进入一个循环,不断等待和处理事件。
- 处理输入源:当有事件到达时,RunLoop 会检查并处理输入源,包括定时器、触摸事件、网络事件等。
- 执行回调:处理完事件后,RunLoop 会调用相应的回调方法,如事件处理函数、委托方法等。
- 进入休眠:如果没有事件需要处理,RunLoop 会进入休眠状态,直到新的事件到达。
二、RunLoop 的组成部分
RunLoop 由以下几个组成部分构成:
-
Input Sources(输入源) :
- Port-Based Sources:基于端口的输入源,用于处理系统事件,如触摸事件、网络事件等。
- Custom Input Sources:自定义输入源,可以手动创建和管理,用于处理自定义事件。
-
Timer Sources(定时源) :
- 定时器(NSTimer):用于在特定时间间隔后触发回调方法。
- CADisplayLink:用于同步屏幕刷新。
-
RunLoop Modes(运行模式) :
-
RunLoop 可以在不同的模式下运行,每个模式可以包含不同的输入源和定时源。常见的模式包括:
- NSDefaultRunLoopMode:默认模式,有事件响应的时候,会阻塞旧事件
- UITrackingRunLoopMode:有事件的时候才会响应的模式,用于跟踪 UI 事件,如滚动。
- NSRunLoopCommonModes:普通模式,不会影响任何事件,包含在多个模式中的输入源和定时源。
-
还有两种系统级别的模式
- app刚启动的时候会执行一次
- 系统检测app各种事件的模式
-
三、线程与Runloop的关系
- 线程与
Runloop是一一对应的,一个Runloop对应一个核心线程,为什么说是核心,因为Runloop是可以嵌套的,但核心的只有一个,他们的对应关系保存在一个全局字典里 Runloop是来管理线程的,线程执行完任务时会进入休眠状态,有任务进来时会被唤醒开始执行任务(事件驱动)Runloop在第一次获取时被创建,线程结束时被销毁- 主线程的
Runloop在程序启动时就默认创建好了 - 子线程的
Runloop是懒加载的,只有在使用时才被创建,因此在子线程中使用NSTimer时要注意确保子线程的Runloop已创建,否则NSTimer不会生效。
四、主线程的RunLoop默认开启的原因
在iOS和macOS应用程序中,主线程的RunLoop是默认开启的,而其他线程的RunLoop则需要手动配置和启动。这是因为主线程通常用于处理用户界面更新和事件处理,而其他线程通常用于执行后台任务。以下是更详细的解释:
-
用户界面更新和事件处理:
- 主线程负责处理所有的用户界面更新和事件,例如触摸事件、手势识别、屏幕刷新等。为了及时响应这些事件并更新界面,主线程需要一个RunLoop来不断循环,等待和处理输入事件。
- UIApplication和NSApplication在应用启动时自动启动主线程的RunLoop,以确保应用能够及时响应用户交互。
-
主线程任务管理:
- 主线程上的RunLoop管理着各种输入源(如触摸事件、定时器、网络事件等),这对于保持应用的流畅性和响应性非常重要。主线程RunLoop会一直运行,直到应用退出。
五、其他线程的RunLoop默认不启动的原因
-
性能和资源管理:
- 不自动启动其他线程的RunLoop有助于节省系统资源。RunLoop需要持续消耗一定的资源来等待和处理事件,因此只有在确实需要的时候才手动启动其他线程的RunLoop。
-
线程用途:
- 辅助线程通常用于执行短期或后台任务,例如数据处理、网络请求等,这些任务一般不需要持续运行的RunLoop。
- 只有在需要处理异步任务或事件时,才需要为辅助线程配置并启动RunLoop。
六、如何线程保活
通过使用 Runloop,可以实现线程保活。将一个 Runloop 添加到线程中,并不断运行该 Runloop,线程可以保持活跃状态。Runloop 会监听输入源(Input Source)和定时源(Timer Source),一旦有事件发生,Runloop 会唤醒线程处理事件。
runtime
在 iOS 和 macOS 开发中,OC 运行时 (Runtime) 是一个强大的工具,它允许在运行时动态操作类和对象。Runtime 提供了一组底层的 C API,可以让开发者在运行时查询和修改类、对象、方法等。下面是一些常见的 Runtime 功能及其使用示例:
一、动态方法解析
Runtime 允许在运行时动态添加方法到类中。这可以通过实现 +resolveInstanceMethod: 和 +resolveClassMethod: 来实现。
二、关联对象
Runtime 允许在运行时为现有对象动态添加属性,这称为关联对象 (Associated Objects)。
三、方法交换 (Method Swizzling)
方法交换是一种强大的技术,可以在运行时交换两个方法的实现。常用于修改系统类的行为,例如 UIViewController 的 viewWillAppear: 方法。
四、动态创建类
Runtime 允许在运行时动态创建类,并为其添加属性和方法。
五、获取类和方法信息
Runtime 允许在运行时获取类的属性、方法、协议等信息。
对象方法和类方法的区别?
- 对象方法能个访问成员变量。
- 类方法中不能直接调用对象方法,想要调用对象方法,必须创建或者传入对象。
- 类方法可以和对象方法重名。
- 如果非要说的多一点深一点的话, 可以说对象方法存在类对象里面,类方法存在元类里面。
一、对象、类对象、元类、元类结构体的组成以及他们是如何相关联的
在 OC 和 Swift 中,对象、类对象、元类和元类结构体是非常重要的概念,它们构成了面向对象编程的基础。理解这些概念及其关系,有助于深入理解语言的运行时机制。以下是对这些概念的详细解释及其相互关系。
- 对象(Instance) :类的实例,包含实例变量,通过
isa指针指向类对象。 - 类对象(Class Object) :描述类的行为和属性,通过
isa指针指向元类。 - 元类(Meta-Class) :描述类对象的行为和属性,通过
isa指针指向根元类或自身。 - 元类结构体:与类对象结构体相似,提供元数据和方法。
二、对象(Instance)
对象是类的实例,它包含了实例变量(属性)和可以调用的实例方法。
组成部分:
- 实例变量:对象的属性(存储在内存中)。
- ISA 指针:指向对象所属类的指针。
在 OC 中,实例对象的结构体可能类似于:
struct objc_object {
Class isa;
// Other instance variables...
};
三、类对象(Class Object)
类对象代表类本身,它包含了类的方法列表、属性列表和其他元数据。每个类对象都有一个 isa 指针,指向其元类。
1. 组成部分:
- ISA 指针:指向元类(Meta-Class)。
- 方法列表:该类的类方法和实例方法列表。
- 属性列表:类的属性。
- 协议列表:类遵循的协议。
在 OC 中,类对象的结构体可能类似于:
struct objc_class {
Class isa; // 指向元类
Class superclass; // 指向父类
void *cache; // 方法缓存
void *vtable; // 虚函数表
struct class_ro_t *ro; // 只读数据,包括方法和属性
};
四、元类(Meta-Class)
元类是类对象的类。它描述了类对象的行为,并包含类方法。每个元类也有一个 isa 指针,指向根元类(通常是 NSObject 的元类)。
组成部分:
- ISA 指针:指向根元类或自身(循环引用)。
- 方法列表:元类的方法列表(即类方法)。
在 OC 中,元类的结构体可能类似于类对象:
struct objc_class {
Class isa; // 指向根元类或自身
Class superclass; // 指向父类的元类
void *cache; // 方法缓存
void *vtable; // 虚函数表
struct class_ro_t *ro; // 只读数据,包括方法和属性
};
元类结构体的组成
元类的结构体与类对象的结构体相似,但其作用是为类对象提供元数据和方法。
五、关系与关联
-
实例对象与类对象:
- 每个实例对象有一个
isa指针,指向类对象。 - 实例对象通过
isa指针找到类对象,从而调用类定义的实例方法。
- 每个实例对象有一个
-
类对象与元类:
- 每个类对象有一个
isa指针,指向元类。 - 类对象通过
isa指针找到元类,从而调用类方法。
- 每个类对象有一个
-
元类与根元类:
- 元类本身是一个类对象,它的
isa指针指向根元类。 - 根元类的
isa指针指向自身,形成一个循环引用。
- 元类本身是一个类对象,它的
关系图示:
Instance Object
|
v
Class Object (isa -> Meta-Class)
|
v
Meta-Class (isa -> Root Meta-Class)
|
v
Root Meta-Class (isa -> Root Meta-Class, forming a loop)
深拷贝和浅拷贝
- 浅拷贝创建一个新的对象,但不复制对象所引用的资源,而是直接引用原对象所引用的资源。因此,浅拷贝后的两个对象共享同一个资源。
- 深拷贝不仅复制对象本身,还递归地复制对象所引用的所有资源。这样,深拷贝后的两个对象完全独立,不共享任何资源。
一、深拷贝与浅拷贝的对比
| 特性 | 浅拷贝 | 深拷贝 |
|---|---|---|
| 复制内容 | 仅复制对象的引用 | 递归复制对象及其引用的所有资源 |
| 资源共享 | 原对象和拷贝对象共享资源 | 原对象和拷贝对象独立拥有各自资源 |
| 内存占用 | 较少 | 较多 |
| 修改影响 | 修改任何一个会影响另一个 | 修改任何一个不会影响另一个 |
二、OC 中的 copy 和 mutableCopy
copy:创建不可变对象的副本。对于不可变对象,copy通常是浅拷贝;对于可变对象,copy是深拷贝。mutableCopy:创建可变对象的副本。无论源对象是否可变,mutableCopy通常是深拷贝。
三、NSString类型为什么要用copy修饰
-
避免引用共享导致的修改:
- 如果属性不使用
copy修饰符,而使用strong或retain,那么当属性被赋值为一个可变字符串(如NSMutableString)时,属性会直接引用这个可变字符串。这样一来,如果外部代码修改了这个可变字符串,属性持有的字符串内容也会被改变,这可能导致不可预见的错误。
- 如果属性不使用
-
确保不可变性:
- 使用
copy修饰符可以确保属性持有的是一个不可变字符串(NSString),即使传递给属性的是一个可变字符串(NSMutableString)。这样一来,属性的值就不会被外部修改。
- 使用
四、怎么理解 copy - on - write
"Copy-on-write"(COW)是一种优化策略,常用于内存管理和数据结构复制中。其核心思想是:当多个对象共享同一份数据时,只有在某个对象尝试修改数据时,才会进行实际的复制操作。这样可以延迟复制操作,节省内存,并提高性能。
在 Swift 和 OC 中,copy 修饰符和许多不可变数据类型都应用了这一策略。以下是对 copy-on-write 的详细解释及其实现和应用。
-
核心思想
- 共享数据:多个对象可以共享同一份数据,避免不必要的复制操作,从而节省内存。
- 延迟复制:只有当某个对象尝试修改数据时,才会进行实际的复制操作。这样,只有在需要时才分配新的内存空间,进行数据复制。
-
实现机制
- 在
copy-on-write机制中,通常会使用引用计数或其他方式来跟踪共享数据的引用。当需要修改数据时,检查引用计数,如果引用计数大于 1,则进行实际的复制操作,确保修改不会影响其他共享数据的对象。
- 在
-
Swift 中的
copy-on-write- Swift 的标准库中的许多集合类型(如
Array、Dictionary和Set)都使用了copy-on-write机制。
- Swift 的标准库中的许多集合类型(如
-
OC 中的
copy-on-write- OC 中的不可变对象(如
NSString、NSArray和NSDictionary)通常也是copy-on-write的。使用copy修饰符声明的属性在赋值时会进行浅拷贝,直到修改发生才进行深拷贝。
- OC 中的不可变对象(如
iOS中block 捕获外部局部变量实际上发生了什么?__block中又做了什么
一、Block 捕获外部局部变量
当一个 Block 在定义时引用了外部的局部变量,这些变量会被捕获,并在 Block 内部使用。具体来说:
- 自动变量(自动局部变量) :默认情况下,Blocks 捕获自动变量的副本(即值拷贝)。
- 静态变量:Block 捕获的是变量的引用(即指针),而不是值。
int a = 10;
void (^myBlock)(void) = ^{
NSLog(@"a = %d", a);
};
a = 20;
myBlock(); // 输出: a = 10
二、__block 关键字
默认情况下,Blocks 不能修改被捕获的外部自动变量。如果你希望在 Block 内部修改这些变量,可以使用 __block 关键字。__block 关键字允许 Block 捕获变量的引用,而不是值,这样 Block 内部可以修改外部变量。
__block int a = 10;
void (^myBlock)(void) = ^{
a = 20;
NSLog(@"a = %d", a);
};
myBlock(); // 输出: a = 20
NSLog(@"a = %d", a); // 输出: a = 20
三、捕获机制的实现
- Block 结构
Block 在 OC 中实际上是一个对象,有一个内存布局。主要结构如下:
struct Block_literal {
void *isa;
int flags;
int reserved;
void (*invoke)(void *, ...);
struct Block_descriptor *descriptor;
// 捕获的变量
};
__block变量
当使用 __block 关键字时,编译器会将该变量包装在一个 __Block_byref 结构中。这种结构使得变量在 Block 内部和外部都可以被访问和修改。
struct __Block_byref_a_0 {
void *__isa;
__Block_byref_a_0 *__forwarding;
int __flags;
int __size;
int a;
};
在 Block 定义中,变量 a 被封装在一个 __Block_byref_a_0 结构中,并通过 __forwarding 指针在 Block 内外共享。
四、Block为什么用copy修饰
在 iOS 开发中,使用 copy 修饰符声明 Block 属性是一个重要的最佳实践。这是因为默认情况下,Blocks 在栈上存储,只有通过 copy 操作才能确保它们在堆上存储,从而在超出定义它们的作用域后仍然有效。
-
Block 的内存管理
-
Blocks 可以有三种存储位置:
-
栈上(Stack) :默认情况下,Block 在定义时位于栈上。这种 Block 只能在定义它的函数或方法作用域内有效。
-
堆上(Heap) :通过
copy操作可以将 Block 从栈复制到堆上,从而在作用域之外依然有效。 -
全局区(Global) :如果 Block 不捕获任何外部变量,它会存储在全局区。
-
-
-
为什么要使用
copy修饰符-
Block 默认在栈上
- 当你定义一个 Block,它默认存储在栈上。这意味着当函数或方法返回时,Block 会失效,因为栈上的内存会被回收。
-
copy将 Block 移到堆上- 为了确保 Block 在离开其作用域后仍然有效,必须将它从栈上复制到堆上。这就是
copy修饰符的作用。
- 为了确保 Block 在离开其作用域后仍然有效,必须将它从栈上复制到堆上。这就是
-
五、oc的block和swift的block又什么区别
| 特性 | OC Block | Swift Closure |
|---|---|---|
| 语法复杂度 | 较复杂 | 简单灵活 |
| 捕获外部变量 | 默认不可变,需用 __block 修饰 | 默认不可变(引用类型除外),但可以捕获副本 |
| 内存管理 | 需要手动管理循环引用 | 自动管理,通过捕获列表解决循环引用 |
| 异步任务 | 支持 | 支持,更加简洁 |
| 尾随闭包 | 不支持 | 支持 |
| 参数简写 | 不支持 | 支持 $0, $1 等简写形式 |
六、逃逸闭包和非逃逸闭包
- 非逃逸闭包: 默认情况下,闭包在函数体内执行,并且在函数返回之前执行完毕。这种闭包称为非逃逸闭包,因为它在函数结束之前就被调用。
func performOperation(closure: () -> Void) {
closure() // 闭包在函数内部被调用
}
performOperation {
print("Closure called inside the function.")
}
- 逃逸闭包: 是指闭包不会在函数体内立即执行,而是可能在函数返回后执行,例如在异步操作中、或者存储为属性供以后使用的情况下。这种闭包逃逸出了函数体,所以要使用
@escaping标记。
func performAsyncOperation(completion: @escaping () -> Void) {
DispatchQueue.global().async {
// 模拟异步操作
completion() // 闭包在异步操作完成后被调用,可能已经超出了函数作用域
}
}
- 逃逸闭包与循环引用: 由于逃逸闭包可能在未来某个时刻调用,因此容易引发强引用循环。当闭包捕获了某个对象,并且该对象也对闭包有强引用时,可能会造成内存泄漏。为了解决这个问题,可以使用弱引用
weak或无主引用unowned。
Category 分类
一、Category 的实现原理
Category 是 OC 中的一个强大特性,通过运行时机制为类添加新的方法,而无需子类化或修改原有类的代码。Category 的实现涉及运行时系统将方法列表合并到目标类中,从而实现方法的动态扩展。
-
Category 在运行时的工作机制
-
在 OC 的运行时系统中,每个类都有一个方法列表(method list),存储了该类的所有方法。Category 的本质是向类的这个方法列表中添加新的方法。
- 在运行时,Category 被表示为一个
category_t结构体,这个结构体包含了 Category 的名称、关联的类、实例方法列表、类方法列表、协议列表和属性列表。 - 每个类在运行时都有一个
method_list_t结构体,表示该类的方法列表。这个结构体包含方法的名称、类型和实现的指针。 - 在程序加载时,OC 运行时系统会找到所有的 Category,并将其方法列表合并到目标类的方法列表中。这一过程通常发生在
objc-runtime加载类的时候。 - 如果 Category 中的方法与类中已有的方法名称相同,Category 的方法会覆盖原有的方法。这是因为在方法列表合并时,Category 的方法会被添加到列表的前面,因此在方法查找过程中,Category 的方法会先被找到。
- 在运行时,Category 被表示为一个
-
Category 的限制
- 不能添加实例变量:由于 Category 仅仅是为类添加方法列表,而不是修改类的内存布局,因此不能添加实例变量。
- 方法冲突:如果多个 Category 中有同名的方法,最后一个被加载的 Category 的方法会生效,可能会导致意外的覆盖和冲突。
- 调试复杂性:由于 Category 可以分布在不同的文件中,有时会增加代码调试的复杂性。
-
二、为什么分类中不能创建属性Property(runtime除外)
在 OC 中,分类(Category)是一种强大的特性,它允许开发者为现有类添加方法,而无需继承或修改原始类的实现。然而,分类有一些限制,其中之一是不能直接添加属性(Property)。要理解这个限制,我们需要深入了解 OC 属性的本质以及分类的工作机制。
1、属性的本质
在 OC 中,属性(Property)实际上是实例变量(Instance Variable, ivar)和存取方法(Accessor Methods, 即 getter 和 setter 方法)的组合:
- 实例变量(ivar) :用于存储属性的值。
- 存取方法:用于访问和修改属性的值。
当你在类的接口中声明一个属性时,编译器会自动生成相应的实例变量和存取方法(如果未显式提供)。
2、分类的工作机制
分类的目的是扩展现有类的功能,可以向类中添加新的方法,但不能直接向类中添加实例变量。这是因为分类的实现是在运行时动态添加到类的,因此无法在编译时修改类的内存布局。
3、分类中的属性声明
虽然你可以在分类中声明属性,但这种声明仅仅是声明存取方法的存在,并不会自动生成实例变量或存取方法的实现。换句话说,分类中声明的属性更像是一种编译器提示,告诉编译器存在这些方法,但需要你自己提供实现。
4、分类中不能添加属性的原因
- 实例变量限制:分类不能添加实例变量。这是因为分类在运行时动态加载,无法改变类的内存布局。而属性通常需要与实例变量关联。
- 存取方法的实现:虽然可以在分类中声明属性,但存取方法需要手动实现。分类中声明的属性不会自动生成
getter和setter方法。 - 编译器行为:在类的主要实现中,编译器负责生成属性的实例变量和方法实现,但分类无法享受这种自动生成的机制。
5、解决方案
虽然分类中不能直接添加属性,但可以通过以下几种方式实现类似的效果:
-
使用关联对象(Associated Objects)
- 关联对象是一种运行时特性,允许你动态地将对象关联到另一个对象上,从而在分类中实现“属性”。
#import <objc/runtime.h>
@interface MyClass (Category)
@property (nonatomic, strong) NSString *associatedProperty;
@end
@implementation MyClass (Category)
- (void)setAssociatedProperty:(NSString *)associatedProperty {
objc_setAssociatedObject(self, @selector(associatedProperty), associatedProperty, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSString *)associatedProperty {
return objc_getAssociatedObject(self, @selector(associatedProperty));
}
@end
-
使用方法实现存取
- 另一种方法是手动实现存取方法,但这通常较为繁琐和不直观。
@interface MyClass (Category)
@property (nonatomic, strong) NSString *manualProperty;
@end
@implementation MyClass (Category)
- (void)setManualProperty:(NSString *)manualProperty {
// 自定义存取方法逻辑
}
- (NSString *)manualProperty {
// 自定义存取方法逻辑
return nil;
}
@end
三、关联对象的原理
关联对象(Associated Objects)通过运行时的哈希表机制,将额外的数据与现有对象相关联,弥补了分类不能添加实例变量的限制。
-
关联对象是通过 OC 的运行时库(Runtime Library)实现的。下面是关联对象的工作机制:
- 在 OC 运行时,关联对象是通过一个全局的哈希表(Hash Table)来存储的。这个哈希表的键是对象的指针,值是另一个哈希表,用于存储与该对象关联的所有对象。这第二个哈希表的键是属性的键,值是关联的对象。
objc_setAssociatedObject函数用于设置关联对象。当调用objc_setAssociatedObject时,运行时会将object作为键,在全局哈希表中查找。如果不存在,会创建一个新的哈希表来存储该对象的关联对象。然后将key和value添加到该哈希表中。objc_getAssociatedObject函数用于获取关联对象。运行时会在全局哈希表中查找object关联的哈希表,然后在该哈希表中查找key,返回对应的关联对象。objc_removeAssociatedObjects函数用于移除对象的所有关联对象。运行时会从全局哈希表中移除object关联的整个哈希表,从而移除所有关联对象。
-
内存管理策略
在设置关联对象时,可以指定不同的内存管理策略。这些策略决定了关联对象在内存中的管理方式,包括:
- `OBJC_ASSOCIATION_ASSIGN`:弱引用,不增加引用计数。
- `OBJC_ASSOCIATION_RETAIN_NONATOMIC`:强引用,不使用原子操作。
- `OBJC_ASSOCIATION_COPY_NONATOMIC`:拷贝,不使用原子操作。
- `OBJC_ASSOCIATION_RETAIN`:强引用,使用原子操作。
- `OBJC_ASSOCIATION_COPY`:拷贝,使用原子操作。
weak修饰符
weak 是一种修饰符,用于声明一个对象属性的弱引用。弱引用不增加对象的引用计数,因此不会阻止对象被释放。当对象被释放时,所有指向该对象的弱引用会自动设为 nil。
一、weak 属性的工作机制
weak 属性的工作机制依赖于 OC 运行时的弱引用哈希表(weak reference table)。这个表记录了所有弱引用的对象;用 weak 指向的对象内存地址作为 key, Value是weak指针的地址数组。,并在对象被释放时,运行时会遍历弱引用表,将所有指向该对象的弱引用设为 nil。这确保了访问已释放对象的弱引用时不会导致野指针错误(dangling pointer error)。
二、内存管理
weak 属性不会增加对象的引用计数(retain count)。因此,如果一个对象只有弱引用指向它,那么该对象会被立即释放。
三、典型使用场景
- 委托模式:在委托模式中,通常使用
weak属性来防止循环引用。 - 视图层次结构:在视图层次结构中,子视图通常对父视图保持弱引用,以避免循环引用。
- block: 在代码块中引用另一个对象的时候,使用__weak打破引用圈,以避免循环引用。
UIView和CALayer
UIView 和 CALayer 是 iOS 界面开发中不可或缺的两部分。UIView 负责处理用户交互和视图管理,而 CALayer 负责实际的绘制和动画工作。
一、UIView的主要功能
- 处理用户交互:
UIView可以响应触摸、手势等用户交互事件。 - 管理视图层次结构:
UIView可以包含其他视图,形成视图层次结构(view hierarchy)。 - 布局和动画:
UIView提供了布局子视图和执行动画的功能。 - 绘制和显示内容:
UIView可以绘制内容,并将其显示在屏幕上。
二、CALayer主要功能
- 绘制内容:
CALayer负责绘制和显示内容,包括图像、颜色、渐变等。 - 动画:
CALayer提供了强大的动画功能,可以为图层的各种属性添加动画。 - 变换和投影:
CALayer支持几何变换(如旋转、缩放)和投影效果(如阴影)。 - 视觉效果:
CALayer可以设置圆角、边框、阴影等视觉效果。
三、UIView 和 CALayer 的区别
1. 用户交互
UIView:可以响应用户交互事件,如触摸和手势。CALayer:不能直接响应用户交互事件,但可以用于优化动画和绘制。
2. 视图层次结构
UIView:可以包含其他UIView,形成视图层次结构。CALayer:可以包含其他CALayer,形成图层层次结构,但没有UIView的便利视图管理功能。
3. 布局和自动布局
UIView:支持自动布局和约束(Auto Layout),可以自动调整子视图的位置和大小。CALayer:不支持自动布局,需要手动管理位置和大小。
4. 绘制和显示
UIView:通过drawRect:方法进行自定义绘制,并将内容显示在屏幕上。CALayer:通过drawInContext:方法进行自定义绘制,并将内容显示在屏幕上。
5. 动画
UIView:提供了简单的动画接口,如UIView.animateWithDuration:animations:。CALayer:提供了更强大的动画功能,如关键帧动画(CAKeyframeAnimation)和动画组(CAAnimationGroup)。
四、CALayer 的工作机制
1. 图层树(Layer Tree)
Core Animation 维护了一个图层树(layer tree),每个 UIView 的 CALayer 都在这个树中。图层树反映了视图层次结构,但它是独立于视图树的。图层树包含三种类型的树:
- 展示树(Presentation Tree) :表示当前屏幕上显示的图层状态。
- 模型树(Model Tree) :表示应用当前的图层状态。
- 渲染树(Render Tree) :用于实际绘制的图层状态。
2. 离屏渲染(Offscreen Rendering)
为了实现复杂的视觉效果,如阴影、圆角和蒙版,Core Animation 可能需要离屏渲染。离屏渲染会创建一个新的图像缓冲区,这可能会影响性能。因此,在使用这些效果时需要谨慎。
3. 图层内容
CALayer 可以显示不同类型的内容,如颜色、图像或自定义绘制。可以通过设置 contents 属性来显示图像。
4. 视觉效果
CALayer 提供了一些强大的视觉效果,如圆角、阴影、边框和变换。
5. 动画
CALayer 支持隐式动画和显式动画。隐式动画是指属性变化时自动产生的动画,而显式动画是通过 CABasicAnimation、CAKeyframeAnimation 等类创建的动画。
6. CALayer 的性能优化
由于 CALayer 的强大功能,有时可能会导致性能问题。以下是一些性能优化建议:
- 避免过度使用离屏渲染:尽量减少使用圆角、阴影和蒙版等效果,或通过调整图层属性(如
shadowPath)来优化性能。 - 减少图层数量:过多的图层会增加内存使用和渲染时间。尝试合并图层或使用合成图像。
- 使用 CATransformLayer:对于复杂的 3D 变换,使用
CATransformLayer代替普通的CALayer,因为它不会在每个图层上创建离屏缓冲区。
KVO和KVC
KVO(Key-Value Observing)和 KVC(Key-Value Coding)是 OC 和 Swift 中用于访问和观察对象属性的强大机制。它们广泛用于 iOS 开发中,特别是在与 Cocoa 和 Cocoa Touch 框架交互时。
一、 Key-Value Coding (KVC)
KVC 是一种通过字符串键(key)间接访问对象属性的方法,而无需直接使用属性的访问器方法(getter 和 setter)。
1. KVC原理
-
键值查找:
- 当调用
valueForKey:或setValue:forKey:方法时,KVC 首先查找与键匹配的属性或方法。 - 查找顺序是:
- 实例变量:查找是否存在
_key、_isKey、key或isKey命名的实例变量。 - getter 和 setter 方法:查找是否存在
getKey、key、isKey等命名的 getter 方法,或者setKey:、_setKey:等命名的 setter 方法。 - 访问器方法:如
valueForKey:和setValue:forKey:方法。
- 实例变量:查找是否存在
- 当调用
-
实现方法:
- Objective-C 运行时在查找到相应的实例变量或方法后,通过消息机制进行调用和访问。
- 如果找不到对应的实例变量或方法,则调用
valueForUndefinedKey:或setValue:forUndefinedKey:方法,这些方法可以被重写以自定义行为。
二、 Key-Value Observing (KVO)
KVO 是一种机制,允许对象观察其他对象属性的变化。KVO 提供了一种响应属性变化的方式,而无需手动编写通知代码。
1. KVO原理
-
动态子类化:
- 当一个对象的属性被观察时,Objective-C 运行时会创建这个对象的一个新子类。
- 这个新子类重写了被观察属性的 setter 方法。在 setter 方法内部,KVO 会插入代码,在属性值改变前后发送通知。
-
isa-swizzling:
- KVO 会改变被观察对象的
isa指针,使其指向新创建的子类。 - 通过这种方式,所有对该属性的访问都被定向到新子类的 setter 方法中。
- KVO 会改变被观察对象的
-
通知机制:
- 属性值改变时,setter 方法会调用
willChangeValueForKey:和didChangeValueForKey:方法,触发 KVO 通知。 - 观察者通过实现
observeValueForKeyPath:ofObject:change:context:方法接收通知并处理属性变化。
- 属性值改变时,setter 方法会调用
2. 在 Swift 中使用 KVO 时,需要注意以下几点:
- 需要继承自
NSObject。 - 属性需要使用
@objc dynamic修饰。 - 使用
NSKeyValueObservation进行观察,避免手动移除观察者带来的潜在问题。
三、KVC 和 KVO 的优缺点
-
优点
- 灵活性:通过字符串键访问和观察属性,提供了很大的灵活性。
- 动态性:允许在运行时动态修改和观察对象属性,而不需要编译时的静态检查。
- 解耦:使得对象之间的通信更加松散耦合。
-
缺点
- 安全性:由于依赖字符串键,编译时无法检查键的正确性,容易导致运行时错误。
- 性能:KVO 和 KVC 都依赖于 OC 运行时特性,会带来一定的性能开销。
- 调试困难:由于其动态性,调试和跟踪属性变化相对复杂。
objc消息发送
一、消息发送的基本过程
当在 Objective-C 中向一个对象发送消息时,实际上是调用了运行时系统的消息传递函数。具体步骤如下:
-
编译器转换:
- 当你编写
[object message]时,编译器会将其转换为objc_msgSend(object, @selector(message))函数调用。 objc_msgSend是一个运行时函数,用于将消息发送给对象。
- 当你编写
-
消息分发:
objc_msgSend函数会首先通过对象的isa指针找到该对象的类。- 接下来,通过类的方法列表(method list)查找与消息对应的
SEL(选择器)。
-
方法调用:
- 如果在类的方法列表中找到了与
SEL对应的方法实现,则直接调用该方法实现。 - 如果没有找到,则会沿着继承链向上查找父类的方法列表,直到根类
NSObject。
- 如果在类的方法列表中找到了与
-
动态方法解析:
- 如果在类及其父类中都没有找到方法实现,运行时会调用
resolveInstanceMethod:或resolveClassMethod:方法,尝试动态添加方法实现。
- 如果在类及其父类中都没有找到方法实现,运行时会调用
-
消息转发:
- 如果动态方法解析也失败,运行时会进入消息转发机制。首先调用
forwardingTargetForSelector:方法,允许将消息转发给其他对象。 - 如果
forwardingTargetForSelector:返回nil,则调用methodSignatureForSelector:和forwardInvocation:方法进行完全消息转发。
- 如果动态方法解析也失败,运行时会进入消息转发机制。首先调用
-
最终处理:
- 如果最终消息转发也未能处理消息,运行时会调用
doesNotRecognizeSelector:方法,导致程序崩溃。
- 如果最终消息转发也未能处理消息,运行时会调用
SQL/CoreData
SQL
- 版本检查 :
- 在应用启动时,检查当前数据库版本与应用程序版本是否匹配。
- 如果当前数据库版本低于应用程序版本,则需要进行升级。
- 备份旧数据库:
- 创建数据库备份,通常是将旧数据库文件重命名,例如添加"_bak"后缀。
- 创建新表 :
- 根据数据库配置文件(如plist文件)中的信息,创建新表。
- 数据迁移 :
- 遍历旧表和新表,对比字段,将数据从旧表迁移到新表。
- 删除备份表:
// 创建新的临时表,把数据导入临时表,然后用临时表替换原表
- (void)baseDBVersionControl {
NSString * version_old = ValueOrEmpty(MMUserDefault.dbVersion);
NSString * version_new = [NSString stringWithFormat:@"%@", DB_Version];
NSLog(@"dbVersionControl before: %@ after: %@",version_old,version_new);
// 数据库版本升级
if (version_old != nil && ![version_new isEqualToString:version_old]) {
// 获取数据库中旧的表
NSArray* existsTables = [self sqliteExistsTables];
NSMutableArray* tmpExistsTables = [NSMutableArray array];
// 修改表名,添加后缀“_bak”,把旧的表当做备份表
for (NSString* tablename in existsTables) {
[tmpExistsTables addObject:[NSString stringWithFormat:@"%@_bak", tablename]];
[self.databaseQueue inDatabase:^(FMDatabase *db) {
NSString* sql = [NSString stringWithFormat:@"ALTER TABLE %@ RENAME TO %@_bak", tablename, tablename];
[db executeUpdate:sql];
}];
}
existsTables = tmpExistsTables;
// 创建新的表
[self initTables];
// 获取新创建的表
NSArray* newAddedTables = [self sqliteNewAddedTables];
// 遍历旧的表和新表,对比取出需要迁移的表的字段
NSDictionary* migrationInfos = [self generateMigrationInfosWithOldTables:existsTables newTables:newAddedTables];
// 数据迁移处理
[migrationInfos enumerateKeysAndObjectsUsingBlock:^(NSString* newTableName, NSArray* publicColumns, BOOL * _Nonnull stop) {
NSMutableString* colunmsString = [NSMutableString new];
for (int i = 0; i<publicColumns.count; i++) {
[colunmsString appendString:publicColumns[i]];
if (i != publicColumns.count-1) {
[colunmsString appendString:@", "];
}
}
NSMutableString* sql = [NSMutableString new];
[sql appendString:@"INSERT INTO "];
[sql appendString:newTableName];
[sql appendString:@"("];
[sql appendString:colunmsString];
[sql appendString:@")"];
[sql appendString:@" SELECT "];
[sql appendString:colunmsString];
[sql appendString:@" FROM "];
[sql appendFormat:@"%@_bak", newTableName];
[self.databaseQueue inDatabase:^(FMDatabase *db) {
[db executeUpdate:sql];
}];
}];
// 删除备份表
[self.databaseQueue inDatabase:^(FMDatabase *db) {
[db beginTransaction];
for (NSString* oldTableName in existsTables) {
NSString* sql = [NSString stringWithFormat:@"DROP TABLE IF EXISTS %@", oldTableName];
[db executeUpdate:sql];
}
[db commit];
}];
MMUserDefault.dbVersion = version_new;
} else {
MMUserDefault.dbVersion = version_new;
}
}
- (NSDictionary*)generateMigrationInfosWithOldTables:(NSArray*)oldTables newTables:(NSArray*)newTables {
NSMutableDictionary<NSString*, NSArray* >* migrationInfos = [NSMutableDictionary dictionary];
for (NSString* newTableName in newTables) {
NSString* oldTableName = [NSString stringWithFormat:@"%@_bak", newTableName];
if ([oldTables containsObject:oldTableName]) {
// 获取表数据库字段信息
NSArray* oldTableColumns = [self sqliteTableColumnsWithTableName:oldTableName];
NSArray* newTableColumns = [self sqliteTableColumnsWithTableName:newTableName];
NSArray* publicColumns = [self publicColumnsWithOldTableColumns:oldTableColumns newTableColumns:newTableColumns];
if (publicColumns.count > 0) {
[migrationInfos setObject:publicColumns forKey:newTableName];
}
}
}
return migrationInfos;
}
- (NSArray*)publicColumnsWithOldTableColumns:(NSArray*)oldTableColumns newTableColumns:(NSArray*)newTableColumns {
NSMutableArray* publicColumns = [NSMutableArray array];
for (NSString* oldTableColumn in oldTableColumns) {
if ([newTableColumns containsObject:oldTableColumn]) {
[publicColumns addObject:oldTableColumn];
}
}
return publicColumns;
}
- (NSArray*)sqliteTableColumnsWithTableName:(NSString*)tableName {
__block NSMutableArray<NSString*>* tableColumes = [NSMutableArray array];
[self.databaseQueue inDatabase:^(FMDatabase *db) {
NSString* sql = [NSString stringWithFormat:@"PRAGMA table_info('%@')", tableName];
FMResultSet *rs = [db executeQuery:sql];
while ([rs next]) {
NSString* columnName = [rs stringForColumn:@"name"];
[tableColumes addObject:columnName];
}
}];
return tableColumes;
}
- (NSArray*)sqliteExistsTables {
__block NSMutableArray<NSString*>* existsTables = [NSMutableArray array];
[self.databaseQueue inDatabase:^(FMDatabase *db) {
NSString* sql = @"SELECT * from sqlite_master WHERE type='table'";
FMResultSet *rs = [db executeQuery:sql];
while ([rs next]) {
NSString* tablename = [rs stringForColumn:@"name"];
[existsTables addObject:tablename];
}
}];
return existsTables;
}
- (NSArray*)sqliteNewAddedTables {
__block NSMutableArray<NSString*>* newAddedTables = [NSMutableArray array];
[self.databaseQueue inDatabase:^(FMDatabase *db) {
NSString* sql = @"SELECT * from sqlite_master WHERE type='table' AND name NOT LIKE '%_bak'";
FMResultSet *rs = [db executeQuery:sql];
while ([rs next]) {
NSString* tablename = [rs stringForColumn:@"name"];
[newAddedTables addObject:tablename];
}
}];
return newAddedTables;
}
二、 CoreData
数据库升级后版本迭代升级如下:
如果IOS App 使用到CoreData,并且在上一个版本上有数据库更新(新增表、字段等操作),那在覆盖安装程序时就要进行CoreData数据库的迁移,具体操作如下:
-
1.选中你的mydata.xcdatamodeld文件,选择菜单editor->Add Model Version 比如取名:mydata2.xcdatamodel
-
2.设置当前版本
选择上级mydata.xcdatamodeld ,在inspector中的Versioned Core Data Model选择Current模版为mydata2
-
3.修改新数据模型mydata2,在新的文件上添加字段及表
-
4.删除原来的类文件,重新生成下类。
网络协议
一、OSI 七层模型
OSI 模型(Open Systems Interconnection Model)将网络通信分为七个层次,每一层都有特定的功能和协议。
-
物理层(Physical Layer) :
- 功能:定义物理设备的电气、机械和功能特性。
- 协议和标准:Ethernet、USB、DSL。
-
数据链路层(Data Link Layer) :
- 功能:提供节点到节点的数据传输,错误检测和纠正。
- 协议:Ethernet、PPP(Point-to-Point Protocol)、MAC(Media Access Control)地址。
-
网络层(Network Layer) :
- 功能:负责路径选择和逻辑地址(IP 地址)的使用。
- 协议:IP(Internet Protocol)、ICMP(Internet Control Message Protocol)。
-
传输层(Transport Layer) :
- 功能:提供端到端的通信和数据传输的可靠性。
- 协议:TCP(Transmission Control Protocol)、UDP(User Datagram Protocol)。
-
会话层(Session Layer) :
- 功能:管理和控制会话(连接)。
- 协议:NetBIOS、RPC(Remote Procedure Call)。
-
表示层(Presentation Layer) :
- 功能:数据的翻译、加密和压缩。
- 协议:SSL/TLS(Secure Sockets Layer / Transport Layer Security)、JPEG、MPEG。
-
应用层(Application Layer) :
- 功能:提供网络服务和应用程序接口。
- 协议:HTTP(Hypertext Transfer Protocol)、FTP(File Transfer Protocol)、SMTP(Simple Mail Transfer Protocol)、DNS(Domain Name System)。
二、TCP
TCP(Transmission Control Protocol)是一个面向连接的协议,它通过三次握手建立连接,通过四次挥手终止连接。这些过程确保了数据传输的可靠性和顺序性。
1. TCP 三次握手
TCP 的三次握手用于在客户端和服务器之间建立连接。这个过程确保双方都能发送和接收数据,并且知道对方的存在。以下是三次握手的详细步骤:
-
SYN(同步序列编号) :
- 客户端发送一个带有 SYN 标志的数据包给服务器,请求建立连接。该数据包包含一个初始序列号(
Seq = x)。 - 客户端状态:
SYN_SENT
- 客户端发送一个带有 SYN 标志的数据包给服务器,请求建立连接。该数据包包含一个初始序列号(
-
SYN-ACK(同步序列编号-确认) :
- 服务器收到 SYN 包后,回复一个带有 SYN 和 ACK 标志的数据包。该数据包包含服务器自己的初始序列号(
Seq = y)和对客户端 SYN 包的确认序列号(Ack = x+1)。 - 服务器状态:
SYN_RECEIVED
- 服务器收到 SYN 包后,回复一个带有 SYN 和 ACK 标志的数据包。该数据包包含服务器自己的初始序列号(
-
ACK(确认) :
- 客户端收到 SYN-ACK 包后,回复一个带有 ACK 标志的数据包。该数据包包含对服务器 SYN 包的确认序列号(
Ack = y+1)。 - 客户端状态:
ESTABLISHED - 服务器状态:
ESTABLISHED
- 客户端收到 SYN-ACK 包后,回复一个带有 ACK 标志的数据包。该数据包包含对服务器 SYN 包的确认序列号(
三次握手完成后,客户端和服务器之间的连接正式建立,双方可以开始传输数据。
Client Server
| SYN=x |
|------------->| (SYN_SENT)
| | (SYN_RECEIVED)
| SYN=y, ACK=x+1 |
|<-------------|
| |
| ACK=y+1 |
|------------->|
| |
(ESTABLISHED) (ESTABLISHED)
2. TCP 四次挥手
TCP 的四次挥手用于在客户端和服务器之间断开连接。这个过程确保双方都能正确地结束数据传输。以下是四次挥手的详细步骤:
-
FIN(终止) :
- 一方(通常是客户端)发送一个带有 FIN 标志的数据包,表示它已经完成数据传输,请求断开连接。
- 客户端状态:
FIN_WAIT_1
-
ACK(确认) :
- 另一方(服务器)收到 FIN 包后,回复一个带有 ACK 标志的数据包,表示已收到断开连接的请求,但仍有数据要传输。
- 服务器状态:
CLOSE_WAIT - 客户端状态:
FIN_WAIT_2
-
FIN(终止) :
- 服务器完成数据传输后,发送一个带有 FIN 标志的数据包,请求断开连接。
- 服务器状态:
LAST_ACK
-
ACK(确认) :
- 客户端收到服务器的 FIN 包后,回复一个带有 ACK 标志的数据包,表示确认断开连接。
- 客户端状态:
TIME_WAIT - 服务器状态:
CLOSED - 客户端在等待一段时间后(
TIME_WAIT),进入CLOSED状态。
Client Server
| FIN=x |
|------------->| (FIN_WAIT_1)
| | (CLOSE_WAIT)
| ACK=x+1 |
|<-------------|
| | (FIN_WAIT_2)
| FIN=y |
|<-------------| (LAST_ACK)
| |
| ACK=y+1 |
|------------->|
| |
(TIME_WAIT) (CLOSED)
3. 关键点
-
三次握手:
- 确保客户端和服务器都能发送和接收数据。
- 确保双方知道对方的存在和初始序列号。
-
四次挥手:
- 确保双方都能正确地结束数据传输。
- 确保双方都知道连接已经断开。
4. 连接的建立与终止的可靠性
- 三次握手:通过三次握手,双方确认了彼此的存在和发送能力,防止了失效的连接请求造成资源浪费。
- 四次挥手:四次挥手确保了数据的完整传输和连接的正确关闭,即使有一方在发送 FIN 包后仍有数据需要传输。
三、tcp和udp的区别
TCP(Transmission Control Protocol,传输控制协议)和UDP(User Datagram Protocol,用户数据报协议)是计算机网络中两种主要的传输层协议,它们在连接性、可靠性以及头部开销等方面存在区别。以下是详细的对比分析:
-
连接性
- TCP:面向连接的协议,通信前需要建立连接(三次握手),通信后需释放连接(四次挥手)。这种机制确保了数据传输的可靠性[^1^][^2^]。
- UDP:无连接的协议,通信前不需要建立连接,发送方可以随时发送数据,接收方也可以随时接收数据。由于无需建立连接,UDP的延迟较小[^3^][^4^]。
-
可靠性
- TCP:提供可靠的数据传输服务,通过序列号、确认应答、超时重传等机制保证数据的正确性和顺序[^2^][^4^]。
- UDP:不提供可靠性保证,数据包可能会丢失、重复或乱序到达。适用于对实时性要求高但对数据完整性要求不高的应用[^2^][^3^]。
-
头部开销
- TCP:头部较长,包含源端口、目的端口、序列号、确认号、数据偏移、保留位、标志位、窗口大小、校验和等多个字段,总共20字节[^4^]。
- UDP:头部较短,只有8个字节,包括源端口、目的端口、长度和校验和[^4^]。
-
拥塞控制
- TCP:有拥塞控制机制,通过慢开始、拥塞避免、快重传和快恢复等算法来防止网络拥塞,提高传输效率[^2^][^4^]。
- UDP:没有拥塞控制机制,只负责将数据报从源端发送到目的端,不关心网络状况和数据传输质量[^2^][^3^]。
-
应用场景
- TCP:适用于需要可靠传输的场景,如文件存储、电子邮件、远程登录等[^2^]。
- UDP:适用于实时性要求较高但对数据可靠性要求不高的场景,如视频流、音频流、实时游戏等[^2^][^3^]。
总的来说,TCP和UDP各有优劣,选择使用哪种协议取决于具体的应用需求。如果需要确保数据的完整性和顺序,应选择TCP;如果更注重传输速度和实时性,且能容忍一定程度的数据丢失,则UDP是更好的选择。
四、HTTP和HTTPS
1. 基本定义
- HTTP:超文本传输协议,用于在客户端和服务器之间传输数据。默认端口是 80。
- HTTPS:超文本传输安全协议,是 HTTP 的安全版本,通过 SSL/TLS(安全套接层/传输层安全)加密传输数据。默认端口是 443。
2. 安全性
-
加密:
- HTTP:数据以明文形式传输,容易被窃听和篡改。
- HTTPS:数据通过 SSL/TLS 加密,确保数据在传输过程中不能被窃听或篡改。
-
数据完整性:
- HTTP:数据在传输过程中可能会被篡改而不被检测到。
- HTTPS:通过校验机制(如消息认证码,MAC)确保数据未被篡改。
-
身份验证:
- HTTP:没有内置的身份验证机制,容易遭受中间人攻击。
- HTTPS:使用数字证书验证服务器的身份,确保客户端连接到的是真正的服务器。
3. 性能
- HTTP:没有加密和解密的开销,通常速度更快。
- HTTPS:由于加密和解密的过程,通常会稍微慢一些,但现代硬件和优化技术使得这个差异可以忽略不计。
待续