普通视图

发现新文章,点击刷新页面。
昨天以前掘金专栏-iOS学习社区

Swift 中的任务和任务组

2024年9月3日 15:05

原文地址

在上一篇文章探索 Swift 中的 async/await: 构建更清晰的并发模型中提到,如果在不支持并发的同步调用环境中调用一个异步方法,会造成编译错误。今天这篇文章中,我们将介绍如何通过任务和任务组来创建异步函数的使用环境。

任务

Swift 中的任务是 WWDC 2021 引入的并发框架的一部分。任务允许我们从非并发方法创建并发环境,使用async/await调用方法。

当首次使用任务时,你可能会注意到调度队列和任务之间的相似之处。两者都允许在不同线程上以特定优先级调度工作。然而,任务有着明显的不同之处,并通过简化调度队列的繁琐性使我们的开发变得更加轻松。

任务的创建和执行

创建一个任务:

let basicTask = Task {
    return "This is the result of the task"
}

print(await basicTask.value)

任务也可以抛出错误:

let basicTask = Task {
    // .. perform some work
    throw ExampleError.somethingIsWrong
}

do {
    print(try await basicTask.value)
} catch {
    print("Basic task failed with error: (error)")
}

任务在创建后立即运行,不需要显式启动。因此,只有在需要开始它的工作时才创建任务。

在上一篇文章探索 Swift 中的 async/await: 构建更清晰的并发模型中描述的 async_let 语法会隐式创建一个子任务。

取消任务

使用 cancel() 来取消任务:

basicTask.cancel()
处理取消任务

checkCancellation() 和 isCancelled 都可以用于检查任务取消,但它们的作用方式有所不同:

  1. Task.checkCancellation():
  • 作用:  主动抛出错误来终止任务。当调用 checkCancellation() 时,如果任务已被取消,它会抛出一个 CancellationError,从而终止当前任务的执行。

  • 使用场景:

    • 你需要在异步操作的特定位置明确地检查取消状态,并在任务被取消时立即停止执行。
    • 你希望利用 Swift 的错误处理机制来处理任务取消的情况。
  • 例子:

Task {
    // ... 一些异步操作
    try Task.checkCancellation() // 检查是否被取消,如果被取消则抛出错误
    // ... 继续执行其他操作 (如果任务没有被取消)
}

2. Task.isCancelled:

  • 作用:  被动地判断任务是否被取消。它是一个布尔属性,用于判断当前任务是否已被标记为取消。

  • 使用场景:

    • 你需要在异步操作中周期性地检查取消状态,并在任务被取消时执行一些清理操作或改变行为。
    • 你希望更灵活地控制任务取消后的行为,而不仅仅是抛出错误。
  • 例子:

Task {
    while !Task.isCancelled { // 循环执行,直到任务被取消
        // ... 执行一些操作
        try await Task.sleep(nanoseconds: 1_000_000_000) // 模拟 1 秒的延迟
    }
    // ... 任务被取消后执行一些清理操作
}

3. 总结:

  • checkCancellation() 用于主动抛出错误来终止任务,适用于需要立即停止执行的情况。
  • isCancelled 用于被动地判断任务是否被取消,适用于需要更灵活地控制取消行为的情况。
监听取消任务
let task = await Task.withTaskCancellationHandler {
    // ...
} onCancel: {
    print("Canceled!")
}

// ... some time later...
task.cancel()  // Prints "Canceled!"

设置优先级

每个任务都有其优先级,类似于使用任务队列时的QOS,代码如下:

截屏2024-08-20 21.26.17.png

任务状态

Swift 中的 Task 有多种状态,反映了其生命周期中的不同阶段。 可以使用 Task.State 枚举类型来访问这些状态。

  • running: 任务正在执行中。这是任务创建后的初始状态,一直持续到任务完成或被取消。
  • suspended: 任务已暂停执行,等待某些条件满足后才能继续。例如,任务可能正在等待网络请求完成或等待某个资源可用。
  • cancelled: 任务已被取消,可以使用 Task.isCancelled 属性或调用 Task.checkCancellation() 方法来检查任务是否已被取消。
  • completed: 任务已成功完成。您可以使用 Task.value 属性来获取任务的结果。

任务组

Swift 中的任务组用来组合多个并行任务,并等待所有任务完成后返回结果。可以将任务组看作一个包含多个动态添加的子任务的容器。子任务可以并行或串行执行,但是只有在其子任务完成后,Task Group才会被标记为完成。

如何使用任务组

使用 withTaskGroup(of:returning:body:)或 withThrowingTaskGroup(of:returning:body:) 函数来创建任务组。添加到任务组中的所有子任务都会自动并发的开始执行。

  1. 使用任务组,返回最终的结果集,例子如下:
let images = await withTaskGroup(of: UIImage.self, returning: [UIImage].self) { taskGroup in
    let photoURLs = await listPhotoURLs(inGallery: "Amsterdam Holiday")
    for photoURL in photoURLs {
        taskGroup.addTask { await downloadPhoto(url: photoURL) }
    }

    return await taskGroup.reduce(into: [UIImage]()) { partialResult, name in
        partialResult.append(name)
    }
}

2. 错误处理, 例子如下:

let images = try await withThrowingTaskGroup(of: UIImage.self, returning: [UIImage].self) { taskGroup in
    let photoURLs = try await listPhotoURLs(inGallery: "Amsterdam Holiday")
    for photoURL in photoURLs {
        taskGroup.addTask { try await downloadPhoto(url: photoURL) }
    }

    return try await taskGroup.reduce(into: [UIImage]()) { partialResult, name in
        partialResult.append(name)
    }
}

在上面的例子中,如果子任务失败,任务组不会失败;为了在子任务失败时,任务组也失败,需要在迭代的时候使用 next()来代替 while reduce(),代码如下:

let images = try await withThrowingTaskGroup(of: UIImage.self, returning: [UIImage].self) { taskGroup in
    let photoURLs = try await listPhotoURLs(inGallery: "Amsterdam Holiday")
    for photoURL in photoURLs {
        taskGroup.addTask { try await downloadPhoto(url: photoURL) }
    }

    var images = [UIImage]()

    /// Note the use of `next()`:
    while let downloadImage = try await taskGroup.next() {
        images.append(downloadImage)
    }
    return images
}

next() 方法接收来自各个任务的错误,让你可以相应地处理这些错误。在这种情况下,我们将错误传递到组闭包,使整个任务组失败。此时,任何其他正在运行的子任务都将被取消。

避免并发改变

不要在创建 TaskGroup 的任务之外的地方修改它。具体来说,以下行为是不被允许的:

  • 将 TaskGroup 传递给其他任务,并在这些任务中添加子任务。
  • 在其他任务中直接修改 TaskGroup 的状态。

因为并发会导致数据竞争,当多个任务同时修改一个 TaskGroup 时,可能会导致数据不一致,甚至程序崩溃。

取消任务组

你可以使用cancelAll()方法显式地取消任务组中的所有子任务。使用addTaskUnlessCancelled()方法添加的任务,只有在任务组没有被取消的情况下才会开始执行。如果任务组已经被取消,这个任务就不会被添加到任务组中,也不会开始执行。

let photos = await withTaskGroup(of: Optional<Data>.self) { group in
    let photoNames = await listPhotos(inGallery: "Summer Vacation")
    for name in photoNames {
        let added = group.addTaskUnlessCancelled {
            guard !Task.isCancelled else { return nil }
            return await downloadPhoto(named: name)
        }
        guard added else { break }
    }


    var results: [Data] = []
    for await photo in group {
        if let photo { results.append(photo) }
    }
    return results
}

关于如何监听取消的任务,请看任务相关部分的讲解。

参考

  1. www.avanderlee.com/concurrency…
  2. www.avanderlee.com/concurrency…
  3. docs.swift.org/swift-book/…

探索 Swift 中的 async/await: 构建更清晰的并发模型

2024年8月21日 11:02

