Swift 如何闪电般异步读取大文件?

news2024/11/27 6:24:34

在这里插入图片描述

功能需求

Apple 系统中(iOS、MacOS、WatchOS等等)读取文件是一个平常的不能再平常的需求,不过当文件很大时,同步读取文件会导致 UI 的挂起,这是不能让用户接受的。

所以,要想读取文件内容的同时保持界面操作丝般顺滑,只有使用异步文件读取技术来拯救我们了!

在本篇博文中,我们将会用 4 种方法来实现文件内容的异步读取。

在读取大小为 1.58GB 的文件时,最快方法比最慢方法快了将近 50 倍,仅需 0.13 秒,而这仅是在 Xcode(SwiftUI) 预览中的结果,真机还会更快!

So,废话少叙!

Let‘s work it out!!!😉


功能分析

1. 准备测试代码

我们准备一个大小为 1.58GB 的 big.zip 文件,将它放入 Xcode 项目的资源目录中,之后所有的测试都会读取该文件。

下面是测试代码,在每个后续测试里我们大部分的修改都在 ContentView 的 read() 方法中:

// AnimView 用来观察界面的挂起
struct AnimView: View {
    var body: some View {
        TimelineView(.animation) { context in
            let value = secondsValue(for: context.date)

            ZStack {
                
                Circle()
                    .stroke(Color.gray.opacity(0.66), lineWidth: 12.0)
                
                Circle()
                    .trim(from: 0, to: value)
                    .stroke(style: StrokeStyle(lineWidth: 15.0, lineCap: .round, lineJoin: .round, miterLimit: 0.0, dash: [], dashPhase: 0.0))
                
                Text("\(Calendar.current.component(.second, from: context.date))")
                    .font(.largeTitle.weight(.black))
            }
        }
    }
    
    private func secondsValue(for date: Date) -> Double {
        let seconds = Calendar.current.component(.second, from: date)
        return Double(seconds) / 60
    }
}

struct ContentView: View {
    
    @State var isFileLoading = false
    @State var fileSize = 0
    @State var readingBeginTime = Date()
    @State var elapsedSeconds: TimeInterval = 0.0
    
    var sizeString: String {
        ByteCountFormatter.string(fromByteCount: Int64(fileSize), countStyle: .file)
    }
    
    let fileUrl = Bundle.main.url(forResource: "big", withExtension: "zip")!
    
    private func reset() {
        isFileLoading = true
        fileSize = 0
        elapsedSeconds = 0.0
        readingBeginTime = Date()
    }
    
    private func read() {
        reset()
        // 我们实际文件读取代码将会在此...
    }
    
    var body: some View {
        NavigationView {
            VStack {
                AnimView()
                    .frame(width: 200, height: 200)
                    .foregroundStyle(Color.red.gradient)
                    .padding()
                
                Text("\(String(format: "耗时 %0.2f 秒", elapsedSeconds))")
                    .font(.title3.weight(.black))
                    .foregroundStyle(Color.blue.gradient)
                
                List(0..<200){ i in
                    Text("Item \(i)")
                        .font(.title3)
                }
                .listStyle(.plain)
                .safeAreaInset(edge: .bottom){
                    Button(action: {
                        read()
                    }){
                        Text("读取文件")
                            .frame(maxWidth: .infinity)
                    }
                    .buttonStyle(.borderedProminent)
                    .controlSize(.large)
                    .tint(.green)
                    .shadow(radius: 5.0)
                    .padding(.horizontal)
                }
            }
            .navigationTitle("文件读取DEMO")
            .toolbar {
                ToolbarItem(placement: .navigationBarLeading){
                    Text("大熊猫侯佩 @ csdn")
                        .foregroundColor(.gray)
                }
                
                ToolbarItem(placement: .primaryAction){
                    if isFileLoading {
                        HStack {
                            Text("文件读取中")
                            // 在文件读取时显示进度小圆环
                            ProgressView()
                        }
                    }else{
                        Text("文件大小 \(sizeString)")
                    }
                }
            }
        }
    }
}

2. 同步文件读取的弊端

同步文件读取很简单,在文件比较小时很给力,但如果文件很大则必然挂起 UI:

// 同步读取,可能会挂起界面操作
private func read() {
    reset()
    
    if let data = try? Data(contentsOf: fileUrl) {
        elapsedSeconds = Date.now.timeIntervalSince(readingBeginTime)
        fileSize = data.count
    }
    
    isFileLoading = false
}

在这里插入图片描述

