基础 在开始建议之前,最好先定义并阐明本文之外的基本概念,这是主要主题 。 您可能已经知道,主线程不应用于繁重的操作,而应主要用于: 接受用户输入/交互; 显示结果并更新UI。 当主线程必须处理太多操作时,最常见的后果是丢帧现象,这是很普遍的现象,当我们不能保证60 fps(或每16.67毫秒一次)时,就会发生这种情况。 如何调试和识别精确丢失的帧? 有时,很容易发现它们,因为最关键的问题是不响应 ,而其他时候则不是,我们需要更准确的信息来跟踪它们。 例如,使用CADisplayLink (以非常快速的方式直接通过代码跟踪它们)或使用TimeProfiler以更准确的方式进行跟踪 。 要使用CADisplayLink,您可以简单地使用此类: 现在您知道您的帧下降了,该怎么办? 您可以采取一些措施,在本文中,我建议一些措施: 减少视图和透明视图的数量 最小化在连续调用的函数中完成的工作量 解码JPEG图像 屏幕外渲染 让我们一一讨论。 1.减少视图和透明视图的数量 为了提高应用程序的性能,要做的第一件事是(尽可能): 减少视图数量; 避免透明。 解决第二点很简单: label.layer.opacity = 1.0 label.backgroundColor = .white 为了轻松发现透明胶片的这种重叠,我们可以依靠一个非常方便的工具:调试->视图调试->渲染-> 颜色混合层 。 该工具使我们可以轻松地发现视图重叠,如以下示例所示: 2.最小化连续调用函数中完成的工作量 似乎很明显,但是像cellForItemAt indexPath或scrollViewDidScroll之类的函数必须非常快,因为它们是连续调用的。 始终确保使用最轻巧的配置方法来拥有“最哑”的视图/单元。 (例如,不涉及布局约束,对象分配等) 3.解码JPEG图像 当我们处理掉帧问题时,“通常的嫌疑人”之一就是图像解码。 通常,此操作是在主线程下通过imageViews完成的。 但这有时会导致我们的应用程序持续减速,尤其是在图像很大时。 为了减轻该问题,一种解决方案是将解码工作移至后台队列 。 这样,操作将不会像UIImageView所采用的常规解码那样高效,但是mainThread将是免费的。 下面让我们看一下卢克·帕罕(Luke Parham)的“ Catstagram”项目中的一些摘录: 在后台解码图像 : 您可以添加一些进一步的缓存控制以提高效率。 […]
,로,글입니다。 。다있습니。 GCD的并发性和线程化。 스레드(thread)프로그램내에서내에서세다말한다。다。 만로한그램은스그램은가지있지만,만로그램이둘스상의있다。 이멀티멀티 스멀티 (多线程)한다。 主线程(UI)和后台线程,以及Apple的Grand Central Dispatch(GCD)和 NSOperatin Queue的产品。 이는thread를관리해야하는해준다。 已在GCD上添加了任务,然后在GCD上添加了GCD。 并发性。 。다수의이실행되는다。 프로세스나리어플이션은상의상의이그스상의가진다。 OS调度程序,然后单击“计划”。 使用时间片 ,使用并行处理。 능가능 race이race(比赛条件)방지 느림(모든이그이작업이끝나길기렸다렸기되) 并发队列。 。이순서대로실행됨을보장하지만이끝나는다없다。 불가불 빠름 들어,용자의사를를다고때다고할지할지않다않다。 。다。 并发队列。 할때는지를로순서가중요하다고串行队列를를다 GCD세가지메인공한공한공한공한공한.。 主队列 : 主线程 동작하며 串行队列 이다。 让mainQueue = DispatchQueue.main 2.全局队列 : 并行队列 이다。 高,默认,低,背景。 QOS(Quality of Service)类의로퍼티를지정정다。 GCD相片和影片。 let backgroundQueue = DispatchQueue.global(qos:DispatchQoS.QoSClass.background) […]
线程基础 当您的应用程序首次启动时,iOS将创建一个新进程,并为此应用程序进程分配内存。 简而言之,一个应用程序进程的内存由三个块组成。 程序存储器存储目标C或快速编译的代码的机器指令。 堆存储使用swift的init()方法或Objective-c的[…alloc] init]创建的对象。 堆栈是用于方法调用的内存区域。 方法将其参数和局部变量存储在堆栈中。 默认情况下,一个应用程序进程由一个线程(主线程)组成。 如果您的iOS应用使用多个线程,则所有线程共享程序内存和堆,但是每个线程都有自己的线程 指令指针(IP) 和 堆栈 。 这意味着每个线程都有自己的程序流,并且如果在一个线程上调用方法,则其他任何线程都无法看到参数和局部变量。 但是在堆上创建的对象可以被所有线程看到,访问和操纵。 例 现在,让我们创建一个简单的程序。 打开Xcode并创建一个新项目。 创建一个名为“ Foo ”的类,如下所示: 如我们所见,并非所有线程都按预期输出值9(如果您的执行产生正确的结果-所有线程上的值为 9,请重新执行它,直到产生错误的结果为止。肯定会。) 为什么所有线程都无法产生正确的结果? 好吧,因为我们的程序不是线程安全的。 正如我们在上面看到的, doIt方法不是线程安全的,因为它在多线程环境中不会产生与在单线程环境中相同的结果。 但是,这种行为可能是什么原因呢? 如前所述,每个线程都有自己的指令指针( IP ) 和 堆栈 但是所有线程共享 的 堆 自Foo课以来 实例在堆上分配,并且在所有线程之间共享,这就是为什么所有线程都会干扰doIt方法的原因 执行。 让我们仔细看看这种干扰。 我们考虑在两个线程Thread-1和Thread-2上执行doIt方法:
最初发布于 swiftrocks.com 。 这些“ DispatchQueues”到底是什么? 为什么必须使用它将UI代码发送到主线程? 如果我什么也不做,它显然仍然有效。 这些“服务质量”队列有什么意义? 我将.main用于所有内容,并且从未遇到问题。 如果调用DispatchQueue.main.sync,为什么会崩溃? 有什么意义呢? 到底这是什么主线程? 如果您要开发iOS应用程序超过几个星期,那么您之前可能已经处理过并发代码。 如果您没有操作系统的知识,那么您可能已经问过自己其中一个问题。 一般而言,多线程是很难完全理解的事情,但是了解CPU如何处理并发性是编写可以完成您期望的工作的优质快速代码的关键。 否则,您可能正在滥用用户的CPU,但认为一切都很好,因为它们太快了,您无法注意到问题所在。 在我们能够回答这些问题之前,我们需要退后一步,了解事情在幕后的运作方式。 流程的定义非常简单:它是一个正在运行的程序。 您的应用是一个过程,Slack是一个过程,Safari是一个过程,依此类推。 它包含指令列表(您的汇编格式代码),并位于磁盘上,直到用户希望运行它为止。 然后,操作系统将把该进程加载到内存中,启动一个指令指针 ,告诉我们当前正在执行该程序的哪条指令,然后让CPU顺序执行其指令,直到结束为止,从而终止该进程。 单线程进程的地址空间 |————————-|- | 说明| |——————————– | 全球数据 |——————————– | 分配的数据(引用类型)| |——————————– | 什么都没有(堆栈和malloc的数据向此处增长)| |——————————– | 堆栈(值类型(如果可能),参数,返回)| |——————————– 每个进程都有自己专用的物理内存部分。 他们不与其他进程共享这些地址。 不行 您正在经历的是一种幻想,它是由CPU所具有的荒谬速度造成的。 一个CPU不能同时做两件事。 对于具有多个内核的CPU,情况略有不同,但是为简单起见,我们假设只有一个CPU:发生的事情是它在Safari中执行某项,然后在Spotify中执行某项,然后在iOS中执行某项,然后在Safari中再次执行某项,依此类推上。 操作系统将把CPU为特定进程所做的任何事情保存在内存中(以寄存器和指针的形式),决定下一个要运行的进程,检索该进程正在做什么,让CPU运行该进程。一会儿,然后重复。 这称为上下文切换 ,它非常非常快地发生,给人的印象是它实际上可以一次运行多个操作。 (在具有多个内核的CPU中,可以将工作划分到各个内核之间,实际上一次执行多个操作。但是,在使用所有内核时,同样的原理也适用。) 操作系统决定下一步应该运行什么的确切方法相当复杂(如果您有兴趣,请阅读文章结尾的书),但是您应该知道,可以手动决定什么是“优先级” ”在我们的应用中。 (iOS的“服务质量”现在是否开始有意义?) 多线程程序具有一个以上的执行点,而不是从main()函数开始并在下面几行结束的某个exit()处结束的单线程进程的经典概念,执行)。 也许另一种思考方式是,每个线程都非常像一个单独的进程,只是有一个区别:它们共享相同的地址空间,因此可以访问相同的数据。 多线程进程的地址空间 […]
在Swift中,我们有一个非常有趣的概念,它称为SE-176上提出并在Swift 4中实现的“ 对内存的独占访问”。但这甚至意味着什么? 好吧,互斥访问是一个规则,强制所有对变量的潜在修改(写)必须与对该变量的任何其他访问互斥。 例如,在多线程环境中工作时,我们有多个线程访问一个变量,并且一个或多个线程可以修改该值,而该变量已被另一个变量修改,则无法从任何读取器或写入器访问该变量。作家。 您可以在提案文档中阅读有关此内容的更多信息。 如何保证独家访问? 通常,如果您知道一个变量将被多个线程修改并读取,则可能会在此调用周围使用NSLock或NSRecursiveLock,以确保任何时候在该代码区域中只有一个线程处于活动状态。 因此,问题是…如果我们锁定了对变量的更改(写),那么数据竞争将如何发生,实际上,在这种情况下,甚至没有任何写操作发生? 🤔 那是我所谈论的有趣的行为。 对于值类型的变异函数,发生的事情是将self作为inout参数隐式传递给该函数。 在内存访问的上下文中, inout参数具有写访问权限,该访问权限自动从函数的开头开始,并在函数完成时结束。 因此,在该示例中,即使我们不对函数内部的数组进行突变,整个方法的执行也具有长期的写访问权限。 从Apple文档中… 函数可以对其所有输入输出参数进行长期写访问。 在评估了所有非输入参数之后,才开始对输入输出参数进行写访问,并持续该函数调用的整个过程。 如果有多个输入/输出参数,则写入访问的开始顺序与参数出现的顺序相同。 您可以在“ 对In-Out参数的访问冲突”部分的“ Memory Safety [1]”文档中找到此内容。 因此,当我们在第一个队列上调用node.clearChilds()对self进行写访问时(也就是节点),然后在第二个队列上再次尝试对node.clearChilds()进行访问时,就会发生数据争用。节点,并尝试在self.childs.isEmpty调用中读取self 。 解决方案非常简单。 如果我们知道对变量函数的写访问已经开始,那么我们将需要锁定此方法的调用方。 因此,我们可以确保对该内容的专有访问权:)) 让我们修复代码,看看会发生什么 似乎一切正常,没有更多的数据争夺了:)) 上面示例中解决此问题的另一种方法可能是将Node类型更改为类而不是结构,因此您没有这种行为,因为在快速类中没有变异方法,而self只是隐式传递给该功能,因此不会启动长期写访问。 当我们在多线程环境中工作时,我们大多数Swift开发人员basically的日常生活基本上是什么,这些概念和细节对于了解非常有用,并且可以使我们避免很多怪异的bug并节省很多调试时间时间。 在下面的参考部分中,有很多关于内存安全,锁,线程安全以及我们在这里探讨的所有其他主题的精彩内容。 这就是本文的全部,希望您喜欢🙂 如果我有问题或您有任何意见或疑问,请告诉我。 我很高兴收到您的反馈feedback 您可以在Twitter上@ LucianoPassos11找到我。 感谢您阅读🙂 Swift编程语言(Swift 4.1):内存安全。 https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/MemorySafety.html [swift-evolution] [评论] SE-0176:强制对内存进行独占访问。 https://github.com/apple/swift-evolution/blob/master/proposals/0176-enforce-exclusive-access-to-memory.md 2017年10月27日,星期五问答:锁,线程安全和Swift:2017年版。 https://www.mikeash.com/pyblog/friday-qa-2017-10-27-locks-thread-safety-and-swift-2017-edition.html 主人处的swift / OwnershipManifesto.md·apple / swift https://github.com/apple/swift/blob/master/docs/OwnershipManifesto.md
自Xcode 8以来可用的线程清理器或TSan-LLVM是一种工具,当多个线程尝试以非原子方式访问同一内存区域并且其中至少一个线程执行一次写操作时,该工具可使我们调试数据竞争。 这种数据竞赛真的很难调试,因为它们极其不可预测,有时可以工作,有时不能,您永远都不知道首先到达那里的线程。 在本文中,我们将尝试更多地了解它可以做什么以及如何工作。 怎么运行的 让我们来看一个包含一段包含两个队列的数据争用的代码示例,并查看Thread Sanitizer如何帮助我们对其进行调试。 在上面的代码中,我们在两个不同的队列中写入相同的全局变量文本 ,然后,我们对该变量进行了读取。 但是阅读代码,您能回答将输出什么值? 哪个队列先写? 这是不可预测的,而且真的很难调试。 那就是Thread Sanitizer来帮助我们的地方。 首先,我们必须在Xcode中启用Thread Sanitizer: 在“产品”>“方案”>“编辑方案”上,在“运行”操作的“诊断”部分中,只需检查“线程清理器” 如果在命令行上运行,则可以为以下每个命令中的每个标志启用它: clang -fsanitize =线程 swiftc -sanitize =线程 xcodebuild -enableThreadSanitizer是 您可以在Apple文档中找到有关启用Thread Sanitizer的信息。 重要 Apple Core Diagnostics文档中有两个非常重要的内容 Thread Sanitizer仅支持64位macOS和64位iOS和tvOS模拟器(不支持watchOS)。 在设备上运行应用程序时,无法使用它。 以及使用此工具对性能的影响 在启用了Thread Sanitizer检查的情况下运行代码可能导致CPU速度降低2%至20%,并使内存使用量增加5%至10%。 通过在-O1优化级别进行编译,可以提高内存利用率和CPU开销。 因此,既然我们已经启用了Thread Sanitizer,现在让我们运行我们之前编写的代码。 我们在运行时看到的第一件事是Thread Sanitizer发出的警告,告诉我们这里存在竞争条件,结果可能无法预测。 同样在Xcode侧栏的警告部分,您可以看到以下警告: 控制台上也有带有相同警告的输出。 在Thread Sanitizer完成所有工作以显示可以在哪里出现这种竞争状况之后,我们可以更轻松地对其进行修复。 引擎盖下 为了更好地理解它,让我们记住Swift编译器的流程 swift编译器采用您的Swift代码,对其进行解析以生成抽象语法树(AST),然后进行语义分析,其中编译器采用解析器生成的AST并进行类型检查的AST并检查是否存在语义问题那。 然后,Swift中级语言生成(SILGen)阶段将通过语义分析生成的AST转换为他们所谓的原始SIL,在对SIL进行了一些优化(例如通用专业化和ARC优化)之后,将该优化的SIL传递给IRGen以生成中间语言。要传递给LLVM的Representation(IR),以使其继续作业并生成目标文件。 好,让我们回到Thread Sanitizer。 您可能已经在启用“线程清除程序”部分中注意到它需要重新编译。 这是因为Thread […]