[原文地址](mp.weixin.qq.com/s?__biz=Mzg…

异步是 iOS 开发中非常常见的操作,之前通常会通过 completion 回调的方式来返回异步结果,为了更好的解释这一点,我们先看一个例子:

fetchImages { result in
    switch result {
    case .success(let images):
        print("Fetched (images.count) images.")
    case .failure(let error):
        print("Fetching images failed with error (error)")
    }
}

即使在这种简单的情况下,由于代码必须编写为一系列完成处理程序,最终会编写嵌套闭包。在这种风格下,具有深度嵌套的更复杂代码很快就会变得笨拙不堪。例子如下:

fetchImages { result in
    switch result {
    case .success(let images):
        print("Fetched (images.count) images.")
        
        resizeImages(images) { result in
            switch result {
            case .success(let images):
                print("Decoded (images.count) images.")
            case .failure(let error):
                print("Decoding images failed with error (error)")
            }
        }
    case .failure(let error):
        print("Fetching images failed with error (error)")
    }
}

传统异步编程的痛点:

  • 回调地狱:
    传统的异步编程方式通常使用回调函数,当一个异步操作完成时,会调用回调函数来处理结果。如果有多个异步操作需要依次执行,就会导致代码嵌套层级过深,难以理解和维护。
  • 错误处理复杂:
    在传统的异步编程中,错误处理需要在回调函数中进行,这使得错误处理逻辑分散,难以追踪。

async/await

Swift 中的 async/await 是一个用于编写异步代码的语法糖,它让异步代码看起来像同步代码一样,从而提高代码的可读性和可维护性。

通过 async/await 来改造文章开头的代码:

do {
    let images = try await fetchImages()
    let resizedImages = try await resizeImages(images)    
    print("Fetched (images.count) images.")
} catch {
    print("Fetching images failed with error (error)")
}

执行顺序是线性的,因此很容易跟踪和理解,代码的可读性增强了。当我们执行复杂的任务时,理解异步代码将会更加容易。

async/await 优势:

  • 更易读的代码:
    async/await 使得异步代码看起来更像同步代码,消除了回调函数带来的嵌套结构,使代码更易于理解和维护。
  • 更简洁的错误处理:
    async/await 允许使用 try/catch 块来处理异步操作中的错误,与同步代码的错误处理方式一致。
  • 更安全的并发:
    async/await 基于 Swift 的结构化并发模型,可以有效地避免数据竞争和死锁等并发问题。

async/await 使用

在异步函数的定义前添加 async 关键字,表示该函数是一个异步函数。如果函数需要标记抛出错误使用 throws。如果函数有返回值,需要在返回箭头(->)之前写上 async。

func fetchImages() async throws -> [UIImage] {
    // perform data request
}

在需要等待异步操作完成的地方使用 await 关键字。

do {
    let images = try await fetchImages()
    print("Fetched (images.count) images.")
} catch {
    print("Fetching images failed with error (error)")
}

异步函数在执行过程中,需要等待某些事情时,它也可以在中间暂停。在异步函数或方法的主体内部,你可以标记每一个需要被暂停执行的地方。

并行执行异步函数

使用 await 调用异步函数一次只运行一段代码。在异步代码运行时,调用者会等待该代码执行完成,然后再继续执行下一行代码。例如:

let firstPhoto = await downloadPhoto(named: photoNames[0])
let secondPhoto = await downloadPhoto(named: photoNames[1])
let thirdPhoto = await downloadPhoto(named: photoNames[2])

let photos = [firstPhoto, secondPhoto, thirdPhoto]
show(photos)

可以使用 async-let 来实现异步代码的并行执行:

async let firstPhoto = downloadPhoto(named: photoNames[0])
async let secondPhoto = downloadPhoto(named: photoNames[1])
async let thirdPhoto = downloadPhoto(named: photoNames[2])

let photos = await [firstPhoto, secondPhoto, thirdPhoto]
show(photos)

异步函数使用环境

首次使用 async-await 时,可能会遇到下面的错误:

17240607763095.png

这个错误是因为在不支持并发的同步调用环境中调用了一个异步方法而引起的。我们可以通过将我们的 fetchData 方法也定义为异步来解决这个错误:

func fetchData() async {
    do {
        try await fetchImages()
    } catch {
        // .. handle error
    }
}

然而,这样做会将错误转移到不同的位置。相反,我们可以使用 Task.init 方法从一个支持并发的新任务中调用异步方法,并将结果分配给我们的视图模型中的属性:

final class ContentViewModel: ObservableObject {
    
    @Published var images: [UIImage] = []
    
    func fetchData() {
        Task { @MainActor in
            do {
                self.images = try await fetchImages()
            } catch {
                // .. handle error
            }
        }
    }
}

参考资料

  1. docs.swift.org/swift-book/…
  2. www.avanderlee.com/swift/async…

iOS RunLoop 介绍

2024年8月13日 14:05

原文地址

RunLoop

一般来说,一个线程一次只能执行一个任务,任务执行完成后线程就会退出。为了保持线程的忙碌状态并在没有任务时将线程置于休眠状态,我们需要一种机制,这就是运行循环(RunLoop)。 截屏2024-08-12 17.41.30.png 简单来说,运行循环是一个事件驱动的大循环,它确保线程在有任务需要处理时保持忙碌状态,并在没有任务时进入休眠状态。下面是一个简单的伪代码示例,展示了运行循环的基本结构:

int main(int argc, char * argv[]) {
     //程序一直运行状态
     while (AppIsRunning) {
          //睡眠状态,等待唤醒事件
          id whoWakesMe = SleepForWakingUp();
          //得到唤醒事件
          id event = GetEvent(whoWakesMe);
          //开始处理事件
          HandleEvent(event);
     }
     return 0;
}

Runloop Mode

运行循环模式是一种机制,用于管理运行循环中的输入源(input sources)、定时器(timers)以及运行循环观察者(run loop observers)。

通过这种机制,运行循环可以在不同的模式下运行,以控制不同类型的事件源和任务,确保只有与当前模式相关联的源被处理,从而增强了对事件源和观察者的精确控制能力。一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source/Timer/Observer。

系统提供了以下几种常见的 Mode :

  • NSDefaultRunLoopMode: 默认 Mode,通常主线程是在这个 Mode 下运行的。
  • UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。
  • UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。
  • NSRunLoopCommonModes:这是一个可配置的常用模式组合。将输入源与这个模式关联也会将其与组中的每个模式关联起来。Timer 计时会被 scrollView 的滑动影响的问题可以通过将 timer 添加到NSRunLoopCommonModes来解决。

事件源

CFRunLoopSource 是对 input sources 的抽象。CFRunLoopSource 分为 source0 和 source1 两种类型:

Source1 是内核自动发送的,包含了一个 mach_port 和一个回调,被用于通过内核和其他线程相互发送消息。

Source0 是App内部事件,由 App 自己管理的 UIEvent、CFSocket 都是 source0,只包含了一个回调,它并不能主动触发事件。使用时,需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。

timer 基于时间的触发器,在未来的指定时间将事件同步传递到线程。通过 CFRunLoopTimerRef 来实现,它和 NSTimer 是 toll-free bridged 的。当其加入到 RunLoop 时,RunLoop 会注册对应的时间点,当时间点到达时,RunLoop 会被唤醒以执行那个回调。

// timer 会自动添加到当前线程的 NSDefaultRunLoopMode 中
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
    // 执行定时器触发的操作
    self.timerTick(timer)
}

// 将 timer 手动添加到当前线程的 NSRunLoopCommonModes 中
let timer = Timer(timeInterval: 1.0, repeats: true) { timer in
    // 执行定时器触发的操作
    self.timerTick(timer)
}

RunLoop.current.add(timer, forMode: .common)

运行循环观察者