可以看到在同步读取 big.zip 文件内容时,挂起界面大约 2 秒多,而且我们准备的等待进度小圆环(右上角的 ProgressView)根本没机会显示。

3. 简单的异步读取

一种非常简单的异步读取方法是:直接将整个读取代码放到后台线程中,在读取完成后再切换回主线程。

private func read() {
    reset()

    func in_main(_ blk: @escaping () -> ()) {
        DispatchQueue.main.async {
            blk()
        }
    }
    
    DispatchQueue.global(qos: .background).async {
        
        defer {
            in_main {
                isFileLoading = false
            }
        }
        
        if let data = try? Data(contentsOf: fileUrl) {
            in_main {
                elapsedSeconds = Date.now.timeIntervalSince(readingBeginTime)
                fileSize = data.count
            }
        } 
    }
}

在这里插入图片描述

可以看到异步读取文件时,界面中的动画、进度环显示和用户操作都不会受到影响。

4. 基于 async / await 并发模型异步读取

从 Swift 5.5 开始,Apple 为我们带来了新的 async / await 并发模型,我们可以使用它来时实现文件异步读取:

func readData(url: URL) async throws -> Data {
    try await withCheckedThrowingContinuation { c in
        DispatchQueue.global(qos: .background).async {
            do {
                let data = try Data(contentsOf: url)
                c.resume(returning: data)
            }catch{
                c.resume(throwing: error)
            }
        }
    }
}

private func read() {
    reset()
    
    Task {
        defer {
            isFileLoading = false
        }
        
        do {
            let data = try await readData(url: fileUrl)
            elapsedSeconds = Date.now.timeIntervalSince(readingBeginTime)
            fileSize = data.count
        }catch{
            print("ERR: \(error.localizedDescription)")
        }
    }
}

在这里插入图片描述

注意在上面代码中,由于任务(Task)的层级体系(Hierarchy)特性,在 read() 方法的 Task 闭包默认会在主线程中执行,所以无需切换。

5. 基于异步序列(AsyncSequence)的读取

除了直接通过的 async / await 并发模型,我们还可以基于其中的异步序列(AsyncSequence)来实现文件的异步读取:

func createStream(url: URL, bufSize: Int = 4096 * 1024) throws -> AsyncThrowingStream<Data,Error> {
    let handle = try FileHandle(forReadingFrom: url)
    return AsyncThrowingStream {
        if let data = try handle.read(upToCount: bufSize), !data.isEmpty {
            return data
        }else{
            try handle.close()
            return nil
        }
    }
}

func readDataFromStream(url: URL, bufSize: Int = 4096 * 1024) async throws -> Data {
    var tmp = Data()
    let stream = try createStream(url: url, bufSize: bufSize)
    
    for try await data in stream {
        tmp.append(data)
    }
    
    return tmp
}

private func read() {
    reset()
    
    Task {
        defer {
            isFileLoading = false
        }
        
        do {
            let data = try await readDataFromStream(url: fileUrl)
            elapsedSeconds = Date.now.timeIntervalSince(readingBeginTime)
            fileSize = data.count
        }catch{
            print("ERR: \(error.localizedDescription)")
        }
    }
}

注意,在上面代码中我们使用了 FileHandle 的文件分块读取机制(可以调整分块的大小:bufSize)来产生异步序列的内容,所以会比文件整个内容的一次性读取耗时久一些。

不过,如果我们的需求是按条件读取文件头部的一小块内容,这种方式无疑是效率最好的(需要修改部分代码)。

在这里插入图片描述

6. 基于 UIDocument 的异步读取

其实 Apple 早就为我们考虑到了文件异步读取的场景,我们可以基于 UIDocument 抽象文档类,来定制自己的文档模型。

首先,需要继承 UIDocument 类(需要导入 UIKit),并实现其中 load 和 contents 两个方法:

import UIKit

class UniversalFile: UIDocument {
    var fileData: Data?			// 文件内容保存在此
    
    override func load(fromContents contents: Any, ofType typeName: String?) throws {
        fileData = contents as? Data
    }
    
    override func contents(forType typeName: String) throws -> Any {
        if let data = fileData {
            return data
        }
        return Data()
    }
}

其中,load() 在文件被读取时调用,contents() 方法则在文件被保存时调用,如果仅仅是读取文件(比如本例中)可以不实现 contents 方法。

接着,我们在 ContentView 的 load() 方法中,使用 UIDocument#open() 方法来处理文件的异步打开。若成功,系统会调用 UniversalFile#load() 方法来保存数据(到 UniversalFile 的 fileData 属性中)以便我后续的读取:

private func read() {
    reset()
    
    let file = UniversalFile(fileURL: fileUrl)
    Task {
    	// 如果文件可以被打开且其数据不为空则可以继续操作
        if await file.open(), let data = await file.fileData {
            elapsedSeconds = Date.now.timeIntervalSince(readingBeginTime)
            fileSize = data.count
        }
        
        isFileLoading = false
    }
}

UIDocument 类除了简单的文件异步打开读取功能外,还提供了很多高级功能,有机会我们可以单开一篇来介绍。

在这里插入图片描述

可以看到用 UIDocument 类读取文件非常快,还没开始就已经结束了,上面打开 1.58GB 大小的文件内容仅需 0.13 秒,可谓十分惊人。

至此,我们实现了所有 4 种文件异步读取方式,任君选择。💯🚀

总结

在本篇博文中,我们讨论了同步读取大文件的弊端,并逐一实现了 4 种异步读取文件的方法,其中最快方法在打开 1.58 GB 的文件仅用时 0.13 秒,希望这些方法抛砖引玉可以启发到大家。

那么,最后还得问一下小伙伴:你们学会了么? 😎

结束语

Hi,我是大熊猫侯佩,一名非自由App开发者,希望我的文章可以解决你的痛点、难点问题。

如果还有问题欢迎在下面一起讨论吧 😉

感谢观赏,再会。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/551364.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

KMP算法及其改进图文详解

文章目录 KMP算法详解什么是KMP算法KMP算法的应用场景KMP算法和暴力求解的比较字符串的前缀、后缀和最长相等前后缀KMP算法实现字符串匹配的具体过程&#xff08;图解&#xff09;从串与主串的下标变化j回退的位置(从串的下标变化)主串的下标变化 Next数组如何运用代码逻辑计算…

[CTF/网络安全] 攻防世界 xff_referer 解题详析

[CTF/网络安全] 攻防世界 xff_referer 解题详析 XFF及refererXFF格式referer格式姿势总结 题目描述&#xff1a;X老师告诉小宁其实xff和referer是可以伪造的。 XFF及referer X-Forwarded-For&#xff08;简称 XFF&#xff09;是一个 HTTP 请求头部字段&#xff0c;它用于表示 …

深入理解计算机系统第七章知识点总结

文章目录 详解ELF文件-> main.o前十六个字节的含义推测elf的大小查看节头部表推断每个section在elf中的具体位置查看.text的内容查看.data的内容关于.bss查看.rodata的内容关于其他的节表示的信息 详解符号表符号编译器如何解析多重定义的全局符号静态库与静态链接构造和使用…

seata的部署和集成

seata的部署和集成 一、部署Seata的tc-server 1.下载 首先我们要下载seata-server包&#xff0c;地址在http://seata.io/zh-cn/blog/download.html 2.解压 在非中文目录解压缩这个zip包&#xff0c;其目录结构如下&#xff1a; 3.修改配置 修改conf目录下的registry.conf文…

开源大模型资料总结

基本只关注开源大模型资料&#xff0c;非开源就不关注了&#xff0c;意义也不大。 基座大模型&#xff1a; LLaMA&#xff1a;7/13/33/65B&#xff0c;1.4T token LLaMA及其子孙模型概述 - 知乎 GLM&#xff1a;6/130B&#xff0c; ChatGLM基座&#xff1a;GLM&#xff08…

【网络】- TCP/IP四层(五层)协议 - 网际层(网络层) - 网际协议IP

目录 一、概述 二、初步了解网际协议 IP  &#x1f449;2.1 与数据链路层的区别  &#x1f449;2.2 网际协议 IP 概览  &#x1f449;2.3 分层的意义 三、IP协议基础知识  &#x1f449;3.1 IP地址属于网络层地址  &#x1f449;3.2 路由控制  &#x1f449;3.3 IP分包与…

solr快速上手:核心概念及solr-admin界面介绍(二)

0. 引言 上一节&#xff0c;我们简单介绍了solr并演示了单节点solr的安装流程&#xff0c;本章&#xff0c;我们继续讲解solr的核心概念 solr快速上手&#xff1a;solr简介及安装&#xff08;一&#xff09; 1. 核心概念 核心&#xff08;索引/表&#xff09; 在es中有索引…

【软件测试】5年测试老鸟总结,自动化测试成功实施,你应该知道的...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 自动化测试 Pytho…

基于html+css的图展示82