Runloop 会在特定时刻发送状态变化的通知。当 RunLoop 的状态发生变化时,观察者就能通过回调来接收这个变化。Runloop 可观察时间点有以下几个:

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry         = (1UL << 0), // 即将进入 Loop
    kCFRunLoopBeforeTimers  = (1UL << 1), // 即将处理 Timer
    kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
    kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
    kCFRunLoopAfterWaiting  = (1UL << 6), // 刚从休眠中唤醒
    kCFRunLoopExit          = (1UL << 7), // 即将退出Loop
    kCFRunLoopAllActivities = 0x0FFFFFFFU   // 观察所有状态变化
};

Runloop 事件队列

每次运行 runloop, 线程的 runloop 会处理挂起的事件,并通知相关的观察者。其具体的流程如下: 截屏2024-08-12 17.41.47.png

源码地址:opensource.apple.com/source/CF/C…

参考

  1. blog.ibireme.com/2015/05/18/…
  2. developer.apple.com/library/arc…

使用Xcode预览快速查看应用程序界面

2024年7月19日 14:06

[原文地址](mp.weixin.qq.com/s?__biz=Mzg…

在应用程序开发过程中,设计和调整界面是一个重要的环节。为了更高效地进行界面设计和开发,苹果在Xcode 15中引入了预览功能,让开发者能够通过代码对应用程序的视图进行更改,并在预览画布中即时查看这些更改的结果。

然而,在Xcode 16中,苹果进一步优化了预览功能,为开发者提供了更强大的工具和功能来提升开发体验。下面我们来一起探索一下Xcode 16中增强的预览功能吧!

如何显示预览视图

当你在Xcode中创建一个视图时,你可以在预览画布中显示它。预览画布展示了你的视图在不同设备的各种配置下的显示情况;

截屏2024-07-18 20.49.34.png Swift预览宏是一段代码片段,用于创建和配置你的视图。你可以使用其中一个预览宏,例如Preview(_:body:),告诉Xcode要显示什么内容。要手动显示或隐藏预览画布,请在Xcode菜单中选择"Editor > Canvas"。

class WeatherViewController: UIViewController {
    // ...
}


// A UIKit UIViewController preview.
#Preview {
    let viewController = WeatherViewController()
    viewController.title = "Current Weather"
    return viewController
}


class WeatherView: UIView {
    var icon: UIImage?
}


// A UIKit UIView preview.
#Preview {
    let view = WeatherView()
    if let image = UIImage(systemName: "sun.max.fill") {
        view.icon = image
    }
    return view
}

与预览视图的交互模式

实时模式

当你在实时模式下与预览视图进行交互,就像在设备或模拟器上来展现和操作你的视图一样。使用该预览模式来测试控制逻辑、动画、文本输入以及对异步代码的响应。 截屏2024-07-18 20.04.01.png

选择模式

在选择模式下,预览会显示视图的快照,以便您可以在画布中与视图的UI元素进行交互。在预览中选择一个控件会突出显示源代码编辑器中相应的代码行。双击某些文本视图,例如标签(Label),会将焦点移动到源代码编辑器,以便您可以快速进行更改。 截屏2024-07-18 20.06.58.png

控制预览的显示方式

  1. 可以使用设备设置来控制预览在特定设备上的显示方式。例如,要查看您的视图在暗黑模式、横竖屏和超大文本下的外观;

截屏2024-07-18 20.10.43.png

  1. 还可以使用变体模式(Variant mode)可以查看你的视图在给定配置的不同变体下的外观。 17213053595690.png
  • 以下是预览画布支持的几种常用变体:
    • 颜色模式变体:预览画布提供了浅色和深色两种颜色模式的预览。
    • 方向变体:预览画布可显示你的视图在各种纵向和横向方向的外观。
    • 动态类型变体:预览画布支持以应用程序的所有辅助功能文本大小显示视图。

通过利用设备的设置功能和变体模式,你可以更全面地测试和优化你的视图,以确保其在不同设备、颜色方案和辅助功能设置下都能以最佳方式呈现给用户。这有助于提升你的应用程序的可用性和用户体验质量。

使用代码来配置预览

除了Xcode提供的预览选项之外,您还可以以代码方式自定义和配置要重复使用的预览。

例如,您可以为每个预览添加一个名称,以便更容易跟踪每个预览显示的内容。当您将预览的名称作为字符串传递给预览宏时,该名称将显示在预览画布中预览的标题中。

#Preview("2x2 Grid Portrait") {
   Content()
}

还可以通过将一个或多个配置特性作为可变参数列表传递给预览宏,来控制预览的显示方式。例如,要在左横向方向中显示您的视图,请通过将landscapeLeft类型属性传递给init(_:traits:body:)预览初始化器,告诉Xcode要显示的方向。

#Preview("Camera setting sunning day", traits: .landscapeLeft) {
    let viewController = CameraViewController()
    if let image = UIImage(systemName: "sun.max.fill") {
        viewController.lastImage = image
    }
    return viewController
}

总结

在创建视图时,只传递视图显示所需的数据,避免传递获取数据的对象;对象会使设置视图预览变得更复杂且性能较差。

相反,使用视图所需的最少数据创建视图,优先选择简单的不可变数据类型。以这种方式创建视图可以更轻松地测试和预览视图,并使其性能更好。

软件架构之SOLID原则

2023年4月17日 13:52

关注公众号,提前Get更多技术好文

当开发大型软件时,编写易于维护和扩展的代码变得至关重要。SOLID原则是一组指导原则,可帮助我们实现高质量、易于维护的代码。这些原则旨在使软件架构更加健壮、灵活和可扩展,从而减少后期维护成本。

SOLID原则由Robert C. Martin在他的书籍《Agile Software Development, Principles, Patterns, and Practices》中提出。它们代表了一组面向对象编程(OOP)的最佳实践。下面将介绍SOLID原则的五个组成部分。

  • 单一职责原则(Single Responsibility Principle, SRP): 一个类应该只有一个引起它变化的原因。这意味着一个类应该只有一项职责,因此它应该只有一个原因需要被修改。通过将代码分解成更小、更简单的部分,我们可以轻松地对其进行修改和维护,从而提高代码的可读性和可维护性。

  • 开放封闭原则(Open-Closed Principle, OCP): 软件实体(类、模块、函数等)应该是对扩展开放的,但对修改关闭的。这意味着当我们需要增加新功能时,我们应该尽可能地利用现有的代码,而不是修改它们。这可以通过使用接口和抽象类等OOP技术来实现。

  • 里氏替换原则(Liskov Substitution Principle, LSP): 所有引用基类的地方必须能够透明地使用其子类的对象。这意味着子类应该能够替换其基类,并且程序的行为不会受到影响。遵循LSP原则可以确保代码的正确性和稳定性。

  • 接口隔离原则(Interface Segregation Principle, ISP): 客户端不应该依赖于它不需要的接口。这意味着我们应该将接口拆分成更小、更特定的部分,以避免客户端代码依赖于不必要的接口。这将提高代码的可维护性和可扩展性。

  • 依赖反转原则(Dependency Inversion Principle, DIP): 高层模块不应该依赖于低层模块,它们应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象。这可以通过依赖注入(DI)等技术实现。遵循DIP原则可以降低模块之间的耦合度,从而提高代码的灵活性和可扩展性。

单一职责原则

SRP 是 SOLID 五大设计原则中最容易被误解的一个,也许是名字的原因,很多程序员认为这个原则就是指:每个模块都应该只做一件事。然而,这并不是 SRP 的全部。在现实环境中,软件系统为了满足用户需求,必然要做出这样那样的修改,所以 SRP 的最终描述就变成了:

任何一个软件模块都应该只对某一类行为者负责。

“软件模块”指的是一组紧密相关的函数和数据结构。相关这个词实际上就隐含了 SRP 这一原则。代码和数据就是靠着与某一类行为者的相关性被组合在一起的。

下面看一个违反 SRP 原则的例子:

工资管理程序中的 Employee 类有三个函数,calculatePay()、reportHours()和save()。这三个函数分别对应三个不同的行为者。

截屏2023-04-09 20.59.28.png

这三个函数被放在同一个类中,这样做实际上是将三类行为者的行为耦合在了一起,这有可能导致CFO团队的命令影响到了COO团队所依赖的功能。

有很多不同的方法可以解决上面的问题,每一种方法都需要将相关函数划分成不同的类,即使每个类都只对应一类行为者。

小结: 单一职责原则主要讨论的是函数与类之间的关系。

开闭原则

开闭原则是由 Bertrand Meyer 在1988年提出的,该设计原则认为:

设计良好的计算机软件应该易于扩展,同时抗拒修改。

一个良好的软件架构师会努力将旧代码的修改需求量降至最小,甚至为0。

下面通过一个例子来了解开闭原则:

假设我们需要设计一个在Web页面上展示财务数据的系统,页面上的数据需要滚动展示,其中负值显示为红色。接下来,该系统的所有者又要求用同样的数据生成一份报表,该报表可以用黑白打印机打印,同时报表格式要得到合理分页等。

  • 首先,我们可以将不同需求的代码分组(SRP)

截屏2023-04-09 20.58.31.png

  • 然后再调整这些分组之间的依赖关系(DIP)

截屏2023-04-09 20.57.42.png

这里很重要的一点是这些单线框的边界都是单向跨越的。也是说,上面的所有组件之间的关系都是单向依赖的。

如果组件A不像因为组件B的修改而受到影响,那么就该让组件B依赖于组件A。

小结: OCP 是我们进行架构设计的主导原则,其主要目标是让系统易于扩展,同时限制其每次被修改所影响的范围。实现方式是通过将系统划分为一系列组件,并将这些组件间的依赖关系按层次结构进行组织,使得高阶组件不会因低阶组件被修改而受到影响。

里氏替换原则

里氏替换原则是由美国计算机科学家Barbara Liskov 在1987年提出的。她在一篇名为《数据抽象和层次》的论文中首次提出了该原则,该原则强调了在面向对象编程中继承的使用,即子类对象应该能够替换其父类对象并且仍然能够保持原有的行为表现。

下面通过一个简单的例子来了解 LSP:

16810452239304.png

上述设计是符合 LSP 原则的,因为 Bi11ing 应用程序的行为并不依赖于其使用的任何一个衍生类。也就是说,这两个衍生类的对象都是可以用来替换 License 类对象的。

接口隔离原则

“接口隔离原则”这个名字来自下图这种软件结构。

16810453903567.png

有多个用户需要操作 OPS 类。但是 User1 只需要使用 op1, User2 只需要使用 op2,User3 只需要使用op3。

在这种情况下,User1 虽然不需要调用 op2、op3,但在源代码层次上也与它们形成依赖关系。这种依赖意 味着我们对 OPS 代码中 op2 所做的任何修改,即使不会影响到 User1 的功能,也会导致它需要被重新编译和部署。 这个问题可以通过将不同的操作隔离成接又来解決,具体如图所示:

16810457636993.png

User1 的源代码会依赖于 U1Ops 和 op1,但不会依赖于OPS。这样一来,我们之后对OPS做的修改只要不影响到 User1 的功能,就不需要重新编译和部署 User1 了。

**小结:**任何层次的软件设计如果依赖了它并不需要的东西,就会带来意料之外的麻烦。

依赖反转原则

依赖反转原则(DIP)主要想告诉我们的是,如果想要设计一个灵活的系统,在源代码层次的依赖关系中就应该 多引用抽象类型,而非具体实现。

我们每次修改抽象接口的时候,一定也会去修改对应的具体实现。相反,当我们修改具体实现时,却很少需要修改相应的抽象接口。所以我们认为接口比实现更稳定。

下面通过一个例子来了解DIP

以一个图书馆管理系统为例,假设系统需要支持三种不同类型的书籍:小说、历史书籍和科学书籍。同时,系统需要能够提供书籍的分类、名称、作者等信息,并支持借出和归还书籍的功能。

按照依赖反转原则,我们可以通过抽象类或接口来定义一个书籍的通用接口,所有的具体书籍类型都实现该接口,如下所示:

protocol Book {
    var name: String { get }
    var author: String { get }
    var type: String { get }
    var isBorrowed: Bool { get set }
}

class Novel: Book {
    var name: String
    var author: String
    var type: String = "小说"
    var isBorrowed: Bool = false
    
    init(name: String, author: String) {
        self.name = name
        self.author = author
    }
}

class HistoryBook: Book {
    var name: String
    var author: String
    var type: String = "历史书籍"
    var isBorrowed: Bool = false
    
    init(name: String, author: String) {
        self.name = name
        self.author = author
    }
}

class ScienceBook: Book {
    var name: String
    var author: String
    var type: String = "科学书籍"
    var isBorrowed: Bool = false
    
    init(name: String, author: String) {
        self.name = name
        self.author = author
    }
}

此时,所有书籍类型都实现了Book接口,并且都能提供相同的属性和方法。这样一来,我们在使用这些不同类型的书籍时,就可以依赖于它们实现的相同接口,而不是依赖于具体的书籍类型。

下面是一个简单的借阅功能实现的例子:

class Library {
    var books: [Book] = []
    
    func borrow(book: Book) {
        guard let index = books.firstIndex(where: { $0.name == book.name }) else {
            print("该书不存在")
            return
        }
        
        if books[index].isBorrowed {
            print("该书已被借出")
        } else {
            books[index].isBorrowed = true
            print("借书成功")
        }
    }
    
    func returnBook(book: Book) {
        guard let index = books.firstIndex(where: { $0.name == book.name }) else {
            print("该书不存在")
            return
        }
        
        if !books[index].isBorrowed {
            print("该书未被借出")
        } else {
            books[index].isBorrowed = false
            print("还书成功")
        }
    }

在这个例子中,Library类并不依赖于具体的书籍类型,而是依赖于Book接口。这使得我们可以轻松地将来增加更多的书籍类型,而无需更改Library类的代码。

优秀的软件架构师会花费大精力来设计接口,以减少未来对其进行改动。 毕竟争取在不修改接口的情况下为软 件增加新的功能是软件设计的基础常识。

也就是说,如果想要在软件架构设计上追求稳定,就必须多使用稳定的抽象接口,少依赖多变的具体实现。下面有几条具体的编码守则:

  • 应在代码中多使用抽象接口,尽量避免使用那些多变的具体实现类。
  • 不要在具体实现类上创建衍生类。在静态类型的编程语言中,继承关系是所有一切源代码依赖关系中最强的、最难被修改的,所以我们对继承的使用应该格外小心。即使是在稍微便于修改的动态类型语言中,这条守则也应该被认真考虑。
  • 不要覆盖(override)包含具体实现的函数。调用包含具体实现的函数通常就意味着引入了源代码级别的依赖。即使覆盖了这些函数,我们也无法消除这其中的依赖。这些函数继承了那些依赖关系。在这里,控制依赖关系的唯一办 法,就是创建一个抽象函数,然后再为该函数提供多种具体实现。
  • 应避免在代码中写入与任何具体实现相关的名字,或者是其他容易变动的事物的名宇。这基本上是DIP原则的另外一个表达方式。

如果想遵守上述的编码守则,我们就必须对那些易变对象的创建过程做一些特殊处理,这样的谨慎是很有必要的,因为,基本在所有的编程语言中,创建对象操作都免不了在源代码层次上依赖对象的具体实现。

在大部分编程语言中,人们都会选择抽象工厂模式来解决源代码依赖问题。

16810529229120.png

这条曲线将整个系统划分为两部分组件:抽象接口层与具体实现层边界。抽象接口组件中包含了应用的所有高阶业务规则,而具体实现组件则包括了所有这些规则所需要做的具体操作及其相关的细节信息。

小结: 依赖反转原则是一条非常重要的软件设计原则,它可以帮助我们设计出高度解耦的系统,提高系统的灵活性和可扩展性。


注:本文大部分内容来自于《架构整洁之道》

iOS定时器的选择:CADisplayLink、NSTimer和GCD定时器

2023年3月29日 14:55

原文地址

iOS应用中经常需要使用定时器来处理某些任务,例如执行动画、更新UI等。iOS提供了多种定时器类型,包括CADisplayLink、NSTimer和GCD定时器。不同的定时器类型适用于不同的场景和需求,因此在选择定时器类型时需要根据具体的情况进行选择。

CADisplayLink

CADisplayLink是一种定时器类型,它可以让你在每秒钟屏幕更新时执行一段代码。CADisplayLink定时器的精度非常高,因为它是和屏幕刷新频率同步的,所以可以确保动画的流畅度。另外,CADisplayLink定时器的调用方法是通过RunLoop进行的,所以它是线程安全的。

使用CADisplayLink定时器的步骤如下:

  • 创建CADisplayLink对象。
  • 设置定时器的目标和选择器。
  • 将CADisplayLink添加到RunLoop中。
CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(update)];
[displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];

NSTimer

NSTimer是iOS中另一种常用的定时器类型,它可以让你在一段时间后执行一段代码。NSTimer定时器的精度相对较低,因为它不是和屏幕刷新频率同步的,所以在一些对精度要求比较高的场景下可能不适用。另外NSTimer定时器的调用方法是通过RunLoop进行的,所以它也是线程安全的。

使用NSTimer定时器的步骤如下:

  • 创建NSTimer对象。
  • 设置定时器的目标和选择器。
  • 将NSTimer添加到RunLoop中。
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(update) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

需要注意的是,:NSTimer 被添加到 RunLoop 后会持有目标对象,容易导致循环引用问题,需要注意解除循环引用。

GCD定时器

GCD定时器是iOS中一种常见的定时器方式,使用Grand Central Dispatch (GCD)框架提供的功能实现。相比于传统的NSTimer和CADisplayLink,GCD定时器具有更高的精度和更好的性能,尤其是在多线程场景下表现更为优秀。

GCD定时器的实现原理是使用GCD的dispatch_source_t来创建一个定时器源,然后将该定时器源与需要执行的任务关联起来。通过GCD的API可以设置定时器的触发时间、重复次数等参数,并且可以很方便地在多线程环境下使用。

下面是一个简单的GCD定时器的示例代码:

// 创建一个GCD定时器
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());

// 设置定时器的触发时间、间隔时间和重复次数
dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC));
uint64_t interval = (uint64_t)(1.0 * NSEC_PER_SEC);
dispatch_source_set_timer(timer, start, interval, 0);

// 设置定时器的触发事件
dispatch_source_set_event_handler(timer, ^{
    NSLog(@"GCD Timer fired");
});

// 启动定时器
dispatch_resume(timer);

需要注意的是,在使用GCD定时器时,我们需要确保在合适的时间停止定时器,并释放相关资源。停止 Dispatch Timer 有两种方法,一种是使用 dispatch_source_cancel,另外一种是使用 dispatch_suspend。

  • 使用dispatch_source_cancel函数停止定时器,示例代码如下:
// 停止定时器
dispatch_source_cancel(timer);

// 释放资源
timer = nil; 
  • 使用dispatch_suspend函数停止定时器,dispatch_suspend 严格上只是把 Timer 暂时挂起,它和 dispatch_resume 是平衡调用的,两者分别会减少和增加 dispatch 对象的挂起计数,当这个计数大于 0 的时候,Timer 就会执行。

另外一个很重要的注意事项,dispatch_suspend 之后的 Timer,不能被释放!下面的代码会引起崩溃:

- (void)dealloc {
    dispatch_suspend(timer);
    
    timer = nil; // EXC_BAD_INSTRUCTION 崩溃
}

这是因为 GCD 的 dispatch source 在释放的时候会判断当前是否处于挂起状态。如果是挂起状态,则需要在调用 dispatch_resume() 恢复到活动状态后才能正常释放,否则会产生崩溃。

总结

在本文中,我们介绍了三种常见的定时器方法:CADisplayLink、NSTimer和GCD定时器。这些定时器方法都有其优点和适用场景。CADisplayLink主要用于渲染动画,NSTimer用于周期性执行任务,而GCD定时器则更加灵活,可以在不同线程中执行任务。

需要注意的是,在使用这些定时器方法时,我们要避免一些常见的问题。例如,在使用CADisplayLink时,要注意循环引用的问题;在使用NSTimer时,要注意循环引用和线程阻塞的问题;在使用GCD定时器时,要注意定时器的生命周期和线程安全的问题。

总的来说,我们应该根据实际的需求选择合适的定时器方法,并且合理地使用这些方法,避免出现一些常见的问题,从而保证程序的正常运行。

深入理解Objective-C中的@Synchronized关键字

2023年3月27日 20:53

原文地址

在多线程编程中,线程之间共享资源时容易出现数据竞争的问题,导致程序出现不可预期的结果。为了避免这种情况,我们需要采用一些同步机制来保证线程之间的安全协作。 @synchronized指令是Objective-C中一种常用的同步机制。

@synchronized指令是Objective-C中一种非常简单方便的创建锁的方式。相比于其他锁,它的语法更加简单,只需要使用任意一个Objective-C对象作为锁标记即可。

- (void)myMethod:(id)anObj {
    @synchronized(anObj) {
        // Everything between the braces is protected by the @synchronized directive.
    }
}

@synchronized指令中传递的对象是用于区分受保护代码块的唯一标识符。如果在两个不同的线程中执行上述方法,分别为anObj参数传递不同的对象,那么每个线程都会获取自己的锁并继续处理,而不会被另一个线程阻塞。但是,如果在这两种情况下都传递相同的对象,则其中一个线程会首先获取锁,另一个线程则会被阻塞,直到第一个线程完成操作。

@Synchronized的底层实现

通过clang查看底层编译代码可知, @Synchronized是通过objc_sync_enter和objc_sync_exit函数来实现锁的获取和释放的,源码如下:

int objc_sync_enter(id obj)
{
    int result = OBJC_SYNC_SUCCESS;

    if (obj) {
        SyncData* data = id2data(obj, ACQUIRE);
        ASSERT(data);
        data->mutex.lock();
    } else {
        // @synchronized(nil) does nothing
        if (DebugNilSync) {
            _objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug");
        }
        objc_sync_nil();
    }

    return result;
}

int objc_sync_exit(id obj)
{
    int result = OBJC_SYNC_SUCCESS;
    
    if (obj) {
        SyncData* data = id2data(obj, RELEASE); 
        if (!data) {
            result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
        } else {
            bool okay = data->mutex.tryUnlock();
            if (!okay) {
                result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
            }
        }
    } else {
        // @synchronized(nil) does nothing
    }
    

    return result;
}
  • 如果传入的obj存在,则走加锁流程;如果obj为nil,则什么也不做。
  • objc_sync_exit和objc_sync_enter是对应的;objc_sync_exit方法就是解锁,如果obj= nil则什么也不做;

通过观察源码可知,objc_sync_exit和objc_sync_enter里的关键是从obj转换到SyncData,然后通过SyncData中的mutex来对临界区上锁。SyncData结构体的定义如下:

typedef struct alignas(CacheLineSize) SyncData {
    struct SyncData* nextData;
    DisguisedPtr<objc_object> object;
    int32_t threadCount;  // number of THREADS using this block
    recursive_mutex_t mutex;
} SyncData;
  • mutex是递归锁,这也是为什么可以在 @Synchronized里嵌套 @Synchronized的原因了。

从obj转换到SyncData的具体实现如下:

截屏2023-03-16 16.10.19.png

这段代码实现了一个锁的缓存机制,目的是为了提高多线程访问同一对象时的效率。当多个线程同时访问同一对象时,每个线程需要获取一个锁,这会造成性能瓶颈。为了避免这个问题,缓存机制会将已经获取的锁缓存起来,以供下次使用。其大致流程如下:

1、首先检查是否启用了快速缓存,如果启用则在快速缓存中查找是否有与obj对应的SyncData对象。
2、如果在快速缓存中找到了匹配的SyncData对象,则将syncLockCount加1,并返回结果。
3、如果没有在快速缓存中找到匹配的SyncData对象,则继续在线程缓存中查找是否有与obj对应的锁。
4、如果在线程缓存中找到了匹配的锁,则将对应锁的计数加1,并将其返回结果。
5、如果没有在线程缓存中找到匹配的锁,则在全局的哈希表中查找是否有与obj对应的SyncData对象。
6、如果在全局的哈希表中找到了匹配的SyncData对象,则会进行多线程操作,将对应锁的计数加1,并返回结果。
7、如果没有在全局的哈希表中找到匹配的SyncData对象,则创建新对象,并将新对象添加到上述的缓存中,以供下次使用。

badcase分析

#import "ViewController.h"

@interface ViewController ()

@property (nonatomic, strong) NSMutableArray *testArray;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    self.testArray = @[].mutableCopy;

    for (NSUInteger i = 0; i < 5000; i++) {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            [self testThreadArray];
        });
    }
}

- (void)testThreadArray {
    @synchronized (self.testArray) {
        self.testArray = @[].mutableCopy;
    }
}


@end

运行这段代码,会出现如下crash:

截屏2023-03-25 20.59.43.png

考虑这个场景,有三个线程A、B、C同时访问一个非原子属性self.testArray,初始值为p0。线程A和线程B由于访问的self.testArray的值一致,产生了竞争,线程A获取了锁并将self.testArray的值重新设置为p1,然后释放了锁。此时线程C访问self.testArray,发现其值为p1,没有竞争,准备对其进行赋值操作。然而,此时线程B由于之前的锁已经被释放,进入代码块,也准备对self.testArray进行赋值操作,这会导致两个线程同时对非原子属性self.testArray进行赋值操作,从而产生crash。

iOS 中的数据持久化

2022年5月20日 12:11

原文地址

在应用开发过程中,数据持久化是不可或缺的一部分。今天的文章会和大家介绍一下 iOS 中的数据持久化方案及相关特点,以便在开发过程中选择合适的数据持久化方案,避免出现不必要的错误。

沙盒机制

出于安全的原因,iOS 应用在安装时,为每个 App 分配了独立的目录,App 只能对自己的目录进行操作,这个目录就被称为沙盒。

与安卓系统不同,iOS 系统比较封闭,没有提供类似的内存卡扩展功能,也没有开放的文件管理,所以 iOS 系统的手机上是看不到文件目录的。

沙盒中主要包含4个目录: MyApp.appDocumentsLibraryTmp,目录结构如下:

截屏2022-05-19 16.55.08.png

MyApp.app

该目录用于存放应用本身的数据,包括资源文件和可执行文件。应用在被安装时,会将该目录签名,如果修改这个目录,签名会被改变,应用将无法启动。不会被 iTunes 和 iCloud 同步。

Documents

通常用来保存用户数据,用户数据通常包括希望向用户公开的任何文件(希望用户创建、导入、删除或编辑的任何内容)。该目录可以通过文件共享提供给用户。会被iTunes和iCloud同步。

NSString *docDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];

Library

此目录通常包含应用程序运行时使用的文件,这些文件对用户是不可见的。该目录除 Caches 子目录以外会被 iTunes 和 iCloud 同步。

Library/Caches

通常用于存储运行时产生的临时文件及缓存文件,在空间不足时可能会被系统自动清除,因此,应用程序应该具备重新创建和下载这些文件的能力。

NSString *cachesDir = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject];
Library/Preferences

通常用于存储应用的偏好设置数据,不要试图直接访问该目录。修改此目录下的 plist 文件,可能会导致修改丢失、延迟反映更改或应用程序崩溃等意外情况。

Temp

用来保存不重要的临时文件,在应用程序没有运行时,系统会自动清除这些文件,因此,在应用程序终止后,不能依赖这些文件的持久性。不会被 iTunes 和 iCloud 同步。

NSString *tmpDir =  NSTemporaryDirectory();

序列化与反序列化

要将对象存储到磁盘,需要将其转化成二进制数据,这一步叫做序列化。相反,将二进制数据转换成对象,则称为反序列化。

iOS 中的对象要实现序列化和反序列化,需要实现 NSCoding 协议:

-(void) encodeWithCoder:(NSCoder *)aCoder;
-(instancetype) initWithCoder:(NSCoder *)aDecoder;

举个例子:

@interface User : NSObject<NSCoding>

-(instancetype) initWithCoder:(NSCoder *)aDecoder {
    _userName = [aDecoder decodeObjectForKey:@"usrName"];
    _passWord = [aDecoder decodeObjectForKey:@"passWord"];
}

-(void) encodeWithCoder:(NSCoder *)aCoder {
    [aCoder encodeObject:_userName forKey:@"userName"];
    [aCoder encodeObject:_passWord forKey:@"passWord"];
}

@end
  • 存储:
BOOL flag = [NSKeyedArchiver archiveRootObject:user toFile:path];
  • 读取:
user = [NSKeyedUnarchiver unarchiveObjectWithFile: path];

此外,也可以使用一些优秀的三方库(如:YYModel)来实现对象的序列化和反序列化。

数据持久化方案介绍

NSUserDefaults

NSUserDefaults 是轻量级的数据持久化方案,主要用于存储应用程序的配置信息等一些比较小的数据。其特点如下:

  • 是一个单例,且是线程安全的。
  • 存储在 Library/Preferences 目录下。
  • 以 plist 的形式进行存储。
  • 当存储的数据是可变类型时,读取时会变为不可变。
synchronize

NSUserDefaults 会定时把缓存中的数据写入磁盘,而不是立即写入,为了防止在写完 NSUserDefaults 后程序退出导致的数据丢失,可以在写入数据后使用 synchronize 来强制立即将数据写入磁盘:

[[NSUserDefaults standardUserDefaults] synchronize];

但也要注意,不能频繁的使用 synchronize。

支持的数据类型

支持的数据类型有 NSData, NSString, NSNumber, NSDate, NSArray, NSDictionary 等系统定义的数据类型,如果要存放其他数据类型或者自定义的对象,则必须将其转换成 NSData 存储。

SQLite3

用来存储大规模的数据,是一款轻型的关系型数据库。它具有操作简单、小巧、快速,可靠的特点。

创建和打开数据库
NSString *docPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
NSString *fileName = [docPath stringByAppendingPathComponent:@"test.sqlite"];
const char *cFileName = fileName.UTF8String;
int result = sqlite3_open(cFileName, &_db); // 打开数据库文件, 如果数据库文件不存在,会自动创建数据库文件
if (result != SQLITE_OK) {
    NSLog(@"打开数据库失败");
    return;
}

NSLog(@"打开数据库成功");
建表
const char *sql = "CREATE TABLE IF NOT EXISTS test_table (id integer PRIMARY KEY AUTOINCREMENT, test_key char)";
char *errMsg = NULL;
result = sqlite3_exec(_db, sql, NULL, NULL, &errMsg);
if (result == SQLITE_OK) {
    NSLog(@"创建表成功");
} else {
    NSLog(@"创建表失败");
}
执行 sql 语句 
sqlite3_stmt *stmt;
const char *insertSQL = "insert into test_table(test_key) values('test')";
int result =  sqlite3_prepare_v2(_db, insertSQL, -1, &stmt, nil)

if (result == SQLITE_OK) {
    NSLog(@"插入数据成功");
} else {
    NSLog(@"插入数据失败");
}
常用的三方库

由于 sqlite3 的原生语言是 C 语言,与 OC 的使用风格不一样,对于 iOS 开发者来说,不是很友好,容易出错,这里列出一些封装好的三方库,大家可以根据实际情况选择使用。

  • FMDB
  • WCDB
  • Realm

CoreData

CoreData 是苹果提供的一种应用数据管理框架,可以通过图形界面的方式快速定义 App 的数据模型,并且提供了对象模型和关系数据映射的能力,将模型对象转化成关系数据保存到 SQLite 数据库中,也可以将保存到 SQLite 数据库中的关系数据转换成对象模型。