准备项目 项目开发工具 Visual Studio Code 1.44.2 版本: 1.44.2 提交: ff915844119ce9485abfe8aa9076ec76b5300ddd 日期: 2020-04-16T16:36:23.138Z Electron: 7.1.11 Chrome: 78.0.3904.130 Node.js: 12.8.1 V8: 7.8.279.23-electron.0 OS: Windows_NT x64 10.0.19044 项目…

chatgpt赋能Python-pythonage

Pythonage - 一款优秀的Python SEO工具 无论是个人博客还是商业网站&#xff0c;SEO&#xff08;搜索引擎优化&#xff09;都是最重要的。Pythonage是一款优秀的Python SEO工具&#xff0c;可以帮助你优化你的网站并提高搜索引擎排名。在这篇文章中&#xff0c;我们将详细介绍…

ChatGPT 使用 拓展资料:开始构建你的优质Prompt

ChatGPT 使用 拓展资料:开始构建你的优质Prompt

【JavaEE】阻塞队列、定时器和线程池

目录 1、阻塞队列 1.1、概念 1.2、生产者消费者模型 1.3、阻塞队列的模拟实现 2、定时器 2.1、使用标准库中的定时器 2.2、模拟实现定时器 3、线程池 3.1、标准库中的线程池 3.1.1、ThreadPoolExecutor类的构造方法 3.1.2、Java标准库的4种拒绝策略【经典面试题】…

Canal内存队列的设计

1、背景 笔者的公司内部使用了开源的Canal数据库中间件来接受binlog数据&#xff0c;并基于此进行数据的订阅和同步到各种同构和异构的数据源上&#xff0c;本文将对Canal内部使用的store模块进行分析。 2、Store模块概览 Canal的store模块用于存储binlog中的每一个event&am…

MySQL- 多表查询(上)

♥️作者&#xff1a;小刘在C站 ♥️个人主页&#xff1a;小刘主页 ♥️每天分享云计算网络运维课堂笔记&#xff0c;努力不一定有收获&#xff0c;但一定会有收获加油&#xff01;一起努力&#xff0c;共赴美好人生&#xff01; ♥️树高千尺&#xff0c;落叶归根人生不易&…

安卓基础巩固(一):布局、组件、动画、Activity、Fragment

文章目录 布局LinearLayoutRelativeLayoutTableLayoutFrameLayoutConstraintLayoutListView基于ArrayAdapter自定义Adaper提升ListView的运行效率 RecyclerView基本属性使用案例布局&#xff08;显示方式&#xff09;监听事件利用View.onClickListener 和 onLongClickListener …

日志收集机制和日志处理流程规范

本博客地址&#xff1a;https://security.blog.csdn.net/article/details/130792958 一、日志收集与处理流程 云原生平台中对日志提取收集以及分析处理的流程与传统日志处理模式大致是一样的&#xff0c;包括收集、ETL、索引、存储、检索、关联、可视化、分析、报告这9个步骤…

Leetcode 二叉树详解

二叉树 树的概念及基本术语见树与二叉树的基础知识 定义&#xff1a;一棵二叉树是结点的一个有限集合&#xff0c;该集合或者为空&#xff0c;或者是由一个根结点加上两棵分别称为左子树和右子树的、互不相交的二叉树组成。 特点&#xff1a;每个结点至多只有两棵子树&#xff…

Vivado综合属性系列之八 DIRECT_ENABLE DIRECT_RESET

目录 一、前言 二、DIRECT_ENABLE、DIRECT_RESET ​ ​2.1 属性说明 ​ ​2.2 工程代码 ​ ​2.3 综合结果 一、前言 在Vivado 2019之前的版本中&#xff0c;对于设计中触发器的使能端口和复位端口是会自动接地&#xff0c;如果需要接设计端口&#xff0c;如果要直连…

GitHub Copilot开发者酷游网址训练营

目标读者 已使用且【酷游网K͜W͜98典neт娜娜宝宝提供】想发挥GitHub Copilot所有潜能的使用者想知道GitHub Copilot未来展望的使用者想了解GitHub Copilot能力的开发者 简介 最近Open AI带起的新世代&#xff0c;热潮汹涌&#xff0c;一堆AI工具蜂拥而至(如:chatGPT和Midjo…

近期关于Transformer结构有潜力的改进方法总结

目录 0 引言1 Gated Linear Unit (GLU)1.1 思路 2 Gated Attention Unit (GAU)2.1 思路2.2 实验结论2.3 混合注意力 3 FlashAttention3.1 标准Attention的实现3.2 FlashAttention的实现针对目标1针对目标2 4 总结5 参考资料 0 引言 标准Transformer在最新的实际大模型中并没有…