截屏2022-05-19 21.46.42.png

加载数据模型
NSURL *modelPath = [[NSBundle mainBundle] URLForResource:@"Student" withExtension:@"momd"];
NSManagedObjectModel *model = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelPath];

创建数据库
NSPersistentStoreCoordinator *coordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:model];
NSString *dataPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).lastObject;
dataPath = [dataPath stringByAppendingPathComponent:@"Student.sqlite"];
[coordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:[NSURL fileURLWithPath:dataPath] options:nil error:nil];
数据库关联缓存
NSManagedObjectContext *context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
context.persistentStoreCoordinator = coordinator;
插入数据
Student * student = [NSEntityDescription insertNewObjectForEntityForName:@"Student" inManagedObjectContext:context];

student.name = @"albert";
student.age = 22;

NSError *error = nil;
[context save:&error];
查询数据
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Student"];
NSPredicate *predicate = [NSPredicate predicateWithFormate:@"age=22"]; // 查询条件
request.predicate = predicate;
NSError *error = nil;
NSArray<Student *> *students = [context executeFetchRequest:request error:&error];

Keychain

提供了一种用于安全存储敏感信息方式。其特点如下:

  • 保存到 keychain 中的信息不会因为卸载或重装 App 而丢失。
  • keychain 是用 SQLite 进行存储的,苹果会对其进行加密。
  • 适合存储一些比较小的数据。
  • 可以通过 Group 的方式,在多个 App间共享。
API
  • SecItemAdd:添加一个item
  • SecItemUpdate:更新已存在的item
  • SecItemCopyMatching:搜索一个已存在的item
  • SecItemDelete:删除一个keychain item
三方库

系统提供的 API 不是 OC 风格,使用起来不是很友好,这里推荐两个三方库供大家选择:

  • KeychainWrapper
  • SAMKeychain

关注公众号 iOS学习社区 get更多技术好文

# iOS GCD 之 dispatch_group_enter 和 dispatch_group_leave

2022年2月14日 20:22

原文地址

在实际开发中,经常需要在几个任务全部执行完成之后,在执行后续操作,在 iOS 中,我们可以通过 NSOperation 等达到这一目的。在本篇文章中,我们会介绍如何通过 dispatch_group_enterdispatch_group_leave 来实现这一功能,以及使用过程中遇到的坑。

如何使用

通过一个例子来看下如何使用 dispatch_group_enterdispatch_group_leave

{
    // 首先 需要创建一个线程组
    dispatch_group_t group = dispatch_group_create();
    // 任务1
    dispatch_group_enter(group);
    NSURLSessionDataTask *task1 = [[NSURLSession sharedSession] dataTaskWithURL:[NSURL URLWithString:@""] completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        NSLog(@"任务1完成");
        dispatch_group_leave(group);
    }];
    
    [task1 resume];
    
     // 任务2
    dispatch_group_enter(group);
    NSURLSessionDataTask *task2 = [[NSURLSession sharedSession] dataTaskWithURL:[NSURL URLWithString:@""] completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        NSLog(@"任务2完成");
        dispatch_group_leave(group);
    }];
    [task2 resume];
    
    // 全部完成
    dispatch_group_notify(group, dispatch_get_main_queue(), ^(){
        NSLog(@"全部完成");
    });
}

任务1和任务2执行完成之后,才会执行全部完成中的任务。

**注意:**在使用时,dispatch_group_enter 和 dispatch_group_leave 需要成对出现,如果 dispatch_group_leave 的调用次数多于 dispatch_group_enter 的调用次数,程序会 crash。相反,虽然不会发生 crash , 但可能不会达到预期效果。

crash 场景分析

使用场景是,需要异步获取多个图片封面,所有都获取完成后,在执行指定任务,代码示例如下:

- (void)fetchCovers {
    dispatch_queue_t queue = dispatch_queue_create("com.demo.xxx", DISPATCH_QUEUE_CONCURRENT);

    dispatch_group_t group = dispatch_group_create();
    for (int i = 0; i < 40; ++i) {
        dispatch_group_enter(group);
        dispatch_async(queue, ^{
            [self fetchCoverByPHAsset:asset targetSize:CGSizeMake(200, 200) resultHandler:^(UIImage * _Nonnull, NSDictionary * _Nonnull, BOOL) {
            dispatch_group_leave(group);
        }];
        });
    }
    
    dispatch_group_notify(group, dispatch_get_main_queue(), ^(){
        // 全部完成后 执行指定任务
    });
}

- (void)fetchCover:(PHAsset *)asset targetSize:(CGSize)targeSize resultHandler:(void (^)(UIImage * _Nonnull, NSDictionary * _Nonnull, BOOL))resultHandler {
    PHImageRequestOptions *options = [[PHImageRequestOptions alloc] init];
    if (@available(iOS 14.0, *)) {
        options.version = PHImageRequestOptionsVersionCurrent;
    }
    options.networkAccessAllowed = YES;
    [[PHImageManager defaultManager] requestImageForAsset:asset
                                               targetSize:targeSize
                                              contentMode:PHImageContentModeAspectFill
                                                  options:options
                                            resultHandler:^(UIImage * _Nullable result, NSDictionary * _Nullable info) {
        if (resultHandler) {
            resultHandler(result, info, [[info objectForKey:PHImageResultIsDegradedKey] boolValue]);
        }
    }];
}

这里有个小 tips : 在使用 requestImageForAsset 获取图片时,如果 options 的 deliveryMode 属性使用默认值,在异步获取图片时,其回调可能会走2次。解决方案是将其显示设置为 PHImageRequestOptionsDeliveryModeHighQualityFormat 或 PHImageRequestOptionsDeliveryModeFastFormat。

    options.deliveryMode = PHImageRequestOptionsDeliveryModeFastFormat; // 或 PHImageRequestOptionsDeliveryModeHighQualityFormat

因为获取图片封面的回调可能会走 2 次,从而导致 dispatch_group_leave 调用次数多于 dispatch_group_enter 的调用次数,因此可能会发生 crash。

源码实现

  • dispatch_group_enter
void 
dispatch_group_enter(dispatch_group_t dg)
{
    uint32_t old_bits = os_atomic_sub_orig2o(dg, dg_bits,
    DISPATCH_GROUP_VALUE_INTERVAL, acquire);
    uint32_t old_value = old_bits & DISPATCH_GROUP_VALUE_MASK;
    if (unlikely(old_value == 0)) {
    _dispatch_retain(dg); // <rdar://problem/22318411>
    }
    if (unlikely(old_value == DISPATCH_GROUP_VALUE_MAX)) {
    DISPATCH_CLIENT_CRASH(old_bits,
    "Too many nested calls to dispatch_group_enter()");
    }
}
  • dispatch_group_leave
void 
dispatch_group_leave(dispatch_group_t dg)
{   
    uint64_t new_state, old_state = os_atomic_add_orig2o(dg, dg_state,
    DISPATCH_GROUP_VALUE_INTERVAL, release);
    uint32_t old_value = (uint32_t)(old_state & DISPATCH_GROUP_VALUE_MASK);
    
    if (unlikely(old_value == DISPATCH_GROUP_VALUE_1)) {
    old_state += DISPATCH_GROUP_VALUE_INTERVAL;
    do {
    new_state = old_state;
    if ((old_state & DISPATCH_GROUP_VALUE_MASK) == 0) {
    new_state &= ~DISPATCH_GROUP_HAS_WAITERS;
    new_state &= ~DISPATCH_GROUP_HAS_NOTIFS;
    } else {
    new_state &= ~DISPATCH_GROUP_HAS_NOTIFS;
    }
    if (old_state == new_state) break;
    } while (unlikely(!os_atomic_cmpxchgv2o(dg, dg_state,
    old_state, new_state, &old_state, relaxed)));
    return _dispatch_group_wake(dg, old_state, true);
    }
    
    if (unlikely(old_value == 0)) {
    DISPATCH_CLIENT_CRASH((uintptr_t)old_value,
    "Unbalanced call to dispatch_group_leave()");
    }
}

Swift 模式匹配

2022年1月26日 13:22

原文地址

模式匹配是 switch 的主要功能,模式匹配是指对相应 case 匹配到的值进行解构的能力。解构是指将特定结构的内容再次分解为更小的条目,先看一个例子:

let harry = ("Harry", "Potter", 21, "Wizard")
let (_, surname, _, _) = harry
print(surname)

// Potter

模式类型

Swift 中提供了 8 种模式,分别是:

  • 通配符模式
  • 标识符模式
  • 值绑定模式
  • 元组模式
  • 枚举用例模式
  • 可选模式
  • 类型转换模式
  • 表达式模式

这些模式不仅能用那个在 switch 语句中,还可以用在 if,guard 和 for 语句中。

通配符模式

通配符模式是指忽略匹配到的值,通过 _ 来实现,看下面的例子:

switch (15, "example", 3.14) {
case (_, _, let pi): print ("pi: \(pi)")
}
        
// pi: 3.14

标识符模式

匹配一个具体值,这和 Objective-C 的 switch 实现是一样的:

let language = "Japanese"

switch language {
case "Japanese": print("おはようございます")
case "English": print("Hello!")
case "German": print("Guten Tag")
default: print("Other")
}

// おはようございます

值绑定模式

值绑定模式是把匹配到的值绑定给一个变量(let)或常量(var):

let point = (3, 2)
switch point {
// 将 point 中的元素绑定到 x 和 y
case let (x, y):
    print("The point is at (\(x), \(y)).")
}

// “The point is at (3, 2).”

元组模式

元组模式是用括号括起来,以逗号分隔的零个或多个模式列表。

let age = 23
let job: String? = "Operator"
let payload: Any = NSDictionary()
switch (age, job, payload) {
case (let age, _, _ as NSDictionary):
    print(age)
default: ()
}

// 23

枚举用例模式

枚举用例模式匹配现有的某个枚举类型的某个成员值。枚举用例模式出现在 switch 语句中的 case 标签中,以及 if、while、guard 和 for-in 语句的 case 条件中。

let e = Entities.soldier(x: 4, y: 5)
switch e {
case let .soldier(x, y):
    print("x:\(x), y:\(y)")
case let .tank(x, y):
    print("x:\(x), y:\(y)")
case let .player(x, y):
    print("x:\(x), y:\(y)")
}

// x:4, y:5

可选模式

可选模式由一个标识符后紧随一个 ? 组成,可以像枚举用例模式一样使用它。

let someOptional: Int? = 42
// 使用可选模式匹配
if case let x? = someOptional {
    print(x)
}

// 42

case let x? 中的 ? 号表示,如果可选类型有值则匹配,否则不匹配。

类型转换模式

类型转换模式转换或匹配类型,它有 2 种类型:

  • is 模式:仅当一个值的类型在运行时和 is 右边的指定类型一致,或者是其子类的情况下,才会匹配。它只做匹配,但不关注返回值。
  • as 模式:和 is 模式的匹配规则一致,如果成功的话会将类型转换到左侧指定的模式中。
let a: Any = 5 

switch a {
// this fails because a is still Any
// error: binary operator '+' cannot be applied to operands of type 'Any' and 'Int'
case is Int: print (a + 1)
// This works and returns '6'
case let n as Int: print (n + 1)
default: ()
}

表达式模式

表达式模式只出现在 switch 语句中的 case 标签中。它的功能非常强大,它可以把 switch 值和实现 ~= 操作符的表达式进行匹配。

  • 范围匹配:
switch 5 {
case 0...10: print("In range 0-10")
default: print("default")
}

// In range 0-10
  • 实现 ~= 操作符,匹配所有血量为 0 的实体:
struct Soldier {
    let hp: Int
    let x: Int
    let y: Int

    static func ~= (pattern: Int, value: Soldier) -> Bool {
        return pattern == value.hp
    }
}

let soldier = Soldier(hp: 0, x: 10, y: 10)
switch soldier {
case 0: print("dead soldier")
default: ()
}

// dead soldier

模式匹配在其它语句中的使用

if case let

case let x = y 模式用来检查 y 是否可以和模式 x 匹配。而 if case let x = y { … } 严格等同于 switch y { case let x: … },当只想与一条 case 匹配时,这种更紧凑的语法更有用。有多个 case 时更适合使用 switch。

enum Media {
  case book(title: String, author: String, year: Int)
  case movie(title: String, director: String, year: Int)
  case website(urlString: String)
}

let m = Media.movie(title: "Captain America: Civil War", director: "Russo Brothers", year: 2016)

if case let Media.movie(title, _, _) = m {
    print("This is a movie named \(title)")
}

// This is a movie named Captain America: Civil War

// 还可以改为 switch 后更冗长的代码
switch m {
case let Media.movie(title, _, _):
    print("This is a movie named \(title)")
default: () // do nothing, but this is mandatory as all switch in Swift must be exhaustive
}

if case let where

我们也可以将 if case let 和 where 语句一起使用,创建多个从属条件,现在的 Swift 版本中,用逗号代替 where,例子如下:

let m = Media.movie(title: "Captain America: Civil War", director: "Russo Brothers", year: 2016)
if case let Media.movie(_, _, year) = m, year < 1888 {
    print("Something seems wrong: the movie's year is before the first movie ever made.")
}

guard case let

guard case let 和 if case let 相似。你可以使用 guard case let 和 guard case let … ,确保内容与模式和条件匹配,否则退出,还以上面的例子为例:

let m = Media.movie(title: "Captain America: Civil War", director: "Russo Brothers", year: 2016)
guard case let Media.movie(_, _, year) = m, year < 1888 else {
    print("It is ok!")
    return
}

// It is ok!

for case let

for case let 让你有条件的遍历一个集合对象。例子如下:

let mediaList: [Media] = [
          .book(title: "Harry Potter and the Philosopher's Stone", author: "J.K. Rowling", year: 1997),
          .movie(title: "Harry Potter and the Philosopher's Stone", director: "Chris Columbus", year: 2001),
          .book(title: "Harry Potter and the Chamber of Secrets", author: "J.K. Rowling", year: 1999),
          .movie(title: "Harry Potter and the Chamber of Secrets", director: "Chris Columbus", year: 2002),
          .book(title: "Harry Potter and the Prisoner of Azkaban", author: "J.K. Rowling", year: 1999),
          .movie(title: "Harry Potter and the Prisoner of Azkaban", director: "Alfonso Cuarón", year: 2004),
          .movie(title: "J.K. Rowling: A Year in the Life", director: "James Runcie", year: 2007),
          .website(urlString: "https://en.wikipedia.org/wiki/List_of_Harry_Potter-related_topics")
        ]
        print("Movies only:")
        for case let Media.movie(title, _, year) in mediaList {
          print(" - \(title) (\(year))")
        }
        
/*  
Movies only:
 - Harry Potter and the Philosopher's Stone (2001)
 - Harry Potter and the Chamber of Secrets (2002)
 - Harry Potter and the Prisoner of Azkaban (2004)
 - J.K. Rowling: A Year in the Life (2007)
 */

for case let where

使用 for case let where 为 for case let 创建从属条件,例子如下:

print("Movies by C. Columbus only:")
for case let Media.movie(title, director, year) in mediaList where director == "Chris Columbus" {
    print(" - \(title) (\(year))")
}

/*
Movies by C. Columbus only:
 - Harry Potter and the Philosopher's Stone (2001)
 - Harry Potter and the Chamber of Secrets (2002)
*/

⚠️注意:使用 for … where 而不带 case 模式匹配依然是符合 Swift 语法规则的,这样写也是 OK 的:

for m in listOfMovies where m.year > 2000 { … }

参考:

  1. appventure.me/guides/patt…
  2. docs.swift.org/swift-book/…
  3. alisoftware.github.io/swift/patte…
❌
❌