URL: https://sunyazhou.com/2025/05/MemoryAlignmentAlgorithm/index.html.md Published At: 2025-05-11 07:30:00 +0000 # GPU内存对齐算法 ![](/assets/images/20240727Magnificationgesture/SwiftUI.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! ## 背景介绍 在学习《Metal》 GPU着色器编程中 有一章讲解GPU资源堆(Resource Heap)的参数缓冲(Arguements Buffer)中需要从CPU送资源到GPU时遇到一段代码计算资源的内存占用的算法,很有意思,在此记录下来. ``` swift let sizeAndAligns = descriptors.map { descriptor in Renderer.device.heapTextureSizeAndAlign(descriptor: descriptor) } heapDescriptor.size = sizeAndAligns.reduce(0) { total, sizeAndAlign in let size = sizeAndAlign.size let align = sizeAndAlign.align return total + size - (size & (align - 1)) + align //这行代码 } if heapDescriptor.size == 0 { return nil } ``` 文档是这样说明的 > You calculate the size of the heap using size and correct alignment within the heap. As long as align is a power of two, (size & (align - 1)) will give you the remainder when size is divided by alignment. For example, if you have a size of 129 bytes, and you want to align it to memory blocks of 128 bytes, this is the result of size - (size & (align - 1)) + align: ``` sh 129 - (129 & (128 - 1)) + 128 = 256 ``` 表达式` 129 & (128 - 1) `本质上是一种高效的取模(求余)操作,但仅当除数是 2 的幂次方时成立。以下是详细解释: #### 1. 数学原理 当 align 是 2 的幂次方(如 128 = 2⁷),align - 1 的二进制形式是全 1(例如 127 的二进制是 01111111)。 按位与(&)操作会将 size 的高位清零,仅保留低 log₂(align) 位的值,即 size % align 的余数。 示例计算 对于 size = 129,align = 128: ``` math \begin{align*} \text{余数} &= 129 \ \% \ 128 \\ &= 129 \ \& \ (128 - 1) \\ &= 129 \ \& \ 127 \\ &= \text{二进制} \ 10000001 \ \& \ 01111111 \\ &= 00000001 \quad (\text{即十进制 1}) \end{align*} ``` #### 2. 与传统模运算的对比 | 方法 | 运算表达式 | 结果 | 适用条件 | ------| ------ | ------ | ------ | | 按位与法 | `size & (align - 1)` | 1 | `align`必须是 2 的幂次方 | | 传统取模法 | `size % align` | 1 | 任意正整数 | 优势 * 性能高效:位运算(&)的硬件执行速度通常比模运算(%)快一个数量级。 * 代码简洁:适用于需要频繁计算对齐的场景(如 GPU 内存管理)。 #### 3. 实际应用场景 在 Metal 中管理堆内存(`MTLHeap`)时,常用此方法计算对齐后的内存大小。例如: ``` swift func alignedSize(size: Int, align: Int) -> Int { assert(align.isPowerOfTwo, "Alignment must be a power of two") let remainder = size & (align - 1) return remainder == 0 ? size : (size - remainder + align) } // 示例:将 129 字节对齐到 128 字节 let aligned = alignedSize(size: 129, align: 128) // 结果为 256 ``` #### 4.非 2 的幂次方如何处理? 若`align`不是 2 的幂次方(如`align = 100`),必须使用传统模运算: ``` swift func alignedSizeGeneric(size: Int, align: Int) -> Int { let remainder = size % align return remainder == 0 ? size : (size - remainder + align) } // 示例:将 250 对齐到 100 let aligned = alignedSizeGeneric(size: 250, align: 100) // 结果为 300 ``` 是的,表达式 `129 & (128 - 1)` 本质上是一种高效的取模(求余)操作,但**仅当除数是 2 的幂次方时成立**。 ### **总结** - `129 & (128 - 1)` **是取模操作**,但仅当 `align` 为 2 的幂次方时成立。 - **适用场景**:GPU 内存对齐、高性能计算中优化取模操作。 - **核心公式**: ``` math \[ \text{AlignedSize} = \text{size} - (\text{size} \ \% \ \text{align}) + \text{align} \] ``` ![](/assets/images/20250511MemoryAlignmentAlgorithm/AlignedSize.webp) 其中 `%` 可通过 `& (align - 1)` 优化(当 `align` 是 2 的幂时)。 URL: https://sunyazhou.com/2025/02/the-basics-on-the-memory-layout-of-swift-struct-instances/index.html.md Published At: 2025-02-23 12:17:00 +0000 # Swift结构体实例内存布局的基础知识 ![](/assets/images/20240727Magnificationgesture/SwiftUI.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! ## 背景介绍 ![](/assets/images/20250223SwiftStructMemoryLayout/VerTexBufferLayout.webp) 2024年学习《Metal.by.Tutorials.4th.2023.12》中有提到swift中的结构体实例的内存布局,我把这些整理了一下. ## 大小、步长和对齐(Size, Stride, Alignment) Swift 结构体实例内存布局的基础知识 > 2018 年 3 月 12 日 ∙ Swift 内部原理 ∙ 作者:Greg Heo 在内存中处理 Swift 类型时,需要考虑三个属性:大小(Size)、步长(Stride) 和 对齐(Alignment)。 ## 大小(Size) 让我们以两个结构体的举例说明 ``` swift struct Year { let year: Int } struct YearWithMonth { let year: Int let month: Int } ``` 我的直觉告诉我,YearWithMonth 的实例比 Year 的实例更大——它在内存中占用了更多的空间。但我们是科学家;我们如何用确凿的数据来验证直觉呢? ## 内存布局(Memory Layout) 我们可以使用 `MemoryLayout` 类型来检查我们的类型在内存中的一些属性。 要查找结构体的大小,可以使用 size 属性并结合泛型参数: ``` swift let size = MemoryLayout.size ``` 如果你有一个类型的实例,可以使用 `size(ofValue:)` 静态函数: ``` swift let instance = Year(year: 1984) let size = MemoryLayout.size(ofValue: instance) ``` 在这两种情况下,大小都被报告为 **8 字节**。 不出所料,我们的结构体 `YearWithMonth` 的大小是 **16 字节**。 ## 回到大小 结构体的大小似乎非常直观——计算每个属性大小的总和。对于这样的结构体: ``` swift struct Puppy { let age: Int let isTrained: Bool } ``` 结构体的大小应该与其属性的大小相匹配: ``` swift MemoryLayout.size + MemoryLayout.size // returns 9, from 8 + 1 MemoryLayout.size // returns 9 ``` 看起来没问题![旁白:真的没问题吗?😈] ## 步长(Stride) 当你在处理单个缓冲区(例如数组)中的多个实例时,类型的步长就变得非常重要。 如果我们有一个连续的小狗数组,每只小狗的大小为 9 字节,那么它在内存中会是什么样子呢? ![](/assets/images/20250223SwiftStructMemoryLayout/stride-nopadding.webp) 事实证明,并非如此。❌ `步长 Stride`决定了两个元素之间的距离,它通常大于或等于大小。 ``` swift MemoryLayout.size // returns 9 MemoryLayout.stride // returns 16 ``` 因此,实际的布局看起来是这样的: ![](/assets/images/20250223SwiftStructMemoryLayout/stride-padding.webp) 也就是说,如果你有一个指向第一个元素的字节指针,并希望移动到第二个元素,步长就是你需要将指针前进的字节距离。 为什么大小和步长会不同?这就引出了内存布局的最后一个神奇数字。 ## 对齐(Alignment) 想象一下,计算机一次获取 8 位(即 1 字节)的内存。无论是获取第 1 个字节还是第 7 个字节,所需的时间是相同的。 ![](/assets/images/20250223SwiftStructMemoryLayout/alignment-byte8.webp) 然后你升级到了一台 16 位计算机,它以 16 位的字(word)为单位访问数据。你仍然有一些旧的软件希望以字节为单位访问数据,但想象一下这里可能发生的魔法:如果软件请求字节 0 和字节 1,计算机现在可以一次性访问字 0,然后将 16 位的结果拆分。 ![](/assets/images/20250223SwiftStructMemoryLayout/alignment-byte16.webp) 在这种理想情况下,字节级的内存访问速度提高了一倍!🎉 现在假设一个不守规矩的程序像这样放入一个 16 位的值: ![](/assets/images/20250223SwiftStructMemoryLayout/alignment-misaligned16.webp) 然后你要求计算机从字节位置 3 读取 16 位的字(word)。问题在于,这个值是对齐不当的。为了读取它,计算机需要读取位置 1 的字,将其切半,再读取位置 2 的字,将其切半,然后将两半拼接在一起。这意味着访问一个 16 位的值需要两次独立的 16 位内存读取——比应有的速度慢了两倍!😭 在某些系统中,未对齐的访问不仅仅是慢的问题——它完全不被允许,并会导致程序崩溃。 ## 简单的 Swift 类型 在 Swift 中,简单类型(如 `Int` 和 `Double`)的对齐值与其大小相同。一个 32 位(4 字节)的整数大小为 4 字节,并且需要对齐到 4 字节。 ``` swift MemoryLayout.size // returns 4 MemoryLayout.alignment // returns 4 MemoryLayout.stride // returns 4 ``` 步长也是 4,这意味着在连续缓冲区中,值之间相隔 4 字节。不需要填充。 ## 复合类型 (Compound Types) 现在回到我们的 `Puppy` 结构体,它有一个 `Int` 和一个 `Bool` 属性。再次考虑值在缓冲区中紧挨在一起的情况: ![](/assets/images/20250223SwiftStructMemoryLayout/alignment-nopadding-bytes.webp) `Bool` 值的位置没有问题,因为它们的对齐值为 1 (`alignment=1`)。但第二个整数是对齐不当的。它是一个 64 位(8 字节)的值,对齐值为 8(`alignment=8`),而它的字节位置不是 8 的倍数。❌ 记住,这种类型的步长是 16,这意味着缓冲区实际上看起来是这样的: ![](/assets/images/20250223SwiftStructMemoryLayout/alignment-padding-bytes.webp) 我们保留了结构体内所有值的对齐要求:第二个整数位于字节 16,这是 8 的倍数。 这就是为什么结构体的步长可以大于其大小:为了添加足够的填充以满足对齐要求。 ## 计算对齐 那么,在我们这段旅程的结尾,`Puppy` 结构体类型的对齐值是多少呢? ``` swift MemoryLayout.alignment // returns 8 ``` 结构体类型的对齐值是其所有属性中最大的对齐值。在 `Int` 和 `Bool` 之间,`Int` 的对齐值更大,为 8,因此结构体使用它。 然后,步长是大小向上取整到对齐值的下一个倍数。在我们的例子中: - 大小是 9 - 9 不是 8 的倍数 - 9 之后的下一个 8 的倍数是 16 - 因此,步长是 16 ## 最后一个复杂点 考虑我们最初的 `Puppy`,并将其与 `AlternatePuppy` 进行对比: ``` swift struct Puppy { let age: Int let isTrained: Bool } // Int, Bool struct AlternatePuppy { let isTrained: Bool let age: Int } // Bool, Int ``` `AlternatePuppy` 结构体的对齐值仍然是 8,步长仍然是 16,但: ``` swift MemoryLayout.size // returns 16 ``` 什么?!我们只是改变了属性的顺序。为什么现在大小不一样了?它应该仍然是9,不是吗?一个布尔值后面跟着一个整数,就像这样: ![](/assets/images/20250223SwiftStructMemoryLayout/alignment-internal-1.webp) 也许你看到了问题所在:8字节的整数不再对齐了!它在内存中实际看起来是这样的: ![](/assets/images/20250223SwiftStructMemoryLayout/alignment-internal-2.webp) 结构体本身必须对齐,结构体内部的属性也必须保持对齐。填充字节会插入到元素之间,整个结构体的大小也会随之扩展。 在这种情况下,步长(stride)仍然是16,因此从`Puppy`到`AlternatePuppy`的实际变化是填充字节的位置。那么这些结构体呢? ``` swift struct CertifiedPuppy1 { let age: Int let isTrained: Bool let isCertified: Bool } // Int, Bool, Bool struct CertifiedPuppy2 { let isTrained: Bool let age: Int let isCertified: Bool } // Bool, Int, Bool ``` 这两个结构体的大小(size)、步长(stride)和对齐方式(alignment)分别是多少呢?🤔(提示) ## 关于闭合大括号 假设你有一个`UnsafeRawPointer`(在C语言中相当于`void *`)。你知道它指向的类型。那么,大小(size)、步长(stride)和对齐方式(alignment)在其中扮演什么角色呢? - **大小(Size)**:是从指针读取以获取所有数据所需的字节数。 - **步长(Stride)**:是向前移动以到达缓冲区中下一个项目的字节数。 - **对齐方式(Alignment)**:是每个实例必须位于的“能被整除的”数字。如果你正在分配内存以复制数据,你需要指定正确的对齐方式(例如:`allocate(byteCount: 100, alignment: 4)`)。 ![](/assets/images/20250223SwiftStructMemoryLayout/size-stride-alignment-summary.webp) 对于我们大多数人来说,大多数时候,我们处理的都是高级集合,比如数组和集合,不需要考虑底层的内存布局。 在其他情况下,你可能需要在平台上使用低级API,或者与C代码进行互操作。如果你有一个Swift结构体数组,并且需要让C代码读取它(或者反过来),那么你就需要担心分配具有正确对齐方式的缓冲区,确保结构体内部的填充字节对齐,以及确保你有正确的步长值,以便正确解释数据。 正如我们所见,即使是计算大小,也没有看起来那么简单——每个属性的大小和对齐方式之间存在相互作用,这决定了结构体的整体大小。因此,理解这三者意味着你正在成为内存管理的高手。 对深入了解感兴趣吗? - [Wikipedia 上的 “Data structure alignment”](https://en.wikipedia.org/wiki/Data_structure_alignment) - [Swift 文档中的 “Type Layout” 文章](https://github.com/apple/swift/blob/master/docs/ABI/TypeLayout.rst),解释了如何计算结构体的大小(size)、步长(stride)和对齐方式(alignment)。 - [LLVM 中的 `getAlignOf` 源码](https://github.com/apple/swift-llvm/blob/stable/lib/IR/Constants.cpp#L1800-L1811) - [Swift 的 `UnsafeMutableRawPointer.allocate(byteCount:alignment:)`,带有大小和对齐参数](https://developer.apple.com/documentation/swift/unsafemutablerawpointer/allocate(bytecount:alignment:)) # 总结 在学习Metal开始的时候要使用swift类型操作内存,对swift中的类型内存布局和对齐了解不是很清楚,整理了这篇文章 希望对你有所帮助 [原文地址The basics on the memory layout of Swift struct instances. ](https://swiftunboxed.com/internals/size-stride-alignment/) URL: https://sunyazhou.com/2025/02/unsafe-swift-using-pointers-and-interacting-with-c/index.html.md Published At: 2025-02-22 14:15:00 +0000 # 如何使用unsafe Swift指针类型直接访问内存并与C交互 ![](/assets/images/20250222UnsafeSwift/banner.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! ### 背景介绍 2024年学习《Metal.by.Tutorials.4th.2023.12》中有提到如何使用`Unsafe Swift` 指针和C交互,主要是在内存中如何标识C的内容,下面这篇文章是书中介绍的英文文章我看了以后觉得消化吸收一下,整理成中文版供各位参考. 在本教程中,您将学习如何使用unsafe Swift 通过各种指针类型直接访问内存。作者:Brody Eller。 > 更新说明:Brody Eller 为 Swift 5.1 更新了本教程。原始版本由 Ray Fix 编写。 默认情况下,Swift 是内存安全的:它防止直接访问内存,并确保在使用之前初始化所有内容。关键短语是“默认情况下”。你也可以使用不安全的 Swift,它允许你通过指针直接访问内存。 本教程将带你快速了解 Swift 中所谓的“不安全”特性。 “不安全”并不意味着代码可能会出错或危险。相反,它指的是需要额外小心的代码,因为它限制了编译器在防止你犯错方面的能力。 如果你需要与不安全的语言(如 C)进行交互、需要提高运行时性能,或者只是想探索 Swift 的内部机制,这些特性会非常有用。在本教程中,你将学习如何使用指针并直接与内存系统交互。 > 注意:虽然这是一个高级主题,但如果你对 Swift 有一定的掌握能力,就可以跟上本教程的内容。如果你需要复习 Swift 技能,请查[ iOS 和 Swift 初学者系列](https://www.kodeco.com/ios/learn)。有 C 语言经验会有所帮助,但不是必需的。 ### 开始前可以下载本篇文章涉及到的demo [Download Materials下载初始项目](https://github.com/sunyazhou13/Using-Pointers-and-Interacting-With-C)。 本教程包含三个空的 Swift Playground 文件: ### 探索Unsafe Swift内存布局 首先打开 UnsafeSwift Playground。由于本教程中的所有代码都是跨平台的,你可以选择任意平台 ![](/assets/images/20250222UnsafeSwift/memory1.webp) 不安全的 Swift 直接与内存系统交互。你可以将内存想象成一系列盒子——实际上有数十亿个盒子——每个盒子里都包含一个数字。 每个盒子都有一个唯一的内存地址。最小的可寻址存储单元是一个字节(byte),通常由 8 个比特(bit)组成。 8 比特的字节可以存储 0 到 255 之间的值。处理器还可以高效地访问内存中的字(word),字通常由多个字节组成。 例如,在 64 位系统上,一个字是 8 个字节(64 比特)。为了更直观地理解这一点,你可以使用 `MemoryLayout` 来查看一些原生 Swift 类型的大小和对齐方式。 将以下代码添加到你的 Playground 中: * 在第一个 Playground 中,你将使用几段简短的代码来探索内存布局,并尝试使用不安全的指针。 * 在第二个 Playground 中,你将使用一个低级的 C API 来执行流式数据压缩,并将其封装为 Swift 风格的接口。 * 在最后一个 Playground 中,你将创建一个跨平台的替代 `arc4random` 的随机数生成器。它内部使用了不安全的 Swift,但对用户隐藏了这一细节。 首先打开 **UnsafeSwift** Playground。由于本教程中的所有代码都是跨平台的,你可以选择任意平台。 ![](/assets/images/20250222UnsafeSwift/memory2.webp) 不安全的 Swift 直接与内存系统交互。你可以将内存想象成一系列盒子——实际上有数十亿个盒子——每个盒子里都包含一个数字。 每个盒子都有一个唯一的内存地址。最小的可寻址存储单元是一个字节(byte),通常由 8 个比特(bit)组成。 8 比特的字节可以存储 0 到 255 之间的值。处理器还可以高效地访问内存中的字(word),字通常由多个字节组成。 例如,在 64 位系统上,一个字是 8 个字节(64 比特)。为了更直观地理解这一点,你可以使用 `MemoryLayout` 来查看一些原生 Swift 类型的大小和对齐方式。 将以下代码添加到你的 Playground 中: ``` swift import Foundation MemoryLayout.size // returns 8 (on 64-bit) MemoryLayout.alignment // returns 8 (on 64-bit) MemoryLayout.stride // returns 8 (on 64-bit) MemoryLayout.size // returns 2 MemoryLayout.alignment // returns 2 MemoryLayout.stride // returns 2 MemoryLayout.size // returns 1 MemoryLayout.alignment // returns 1 MemoryLayout.stride // returns 1 MemoryLayout.size // returns 4 MemoryLayout.alignment // returns 4 MemoryLayout.stride // returns 4 MemoryLayout.size // returns 8 MemoryLayout.alignment // returns 8 MemoryLayout.stride // returns 8 ``` `MemoryLayout` 是一个在编译时评估的泛型类型。它用于确定指定 `Type` 的大小(size)、对齐方式(alignment)和步长(stride),并返回以字节为单位的值。 例如,`Int16` 的大小为 2 个字节,对齐方式也是 2。这意味着它必须从偶数地址开始——即地址可以被 2 整除。 例如,可以在地址 100 分配一个 `Int16`,但不能在地址 101 分配——奇数地址违反了所需的对齐要求。 当你将一堆 `Int16` 打包在一起时,它们会按照步长(stride)的间隔排列。对于这些基本类型,步长与大小是相同的。 ### 检查结构体的内存布局 接下来,通过将以下代码添加到 Playground 中,查看一些用户定义的结构体`user-defined struct`的内存布局: ``` swift struct EmptyStruct {} MemoryLayout.size // returns 0 MemoryLayout.alignment // returns 1 MemoryLayout.stride // returns 1 struct SampleStruct { let number: UInt32 let flag: Bool } MemoryLayout.size // returns 5 MemoryLayout.alignment // returns 4 MemoryLayout.stride // returns 8 ``` 空结构体的大小为零。由于对齐方式为 1,它可以存在于任何地址,因为所有数字都可以被 1 整除。 有趣的是,步长(`stride`)为 1。这是因为即使 `EmptyStruct` 的大小为零,你创建的每个 `EmptyStruct` 都必须有一个唯一的内存地址。 对于 `SampleStruct`,其大小为 5,但步长为 8。这是因为它的对齐要求它必须位于 4 字节的边界上。在这种情况下,Swift 能做到的最佳打包间隔是 8 个字节。 为了查看类(class)和结构体(struct)在内存布局上的区别,请添加以下代码: ``` swift class EmptyClass {} MemoryLayout.size // returns 8 (on 64-bit) MemoryLayout.stride // returns 8 (on 64-bit) MemoryLayout.alignment // returns 8 (on 64-bit) class SampleClass { let number: Int64 = 0 let flag = false } MemoryLayout.size // returns 8 (on 64-bit) MemoryLayout.stride // returns 8 (on 64-bit) MemoryLayout.alignment // returns 8 (on 64-bit) ``` 类是引用类型,因此 `MemoryLayout` 报告的是引用的大小:8 个字节。 如果你想更详细地探索内存布局,可以查看 Mike Ash 的精彩演讲:[Exploring Swift Memory Layout](https://mikeash.com/pyblog/friday-qa-2014-07-18-exploring-swift-memory-layout.html)。 ### Using Pointers in Unsafe Swift在不安全的 Swift 中使用指针 指针封装了一个内存地址。 涉及直接内存访问的类型会带有 `unsafe` 前缀,因此指针类型的名称为 `UnsafePointer`。 额外的输入可能看起来有些烦人,但它提醒你正在访问编译器未检查的内存。如果操作不当,可能会导致未定义行为,而不仅仅是一个可预测的崩溃。 Swift 并不像 C 语言中的 `char *` 那样,只提供一种非结构化的 `UnsafePointer` 类型来访问内存。Swift 提供了近十种指针类型,每种类型都有不同的功能和用途。 你应该始终根据需求选择最合适的指针类型。这不仅能更好地表达意图,还能减少错误并避免未定义行为。 不安全的 Swift 指针使用一种可预测的命名方案来描述指针的特性:可变的(mutable)或不可变的(immutable)、原始的(raw)或类型化的(typed)、缓冲区风格(buffer style)或非缓冲区风格。总共有八种指针组合。你将在接下来的部分中了解更多关于它们的内容。 ![](/assets/images/20250222UnsafeSwift/pointers1.webp) ### Using Raw Pointers 使用原始指针 在本节中,你将使用不安全的 Swift 指针来存储和加载两个整数。将以下代码添加到你的 Playground 中: ``` swift // 1 let count = 2 let stride = MemoryLayout.stride let alignment = MemoryLayout.alignment let byteCount = stride * count // 2 do { print("Raw pointers") // 3 let pointer = UnsafeMutableRawPointer.allocate( byteCount: byteCount, alignment: alignment) // 4 defer { pointer.deallocate() } // 5 pointer.storeBytes(of: 42, as: Int.self) pointer.advanced(by: stride).storeBytes(of: 6, as: Int.self) pointer.load(as: Int.self) pointer.advanced(by: stride).load(as: Int.self) // 6 let bufferPointer = UnsafeRawBufferPointer(start: pointer, count: byteCount) for (index, byte) in bufferPointer.enumerated() { print("byte \(index): \(byte)") } } ``` 以下是代码的详细说明: * 1.这些常量保存了常用的值: - `count` 保存要存储的整数的数量。 - `stride` 保存 `Int` 类型的步长(stride)。 - `alignment` 保存 `Int` 类型的对齐方式。 - `byteCount` 保存所需的总字节数。 * 2.一个 `do` 块添加了一个作用域级别,这样你可以在接下来的示例中重用变量名。 * 3.`UnsafeMutableRawPointer.allocate` 分配所需的字节。此方法返回一个 `UnsafeMutableRawPointer`。该类型的名称告诉你,该指针可以加载和存储(或修改)原始字节。 * 4.`defer` 块确保你正确地释放指针。ARC(自动引用计数)在这里不会帮助你——你需要自己管理内存!你可以在 [官方 Swift 文档](https://docs.swift.org/swift-book/LanguageGuide/ErrorHandling.html#ID514) 中阅读更多关于 `defer` 语句的内容。 * 5.`storeBytes` 和 `load` 方法用于存储和加载字节。你可以通过将指针前进 `stride` 个字节来计算第二个整数的内存地址。由于指针是 `Strideable` 的,你也可以使用指针算术,例如:`(pointer+stride).storeBytes(of: 6, as: Int.self)`。 * 6.`UnsafeRawBufferPointer` 允许你将内存视为字节集合来访问。这意味着你可以迭代字节并使用下标访问它们。你还可以使用像 `filter`、`map` 和 `reduce` 这样的方法。你可以使用原始指针初始化缓冲区指针。 尽管 `UnsafeRawBufferPointer` 是不安全的,但你仍然可以通过将其约束为特定类型来使其更安全。 ### Using Typed Pointers 使用类型化的指针 你可以通过使用类型化指针来简化前面的示例。将以下代码添加到你的 Playground 中: ``` swift do { print("Typed pointers") let pointer = UnsafeMutablePointer.allocate(capacity: count) pointer.initialize(repeating: 0, count: count) defer { pointer.deinitialize(count: count) pointer.deallocate() } pointer.pointee = 42 pointer.advanced(by: 1).pointee = 6 pointer.pointee pointer.advanced(by: 1).pointee let bufferPointer = UnsafeBufferPointer(start: pointer, count: count) for (index, value) in bufferPointer.enumerated() { print("value \(index): \(value)") } } ``` 注意以下区别: 1. 你使用 `UnsafeMutablePointer.allocate` 分配内存。泛型参数让 Swift 知道你将使用该指针来加载和存储 `Int` 类型的值。 2. 在使用类型化内存之前,必须先初始化它,并在使用后反初始化它。你可以分别使用 `initialize` 和 `deinitialize` 方法来完成这些操作。反初始化仅对非平凡类型(non-trivial types)是必需的。然而,包含反初始化操作是一种很好的方式,可以确保代码在未来切换到非平凡类型时仍然有效。通常这不会带来任何开销,因为编译器会将其优化掉。 3. 类型化指针有一个 `pointee` 属性,它提供了一种类型安全的方式来加载和存储值。 4. 当推进类型化指针时,你可以简单地指定你想要推进的值的数量。指针可以根据它指向的值的类型计算出正确的步长(stride)。同样,指针算术也适用。你也可以写成 `(pointer+1).pointee = 6`。 5. 对于类型化的缓冲区指针也是如此:它们迭代的是值而不是字节。 接下来,你将学习如何从无约束的 `UnsafeRawBufferPointer` 转换为更安全的、类型约束的 `UnsafeRawBufferPointer`。 ### Converting Raw Pointers to Typed Pointers转换原始指针类型到类型化的指针 你并不总是需要直接初始化类型化指针。你也可以从原始指针中派生出它们。 将以下代码添加到你的 Playground 中: ``` swift do { print("Converting raw pointers to typed pointers") let rawPointer = UnsafeMutableRawPointer.allocate( byteCount: byteCount, alignment: alignment) defer { rawPointer.deallocate() } let typedPointer = rawPointer.bindMemory(to: Int.self, capacity: count) typedPointer.initialize(repeating: 0, count: count) defer { typedPointer.deinitialize(count: count) } typedPointer.pointee = 42 typedPointer.advanced(by: 1).pointee = 6 typedPointer.pointee typedPointer.advanced(by: 1).pointee let bufferPointer = UnsafeBufferPointer(start: typedPointer, count: count) for (index, value) in bufferPointer.enumerated() { print("value \(index): \(value)") } } ``` 这个示例与之前的示例类似,不同之处在于它首先创建了一个原始指针。你通过将内存绑定到所需的类型 `Int` 来创建类型化指针。 通过绑定内存,你可以以类型安全的方式访问它。当你创建类型化指针时,内存绑定会在幕后进行。 这个示例的其余部分也与之前的示例相同。一旦你进入类型化指针的领域,就可以使用 `pointee` 等特性。 ### 获取实例的字节 通常,你已经有一个类型的实例,并且想要检查构成它的字节。你可以使用 `withUnsafeBytes(of:)` 方法来实现这一点。 为此,将以下代码添加到你的 Playground 中: ``` swift do { print("Getting the bytes of an instance") var sampleStruct = SampleStruct(number: 25, flag: true) withUnsafeBytes(of: &sampleStruct) { bytes in for byte in bytes { print(byte) } } } ``` 这会打印出 `SampleStruct` 实例的原始字节。 `withUnsafeBytes(of:)` 允许你访问一个 `UnsafeRawBufferPointer`,你可以在闭包中使用它。 `withUnsafeBytes` 也可以作为 `Array` 和 `Data` 的实例方法使用。 ### Computing a Checksum计算校验和 使用 `withUnsafeBytes(of:)`,你可以返回一个结果。例如,你可以使用它来计算结构中字节的 32 位校验和。 将以下代码添加到你的 Playground 中: ``` swift do { print("Checksum the bytes of a struct") var sampleStruct = SampleStruct(number: 25, flag: true) let checksum = withUnsafeBytes(of: &sampleStruct) { (bytes) -> UInt32 in return ~bytes.reduce(UInt32(0)) { $0 + numericCast($1) } } print("checksum", checksum) // prints checksum 4294967269 } ``` `reduce` 调用将字节相加,然后 `~` 翻转位。虽然这不是最强大的错误检测方法,但它展示了这个概念。 现在你已经了解了如何使用不安全的 Swift,接下来是时候学习一些你绝对不应该用它做的事情了。 ### 不安全代码的三条规则 在编写不安全代码时,务必小心避免未定义行为。以下是一些错误代码的示例: #### 不要从 `withUnsafeBytes` 返回指针! ``` swift // Rule #1 do { print("1. Don't return the pointer from withUnsafeBytes!") var sampleStruct = SampleStruct(number: 25, flag: true) let bytes = withUnsafeBytes(of: &sampleStruct) { bytes in return bytes // strange bugs here we come ☠️☠️☠️ } print("Horse is out of the barn!", bytes) // undefined!!! } ``` 你绝不应该让指针逃逸出 `withUnsafeBytes(of:)` 闭包。即使你的代码现在可以运行,未来也可能会导致奇怪的错误。 #### 一次只绑定一种类型! ``` swift // Rule #2 do { print("2. Only bind to one type at a time!") let count = 3 let stride = MemoryLayout.stride let alignment = MemoryLayout.alignment let byteCount = count * stride let pointer = UnsafeMutableRawPointer.allocate( byteCount: byteCount, alignment: alignment) let typedPointer1 = pointer.bindMemory(to: UInt16.self, capacity: count) // Breakin' the Law... Breakin' the Law (Undefined behavior) let typedPointer2 = pointer.bindMemory(to: Bool.self, capacity: count * 2) // If you must, do it this way: typedPointer1.withMemoryRebound(to: Bool.self, capacity: count * 2) { (boolPointer: UnsafeMutablePointer) in print(boolPointer.pointee) // See Rule #1, don't return the pointer } } ``` 永远不要将内存同时绑定到两种不相关的类型。这被称为 **类型双关(Type Punning)**,而 Swift 不喜欢双关。:] 相反,可以使用 `withMemoryRebound(to:capacity:)` 等方法临时重新绑定内存。 此外,从平凡类型(如 `Int`)重新绑定到非平凡类型(如类)是非法的。不要这样做。 #### 不要越界……哎呀! ``` swift // Rule #3... wait do { print("3. Don't walk off the end... whoops!") let count = 3 let stride = MemoryLayout.stride let alignment = MemoryLayout.alignment let byteCount = count * stride let pointer = UnsafeMutableRawPointer.allocate( byteCount: byteCount, alignment: alignment) let bufferPointer = UnsafeRawBufferPointer(start: pointer, count: byteCount + 1) // OMG +1???? for byte in bufferPointer { print(byte) // pawing through memory like an animal } } ``` 在编写不安全代码时,**越界错误**(off-by-one errors)的问题会更加严重。务必小心,仔细检查并测试你的代码! ### 不安全的 Swift 示例 1:压缩 是时候运用你所学到的知识来封装一个 C API 了。Cocoa 包含一个实现了常见数据压缩算法的 C 模块。这些算法包括: - **LZ4**:适用于速度至关重要的情况。 - **LZ4A**:适用于需要最高压缩比且不关心速度的情况。 - **ZLIB**:在空间和速度之间取得平衡。 - **LZFSE**:新的开源算法,在空间和速度之间取得了更好的平衡。 现在,打开初始项目中的 **Compression** Playground。 首先,你将使用 `Data` 定义一个纯 Swift API,将 Playground 的内容替换为以下代码: ``` swift import Foundation import Compression enum CompressionAlgorithm { case lz4 // speed is critical case lz4a // space is critical case zlib // reasonable speed and space case lzfse // better speed and space } enum CompressionOperation { case compression, decompression } /// return compressed or uncompressed data depending on the operation func perform( _ operation: CompressionOperation, on input: Data, using algorithm: CompressionAlgorithm, workingBufferSize: Int = 2000) -> Data? { return nil } ``` 执行压缩和解压缩的函数是 `perform`,目前它被存根化(stubbed out)并返回 `nil`。稍后你将为其添加一些不安全的代码。 接下来,将以下代码添加到 Playground 的末尾: ``` swift /// Compressed keeps the compressed data and the algorithm /// together as one unit, so you never forget how the data was /// compressed. struct Compressed { let data: Data let algorithm: CompressionAlgorithm init(data: Data, algorithm: CompressionAlgorithm) { self.data = data self.algorithm = algorithm } /// Compresses the input with the specified algorithm. Returns nil if it fails. static func compress( input: Data,with algorithm: CompressionAlgorithm) -> Compressed? { guard let data = perform(.compression, on: input, using: algorithm) else { return nil } return Compressed(data: data, algorithm: algorithm) } /// Uncompressed data. Returns nil if the data cannot be decompressed. func decompressed() -> Data? { return perform(.decompression, on: data, using: algorithm) } } ``` `Compressed` 结构体存储了压缩后的数据以及用于创建它的算法。这使得在决定使用哪种解压缩算法时,代码更不容易出错。 接下来,将以下代码添加到 Playground 的末尾: ``` swift /// For discoverability, adds a compressed method to Data extension Data { /// Returns compressed data or nil if compression fails. func compressed(with algorithm: CompressionAlgorithm) -> Compressed? { return Compressed.compress(input: self, with: algorithm) } } // Example usage: let input = Data(Array(repeating: UInt8(123), count: 10000)) let compressed = input.compressed(with: .lzfse) compressed?.data.count // in most cases much less than original input count let restoredInput = compressed?.decompressed() input == restoredInput // true ``` 主要的入口点是 `Data` 类型的扩展。你添加了一个名为 `compressed(with:)` 的方法,它返回一个可选的 `Compressed` 结构体。该方法简单地调用了 `Compressed` 上的静态方法 `compress(input:with:)`。 最后有一个示例,但目前它还不能正常工作。是时候修复它了! 滚动到你输入的第一个代码块,并开始实现 `perform(_:on:using:workingBufferSize:)`,在 `return nil` 之前插入以下代码: ``` swift // set the algorithm let streamAlgorithm: compression_algorithm switch algorithm { case .lz4: streamAlgorithm = COMPRESSION_LZ4 case .lz4a: streamAlgorithm = COMPRESSION_LZMA case .zlib: streamAlgorithm = COMPRESSION_ZLIB case .lzfse: streamAlgorithm = COMPRESSION_LZFSE } // set the stream operation and flags let streamOperation: compression_stream_operation let flags: Int32 switch operation { case .compression: streamOperation = COMPRESSION_STREAM_ENCODE flags = Int32(COMPRESSION_STREAM_FINALIZE.rawValue) case .decompression: streamOperation = COMPRESSION_STREAM_DECODE flags = 0 } ``` 这将你的 Swift 类型转换为压缩算法所需的 C 类型。 接下来,将 `return nil` 替换为: ``` swift // 1: create a stream var streamPointer = UnsafeMutablePointer.allocate(capacity: 1) defer { streamPointer.deallocate() } // 2: initialize the stream var stream = streamPointer.pointee var status = compression_stream_init(&stream, streamOperation, streamAlgorithm) guard status != COMPRESSION_STATUS_ERROR else { return nil } defer { compression_stream_destroy(&stream) } // 3: set up a destination buffer let dstSize = workingBufferSize let dstPointer = UnsafeMutablePointer.allocate(capacity: dstSize) defer { dstPointer.deallocate() } return nil // To be continued ``` 以下是正在发生的事情: 编译器在这里做了一些特殊的事情:它使用了 `in-out` 的 `&` 标记,将你的 `compression_stream` 转换为一个 `UnsafeMutablePointer` 类型。或者,你也可以直接传递 `streamPointer`,这样就不需要这种特殊的转换了。 分配一个 `compression_stream` 并使用 `defer` 块安排其释放。 然后,通过 `pointee` 属性获取流,并将其传递给 `compression_stream_init` 函数。 编译器在这里做了一些特殊的事情:它使用了 `in-out` 的 `&` 标记,将你的 `compression_stream` 转换为一个 `UnsafeMutablePointer` 类型。或者,你也可以直接传递 `streamPointer`,这样就不需要这种特殊的转换了。 最后,创建一个目标缓冲区作为你的工作缓冲区。 接下来,通过将最终的 `return nil` 替换为: ``` swift // process the input return input.withUnsafeBytes { srcRawBufferPointer in // 1 var output = Data() // 2 let srcBufferPointer = srcRawBufferPointer.bindMemory(to: UInt8.self) guard let srcPointer = srcBufferPointer.baseAddress else { return nil } stream.src_ptr = srcPointer stream.src_size = input.count stream.dst_ptr = dstPointer stream.dst_size = dstSize // 3 while status == COMPRESSION_STATUS_OK { // process the stream status = compression_stream_process(&stream, flags) // collect bytes from the stream and reset switch status { case COMPRESSION_STATUS_OK: // 4 output.append(dstPointer, count: dstSize) stream.dst_ptr = dstPointer stream.dst_size = dstSize case COMPRESSION_STATUS_ERROR: return nil case COMPRESSION_STATUS_END: // 5 output.append(dstPointer, count: stream.dst_ptr - dstPointer) default: fatalError() } } return output } ``` 这是真正执行工作的地方。以下是它的具体操作: - 创建一个 `Data` 对象,用于存放输出内容——这可能是压缩后的数据,也可能是解压缩后的数据,具体取决于当前执行的操作。 - 使用你分配的指针及其大小设置源缓冲区和目标缓冲区。 - 在这里,只要 `compression_stream_process` 返回 `COMPRESSION_STATUS_OK`,就会持续调用它。 - 然后,将目标缓冲区的内容复制到输出中,最终从这个函数返回。 - 当最后一个数据包到达时,它会被标记为 `COMPRESSION_STATUS_END`,此时你可能只需要复制目标缓冲区的一部分内容。 - 在这个例子中,你可以看到一个包含 10,000 个元素的数组被压缩到了 153 字节。效果还不错。 ### Unsafe Swift 示例 2:随机数生成器 随机数对于许多应用来说非常重要,从游戏到机器学习都有广泛用途。 macOS 提供了 `arc4random`,它可以生成密码学安全的随机数。不幸的是,这个函数在 Linux 上不可用。此外,`arc4random` 只能提供 `UInt32` 类型的随机数。然而,`/dev/urandom` 提供了一个无限的、高质量的随机数来源。 在本节中,你将利用新学到的知识来读取这个文件,并生成类型安全的随机数。 ![](/assets/images/20250222UnsafeSwift/hexdump.webp) 首先,创建一个新的 Playground,命名为 RandomNumbers,或者打开项目中的初始 Playground。 确保这次选择的是 macOS 平台。 准备就绪后,将默认内容替换为: ``` swift import Foundation enum RandomSource { static let file = fopen("/dev/urandom", "r")! static let queue = DispatchQueue(label: "random") static func get(count: Int) -> [Int8] { let capacity = count + 1 // fgets adds null termination var data = UnsafeMutablePointer.allocate(capacity: capacity) defer { data.deallocate() } queue.sync { fgets(data, Int32(capacity), file) } return Array(UnsafeMutableBufferPointer(start: data, count: count)) } } ``` 你将文件变量声明为 `static`,这样系统中就只会存在一个实例。你将依赖系统在进程退出时关闭它。 由于多个线程可能需要随机数,你需要通过一个串行的 GCD 队列来保护对它的访问。 `get` 函数是实际执行工作的地方。 首先,创建一个未分配的存储空间,其大小比你需要的多一个,因为 `fgets` 总是以 `\0`(空字符)结尾。 接下来,在 GCD 队列中操作,从文件中获取数据。 最后,通过将数据包装在 `UnsafeMutableBufferPointer` 中(它可以作为一个序列),将其复制到标准数组中。 到目前为止,这只能安全地为你提供一个 `Int8` 类型的数组。现在,你将对其进行扩展。 在你的 Playground 的末尾添加以下内容: ``` swift extension BinaryInteger { static var randomized: Self { let numbers = RandomSource.get(count: MemoryLayout.size) return numbers.withUnsafeBufferPointer { bufferPointer in return bufferPointer.baseAddress!.withMemoryRebound( to: Self.self, capacity: 1) { return $0.pointee } } } } Int8.randomized UInt8.randomized Int16.randomized UInt16.randomized Int16.randomized UInt32.randomized Int64.randomized UInt64.randomized ``` 这为 `BinaryInteger` 协议的所有子类型添加了一个静态的 `randomized` 属性。关于这方面的更多内容,可以查看我们关于协议导向编程的教程。 首先,你获取随机数。然后,使用返回的数组的字节,将 `Int8` 值重新绑定为请求的类型,并返回一个副本。 至此,一切都完成了!你现在以一种安全的方式生成随机数,而这一切的背后正是利用了 Swift 的不安全特性。 ### 下一步要做这么? 恭喜你完成了本教程!你可以通过本教程顶部或底部的“[下载材料](https://github.com/sunyazhou13/Using-Pointers-and-Interacting-With-C)”链接下载完整的项目文件。 如果你想进一步了解 Swift 的不安全特性,还有很多额外的资源可以探索: - **[Swift Evolution 0107: UnsafeRawPointer API](https://github.com/apple/swift-evolution/blob/master/proposals/0107-unsaferawpointer.md)** 这篇文章详细介绍了 Swift 的内存模型,帮助你更好地理解 API 文档。 - **[Swift Evolution 0138: UnsafeRawBufferPointer API](https://github.com/apple/swift-evolution/blob/master/proposals/0138-unsaferawbufferpointer.md)** 这篇文章深入探讨了如何处理未类型化的内存,并提供了从中受益的开源项目的链接。 - **[导入的 C 和 Objective-C API](https://developer.apple.com/documentation/swift/imported_c_and_objective-c_apis)** 这部分内容可以帮助你了解 Swift 是如何与 C 语言交互的。 希望你喜欢这个教程!如果你有任何问题或想要分享的经验,请随时在论坛中讨论! # 总结 以上是去年欠下的技术债.今天要还上,这里介绍的额unsafe swift中操作内存的方函数方法 值得大家深入学习,虽然翻译的过于机器化,等抽空我重新整理一下. [原文链接Unsafe Swift: Using Pointers and Interacting With C](https://www.kodeco.com/7181017-) URL: https://sunyazhou.com/2024/12/FinalSummary/index.html.md Published At: 2024-12-31 23:53:00 +0000 # 2024年终总结 ![](/assets/images/20241231FinalSummary/banner.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! --- 清平乐·2024年终回顾 岁末天寒,回首流年远。 风霜雨雪征途漫,梦想未曾改变。 经济通缩卷起,尽显人之无力. 裁员压榨凋敝, 前途渺茫寥寂. > 2024年冬纪念那些曾毫无人文主义精神和艺术气质的企业在互联网的浪潮变革洗礼中对待底层打工人惨无人道的杀戮. --- 这一年除了时刻面临被裁的风险外几乎生活过得毫无波澜,两鬓白发诉说着平静毫无追求的日子,这是缝缝补补的一年,几乎所有人都对自己的开支控制的非常到位,能花少部分钱修的指定不花钱换,纵观多年前所未有之经济现象,大家都在疯狂攒钱拒绝消费,迫不得已消费的话也只是"口红经济"的情况而已. 当对前途毫无期待的人们失去对未来经济向好的信心,那生活会过的毫无追求和期待,只能和现实写照下的自己妥协,勉强活着, 那样的人们中就有我一个. 在这种复杂的局面下来回首一下这一年中的人和事. ## 2024年回顾 * 国际形势 * 生活 * 工作 * 学习 * 爱好 * 买房 ## 国际形势 懂王再次当选总统 估计又要开启开挂的4年任期 只要地球不爆炸我都可以接受,毕竟还得是懂王,我预测如下的事情会发生 * 俄乌战争终结者 (到底谁终结谁不好说) * 中美之间贸易摩擦加剧 * 巴以地区迎来懂王的和平妥协 (不知道怎么个妥协方式,毕竟美国背后那是犹太财阀) * 继续搞事情 忍看销烟之地重生战火,痛惜负伤之兵再举刀枪. 战火指定不断. 其它的大家也都能想到,只有一点我猜测大家想不到,美国的各种职能部门也面临裁员,因为降本增效的马斯克当上了效率部门的大佬,这下好了.原本裁员只有互联网能有的事这一幕搬到了美国各种部门,什么CIA,什么国防部,什么五角炮楼该撤撤该裁裁. 看着得劲,过瘾. 我现在想到的一幕是这些大佬被裁员了要是送卖外那场面估计挺炸裂. 毕竟也要为自己的生计着想吗 纵然你有百万刀乐 马斯克各种改革给退休金给你降本增效了我看这些曾经不可一视的人物能不能变成人名, 想想挺有意思,世界总是这样变化莫测,即便这些曾经的大人物对此表示不满的话发Twitter 我看他会不会被封号.那X可是musk说了算.这招干的釜底抽薪让你没脾气. 话说俄乌战争仍在继续.给我最直观的感受是 92#汽油从开战前的6.2元/L干到现在高高在上的7.9元/L,这仗打的我生活方方面面都受到了不同程度的影响,赶紧懂王上位24小时终结了吧! 我国的反应这一直是个MG话题,总体来说就是拿老百姓的生活硬扛,房地产泡沫破裂,许家印变成阶下囚.各种房地产破产跑路,国内经济持续低迷,失业率严重,经济就在不稳定的边缘反复试探,这一年发生太多让百姓难以承受的苦,地产商卷钱跑路,百姓掏空6个钱包买的房子没了兑现,百姓想稳定赚钱,各种企业裁员没有了工作,百姓想消费拿空气消费么? 百姓想回乡创业搞种植,各种农产品的价格低到比肥料都便宜,百姓想搞养殖,看看 现在这猪肉价格 10几元/斤 肉价比饲料都便宜,想送外卖 有年龄限制,然后行业饱和.... 写到这里我想起来 邓公那句话"中国人民已经承受了太多太多,他们已经够勤奋了,够吃苦耐劳了",能不能给普通老百姓一个活路,非要等着百姓每个人都还不上房贷,一家老小上街乞讨才是我们追求的特色社会主义现代化吗? 我们已经走了很长一段时间的榨取型社会主义制度,然而就目前为止也没有停下的脚步的迹象。无论劳动者奉献多少也拿不到社会资源分配的公平那部分,奉献少了只能裁员。 ## 生活 疫情自去年年初时大家阳的阳,全民免疫开始,逐渐退出了时代舞台,3年的煎熬终于有了结果,仿佛打醒了人们以后一定攒够足够的钱,应对长时间突发并持续时间长的大灾大难. 疫情的结束也标志着大家又能回归正常生活,这让我思考了几个问题 ### 如何才能振兴家族? 我的思考如下: **一个家族贫穷的真正原因是整个家族没有一个格局大的引路人从商或者从政.老人没有办法为懵懂的下一代做规划,自己也不注重格局,一辈子都在给别人打工,家族里的长辈自私自利,不重视下一代的教育,等老了又利用孝道来绑架子女,而子女之间也是各干各的,彼此之间不团结,一点利益就争吵不休,犹如一盘散沙,不懂得整合优势资源**——黄宏升 **任何一个优秀的家族都不是靠一代人的努力,而是靠几代人的相互激励,共同努力,如果祖辈打下的基础好,只需言听计从,维持现状即可日渐佳境.如果父辈都是普通人,就需要我们这一代人去努力耕耘,千万不要寄希望于下一代,我们混好了下一代才有可能更好.这一代混得不好.下一代大概率会更难.三代望有贵族之崛起,历代之蜕变,皆需脱皮一层,皆需历代之力拓.** 所以就会得出以下结论: 最可怜的下一代人是没家底有家教. 最危险的下一代人是有家底没家教. 最厉害的下一代人是既有家底又有家教. 当然最失败的下一代当然是既没家底也没家教. 正是不同的教育环境让自己的后代和别人的后代差距越拉越大,而且后代逆袭的概率还越来越低. ### 当我们面临生存危机时谁能为我们的生活兜底? **回顾这些年,我似乎总是手握一副不尽如人意的牌,却始终在奋力打好。从出生的那一刻起,到接受教育的历程,再到买房置业直至如今抚养孩子,我逐渐有了一种深刻的领悟。拥有父母,有时是福祉,有时也是负担。福祉在于,如果他们拥有退休金,那么在我失业或经济困难时,他们能为我提供基本的生活保障。然而,当父母与我的年纪差距较大时,问题便显现出来。本应是他们年富力强、帮助我照顾孩子的阶段,却因年龄和健康的限制,使得他们在带孩子时显得力不从心。这种与同龄人父母身体强健的对比,让我产生了难以承受的心理落差。** 这种苦涩的生活经历,既非我的过错,也似乎与我有关。非我的过错在于,我无法选择自己的出生时机和父母的年纪。而我的过错则在于,我未能积累足够的财富来摆脱这些困境,实现真正的独立,不再依赖他们。但生活总要继续,我深知唯有努力活下去,避免自身重蹈覆辙,才是最优解。因此,我将这段经历视为`磨练灵魂的历程`,从未停止过自我提升与成长。Keep honing. ### 曾经熟悉的朋友或同学目前都在做什么? 每当我踏上地铁,拿出手机打开iBook看《Metal by Tutorials 4th 2023》时,看着大家都前所未闻的图形图像技术专业书籍,对着冰冷的技术点和着色器代码.我时常在想 身边真有像我这样的人每天研究奇奇怪怪的晦涩难懂的函数的人吗?他们这个时候都在干啥? 那时候感觉自己 无比孤独, 也许就像《北派盗墓笔记》中 秦西达说项云峰:"强者注定孤独..." ## 工作 现在的工作已经不是稳不稳定的问题了,是啥时候等着被裁领大礼包准备研究干啥了,每天都在继续钻研和准备找工作之间挣扎徘徊.对工作失去了信心,毕竟大家的遭遇几乎和我好不到哪去,对于目前的公司我只能说我还能对付干,因为我不确定到底是它干废还是我干走哪个先发生. 同事质量真的是一年不如一年,如果不是非核心部门的公司也许同事质量不高,已经很难回到曾经我在百度时遇到那些NB的同事,没事教我写写文章博客,练练书法写写代码.已经找不回来了.这可能和当下的经济形势有关, ### 让我最震惊的一件事 清华大学毕业的百度前同事涛兄不得不再次在我的年终总结出镜,就是那个数学120分高考打119分那一分因为卷面整洁度不够扣一分(可有可无的), 今年听说他被裁员了,让我十分震惊, 除了安慰他以外让我不得不 重新认识 考个好大学,好学历现在还有用不. 现在本科或研究生找不到工作,让我产生了对读书是否有用的思考。读书到底有没有用我不知道,但积累无形的资产,形成自己的认知、观念和方法论需要它,它会潜移默化的改变我们的一生,,也许你并不知道知识的用处,但你一定承受不了无知的代价。读书的过程也许是对自己的磨炼和塑造,培养克服困难的品质。 不过有一件事我们似乎忽视了,读书没有教我如何赚钱,毕竟这东西没有写到书本里.这个就是主要原因为什么上述种种发生的事情苦寻无果, 我相信这是所有人都部署的研究课题. 工作目前就个球样,年纪大待遇差. ### 值得欣慰的一件事 今年Q1拿了一个 卓越个人逐光奖勉勉强强能在年终总结提一提 ![](/assets/images/20241231FinalSummary/award1.webp) ![](/assets/images/20241231FinalSummary/award2.webp) 给的经费也就2k 请大家吃了顿饭胡乱的报销了,啥用没有. 哦 还给一捆鲜花 仅此而已. 好久没拿奖了.距离上次好像是在快手时拿到的优秀讲师吧!惭愧惭愧 拿这个奖项主要是发布了一篇《酷我音乐iOS小组件适配开发实践 》文章到`QQ音乐技术团队`的微信公众号,没想到能现在各大搜索引擎只要搜索小组件开发就有我的文章,也算一点点鼓励吧! #### 能修尽量不换新的物件 几年前在得物上买了 一双AJ1 球鞋,因为常年的穿着,导致前头皮质材料老化, 问了各种修鞋店都弄不了,无奈拼多多找个专业维修aj1球鞋的师傅,花了150元左右修好了,效果不错 ![](/assets/images/20241231FinalSummary/aj1.webp) 还有一个AirPod Pro耳机还是快手2019年春晚直播活动时 配发的,这么多年过去,内部扬声器喇叭好像脱落开胶了一样,在地铁学习时总响,于是拼多多找了个专业维修师傅 50元搞定,找苹果官方答复是要么直接换新,否则无法维修. ![](/assets/images/20241231FinalSummary/airpod.webp) 这两件个人物品完全提现了2024年的我缝缝补补,没办法消费降级,我的全部财富都被政府一次性收割换房了,穷人的无奈... ## 学习 ### C++的持续学习 ![](/assets/images/20241231FinalSummary/Cplusplus.webp) 在地铁上的零碎时间里,我竟然意外地完成了一整本C++的学习。合上书的那一刻,我仿佛站在了一座宏伟的知识殿堂的门前,门后是无数条蜿蜒的通道,每一条都通向更深的学问,每一条都需要我投入更多的时间和精力去探索。我感觉自己正站在一棵大树的根节点上,正准备沿着树的分支,一步步深入到每一个子节点,甚至是最末端的叶子节点。在这个过程中,我只有前驱和后继的指引,它们将带领我继续在编程的世界中前行和探索。 ### Metal图形图像的学习 当完成了上一本,紧接着就开始下一本《Metal.by.Tutorials.4th.2023.12》Apple平台的图形图像. ![](/assets/images/20241231FinalSummary/Metal.webp) 当我读到这本《Metal》计算机图形学书籍的一半时,我深刻地意识到,我在大学时期所学的计算机知识不过是浅尝辄止、浮皮潦草,对于图形学这一领域更是未曾深入探究,这让我感到自己浪费了宝贵的时光。现在,我才明白,这才是真正的计算机技术精髓。如果在我的职业生涯早期,能有一位导师为我指引方向,或许我就不会像现在这样,时刻担心着被裁员的命运。 为了弄清楚Metal提供的API,我尝试写了几篇纸,弄清楚 queu、command buffer、 command encoder、pipeline buffer object... ![](/assets/images/20241231FinalSummary/Metal01.webp) ![](/assets/images/20241231FinalSummary/Metal02.webp) > 画的过于潦草献丑了. 正如牛顿所说,他之所以能看得更远,是因为他站在了巨人的肩膀上。我渴望努力成为那个巨人,让更多的牛顿看的更远。 这本书非常有挑战性.全英文,也着实锻炼了一下我很久不使用的英文阅读理解技能,适当的时候用一下iOS自带翻译,不过这都是我理解一遍和翻译对照一遍,当我查看翻译和我理解的内容做diff时我能明显感觉自己的阅读理解在逐步提高,只是这本书的作者我实在不能苛求它用标准的英式英语,它说的很多内容我都未听过然而又可以成为一种不常用的名词理解形式而存在. 在学习这本书的时候我不得不把线性代数跟随着一点点学习了一下[《线性代数》正课,零废话,超精讲!【孔祥仁】](https://b23.tv/MHAsU12) 因为图形学大部分都是线性代数范畴,这属于被动跟随学习. 刚开始的讲的二阶行列式大概和我们使用的矩阵运算有直接关联.因为在3D的世界里对一个物体执行 位置平移、旋转、缩放(仿射变换)都需要用到行列式类似的式子. 这几乎可以说是图形学的基础,如果后面编写着色器时对其内部的函数不了解会很懵,这些都是线性代数在背后做理论支撑的. 这本书让我最印象深刻的知识点或者说是我的知识盲区是我对光照的理解,原来很多前辈在这方面做了很多研究eg:[Phong light model 冯氏光照模型](https://www.cs.utexas.edu/~bajaj/graphics2012/cs354/lectures/lect14.pdf)论文,菲涅尔光照模型等. 基于这些论文提供的公式理论,可以处理光照的反射和漫射以及环境光遮挡等技术(在着色器中写的代码就是基于这种公式),比如灯光模型分为 聚光灯、点光源、直射光(太阳光),基于这些光照基础,才最终衍生出基于 `[BPR](物理的渲染技术)` 甚至后来的多次反射形成的光线追踪技术(Ray Tracking). 说真的就是这一本简单的书,触动过我心灵深处的认知N次,也让我不断觉醒持续学习这种图形学. **进步的时候也许就是工程师最快乐的时候吧** (当然财富自由也是),显然研究图形图像有时候很晦涩,对于目前当下的我们来说赚更多的钱基本没有什么可能. 研究一些深度的技术也许是一件苦中作乐的事. 对于学习这件事,不知大家是否和我一样,年初的时候打算学一堆东西,然而经常被其它高优先级的事情抢占式调度,导致最后啥也没学到,最后年终也没啥说的,于是2024年初的时候我改变了思路, 不管怎么样先尽力专心研究一样技术,完成后再研究下一个.如果这中间实在是感到无聊和百无聊赖暂时找点有意思的事,等缓解过后再循序渐进的恢复到之前的学习方向上.经过一年的实践这种方式很有效果. 我把这种方式称为`线性偶发被干扰学习法` ## 爱好 2020年左右,那时候我感觉我有一种抑郁症,对北京的工作毫无追求, 其实那时候自己没有察觉,如果有认知的提升也许会好一些, 然而现在会想起来还是觉得当时自己没有找到一种方式解决这种抑郁症的情况, 直到后来遇到了台球,并且不断的深入研究,看比赛赛事熟悉规则,实践过程中学习B站的教程 现在还在坚持热爱并一直没有放弃,现在的我打球已经不能说是打球了,是在逐渐了解球房的经营方式,运维成本,器材的品牌和维修相关, 想把这个发展成为第二曲线的前提首先要对它有个全面的了解,这样才知道这门生意能不能做,做需要多少投入,自己就是一名台球爱好者,并且总是尝试一些技术型思维理解台球行业的发展. 这门生意我看好的原因有两个 * 当下的经济通缩背景下普通人群很少消费 * [口红经济](https://baike.baidu.com/item/%E5%8F%A3%E7%BA%A2%E7%BB%8F%E6%B5%8E/1011374) 的作用 因为现在经济形势较差,当工作不稳定时收入变的非常重要,大家不敢消费,只能用少的钱满足消费需求,这种台球经济非常符合这种经济背景下的消费方式.花一点钱能带来很多快乐那种. ![](/assets/images/20241231FinalSummary/daiyong.webp) 台球对我来说非常治愈,当我工作压力很大时这种运动非常优雅,经过了2年多的练习,对台球有个基本认识,前同事从新加坡回国我用台球跟他分享了一下一个人如何战胜背井离乡举目无亲的孤独感. ![](/assets/images/20241231FinalSummary/wufendian.webp) 人生如同那颗白色的母球,在五分点的挑战中,三点一线,瞄准袋口。当我们击球的质量不佳,或是击球点不准确时,这些失误都会反馈到白球的轨迹上。随着我们不断寻找正确的击球点,纠正自己的错误,我们就能调整正确的生活方向,使人生重回正轨。 ### 我对台球爱好的沉思 有时候,我会陷入深思:人究竟是否需要拥有爱好?爱好和梦想一样,往往需要金钱的支撑。当个人的实力与梦想不相符时,爱好是否变成了一种奢侈? 首先,爱好无疑能够满足我的情绪价值,为我的心灵带来愉悦和满足。它能够激发我们的热情,让我们在忙碌和压力之中找到慰藉。 其次,爱好有助于塑造个人的独立性格。通过追求自己的兴趣,我们能够发现自我,培养独特的个性和创造力。 再者,爱好完全可以在经济能力允许的范围内进行。它不必是昂贵的,重要的是找到那些能够带来快乐和成就感的活动,而不必过分追求物质上的投入。 最后,如果没有爱好,我不禁要问,人活着究竟是为了忍受生活的苦涩,还是为了追求那些让我们坚持不懈的目标?爱好不仅丰富了我们的生活,也是我们追求幸福和满足感的重要途径。 ### 乔氏在推广中式台球加入奥运会成为一项体育赛事 曾经我们顶着痞子运动的帽子,在街边在巷尾任汗水挥洒,对于出身高贵的斯诺克 9球,我们只能仰视,枉自嗟呀,我们犹如石缝中的小草,顶着难以出头的重压,我们卑微弱小被冷眼歧视,甚至险些被无情扼杀,丁俊晖有了自己的别墅,潘晓婷的跑车也很豪华。可我们什么都没有,只有工作之余苦苦的练球浪迹天涯,我们用自己来证明这项运动的存在,尝尽人间的酸甜苦辣,因为热爱使我们割舍不下,因为梦想使我们内心无比强大,我们咬牙坚持,孤独练球,令多少人泪目激情迸发,一位大叔听到了我们痛苦的呐喊,乔氏扛起大旗与我们并肩拼杀,乔氏让我们充满了无尽的希望,我们让乔氏奋力拼搏,永不放弃,绝不倒下,一群不像命运低头的叛逆者活出了一口气,重压下挥出的每一杆都是对命运无声的击打。而这位老人却把这项运动推向了世界奥运的壮举成为了佳话,从此我才知道原来台球运动可以这样伟大。乔氏中式8球大师赛已经18岁了,它终将在世界体育赛事中勇闯天涯,为我们这些卑微的台球爱好者抹去那痞子运动的帽子的同时把这项运动的光辉留在世界台坛的聚光灯下。 ### 台球的未来 这项运动让我感受到了前所未有的公平竞技,全凭实力说话,其中包含很多技术技巧和知识, 普通人看来估计也就是几根木头棍子加一张绿色台泥的案板而已, 其实这里面有很多机会等待挖掘,比如生产质量最好的胶边来自[中国振宏胶边](http://www.j-hrubber.com/)(就是球岸的橡胶条),生产的好台尼的厂家中国的[利百文](https://www.liberwintex.com/)和[英国世创6811](https://strachan.co/zh-hans/), 球杆各种品牌都被职业选手充值了,不过也有很多好的, 比如 皮尔力、李斯球馆、兰蔻各种品牌就和汽车一样应有尽有, 这里面还有一些生产皮头和巧粉的企业,比如小麦皮头,HR巧粉等.其实不深入这行业根本不了解这行业的供应链,甚至青石板的供应商、比利时进口的雅乐美TV球. 最近这几年很流行的 互义灯具(专门生产台球球台上方的无影灯企业) 还有像盛利者沙发,专门生产 台球专用沙发. 这是一门内行人的生意,我们总说自己在目前的就业压力下很难找到自己的第二曲线,我想说无论啥曲线都需要花时间和精力去了解和深入研究学习,哪个曲线都是留给有准备的人. ## 买房 年初的时候在北京大兴区买了一套共有产权房,首先随着房地产政策逐渐放开,各种政策的刺激下,我不得不考虑在北京长期生活 先说说我都赶上哪些政策了吧 * 限房不现商贷(之前的政策:就是如果你在全国各地只要有一处房产有贷款,基本都是商贷,在北京就会首付80%才能买房, 这项政策放开后,仅限北京地区的商贷了)也就是说 在外地用过商贷和在北京买房没有直接限制了. * 监管账户 (首付款直接打到开发商和政府监管的账户,防止房地产商暴雷 烂尾) 这也是这几年经常出现的问题导致的政府不得不出手 整治开发商的政策之一,资金紧了监管账户 只有封顶才能给开发商放款的操作也着实击溃了很多烂尾楼地产商. * 利率下调 公积金利率 5年期以上 2.85% 这利率 已经远比之前的商贷 4.9%低太多了. * 绿色建筑公积金额度上浮 (这个和开发商建设有关系 建设的符合国家规定的二级建筑 ) 这个我最后没用上 * 北京五环外 连续社保 3年即可,这个我也没用上,我用的是工作居住证没有这个限制. ... 后来又出了很多挤牙膏的政策我就不一一列举了,我就说一点大家看不到的背面吧: 在北京买房如果你用公积金贷款 那么你要保证你半年内不失业,因为北京公积金从封顶到交房 也就一个月内就要办好,我从年初分组摇号选房到最后办完公积金放款用了整整半年, 前几个月是房子封顶,最后一个月是办公积金,我想说这中途要是失业的话公积金状态变成封存那么恭喜你估计啥房贷你也办不下来了,你几乎被社会抛弃了一样,商贷看你没有稳定工作不敢放贷给你,公积金账户封存得下家公司给你缴纳时才能给你状态变成缴存,你怎么保证你在短时间内找到一份能给自己缴纳五险一金的公司,这不确定性太强了吧! 并且你通过代缴的方式缴纳好像也不靠谱吧! 总之我觉得在目前的经济形势很差劲的时候没有勇气和实力 不要尝试这个操作,非常差劲. 还好我目前的公司没有给我大礼包整走,否则我就是在拿自己当例子给各位讲笑话了,看到这各位以为我在讲笑话,我想说这就是非常客观的现实没有一点玩笑, 这种方式我认为可以给国家提提意见,这种方式对失业群体非常不友好,谁能保证自己工作很稳定,在当下没有人能敢保证自己稳定, 这种状态下去买房谁会去买那不有病吗? 工作收入不稳定有几个人愿意冒险一试找刺激. ![](/assets/images/20241231FinalSummary/gjj.webp) 经过此次买房让我觉得好险好险,没钱的穷人在北京买房真的是一项巨大的挑战. 真的不知道失业和明天哪个先来!没有父母为自己生活兜底,这个国家又一次性拿走年轻人的全部收入甚至掏空父母的钱包,就这样还能有什么消费,经济岂能向好,经济只能停滞不前. # 总结 2024年我真切的感受到了自己很穷,能修好的东西绝不会消费去买新的,这一年即便世界格局如何变化动荡,生活在夹缝中的自己 一直在左右摇摆中绝处逢生. 2024年发表了很多很水的文章,有的不值得一提完全就是防止忘了找不到,工作很忙加上家里小朋友占据了很多业余时间,我现在都觉得学习最好的地方是 地铁8号线. 上班的路上来回两个小时至少50%的时间不是消费给垃圾视频,而是在钻研技术,虽然别人看着毫无乐趣, 其实自己这算是在苦中作乐吧!全当行业能继续发展给自己的技术护城河挖挖更深的沟渠.虽然目前也没有什么最优解能解决此问题,不过还在尝试新的方式寻找最优解 2024年工作中卷出新高度在目前的时代背景下成为了流行趋势,未来几年估计比这更差劲,我几乎是不报什么好的希望的消极社会分子. 差不多就到这吧! 2025年 总要有所期盼, 我的期盼是把Metal学完用起来就这么简单. 因为贫穷改变不了,现状改变不了,财富自由那是做梦,赚更多钱那也几乎扯淡,唯一可控的是自己还能坚持不懈的学习提高认知 才是解决上述问题的最优解. 最后放上我在《盐铁论》读到最有用的两句话吧! ![](/assets/images/20241231FinalSummary/yantielun.webp) 2024再见. URL: https://sunyazhou.com/2024/11/coretextcalculatedheight/index.html.md Published At: 2024-11-06 02:25:00 +0000 # CoreText渲染字体的时如何计算字体所需要的高度? ![](/assets/images/20240727Magnificationgesture/SwiftUI.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! 主要是使用 `CTFramesetterSuggestFrameSizeWithConstraints` 计算文本的高度和宽度,记得要给`CTFramesetterRef` 设置相关的属性(行间距和自动换行)。以下是使用 CoreText 绘制并计算文本高度的 Objective-C 代码示例: ``` objc - (void)calculatedHeight:(CGSize)bounds { NSString *text = @"This\nis\nsome\nmulti-line\nsample\ntext."; UIFont *uiFont = [UIFont fontWithName:@"Helvetica" size:17.0]; CTFontRef ctFont = CTFontCreateWithName((CFStringRef) uiFont.fontName, uiFont.pointSize, NULL); // 设置行间距 CGFloat leading = uiFont.lineHeight - uiFont.ascender + uiFont.descender; CTParagraphStyleSetting LineSpacing; LineSpacing.spec = kCTParagraphStyleSpecifierLineSpacingAdjustment; LineSpacing.value = &leading; LineSpacing.valueSize = sizeof(CGFloat); // 设置换行模式 CTParagraphStyleSetting lineBreakMode; CTLineBreakMode lineBreak = kCTLineBreakByCharWrapping; lineBreakMode.spec = kCTParagraphStyleSpecifierLineBreakMode; lineBreakMode.value = &lineBreak; lineBreakMode.valueSize = sizeof(CTLineBreakMode); CTParagraphStyleSetting paragraphSettings[] = {lineBreakMode,LineSpacing}; CTParagraphStyleRef paragraphStyle = CTParagraphStyleCreate(paragraphSettings, 2); CFRange textRange = CFRangeMake(0, text.length); CFMutableAttributedStringRef string = CFAttributedStringCreateMutable(kCFAllocatorDefault, text.length); CFAttributedStringReplaceString(string, CFRangeMake(0, 0), (CFStringRef) text); // 设置字体行间距和大小 CFAttributedStringSetAttribute(string, textRange, kCTFontAttributeName, ctFont); CFAttributedStringSetAttribute(string, textRange, kCTParagraphStyleAttributeName, paragraphStyle); CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString(string); CFRange fitRange; // 计算文本需要的高度 CGSize frameSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, textRange, NULL, bounds, &fitRange); CFRelease(framesetter); CFRelease(string); } ``` # 总结 记录一些容易遗忘的代码 URL: https://sunyazhou.com/2024/11/armthreadstate/index.html.md Published At: 2024-11-04 02:31:00 +0000 # iOS的dSYM中ARM Thread State寄存器有哪些? ![](/assets/images/20240727Magnificationgesture/SwiftUI.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! # 背景 很久以前记得很多人经常面试喜欢问 iOS的dSYM中ARM Thread State的寄存器有哪些,分别代表什么意思? 基于一段数据 我们简单记录一下这个问题 ``` sh Thread 0 crashed with ARM Thread State (64-bit): x0:000000000000000000 x1:000000000000000000 x2:0x000000016bd31ce0 x3:0x000000016bd31d20 x4:0x0000000000000010 x5:0x00000000000022e0 x6:0x00000002817762e0 x7:0x00000000000000f0 x8:0x0000000281f7b930 x9:0x00000000000006bb x10:0x000000018aa49cf8 x11:0x00ff0001238cf400 x12:0x00000000000000b5 x13:0x00000001238cff40 x14:0x02000001e0ad6c89 x15:0x00000001e0ad6c88 x16:0x00000001d8922e40 x17:0x00000001122301e8 x18:000000000000000000 x19:0x000000016bd31cc0 x20:000000000000000000 x21:000000000000000000 x22:0x000000011222c21a x23:0x000000018652ddb0 x24:000000000000000000 x25:0x000000028115e880 x26:0x000000010d8ca6ed x27:0x0000000281328930 x28:0x0000000000000001 fp:0x000000016bd31c60 lr:0x0000000112216898 sp:0x000000016bd31c30 pc:0x00000001d8922e44 cpsr:0x0000000060001000 ``` 这是一段线程崩溃后的的 主线程crash 的线程状态 ## ARM Thread State寄存器含义 在iOS的dSYM中,ARM Thread State寄存器提供了程序崩溃时的上下文信息,以下是一些主要寄存器的含义: - **x0-x30**:这些是通用寄存器,用于存储临时数据。在函数调用中,x0-x7通常用于传递参数,而x19-x28则用于保存局部变量和函数的返回地址。 - **fp (Frame Pointer)**:帧指针寄存器,通常用于指向当前函数的栈帧,以便于函数调用和返回时栈的管理和访问局部变量。 - **lr (Link Register)**:链接寄存器,存储着函数调用返回后的地址,即下一个要执行的指令地址。 - **sp (Stack Pointer)**:栈指针寄存器,指向当前线程的栈顶。 - **pc (Program Counter)**:程序计数器,指向下一条要执行的指令地址。 - **cpsr (Current Program Status Register)**:当前程序状态寄存器,包含处理器的状态和控制位,如条件标志位、中断使能位等。 在您提供的崩溃日志中,pc寄存器的值为`0x00000001d8922e44`,这通常指向导致崩溃的指令地址。您可以使用`atos`命令结合dSYM文件来解析这个地址对应的代码位置,例如: ``` bash atos -arch arm64 -o YourApp.app.dSYM/Contents/Resources/DWARF/YourApp -l 0xXXXXXXXX 0xXXXXXXXX ``` 其中-arch arm64指定了架构,-o后面跟着的是dSYM文件的路径,-l后面跟着的是线程号和pc寄存器的值。这样可以帮助你找到崩溃发生时正在执行的代码行。 ## 如何解析ARM Thread State寄存器中的`pc`值? 解析ARM Thread State寄存器中的`pc`(Program Counter)值通常涉及到以下几个步骤: 1.**获取崩溃时的`pc`值**: 这是导致程序崩溃的指令的内存地址。在你的崩溃日志中,`pc`的值是`0x00000001d8922e44`。 2.**获取应用程序的dSYM文件**: dSYM文件包含了应用程序的调试信息,包括符号表,这些符号表将内存地址映射到源代码中的函数和行号。确保你有对应崩溃时应用程序版本的dSYM文件。 3.**使用调试工具解析地址**: 你可以使用Xcode的调试工具,或者命令行工具如`atos`(Address to Symbol)来将`pc`值转换为源代码中的函数名和行号。 使用`atos`的命令行示例如下: ``` bash atos -arch arm64 -o YourApp.app.dSYM/Contents/Resources/DWARF/YourApp -l 0xXXXXXXXXXXXXXXXX 0xXXXXXXXXXXXXXXXX ``` 其中: * `-arch arm64` 指定了架构类型。 * `-o` 后面跟着的是你的应用程序的dSYM文件路径。 * ` -l` 后面跟着的是线程号和pc寄存器的值。 请将YourApp.app.dSYM/Contents/Resources/DWARF/YourApp替换为你的实际dSYM文件路径,将0xXXXXXXXXXXXXXXXX和0xXXXXXXXXXXXXXXXX替换为你的实际线程号和pc值。 4.分析结果: atos命令会输出导致崩溃的函数名和行号。例如: ``` bash 0x00000001d8922e44: -[YourViewController yourMethod] (YourViewController.m:123) ``` 这表明崩溃发生在YourViewController类的yourMethod方法中,位于YourViewController.m文件的第123行。 5.调试和修复: 根据解析出的函数名和行号,你可以在源代码中定位到具体的代码位置,进一步分析和修复导致崩溃的问题。 # 总结 记录一下容易被遗忘的内容, 帮助修复工程中crash的问题 URL: https://sunyazhou.com/2024/10/swiftuiimage/index.html.md Published At: 2024-10-28 10:35:00 +0000 # SwiftUI中的Image修改器(视图修饰符)有哪些? ![](/assets/images/20240727Magnificationgesture/SwiftUI.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! 在SwiftUI中,Image视图可以用来显示图片资源。你可以通过多种修改器(即视图修饰符)来改变Image的外观和行为。以下是一些常用的Image修改器: * 1.`nterpolation`:设置图片的插值方式,用于定义图片缩放时的渲染质量。 ``` swift Image("example") .interpolation(.medium) ``` * 2.`resizable`:使图片可拉伸,通常与.aspectRatio结合使用以保持图片的原始宽高比。 ``` swift Image("example") .resizable() ``` * 3.`aspectRatio`:设置图片的宽高比,可以是固定值或从图片本身继承。 ``` swift Image("example") .aspectRatio(contentMode: .fit) ``` * 4.`frame`:设置图片的固定大小。 ``` swift Image("example") .frame(width: 100, height: 100) ``` * 5.`clipped`:决定是否裁剪图片以适应指定的尺寸。 ``` swift Image("example") .clipped() ``` * 6.`antialiased`:设置是否对图片进行抗锯齿处理。 ``` swift Image("example") .antialiased() ``` * 7.`renderingMode`:改变图片的渲染模式,比如.original、.template等。 ``` swift Image("example") .renderingMode(.template) ``` * 8.`opacity`:设置图片的透明度。 ``` swift Image("example") .opacity(0.5) ``` * 9.`overlay`:在图片上覆盖另一个视图。 ``` swift Image("example") .overlay(Circle().foregroundColor(.red)) ``` * 10.`background`:在图片背后设置一个背景颜色或另一个视图。 ``` swift Image("example") .background(Color.gray) ``` * 11.`clipShape`:将图片裁剪成特定形状,比如圆形、矩形等。 ``` swift Image("example") .clipShape(Circle()) ``` * 12.`mask(shape:)`:使用另一个视图的形状来裁剪图片。 ``` swift Image("example") .mask(Circle()) ``` * 13.`padding`:在图片周围添加内边距。 ``` swift Image("example") .padding() ``` * 14.`contentShape`:为图片指定一个隐含的轮廓,这在交互设计中很有用。 ``` swift Image("example") .contentShape(Rectangle()) ``` * 15.`onTapGesture`:为图片添加点击手势识别。 ``` swift Image("example") .onTapGesture { print("Image tapped") } ``` * 16.`scaledToFit()`:将图片缩放以适应其父视图,同时保持其宽高比。 ``` swift Image("example") .resizable() .frame(width: 100, height: 100) ``` * 17.`scaledToFill()`:将图片缩放以填充其父视图,可能会裁剪图片的某些部分。 ``` swift Image("example") .resizable() .scaledToFill() ``` * 18.`cornerRadius(radius:)`:给图片添加圆角。 ``` swift Image("example") .cornerRadius(10) ``` * 19.`foregroundColor(color:)`:设置图片的前景色,通常用于模板图片。 ``` swift Image("example") .renderingMode(.template) .foregroundColor(.blue) ``` * 20.`symbolRenderingMode(mode:)`:用于系统图标,设置图标的渲染模式。 ``` swift Image(systemName: "wifi") .symbolRenderingMode(.hierarchical) .foregroundColor(.blue) ``` * 21.`foregroundStyle(style:)`:用于系统图标,设置图标的多层次颜色。 ``` swift Image(systemName: "wifi") .foregroundStyle(Color.pink, Color.green) ``` * 22.`visualEffect(effect:)`:在不破坏当前布局的情况下,直接在闭包中使用视图的GeometryProxy,并对视图应用某些特定的修饰符。 ``` swift Image("example") .visualEffect { content, geometryProxy in content.offset(x: geometryProxy.frame(in: .global).origin.y) } ``` # 总结 这些是Image视图最常用的一些修饰符,记录一下 URL: https://sunyazhou.com/2024/10/hangmonitor/index.html.md Published At: 2024-10-22 05:01:00 +0000 # iOS卡顿监控代码 # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! 监控原理是注册runloop观察者,检测耗时,记录调用栈,上报后台分析。长时间卡顿后,若未进入下一个活跃状态,则标记为卡死崩溃上报。 以下是一个 iOS 卡死监控的代码示例: ``` objc // // MTHangMonitor.h // HangTest // // Created by sunyazhou on 2024/10/22. // #import #import #import #import NS_ASSUME_NONNULL_BEGIN // 定义 Runloop 模式的枚举 typedef enum { eRunloopDefaultMode, // 默认模式 eRunloopTrackingMode // 追踪模式 } RunloopMode; // 全局变量,用于记录 Runloop 的活动状态和模式 static CFRunLoopActivity g_runLoopActivity; static RunloopMode g_runLoopMode; static BOOL g_bRun = NO; // 标记 Runloop 是否在运行 static struct timeval g_tvRun; // 记录 Runloop 开始运行的时间 // HangMonitor 类,用于监控卡死情况 @interface MTHangMonitor : NSObject @property (nonatomic, assign) CFRunLoopObserverRef runLoopBeginObserver; // Runloop 开始观察者 @property (nonatomic, assign) CFRunLoopObserverRef runLoopEndObserver; // Runloop 结束观察者 @property (nonatomic, strong) dispatch_semaphore_t semaphore; // 信号量,用于同步 @property (nonatomic, assign) NSTimeInterval timeoutInterval; // 超时时间 + (instancetype)sharedInstance; - (void)addRunLoopObserver; // 添加 Runloop 观察者的方法 - (void)startMonitor; // 启动监控的方法 - (void)logStackTrace; // 记录调用栈的方法 - (void)reportHang; // 上报卡死的方法 @end @implementation MTHangMonitor // 单例模式,确保 HangMonitor 只有一个实例 + (instancetype)sharedInstance { static MTHangMonitor *instance; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ instance = [[MTHangMonitor alloc] init]; }); return instance; } // 初始化方法 - (instancetype)init { self = [super init]; if (self) { _timeoutInterval = 6.0; // 设置超时时间为6秒 _semaphore = dispatch_semaphore_create(0); // 创建信号量 [self addRunLoopObserver]; // 添加 Runloop 观察者 [self startMonitor]; // 启动监控 } return self; } // 添加 Runloop 观察者的方法 - (void)addRunLoopObserver { NSRunLoop *curRunLoop = [NSRunLoop currentRunLoop]; // 获取当前 Runloop // 创建第一个观察者,监控 Runloop 是否处于运行状态 CFRunLoopObserverContext context = {0, (__bridge void *) self, NULL, NULL, NULL}; CFRunLoopObserverRef beginObserver = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, LONG_MIN, &myRunLoopBeginCallback, &context); CFRetain(beginObserver); // 保留观察者,防止被释放 self.runLoopBeginObserver = beginObserver; // 创建第二个观察者,监控 Runloop 是否处于睡眠状态 CFRunLoopObserverRef endObserver = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, LONG_MAX, &myRunLoopEndCallback, &context); CFRetain(endObserver); // 保留观察者,防止被释放 self.runLoopEndObserver = endObserver; // 将观察者添加到当前 Runloop 中 CFRunLoopRef runloop = [curRunLoop getCFRunLoop]; CFRunLoopAddObserver(runloop, beginObserver, kCFRunLoopCommonModes); CFRunLoopAddObserver(runloop, endObserver, kCFRunLoopCommonModes); } // 第一个观察者的回调函数,监控 Runloop 是否处于运行状态 void myRunLoopBeginCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) { MTHangMonitor *monitor = (__bridge MTHangMonitor *)info; g_runLoopActivity = activity; // 更新全局变量,记录当前的 Runloop 活动状态 g_runLoopMode = eRunloopDefaultMode; // 更新全局变量,记录当前的 Runloop 模式 switch (activity) { case kCFRunLoopEntry: g_bRun = YES; // 标记 Runloop 进入运行状态 break; case kCFRunLoopBeforeTimers: case kCFRunLoopBeforeSources: case kCFRunLoopAfterWaiting: if (g_bRun == NO) { gettimeofday(&g_tvRun, NULL); // 记录 Runloop 开始运行的时间 } g_bRun = YES; // 标记 Runloop 处于运行状态 break; case kCFRunLoopAllActivities: break; default: break; } dispatch_semaphore_signal(monitor.semaphore); // 发送信号量 } // 第二个观察者的回调函数,监控 Runloop 是否处于睡眠状态 void myRunLoopEndCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) { MTHangMonitor *monitor = (__bridge MTHangMonitor *)info; g_runLoopActivity = activity; // 更新全局变量,记录当前的 Runloop 活动状态 g_runLoopMode = eRunloopDefaultMode; // 更新全局变量,记录当前的 Runloop 模式 switch (activity) { case kCFRunLoopBeforeWaiting: gettimeofday(&g_tvRun, NULL); // 记录 Runloop 进入睡眠状态的时间 g_bRun = NO; // 标记 Runloop 进入睡眠状态 break; case kCFRunLoopExit: g_bRun = NO; // 标记 Runloop 退出运行状态 break; case kCFRunLoopAllActivities: break; default: break; } dispatch_semaphore_signal(monitor.semaphore); // 发送信号量 } // 启动监控的方法 - (void)startMonitor { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ while (YES) { long result = dispatch_semaphore_wait(self.semaphore, dispatch_time(DISPATCH_TIME_NOW, self.timeoutInterval * NSEC_PER_SEC)); if (result != 0) { if (g_runLoopActivity == kCFRunLoopBeforeSources || g_runLoopActivity == kCFRunLoopAfterWaiting) { [self logStackTrace]; // 记录调用栈 [self reportHang]; // 上报卡死 } } } }); } // 记录调用栈的方法 - (void)logStackTrace { void *callstack[128]; int frames = backtrace(callstack, 128); char **strs = backtrace_symbols(callstack, frames); NSMutableString *stackTrace = [NSMutableString stringWithString:@"\n"]; for (int i = 0; i < frames; i++) { [stackTrace appendFormat:@"%s\n", strs[i]]; } free(strs); NSLog(@"%@", stackTrace); } // 上报卡死的方法 - (void)reportHang { // 在这里实现上报后台分析的逻辑 NSLog(@"检测到卡死崩溃,进行上报"); } NS_ASSUME_NONNULL_END ``` 以上代码中 HangMonitor 类会在主线程的 RunLoop 活动中检测是否有长时间的卡顿,并在检测到卡顿时记录调用栈并上报后台进行分析。超时时间设定为 6 秒,以覆盖大部分用户感知场景并减少性能损耗。 使用示例代码: ``` objc #import #import "AppDelegate.h" #import "MTHangMonitor.h" int main(int argc, char * argv[]) { NSString * appDelegateClassName; @autoreleasepool { __unused MTHangMonitor *monitor = [MTHangMonitor sharedInstance]; // 获取 HangMonitor 单例 // Setup code that might create autoreleased objects goes here. appDelegateClassName = NSStringFromClass([AppDelegate class]); } return UIApplicationMain(argc, argv, nil, appDelegateClassName); } ``` # 总结 卡顿监控代码 [代码引用自 二刷 iOS 性能与编译,简单点说 ](https://mp.weixin.qq.com/s/X96VdTsskmNVCoqMzZjbgg) URL: https://sunyazhou.com/2024/10/uiimagemirror/index.html.md Published At: 2024-10-16 01:46:00 +0000 # UIImage镜像 # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! # 示例代码记录 ``` objc @implementation UIImage (MTMirrorImage) - (nullable UIImage *)mirrorImageHorizontally { if (self == nil) { return nil; } CGSize size = self.size; CGRect rect = CGRectMake(0, 0, size.width, size.height); // 创建一个位图上下文 CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); unsigned char *bytes = (unsigned char *)malloc(size.width * size.height * 4); CGContextRef context = CGBitmapContextCreate(bytes, size.width, size.height, 8, size.width * 4, colorSpace, kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedLast); // 将原始图像绘制到位图上下文中 CGContextDrawImage(context, rect, self.CGImage); // 水平翻转位图上下文中的图像 CGContextTranslateCTM(context, size.width, 0); CGContextScaleCTM(context, -1, 1); CGContextDrawImage(context, rect, self.CGImage); // 从位图上下文中获取新的图像 CGImageRef newImageRef = CGBitmapContextCreateImage(context); UIImage *newImage = [UIImage imageWithCGImage:newImageRef]; // 释放资源 CGImageRelease(newImageRef); CGContextRelease(context); free(bytes); CGColorSpaceRelease(colorSpace); return newImage; } - (nullable UIImage *)mirrorImageVertically; { if (self == nil) { return nil; } CGSize size = self.size; CGRect rect = CGRectMake(0, 0, size.width, size.height); // 创建一个位图上下文 CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); unsigned char *bytes = (unsigned char *)malloc(size.width * size.height * 4); CGContextRef context = CGBitmapContextCreate(bytes, size.width, size.height, 8, size.width * 4, colorSpace, kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedLast); // 将原始图像绘制到位图上下文中 CGContextDrawImage(context, rect, self.CGImage); // 垂直翻转位图上下文中的图像 CGContextTranslateCTM(context, 0, size.height); CGContextScaleCTM(context, 1, -1); CGContextDrawImage(context, rect, self.CGImage); // 从位图上下文中获取新的图像 CGImageRef newImageRef = CGBitmapContextCreateImage(context); UIImage *newImage = [UIImage imageWithCGImage:newImageRef]; // 释放资源 CGImageRelease(newImageRef); CGContextRelease(context); free(bytes); CGColorSpaceRelease(colorSpace); return newImage; } @end ``` URL: https://sunyazhou.com/2024/09/cfnotificationprocesscommunication/index.html.md Published At: 2024-09-02 12:08:00 +0000 # CFNotification进程间通讯 ![](/assets/images/20240727Magnificationgesture/SwiftUI.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! # 背景 在iOS开发中,应用扩展(App Extensions)与其容器应用(Containing App)在不同的进程中运行。这种隔离带来了一个挑战:当你需要在主应用和它的扩展之间进行通信时。虽然NSNotificationCenter是在同一应用内传递数据给不同视图控制器的常见选择,但在处理进程间通信时就力不从心了。你是否曾想过如何在主应用及其扩展之间传递数据?Darwin通知为这一场景提供了一个强大的解决方案。在本文中,我们将探讨如何实现一个Darwin通知管理器,并使用它来促进主应用和其扩展之间的实时数据传输。 什么是Darwin通知(又称CFNotificationCenterGetDarwinNotifyCenter)? CFNotificationCenterGetDarwinNotifyCenter是苹果Core Foundation框架中的一个函数,提供了访问Darwin通知中心的权限。这个通知中心被设计用于系统级的通知,允许像你的应用和其扩展这样的不同进程彼此通信。 它是如何工作的? 系统级通信:与仅仅限于应用内部的NSNotificationCenter不同,Darwin通知中心可以发送能够被设备上其他进程观察到的通知。 ## Darwin Notifications,也称为CFNotificationCenterGetDarwinNotifyCenter,是什么? `CFNotificationCenterGetDarwinNotifyCenter`是苹果公司Core Foundation框架中的一个函数,它提供了对Darwin通知中心的访问权限。该通知中心被设计用于系统级的通告,允许不同进程(例如你的应用及其扩展)彼此通信。 ### 它是如何工作的? **系统级通信**:与仅限于应用进程的NSNotificationCenter不同,Darwin通知中心可以发送能被设备上其他进程观察到的通知。这对于应用间以及应用到扩展的通信来说是一个理想的选择。 **不支持userInfo字典**:一个限制是Darwin通知不支持发送额外的数据(如userInfo字典)。这意味着你只能发送一个简单的通知,没有任何额外信息。这是因为底层机制notify_post()只接受一个字符串标识符作为通知。 ### Darwin通知的一个用例是 例如,当广播上传扩展开始或停止时,你可以使用Darwin通知来通知主应用。我看到大多数人使用UserDefaults或Keychain,但我个人认为Darwin通知最适合这个用例。 ### 实现Darwin通知管理器 首先,我们将创建一个`DarwinNotificationManager`类,使用`CFNotificationCenter` API来跨进程发布和观察通知。 ``` swift import Foundation class DarwinNotificationManager { static let shared = DarwinNotificationManager() private init() {} // 1 private var callbacks: [String: () -> Void] = [:] // Method to post a Darwin notification func postNotification(name: String) { let notificationCenter = CFNotificationCenterGetDarwinNotifyCenter() CFNotificationCenterPostNotification(notificationCenter, CFNotificationName(name as CFString), nil, nil, true) } // 2 func startObserving(name: String, callback: @escaping () -> Void) { callbacks[name] = callback let notificationCenter = CFNotificationCenterGetDarwinNotifyCenter() CFNotificationCenterAddObserver(notificationCenter, Unmanaged.passUnretained(self).toOpaque(), DarwinNotificationManager.notificationCallback, name as CFString, nil, .deliverImmediately) } // 3 func stopObserving(name: String) { let notificationCenter = CFNotificationCenterGetDarwinNotifyCenter() CFNotificationCenterRemoveObserver(notificationCenter, Unmanaged.passUnretained(self).toOpaque(), CFNotificationName(name as CFString), nil) callbacks.removeValue(forKey: name) } // 4 private static let notificationCallback: CFNotificationCallback = { center, observer, name, _, _ in guard let observer = observer else { return } let manager = Unmanaged.fromOpaque(observer).takeUnretainedValue() if let name = name?.rawValue as String?, let callback = manager.callbacks[name] { callback() } } } ``` #### Darwin通知管理器的解析 ``` swift private var callbacks: [String: () -> Void] = [:] ``` 回调`callbacks `函数存储每个通知名称的回调,以便在收到通知时执行。 ``` swift func startObserving(name: String, callback: @escaping () -> Void) { callbacks[name] = callback let notificationCenter = CFNotificationCenterGetDarwinNotifyCenter() CFNotificationCenterAddObserver(notificationCenter, Unmanaged.passUnretained(self).toOpaque(), DarwinNotificationManager.notificationCallback, name as CFString, nil, .deliverImmediately) } ``` `startObserving`方法为通知注册一个回调,并添加观察者来监听它。当你想开始监听通知时调用这个方法。它通常在视图初始化时被调用。 ``` swift func stopObserving(name: String) { let notificationCenter = CFNotificationCenterGetDarwinNotifyCenter() CFNotificationCenterRemoveObserver(notificationCenter, Unmanaged.passUnretained(self).toOpaque(), CFNotificationName(name as CFString), nil) callbacks.removeValue(forKey: name) } ``` `stopObserving`方法移除通知的观察者并删除其回调,以停止监听。它通常在视图被释放时调用。 ``` swift private static let notificationCallback: CFNotificationCallback = { center, observer, name, _, _ in guard let observer = observer else { return } let manager = Unmanaged.fromOpaque(observer).takeUnretainedValue() if let name = name?.rawValue as String?, let callback = manager.callbacks[name] { callback() } } ``` `notificationCallback`在收到相应的通知时执行存储的回调。 #### 在广播扩展中使用管理器 ``` swift import ReplayKit class SampleHandler: RPBroadcastSampleHandler { override func broadcastStarted(withSetupInfo setupInfo: [String : NSObject]?) { DarwinNotificationManager.shared.postNotification(name: "com.yourapp.BroadcastStarted") } override func broadcastFinished() { DarwinNotificationManager.shared.postNotification(name: "com.yourapp.BroadcastStopped") } } ``` #### 在UIKit视图中观察Darwin通知 ``` swift class DashboardViewController: UIViewController { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) self.configureCallbacks() } fileprivate func configureCallbacks() { DarwinNotificationManager.shared.startObserving(name: "com.yourapp.BroadcastStarted") { print("*******Broadcast has started*******") // Handle the event when broadcast starts } DarwinNotificationManager.shared.startObserving(name: "com.yourapp.BroadcastStopped") { print("*******Broadcast has stopped*******") // Handle the event when broadcast starts } } } ``` #### 在SwiftUI视图中观察Darwin通知 ``` swift import SwiftUI import Foundation struct BroadcastView: View { @State private var broadcastStatus: String = "Not Broadcasting" var body: some View { VStack { Text(broadcastStatus) .font(.largeTitle) .padding() } .onAppear { configureCallbacks() } .onDisappear { stopCallbacks() } } private func configureCallbacks() { DarwinNotificationManager.shared.startObserving(name: "com.yourapp.BroadcastStarted") { broadcastStatus = "Broadcasting..." print("*******Broadcast has started*******") } DarwinNotificationManager.shared.startObserving(name: "com.yourapp.BroadcastStopped") { broadcastStatus = "Not Broadcasting" print("*******Broadcast has stopped*******") } } private func stopCallbacks() { DarwinNotificationManager.shared.stopObserving(name: "com.yourapp.BroadcastStarted") DarwinNotificationManager.shared.stopObserving(name: "com.yourapp.BroadcastStopped") } } ``` 正如我之前提到的,在`onAppear`中开始观察,在`onDisappear`中停止观察。这将确保你的代码不会导致内存泄漏。 ### 关键要点 1.**进程间通信**:Darwin通知为不同进程之间的通信提供了一种强大的机制,如主应用与其扩展之间的通信,克服了`NSNotificationCente`r的限制。 2.**系统级覆盖**:与仅限于单个应用的`NSNotificationCenter`不同,Darwin通知可以被设备上的任何进程观察到,使它们成为应用-扩展通信的理想选择。 3.**不支持有效负载**:Darwin通知不支持发送额外的数据(如`userInfo`字典)。它们仅限于通知观察者事件发生,没有额外的上下文。 4.**高效的通知处理**:通过使用`CFNotificationCenterGetDarwinNotifyCenter`,开发人员可以有效地发布和观察通知,无需管理额外数据的开销。 5.**SwiftUI集成**:`Darwin`通知可以轻松集成到SwiftUI应用程序中,允许在不同应用组件之间进行实时更新和状态管理。 # 总结 我的脑海里总是将这些进程间通知比作彗星穿过我们的地球。正如彗星是罕见且令人敬畏的事件,吸引我们的注意力一样,Darwin通知作为关键的信号,让应用生态系统的不同部分能够无缝通信。这难道不是一个很好的比喻吗? 所以,就是这样了。如果你一直读到这里,我真的很感激。我希望这次对Darwin通知的探索能激发你以新的方式思考进程间通信。我迫不及待想看到你如何在应用中使用Darwin通知。请在下面的评论中告诉我你的想法和经验。你的反馈和想法总是受欢迎的!也别忘了与你的网络分享这篇文章! [原文链接 Send data Between iOS Apps and Extensions Using Darwin Notifications](https://ohmyswift.com/blog/2024/08/27/send-data-between-ios-apps-and-extensions-using-darwin-notifications/) [CFNotificationCenter文档](https://developer.apple.com/documentation/corefoundation/cfnotificationcenter) URL: https://sunyazhou.com/2024/08/multiblockembedded/index.html.md Published At: 2024-08-22 12:32:00 +0000 # 在Objective-C中多层block嵌套时如何使用weak self和strong self ![](/assets/images/20240822MultiBlockEmbedded/Objective-C.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! ## 开发中遇到的问题 在Objective-C中如何 正确的使用 block的 weakSelf和 strongSelf在 多层blokc 在Objective-C中,`weak self`和`strong self`用于解决循环引用问题。当你在一个方法中使用了`self`作为block的参数时,你需要使用`__weak typeof(self) weakSelf = self;`来创建一个弱引用,以避免循环引用。然后在block内部,你可以使用`strongSelf`来获取一个强引用。 以下是一个示例,展示了如何在三层嵌套的方法中使用`weak self`和`strong self`: ``` objc - (void)outerMethod { __weak typeof(self) weakSelf = self; [self innerMethod1:^{ __weak typeof(weakSelf) weakSelf2 = weakSelf; [weakSelf2 innerMethod2:^{ __weak typeof(weakSelf2) weakSelf3 = weakSelf2; [weakSelf3 innerMethod3:^{ __strong typeof(weakSelf3) strongSelf = weakSelf3; // 在这里使用strongSelf进行操作 }]; }]; }]; } - (void)innerMethod1:(void (^)(void))completion { // ... if (completion) { completion(); } } - (void)innerMethod2:(void (^)(void))completion { // ... if (completion) { completion(); } } - (void)innerMethod3:(void (^)(void))completion { // ... if (completion) { completion(); } } ``` 在这个示例中,我们在每一层方法中都使用了`__weak typeof(self) weakSelf = self;`来创建弱引用,并在下一层方法中使用`__weak typeof(weakSelf) weakSelf2 = weakSelf;`。最后,在最内层的方法中,我们使用`__strong typeof(weakSelf3) strongSelf = weakSelf3;`来获取一个强引用,以便在该层方法中使用。 URL: https://sunyazhou.com/2024/08/dispatchafternsintegermax/index.html.md Published At: 2024-08-19 01:50:00 +0000 # NSIntegerMax整数溢出问题记录 # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! ## 开发中遇到的问题 ![](/assets/images/20240819dispatchafterNSIntegermax/popup.gif) 最近开发 测试同学提了一个bug, 首页底部tab的气泡一闪而过瞬间消失,认真追踪代码后发现,配置后台下发`-1`,客户端同学把这个`-1`替换成了NSIntegerMax 如下代码,是控制一个气泡 从展示到结束的代码实现. ``` objc NSInteger delaySeconds = NSIntegerMax; NSLog(@"%@,展示前,%zd",[NSDate date],delaySeconds); dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delaySeconds * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ NSLog(@"%@,展示后,%zd",[NSDate date],delaySeconds); }); ``` 假设气泡展示10秒后调用结束的代码移除气泡,但是这段代码 会立即执行. ``` sh 2024-08-19 02:03:16 +0000,展示前,9223372036854775807 2024-08-19 02:03:16 +0000,展示后,9223372036854775807 ``` ### 为什么会立即执行? ![](/assets/images/20240819dispatchafterNSIntegermax/NSIntegerMax1.webp) `NSIntegerMax * NSEC_PER_SEC`= -10亿 负数当然会触发 `dispatch_after()`立即执行. 当你遇到 `NSIntegerMax * NSEC_PER_SEC` 结果为负数的情况时,这通常意味着发生了整数溢出。`NSIntegerMax` 表示 `NSInteger` 类型的最大值,当它与 `NSEC_PER_SEC`(每秒的纳秒数,等于 `1,000,000,000`)相乘时,结果可能会超出 `NSInteger` 能够表示的范围。 在32位系统上,`NSInteger` 是一个32位整数,其最大值是 `2,147,483,647`。而 `NSEC_PER_SEC` 是一个非常大的数,所以当它们相乘时,结果会超过32位整数的最大值,导致溢出。 在64位系统上,`NSInteger` 是一个64位整数,其最大值是 `9,223,372,036,854,775,807`。尽管64位整数的表示范围要大得多,但当它与 `NSEC_PER_SEC` 相乘时,结果仍然可能超出 `NSInteger` 的范围,因为 `NSInteger` 实际上是一个有符号整数,其最大值是 `9,223,372,036,854,775,807`,而 `NSEC_PER_SEC` 乘以 `NSIntegerMax` 会得到一个大于这个值的数字。 整数溢出通常发生在两种情况下: 1. **无符号整数溢出**:当一个无符号整数达到其最大值并继续增加时,它会回绕到0并从0开始重新计数。 2. **有符号整数溢出**:当一个有符号整数达到其最大值并继续增加时,它将变成一个负数。 在这种情况下,由于 `NSInteger` 是有符号的,当它与 `NSEC_PER_SEC` 相乘的结果超出其表示范围时,结果会变为负数。 为了避免这种情况,你应该使用更大的数据类型,比如 `uint64_t` 或 `int64_t`,这些类型可以安全地存储更大的数值。例如: ```objective-c uint64_t delayInNanoseconds = (uint64_t)NSIntegerMax * NSEC_PER_SEC; ``` 使用 `uint64_t` 可以确保乘法操作不会导致负数,因为 `uint64_t` 是一个无符号的64位整数。 ### 解决方式 * 定义两个时间最大长度的宏来替代最大值 ``` objc #define SECONDS_IN_A_YEAR 31536000LL // 非闰年 #define SECONDS_IN_A_LEAP_YEAR 31622400LL // 闰年 #define SECONDS_IN_A_MONTH 2629746LL //一个月最大秒数 #define SECONDS_IN_A_DAY 86400 //一天最大秒数 ``` 在 Objective-C 中,没有一个标准的宏直接表示一天中的最大秒数。但是,你可以使用一些基本的时间单位宏来计算一天的总秒数。 一天有 24 小时,每小时有 60 分钟,每分钟有 60 秒。所以,一天的总秒数可以通过以下公式计算得出: \[ \text{一天的总秒数} = 24 \times 60 \times 60 \] 一天的总秒数 = 24 * 60* 60 这等于 86,400 秒。 如果你需要在 Objective-C 代码中使用这个值,你可以定义一个宏或者常量来表示它: ```objective-c #define SECONDS_IN_A_DAY 86400 ``` 或者使用 `const` 常量: ```objective-c const int64_t SecondsInADay = 86400LL; ``` 使用 `int64_t` 类型可以确保这个常量足够大,即使在 32 位系统上也能正确表示一天的总秒数。`LL` 后缀确保了这个数字被解释为长长整型(`long long`)常量。 在实际编程中,你可以根据需要使用这个值来进行时间计算。 一个月和一年中的天数不是固定的,因为它们依赖于特定的日历规则。不过,我们可以给出一些近似值和计算方法。 ### 一个月的秒数 对于一个月份,我们通常使用平均值来近似计算。一个月平均大约有 30.44 天(考虑了不同月份的天数和闰年的影响)。因此,一个月的总秒数可以近似为: \[ \text{一个月的总秒数} \approx 30.44 \times 24 \times 60 \times 60 \] 计算结果大约是: \[ 2,629,746 \text{ 秒} \] 在 Objective-C 中,你可以这样定义一个月的秒数: ```objective-c #define SECONDS_IN_A_MONTH 2629746LL ``` ### 一年的秒数 对于一年,我们通常假设它有 365 天,除非是闰年,那时会有 366 天。因此,一年的总秒数可以这样计算: - 非闰年: \[ \text{一年的总秒数} = 365 \times 24 \times 60 \times 60 \] - 闰年: \[ \text{一年的总秒数} = 366 \times 24 \times 60 \times 60 \] 非闰年: 一年的总秒数 = 365 × 24 × 60 × 60 闰年: 一年的总秒数 = 366 × 24 × 60 × 60 计算结果大约是: - 非闰年:31,536,000 秒 - 闰年:31,622,400 秒 在 Objective-C 中,你可以这样定义一年的秒数: ```objective-c #define SECONDS_IN_A_YEAR 31536000LL // 非闰年 #define SECONDS_IN_A_LEAP_YEAR 31622400LL // 闰年 ``` 请注意,这些值是基于近似计算的,实际的月份和年份长度可能会有所不同。在处理具体的日期和时间计算时,通常需要考虑更复杂的日历规则。在 Objective-C 中,你可以使用 `NSCalendar` 类和 `NSDate` 类来更准确地处理日期和时间。 # 总结 这里的核心问题是不要用一个 NSIntegerMax 去乘以 `一个值` 得出的解决超过了 NSInteger能表示的最大范围,开发中一定要注意. URL: https://sunyazhou.com/2024/08/PickingInMetal/index.html.md Published At: 2024-08-12 08:55:00 +0000 # PickingInMetal # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! ![](/assets/images/20240813PickingAndHitTestinginMetal/picking.gif) # 最近学习Metal 当学习到第十二的时候遇到了一个之前想解决却从未找到答案的方式。如何检测 一个3D场景中某个元素的点击问题,比如引入一个3D场景在手机端或者PC端, 通过手指触摸或者鼠标点击能知道 我点击是哪个3D模型 在Metal.by.Tutorials.4th.2023.12.pdf这本书中我找到了答案-`Object Picking` ![](/assets/images/20240813PickingAndHitTestinginMetal/Metal.by.Tutorials.4th.2023.12.webp) ``` sh To get started with multipass rendering, you’ll create a simple render pass that adds object picking to your app. When you click a model in your scene, that model will render in a slightly different shade. There are several ways to hit-test rendered objects. For example, you could do the math to convert the 2D touch location to a 3D ray and then perform ray intersection to see which object intersects the ray. Warren Moore describes this method in his Picking and Hit-Testing in Metal (https://bit.ly/3rlzm9b) article. Alternatively, you could render a texture where each object is rendered in a different color or object ID. Then, you calculate the texture coordinate from the screen touch location and read the texture to see which object was hit. You’re going to store the model’s object ID into a texture in one render pass. You’ll then send the touch location to the fragment shader in the second render pass and read the texture from the first pass. If the fragment being rendered is from the selected object, you’ll render that fragment in a different color. ``` 这篇文章解决了我探索很久的问题.如何解决 2D空间点击3D空间元素的问题,其核心是采用一种叫做**3D射和物体求交集的方式**. 下面的文章解决了此问题,除了这种方式以外还有一种是**利用颜色ID来区分物体点击**. 一下是picking技术的文章精选 [Picking and Hit-Testing in Metal](https://bit.ly/3rlzm9b) [Picking and Hit-Testing in Metal Demo](https://github.com/metal-by-example/metal-picking) # 总结 和之前图形学同事探讨,他给我一些学习vulkan的方向资料,我整理到这里 ``` sh https://github.com/KhronosGroup/Vulkan-Guide https://github.com/KhronosGroup/Khronosdotorg/blob/main/api/vulkan/resources.md Tutorial: https://gavinkg.github.io/ILearnVulkanFromScratch-CN/ https://vulkan-tutorial.com/ https://software.intel.com/content/www/us/en/develop/articles/api-without-secrets-introduction-to-vulkan-preface.html https://renderdoc.org/vulkan-in-30-minutes.html https://www.fasterthan.life/blog/2017/7/11/i-am-graphics-and-so-can-you-part-1 ``` URL: https://sunyazhou.com/2024/08/docatchinswift/index.html.md Published At: 2024-08-11 01:55:00 +0000 # Swift中的do catch ![](/assets/images/20240727Magnificationgesture/SwiftUI.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! # Swift中的do-catch语句 在Swift中,`do-catch`语句是用于异常处理的机制。它允许你执行可能会抛出错误的代码块(在`do`块中),并捕获这些错误(在`catch`块中)以便处理。这是Swift中处理运行时错误的一种优雅方式。 ## 示例 ```swift do { // 尝试执行的代码,这里可能会抛出错误 let number = "这不是一个数字" let myNumber = Int(number) // 这行可能会因为转换失败而抛出错误 // 如果上面的代码没有抛出错误,这里的代码将执行 print("转换成功: \(myNumber)") } catch let error as NSError { // 捕获并处理错误 print("发生错误: \(error.localizedDescription)") } ``` ## 解释 1. **do块** - 在`do`块中,你放置可能会抛出错误的代码。 - 示例中尝试将一个字符串`"这不是一个数字"`转换为`Int`类型,这通常会失败并抛出一个错误。 2. **catch块** - `catch`块紧随`do`块之后。 - 它用于捕获`do`块中抛出的错误。 - 你可以根据错误的具体类型(通过模式匹配)来捕获不同类型的错误。在示例中,我们捕获了所有符合`NSError`类型的错误。 - 一旦捕获到错误,你就可以在`catch`块中编写代码来处理这个错误。 ## 注意 - 从Swift 2开始,`NSError`的使用被Swift的错误处理机制所取代,但示例中仍然使用它是因为它展示了如何捕获和处理错误。 - 在最新的Swift版本中,你更可能会看到使用`Error`协议的错误处理,而不是`NSError`。 ## 简化的catch块 ```swift do { // 尝试执行的代码 } catch { // 捕获所有错误 print("发生错误") } ``` 在这个简化的版本中,我们没有指定错误的具体类型,因此它会捕获`do`块中抛出的任何类型的错误。 # 总结 在Metal学习的demo工程中遇到了之前遗忘的内容,记录一下. ``` swift do { pipelineState = try device.makeRenderPipelineState(descriptor: pipelineDescriptor) } catch { fatalError(error.localizedDescription) } ``` URL: https://sunyazhou.com/2024/07/uiapplicationsignificanttimechangenotification/index.html.md Published At: 2024-07-31 03:17:00 +0000 # iOS中如何解决跨天日期变化 ![](/assets/images/20240727Magnificationgesture/SwiftUI.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! ## 背景介绍 最近工作中遇到一个需求是 当app在前台并且 过午夜12点时 触发某些逻辑,而且还需要在前台触发, 也就是说如果在后台挂起状态 回到前台才要触发,一般用于一些签到之类的逻辑. ## UIApplicationSignificantTimeChangeNotification 在iOS应用开发中,`UIApplicationSignificantTimeChangeNotification`是一个强大的工具,它允许开发者在时间发生重大变化时接收通知。这种变化包括日期的更改、时区的变动或夏令时(DST)的开始和结束或运营商的时间更新。这个功能尤其对于需要在特定时间执行某些任务的应用非常有用,例如在`午夜12点`进行数据备份或更新。 要在应用中接收UIApplicationSignificantTimeChangeNotification,首先需要向系统注册以接收此通知。这可以在应用的任何部分完成,但通常是在applicationDidFinishLaunching:或类似的应用生命周期方法中进行。 以下是如何注册的示例代码: ``` objc //注册通知 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(significantTimeChangeHandler:) name:UIApplicationSignificantTimeChangeNotification object:nil]; ... - (void)significantTimeChangeHandler:(NSNotification *)notification { if ([notification.name isEqualToString:UIApplicationSignificantTimeChangeNotification]) { // 获取当前日期 NSDate *currentDate = [NSDate date]; // 获取昨天的日期 NSCalendar *calendar = [NSCalendar currentCalendar]; NSDateComponents *comps = [calendar components:NSCalendarUnitDay fromDate:currentDate]; comps.day = comps.day - 1; NSDate *yesterday = [calendar dateFromComponents:comps]; // 检查是否跨过午夜 if ([currentDate compare:yesterday] == NSOrderedDescending) { // 在这里执行跨日期后需要的操作,如数据备份等 NSLog(@"Crossed midnight, perform necessary actions."); } } } ``` 此方法首先确认收到的通知是我们所关心的时间显著变化通知。然后,它获取当前的日期,并计算出昨天的日期。通过比较这两个日期,我们可以确定是否跨过了午夜。如果是这样,那么可以执行需要在新一天开始时进行的任何操作,比如数据备份、刷新用户界面等。 这个通知单的官方注释如下 ``` txt A notification that posts when there is a significant change in time, for example, change to a new day (midnight), carrier time update, and change to or from daylight savings time. 当时间发生重大变化时发布的通知,例如,更改为新的一天(午夜)、运营商时间更新,以及更改为夏令时。 This notification does not contain a userInfo dictionary. 此通知不包含userInfo字典。 If your app is currently suspended, this message is queued until your app returns to the foreground, at which point it is delivered. If multiple time changes occur, only the most recent one is delivered. 如果你的应用程序当前处于挂起状态,则此消息将一直排队,直到你的应用程序返回前台,并在前台发送。如果发生多个时间更改,则只发送最近的一个。 ``` > [官方文档](https://developer.apple.com/documentation/uikit/uiapplicationsignificanttimechangenotification) # 总结 这个通知并不常用,但能完全满足需求中要求的 跨天变化,日期修改回到前台等逻辑,并且还能在修改夏令时或运营商时间更新时触发. URL: https://sunyazhou.com/2024/07/magnificationgesture/index.html.md Published At: 2024-07-27 14:29:00 +0000 # swiftUI中添加拟合手势MagnificationGesture ![](/assets/images/20240727Magnificationgesture/SwiftUI.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! # MagnificationGesture 介绍 `MagnificationGesture` 是SwiftUI中用于处理缩放手势的手势识别器。它允许用户通过捏合(即两个手指靠近或远离)的手势来放大或缩小视图中的元素。这种手势在多种应用场景中都非常有用,如图片缩放、地图缩放、用户界面缩放等。 ![](/assets/images/20240727Magnificationgesture/MagnificationGesture.gif) ## 主要特点 - **缩放手势识别**:`MagnificationGesture` 能够识别用户的捏合手势,并根据手势的方向(靠近或远离)来放大或缩小视图。 - **实时响应**:在用户进行缩放手势时,`MagnificationGesture` 能够实时地调整视图的大小,提供流畅的交互体验。 - **可定制性**:开发者可以通过设置不同的参数和监听器来定制`MagnificationGesture`的行为,以满足不同的应用需求。 ## 使用场景 1. **图片缩放**:在图片查看应用中,用户可以使用`MagnificationGesture`来放大或缩小图片,以便更清晰地查看图片的细节。 2. **地图缩放**:在地图应用中,`MagnificationGesture`允许用户通过捏合手势来放大或缩小地图,以便查看不同层级的地理信息。 3. **用户界面缩放**:在需要自定义界面大小的应用中,`MagnificationGesture`可以用于实现用户界面的缩放功能,提升用户的个性化体验。 ## 示例代码 ``` swift import SwiftUI struct MagnificationGestureDemo: View { @GestureState private var scalingRatio: CGFloat = 1.0 var body: some View { Image("exampleImage") // 替换为实际图片名称 .resizable() .frame(width: 200, height: 200) .scaleEffect(scalingRatio) // 应用缩放效果 .gesture( MagnificationGesture() .updating($scalingRatio, body: { value, state, _ in state = value // 更新缩放比例 }) ) } } ``` 在上面的示例中,`MagnificationGesture`被添加到了一个图片视图上,并通过`.updating`修饰符来更新一个名为`scalingRatio`的`@GestureState`变量,该变量记录了当前的缩放比例。当用户进行缩放手势时,`scalingRatio`的值会实时更新,并通过`.scaleEffect`修饰符应用到图片上,从而实现图片的缩放效果。 # 总结 `MagnificationGesture`是SwiftUI中一个非常实用的手势识别器,它允许开发者通过简单的代码实现复杂的缩放手势交互效果。在开发需要缩放功能的应用时,`MagnificationGesture`是一个不可或缺的工具。 URL: https://sunyazhou.com/2024/07/UIViewrepresentable/index.html.md Published At: 2024-07-26 14:28:00 +0000 # 使用UIViewRepresentable 在SwiftUI中桥接 UIKit 视图 ![](/assets/images/20240726UIViewrepresentable/UIViewRepresentable.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! ## UIViewRepresentable介绍 `UIViewRepresentable` 是 SwiftUI 框架中的一个协议,它主要用于在 SwiftUI 环境中封装 UIKit 视图(`UIView` 及其子类)。SwiftUI 是 Apple 推出的一个用于构建跨平台用户界面的现代框架,它支持 iOS、macOS、watchOS 和 tvOS。然而,由于 SwiftUI 是在相对较新的时间点上推出的,许多现有的 UIKit 视图和控件尚未被直接集成到 SwiftUI 中。 `UIViewRepresentable` 协议允许开发者将 UIKit 视图桥接到 SwiftUI 视图系统中,使得在 SwiftUI 应用中能够利用 UIKit 强大的功能和现有的 UIKit 组件。通过使用 `UIViewRepresentable`,开发者可以创建一个 SwiftUI 视图,该视图内部封装了一个或多个 UIKit 视图,并在 SwiftUI 的视图中进行管理。 ## 如何使用 UIViewRepresentable * **定义一个遵循`UIViewRepresentable`的`SwiftUI`视图**:你需要创建一个 SwiftUI 视图,该视图遵循`UIViewRepresentable`协议。 * **实现协议要求的两个方法:** * `makeUIView(context: Context) -> UIView`:这个方法用于创建并返回一个 UIKit 视图实例。这个视图将被封装在 SwiftUI 视图内部。 * `updateUIView(_ uiView: UIViewType, context: Context)`:当 SwiftUI 视图需要更新其内部封装的 UIKit 视图时,会调用这个方法。你可以在这里设置 UIKit 视图的属性或添加子视图等。 * **在 SwiftUI 视图层次结构中使用你的封装视图**:创建并初始化你的封装视图后,就可以像使用其他 SwiftUI 视图一样在界面中使用它了。 #### 示例 以下是一个简单的示例,展示了如何使用`UIViewRepresentable`来封装一个 UIKit 的 `UIButton`: ``` swift import SwiftUI import UIKit struct MyButton: UIViewRepresentable { func makeUIView(context: Context) -> UIButton { let button = UIButton() button.setTitle("Click Me", for: .normal) button.addTarget(context.coordinator, action: #selector(Coordinator.buttonPressed), for: .touchUpInside) button.backgroundColor = .blue return button } func updateUIView(_ uiView: UIButton, context: Context) { // 这里可以根据需要更新按钮 } class Coordinator: NSObject { @objc func buttonPressed() { print("Button was pressed!") } } func makeCoordinator() -> Coordinator { return Coordinator() } } ``` 在这个示例中,`MyButton` 是一个封装了 `UIButton` 的 SwiftUI 视图。我们设置了按钮的标题、颜色,并添加了一个点击事件的处理器。点击事件通过`Coordinator`类来处理,这是`UIViewRepresentable`协议的一个常见模式,用于处理 UIKit 视图中的事件。 ### `UIViewRepresentable` 中的 `makeCoordinator` 方法调用时机 在 SwiftUI 的 `UIViewRepresentable` 协议中,`makeCoordinator` 方法扮演着重要的角色,尤其是在处理 UIKit 视图中的事件和回调时。以下是关于 `makeCoordinator` 方法调用时机的详细说明。 ## 调用时机 ### 1. 视图创建与渲染 - 当 SwiftUI 需要渲染 `UIViewRepresentable` 视图时,这通常发生在该视图首次被添加到视图层次结构中,或者当 SwiftUI 决定需要重新渲染该视图(例如,由于状态变化或布局更新)时。 ### 2. `makeUIView` 调用 - 在 `UIViewRepresentable` 视图准备将其内部的 UIKit 视图添加到视图层次结构之前,`makeUIView` 方法会被调用。这个方法负责创建并返回一个 UIKit 视图实例。 ### 3. `makeCoordinator` 调用 - 紧接着 `makeUIView` 方法之后(或者在某些情况下,可能几乎同时),`makeCoordinator` 方法会被调用。这个方法的目的是创建一个协调器(coordinator)对象,该对象通常用于处理 UIKit 视图中的事件或回调。 ### 4. 协调器与 UIKit 视图交互 - 在 `makeUIView` 中,你可能会设置 UIKit 视图的一些属性或事件监听器,这些监听器可以指向协调器中的方法。这样,当 UIKit 视图中的事件发生时(如按钮点击),相应的协调器方法就会被调用。 ### 5. `updateUIView` 的可能调用 - 在 `UIViewRepresentable` 视图的生命周期中,如果其状态发生变化,并且这些变化需要反映在其内部的 UIKit 视图上,那么 `updateUIView` 方法可能会被调用。这个方法允许你根据 `UIViewRepresentable` 视图的状态更新其内部的 UIKit 视图。 ## 关键点 - `makeCoordinator` 方法在 `UIViewRepresentable` 视图实例的生命周期中只会被调用一次(除非视图被重新创建,例如,由于内存回收和重新加载)。 - 一旦协调器对象被创建,它就会通过 `Context` 参数与 `UIViewRepresentable` 视图保持关联,直到视图被销毁。 - 你可以将 `makeCoordinator` 方法视为初始化 `UIViewRepresentable` 视图时设置事件处理和回调逻辑的一部分。 通过理解 `makeCoordinator` 的调用时机,你可以更有效地在 SwiftUI 应用中桥接 UIKit 视图,并处理它们之间的事件和交互。 URL: https://sunyazhou.com/2024/07/kerning/index.html.md Published At: 2024-07-18 07:01:00 +0000 # Kerning文字排版计算 # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! ## 记录一段代码用于解决iOS文字排版的技术问题 ``` objc CFAttributedStringRef attributedString; CTTypesetterRef typesetter = CTTypesetterCreateWithAttributedString(attributedString); CTLineRef line = CTTypesetterCreateLine(typesetter, CFRangeMake(0, 0)); NSArray *runs = (__bridge NSArray *)CTLineGetGlyphRuns(line); CTRunRef run = (__bridge CTRunRef)runs[runIdx]; const CFIndex glyphCount = CTRunGetGlyphCount(run); CGPoint *glyphPositions = malloc(sizeof(CGPoint) * glyphCount); CTRunGetPositions(run, CFRangeMake(0, 0), glyphPositions); CGRect bounds = CTRunGetImageBounds(run, NULL, CFRangeMake(0, 0)); CGGlyph *glyphs = malloc(sizeof(CGGlyph) * glyphCount); CTRunGetGlyphs(run, CFRangeMake(0, 0), glyphs); NSDictionary *runAttributes = (__bridge NSDictionary *)CTRunGetAttributes(run); CTFontRef font = (__bridge CTFontRef)runAttributes[NSFontAttributeName]; CGSize size; CTFontGetAdvancesForGlyphs(font, 0, &glyphs[glyphIdx], &size, 1); CTFontGetBoundingRectsForGlyphs() CTRunGetPositions(run, CFRangeMake(0, 0), glyphPositions);//这个应该拿到的就是带Kerning信息的position ``` 以上代码需要写demo验证. URL: https://sunyazhou.com/2024/07/hitteststd/index.html.md Published At: 2024-07-02 08:01:00 +0000 # hitTest标准写法 # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! # HitTest标准写法 ``` objc - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) { return nil; } if ([self pointInside:point withEvent:event]) { for (UIView *subview in [self.subviews reverseObjectEnumerator]) { CGPoint convertedPoint = [subview convertPoint:point fromView:self]; UIView *hitTestView = [subview hitTest:convertedPoint withEvent:event]; if (hitTestView) { if (hitTestView.superview == self) { // self的所有子view都不应该响应事件 return nil; } return hitTestView; } } return nil; } return nil; } ``` # 总结 代码review出现问题记录以下,hittest的标准写法 URL: https://sunyazhou.com/2024/05/smoothstep/index.html.md Published At: 2024-05-15 01:42:00 +0000 # 平滑阶梯函数smoothstep # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! # 如何控制一个值 在某个区间内,使其不超过最大区间极限和最小区间的极限? ## 在开发中我们经常计算一些区间值,:如下 ``` c float a = 某个输入值 float maxValue = 1; //check 0 < a <极限值 假设maxValue = 1 if (a <= 0) { a = 0; } else if (a >= maxValue) { a = maxValue } return a; ``` 显然这是刚学C语言时候的我写出来的毫无技术含量的代码. ## 工作多年后的写法 ``` c //引入标准库头文件 这里省略导入头文件过程... float a = 某个输入值 float maxValue = 1; //优雅永不过时 float a = min(max(0, a), maxValue); // 0 <= a <= 1 return a; ``` 这种写法虽然一行代码搞定,显得NB多了,控制了这个值的最小和最大区间不超过范围. 曾经还因为这个问题问过前公司的所有技术同学,我的提问如下 **有没有一个函数能把min + max 合成一下 控制一个值在某个区间范围内.** 答复是很多人都不清楚.擦 ## smoothste() 埃尔米特(Hermite)平滑插值函数 然而多年后 我发现自己并不优雅. 标准库中有很多函数需要自己提高认知才知道它是做什么的. 当我阅读到 `Metal by Tutorials`,这本书时有一段片元着色器的代码引起了我的注意. ``` txt “smoothstep smoothstep(edge0, edge1, x) returns a smooth Hermite interpolation between 0 and 1. Note: edge1 must be greater than edge0, and x should be edge0 <= x <= edge1.” 摘录来自 Metal by Tutorials 此材料可能受版权保护。 ``` 以下是片元着色器的代码. ``` c float color = smoothstep(0, params.width, in.position.x); return float4(color, color, color, 1); ``` 是的 这么多年我用的这个函数它其实叫做 `平滑阶梯函数`. # 总结 显然 接下来的操作是: 定义一个全局内联函数 封装 smoothstep() ``` c inline xx_smoothstep(T minEdge, T maxEdge, value) { return smoothstep(minEdge, maxEdge, value); } ``` 这种思路解决工程中所以范围值问题.原来这么多年追求的技术源于认知的提升. URL: https://sunyazhou.com/2024/05/arktsanimation/index.html.md Published At: 2024-05-05 11:05:00 +0000 # HarmonyOS动画分类 ![](/assets/images/20240116HarmonyPhoneSendFileTomacOS/harmonyOS.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! # 动画背景介绍 在鸿蒙开发中动画分类比iOS开发的分类更加丰富,包含如下动画类别 * 属性动画(animation) * 显式动画(animateTo) * 关键帧动画(keyframeAnimationTo) * 转场动画(Transition) * 页面间转场(pageTransition) * 组件内转场(transition) * 共享元素转场(sharedTransition) * 组件内隐式共享元素转场(geometryTransition) * 路径转场(motionPath) * 粒子动画(Particle) 详细资料来源[鸿蒙开发文档-动画](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-transition-animation-component-0000001862687721#ZH-CN_TOPIC_0000001862687721__transitioneffect10%E5%AF%B9%E8%B1%A1%E8%AF%B4%E6%98%8E) ## 属性动画animation ![](/assets/images/20240505ArkTSAnimation/HarmonyOSAnimation.gif) ``` ts import { SizeT } from '@ohos.arkui.node'; @Entry @Component struct MTAnimation3 { @State message: string = '迈腾大队长'; @State buttonSize: Size = {width: 266, height: 108}; @State didChanged: boolean = true; build() { Row() { Column() { Text(this.message) .fontSize(50) .fontWeight(FontWeight.Bold) Button("sunyazhou.com") //.animation({}) // 公式:animation 增加到那个地方的后面,前面就会被animation管理,否则不生效 .onClick( ()=> { console.log("点击sunyazhou.com按钮") if (this.didChanged) { this.buttonSize = {width: 166, height:80} } else { this.buttonSize = {width: 266, height:108} } this.didChanged = !this.didChanged //反置 交换 }) .width(this.buttonSize.width) .animation({ duration: 1000, curve: Curve.EaseInOut, // iterations: 1, //执行次数,(动画来回算2次) playMode: PlayMode.Alternate, //动画结束停在动画结束的位置 onFinish: ()=> { console.log("动画执行完成") } }) //我只负责前面的代码 有动画,后面的代码,我不管(在这里之前的代码都受animation控制) .height(this.buttonSize.height) //注意;这行代码不在动画范围内 } .width('100%') } .height('100%') } } ``` > 注意 :.animation动画只对添加代码之前的属性生效,之后的属性不生效,[属性动画官网文档](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-animatorproperty-0000001815927688) ## 显式动画(animateTo) 学过iOS开发都知道iOS中的显式动画是`[UIView animateWithDuratio...]` ``` objc + (void)animateWithDuration:(NSTimeInterval)duration animations:(void (^)(void))animations API_AVAILABLE(ios(4.0)); ``` 在鸿蒙开发中,这种类型的动画叫做`animateTo` 先看下示意图,我想让图片旋转90度,然后再回去,这在iOS中直接就改transfrom,并且把改动的代码放到上述UIView的animation中皆可, ``` objc CGAffineTransformRotate(transform, M_PI_2); //旋转90° ... CGAffineTransformIdentity; ``` ![](/assets/images/20240505ArkTSAnimation/HarmonyOSAnimateTo.gif) 这里 HarmonyOS的ArkUI示例代码演示: ``` ts @Entry @Component struct MTAnimation2 { @State message: string = '显式动画(animateTo)'; @State rotateValue: number = 0; @State color: Color = Color.Blue; @State isStart: boolean = false; build() { Row() { Column() { Text(this.message) .fontSize(50) .fontWeight(FontWeight.Bold) Divider() .height(20) Image($r('app.media.sunyazhou')) .width(333) .height(333) .rotate({ angle:this.rotateValue, //表面上是旋转功能, 实际上需要配合 x轴y抽z轴 x:0, y:0, z:1, }) .onClick(() => { animateTo({ duration: 1000, //ms curve : Curve.EaseInOut, //动画速率 onFinish:() => { this.message = "动画执行完成" this.color = Color.Green } }, ()=> { if (this.isStart) { this.rotateValue = 0 } else { this.rotateValue = 90 } this.isStart = !this.isStart }) }) } .width('100%') } .height('100%') } } ``` 如果把`.rotate`中的x,y,z,都改成1并旋转625度的话是这样的 ![](/assets/images/20240505ArkTSAnimation/HarmonyOSAnimateTo2.gif) 当我们在动画中连续点击动画的时候它很跟手,和iOS中的UIView Animation一样中间被打断直接执行下次显式动画,下图演示跟手效果. ![](/assets/images/20240505ArkTSAnimation/HarmonyOSAnimateTo3.gif) [更多细节请访问 HarmonyOS官方文档 显式动画 (animateTo)](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-explicit-animation-0000001862687717) ## 关键帧动画 关键帧动画在鸿蒙开发中是借助`UIContext`实现 ![](/assets/images/20240505ArkTSAnimation/HarmonyOSKeyframeAnimation.gif) ``` ts // xxx.ets import { UIContext } from '@ohos.arkui.UIContext'; @Entry @Component struct KeyframeDemo { @State myScale: number = 1.0; uiContext: UIContext | undefined = undefined; aboutToAppear() { this.uiContext = this.getUIContext?.(); } build() { Column() { Circle() .width(100) .height(100) .fill("#46B1E3") .margin(100) .scale({ x: this.myScale, y: this.myScale }) .onClick(() => { if (!this.uiContext) { console.info("no uiContext, keyframe failed"); return; } this.myScale = 1; // 设置关键帧动画整体播放3次 this.uiContext.keyframeAnimateTo({ iterations: 3 }, [ { // 第一段关键帧动画时长为800ms,scale属性做从1到1.5的动画 duration: 800, event: () => { this.myScale = 1.5; } }, { // 第二段关键帧动画时长为500ms,scale属性做从1.5到1的动画 duration: 500, event: () => { this.myScale = 1; } } ]); }) }.width('100%').margin({ top: 5 }) } } ``` ## 组件内转场动画 组件内转场动画 如下图: ![](/assets/images/20240505ArkTSAnimation/transition1.gif) 实现代码 ``` ts @Entry @Component struct MTAnimation1 { @State phones: string[] = [ 'HUAWEI Mete 60 Pro WIt9000S', 'HUAWEI Mete 40 Pro+ 5G soc', 'Xiaomi 14 Pro 第三代骁龙 5G', 'OPPO Find X Pro 第二代骁龙 5G' ] @State topIndex: number = 0 @State bottomIndex: number = this.phones.length build() { Column({space:12}) { Column() { ForEach(this.phones, (item:string)=> { Text(item).ft_text() .transition( TransitionEffect.asymmetric( // 1.出现时做从指定的透明度为0变为默认的透明度1的动画,该动画时长为1000ms, // 以及做从指定的绕z轴旋转180°变为默认的旋转角为0的动画,该动画1000ms后播放,时长为1000ms // 2.消失时做从默认的透明度为1变为指定的透明度0的动画,该动画1000ms后播放, // 时长为1000ms,以及做从默认的旋转角0变为指定的绕z轴旋转180°的动画,该动画时长为1000ms TransitionEffect.OPACITY.animation({ duration: 1000 }).combine( TransitionEffect.rotate({ z: 1, angle: 180 }).animation({ delay: 1000, duration: 1000 })), TransitionEffect.OPACITY.animation({ delay: 1000, duration: 1000 }).combine( TransitionEffect.rotate({ z: 1, angle: 180 }).animation({ duration: 1000 }) // TransitionEffect.translate({x: 600, y: 0}).animation({duration: 1000}) ) ) ) // .transition(TransitionEffect.OPACITY.animation({duration: 2000, curve: Curve.Ease}) // .combine(TransitionEffect.rotate({z: 1, angle: 180}) // )) // .transition( // TransitionEffect.asymmetric( // TransitionEffect.translate({x: 333, y: 333}), // TransitionEffect.IDENTITY // ) // ) }, (item: string) => JSON.stringify(item)) }.ft_column() Button('在顶部增加手机').ft_btn(Color.Red,() => { animateTo({}, ()=> { if (this.topIndex == 0) { this.phones.unshift('iPhone 15 Pro Max') } else { this.phones.unshift('iPhone 15 Pro Max'+ '(' + this.topIndex + ')') } this.topIndex++ }) }) Button('在底部增加手机').ft_btn(Color.Green, ()=> { animateTo({}, ()=> { if (this.bottomIndex == 0) { this.phones.push('iPhone 14 Pro Max') } else { this.phones.push('iPhone 14 Pro Max' + '('+ this.bottomIndex +')') } this.bottomIndex++ }) }) Button('在头部删除手机').ft_btn(Color.Blue, ()=> { animateTo({}, ()=> { this.phones.shift() }) }) Button('在底部删除手机').ft_btn(Color.Pink, () => { animateTo({}, ()=> { this.phones.pop() }) }) } .width('100%') .height('100%') } } @Extend(Column) function ft_column() { .margin(10) .justifyContent(FlexAlign.Start) .alignItems(HorizontalAlign.Center) .width('90%') .height('50%') } @Extend(Text) function ft_text() { .width(300) .height(60) .fontSize(18) .margin({top: 3}) .backgroundColor(Color.Yellow) .textAlign(TextAlign.Center) } @Extend(Button) function ft_btn(bgColor: Color, click: Function) { .width(200) .height(50) .fontSize(18) .backgroundColor(bgColor) .onClick(()=> { click() //此处的cLick是一个形参。具体代表的是调用除传进来的函数。后方跟小括号代表执行传进来的函数。 }) } function item(item: string, index: number): string { throw new Error('Function not implemented.') } ``` 这里比较核心的代码如下 ``` ts Text(item).ft_text() .transition( TransitionEffect.asymmetric( // 1.出现时做从指定的透明度为0变为默认的透明度1的动画,该动画时长为1000ms, // 以及做从指定的绕z轴旋转180°变为默认的旋转角为0的动画,该动画1000ms后播放,时长为1000ms // 2.消失时做从默认的透明度为1变为指定的透明度0的动画,该动画1000ms后播放, // 时长为1000ms,以及做从默认的旋转角0变为指定的绕z轴旋转180°的动画,该动画时长为1000ms TransitionEffect.OPACITY.animation({ duration: 1000 }).combine( TransitionEffect.rotate({ z: 1, angle: 180 }).animation({ delay: 1000, duration: 1000 })), TransitionEffect.OPACITY.animation({ delay: 1000, duration: 1000 }).combine( TransitionEffect.rotate({ z: 1, angle: 180 }).animation({ duration: 1000 }) // TransitionEffect.translate({x: 600, y: 0}).animation({duration: 1000}) ) ) ) // .transition(TransitionEffect.OPACITY.animation({duration: 2000, curve: Curve.Ease}) // .combine(TransitionEffect.rotate({z: 1, angle: 180}) // )) // .transition( // TransitionEffect.asymmetric( // TransitionEffect.translate({x: 333, y: 333}), // TransitionEffect.IDENTITY // ) // ) ``` 找了一下文档[组件内转场 (transition)](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-transition-animation-component-0000001862687721)这里有详细的介绍. 这个动画有点类似iOS的仿射动画.只是平台不一样,这里的动画也非常丰富.后续会持续把其他动画实现一遍记录在这里 # 总结 鸿蒙开发已经学了3遍了,有时候需要记录一些动画和内容.全当学习笔记.希望能帮助其它开发者. URL: https://sunyazhou.com/2024/04/keepimagescale/index.html.md Published At: 2024-04-02 09:16:00 +0000 # 保持原UIImage缩放比的计算方法 # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或引用,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,这样您将能在第一时间获取本站信息. # 代码记录 ``` objc /// 保持宽高比不变的前提的 宽高不超过 最大限制2048 /// - Parameter imageSize: 原始大小 - (CGSize)keepScaleSize:(CGSize)imageSize { if (kw_is_float_zero(imageSize.width) || kw_is_float_zero(imageSize.height)) { return imageSize; } //check有没有超过最大限制 if (imageSize.width < 2048 && imageSize.height < 2048) { return imageSize; } //超过最大限制 CGSize resize = CGSizeZero; if (imageSize.width > imageSize.height) { //最长边是 宽 CGFloat ratio = imageSize.height / imageSize.width; CGFloat disWidth = 2048; CGFloat disHeight = disWidth * ratio; resize = CGSizeMake(disWidth, disHeight); } else { //最长边是 高 CGFloat ratio = imageSize.width / imageSize.height; CGFloat disHeight = 2048; CGFloat disWidth = disHeight * ratio; resize = CGSizeMake(disWidth, disHeight); } return resize; } ``` # 总结 开发过程中有一些经常忘记,却很容易的计算方法,当用到的时候很难找,记录一下代码片段 URL: https://sunyazhou.com/2024/03/masonryrelayoutviews/index.html.md Published At: 2024-03-22 13:24:00 +0000 # 使用Masonry高阶方法对子视图统一布局 # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! ## 背景介绍 ![](/assets/images/20240322MasonryRelayoutViews/MasonryRelayout.gif) 在开发过程中,经常遇到某些入口的出现和消失不是按照指定的时序发生,比如 上面这三个入口,出现的时机不区分先后,但是出现的顺序是固定的, 这里就存在一些很不好处理的问题, 比如A视图出现 依赖B视图的位置,如果B不在那要继续向上或者向下依赖. ## 面临的挑战案例 基于上述的背景描述,我们需要处理的问题如下 * 假设 入口视图的出现时机或者消失时机, 不是时序顺序的,是时序随机的. * 各个入口视图的有依赖关系, 或者顺序固定 要如何处理 * 有没有简单更有效的方式 使用少量代码解决上述问题的最优解. 根据上述的挑战我们来分析一下如何解决 * 要给所有入口视图添加优先级, 添加和删除都需要排序 * 各种视图添加的时间是不固定的,那么就要有一个公用的方法控制他们添加和移除或者说是显示和消失都必须要调用的方法用于布局 * 基于简单的Masonry代码能不能 几行搞定. ### 最优解的方式实现 首先我们先封装一个UIView的子类,对外提供各种入口的show和dismiss方法.内部要对这些入口添加或者消失的时候调用relayout的函数方法. relayout方法中要对现有的视图进行排序. 然后统一用Masonry提供的方法解决布局问题. ``` objc typedef NS_ENUM(NSUInteger, MTContainerViewPriority) { MTContainerViewPriorityL1 = 101, MTContainerViewPriorityL2 = 102, MTContainerViewPriorityL3 = 103, //more ... }; @interface MTContainerView : UIView - (void)showView1; - (void)dismissView1; - (void)showView2; - (void)dimissView2; - (void)showView3; - (void)dismissView3; @end ``` 实现文件 ``` objc #import "MTContainerView.h" #import const CGSize MTContainerSize = { 40 , 40}; @interface MTContainerView () @property (nonatomic, strong) UIView *view1; @property (nonatomic, strong) UIView *view2; @property (nonatomic, strong) UIView *view3; @end @implementation MTContainerView #pragma mark - #pragma mark - private methods 私有方法 - (void)layoutAllEntryViewsIfNeeded { NSSortDescriptor *ascendingSort = [[NSSortDescriptor alloc] initWithKey:@"tag" ascending:YES]; NSArray *allEntries = [[self subviews] sortedArrayUsingDescriptors:[NSArray arrayWithObject:ascendingSort]]; if (allEntries.count == 0) { return; } if (allEntries.count == 1) { UIView *entryView = [allEntries objectAtIndex:0]; [entryView mas_remakeConstraints:^(MASConstraintMaker *make) { make.size.mas_equalTo(MTContainerSize); make.right.equalTo(self.mas_right).offset(-10); make.centerY.equalTo(self.mas_centerY); }]; } else { // 使用 mas_distributeViewsAlongAxis 方法对三个视图进行水平右对齐并一次排开 [allEntries mas_remakeConstraints:^(MASConstraintMaker *make) { make.size.mas_equalTo(MTContainerSize); make.centerY.equalTo(self.mas_centerY); }]; //必须 allEntries.count >= 2 才能用下述方法, 下面间距算法 容器宽度-所用容量宽度(包含右侧间隙+每个item大小+每个item之间的间隙) CGFloat leadSpace = CGRectGetWidth(self.frame) - allEntries.count * MTContainerSize.width - 10 - (allEntries.count - 1) * 10; [allEntries mas_distributeViewsAlongAxis:MASAxisTypeHorizontal withFixedSpacing:10 leadSpacing:leadSpace tailSpacing:10]; } [UIView animateWithDuration:0.3 animations:^{ [self layoutIfNeeded]; }]; } - (UIColor *)randomColor { CGFloat hue = ( arc4random() % 256 / 256.0 ); // 0.0 to 1.0 CGFloat saturation = ( arc4random() % 128 / 256.0 ) + 0.5; // 0.5 to 1.0, away from white CGFloat brightness = ( arc4random() % 128 / 256.0 ) + 0.5; // 0.5 to 1.0, away from black UIColor *color = [UIColor colorWithHue:hue saturation:saturation brightness:brightness alpha:1]; return color; } #pragma mark - #pragma mark - public methods 公有方法 - (void)showView1 { if (self.view1 == nil) { self.view1 = [[UIView alloc] initWithFrame:CGRectMake(CGRectGetWidth(UIScreen.mainScreen.bounds), 20, MTContainerSize.width, MTContainerSize.height)]; self.view1.backgroundColor = [self randomColor]; self.view1.tag = MTContainerViewPriorityL1; } if (self.view1.superview == nil) { [self addSubview:self.view1]; } [self layoutAllEntryViewsIfNeeded]; } - (void)dismissView1 { if (self.view1.superview) { [self.view1 removeFromSuperview]; } self.view1 = nil; [self layoutAllEntryViewsIfNeeded]; } - (void)showView2 { if (self.view2 == nil) { self.view2 = [[UIView alloc] initWithFrame:CGRectMake(CGRectGetWidth(UIScreen.mainScreen.bounds), 20, MTContainerSize.width, MTContainerSize.height)]; self.view2.backgroundColor = [self randomColor]; self.view2.tag = MTContainerViewPriorityL2; } if (self.view2.superview == nil) { [self addSubview:self.view2]; } [self layoutAllEntryViewsIfNeeded]; } - (void)dimissView2 { if (self.view2.superview) { [self.view2 removeFromSuperview]; } self.view2 = nil; [self layoutAllEntryViewsIfNeeded]; } - (void)showView3 { if (self.view3 == nil) { self.view3 = [[UIView alloc] initWithFrame:CGRectMake(CGRectGetWidth(UIScreen.mainScreen.bounds), 20, MTContainerSize.width, MTContainerSize.height)]; self.view3.backgroundColor = [self randomColor]; self.view3.tag = MTContainerViewPriorityL3; } if (self.view3.superview == nil) { [self addSubview:self.view3]; } [self layoutAllEntryViewsIfNeeded]; } - (void)dismissView3 { if (self.view3.superview) { [self.view3 removeFromSuperview]; } self.view3 = nil; [self layoutAllEntryViewsIfNeeded]; } @end ``` 这里我们模拟不按顺序 不同时序 添加视图 ``` objc - (void)viewDidLoad { [super viewDidLoad]; self.continerView = [[MTContainerView alloc] initWithFrame:CGRectZero]; self.continerView.backgroundColor = [UIColor cyanColor]; [self.view addSubview:self.continerView]; [self.continerView mas_remakeConstraints:^(MASConstraintMaker *make) { make.left.right.equalTo(self.view); make.top.mas_equalTo(self.mas_topLayoutGuideBottom); make.height.equalTo(@60); }]; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [self.continerView showView3]; }); dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(4 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [self.continerView showView1]; }); dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(7 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [self.continerView showView2]; }); } ``` ## 核心实现代码 ``` objc - (void)layoutAllEntryViewsIfNeeded { NSSortDescriptor *ascendingSort = [[NSSortDescriptor alloc] initWithKey:@"tag" ascending:YES]; NSArray *allEntries = [[self subviews] sortedArrayUsingDescriptors:[NSArray arrayWithObject:ascendingSort]]; if (allEntries.count == 0) { return; } if (allEntries.count == 1) { UIView *entryView = [allEntries objectAtIndex:0]; [entryView mas_remakeConstraints:^(MASConstraintMaker *make) { make.size.mas_equalTo(MTContainerSize); make.right.equalTo(self.mas_right).offset(-10); make.centerY.equalTo(self.mas_centerY); }]; } else { // 使用 mas_distributeViewsAlongAxis 方法对三个视图进行水平右对齐并一次排开 [allEntries mas_remakeConstraints:^(MASConstraintMaker *make) { make.size.mas_equalTo(MTContainerSize); make.centerY.equalTo(self.mas_centerY); }]; //必须 allEntries.count >= 2 才能用下述方法, 下面间距算法 容器宽度-所用容量宽度(包含右侧间隙+每个item大小+每个item之间的间隙) CGFloat leadSpace = CGRectGetWidth(self.frame) - allEntries.count * MTContainerSize.width - 10 - (allEntries.count - 1) * 10; [allEntries mas_distributeViewsAlongAxis:MASAxisTypeHorizontal withFixedSpacing:10 leadSpacing:leadSpace tailSpacing:10]; } [UIView animateWithDuration:0.3 animations:^{ [self layoutIfNeeded]; }]; } ``` 这里有几个问题需要说清楚 * mas_makeConstraints 是Masonry给NSArray扩展的方法.用于批量处理视图使用,它必须保证NSArray.count > 1 * 统一布局 实现的是固定大小, 如果要实现 多个视图不同大小,那目前这种方式不适用 * Masonry没有像ArkUI和SwiftUI中声明式编程,那种容器对齐的方式,比如 start、center、end等内容对齐.所以会看到有如下代码 ``` objc CGFloat leadSpace = CGRectGetWidth(self.frame) - allEntries.count * MTContainerSize.width - 10 - (allEntries.count - 1) * 10; [allEntries mas_distributeViewsAlongAxis:MASAxisTypeHorizontal withFixedSpacing:10 leadSpacing:leadSpace tailSpacing:10]; ``` 计算`leadSpace`左侧向右的偏移距离. 通过上述实现我们就有了如下的demo ![](/assets/images/20240322MasonryRelayoutViews/MasonryRelayoutDemo.gif) 这里的核心代码是Masonry提供的数组扩展方法 ``` objc - (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block { self.translatesAutoresizingMaskIntoConstraints = NO; MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self]; block(constraintMaker); return [constraintMaker install]; } - (void)mas_distributeViewsAlongAxis:(MASAxisType)axisType withFixedSpacing:(CGFloat)fixedSpacing leadSpacing:(CGFloat)leadSpacing tailSpacing:(CGFloat)tailSpacing; - (void)mas_distributeViewsAlongAxis:(MASAxisType)axisType withFixedItemLength:(CGFloat)fixedItemLength leadSpacing:(CGFloat)leadSpacing tailSpacing:(CGFloat)tailSpacing; ``` 以上三个方法是实现上述视图的关键 # 总结 深入了解Masonry的api使用.用高阶用法实现复杂的功能. [本文demo 点击下载](https://github.com/sunyazhou13/MasonryRelayoutDemo) URL: https://sunyazhou.com/2024/02/MotionShake/index.html.md Published At: 2024-02-21 06:56:00 +0000 # 运动传感器摇晃检测 ![](/assets/images/20240222MotionShake/CMMotion.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或引用,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,这样您将能在第一时间获取本站信息. ## 背景说明 最近开发遇到用户反馈,开启晃动手机切换歌曲时,放裤兜或者衣服口袋中,很容易触发主动切换歌曲,带着这个问题,我仔 细研究了一下固有代码. 很显然用户使用摇一摇手机切换歌曲的灵敏度太高了.那怎么调整灵敏度到一个合理区间呢? # 实现摇晃动作的几种方式 * 1.系统事件 * 2.CMMotionManager加速计api * 3.UIAccelerometer ## 系统的摇一摇事件 我们可以写一个继承自`UIResponder`的类,实现如下方法 ``` objc -(BOOL)canBecomeFirstResponder { return YES; } - (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event { if (event.subtype == UIEventSubtypeMotionShake) { NSLog(@"摇晃手势被检测到"); // 在这里处理摇晃手势事件 } } ``` 比如在UIViewController中我们 实现上述代码 ``` objc - (void)viewDidLoad { [super viewDidLoad]; [self becomeFirstResponder]; } - (BOOL)canBecomeFirstResponder { return YES; } - (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event { if (event.subtype == UIEventSubtypeMotionShake) { NSLog(@"摇晃手势被检测到"); // 在这里处理摇晃手势事件 } } ``` 就可以在Objective-C中实现摇晃手势检测了 > 然而这种模式没有入口让我设置阈值控制摇一摇的灵敏度 ## CMMotionManager 首先先搞清楚这里有啥,iOS 中常见传感器如下所示: | 类型 | 作用 | 备注 | | ------| ------ | ------ | | 环境光传感器 | 感应光照强度 | | | 距离传感器 | 感应靠近设备屏幕的物体 | | | 磁力计传感器 | 感应周边磁场 | | | 内部温度传感器 | 感应设备内部温度(非公开) | | 湿度传感器 | 感应设备是否进水(非微电子传感器) | | 陀螺仪 | 感应持握方式 | | 加速计 | 感应设备运动 | CMMotionManager 是 `Core Motion` 库的核心类,负责获取和处理手机的运动信息,它可以获取的数据有: * 加速度,标识设备在三维空间中的瞬时加速度 * 陀螺仪,标识设备在三个主轴上的瞬时旋转 * 磁场信息,标识设备相对于地球磁场的方位 * 设备运动数据,标识关键的运动相关属性,包括设备用户引起的加速度、姿态、旋转速率、相对于校准磁场的方位以及相对于重力的方位等,这些数据均来自于 Core Motion 的传感器融合算法,从这一个数据接口即可获取以上三种数据,因此使用较为广泛.比如nike的跑鞋app 计算步数就是依赖于这个传感器. [了解更多CMMotion可以参考这里](https://www.jianshu.com/p/2f5cca76c5ee) 使用这个`CMMotionManager `之前我们要保证在info.plist中加入Privacy – Motion Usage Description.让用户知道我们为什么要用这个传感器. ``` xml NSMotionUsageDescription 请选择“允许”,可为您提供晃动切换歌曲 ``` 下面是使用的示例代码 ``` objc #import @interface ShakeDetector : NSObject @property (nonatomic, strong) CMMotionManager *motionManager; - (void)startShakeDetection; @end #import "ShakeDetector.h" @implementation ShakeDetector - (void)startShakeDetection { self.motionManager = [[CMMotionManager alloc] init]; self.motionManager.deviceMotionUpdateInterval = 1.0/60.0; [self.motionManager startDeviceMotionUpdatesToQueue:[NSOperationQueue mainQueue] withHandler:^(CMDeviceMotion *motion, NSError *error) { CMAcceleration userAcceleration = motion.userAcceleration; double accelerationThreshold = 0.30; if (fabs(userAcceleration.x) > accelerationThreshold || fabs(userAcceleration.y) > accelerationThreshold || fabs(userAcceleration.z) > accelerationThreshold) { // 在这里处理摇晃动作的逻辑 NSLog(@"Device shaken!"); } }]; } @end ``` ### 实现原理 实现原理:通过x、y、z三个轴的方向的加速度计算出摇动手机时,手机摇动方向的加速度a, $$ \begin{align} g = \sqrt{x^2+y^2+z^2} \end{align} $$ 加速计中的单位为:g(重力加速度9.8米/秒), 当g > 1.6, 记录一次摇动.参考范围(2.0~3.0). ``` objc typedef struct { double x; double y; double z; } CMAcceleration; ``` 通过传感器返回的`CMAcceleration `结构体,和我们指定的阈值做检测,通过加速计的x, y, z分别check是否大于我们设置的阈值.如果其中任何一个值大于我们指定的阈值,就说明我们检测到了`摇晃动作`可以理解为摇一摇. ``` objc double threshold = 2.45; //指定灵敏度阈值 if (fabs(acceleration.x) > threshold || fabs(acceleration.y) > threshold || fabs(acceleration.z) > threshold) { ... } ``` `threshold`阈值这里需要可以参考如下: * **对于一般的摇一摇功能,阈值大小可以在1.0到2.0之间**. * **如果需要更高的灵敏度,可以选择较小的阈值,例如0.5到1.0**. * **如果需要较低的灵敏度,可以选择较大的阈值,例如2.0到3.0**. `2.45`是我测试出来比较适合大部分人手摇晃的力量,并且避免轻微晃动触发得出的理想值. ### 队列控制 当我使用`CMMotionManager`时注意,最好是放在一个单独的队列中.主要是担心放主线程影响主线程性能. ``` objc //注意这里 customeMotionOperationQueue [self.motionManager startAccelerometerUpdatesToQueue:customeMotionOperationQueue withHandler:..]; ``` ### 频率优化 因为根据上述原理介绍我们可以通过x, y, z轴的加速度来检测当前是否是摇晃,但是有可能 上下操作过快会导致检测触发多次,为了控制 多次之间的间隙太短问题,我们通过如下代码控制频率 ``` objc @property (nonatomic, assign) CFAbsoluteTime beforeTime; //记得初始化赋值. ... // 检测到摇晃动作 CFAbsoluteTime afterTime = CFAbsoluteTimeGetCurrent(); // 记录执行摇晃检测逻辑后的时间 CFTimeInterval timeDifference = afterTime - self.beforeTime; // 计算时间差 单位秒 s CFTimeInterval intervalSenonds = 1.0; if (timeDifference >= intervalSenonds) { //控制检测前后间隔 //NSLog(@"检测到摇晃动作,距离上次检测: f seconds", timeDifference); self.beforeTime = CFAbsoluteTimeGetCurrent(); // 记录执行摇晃检测逻辑前的时间 if (self.didAcceleratorDectecdBlock) { self.didAcceleratorDectecdBlock(); } } else { //NSLog(@"检测到摇晃动作,间隔不满足 f seconds,忽略本次检测!",intervalSenonds); } ``` 这样就控制了加速计多次检测触发频率比较频繁的回调问题. ### 编写工具类 20240326更新,优化晃动算法,防止误触 然后写个工具类,把上述的内容全部放到一个工具类中供大家使用, 我们写一个MTCMMotionTool类用于封装加速计传感器的实现 //.h文件 ``` objc typedef NS_ENUM(NSUInteger, MTAccelerationAlgorithm) { MTAccelerationAlgorithmNormal = 0, //常规算法摇一摇 MTAccelerationAlgorithmLPF = 1, //低通滤波器来平滑加速度 减少误触 }; /** 利用门面模式,对外暴露统一接口 */ @interface MTCMMotionTool : NSObject /** 1.对于一般的摇一摇功能,阈值大小可以在1.0到2.0之间。 如果需要更高的灵敏度,可以选择较小的阈值,例如0.5到1.0。 如果需要较低的灵敏度,可以选择较大的阈值,例如2.0到3.0。 2.对于LPF低通录波器平滑算法,阈值大小参考范围 0.33~0.88 */ @property (nonatomic, assign) CGFloat accelerateThreshold; //加速计灵敏度阈值,Normal算法默认 2.45, LPF算法0.38(建议控制在0.33~0.88) @property (nonatomic, assign) CGFloat accelerateDetectedInterval; //加速计检查动作后的前后两次间隔时间,防止频繁检测执行 单位秒Senonds.default 1s. @property (nonatomic, copy) void (^didAcceleratorDectecdBlock)(void); @property (nonatomic, assign) MTAccelerationAlgorithm accelerationAlgorithm; //使用加速计 检测摇一摇算法类型 //启动加速计 - (void)startAccelerometer; //停止加速计 - (void)stopAccelerometer; @end ``` //.m文件 ``` objc #define kFilteringFactor 0.1 // 初始化低通滤波器 @interface MTCMMotionTool() @property (nonatomic, strong) CMMotionManager *motionManager; @property (nonatomic, strong) NSOperationQueue *cmMotionOperationQueue; @property (nonatomic, assign) CFAbsoluteTime beforeTime; /// 传统加速计暂存值 @property (nonatomic, assign) UIAccelerationValue accelerationX; @property (nonatomic, assign) UIAccelerationValue accelerationY; @property (nonatomic, assign) double currentRawReading; /// 低通滤波器平滑用到加速计 @property (nonatomic, assign) CMAcceleration previousAcceleration; @end @implementation MTCMMotionTool - (instancetype)init { self = [super init]; if (self) { self.accelerateThreshold = 0.0f; self.accelerateDetectedInterval = 1; //1s CMAcceleration acceleration; acceleration.x = 0; acceleration.y = 0; acceleration.z = 0; self.previousAcceleration = acceleration; self.beforeTime = CFAbsoluteTimeGetCurrent(); // 记录执行摇晃检测逻辑前的时间 } return self; } #pragma mark - #pragma mark - private methods 私有方法 //Note that when the updates are stopped, all operations in the given NSOperationQueue will be cancelled - (void)createOperationQueueIfNeeded { if (self.cmMotionOperationQueue == nil) { self.cmMotionOperationQueue = [[NSOperationQueue alloc] init]; } } - (void)startAccelerometerUpdates { [self createOperationQueueIfNeeded]; //按需创建队列,当队里中的各种传感器stop时 会自动移除operation. if (self.motionManager == nil) { self.motionManager = [[CMMotionManager alloc] init]; } if (self.motionManager.isAccelerometerAvailable) { self.motionManager.accelerometerUpdateInterval = 0.2; __weak typeof(self) weakSelf = self; [self.motionManager startAccelerometerUpdatesToQueue:self.cmMotionOperationQueue withHandler:^(CMAccelerometerData *accelerometerData, NSError *error) { __strong typeof(weakSelf) strongSelf = weakSelf; if (accelerometerData) { [self detectShake:accelerometerData.acceleration]; } }]; } } - (void)stopAccelerometerUpdates { if (self.motionManager) { [self.motionManager stopAccelerometerUpdates]; self.motionManager = nil; } } - (void)detectShake:(CMAcceleration)acceleration { if (self.accelerationAlgorithm == MTAccelerationAlgorithmNormal) { [self normalDetectShake:acceleration]; } else if (self.accelerationAlgorithm == MTAccelerationAlgorithmLPF) { [self lpfDetectShake:acceleration]; } else { //使用其它算法实现摇一摇 } } - (void)normalDetectShake:(CMAcceleration)acceleration { double threshold = self.accelerateThreshold; if (fabs(acceleration.x) > threshold || fabs(acceleration.y) > threshold || fabs(acceleration.z) > threshold) { // 检测到摇晃动作 CFAbsoluteTime afterTime = CFAbsoluteTimeGetCurrent(); // 记录执行摇晃检测逻辑后的时间 CFTimeInterval timeDifference = afterTime - self.beforeTime; // 计算时间差 单位秒 s CFTimeInterval intervalSenonds = self.accelerateDetectedInterval; if (timeDifference >= intervalSenonds) { //控制检测前后间隔 //NSLog(@"检测到摇晃动作,距离上次检测: 1f seconds", timeDifference); self.beforeTime = CFAbsoluteTimeGetCurrent(); // 记录执行摇晃检测逻辑前的时间 if (self.didAcceleratorDectecdBlock) { self.didAcceleratorDectecdBlock(); } } else { //NSLog(@"检测到摇晃动作,间隔不满足 1f seconds,忽略本次检测!",intervalSenonds); } } } //低通滤波器来平滑加速度数据,并计算加速度变化率。通过调整 kFilteringFactor 和阈值来适应具体需求,可以减少误触的可能性 - (void)lpfDetectShake:(CMAcceleration)acceleration { // 应用低通滤波器 CMAcceleration filteredAcceleration; filteredAcceleration.x = (acceleration.x * kFilteringFactor) + (self.previousAcceleration.x * (1.0 - kFilteringFactor)); filteredAcceleration.y = (acceleration.y * kFilteringFactor) + (self.previousAcceleration.y * (1.0 - kFilteringFactor)); filteredAcceleration.z = (acceleration.z * kFilteringFactor) + (self.previousAcceleration.z * (1.0 - kFilteringFactor)); // 计算加速度变化率 double deltaX = fabs(filteredAcceleration.x - self.previousAcceleration.x); double deltaY = fabs(filteredAcceleration.y - self.previousAcceleration.y); double deltaZ = fabs(filteredAcceleration.z - self.previousAcceleration.z); // 更新上一次加速度 self.previousAcceleration = filteredAcceleration; // 判断是否发生了摇晃 double threshold = self.accelerateThreshold; if (deltaX > threshold || deltaY > threshold || deltaZ > threshold) { // 检测到摇晃动作 CFAbsoluteTime afterTime = CFAbsoluteTimeGetCurrent(); // 记录执行摇晃检测逻辑后的时间 CFTimeInterval timeDifference = afterTime - self.beforeTime; // 计算时间差 单位秒 s CFTimeInterval intervalSenonds = self.accelerateDetectedInterval; if (timeDifference >= intervalSenonds) { //控制检测前后间隔 //NSLog(@"LFP算法检测到摇晃动作,距离上次检测: 1f seconds", timeDifference); self.beforeTime = CFAbsoluteTimeGetCurrent(); // 记录执行摇晃检测逻辑前的时间 if (self.didAcceleratorDectecdBlock) { self.didAcceleratorDectecdBlock(); } //NSLog(@"LFP算法检测到摇晃动,{2f,2f,2f}",deltaX, deltaY, deltaZ); } else { //NSLog(@"LFP算法检测到摇晃动作,间隔不满足 1f seconds,忽略本次检测!",intervalSenonds); } } } #pragma mark - #pragma mark - public methods 公有方法 - (void)startAccelerometer { [self startAccelerometerUpdates]; } - (void)stopAccelerometer { [self stopAccelerometerUpdates]; } #pragma mark - #pragma mark - UIAccelerometerDelegate #pragma mark- shake change song CGFloat KWCMMgrRadiansToDegrees(CGFloat radians) {return radians * 180/M_PI;} #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" #pragma clang diagnostic ignored "-Wdeprecated-implementations" -(void)accelerometer:(UIAccelerometer *)accelerometer didAccelerate:(UIAcceleration *)acceleration{ static double shakeDate = 0.0f; self.accelerationX = acceleration.x * kFilteringFactor + self.accelerationX * (1.0 - kFilteringFactor); self.accelerationY = acceleration.y * kFilteringFactor + self.accelerationY * (1.0 - kFilteringFactor); if (fabs(acceleration.x) >= self.self.accelerateThreshold|| fabs(acceleration.y) >= self.accelerateThreshold ) { if ([NSDate timeIntervalSinceReferenceDate] - shakeDate > self.accelerateDetectedInterval) { self.accelerationX = acceleration.x * kFilteringFactor + self.accelerationX * (1.0 - kFilteringFactor); self.accelerationY = acceleration.y * kFilteringFactor + self.accelerationY * (1.0 - kFilteringFactor); self.currentRawReading =atan2(self.accelerationY, self.accelerationX); float rotation = -KWCMMgrRadiansToDegrees(self.currentRawReading); if (fabsf(rotation) > 70.0 ) { if (self.didAcceleratorDectecdBlock) { self.didAcceleratorDectecdBlock(); } shakeDate = [NSDate timeIntervalSinceReferenceDate]; } } } } #pragma mark - #pragma mark - life cycle 视图的生命周期 - (void)dealloc { if (self.cmMotionOperationQueue) { [self.cmMotionOperationQueue cancelAllOperations]; self.cmMotionOperationQueue = nil; } } @end ``` 以上就是 CMMotionManager方案的实现代码. ## UIAccelerometer 这个类远古时期的方案,从iOS2.0~iOS5.0的方式, 现在都iOS17时代了,我觉得它应该领退休金了,可是它坚持依然坚守岗位,依然在发挥作用. ``` objc UIKIT_EXTERN API_DEPRECATED("UIAcceleration has been replaced by the CoreMotion framework", ios(2.0, 5.0)) API_UNAVAILABLE(visionos) API_UNAVAILABLE(tvos) NS_SWIFT_UI_ACTOR ``` 使用起来比较简单粗暴. ``` objc if (enableShake) { [[UIAccelerometer sharedAccelerometer] setDelegate:nil]; [[UIAccelerometer sharedAccelerometer] setDelegate:self]; [[UIAccelerometer sharedAccelerometer] setUpdateInterval:0.1]; } else { [[UIAccelerometer sharedAccelerometer] setDelegate:nil]; } ``` 然后实现代理后. ``` objc - (void)accelerometer:(UIAccelerometer *)accelerometer didAccelerate:(UIAcceleration *)acceleration { acceleration.x ... acceleration.y ... acceleration.z ... ... 做和之前CMMotionManager回调中同样逻辑check就好. ... } ``` 这个`UIAcceleration`类在iOS6之前是一个class ``` objc @interface UIAcceleration : NSObject @property(nonatomic,readonly) NSTimeInterval timestamp; @property(nonatomic,readonly) UIAccelerationValue x; @property(nonatomic,readonly) UIAccelerationValue y; @property(nonatomic,readonly) UIAccelerationValue z; @end ``` 在后来的传感器整合中 变成了结构体, 如果非要说UIAcceleration有啥优势的话,莫过于它不用在plist中添加隐私描述就能拿到用户传感器数据,不知道有没有啥适配问题如果不加隐私描述. ### 2024年3月26日更新 增加低通滤波器平滑算法,防止摇晃导致误触. ``` objc //低通滤波器来平滑加速度数据,并计算加速度变化率。通过调整 kFilteringFactor 和阈值来适应具体需求,可以减少误触的可能性 - (void)lpfDetectShake:(CMAcceleration)acceleration { // 应用低通滤波器 CMAcceleration filteredAcceleration; filteredAcceleration.x = (acceleration.x * kFilteringFactor) + (self.previousAcceleration.x * (1.0 - kFilteringFactor)); filteredAcceleration.y = (acceleration.y * kFilteringFactor) + (self.previousAcceleration.y * (1.0 - kFilteringFactor)); filteredAcceleration.z = (acceleration.z * kFilteringFactor) + (self.previousAcceleration.z * (1.0 - kFilteringFactor)); // 计算加速度变化率 double deltaX = fabs(filteredAcceleration.x - self.previousAcceleration.x); double deltaY = fabs(filteredAcceleration.y - self.previousAcceleration.y); double deltaZ = fabs(filteredAcceleration.z - self.previousAcceleration.z); // 更新上一次加速度 self.previousAcceleration = filteredAcceleration; // 判断是否发生了摇晃 double threshold = self.accelerateThreshold; if (deltaX > threshold || deltaY > threshold || deltaZ > threshold) { // 检测到摇晃动作 CFAbsoluteTime afterTime = CFAbsoluteTimeGetCurrent(); // 记录执行摇晃检测逻辑后的时间 CFTimeInterval timeDifference = afterTime - self.beforeTime; // 计算时间差 单位秒 s CFTimeInterval intervalSenonds = self.accelerateDetectedInterval; if (timeDifference >= intervalSenonds) { //控制检测前后间隔 //NSLog(@"LFP算法检测到摇晃动作,距离上次检测: 1f seconds", timeDifference); self.beforeTime = CFAbsoluteTimeGetCurrent(); // 记录执行摇晃检测逻辑前的时间 if (self.didAcceleratorDectecdBlock) { self.didAcceleratorDectecdBlock(); } //NSLog(@"LFP算法检测到摇晃动,{2f,2f,2f}",deltaX, deltaY, deltaZ); } else { //NSLog(@"LFP算法检测到摇晃动作,间隔不满足 1f seconds,忽略本次检测!",intervalSenonds); } } } ``` # 总结 以上就是几种不同方式检测类似摇晃、摇一摇功能的代码,按需索取,复杂一些就选择CMMotionManager,如果就简单想实现摇一摇就选择系统的事件就好了, 非常规情况下使用UIAcceleration. 当然大家也可以把三种方案都封装一下内部可以通过选择来控制使用哪种. 以上就是运动传感器的加速计在应用中的实现优化,水文见笑见笑. [参考CMDevice​Motion](https://nshipster.com/cmdevicemotion/) [Swift – 實現搖一搖功能](https://badgameshow.com/steven/swift/https-badgameshow-com-steven-195/) URL: https://sunyazhou.com/2024/01/mpremotecommandlikecommand/index.html.md Published At: 2024-01-25 05:03:00 +0000 # iOS控制中心收藏按钮likeCommand动画 # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! ## 背景描述 最近开发中,产品要求在音频播放控制中心中增加 收藏按钮, 查看了QQ音乐和网易云音乐发现他们都已经有了此按钮,然而在酷我音乐的app切后台锁屏后这个按钮没有漏出,查了一下代码发现按钮没有添加,于是加了按钮发现没有动画效果. 就此问题查了全网没有一个说清楚的,经过反复测试发现,苹果给我们提供了一个新的api我们没有注意. 先看看做完啥效果, ![](/assets/images/20240125MPRemoteCommandLikecommand/MPRemoteCommand.gif) 这里要用到的关键代码api如下: ``` objc @interface MPFeedbackCommand : MPRemoteCommand /// Whether the feedback command is in an "active" state. An example of when a /// feedback command would be active is if the user already "liked" a particular /// content item. @property (nonatomic, assign, getter = isActive) BOOL active; //就是这个 ... @end ``` 下面是完整的添加此功能的代码. ``` objc if (@available(iOS 17.1, *)) { MPRemoteCommandCenter *center = [MPRemoteCommandCenter sharedCommandCenter]; [center.likeCommand setEnabled:YES]; [center.likeCommand setLocalizedTitle:@"收藏"]; [center.likeCommand setLocalizedShortTitle:@"收藏此歌曲"]; //TODO: check 是否 已收藏 [center.likeCommand setActive:NO]; //假设默认此歌曲没有被收藏的效果是没有 电量喜欢 [center.likeCommand addTargetWithHandler:^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent * _Nonnull event) { // ... 处理收藏歌曲逻辑的代码 此处省略 if (@available(iOS 17.1, *)) { MPFeedbackCommand *likeCommand = (MPFeedbackCommand *)event.command; if (likeCommand && likeCommand.isEnabled) { BOOL lastActive = likeCommand.isActive; [likeCommand setActive:!lastActive]; //TODO: 此处代码模拟已收藏和取消收藏,这里得到结果后 再次设置Active将会出现动画效果 } } return MPRemoteCommandHandlerStatusSuccess; //如果点击收藏成功可以返回这个状态 }]; }]; } ``` 以上是实现 收藏动效的全部代码. ## 踩坑记录 这里注意下 `MPRemoteCommandCenter `中 有如下代码 ``` objc // Feedback Commands // These are generalized to three distinct actions. Your application can provide // additional context about these actions with the localizedTitle property in // MPFeedbackCommand. @property (nonatomic, readonly) MPFeedbackCommand *likeCommand; @property (nonatomic, readonly) MPFeedbackCommand *dislikeCommand; ``` 一开始我尝试使用disklikeCommand按钮,结果发现没有效果, 苹果这个操作我也是真没看懂,如果想要一个按钮解决两种状态 那为何不提供这个按钮可以选中的状态,为啥搞两个按钮迷惑开发者,整得我读完文档我也没有理解一个按钮能出动效,两个按钮就不能出动效的方式. 这是一个可以被废弃的按钮,留着意义不大,建议苹果官方看到的话去掉吧!至少能让开发者少走一些弯路 # 总结 开发中总会出现一些奇奇怪怪的API,善于积累,勤于实践,记录过程,解决问题,以上就是本章的全部内容. URL: https://sunyazhou.com/2024/01/arktsbasic/index.html.md Published At: 2024-01-19 02:19:00 +0000 # ArkTS和ArkUI基础语法 ![](/assets/images/20240116HarmonyPhoneSendFileTomacOS/harmonyOS.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! 以下内容是学习记录 ## DevEco Studio快捷键 | 快捷键 | 用途 | 备注 | | ------| ------ | ------ | | ⌘(Command) + `B` | 进入到类或者对象的定义文件中中 | 类似Xcode中的 ⌘(Command) + `→`| | ⌘(Command) + ⇧(Shift) + ⌫(Back) | 与上面相反,返回上一级 | 类似Xcode中的 ⌘(Command) + `←`| | | | | | | | | 在看过几遍鸿蒙教程视频和文档后,我觉得把容易遗忘的基础都记录下来,以备后续使用的时候随时查找. ## ArkTS基础部分 ### 页面和自定义组件组成生命周期 首先我们要了解一下一个组件是组成UI的基本单元,我们要明确自定义组件和页面的关系 * 自定义组件:`@Component`装饰的UI单元,可以组合多个系统组件实现UI的复用,可以调用组件的生命周期。 * 页面:即应用的UI页面。可以由一个或者多个自定义组件组成,[@Entry](https://developer.harmonyos.com/cn/docs/documentation/doc-guides-V2/arkts-create-custom-components-0000001580025742-V2#ZH-CN_TOPIC_0000001711026924__%E8%87%AA%E5%AE%9A%E4%B9%89%E7%BB%84%E4%BB%B6%E7%9A%84%E5%9F%BA%E6%9C%AC%E7%BB%93%E6%9E%84)装饰的自定义组件为页面的入口组件,即页面的根节点,一个页面有且仅能有一个@Entry。只有被@Entry装饰的组件才可以调用页面的生命周期。 ``` ts @Entry @Component struct LiftCycle { build() { ... } } ``` * struct:自定义组件基于struct实现,struct + 自定义组件名 + {...}的组合构成自定义组件,不能有继承关系。对于struct的实例化,可以省略new (__自定义组件名、类名、函数名不能和系统组件名相同。__) * @Component:@Component装饰器仅能装饰struct关键字声明的数据结构。struct被@Component装饰后具备组件化的能力,需要实现build方法描述UI,一个struct只能被一个@Component装饰。(__从API version 9开始,该装饰器支持在ArkTS卡片中使用。__) * build()函数:build()函数用于定义自定义组件的声明式UI描述,自定义组件必须定义build()函数。 * @Entry:@Entry装饰的自定义组件将作为UI页面的入口。在单个UI页面中,最多可以使用@Entry装饰一个自定义组件。@Entry可以接受一个可选的LocalStorage的参数。 > 从API version 9开始,该装饰器支持在ArkTS卡片中使用。 > 从API version 10开始,@Entry可以接受一个可选的[LocalStorage](https://developer.harmonyos.com/cn/docs/documentation/doc-guides-V2/arkts-localstorage-0000001630265133-V2)的参数或者一个可选的EntryOptions参数。 #### EntryOptions10+ 命名路由跳转选项 | 名称 | 类型 | 必填 | 说明 | | ------| ------ | ------ | ------ | | routeName | string | 否 | 表示作为命名路由页面的名字。 | | storage | [LocalStorage](https://developer.harmonyos.com/cn/docs/documentation/doc-guides-V2/arkts-localstorage-0000001630265133-V2) | 否 | 页面级的UI状态存储。 | ``` ts @Entry({ routeName : 'myPage' }) @Component struct MyComponent { } ``` * @Reusable:@Reusable装饰的自定义组件具备可复用能力 ``` ts @Reusable @Component struct MyComponent { } ``` > 从API version 10开始,该装饰器支持在ArkTS卡片中使用。 ### 页面和组件的生命周期 被?**@Entry**装饰的组件生命周期,提供以下生命周期接口: * `onPageShow`:页面每次显示时触发一次,包括路由过程、应用进入前台等场景,仅@Entry装饰的自定义组件生效。 * `onPageHide`:页面每次隐藏时触发一次,包括路由过程、应用进入后台等场景,仅@Entry装饰的自定义组件生效。 * `onBackPress`:当用户点击返回按钮时触发,仅@Entry装饰的自定义组件生效。 //被@Entry装饰的组件 的生命周期 代码如下演示 ``` ts //页面每次显示的时候被触发 onPageShow(): void { console.log("LiftCycle onPageShow") } //页面每次隐藏的时候被触发 onPageHide(): void { console.log("LiftCycle onPageHide") } //点击返回按钮时触发 onBackPress(): boolean | void { console.log("LiftCycle onBackPress") } ``` 组件生命周期,即一般用**@Component**装饰的自定义组件的生命周期,提供以下生命周期接口: * `aboutToAppear`:组件即将出现时回调该接口,具体时机为在创建自定义组件的新实例后,在执行其build()函数之前执行。 * `aboutToDisappear`:aboutToDisappear函数在自定义组件析构销毁之前执行。不允许在aboutToDisappear函数中改变状态变量,特别是@Link变量的修改可能会导致应用程序行为不稳定 ``` ts //被@Component修饰的 自定义组件的生命周期 aboutToAppear(): void { console.log("LiftCycle aboutToAppear") } aboutToDisappear(): void { console.log("LiftCycle aboutToDisappear") } ``` 生命周期流程如下图所示,下图展示的是被**@Entry装饰的组件(首页)生命周期。 ![](/assets/images/20240119ArkTSBasic/EntryLifeCycle.webp) **由此可知, @Component组件的声明周期方法 中间包含了@Entry方法全部生命周期方法调用.** 示例代码演示了一个LifeCycle中 添加一个Child子组件,点击按钮push到新页面LifeCycleDetail ``` ts // LiftCycle.ets import router from '@ohos.router'; @Entry @Component struct LiftCycle { @State showChild: boolean = true; @State btnColor:string = "#FF007DFF" // 组件生命周期 aboutToAppear() { console.info('LiftCycle aboutToAppear'); } // 只有被@Entry装饰的组件才可以调用页面的生命周期 onPageShow() { console.info('LiftCycle onPageShow'); } // 只有被@Entry装饰的组件才可以调用页面的生命周期 onPageHide() { console.info('LiftCycle onPageHide'); } // 只有被@Entry装饰的组件才可以调用页面的生命周期 onBackPress() { console.info('LiftCycle onBackPress'); this.btnColor ="#FFEE0606" return true // 返回true表示页面自己处理返回逻辑,不进行页面路由;返回false表示使用默认的路由返回逻辑,不设置返回值按照false处理 } // 组件生命周期 aboutToDisappear() { console.info('LiftCycle aboutToDisappear'); } build() { Column() { // this.showChild为true,创建Child子组件,执行Child aboutToAppear if (this.showChild) { Child() } // this.showChild为false,删除Child子组件,执行Child aboutToDisappear Button('delete Child') .margin(20) .backgroundColor(this.btnColor) .onClick(() => { this.showChild = false; }) // push到page页面,执行onPageHide Button('push to next page') .onClick(() => { router.pushUrl({ url: 'pages/LifeCycleDetail' }); }) } } } @Component struct Child { @State title: string = 'SUNYAZHOU.COM'; // 组件生命周期 aboutToAppear() { console.info('Child aboutToAppear') } // 组件生命周期 aboutToDisappear() { console.info('Child aboutToDisappear') } build() { Text(this.title).fontSize(50).margin(20).onClick(() => { this.title = 'SUNYAZHOU.COM ArkUI'; }) } } ``` LifeCycleDetail代码如下 ``` ts @Entry @Component struct LifeCycleDetail { @State textColor: Color = Color.Black; @State num: number = 0 onPageShow() { this.num = 5 console.log("LifeCycleDetail onPageShow"); } onPageHide() { console.log("LifeCycleDetail onPageHide"); } onBackPress() { // 不设置返回值按照false处理 this.textColor = Color.Grey this.num = 0 console.log("LifeCycleDetail onBackPress"); } aboutToAppear() { this.textColor = Color.Blue } build() { Column() { Text(`num 的值为:${this.num}`) .fontSize(30) .fontWeight(FontWeight.Bold) .fontColor(this.textColor) .margin(20) .onClick(() => { this.num += 5 }) } .width('100%') } } ``` 当我们启动预览的时候声明周期函数如下: ``` sh app Log: LiftCycle aboutToAppear app Log: Child aboutToAppear app Log: LiftCycle onPageShow ``` 当我们点击Push的时候 ``` sh app Log: LiftCycle onPageHide app Log: LifeCycleDetail onPageShow ``` 点击返回的时候 ``` sh LifeCycleDetail onBackPress LifeCycleDetail onPageHide LiftCycle onPageShow ``` 删除 Child的时候 ``` sh app Log: Child aboutToDisappear ``` ![](/assets/images/20240119ArkTSBasic/EntryLifeCycle.gif) [页面和自定义组件生命周期 官方文档](https://developer.harmonyos.com/cn/docs/documentation/doc-guides-V2/arkts-page-custom-components-lifecycle-0000001630265125-V2) ### 基础类型和函数方法 ``` ts let number1: number = 99 // 默认情况下 正常情况下给的数字 就是十进制的哦 let number2: number = 0b10011 // 2进制 由0b开头的 let number3: number = 0o1234567 // 8进制 由0o开头的 let number4: number = 0x6464ab // 16进制 由日x开头的 // TODO 字符串 let string1: string = 'sunyazhou' let string2: string = "sunyazhou" let string3:string = "你的名字是: ${string2}" // TODO 联合类型、 布尔 真ture/假false let objectType : string | number | boolean objectType = true objectType = "sunyazhou" objectType = 635464 objectType = false // TODO 数组 let stringArray1: Array = ['AAA','BBB','CCC']; //0下标开始的 let stringArray2: string[] = ['AAA','BBB','CCC']; // TODO 枚举 enum Color {Red, Green, Yellow}; let color: Color = Color.Red; // TODO 元组 和swift中的元组一样,可以理解为多类型的字典,key都是字符串 value是不同的数据类型 let name1:[string, number]; name1 = [@"孙先生", 20]; //必须按照规定顺序和类型写内容 // TODO void 无返回类型型 function name(params): void {} // Null let str1: null = null // undefined let str2: undefined = undefined ``` ### 作用域范围 ``` ts @Entry @Component struct LearnDetail { @State message: string = 'Hello World'; // 里面不加let,外面的成员需要加let number1: number = 99 build() { Row() { Column() { Text(this.message) .fontSize(50) .fontWeight(FontWeight.Bold) } .width('100%') } .height('100%') } } ``` ## ArkUI部分 ### 图片控件 加载 常规本地资源 ``` ts Image($r(app.media.icon)) ``` 加载 网络资源 ``` ts Image("https://www.sunyazhou.com/assets/images/20240116HarmonyPhoneSendFileTomacOS/harmonyOS.webp") ``` 加载 本地任何资源 ``` ts Image($rawfile("sunyazhou.png")) ``` ### 装饰器@Styles @Styles装饰器可以将多条样式设置提炼成一个方法,直接在组件声明的位置调用。通过@Styles装饰器可以快速定义并复用自定义样式。用于快速定义并复用自定义样式. * 当前@Styles仅支持通用属性和通用事件。 * @Styles方法不支持参数 > 从API version 9开始,该装饰器支持在ArkTS卡片中使用。 使用全局的@Styles封装的样式 ``` ts @Styles function globalStyles() { .width(150) .height(300) .backgroundColor(Color.Pink) } ``` 定义在组件内的@Styles封装的样式 ``` ts struct LearnDetail { @State heightValue: number = 100 // 定义在组件内的@Styles封装的样式 @Styles innerStyle() { .width(200) .height(this.heightValue) .backgroundColor(Color.Yellow) .onClick(() => { this.heightValue = 200 }) } build() { ... } } ``` 如何使用 ``` ts @Entry @Component struct LearnDetail { @State heightValue: number = 100 // 定义在组件内的@Styles封装的样式 @Styles innerStyle() { .width(200) .height(this.heightValue) .backgroundColor(Color.Yellow) .onClick(() => { this.heightValue = 200 }) } build() { Row() { Column() { // 使用全局的@Styles封装的样式 Text('sunyazhou.com') .globalStyles () .fontSize(30) // 使用组件内的@Styles封装的样式 Text('迈腾大队长') .innerStyle() .fontSize(30) } .width('100%') } .height('100%') } } @Styles function globalStyles() { .width(150) .height(300) .backgroundColor(Color.Pink) } ``` 以上是是如何使用 @Styles装饰器的代码, [参考官方@Styles文档](https://developer.harmonyos.com/cn/docs/documentation/doc-guides-V2/arkts-style-0000001630145729-V2) ### @Extend装饰器: 定义扩展组件样式 装饰器使用语法 ``` ts @Extend(UIComponentName) function functionName { ... } ``` * 和@Styles不同,@Extend仅支持在全局定义,不支持在组件内部定义。 * 和@Styles不同,@Extend支持封装指定的组件的私有属性和私有事件,以及预定义相同组件的@Extend的方法。 * 和@Styles不同,@Extend装饰的方法支持参数,开发者可以在调用时传递参数,调用遵循TS方法传值调用。 * @Extend装饰的方法的参数可以为function,作为Event事件的句柄 * @Extend的参数可以为状态变量,当状态变量改变时,UI可以正常的被刷新渲染。 * @Extend可以协变调用 如下调用协变调用 ``` ts // @Extend(Text)可以支持Text的私有属性fontColor @Extend(Text) function fancy () { .fontColor(Color.Red) } // superFancyText可以调用预定义的fancy @Extend(Text) function superFancyText(size:number) { .fontSize(size) .fancy() //这里调用的是上方定义的@extend } ``` 使用@Extend示例代码如下: ``` ts @Entry @Component struct LearnDetail { @State heightValue: number = 100 build() { Row() { Column() { Text("sunyazhou.com").textExtend1(20, Color.Green) Text("迈腾大队长") .textExtend1(20, Color.Blue) } .width('100%') } .height('100%') } } @Extend(Text) function textStyles1() { .textAlign(TextAlign.Center) .fontStyle(FontStyle.Italic) .decoration({ type: TextDecorationType.Underline }) } @Extend(Text) function textExtend1(fontSize: number, fontColor: Color) { .fontSize(fontSize) .fontColor(fontColor) .textStyles1() } ``` ![](/assets/images/20240119ArkTSBasic/extend_example.webp) [参考@Extend官方文档](https://developer.harmonyos.com/cn/docs/documentation/doc-guides-V2/arkts-extend-0000001580345074-V2) ### @Prop装饰器:父子单向同步 初始化规则图示 ![](/assets/images/20240119ArkTSBasic/rules.webp) 下面是单向传递示例代码 * Prop不能赋值 ``` ts @Entry @Component struct LearnDetail { @State msg: string = "sunyazhou.com" build() { Row() { Column() { Text(this.msg).textExtend1(30, Color.Green) Button("点击修改传透到子组件",{type: ButtonType.Normal}) .borderRadius(8) .backgroundColor(0x317aff) .width(180) .height(40) .onClick(()=>{ console.log('点击修改传透到子组件') this.msg = this.msg === "sunyazhou.com" ? "迈腾大队长" : "sunyazhou.com" }) LearnDetailProp1({name :this.msg}) } .width('100%') } .height('100%') } } // @prop装饰状态数据,方便父与子组件之问进行数据传递与同步 父State--------->prop 单向 @Component struct LearnDetailProp1 { @Prop name: string //Prop不能赋值 build() { Column() { Text("www." + this.name).textStyles1() Button("单向传递").buttonStyle1(ButtonType.Normal) .onClick(()=>{ this.name = "Prop修饰器修改内容" }) } } } @Extend(Button) function buttonStyle1 (type :ButtonType) { .type(type) .borderRadius(8) .backgroundColor(0x317aff) .width(90) .height(40) } @Extend(Text) function textStyles1() { .textAlign(TextAlign.Center) .fontStyle(FontStyle.Italic) .decoration({ type: TextDecorationType.Underline }) } @Extend(Text) function textExtend1(fontSize: number, fontColor: Color) { .fontSize(fontSize) .fontColor(fontColor) .textStyles1() } ``` ![](/assets/images/20240119ArkTSBasic/prop.gif) [@Prop参考文档](https://developer.harmonyos.com/cn/docs/documentation/doc-guides-V2/arkts-prop-0000001580185150-V2) ### @Link装饰器:父子双向同步 示例代码如果 ``` ts // @Link装饰状态数据,方便父与子组件之问进行数据传递与同步 父State <--------->prop 双向传递 @Component struct LearnDetailLink1 { @Link lineName: string //@Link不能赋值 build() { Column() { Text("Link数据:" + this.lineName).textStyles1() Button("双向传递").buttonStyle1(ButtonType.Normal) .onClick(()=> { this.lineName = "被修改的 Link数据" }) } } } ``` 效果展示 ![](/assets/images/20240119ArkTSBasic/link.gif) 基于上述@Prop代码完整展示 ``` ts @Entry @Component struct LearnDetail { @State msg: string = "sunyazhou.com" build() { Row() { Column() { Text(this.msg).textExtend1(30, Color.Green) Button("点击修改传透到子组件",{type: ButtonType.Normal}) .borderRadius(8) .backgroundColor(0x317aff) .width(180) .height(40) .onClick(()=>{ console.log('点击修改传透到子组件') this.msg = this.msg === "sunyazhou.com" ? "迈腾大队长" : "sunyazhou.com" }) Divider() LearnDetailProp1({name :this.msg}) Divider() LearnDetailLink1({lineName :this.msg}) } .width('100%') } .height('100%') } } @Component struct LearnDetailProp1 { @Prop name: string //Prop不能赋值 build() { Column() { Text("www." + this.name).textStyles1() Button("单向传递").buttonStyle1(ButtonType.Normal) .onClick(()=>{ this.name = "Prop修饰器修改内容" }) } } } // @Link装饰状态数据,方便父与子组件之问进行数据传递与同步 父State <--------->prop 双向传递 @Component struct LearnDetailLink1 { @Link lineName: string //@Link不能赋值 build() { Column() { Text("Link数据:" + this.lineName).textStyles1() Button("双向传递").buttonStyle1(ButtonType.Normal) .onClick(()=> { this.lineName = "被修改的 Link数据" }) } } } @Extend(Button) function buttonStyle1 (type :ButtonType) { .type(type) .borderRadius(8) .backgroundColor(0x317aff) .width(90) .height(40) } @Extend(Text) function textStyles1() { .textAlign(TextAlign.Center) .fontStyle(FontStyle.Italic) .decoration({ type: TextDecorationType.Underline }) } @Extend(Text) function textExtend1(fontSize: number, fontColor: Color) { .fontSize(fontSize) .fontColor(fontColor) .textStyles1() } ``` [@Link参考文档](https://developer.harmonyos.com/cn/docs/documentation/doc-guides-V2/arkts-link-0000001630145733-V2) ## @Provide装饰器和@Consume装饰器:与后代组件双向同步 @Provide和@Consume,应用于与后代组件的双向数据同步,应用于状态数据在多个层级之间传递的场景。不同于上文提到的父子组件之间通过命名参数机制传递,@Provide和@Consume摆脱参数传递机制的束缚,实现跨层级传递。 其中@Provide装饰的变量是在祖先组件中,可以理解为被“提供”给后代的状态变量。@Consume装饰的变量是在后代组件中,去“消费(绑定)”祖先组件提供的变量。 #### @Provide/@Consume装饰的状态变量有以下特性: * @Provide装饰的状态变量自动对其所有后代组件可用,即该变量被“provide”给他的后代组件。由此可见,@Provide的方便之处在于,开发者不需要多次在组件之间传递变量。 * 后代通过使用@Consume去获取@Provide提供的变量,建立在@Provide和@Consume之间的双向数据同步,与@State/@Link不同的是,前者可以在多层级的父子组件之间传递。 * @Provide和@Consume可以通过相同的变量名或者相同的变量别名绑定,建议类型相同,否则会发生类型隐式转换,从而导致应用行为异常。 ``` ts // 通过相同的变量名绑定 @Provide a: number = 0; @Consume a: number; // 通过相同的变量别名绑定 @Provide('a') b: number = 0; @Consume('a') c: number; ``` 显然这修饰器是统一标识 类型一直 根据文档说明如下 | @Provide变量装饰器 | 说明 | | ------| ------ | | 装饰器参数 | 别名:常量字符串,可选。如果指定了别名,则通过别名来绑定变量;如果未指定别名,则通过变量名绑定变量。 | | 同步类型 | 双向同步。从@Provide变量到所有@Consume变量以及相反的方向的数据同步。双向同步的操作与@State和@Link的组合相同。 | ... 更多内容请参照官方文档[官网文档](https://developer.harmonyos.com/cn/docs/documentation/doc-guides-V2/arkts-provide-and-consume-0000001580345078-V2) ``` ts @Entry @Component struct ProvideConsumeDemo { @Provide("com.sunyazhou.message.provide_consume") message: string = "sunyazhou.com" build() { Row() { Column() { Text(this.message).textExtend2(30, Color.Black) .onClick( ()=> { this.message = this.message === "迈腾大队长"? "sunyazhou.com": "迈腾大队长" }) Divider() //... 假设这里中间有 100层Component创建和使用 ProvideConsumeDemo2() } .width('100%') } .height('100%') } } @Component struct ProvideConsumeDemo2 { @Consume("com.sunyazhou.message.provide_consume") info: string //和之前介绍的@Prop @Link一样 consume不能赋值 build() { Column() { Text(this.info).textExtend2(45, Color.Green) } } } @Extend(Button) function buttonStyle2 (type :ButtonType) { .type(type) .borderRadius(8) .backgroundColor(0x317aff) .width(90) .height(40) } @Extend(Text) function textStyles2() { .textAlign(TextAlign.Center) .fontStyle(FontStyle.Italic) .decoration({ type: TextDecorationType.Underline }) } @Extend(Text) function textExtend2(fontSize: number, fontColor: Color) { .fontSize(fontSize) .fontColor(fontColor) .textStyles2() ``` 效果如下: ![](/assets/images/20240119ArkTSBasic/provideconsume.gif) [@Provide装饰器和@Consume装饰器官网文档](https://developer.harmonyos.com/cn/docs/documentation/doc-guides-V2/arkts-provide-and-consume-0000001580345078-V2) ## @Watch修饰器 用于监听状态变量更改通知 @Watch应用于对状态变量的监听。如果开发者需要关注某个状态变量的值是否改变,可以使用@Watch为状态变量设置回调函数。 ``` ts @State @Watch("didMessageChanged") num1: number = 10; didMessageChanged () { //此方法被触发,代表其它地方修改了 @Watch 修饰的变量 console.log("监听到消息发生变化:" + this.num1) } ``` ![](/assets/images/20240119ArkTSBasic/watch.gif) 完整示例代码 ``` ts @Entry @Component struct WatchDemo { @State @Watch("didMessageChanged") price: number = 0; didMessageChanged () { //此方法被触发,代表其它地方修改了 @Watch 修饰的变量 if (this.price >= 10) { //TODO: 处理享受9折... console.log("监听到消息发生变化:" + this.price * 0.9) } else { console.log("监听到消息发生变化:" + this.price) } } build() { Row() { Column() { Text("测试值" + this.price) .fontSize(50) .fontWeight(FontWeight.Bold) .onClick( ()=> { this.price ++ }) } .width('100%') } .height('100%') } } ``` [@Watch装饰器官方文档](https://developer.harmonyos.com/cn/docs/documentation/doc-guides-V2/arkts-watch-0000001630305681-V2) ## ForEach:循环渲染 假设我们要做一个像iOS中的UITableView列表我们可以使用ArkUI中的`ForEach` ``` ts @Entry @Component struct ForEachDemo { @State message: string = 'sunyazhou.com'; @State tags: Array = ['Algorithm29','ArkTS1','AVFoundation15','AVKit1','C++19','Cocoapods5','Dart2','Git3','HarmonyOS3','iOS119','...'] build() { Row() { Column() { Text(this.message) .fontSize(38) .fontWeight(FontWeight.Bold) Divider() ForEach(this.tags, (tag : string) => { Text("Blog tag has "+ tag) .textAlign(TextAlign.Start) .fontSize(18) .width('80%') .backgroundColor('#00E5EE') }, (tag: string)=>{ return tag }) } .width('100%') } .height('100%') } } ``` ![](/assets/images/20240119ArkTSBasic/ForEach.webp) 这里有个坑, __ForEach(this.tags, (tag : string*这里必须标注类型在Harmonry4.1中*) => {}__ 如果不标注类型就容易报错 ``` sh Use explicit types instead of "any", "unknown" (arkts-no-any-unknown) ``` ![](/assets/images/20240119ArkTSBasic/ForEachError.webp) [ForEach:循环渲染](官方文档) ## 组件通用特性-点击事件 我们可以通过点击事件对象拿到相应的位置信息. ``` ts @Entry @Component struct UniversalEventDemo { @State message: string = 'https://www.sunyazhou.com/'; //TODO 所有的 组件 的 通用特性之 事件系 build() { Column(){ Row() { Button('按钮1', {type: ButtonType.Normal}).width('100').height('66') .onClick((event: ClickEvent) => { this.message = `屏幕X:${event.windowX} \n屏幕Y:${event.windowY} \n按钮X:${event.x} \n按钮Y:${event.y} \n宽度:${event.target.area.width} \n高度:${event.target.area.height}` }) } Text(this.message).margin(20).fontSize(12) }.height('100%').alignItems(HorizontalAlign.Start).padding({top: 33, left: 50}) } } ``` `ClickEvent `类可以拿到如下各种变量 ![](/assets/images/20240119ArkTSBasic/ClickEvent.webp) ## 组件通用特性-触摸事件 ``` ts @Entry @Component struct UniversalEventDemo { @State message: string = 'https://www.sunyazhou.com/'; @State eventType :string = '' build() { Column(){ Row() { Button('按钮1', {type: ButtonType.Normal}).width('100').height('66') .onTouch((event: TouchEvent)=> { if (event.type == TouchType.Down) { this.eventType = '按下-Down' } if (event.type == TouchType.Up) { this.eventType = '抬起-Up' } if (event.type == TouchType.Move) { this.eventType = '触摸中-Move' } this.message = '触摸类型:'+ this.eventType + '\n' + 'x:' + event.touches[0].x + '\n' + 'y:' + event.touches[0].y + '\n' + '宽度:' + event.target.area.width + '\n' '高度:' + event.target.area.height + '\n' }) } Text(this.message).margin(20).fontSize(12) }.height('100%').alignItems(HorizontalAlign.Start).padding({top: 33, left: 50}) } } ``` ![](/assets/images/20240119ArkTSBasic/TouchEvent.gif) ## 组件通用的尺寸排版学习 ``` ts @Entry @Component struct LayoutDemo { build() { Column() { Text('组件通用的尺寸排版学习') Divider() Row() { Text('https://www.sunyazhou.com/').fontSize(20).fontColor(Color.Green).width('90%') .textAlign(TextAlign.Center) } .backgroundColor("#00F5FF") Row(){ Text('左侧').fontSize(20).backgroundColor(Color.Yellow).height(100) Row() { Row() { Text('本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. ' + '本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或使用,' + '请尊重版权并且保留原文链接,谢谢您的理解合作.' + ' 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,' + '这样您将能在第一时间获取本站信息.') .fontSize(15) .fontColor(Color.Pink) .width('90%') } } .width(200) .height(200) .backgroundColor(Color.Gray) .padding(20) //外边距 .margin({top: 28, bottom: 28, left:20, right:20}) //内边距 .border({width: 10, color: Color.Blue}) //内部边框 Text('右侧').fontSize(22).backgroundColor(Color.Red).backgroundColor(Color.Green) } Row() { Text('© 2024 sunyazhou. 保留部分权利').fontSize(20).fontColor(Color.White).width('90%') .textAlign(TextAlign.Center) } .backgroundColor(Color.Orange) } .backgroundColor(Color.Transparent) } } ``` ![](/assets/images/20240119ArkTSBasic/LayoutStudy1.webp) `position`和`markAnchor`,以及`offset`的使用如下. ``` ts @Entry @Component struct LayoutDemo2 { build() { Column({space:8}) { // Row() { Text('A').fontSize(24).fontColor(Color.Blue).width('25%').backgroundColor(Color.Red) Text('B').fontSize(24).fontColor(Color.Blue).width('25%').backgroundColor(Color.Black) Text('C').fontSize(24).fontColor(Color.Blue).width('25%').backgroundColor(Color.Yellow) Text('D').fontSize(24).fontColor(Color.Blue).width('25%').backgroundColor(Color.Grey) } .backgroundColor(Color.Green) .width('100%') .height(100) .direction(Direction.Rtl) Divider() Column({space: 8}) { Row() { Text('A').fontSize(24).fontColor(Color.Orange).width('25%').backgroundColor(Color.Red) Text('B').fontSize(24).fontColor(Color.Orange).width('25%').backgroundColor(Color.Black) .position({x: 66, y: 10}) //这种指定x y 适配性较差 Text('C').fontSize(24).fontColor(Color.Orange).width('25%').backgroundColor(Color.Yellow) Text('D').fontSize(24).fontColor(Color.Orange).width('25%').backgroundColor(Color.Grey) .position({x: '70%',y: '70%'}) //推荐方式适配性比较好 } .backgroundColor(Color.Green) .width('90%') .height(100) .direction(Direction.Ltr) } Divider() //当前 mark 默认: .markAnchor({x: 0, y: 0}) Column({ space: 8}) { Stack() { Row().width(111).height(111).backgroundColor(Color.Grey) } Text("100").fontSize(22).fontColor(Color.Black).width('25').height(25).backgroundColor(Color.Red) .markAnchor({x: 88, y: 100}) //自己当前值 + x 80, y 100. Text("200").fontSize(22).fontColor(Color.Black).width('25').height(25).backgroundColor(Color.Green) .markAnchor({x: 88, y: 100}) //自己当前值 + x 80, y 100. Text("300").fontSize(22).fontColor(Color.Black).width('25').height(25).backgroundColor(Color.Blue) .markAnchor({x: -88, y: 160}) Text("400").fontSize(22).fontColor(Color.Black).width('25').height(25).backgroundColor(Color.Red) .markAnchor({x: -88, y: 160}) } } } } ``` ![](/assets/images/20240119ArkTSBasic/LayoutStudy2.webp) ``` ts //当前 offset Column({ space: 8}) { Stack() { Row().width(111).height(111).backgroundColor(Color.Grey) } Text("100").fontSize(22).fontColor(Color.Black).width('25').height(25).backgroundColor(Color.Red) .offset({x: '-22%', y: '-12%'}) //自己当前值 + x值%, y值%. Text("200").fontSize(22).fontColor(Color.Black).width('25').height(25).backgroundColor(Color.Green) .offset({x: '-22%', y: '-12%'}) //自己当前值 + x值%, y值%. Text("300").fontSize(22).fontColor(Color.Black).width('25').height(25).backgroundColor(Color.Blue) .offset({x: '22%', y: '-20%'}) //自己当前值 + x值%, y值%. Text("400").fontSize(22).fontColor(Color.Black).width('25').height(25).backgroundColor(Color.Red) .offset({x: '22%', y: '-20%'}) //自己当前值 + x值%, y值%. } ``` ![](/assets/images/20240119ArkTSBasic/LayoutStudy3.webp) ### 组件的对齐方式 两者一致的特点(特点: 从外到内的获取宽高) Column 主轴方向↓, 交叉轴→ justifyContent 垂直 Row 主轴方向→, 交叉轴↓ justifycontent 水平 ### Flex可自主选择水平和垂直布局容器 * **direction: FlexDirection.Column** 纵向 * **direction: FlexDirection.Row** 横向 ``` ts @Entry @Component struct FlexPage { build() { Column({space: 20}) { Flex({ direction: FlexDirection.Row, //这里Row和Column自主选择 justifyContent: FlexAlign.SpaceEvenly, //主轴方向 .Column垂直 .Row水平 alignItems: ItemAlign.Start, //交叉轴方向 .Cotumn 左边开始 右边开始 .Row //wrap: FlexWrap.Wrap //换行 wrap: FlexWrap.NoWrap //不换行 }) { Text("10").width('6%').height(60).backgroundColor(Color.Orange) Text("20").width('20%').height(70).backgroundColor(Color.Red) Text("30").width('30%').height(80).backgroundColor(Color.Blue) Text("40").width('16%').height(90).backgroundColor(Color.Black) Text("50").width('50%').height(100).backgroundColor(Color.Pink) Text("60").width('30%').height(90).backgroundColor(Color.Brown) Text("70").width('15%').height(120).backgroundColor(Color.White) } .height(180) .width('90%') .backgroundColor(Color.Gray) } .backgroundColor('#ff8ce53d') .width('100%') } } ``` 下面是Column的对齐方向 ![](/assets/images/20240119ArkTSBasic/FlexColumn.webp) 下面是Row的对齐方向 ![](/assets/images/20240119ArkTSBasic/FlexRow.webp) # 总结 随时积累记录 URL: https://sunyazhou.com/2024/01/HarmonyPhoneSendFileTomacOS/index.html.md Published At: 2024-01-16 01:49:00 +0000 # 解决如何从鸿蒙手机传文件到macOS ![](/assets/images/20240116HarmonyPhoneSendFileTomacOS/harmonyOS.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! ## 问题描述 最近学习中遇到了一个问题,用mac电脑开发鸿蒙, 公司内部的测试机是HUAWEI Mate 60 Pro, 安装的是HarmonyOS NEXT Developer Preview. 对于一个新式手机需要和它进行内部文件互传成了痛点,最近开发总需要从手机录屏中获取mp4视频文件和截图,因为鸿蒙是完全脱离了Android, 找了一些手机助手根本识别不到此手机,包括官方推荐使用的[HiSuite华为手机助手](https://consumer.huawei.com/cn/support/hisuite/) ![](/assets/images/20240116HarmonyPhoneSendFileTomacOS/HiSuite.webp) 鸿蒙系统并不想苹果电脑, 一套通用的AirDrop可以 自由传输于 自家生态下的大部分设备,鸿蒙目前的阶段还实现不了,鸿蒙的AirDrop叫 `华为共享`, 也需要华为生态体系下的设备才能共享文件无缝传输,但是macOS显然不是华为生态下的产品,那怎么解决此问题呢? ## 使用android传统的hdc方式 #### hdc hdc(HarmonyOS Device Connector)是HarmonyOS为开发人员提供的用于调试的命令行工具,通过该工具可以在windows/linux/mac系统上与真实设备进行交互。 > [hdc官方文档](https://developer.harmonyos.com/cn/docs/documentation/doc-guides-V2/ide-command-line-hdc-0000001237908229-V2#section116322265308) #### 准备工作 通过usb数据线连接到mac电脑, 下面是我电脑的配置 ![](/assets/images/20240116HarmonyPhoneSendFileTomacOS/systeminfo.webp) 然后安装hdc环境到电脑上,上述文档都有说明这里不再赘述,假设你成功安装并运行. 输入`hdc -v` ``` sh Ver: 1.2.0a ``` #### 开始连接设备 传文件 查看端口所有设备 ``` sh 输入: hdc list targets -v start server at tcp:7035 FMR0223823025245 USB Connected localhost hd ``` 然后 挂载设备 ``` sh 输入:hdc target mount Mount finish ``` 授予设备端hdc后台服务进程root权限 ``` sh hdc smode ``` 连接设备时,若仅有一台,无需指定设备标识。若有多台,一次仅能连接一台,每次连接时需要指定连接设备的标识,命令格式如下 ``` sh hdc -t FMR0223823025245 shell ``` 然后输出如下 ``` sh # ``` > 这样就进入了交互式终端开始使用shell通讯了. 接下来我们找到我们要copy的文件目录,如果找不到使用如下命令查看文件大小的方式 ``` sh du -sh * ``` 鸿蒙 的文件目录 比如 相册 是存在于`/storage/media/100/local/files/Photo`目录下. 如果不对基本都大同小异. 我们先查看一下我们要从手机 copy到电脑的文件使用 `du -sh *`命令看看大小 ``` sh # du -sh * 3.5K 1 3.5K 16 13M 2 1.5M 3 1.7M 4 ``` 数字是文件目录的名称,我们 找到相关目录文件 后使用pwd打印一下当前工作目录,拼接上想要copy的文件即可实现copy. 假设我们获取的文件绝对路径是`/storage/media/100/local/files/Photo/4/VID_1705287805_004.mp4` 这时候我们新开一个终端.并输入如下命令.(参考文档可以找到更多关于文件操作的指令) ``` sh hdc file recv /storage/media/100/local/files/Photo/4/VID_1705287805_004.mp4 ~/Downloads/ ``` 输出 ``` sh [I][2024-01-16 11:11:29] HdcFile::TransferSummary success FileTransfer finish, Size:1823388, File count = 1, time:140ms rate:13024.20kB/s ``` ![](/assets/images/20240116HarmonyPhoneSendFileTomacOS/file.webp) 通过上述操作我们就从华为的鸿蒙手机中把相应的文件传到了我们的macOS上了 ## 其它操作 比如把文件 从macOS上传到鸿蒙手机上 这里就不一一测试了,这样操作非常方便,完全命令行式的方式 ## 使用IDE 设备文件浏览工具(2024年3月2日更新) 在最新版本的Dev-Eco Studio(DevEco Studio NEXT Developer Preview2)中,加入了新的设备信息浏览工具 位置在IDE的右下角 ![](/assets/images/20240116HarmonyPhoneSendFileTomacOS/DeviceFileBrowserEntry.webp), ![](/assets/images/20240116HarmonyPhoneSendFileTomacOS/DeviceFileBrowser.webp) 这里拿截图举个例子,上述是截图的图片保存路径. ## 更新新版本hdc工具的环境变量配置 ``` sh export PATH=$PATH:~/Library/Huawei/sdk/HarmonyOS-NEXT-DP2/base/toolchains export HDC_SERVER_PORT=7035 export OHPM_HOME=~/Library/Huawei/ohpm export PATH=${OHPM_HOME}/bin:${PATH} ``` 我用的`.zshrc`的文件,所以环境变量写到了`.zshrc`里面. # 总结 不得不说 这个功能对于现在的环境来说非常必要开发一个图形页面app来传手机上的文件到其它端,包括兼容现在的macOS,iOS,iPadOS等等苹果生态以及Android生态的下的设备进行组网,实现近场通信互传文件,这样的app非常必要. URL: https://sunyazhou.com/2024/01/harmonyoslaunchpage/index.html.md Published At: 2024-01-15 12:55:00 +0000 # 鸿蒙启动页面开发 ![](/assets/images/20240115HarmonyOSLaunchPage/HarmonyLogo.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! ## 鸿蒙OS开发 2024年技术不能只停留在嘴上,行胜于言,经过几个月的鸿蒙开发的学习,觉得还是要把容易忘记的内容记录下来,今天 带了的第一个简单代码是开启鸿蒙开发的入门篇,做一个简单的闪屏页 ### 先看下实现效果 ![](/assets/images/20240115HarmonyOSLaunchPage/launch.gif) ### 这里使用的HarmonyOS4.1环境 代码如下: ``` ts import router from '@ohos.router' @Entry @Component struct Index { onPageShow() { setTimeout(()=> { console.log("闪屏1s结束") router.pushUrl({ url:'pages/Home' }) }, 3000) } build() { Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) { Image($r("app.media.sunyazhou")) .width(100) .height(100) Text("迈腾大队长") .fontSize(26) .fontColor(Color.White) .margin({top: 300}) Text("SUNYAZHOU.COM 版权所有") .fontSize(16) .textAlign(TextAlign.Center) .fontColor(Color.White) .margin({top: 10}) } .width('100%') .height('100%') .backgroundColor('#66CDAA') } } ``` 进入首页的基本代码 ``` ts @Entry @Component struct Home { build() { Column(){ Text("Home首页") .fontSize(26) .fontColor(Color.White) .margin({top: 300}) }.width('100%').height('100%').backgroundColor('#00FFFF') } } ``` 上述代码比较重要的是`onPageShow()`中的`setTimeout`函数,这个函数自带定时器, 3000代表 3s, 也就是说 3秒后利用路由直接进入Home页. 这里使用的是Flex布局, * direction: FlexDirection.Column, * alignItems: ItemAlign.Center, * justifyContent: FlexAlign.Center 这三个分别代表了Flex主轴方向、内容对齐方向、交叉轴方向.如果有不理解的话请自行反馈[华为开发文档](https://developer.harmonyos.com/cn/develop/) # 总结 今天的简单的介绍启动闪屏页的简单开发,大家可以基于这个做一些广告和启动页配置.至于复杂的内容有待后续探索,感谢观看. URL: https://sunyazhou.com/2023/12/FinalSummary/index.html.md Published At: 2023-12-31 08:02:00 +0000 # 2023年终总结 ![](/assets/images/20231231FinalSummary/banner.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! ``` txt 三十几岁想从前,人生已经太艰难. 增效增笑增增笑,裁员裁源裁剩闲. 轻舟想过万重山,必将此舟卷上天. 重振旗鼓卷巨浪,破壁前行勇向前. ``` > 这不是AI写的诗哈,chatGPT目前还写不出我这水(低)平(档). 这一年,我想用B站上非常nice三国周郎的经典PUA语录来抒发一下不堪回首的2023, "近日养伤,深感人生之艰难,就像那不息之长河,虽有东去大海之志,却流程缓慢征程多艰.唉! 然江河水总有入海之时,而人生之志,却常常难以实现,令人抱恨终身." 这是坚持写年终总结的第几个年头我已经不知道了,如果非要给今年的总结写个主题的话我肤浅的认为: "大都督,闲言少叙吧,俺老孙只想保住工作,勉强糊口,在完成此目的的前提下,我才有可能跟大都督讨论一下您的人生之志,鄙人没有大都督之志,鄙人之志,甚为肤浅,苟住工作后,才能与公瑾抬头相见."最为贴切. * [2022年终总结](https://www.sunyazhou.com/2022/12/FinalSummary/) * [2021年终总结](https://www.sunyazhou.com/2021/12/FinalSummary/) * [2020年终总结](https://www.sunyazhou.com/2020/12/FinalSummary/) * [2019年终总结](https://www.sunyazhou.com/2019/12/FinalSummary/) * [2018年终总结](https://www.sunyazhou.com/2018/12/FinalSummary/) * [2017年终总结](https://www.sunyazhou.com/2017/12/FinalSummary/) ## 2023年回顾 * 地球的和平 * 可笑的实事 * 平庸的生活 * 努力的工作 * 不善于学习 * 肤浅的爱好 * 败家的好物 我一直都认为自己是个肤浅的人,如果读者慷慨的话请继续读下去,虽然都是一堆流水账,也许肤浅的人生也有发光点,也许能照亮你在肤浅的道路上不断修正自己的不足且提升你的认知. ### 地球的和平 ![](/assets/images/20231231FinalSummary/peace.webp) 今年真的不堪回首,三年的疫情终于落下来帷幕,大家都敢摘掉口罩了,每个人都有了抗体(直到本文撰写的现在2023年12月份大家又把口罩戴上了,甲流乙流支原体等感染的胁迫下不得不防范起来). 从俄乌开战至今都没有停战的迹象的同时,巴以冲突又重蹈俄乌的覆辙.活在和平国度的我们也未尝幸福,也许和平真的只在大炮射程范围之内,真理真的只在剑锋之间吧! 和平终将还得我们中华崛起才能实现. 由于世界的格局风起云涌,我们的生活步履艰难,企业裁员,员工失业成为这个时代的主题, 消费低迷,美元不断加息,货币贬值严重,高昂的房价已经不是我这种肤浅的人敢品头论足的奢侈品了,就因为世界没有了和平,经济陷入停滞,停滞也就算了罗宾还联合脚盆鸡、小溪吧,小袋鼠从各种国际贸易上对我们进行无差别制裁,各种双标导致我们在地球村的地位被撼动,导致我们的企业出不去国门发展贸易,我们陷入了经济内循环,GM又在近10年的时间内掏空了我们的钱包,我们现在的生活水平我只能说“峰峦如聚,波涛如怒, 我所有的努力都不如土(土地财政),兴,百姓苦;亡,百姓苦”,别笑张养浩,我知道你也苦,所以你才写出《山坡羊·潼关怀古》. biden也快到任期了,在他在位的这几年间世界人民真的陷入到从未有过的恐惧感的世界中,忍看硝烟之地重生战火, 痛惜负伤之兵再举刀枪.从伊拉克撤离美军,支持北约加入乌克兰的灭国战争,支持乌克兰搞生物实验灭绝人类,支持以色列打巴勒斯坦,放任脚盆鸡排放核污水向太平洋,疯狂印钞致使美金超发收割全世界,哪里有战争哪里就一定有罗宾,哪里就永无宁日. 科学曾经无国界,现在大国的竞争也让科学有了国界,RTX4090显卡说不让你生产就不让你生产,A100显卡说不卖给我们就不卖给我们,整的马斯克都说买显卡就像买违禁品一样困难.没有了这些高端的显卡我们可能不能更快的发展人工智能、自动驾驶、人脸识别、计算机视觉、医学图像处理,等等.就一块破显卡就能卡主我们脖子,就一块显卡就能让我们的生活陷入无尽的沼泽泥潭. 我喜欢抛开外表看本质,本质是我们还是没有足够的基础科学的支撑做技术积累,我们所拥有的知识还不足以打败世界制造顶尖芯片的水平.本质还是我们无知.无法用现有的知识武装自己成为制造顶级芯片的工程师,也就无法反制罗宾的制裁,也就无法改变现在世界的格局,甚至维护世界的和平秩序. 在一个连自己工作都保不住的环境下,地球村的和平,有点可笑.更可笑的是也不知道现在卡我们脖子的大学经营咋样了,如果可能的话我真像给老曹发邮件问问,福耀科技大学现在什么进度,能不能向我汇报一下,我可是存着34项卡脖子专业等着你这大学开干挨个突破呢! 我希望通过自己的廖胜于无的努力,在未来的发展时间里,对世界和平做出一些不知道能到什么程度的贡献吧!我应该互相勉励一下大家可是又担心这是PUA,算了,我们行胜于言. ### 可笑的实事 这一年可笑的事真是层出不穷,我只说两件人民最关切的问题.房产暴雷和房子解限购 #### 恒大暴雷 我在2021年的时候曾就许家印造车这事就说过,这有钱人有钱他不会花,这话在2023年终于得到了验证,恒大房地产率先暴雷,凉了,恒大的汽车品牌恒驰也未能幸免,许家印这步我是真没看懂,到底是x钱还是真不会花钱,我当时就说你给国家造一所大学,这大学专门开设解决卡中国脖子的技术专业,培养专业极强的大学生,干不好就把这资产申请破产给国家,不说别的你花几十个亿打造几个世界顶级实验室,肯定比造车更有希望,现在倒好,我也不清楚你是X钱还是在干啥,给自己干笆篱子去了. 但凡许家印有点我这种肤浅的认知都不会走到今天这种地步.也许这就是经济下行压力大,CPI消费降级,才导致房地产暴雷.可是买恒大房产的业主可是大部分都是像我这样的平民百姓,你吃的五饱六饱,到底有没有关注过我这种底层老百姓的民生,到底能不能烂尾,保交楼都扯淡吧!这个国家要是有你这样的贪婪资本家,老百姓还怎么实现共同富裕,还怎么奔小康,还怎么解决温饱. 希望老许良心没完全泯灭,给买恒大房子的业主一个交代.我没买这的房子,我只是替买恒大房产的业主捎个话,用以证明我一股正义感涌上心头. #### 房子解限购 ![](/assets/images/20231231FinalSummary/policy.webp) 北京买房终于解开了限房不限贷了,不再被外地买房贷款记录所困扰了,北京的房产解限购也许就从今年开始,首都的底牌也没有几张好牌了,为了挽回经济下行的颓势, 公积金今年11月份终于实现了认房不认商贷,也就是说像我这种2017年买老家的房子没有机会使用北京公积金用商贷贷款,这种情况在北京还算首套房,还能享受公积金首套房贷款. 这是今年也许最好的政策了,然而好像也没啥卵用,工作不稳定的大家觉得政府这是挠痒痒,工作不稳定企业裁员,谁都不敢轻易买房,是呀!经过3年的疫情消耗,大家也认清了现实,攒钱不消费,少消费,买东西够用就行,不用追求多好,消费观念很务实.这是好事也是坏事,好事是大家都懂了勤俭节约,坏事是消费低迷后经济增速放缓,就业就更难了, 经济的三驾马车中的一架不灵了,带来的副作用可是大家非常难以承受的. ![](/assets/images/20231231FinalSummary/policynew.webp) 好政策一个接着一个,首付降至30%了,贷款商贷也降到4.2%的利率了.在我看来这是房子卖不动,为了刺激经济,割韭菜都割到韭菜根了. > 不知道从何时起,逐渐开始喜欢浏览政府的官方网站并关注它的动向.有时候我觉得缺少一个能把官方网站解读清楚的次级网站,专门用大白话解读政府的一举一动,这是一个很不错的市场. ### 平庸的生活 #### 父亲的离去 今年家父去世了,我很悲伤,父亲的离开让我变的意志消沉,从前我都是依赖性很强的孩子,有时候很怀念小时候父亲负责给村里的自来水塔抽水,每年的冬天过年收拾院子,准备柴草,正月十五的时候用废旧的机油引燃院子的灯火.夏天老爸收拾家里的道路和街角,那些年家里真的很穷,还在使用长春一汽制造的12马力拖拉机,也许这车就像父亲的灵魂,破旧但干净的就差刮腻子喷漆让他焕然一新一样. 每当想起父亲的背影,也许只有李宗盛的《新写的旧歌》能表达我和父亲之间的感情, 这些年我只顾自己, 有时候我的追求他无力参与, 当庸碌无为的日子悄然如约而至, 我只顾着自己卑微的喘息,甚至没有陪他,失去呼吸. 爸,当我有一天乘风去见你,再聊聊这文章里来不及说的千言万语. 说真的,我即便上大学的时候都没有意识到这些场景将成为回不去的从前.直到工作了这些年过年回家才渐渐地意识到,农村向城市迁移的速度逐渐加快,我很喜欢农村家里种的蔬菜,夏天回去给父亲上坟时,我回了趟家,拿了一些菜,家里园子和院子现在只能留给认识的还没搬进城里的左邻右舍使用了. #### 家里的小朋友 父亲离开几个月后,家里的小朋友出生了,是个皮夹克,小朋友出生时好胖,也许和我小时候差不多,小家伙很活泼,我家那位坚持在老家生.连起名字都是老婆家的两位资深医学博士相互交换意见决定的.遗憾的老爸没有看到. 剩下的任务也许就是陪着他健康成长,一起跟我学习英语. 这位小朋友的到来让我意想不到家里亲戚非常喜欢,老婆家亲戚从老到小,每天都陪着左右. #### 工作居住证 在TME这一年多里,我成功的拿到了失去很久的工作居住证,在北京买房、摇车牌再也不用5年社保了,下证的第一时间给小朋友办理医保卡"一老一小",然后摇号资格申请. 没想到小朋友才出生几个月就拥有了我们参加工作才有的社保卡, 在医疗方面作为一个纳税人我享受到了我该有的福利. #### 共产房 有了工作居住证第一时间安排上北京的共产房, 北京的公产房是我等唯一有可能性上车的房子,户型和面积都需要做相应的取舍,甚至后续的卖房和出租. ![](/assets/images/20231231FinalSummary/gongchanfang1.webp) ![](/assets/images/20231231FinalSummary/gongchanfang2.webp) 这个共产房对购买的购房者要求非常苛刻,不是有钱就行,你得从未在北京有过任何房屋住宅交易记录,单身人士还得30周岁以上,自从北京解开限房不限贷,这几天有出来个降低首付比例到30%的政策后,买房者可能会多那么一点自信, 非京籍买共产房还分好几组, 主要是看申购人数和供应套数. 总之一堆条条框框如果你感兴趣可以参考[大兴区亦生悦小区共有产权住房项目申购登记公告](https://www.bjdx.gov.cn/bjsdxqrmzf/zwxx/ztfw/zfbzztfw/tzgg3442/2101376/index.html)看看这个共产房在北京咋回事. 基本大概率外地非京籍买房限制 五年连续社保之类的、工作地点、社保缴纳区域、是否是区引进人才之类的人优先组.像我这种社保在海淀区只能沦为非京籍最后一组选房, 好房子得先可着北京人、和供应地块的区 社保人群, 城六区基本就是凑热闹. 通过一个共产房我发现两个事情: * 北京各个区之间也有地域的歧视,比如大兴地块必须在大兴缴纳社保的人群第一组选房摇号优先.其它区基本没啥机会看热闹 * 有北京人的地方基本就会被政策倾斜,那些为首都交税做贡献的人没有得到他们最基本的尊重.基本都是有北京人的地方对外地人而言就没有公平可言 这个共产房把以上两条体现的淋漓尽致.这种屈辱的打击也许是我们大部分人一生中不得不品尝的`苦涩经历`.希望那些和我一样的人能从消沉中走出来继续期待还未升起的太阳. 我其实非常希望把北京还给北京人,我们自己的家乡但凡有一点点发展的机会我们都不会苟且在北京的土地上跟北京人抢房子.什么GDP啊、CPI啊 都靠北京人就好了.外地人给自己的家乡贡献消费和GDP就好.到时候外地区域也搞像北京这样的政策,买共产房 外地人摇号最后一组.本地人优先. 这是在21世纪的中国,社会的文明还未曾到达人人平等地步,言论的自由被打压的只剩喘息的声响. 说到这大家应该也就明白了,很有中产有钱了为啥都去了别的国家,新加坡了,加拿大、澳洲之类的了. 因为公民没有被授予平等的权力.没有平等的人权和言论自由、只有打压、政策倾斜、变相转移福利给本地人,三年的口罩让大家逐渐认识到了,我们的这个国度还存在很多社会滞后的服务,已经严重跟不上时代发展的脚步. 共产房在本文发表时仍在摇号选房中,我基本不抱希望,就凑个热闹感受一下流程,什么时候北京人不做接盘侠了我也许还能有机会上车. 通过此次买共产房的经历让我深刻的领悟到了一件事,首都渐渐地会分为富人区和穷人区,而城市更愿意吸纳高学历高收入的富人,提高房价和一些门槛只为让这些人为城市贡献更多,而剔除无法给与更多贡献的穷人,富人会拥有更好的教育环境和生活环境,在城市形成垄断和资本,享受更多资源,而普通无法立足的人会慢慢淘汰回家乡,回到家乡对当地政府的财政和养老会造成压力和负担,小城市里一家人辛苦培养的高材生,会去发达城市和更好的国家,家乡因为没有更好的资源留住人才,而发展不起来,小城市发展越来越不好,大城市越来越好,形成恶性循环,首都通过福利吸引了全中国的富人和有知识的人来此,形成了垄断和繁荣,全国各地的人才都供应到了首都,城市的银行80%的财务集中在不到5%的人手中,大城市形成牢固的阶层,占据优秀的文化资源和大量的财富,而其它小城市或者小城镇最终成了大城市的陪葬, 北京的抢房经历让我明白,一个城市的繁荣是吸纳了许多不同的人群,他们来自天南海北,为这个城市建设和攫取财富, 当城市失去公平, 形成阶层和垄断, 肆意愚弄和占据他人的权利,最终会得到报应.即使北京成为了国际化的大都市,但旁边安静苍凉的其它省份土地上,一直在见证者这里曾经发生过的一切,我真的希望北京这座特大城市能给予许多奋斗在城市的人们更多的温暖和立足之地,一起享受城市发展的成果, 创造一个公平的现代文明,善待我们这些北漂, 因为这些人是籍籍无名的奋斗者,这些人象征着无数的普通人,不要让我们这些人做了贡献后被赶走变得了却无痕. ### 努力的工作 ![](/assets/images/20231231FinalSummary/work.webp) 最近这一年半时间里,几乎都是在腾讯音乐娱乐-酷我音乐团队效力并苟活, 公司之前在国家会议中心办公, 今年7月份搬到了东四十条,从4环外搬到了2环里,带来最直接的痛点是上班时间从30分钟摩托车到1个小时地铁的差距,最主要的是摩托车骑不了了.很忧伤,但也有好处,好处就是我每天在地铁上的时间多了就有了很多时间学习,每天都在地铁上下班的时间多有2个小时的学习时间,当然条件艰苦一些,需要人挤人,一路几乎都是站着. 有时候我都不想跟老家的同学、老师说这个,但有些事总需要面对,不说人家也懂,只是这几年随着工作机会的减少,地铁上有很多人拿起来书本,拿起了手机看教程,我真的不是危言耸听,当你来北京坐地铁就会感受到了,只有不断学习提高认知才能更出色,大部分人都在努力,如果自己止步不前那不就是混吃等死吗?说到这会不会觉得很卷,这不算卷这最多是每个人都合理的利用业余时间给自己充电而已. 团队最近被裁员整走了两个人, 工作上很多同事都在踩刹车,我有时候踩油门,有时候踩刹车, 有时候的感受是,这只是一份工作,别想太多,对于一个强迫症神经质的程序员,渐渐地学会了妥协和安静,对于公司的各种降本增效手段折磨的员工已经没有什么心情努力工作, 感觉状态就像是 等着被裁,拿了大礼包走人的感受.已经对自己擅长的开发失去了太多信心. 不过我还是能在一些技术的开发上做一些技术沉淀.保留曾经那份对技术的热爱和影响力的提升. 10月份开发的小组件,我向腾讯音乐技术团队的公众号投了一篇文章,介绍一些开发细节 ![](/assets/images/20231231FinalSummary/article.webp) 当内部审核稿件的时候被砍掉的只剩下一些没什么技术含量的细节,核心的内容不允许被public出来,主要是为了保持业界的领先性.我希望在这里记录一下我对文章投稿被删减引发的思考: 有时候我觉得成为TME的一名iOS开发者很惭愧,它让我觉得自己很自私,我们一味的从互联网摄入知识的同时却从未向外输出有价值的内容,没有开源精神,然而出现这种问题的主要原因是做点有价值的东西不允许对外public,就是为了保证此技术在业界的领先性,当然这种技术也仅限于一些没有太高技术含量的内容,这种封闭的领先性让我觉得又可笑又可悲,可笑的是我们总在强调一些没有技术含量的技巧细节的领先,可悲的是我们都在闭源的环境中闭门造车式的成长,才会造成今天的行业内卷,从未在基础科学上的领先。我们其实内心都很清楚我们是踩在开源代码这位巨人的肩膀上才有了今天,然而我们不得不妥协于今天的就业环境所造成的现状,开源等于放弃领先性,等于不听指挥,等于被裁员。 此刻我明白了我做不到成为具有开源精神的开发者的最大敌人是"财富的贫穷",通俗点就是差钱儿。本质上还是因为贫穷迫使我们没有选择的权利,换句话说我们缺乏相关的知识去解决自己被贫穷受限的问题。 所以我还是需要通过学习提升自己的知识水平层次和认知,在未来解决财富自由的问题。 2023年我肤浅的认为工作的基本内容是用自己低廉的技术换取生活的基本保障.没有啥好说的,就是各种茫然的不知所措.没有职业规划,没有远大的理想,只有埋头苦干的坚韧不拔. ### 不善于学习 2023年学习的课程有: * flutter开发教程 * SwiftUI开发教程 * 鸿蒙OS 4.0开发教程 年初的时候为了拯救公司即将增速放缓的app 波点音乐,我这边被抽调过去支援flutter开发,主要工作就是迁移集团提供的直播SDK,和flutter交互,缺基建就搞基建,缺持续集成就搞持续集成,缺跳转库就开发集成混合导航栈.趁着这个时间我业余的周末都在学习flutter,大概用了6个周末就把整套教程看完了,也明白咋回事了,剩下的工作就是给波点干活搞flutter. ![](/assets/images/20231231FinalSummary/flutter.webp) 上面是B站上的flutter教程分享给大家[最新Dart、Flutter教程-带你进入Flutter的世界](https://www.bilibili.com/video/BV1KE41117XV?p=1) 学习了一段时间发现,它的框架太好了,基本都把所有的工作都做了. 下半年 接到任务,开发iOS17小组件,而且这小组件还必须用SwiftUI.从学习完Flutter立马用至少10几个周末的时间学习SwiftUI. ![](/assets/images/20231231FinalSummary/swiftUI.webp) [SwiftUI开发教程](https://www.bilibili.com/video/BV1pW4y1j7MC/?spm_id_from=333.999.0.0&vd_source=9309f71afe97e633abeadc8407870e76) 这是一个台湾的美女推出的教程,感觉像是推出了大半年,还剩余一些SwiftUI的新内容没有提及,整体思路比较清晰,只是她讲课的风格比较上蹿下跳,学习前要有思想准备. 最后用了这个SwiftUI开发了酷我音乐的小组件,还算是从学习落地到了代码,完成了一次标志性的跨越. 还有鸿蒙开发 [HarmonyOS4.0教程](https://www.bilibili.com/video/BV1pb4y1g75m/?spm_id_from=333.999.0.0&vd_source=9309f71afe97e633abeadc8407870e76) [鸿蒙HarmonyOS讲明白了(鸿蒙4)开发应用从入门](https://www.bilibili.com/video/BV1dw411b7Jm/?spm_id_from=333.999.0.0&vd_source=9309f71afe97e633abeadc8407870e76) 鸿蒙这里只是一些基础 官方资料: ArkUI实战 文档 [https://www.arkui.club/](https://www.arkui.club/) 开发者指南: [https://developer.harmonyos.com/cn/docs/documentation/doc-guides-V3/start-overview-0000001478061421-V3?catalogVersion=V3](https://developer.harmonyos.com/cn/docs/documentation/doc-guides-V3/start-overview-0000001478061421-V3?catalogVersion=V3) 官方内部提供未发布的[SDK IDE以及模拟器 ](https://developer.harmonyos.com/deveco-developer-suite/enabling/kit?currentPage=1&pageSize=100) 学习鸿蒙学了有一段时间了,我的理解就是勤加练习,掌握大概框架和底层服务就好.一些其它的深入内容边战斗边学习就好. 现在的B站太好了,能学习各种课程还没有广告还免费.真的良心.还记得去年学习完成swift的课程,感觉也没什么,但是swift和SwiftUI每年发布一版我就得学习一遍,每次的改动太多了,根本跟不过来. 就这样学习的时间经常被打乱节奏,由于经常被抢占式调度学习任务,导致没能持续不断的把这本书看完.这是目前的进度 ![](/assets/images/20231231FinalSummary/cxxprmerplus.webp) 我解释一下啥是`抢占式调度`,就是原计划我是按照每天的上下班时间看这本书,但是往往都是看到一半或者其他进度过程中被一些新技术例如鸿蒙开发教程的视频打断,导致我只能先把工作相关的技术学习任务先学习完,再抽时间学习剩余的任务.就这样反反复复被各种抢占式调度占用了很多业余时间. 如果你和我一样,那么恭喜你,目前我没有找到最优解去解决,时间如何被抢占式调度,我只能说财富自由的时候我也许会有. 一堆没有时间看的书,堆起来都快有PS5高了吧! ![](/assets/images/20231231FinalSummary/books.gif) 这一年感觉很累,学习的东西越来越多,从flutter到SwiftUI,从SwiftUI到HarmonyOS然后再到一堆没有看完的iBook.以及自己喜欢的无人机开发方向都还未启动学习计划. 从事计算机行业本来也许就需要这么累.总有一堆学习不完的课程、书籍、文章、教程、等等. ### 肤浅的爱好-台球 ![](/assets/images/20231231FinalSummary/cueball.webp) 最近这一年时间里,公司有社团活动,我跟同事一起逐渐的学起了台球,直到现在我还一直组织公司的台球活动.台球有一种魔力,能让我这个没有任何业余爱好的人爱不释手,我的球技也日益增进,每次看见绿色台尼眼睛很舒服,拿起球杆时会把工作和生活上的一些不愉快遗忘. 以前自己对台球的理解只能说浮皮潦草,这一年里自己通过观看B站的台球教学和比赛逐渐对这个球体撞击运动产生了浓厚的兴趣, 从退台、占位、入位、姿势、握杆、瞄点、运杆、发力, 下杆法、高中低杆、左右旋转、大中小力的掌控. 五分点、跳球、加塞让点、清台思路、K球、等全都一一尝试并训练. 这一年在台球领域的学习和时间投入非常多,当然也仅限于业余时间. 这期间也逐渐了解了很多台球明星以及器材品牌,以及这项运动的发展,目前只有乔氏在大力推进台球这项运动向奥林匹克赛事迈进,这基本就是国民级的体育运动.在台球的世界里只有实力的比拼,没有学历的歧视. 在近几年的发展中,台球逐渐成为了大众休闲娱乐的运动项目之一,这项运动不像羽毛球或者其它运动类项目那么耗费体力,但挑战难度和观赏性非常在线. 这里推荐一些B站的台球教程 学习台球可以先看一下 B站台球教学 * [谢召辉台球教学](https://www.bilibili.com/video/BV1wX4y1a7m8/?spm_id_from=333.999.0.0) * [石鑫 台球教学](https://www.bilibili.com/video/BV1Vs4y1P7LF/?spm_id_from=333.999.0.0&vd_source=9309f71afe97e633abeadc8407870e76) * [天王代勇 台球教学](https://www.bilibili.com/video/BV1ep4y1G7gM/?spm_id_from=333.999.0.0&vd_source=9309f71afe97e633abeadc8407870e76) * [加雷斯·波茨台球教学](https://www.bilibili.com/video/BV1wX4y1J7Dq/?spm_id_from=333.999.0.0&vd_source=9309f71afe97e633abeadc8407870e76) 这项运动终将会成为一项体育赛事,在北京有一些球馆是无烟球馆,大家喜欢的话可以尝试一下. 我说说我为啥花时间在这上边,这几年大家过得很压抑,年轻人大部分看不到未来的希望,大部分人选择躺平及时行乐,这个运动非常适合及时行乐,消费还算可以,大众消费得起,还乐趣很多.也许这就是年轻人的觉醒,内卷卷的残酷,房价高的离谱,勤劳不能致富,未来没有出路,既然人生这般辛苦,又为何风尘仆仆,与其奋斗不如躺平. 我是这样看待现在的裁员潮和内卷躺平没出路的问题,就三个字 `"随它去" `let it be, 不要为这些事情费神上心,因为它完全超出了你的能力,你根本影响不了,无论事情发展到哪一步,你只能接受,就算你感到痛苦和烦恼,难道会改变局面吗?我们的时间很宝贵,不要用于那些改变不了的事情,而要用于你能够改变的事情.并且这几年的裁员属于全行业收缩,不是员工的问题,`行业的问题超出了你个人的努力`.如果公司把整个部门和业务线都砍了,里面最好的员工也不可避免会受到影响.不要灰心焦虑,更不要怀疑自己,既然事情已经发生了,你所能做的就是随它去,别把时间用于长吁短叹、愤世嫉俗、而要保持学习,磨炼自己的技术,等到行业回暖时,就会有回报,这些行业之所以会收缩,就是因为前期的市场需求旺盛,扩张太快,这说明他们的市场需求是真实存在的,只要市场需求还在,就有发展空间,你练好本领,不愁将来没有用武之地. 台球让我的业余时间不在孤独,绿色的台尼能让我疲劳一周的眼睛得到放松.其实就是一种身心健康的运动方式而已.比下班和同事朋友一起吃吃喝喝会更有意思. ### 败家的好物 这一年非常深度使用PDD买东西,而且都很实用,除了其它平台更具性价比外首选pdd,现在买东西养成了一个习惯我把它称为`遛货`,就是我如果很想买一件商品,我就按耐忍住半个月观察一下这个商品价格,如果他跌了就下手,涨了就不买,啥时候跌啥时候买,经过几次`遛货`我发现pdd买贵了投诉官方客服指定能给退,直接投诉到底就完事了. 今年败家的东西比较少,随着消费降级,自己买生活用品都很少了,下面的是纯业余爱好. * 威联通NAS * 台球杆 * HomePod * AppleWatch * 苹果触摸板 * PS5游戏光盘 * 摩托车头盔 * 挎包 * 显示器总成线控版 * 刨丝器 #### 威联通NAS 464C-8G ![](/assets/images/20231231FinalSummary/nas1.webp) ![](/assets/images/20231231FinalSummary/nas2.webp) 这装备可以说存储一生的资料都够用了吧. 双11前夕京东特价促销 会员价1823买的. 买到手加了4块4T希捷酷狼Nas专用硬盘.主要是存储资料和学习的一些内容.可以用这个架设git服务器,用这个开设个人博客网站作为云存储等等,这个也许只有开发人员才懂它的作用,以后再也不用担心换电脑了,资料云端本地传到nas上,其它电脑轻松访问. #### 台球杆 ![](/assets/images/20231231FinalSummary/cue.webp) 这个台球杆可以说1倍的价钱双倍的快乐,我选的是几百块钱的手工杆,不是很出名,是一个广东台山的很古老的手工杆品牌.白沙.用着质感和使用体验非常好.我很喜欢这只球杆.它陪伴我打球的过程中有很多运气加持. #### HomePod ![](/assets/images/20231231FinalSummary/homepod.webp) 双12 遛货遛了好几个月,观察它的价格低于常规价格后果断下手,音质非常好,就是延迟太高,没有蓝牙延迟低.这个性价比不高哈,不推荐大家买,因为这个一个很贵,有钱的小伙伴都买好几个组装成立体声播放,好处就是它可以单独控制也可以集群控制,亦可以 单独分开控制,控制很简单跟它说 "hi siri ..." #### Apple Watch ![](/assets/images/20231231FinalSummary/applewatch.webp) 之前的手表电池不行了,pdd买了一块电池,结果自己动手的时候发现,屏幕线圈有一层nfc接触电路,导致把之前的手表弄坏了,查了一下维修比买二手价格还贵,果断放弃,pdd买个最便宜的se. #### 苹果触摸板 ![](/assets/images/20231231FinalSummary/trackpad.webp) 苹果的设备还是触摸板比较好用,学习工作写代码,还是触摸板比妙控鼠标要好很多,于是淘汰了旧的妙控鼠标,换成了触摸板. #### PS5游戏光盘 ![](/assets/images/20231231FinalSummary/games.webp) 业余败家买了几本DVD光盘碟片游戏.当做收藏吧! #### 摩托车头盔 ![](/assets/images/20231231FinalSummary/helmet.webp) 骑摩托车的时候比较不安全,买了一个好一些的.安全重要 #### 挎包 ![](/assets/images/20231231FinalSummary/bags.webp) 为了解决每次出门带各种东西弄得裤兜非常难看,鼓鼓的一点都不优雅,岁数大了,渐渐地变得注意自己的体面和形象,自己买了一个挎包, 实际上是买了3个,其余两个送给了同学和朋友,在我父亲去世的时候我借哥们的车用了好几天,兄弟一分钱都没收,为了表达我的感谢,我送他们两个人一人一个挎包. #### 显示器总成 ![](/assets/images/20231231FinalSummary/lcd.webp) 之前有一个旧的苹果电脑,太老了很卡,于是把电脑拆了之后发现屏幕是好的,上网上搜了一下可以买个mvds驱动板可以改成 显示器,于是花了200多买了驱动板安装上果然不错,变废为宝合理利用家里的电器. #### 刨丝器 ![](/assets/images/20231231FinalSummary/grater.webp) 我相信这个如果你不做饭根本不会意识到它的重要性,我经常下厨做饭,对厨房工具比较重视,切土豆丝是一件很麻烦的事,如果不是为了练习技术,我建议还是买一个饭店用的刨丝器,就是上图的工具,买回来发现,饭店的厨师好像也不香了,工具切的非常好. # 总结 2023年在不知不觉中就到了年终,有时候真的感叹,我还能赶上 1打头和2打头的 年份,跨世纪的事我赶上了,时间过得真的太快了,没怎么开始一年就结束了,希望2024年我开启新的学习计划,持续输出高精尖技术文章,拓宽认知,早日实现共同富裕,2024 平稳度过就好, 明年见! URL: https://sunyazhou.com/2023/10/sqlstandard/index.html.md Published At: 2023-10-31 02:01:00 +0000 # SQL语句的标准 # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! ## 背景介绍 最近工作太忙,没有保证博客的产出,今天偶有时间来探讨一下最近看到的一篇文章 [orona技术专题-时序数据分析](https://mp.weixin.qq.com/s/CMgxtw0AisqtNeY1gRgJsQ) 来自于网易云音乐技术团队, 这篇文章简单的阐述了一下做数据统计相关的大数据工作, 不过这不是我今天要介绍的重点,我想介绍的重点来自于这篇文章用到的sql语句 在我上大学时候老师教的SQL语句在当时看来已经很实用且简单.但忽视了一个很重要的内容,代码的整齐划一,代码标准和规范. 今天来到了来自网易云音乐的文章内容中,我观察了一下SQL语句 ``` sql SELECT toStartOfDay(time), avg(degree) FROM table_temperature WHERE time>='2023-09-01' AND time<'2023-10-01' AND city='杭州' GROUP BY toStartOfDay(time) ``` 继续观察 ``` sql SELECT toYear(time), model, avg(price) FROM table_gas WHERE time>='2013-01-01' AND time<'2023-01-01' GROUP BY toYear(time), model ``` 顿时让我感觉 这个写法我必须记录一下,真的很标准 ``` sql CREATE CONTINUE QUERY "cq_event" ON "apm_log" BEGIN SELECT SUM("pv") as pv INTO "one_year"."cq_hour_event" FROM "one_week"."cq_minute_event" GROUP BY time(1h), * END ``` 还有下面 ``` sql SELECT TUMBLE_START(PROCTIME(), INTERVAL '1' MINUTE) as wTime, count(os) as pv, os as osName, moduleName as moduleName FROM performance_log WHERE props['mspm'] = 'ReactNativeApplication' GROUP BY TUMBLE(PROCTIME(), INTERVAL '1' MINUTE), os, props['moduleName'] ``` 等等 #### 创建表 ``` sql CREATE TABLE rn_monitor_cold_boot_stage_local ( `appName` String, -- 应用名,如 云音乐 `osName` String, -- 操作系统名 `appVersion` String, -- 应用版本 `rnModuleName` String, -- ReactNative 模块名 `deviceTag` String, -- 设备性能分档 `uploadTime` DateTime, -- 日志到达服务端时间 `uid` String, -- 用户 uid `stageName` String, -- 阶段名 `stageCost` Float32, -- 阶段耗时 ) ENGINE = MergeTree PARTITION BY (appName, osName, toYYYYMMDD(uploadTime)) ORDER BY (rnModuleName, uploadTime) TTL uploadTime + toIntervalDay(90) SETTINGS index_granularity = 8192, use_minimalistic_part_header_in_zookeeper = 1 ``` #### 查询表 ``` sql SELECT toStartOfDay(uploadTime) as "time", avg(stageCost) AS "avg", quantiles(0.5, 0.9)(stageCost) AS "quantiles", count() AS "pv", uniq(uid) AS "uv" FROM rn_monitor_cold_boot_stage_shard WHERE uploadTime>=1682006400 AND uploadTime<=1682611199 AND stageName='render' AND rnModuleName='rn-playlistrank' GROUP BY toStartOfDay(uploadTime) ORDER BY toStartOfDay(uploadTime) ASC ``` 不知道大家有没有真正观察 * 1.关键字单独一行 * 2.括号换行单独一行并且对其 * 3.字段单独一行用`,`逗号隔开 * 4.开始结束配对 * 5.关键字必须靠前对其首行 * 6.AND和 ASCd等关键字 放后 虽然我总结的过于零散, 但是从上述标准的写法来看,我肤浅的认为这就是SQL应该有的标准,写SQL就应该这样写,给别人提供更好的阅读性. # 总结 今天简单的介绍了一下SQL的标准写法,这对经常操作数据库的同学可能是家常便饭,也可能大家都认为自己知道就我不知道觉得我可笑,总之 这是我对知识的积累和不断提高自己的认知.为了记录这些好的代码我记录了下来这篇文章 [SQL 参考的 orona技术专题-时序数据分析](https://mp.weixin.qq.com/s/CMgxtw0AisqtNeY1gRgJsQ) URL: https://sunyazhou.com/2023/09/ios17widgetURL/index.html.md Published At: 2023-09-12 01:54:00 +0000 # 解决iOS小组件开发中widgetURL问题 ![](/assets/images/20230912iOS17WidgetURL/banner.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! ## 背景描述 最近再开发iOS17的小组件遇到一个非常奇怪且很脑残的问题 在小组件开发过程中有如下代码使用深度链接(deeping URL )调用open URL 打开host app. 当我第一次使用下面代码的时候 总是最后一个生效 ``` swift HStack(alignment: .bottom) { Image(itemInfo!.didCollected ? "kw_widget_absorption_color_like" : "kw_widget_absorption_color_unlike") .resizable() .aspectRatio(contentMode: .fit) .frame(minWidth: itemSize.width, maxWidth: .infinity, minHeight:itemSize.height, maxHeight:.infinity) .widgetURL(URL(string: "sunyazhou://collectOrNot")) .border(.red) Image(itemInfo!.isPlay ? "kw_widget_absorption_color_play" : "kw_widget_absorption_color_pause") .resizable() .aspectRatio(contentMode: .fit) .frame(minWidth: itemSize.width, maxWidth: .infinity, minHeight:itemSize.height, maxHeight:.infinity) .widgetURL(URL(string: "sunyazhou://playOrPause")) .border(.cyan) Image("kw_widget_absorption_color_next") .resizable() .aspectRatio(contentMode: .fit) .frame(minWidth: itemSize.width, maxWidth: .infinity, minHeight:itemSize.height, maxHeight:.infinity) .widgetURL(URL(string: "sunyazhou://playNext")) .border(.blue) } ``` ![](/assets/images/20230912iOS17WidgetURL/widget1.webp) 也就是这三个按钮我点击哪个都是最后一个Image生效,我认真翻阅了一下文档,只能说太坑了 ``` swift @available(iOS 14.0, macOS 11.0, watchOS 9.0, *) @available(tvOS, unavailable) extension View { /// Sets the URL to open in the containing app when the user clicks the widget. /// - Parameter url: The URL to open in the containing app. /// - Returns: A view that opens the specified URL when the user clicks /// the widget. /// 这行 /// Widgets support one `widgetURL` modifier in their view hierarchy. 这行 /// If multiple views have `widgetURL` modifiers, the behavior is 这行 /// undefined. public func widgetURL(_ url: URL?) -> some View } ``` 它明确标识如果并排添加多个widgetURL的话,这种行为是未定义不确定的,作为一个负责任的iOS开发者我必须对这种注释提出批评, 如果你要是告诉我不确定那就在编辑代码的时候告诉开发者这玩意最多只能添加一个View上,如果添加多个的话要用 其它方法 然后给个相关链接 #### 解决办法 使用如下代码 ``` swift Link(destination: URL(string: "wig://\(item.id)")!) { ZStack { // some views } } ``` 于是我就改成了如下 : ``` swift HStack(alignment: .bottom) { Link(destination: URL(string: "sunyazhou://collectOrNot")!) { Image(itemInfo!.didCollected ? "kw_widget_absorption_color_like" : "kw_widget_absorption_color_unlike") .resizable() .aspectRatio(contentMode: .fit) .frame(minWidth: itemSize.width, maxWidth: .infinity, minHeight:itemSize.height, maxHeight:.infinity) .border(.red) } Link(destination: URL(string: "sunyazhou://playOrPause")!) { Image(itemInfo!.isPlay ? "kw_widget_absorption_color_play" : "kw_widget_absorption_color_pause") .resizable() .aspectRatio(contentMode: .fit) .frame(minWidth: itemSize.width, maxWidth: .infinity, minHeight:itemSize.height, maxHeight:.infinity) .border(.cyan) } Link(destination: URL(string: "sunyazhou://playNext")!) { Image("kw_widget_absorption_color_next") .resizable() .aspectRatio(contentMode: .fit) .frame(minWidth: itemSize.width, maxWidth: .infinity, minHeight:itemSize.height, maxHeight:.infinity) .border(.blue) } } ``` > 注意: `Link` 仅支持widget的`Families`中的`systemMedium`和`systemLarge`,不支持`systemSmall` # 总结 小组件开发中很多在常规swiftUI中符合自己预期的功能在小组件中就很不符合预期,请大家开发小组件的话记得多看看文档. [小组件开发相关文章](https://mp.weixin.qq.com/s/684dX2rFCUq1Tum6D0oJeA) URL: https://sunyazhou.com/2023/09/learnswiftuichapter5/index.html.md Published At: 2023-09-09 07:55:00 +0000 # SwiftUI第五章学习总结 # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! ## SwiftUI课程 最近坚持学习swiftUI,周末有空把第四章都看完了,我这里说的看是动手实践+教程学习.记录一些容易遗忘的内容 ### 主要内容包括 * 处理tabbar透明问题问题 * 处理单位格式化问题 ### tabbar透明问题问题 用SwiftUI写完各种UI后发现 tabbar的视图被遮挡, ![](/assets/images/20230805LearnSwiftUIChapter5/before.gif) ![](/assets/images/20230805LearnSwiftUIChapter5/after.gif) 需要启动app的时候使用如下函数`applyTabbarBackground()` ``` swfit import SwiftUI @main struct AppEntry: App { init() { applyTabbarBackground() } var body: some Scene { WindowGroup { HomeScreen() } } func applyTabbarBackground() { let tabbarAppearence = UITabBarAppearance() tabbarAppearence.configureWithTransparentBackground() tabbarAppearence.backgroundColor = .secondarySystemBackground.withAlphaComponent(0.3) tabbarAppearence.backgroundEffect = UIBlurEffect(style: .systemChromeMaterial) UITabBar.appearance().scrollEdgeAppearance = tabbarAppearence } } ``` #### 处理单位格式化问题 iOS系统提供了国家化的单位类 `Measurement `,帮我们处理 克,g, 英镑, pounds等单位的处理 ``` swift var desciption : String { let preferredUnit = Unit.getPreferredUnit(from: store) let measureMent = Measurement(value: wrappedValue, unit: unit.dimension) let converted = measureMent.converted(to: preferredUnit.dimension) // return converted.formatted(.measurement(width: .abbreviated, usage: .asProvided, numberFormatStyle: .number.precision(.fractionLength(0...1)))) return converted.value.formatted(.number.precision(.fractionLength(0...1))) + " " + preferredUnit.localizedSymbol } ``` 这里推荐看一下官方的WWDC20视频[Formatters: Make data human-friendly](https://developer.apple.com/videos/play/wwdc2020/10160/) 影片中介紹了能把日期、單位、數字和文字等等資料,根據使用者 Locale 進行格式化的工具 # 总结 其实第五章讲的比较多的是属性封装器的高度封装,我看篇幅实在太大,与其我在这记录一下不如大家亲自看一下[教程视频](https://www.bilibili.com/video/BV1bA411y71h/?spm_id_from=333.788&vd_source=9309f71afe97e633abeadc8407870e76),讲的比较透彻。 其次大篇讲述单元测试, 这里由于本人不太愿意写单元测试直接跳过。。。 一直跟进这门课程希望有所收获,下面是整理的一对课程资料 请查阅 # SwiftUI 入門課程 放置 [SwiftUI 入門課程](https://www.youtube.com/playlist?list=PLXM8k1EWy5khONZ9M9ytK8mMrcEOXvGsE) 的相關檔案,以及每一章節的相關連結、延伸閱讀。 ### Chapter 1:基本介紹 介紹 Xcode 介面和 SwiftUI 的基本架構。 ##### 相關連結 * [1-1 展示的手機版本、升級趨勢網站](https://mixpanel.com/trends) * [1-3 使用的盤子圖片來源](https://www.flaticon.com/free-sticker/dinner_7603521) * [1-5 展示的裝置資訊網站](https://iosref.com/res) * [1-5 排版類型延伸閱讀](http://defagos.github.io/understanding_swiftui_layout_behaviors/) * 如果你從 UIKit 過來可能會問 AppDelegate 去哪了。 請搭配[這個 property wrapper](https://developer.apple.com/documentation/swiftui/uiapplicationdelegateadaptor) 使用。 不過,如果只是要啟動時進行一些操作,在 App 的 init 中進行即可;如果是畫面切換相關事件,請用 [ScenePhase](https://developer.apple.com/documentation/swiftui/scenephase)。 --- ### Chapter 2:排版 練習排版和基本的重構程式碼。 ##### 相關連結 * [在 SwiftUI 中实现视图居中的若干种方法](https://www.fatbobman.com/posts/centering_the_View_in_SwiftUI/) * 除了影片中搭配計算屬性使用 **@ViewBuilder**,你可能也會[在啟動或 closure 中使用它](https://swiftontap.com/viewbuilder)。 --- ### Chapter 3:屬性包裝 介紹 SwiftUI 中常用的屬性包裝:State、Binding 和 Environment;練習建立清單、表單,並使用 enum 整理程式碼。 ##### 相關連結 * [EnvironmentValues 環境變數](https://developer.apple.com/documentation/swiftui/environmentvalues) * 影片中提到的「**在 result builder 中,local 變數會被當作建造的 block**」,詳細的資訊可以在這個 [evolution 記錄](https://github.com/apple/swift-evolution/blob/main/proposals/0289-result-builders.md#the-result-builder-transform)中了解,在 *The result builder transform* 的分類下可以認識 result builder 對不同語句的判斷。 --- ### Chapter 4:資料持久化 介紹 iOS 環境原生的資料持久化方式以及編碼的概念介紹,並實作一個設定畫面,使用 AppStorage 儲存布林、enum 和 Array 的資料。 ##### 相關連結 * [官方的資料持久化文件](https://developer.apple.com/documentation/swiftui/persistent-storage) * [會新增 Presentation 的調整器](https://developer.apple.com/documentation/swiftui/view-presentation) * 影片中提到可以嘗試**建立自己的 AppStorage 屬性包裝**,如果有興趣可以參考 [SwiftLee 的這篇文章](https://www.avanderlee.com/swift/appstorage-explained/ ),不過這個是個相對進階的內容,你會需要有基本 Combine 概念、了解 ObservableObject 和 DynamicProperty 。 * 使用 FileManager 時,你可能會需要知道[如何取得檔案 URL](https://chaocode.co/blog/getting-url)。 --- ### Chapter 5:測試 介紹測試的基本概念、Xcode 的測試介面並實作一個測試,以及使用 Measurement 進行單位轉換並且根據使用者 Locale 顯示在地化的單位字串。 ##### 相關連結 * [WWDC20: 在地化的格式化工具](https://developer.apple.com/videos/play/wwdc2020/10160/),影片中介紹了能把日期、單位、數字和文字等等資料,根據使用者 Locale 進行格式化的工具。 * [WWDC19: 測試、Test Plan、CICD 介紹](https://developer.apple.com/wwdc19/403) * 了解 [Locale](https://developer.apple.com/documentation/foundation/locale),Locale 並不單指語言,而是結合語言加上地區,提供更精確的慣用法。例如同樣是英文,在不同國家寫日期的順序依然會有所不同。 - 在你的 app 沒有做其他語言之前,**Locale 會被設定成你的專案的 base language**,詳細的介紹可以看[這篇文章](https://medium.com/swlh/know-your-language-locale-in-swift-beae4fcc5174),裡面也提供了`取得使用者偏好 / 正在使用的語言的方法`,在你還沒做多語言之前,你可以嘗試取得這些值來強制修改 Locale。 * [在 iOS16 加上工具列的背景色](https://sarunw.com/posts/swiftui-tabview-color/):文章是針對 TabBar 介紹,不過這個調整器 `toolbarBackground` 也能用來修改 Navigation Bar。 * 你可能會發現 TabBar 在 iOS14 以前長得不一樣 🥲,如果你想要全部統一的話可以參考[這篇文章中的程式碼](https://blog.personal-factory.com/2021/12/29/ios15-transparent-navigationbar-and-tabbar-by-default/)做修改。 --- ### Chapter 6:網路呼叫 介紹基本的網路概念和 Codable 的進一步應用,並且建立一個串接 [The Cat API](https://thecatapi.com/) 的新專案。 新專案的實作內容包含: - 建立一個專門處理網路的 Manager。 - 處理錯誤和顯示 alert。 - **觀察 reference type**的`ObservableObject`。 - 自動載入更多內容的 Infinite Scroll。 - 認識`.task` 調整器和建立一個新的`Task`的差別。 ##### 相關連結 * 6-2 [常見 HTTP 狀態碼](https://developer.mozilla.org/zh-TW/docs/Web/HTTP/Status) * 6-2 [MIME 類型名稱對照](https://www.iana.org/assignments/media-types/media-types.xhtml) * 6-2 [判斷是否使用 Cache 資料的流程圖](https://developer.apple.com/documentation/foundation/nsurlrequest/cachepolicy/useprotocolcachepolicy) * 6-3 影片中提到的[將回傳加入 cache 的條件](https://developer.apple.com/documentation/foundation/urlsessiondatadelegate/1411612-urlsession) * 6-5 影片中使用的[快速產生 JSON 解析程式碼的網站](https://app.quicktype.io/)。要記得用自動產生的程式碼的時候,不管多簡單的資料都要自己再檢查一遍哦 * 6-8 [StateObject 的文件](https://developer.apple.com/documentation/swiftui/stateobject):這個文件簡單介紹了搭配 `ObservableObject` 的三個屬性包裝器,以及它們的更新時機。建議大概閱讀啟動和更新的部分,當未來遇到 StateObject 重複被啟動或是沒有如預期的更新的時候再次回來閱讀。 * 6-8 如果對 **StateObject 和 ObservedObject** 的差別有疑惑,可以參考 onevcat 的[這篇文章](https://onevcat.com/2020/06/stateobject/)。 * 6-11 [onAppear 和 task 調整器的差別](https://byby.dev/swiftui-task-vs-onappear),這篇文章提到的差別我覺得都蠻重要的,除了影片中提過的,還有額外講到 task 搭配 id 的用法。 * 6-11 如果對於使用`Task`和`Task.detached`的時機不太確定,可以參考[這篇文章](https://www.donnywals.com/understanding-unstructured-and-detached-tasks-in-swift/)裡面的 **When to use unstructured tasks** 和 **When to use detached tasks**。 ###### 另外,目前主流的做法是避免使用 detached,這並不是因為它不好,而是沒有什麼非得要用的原因(i.e. 沒必要讓自己的程式碼變複雜)。不過,我個人覺得 detached 的 explicit 對於初期掌握自己的程式碼在做什麼很有幫助,還有因為它沒有繼承而產生的一些報錯和警告也對初期學習很有幫助。 * 6-11 如果想瞭解更多關於`onAppear`出現的時機,可以看這篇關於 [View 的生命週期](https://www.vadimbulavin.com/swiftui-view-lifecycle/) 的文章。 * 6-11 影片中提到的,ObservableObject 會讓整個 View 進到 MainActor,[這篇文章](https://oleb.net/2022/swiftui-task-mainactor/)詳細講了關於 View struct 神秘的 @MainActor 情況。再一次強調,我覺得不用太在意這個,遇到這個錯誤就直接改進 MainActor 中就好。. URL: https://sunyazhou.com/2023/09/learnswiftuichapter4/index.html.md Published At: 2023-09-03 02:51:00 +0000 # SwiftUI第四章学习总结 # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! ## SwiftUI课程 最近坚持学习swiftUI,周末有空把第四章都看完了,我这里说的看是动手实践+教程学习.记录一些容易遗忘的内容 ### 主要内容包括 * SwiftUI中对应的UIKit相似组件 * @AppStorage UserDefault的属性封装器 * .environment(\\.colorScheme, shouldUseDarkMode ? .dark : .light) * 编码和解码(序列化类型Json) * 实现Codable协议自动生成`Decoder`和`Encoder` #### SwiftUI中对应的UIKit相似组件 下面是该SwiftUI框架对应的UI示意图 ![](/assets/images/20230805LearnSwiftUIChapter4/SwiftUIMap.webp) ``` swift var body: some View { Form { Section("基本设定") { Toggle(isOn: $shouldUseDarkMode) { Label("深色模式", systemImage: .moon) } Picker(selection: $unit) { ForEach(Unit.allCases) { $0 } } label: { Label("单位", systemImage: .unitSign) } Picker(selection: $startTab) { Text("随机食物").tag(HomeScreen.Tab.picker) Text("食物清单").tag(HomeScreen.Tab.list) } label: { Label("启动画面", systemImage: .house) } } Section("危险区域") { ForEach(Dialog.allCases){ dialog in Button(dialog.rawValue) { confirmationDialog = dialog } .tint(Color(.label)) } } .confirmationDialog(confirmationDialog.rawValue, isPresented: shouldShowDialog, titleVisibility: .visible) { Button("确定", role: .destructive, action: confirmationDialog.action) Button("取消", role: .cancel) { } } message: { Text(confirmationDialog.message) } } } ``` * Form --> 有点类似UIKit中的UITableView * Section --> 有点类似UIKit中的UITableView中的Section 在swiftUI中被独立成一种容器 * Toggle/Picker/Button --> 有点类似UIKit中的UITableViewCell并且里面已经布局好了Switch和其他选择器、按钮等 #### @AppStorage UserDefault的属性封装器 这个`@AppStorage` 是SwiftUI中的UserDefault的封装形式,用于本地存储类似NSUserDefault的方式存储key和value或者object,最终归档到plist文件中. 举个例子 ``` swift @AppStorage("shouldUseDarkMode") private var shouldUseDarkMode: Bool = false ``` 当在swiftUI中声明一个变量叫做`shouldUseDarkMode` 它的默认值是false,当UserDefault中取值取到的时候,这个`shouldUseDarkMode`成员变量就是取到的真实值,如果没取到 false作为没取到的备选默认值. @AppStorage("shouldUseDarkMode") 这里的字符串就是UserDefault要取值时候的key. #### .environment 环境变量 ``` swift @AppStorage(.shouldUseDarkMode) private var shouldUseDarkMode: Bool = false //深色模式 @AppStorage(.unit) private var unit: Unit = .gram @AppStorage(.startTab) private var startTab: HomeScreen.Tab = .picker @State private var confirmationDialog: Dialog = .inactive ``` ![](/assets/images/20230805LearnSwiftUIChapter4/darkmode.webp) 这里的Toggle也就是我们之前在UIKit中学习的switch开关触发的值直接关联到`@AppStorage("shouldUseDarkMode")`中,并且也会更新成员变量,这些操作都是SwiftUI帮我们做的. 当做完这个操作后要马上生效 这个时候我们要改动类似工程中的如下代码: ``` swift var body: some View { TabView(selection: $tab) { ForEach(Tab.allCases, id: \.self) { $0 } } .environment(\.colorScheme, shouldUseDarkMode ? .dark : .light) } ``` 这样改动虽然马上生效但是并不会作用于全局的ViewController之类的视图上,这个改动只会改动从响应者链条中最顶层的VC,假设有个某某DetailVC present出来 它将不受此环境控制 ![](/assets/images/20230805LearnSwiftUIChapter4/darkmode2.webp) 为了解决这个问题需要在顶层使用如下代码包裹起来,并使用`preferredColorScheme()`函数更改才会全局生效 ``` swift NavigationStack { TabView(selection: $tab) { ForEach(Tab.allCases, id: \.self) { $0 } } .preferredColorScheme(shouldUseDarkMode ? .dark : .light) } ``` ![](/assets/images/20230805LearnSwiftUIChapter4/darkmode3.webp) 如上就是这一部分学习到的技巧 完成代码如下 : ``` swift struct HomeScreen: View { @AppStorage("shouldUseDarkMode") var shouldUseDarkMode = false @State var tab: Tab = .settings var body: some View { NavigationStack { TabView(selection: $tab) { ForEach(Tab.allCases, id: \.self) { $0 } } // .environment(\.colorScheme, shouldUseDarkMode ? .dark : .light) .preferredColorScheme(shouldUseDarkMode ? .dark : .light) } } } ``` #### 编码和解码(序列化类型Json) 上述代码都是简单的用@AppStorage 存储普通数据类型,但是 如果它是一个Person对象的话该如何存储呢? 如果想被@AppStorage使用,也就是UserDefault使用则必须要实现编码和解码,有点类似NSObject要是Copy协议一样类似的操作,在swift里面是一个合成协议叫`Codable`,这个`Codable`中包含Decodable & Encodable 如下代码: ``` swift struct Person : Codable { var name: String var age: Int } //编码 调用时 let person = Person(name: "sunyazhou.com", age: 33) let data: Data = try! JSONEncoder().encode(person) let string: String = try! String(data: data, encoding: .utf8) ?? "" print(string) //输出; {"name":"sunyazhou.com","age":33} //解码调用时 let string = """ {"name":"sunyazhou.com","age":33} """ let data: Data = string.data(using: .utf8)! let person: Person = try! JSONDecoder().decode(Person.self, from: data) print(person) //输出:Person(name: "sunyazhou.com", age: 33) ``` #### 实现Codable协议自动生成`Decoder`和`Encoder` Xcode14以后会自动帮我们生成一个struct的编码和解码方式 当我们 按住`command+左键`点击Pseron的结构体的时候会出现一个列表 如下图: ![](/assets/images/20230805LearnSwiftUIChapter4/RawRepresentable.gif) 点击AddExplicitCodableImplementation选项会自动帮我们为Person生成如下. ``` swift struct Food: Equatable, Identifiable, Codable { var id = UUID() var name: String var image: String @Suffix("大卡") var calorie : Double = .zero @Suffix("g") var carb : Double = .zero @Suffix("g") var fat : Double = .zero @Suffix("g") var protein : Double = .zero enum CodingKeys: CodingKey { case id case name case image case calorie case carb case fat case protein } init(from decoder: Decoder) throws { let container: KeyedDecodingContainer = try decoder.container(keyedBy: Food.CodingKeys.self) self.id = try container.decode(UUID.self, forKey: Food.CodingKeys.id) self.name = try container.decode(String.self, forKey: Food.CodingKeys.name) self.image = try container.decode(String.self, forKey: Food.CodingKeys.image) self._calorie = try container.decode(Suffix.self, forKey: Food.CodingKeys.calorie) self._carb = try container.decode(Suffix.self, forKey: Food.CodingKeys.carb) self._fat = try container.decode(Suffix.self, forKey: Food.CodingKeys.fat) self._protein = try container.decode(Suffix.self, forKey: Food.CodingKeys.protein) } func encode(to encoder: Encoder) throws { var container: KeyedEncodingContainer = encoder.container(keyedBy: Food.CodingKeys.self) try container.encode(self.id, forKey: Food.CodingKeys.id) try container.encode(self.name, forKey: Food.CodingKeys.name) try container.encode(self.image, forKey: Food.CodingKeys.image) try container.encode(self._calorie, forKey: Food.CodingKeys.calorie) try container.encode(self._carb, forKey: Food.CodingKeys.carb) try container.encode(self._fat, forKey: Food.CodingKeys.fat) try container.encode(self._protein, forKey: Food.CodingKeys.protein) } } ``` > `@Suffix()`是我自定义的一个属性封装器用于给某个变量字符添加默认的后缀,这里可以不用care它. 这里注意有的xcode不会生成这么全面是因为有很多extension 比如: ``` swift extension Food: Codable{ } ``` 这的代码是在扩展里写的不是在Person声明的地方写的Xcode会找不到,最好的方式肯定是写在一起最好. # 总结 以上是学习完第四章的内容总结,潦草了些,希望通过记录能让以后使用的时候印象更加深刻也方便分享给其它需要的人. [第四章demo](https://github.com/sunyazhou13/FoodPicker) URL: https://sunyazhou.com/2023/08/learnswiftuichapter3/index.html.md Published At: 2023-08-05 06:13:00 +0000 # SwiftUI第三章学习总结 ![](/assets/images/20230604LearnSwiftUIChapter1/swiftuilogo.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! ## SwiftUI课程 最近坚持学习swiftUI已经学习到第三章了,把一些重要的内容都记录下来 ### 主要内容包括 * present子VC * 用enum管理icon * 注释中可以使用markdown * extension写法 ### present子VC 在swiftUI中不能说是弹出VC而是弹出 some View的视图. ``` swift var foodDetailView: some View { VStack { if (shouldShowInfo) { Grid(horizontalSpacing: 12, verticalSpacing: 12) { GridRow { Text("蛋白质") Text("脂肪") Text("碳水") }.frame(minWidth: 40) Divider() .gridCellUnsizedAxes(.horizontal) .padding(.horizontal, -10) GridRow { Text(selectedFood!.$protein) Text(selectedFood!.$carb) Text(selectedFood!.$fat) } } .font(.title3) .padding(.horizontal) .padding() .roudedRectBackground() .transition(.moveUpWithOpacity) } } .maxWidth() .clipped() } ``` 其实就是创建一个View就完事了根本不需要复杂的操作,没有 我们在UIKit中的push和pop,或者present和dismiss。这里基本都是一些视图基于状态的监听 show不show改变了我们之前的使用方式. ### 用enum管理icon 当使用icon多了的时候需要用变量来从统一的数据源取图,因为swift的枚举可以声明各种类型,一般都是字符串.所以字符串枚举变量就应声而出 ``` swift import SwiftUI enum SFSymbol: String { case pencil case plus = "plus.circle.fill" case chevronUp = "chevron.up" case chevronDown = "chevron.down" case xmark = "xmark.circle.fill" case forkAndKnife = "fork.knife" case info = "info.circle.fill" } extension SFSymbol : View { var body: Image { Image(systemName: rawValue) } func resizable() -> Image { self.body.resizable() } } extension Label where Title == Text, Icon == Image { init(_ text: String, systemImage: SFSymbol) { self.init(text, systemImage: systemImage.rawValue) } } ``` 使用的时候 ``` swift var addButton : some View { Button { sheet = .newFood { food.append($0) } } label: { SFSymbol.plus //直接使用 .font(.system(size: 50)) .padding() .symbolRenderingMode(.palette) .foregroundStyle(.white, Color.accentColor.gradient) } } ``` #### 注释中可以使用markdown 先看两个函数 ``` swift /// - Tag:push func push(to alignment: TextAlignment) -> some View { switch alignment { case .leading: return frame(maxWidth: .infinity, alignment: .leading) case .center: return frame(maxWidth: .infinity, alignment: .center) case .trailing: return frame(maxWidth: .infinity, alignment: .trailing) } } /// 使用最大宽度 Shortcut:[push(to:.center)](x-source-tag://push) func maxWidth() -> some View { push(to: .center) } ``` > 注意: /// 使用最大宽度 Shortcut:\[push(to:.center)\](x-source-tag://push) 你发现没有注释默认支持了markdown ![](/assets/images/20230805LearnSwiftUIChapter3/note1.webp) 蓝色跳转拦截的函数声明 `/// - Tag:push`, `Tag`必须大小写写清楚,不能有空格,使用`x-source-tag`的`scheme`跳转至`push` ![](/assets/images/20230805LearnSwiftUIChapter3/note2.webp) ### extension写法 当给swift工程写很多扩展之后,类会变得越来越多并且越来越不好管理,为了解决不好管理,可读性差问题 我们约定俗成,**当创建swift扩展的时候使用尾部文件追加+号来命名** 如下图: ![](/assets/images/20230805LearnSwiftUIChapter3/extension.webp) # 总结 第三章的内容特别漫长,需要有耐心, 中间还讲了一个表单,我没有在这里表述是因为 东西多而且还复杂,需要大家认真看视频比我在这里写总结要更有说服力. [第三章课程链接](https://www.bilibili.com/video/BV1A84y147o8/?spm_id_from=333.880.my_history.page.click&vd_source=9309f71afe97e633abeadc8407870e76) [工程代码](https://github.com/jane-chao/SwiftUIBeginnerCourse) [本文完结代码示例](https://github.com/sunyazhou13/FoodPicker) URL: https://sunyazhou.com/2023/08/ios17widget/index.html.md Published At: 2023-08-04 06:56:00 +0000 # 开发iOS17小组件 # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! # iOS17关于小组件的WWDC [WWDC23](https://developer.apple.com/wwdc23/)中提供了一些关于小组件的视频,最近开发小组件遇到了一些问题,在这里解读一下视频中的内容 ## Bring widgets to new places [Bring widgets to new places](https://developer.apple.com/videos/play/wwdc2023/10027/?time=180) The widget ecosystem is expanding: Discover how you can use the latest WidgetKit APIs to make your widget look great everywhere. We'll show you how to identify your widget's background, adjust layout dynamically, and prepare colors for vibrant rendering so that your widget can sit seamlessly in any environment. 小部件生态系统正在扩展:了解如何使用最新的WidgetKit api使您的小部件在任何地方看起来都很棒。我们将向您展示如何识别小部件的背景,动态调整布局,并为充满活力的渲染准备颜色,以便您的小部件可以在任何环境中无缝地运行。 ### Transition to content margins [ Transition to content margins](https://developer.apple.com/videos/play/wwdc2023/10027/?time=104) 从 ios14 引入桌面小组件,后又在 ios16 增加锁屏小组件,现在,它可以扩展到:mac 桌面、pad 锁屏、iphone StandBy 以及 iwatch 的 new Smart Stack。所以确保一次编码后在各个位置都能表现出色是件很重要的事。我的理解就是iOS, iPadOS, WatchOS, macOS 全苹果平台通用适配.也就是说编码一次小组件可以运行到所有苹果的系统上. 以 watchOS 的 safe area 为例: * watchOS9 and below: 使用了system-defined 的 safe area 以避免小组件太贴边。但开发者可以通过`Color.blue.ignoresSafeArea()` 来忽略它 * watchOS10 and above: 方法`ignoresSafeArea()`不在起作用,而应该对 `widget configuration` 应用 `contentMarginsDisabled`修改器来实现这一目的 ![](/assets/images/20230804iOS17Widget/widget1.gif) ![](/assets/images/20230804iOS17Widget/widget2.webp) ![](/assets/images/20230804iOS17Widget/widget3.webp) ### Add a removable background 现有的 accessory family 小组件可以自动适配到 ipad 的锁屏上。ipad can also show system small widgets right alongside them。 但会有一个半透明的背景,如果想去掉它,可以使用: ``` swift ZStack { // TODO }.containerBackground(for: .widget) { Color.gameBackground } ``` 这样系统就可以根据显示的位置来自动决定是否要显示背景。对于,watchOS 上的组件也一样,系统会默认给予一个半透明背景,也可以通过这种方式交给系统来进行处理背景,以展示丰富多彩的样式。 如果确实不想系统对某些组件(如:ios上的桌面照片组件)使用可移除的背景,可以在小组件的 widget configuration 上应用 `containerBackgroundRemovable(false)` 修改器。 ### Dynamically adjust layout 当使用 StandBy mode 时,小组件会被放大,并且会移除背景。可以通过使用以下代码来检查背景是否被移除来进行适配: ``` swift @Environment(\.showsWidgetContainerBackground) var showsWidgetBackground ``` ### Prepare for vibrant rendering 在 ipad lock screen 上的小组件,系统可能会根据需要调整饱和度(desaturate),也就是说小组件可能需要根据实际情况进行不同的色彩、背景等样式微调,此时可以通过以下代码来检查渲染是否为 .vibrant 来进行适配: ``` swift @Environment(\.widgetRenderingMode) var widgetRender ``` ## Adding interactivity to widgets and Live Activities [Adding interactivity to widgets and Live Activities ](https://developer.apple.com/documentation/WidgetKit/Adding-interactivity-to-widgets-and-Live-Activities) ### AppIntent 这可能是此次WWDC中小组件开发最感兴趣的内容之一了,它简化了交互式小组件的现有实现方式,同时也对现有的交互效果有了明显的改善。 从 iOS 17, iPadOS 17, 以及 macOS 14 开始,widgets 和 Live Activities 可以使用 buttons 和 toggles 在不启动主 app 的情况下提供一些特殊功能。 > 注意:仅仅 Button 和 Toggle 组件支持,需要引入import AppIntents模块 以下类型的小组件在新版本 sdk 中包含此特性的 Button 和 Toggle: ``` swift WidgetFamily.systemSmall WidgetFamily.systemMedium WidgetFamily.systemLarge WidgetFamily.systemExtraLarge WidgetFamily.accessoryCircular on iPhone and iPad WidgetFamily.accessoryRectangular on iPhone and iPad ``` 在具体开发过程中需要实现自己的 AppIntent 对象: ``` swift struct MyIntent : AppIntent { static var title: LocalizedStringResource = "title" func perform() async throws -> some IntentResult { await MyActionWhichMaybeAsync() return .result() } } ``` 这里需要说明的是: * 当 perform() 返回后,系统会立即触发 timeline 更新 * 所以 perform 中如果是异步操作时,需要 await 异步操作结束 * 需要在 perform 中更新小组件数据并持久化 AppIntent 工作的本质就是由系统触发 timeline 来实现小组件视图(不可变对象)更新。具体的使用方式可以参考[官方代码示例](https://developer.apple.com/documentation/WidgetKit/Adding-interactivity-to-widgets-and-Live-Activities)。 ### Preview 这里有个有趣的地方就是更新了 preview 的方式:相比原来的: ``` swift struct MyWidgetView_Previews: PreviewProvider { static var previews: some View { MyWidgetView().previewContext(WidgetPreviewContext(family: .systemLarge)) } } ``` 增加了 timeline 的支持,可以方便的观察动画效果等: ``` swift #Preview(as: WidgetFamily.systemLarge) { // TODO:view } timeline: { // TODO:timeline list } ``` ### Animating data updates in widgets and Live Activities [Animating data updates in widgets and Live Activities](https://developer.apple.com/documentation/WidgetKit/Animating-data-updates-in-widgets-and-live-activities) 把 Live Activity 中的效果扩展到了 Widget 上,Live Activity 中的 timer 类型 Text 自带翻页特效,如果不想使用的话可以使用以下方式去掉: ``` swift Text(Date.now, style: .timer).contentTransition(.identity) ``` # 总结 iOS小组件有一些细微改动的,经过WWDC分析 我们还是需要认真学习的. URL: https://sunyazhou.com/2023/08/swiftuiextention1/index.html.md Published At: 2023-08-02 09:13:00 +0000 # SwiftUI可用性检测,解决小组件iOS17 available问题 # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! # 问题 ![](/assets/images/20230802swiftuiextention1/WidgetiOS17.webp) 最近在开发iOS17上的小组件使用SwiftUI框架,第一次进行工程化遇到一个 api可用性检测问题 之前的小组件是锁屏小组件,在iOS16上运行,最近iOS17更新了新内容,导致运行起来之后被提示需要增加 ``` swift containerBackground(.red.gradient, for: .widget) ``` 这样的适配容器,这就导致 在iOS16上的api `some View`要使用iOS17的API ``` swift ZStack(alignment: .bottomTrailing) { Image("widget_clock") .resizable() .aspectRatio(contentMode: .fill) VStack(spacing: 0) { Text("\(entry.date.hour)") Text("\(entry.date.min)") } .font(.system(size: 60, weight: .bold)) .foregroundColor(.white) } . if #available(iOSApplicationExtension 17.0, *) { //这行代码报错 告知我不能这样做 不识别if $0.containerBackground(.red.gradient, for: .widget) } else { // Fallback on earlier versions } ``` 研究半天 发现还得给view扩展一个返回自己的函数来链式配置. 这写法真是不太明白咋写合适最终只能 给View写个extension ``` swift import Foundation import SwiftUI public extension View { func modify(@ViewBuilder _ transform: (Self) -> Content) -> Content { transform(self) } } ``` 然后使用的时候 ``` swift .modify{ if #available(iOSApplicationExtension 17.0, *) { $0.containerBackground(.red.gradient, for: .widget) } else { // Fallback on earlier versions } } ``` 这样在swiftUI的 body 里面才能顺利编译通过 完整的测试代码如下 ``` swift struct MomentsWidget: Widget { let kind: String = WidgetType.moments.kind var body: some WidgetConfiguration { IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: MomentsWidgetProvider()) { entry in MomentsWidgetEntryView(entry: entry) .modify{ //这里是适配代码 if #available(iOSApplicationExtension 17.0, *) { $0.containerBackground(.white.gradient, for: .widget) } else { // Fallback on earlier versions } } } // 这里定义的就是小组件弹出界面中的标题与副标题 .configurationDisplayName("好友动态") .description("通过该组件可以创建好友动态列表") .supportedFamilies([.systemLarge]) } } ``` # 总结 1.写swiftUI感觉像是第一次接触UIKit时候那样 一开始比较困难时因为对它缺少认知 2.swiftUI的框架设计应该把这种问题考虑进去 提供一个专用的API,或者至少不要让Xcode的提示出错吧,Xcode提供的东西都出错只能说明这个东西还不成熟. [参考stackoverflow](https://stackoverflow.com/questions/76595240/widget-on-ios-17-beta-device-adopt-containerbackground-api) URL: https://sunyazhou.com/2023/06/learnswiftuichapter2/index.html.md Published At: 2023-06-18 10:52:00 +0000 # SwiftUI第二章学习总结 ![](/assets/images/20230604LearnSwiftUIChapter1/swiftuilogo.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! # SwiftUI课程 最近在听B站以为来自祖国宝岛台湾省的一个女博主(声音很嗲dia)讲解SwiftUI课程,讲的不错把学习的内容记录下来: ## 主要内容包含 * @propertyWrapper * VStack、HStack、ZStack等使用 * 封装extension处理颜色、转场动画、ShapeStyle * @ViewBuilder、Group容器使用、Grid的行列使用 #### 属性封装器 创建了一个`SwiftUI`的类`SuffixWrapper.swift` ``` swift // // SuffixWrapper.swift // FoodPicker // // Created by sunyazhou on 2023/6/18. // import Foundation import SwiftUI @propertyWrapper struct Suffix: Equatable { var wrappedValue: Double private let suffix: String init(wrappedValue: Double, _ suffix: String) { self.wrappedValue = wrappedValue self.suffix = suffix } var projectedValue : String { wrappedValue.formatted() + " \(suffix)" } } ``` > 这里记录一下遇到的坑是`projectedValue ` 在使用的代码的时候如果想让左右Double类型的变量提供一个 字符串 以` g`结尾的字符串变量可以使用这个属性封装器. ``` swift selectedFood.$protein //用$加变量名称就是访问的projectedValue ``` 这个主要看下 Food模型代码大家可以一目了然我说的是什么意思了. ``` swift import Foundation struct Food: Equatable { var name: String var image: String @Suffix("大卡") var calorie : Double = .zero @Suffix("g") var carb : Double = .zero @Suffix("g") var fat : Double = .zero @Suffix("g") var protein : Double = .zero static let examples = [ Food(name: "漢堡", image: "🍔", calorie: 294, carb: 14, fat: 24, protein: 17), Food(name: "沙拉", image: "🥗", calorie: 89, carb: 20, fat: 0, protein: 1.8), Food(name: "披薩", image: "🍕", calorie: 266, carb: 33, fat: 10, protein: 11), Food(name: "義大利麵", image: "🍝", calorie: 339, carb: 74, fat: 1.1, protein: 12), Food(name: "雞腿便當", image: "🍗🍱", calorie: 191, carb: 19, fat: 8.1, protein: 11.7), Food(name: "刀削麵", image: "🍜", calorie: 256, carb: 56, fat: 1, protein: 8), Food(name: "火鍋", image: "🍲", calorie: 233, carb: 26.5, fat: 17, protein: 22), Food(name: "牛肉麵", image: "🐄🍜", calorie: 219, carb: 33, fat: 5, protein: 9), Food(name: "關東煮", image: "🥘", calorie: 80, carb: 4, fat: 4, protein: 6), ] } ``` > 带@Suffix关键字就是我们自定义的属性封装器,为普通变量提供一个字符串带`g`后缀的字符方法 #### VStack、HStack、ZStack等使用 他们分别是: * 纵轴 * 横轴 * Z轴 > 注意: 它最多支持10层视图,如果超过了请使用Group圈一下. 这里说几个比较重要的点 当搭配这些视图使用的时候 避免不了子视图之间要用间距,SwiftUI提供了默认的间距对象 ``` swift Spacer().layoutPriority(1) //注意这里的layoutPriority(1) ``` 当我们留有空白大小的位置需要填充时可以使用`Spacer()`,这玩意有个坑就是如果外不各种Stack不给定它大小他将平均取所有Stack中最小的. 需要给它提高优先级让它提前布局知道还剩余多大空间需要撑满,才不会显示异常.所以使用`layoutPriority(1)`为了让它提前推导出它需要的空白大小. 容器的外部设置可以自动被继承给子容器(这里的容器eg: ScrollView、VStack...) ``` swift ScrollView { VStack(spacing: 30) { foodImage Text("今天吃什么?").bold() selectedFoodInfoView Spacer().layoutPriority(1) selectFoodButton cancelButton } .padding() .frame(maxWidth: .infinity, minHeight: UIScreen.main.bounds.height - 100) .font(.title) .mainButtonStyle() .animation(.mySpring, value: shouldShowInfo) .animation(.myEase, value: selectedFood) } .background(.bg2) ``` 假设`.background(.bg2)`默认VStack也是`.bg2`颜色的背景 #### 封装extension处理颜色、转场动画、ShapeStyle SwiftUI里面比objc中使用 扩展更加频繁 ``` swift // // Extensions.swift // FoodPicker // // Created by sunyazhou on 2023/6/18. // import Foundation import SwiftUI extension View { func mainButtonStyle() -> some View { buttonStyle(.borderedProminent) .buttonBorderShape(.capsule) .controlSize(.large) } func roudedRectBackground(radius: CGFloat = 8, fill: some ShapeStyle = .bg ) -> some View { background(RoundedRectangle(cornerRadius: radius).fill(fill)) } } extension Animation { static let mySpring = Animation.spring(dampingFraction: 0.55) static let myEase = Animation.easeInOut(duration: 0.6) } extension ShapeStyle where Self == Color { static var bg: Color { Color(.systemBackground) } static var bg2: Color { Color(.secondarySystemBackground) } } extension AnyTransition { static let delayInsertionOpacity = Self.asymmetric( insertion:.opacity.animation(.easeInOut(duration: 0.5).delay(0.2)), removal:.opacity.animation(.easeInOut(duration: 0.4)) ) static let moveUpWithOpacity = Self.move(edge: .top).combined(with: .opacity) } ``` 这里说一下这个`extension ShapeStyle where Self == Color` 因为有一个协议叫`ShapeStyle `不单纯是一个颜色或者填充的对象可以设置背景颜色,其它类似渐变的图像也算使用遵守`ShapeStyle `的样式.这里准确来说`Color`只是ShapeStyle的一种. 当我们访问颜色的时候正常用的是 ``` swift Color().bg2 //bg2颜色 ``` 而符合`ShapeStyle`协议的对象可以直接用 ``` swift .bg2 //设置颜色 省略了输入Color ``` #### @ViewBuilder、Group容器使用、Grid的行列使用 `@ViewBuilder`限制了some View中 例如VStack、HStack等各种Stack的数量最多十层 如果想使用的话需要把多级视图用`Group `或者`Grid`圈起来 这样说你可能理解不了我举个例子: 假设一个ViewBuilder的UIView最多让你`addSubView:`10次,你有很多subview,那么你就需要把你的subview按照你的功能划分或者规则 几个放在一个UIView上,假设它就叫GroupView吧,让后把GroupView的实例add到符合`ViewBuilder `的协议View上.这样就相当于一个Group里面可以放1~9个不等, 但是你最终提供的还是一个UIView的实例add到上去了. > 这就相当于跳板原理,一个公司有一个局域网、但是交换机最多有10个端口,为了让更多人加入必须通过1拖10的形式增设交换机来满足更多人上网的需求.这样你理解了吧! 至于为啥是10个 我找到了这样的代码 ``` swift @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) extension ViewBuilder { public static func buildBlock(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4, _ c5: C5, _ c6: C6, _ c7: C7, _ c8: C8, _ c9: C9) -> TupleView<(C0, C1, C2, C3, C4, C5, C6, C7, C8, C9)> where C0 : View, C1 : View, C2 : View, C3 : View, C4 : View, C5 : View, C6 : View, C7 : View, C8 : View, C9 : View } ``` 如果你想让它支持更多你得处理他们的顺序关系,这是它提供最多处理各种层级关系的函数接口没有实现代码.不知道我们自己为它扩展行不行! #### Group 和 Grid Group相当于Fultter里面的container.可以当几个视图进去进行统一配置,采用统一管理,内部子视图可以默认使用它的配置也可以覆盖它的配置修改使用自己的配置,总之就是帮开发者做了很多统一管理子视图的容器,想自定义需要自己配置就好. Grid 使格子布局那种 应该是iOS16以后推出的类似Group一样的容器,主要是负责管理N行N列这种类似Excel表格一样的制表容器.能加分割线 ``` swift VStack { if (shouldShowInfo) { Grid { GridRow { Text("蛋白质") Text("脂肪") Text("碳水") }.frame(minWidth: 40) Divider() .gridCellUnsizedAxes(.horizontal) .padding(.horizontal, -10) GridRow { Text(selectedFood!.$protein) Text(selectedFood!.$carb) Text(selectedFood!.$fat) } } .font(.title3) .padding(.horizontal) .padding() .roudedRectBackground() .transition(.moveUpWithOpacity) } } .frame(maxWidth: .infinity) .clipped() ``` 分割线`Divider` 也可以使用下面代码手动加分割 ``` swift HStack { VStack(spacing: 12) { Text("蛋白质") Text(selectedFood!.protein.formatted() + " g") } Divider().frame(width: 1).padding(.horizontal) VStack(spacing: 12) { Text("脂肪") Text(selectedFood!.fat.formatted() + " g") } Divider().frame(width: 1).padding(.horizontal) VStack(spacing: 12) { Text("碳水") Text(selectedFood!.carb.formatted() + " g") } } .font(.title3) .padding(.horizontal) .padding() .background(RoundedRectangle(cornerRadius: 8).foregroundColor(Color(.systemBackground))) ``` # 总结 工作时间很紧张,周末有时间记录一些重要容易被遗忘的内容,很水,希望大家不要介意.第二章比较实用一些,希望能记录和分享技术.天气太热了,住在没有空调的北京某出租屋,学习真的是一件汗流浃背,体验酷暑的苦涩经历. [第二章demo](https://github.com/sunyazhou13/FoodPicker) [2-1 排版练习 (1/2) - SwiftUI 新手入门](https://www.bilibili.com/video/BV1pW4y1j7MC/?spm_id_from=333.788&vd_source=9309f71afe97e633abeadc8407870e76) URL: https://sunyazhou.com/2023/06/ios16wkwebview/index.html.md Published At: 2023-06-15 00:24:00 +0000 # 解决iOS16 WKWebview无法调试问题 # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! ## 问题 最近在IOS16更新后,无法在mac端的safari看到调试信息了.这让我很是头疼,经过查各种资料发现一个Aeppl WebKit团队很脑残的一个操作 [Enabling the Inspection of Web Content in Apps](https://webkit.org/blog/13936/enabling-the-inspection-of-web-content-in-apps/) 某版本更新以后,新增了个isInspectable的属性,而且默认是关闭的。如果是iOS16以下就没有这个问题是因为iOS16的SDK中才有的这个字段, 这种16.4的新特性很SB,不做向下兼容考虑. 正确的方式应该是设置`webView.inspectable` 为`YES` ``` objc WKWebViewConfiguration *webConfiguration = [WKWebViewConfiguration new]; WKWebView *webView = [[WKWebView alloc] initWithFrame:CGRectZero configuration:webConfiguration]; webView.inspectable = YES; ``` > 注意这个API是iOS16.4才有的特性,记得加上可用性检测的API > 还有如果你公司的打包机没有最近版本的Xcode请记得升级,否则有可能造成这个API在苹果的iOS16SDK中才有,其它SDK中没有引起的打包机打包失败. # 总结 如果开发WKWebView的同学记得关注一下,有很多开发者觉得看到了不是干这个业务的这个不需要关注,往往很多时候你偶尔遇到就会手足无措,还是建议你要么记住这个问题,要么告诉一下周围同事周知这个事情,总之不要犯没有经验而不去学习了解的错误. [官方对此问题解释](https://webkit.org/blog/13936/enabling-the-inspection-of-web-content-in-apps/) [https://zhuanlan.zhihu.com/p/622049301](https://zhuanlan.zhihu.com/p/622049301) URL: https://sunyazhou.com/2023/06/blackwhitefilter/index.html.md Published At: 2023-06-14 01:37:00 +0000 # UIImage黑白滤镜 # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! ## 背景 最近开发功能产品有一个需求是这样的,将一张图片变成黑白图片,我们第一想法是加render着色,然而着色不产品想要的需求,产品的意思是加黑白滤镜,之前开发的同事曾经搞过一些私有API做混淆的方式实现了公祭日之类的纪念活动把工程里所有的视图都搞成了黑白色.可是我总不能为了一个艰难的功能动用私有API做混淆被苹果教育吧! 经过各种查找都没有最优解,我想到了ChatGPT,经过和ChatGPT问答的形式它给出了如下代码: ``` objc #import #import @interface UIImage (RenderingColor) /// 给一张图片加黑白滤镜 /// - Parameter image: 图片 + (UIImage *)applyBlackWhiteFilterToImage:(UIImage *)image; @end @implementation UIImage (RenderingColor) + (UIImage *)applyBlackWhiteFilterToImage:(UIImage *)image { if (image == nil) { return nil; } CIImage *ciImage = [CIImage imageWithCGImage:image.CGImage]; CIFilter *filter = [CIFilter filterWithName:@"CIPhotoEffectMono"]; // Create the black-and-white filter [filter setValue:ciImage forKey:kCIInputImageKey]; CIContext *context = [CIContext contextWithOptions:nil]; CIImage *outputImage = [filter outputImage]; CGImageRef cgImage = [context createCGImage:outputImage fromRect:[outputImage extent]]; UIImage *filteredImage = [UIImage imageWithCGImage:cgImage]; CGImageRelease(cgImage); return filteredImage; } @end ``` 通过上述操作我们就轻松的实现了给一个UIImage 加黑白滤镜让它变成灰白色的效果, ![](/assets/images/20230614BlackWhiteFilter/BlackWhiteFilter.webp) #### 能给CALayer加吗? 能 ``` objc #import #import @interface CALayer (BlackWhiteFilter) - (void)applyBlackWhiteFilter; @end @implementation CALayer (BlackWhiteFilter) - (void)applyBlackWhiteFilter { CIFilter *filter = [CIFilter filterWithName:@"CIPhotoEffectMono"]; // Create the black-and-white filter // Convert the CALayer's contents to a CIImage if ([self.contents isKindOfClass:[UIImage class]]) { CIImage *ciImage = [CIImage imageWithCGImage:(CGImageRef)self.contents]; // Set the input image for the filter [filter setValue:ciImage forKey:kCIInputImageKey]; // Apply the filter CIContext *context = [CIContext contextWithOptions:nil]; CIImage *outputImage = [filter outputImage]; CGImageRef cgImage = [context createCGImage:outputImage fromRect:[outputImage extent]]; // Set the filtered image as the layer's contents self.contents = (__bridge id)cgImage; CGImageRelease(cgImage); } } @end ``` 本质核心是使用`CIPhotoEffectMono`内置滤镜对图片进行绘制 > 1.这个是iOS5.0以上提供的方法,大家不用担心系统兼容问题 > 2.这个不是私有API请放心使用,没有审核风险 这个方法的最低兼容iOS系统是iOS 5.0及以上版本。关于私有API和App Store审核风险的问题,请注意以下几点: * 1.该方法并不使用私有API,它是使用了Core Image框架提供的公共接口来创建和应用滤镜效果。 * 2.Core Image框架是iOS的公共框架,不属于私有API。因此,使用该方法不会违反苹果的App Store审核指南。 * 3.提供了适当的错误处理和安全检查,以确保在不支持的设备上不会发生崩溃或异常行为。 # 总结 经过反复找寻互联网上的资料,千篇一律,各种复制粘贴的文章真是没法看,不好使不说还不解决问题的同时制造很多互联网垃圾.希望互联网上这样的事情能变的纯净一些. URL: https://sunyazhou.com/2023/06/learnaboutvisionos/index.html.md Published At: 2023-06-09 09:08:00 +0000 # 学习如何在visionOS上开发APP ![](/assets/images/20230609LearnAboutVisionOS/visionos.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! ## 初识visionOS visionOS 可以理解为新的运行在苹果头显上的iOS系统, 用苹果的话就是各种高大上什么空间技术...。里面会用到SwiftUI, UIKit, RealityKit和ARKit框架,在visionOS上开发APP的话最好是拥有以上各种Kit开发经验,如果不了解还是希望你学学.或者直接从SwiftUI开始. * SwiftUI 相当于之前的UIKit只不过是新的写UI的框架系统用于swift开发 * UIKit是原始Objective-C的UI系统 * RealityKit增强现实框架 * ARKit虚拟现实框架 ## spatial computing(空间计算) 了解构成空间计算的基础—窗口(windows)、体积(volumes )和空间(spaces)—并了解如何使用这些元素来构建引人入胜的沉浸式体验。我们将带您了解用于为visionOS创建应用程序的框架,并向您展示如何设计深度,规模和沉浸感。探索如何使用来自Apple的工具,如Xcode和新的Reality Composer Pro,以及如何制作适合每个人的空间计算应用程序。 * windows 这玩意很重要,就是我们之前现实UIView之类的window,一般用于显示2D的内容视图容器, * volumes 这个是对window的扩充和增强容器,用于容纳2D和3D内容 * spaces 相当于摄影机,也就是我们人眼的位置,用于显示款贯穿和全屏以及3D大屏曲面模式的一种类型. ![](/assets/images/20230609LearnAboutVisionOS/visionos0.webp) 官方的解释如下: * windows 你可以在你的visionOS应用中创建一个或多个窗口。它们是用SwiftUI构建的,包含传统的视图和控件,你可以通过添加3D内容来增加体验的深度。 * volumes 使用3D体积为你的应用添加深度。卷是SwiftUI场景,可以使用RealityKit或Unity展示3D内容,创建可从共享空间或应用程序的完整空间的任何角度查看的体验。 * spaces 默认情况下,应用程序启动到共享空间,它们并排存在,就像Mac桌面上的多个应用程序一样。应用程序可以使用窗口和卷来显示内容,用户可以在他们喜欢的任何地方重新定位这些元素。为了获得更身临其境的体验,应用程序可以打开一个专门的Full Space,只显示该应用程序的内容。在Full Space中,应用程序可以使用窗口和卷,创建无限的3D内容,打开通往不同世界的门户,甚至让人完全沉浸在一个环境中。 了解构成空间计算的基础—窗口、体积和空间—并了解如何使用这些元素来构建引人入胜的沉浸式体验。我们将带您了解用于为visionOS创建应用程序的框架,并向您展示如何设计深度,规模和沉浸感。探索如何使用来自Apple的工具,如Xcode和新的Reality Composer Pro,以及如何制作适合每个人的空间计算应用程序。 这里有了解这个的4个视频 ![](/assets/images/20230609LearnAboutVisionOS/MeetSpatialComputing1.webp) * [Get started with building apps for spatial computing](https://developer.apple.com/videos/play/wwdc2023/10260/) * [Principles of spatial design](https://developer.apple.com/videos/play/wwdc2023/10072/) * [Create accessible spatial experiences](https://developer.apple.com/videos/play/wwdc2023/10034/) * [Develop your first immersive app](https://developer.apple.com/videos/play/wwdc2023/10203/) 一旦您熟悉了visionOS的基础知识,就可以进一步了解支持该平台的框架。参观一下visionOS的SwiftUI,了解如何为窗口和卷添加深度,以及如何使用Full Space让人们以前所未有的方式体验您的应用程序。我们还将向您介绍用于空间计算的UIKit,并分享如何与SwiftUI一起使用它。 ![](/assets/images/20230609LearnAboutVisionOS/MeetSpatialComputing2.webp) [Meet SwiftUI for spatial computing](https://developer.apple.com/videos/play/wwdc2023/10109/) [Meet UIKit for spatial computing](https://developer.apple.com/videos/play/wwdc2023/111215/) 这俩是UIKit和SwiftKit的内容 ## 探索SwiftUI和 RealityKit 要更深入地了解SwiftUI和RealityKit,请探索专注于SwiftUI场景类型的专门系列会议,以帮助您跨窗口、卷和空间构建出色的体验。了解Model 3D API,了解如何在应用程序中添加深度和维度,并了解如何使用RealityView渲染3D内容。我们将帮助您准备好进入ImmersiveSpace—一种新的SwiftUI场景类型,可以让您为visionOS创造出色的沉浸式体验。学习管理场景类型的最佳实践,增加沉浸感,并建立一个“走出这个世界”的体验。 ![](/assets/images/20230609LearnAboutVisionOS/ExploreSwiftUIandRealityKit1.webp) * [Elevate your windowed app for spatial computing](https://developer.apple.com/videos/play/wwdc2023/10110/) * [Take SwiftUI to the next dimension](https://developer.apple.com/videos/play/wwdc2023/10113/) * [Go beyond the window with SwiftUI](https://developer.apple.com/videos/play/wwdc2023/10111/) 在我们的第二个系列中,了解如何使用RealityKit为您的应用程序带来引人入胜的沉浸式内容。开始使用RealityKit实体,组件和系统,并了解如何将3D模型和效果添加到项目中。我们将向您展示如何将您的内容嵌入到实体层次结构中,使用锚将虚拟内容和现实世界混合在一起,将粒子效果带入您的应用程序,添加视频内容,并通过门户创建更身临其境的体验。 ![](/assets/images/20230609LearnAboutVisionOS/ExploreSwiftUIandRealityKit2.webp) * [Enhance your spatial computing app with RealityKit](https://developer.apple.com/videos/play/wwdc2023/10081/) * [Build spatial experiences with RealityKit](https://developer.apple.com/videos/play/wwdc2023/10080/) ## 重新学习ARKit 最后,我们将帮助您了解visionOS上的ARKit。该平台使用ARKit算法来处理持久性、世界映射、分割、抠图和环境照明等功能。这些算法一直在运行,允许应用程序和游戏在共享空间中自动受益于ARKit。一旦你的应用打开了一个专用的Full Space,它就可以利用ARKit api并将虚拟内容与现实世界融合在一起。 我们将分享这个框架是如何完全重新构想的,让你在保护隐私的同时建立互动体验。了解如何制作与某人房间交互的3D内容-无论您是想将虚拟球从地板上弹起还是将虚拟油漆扔到墙上。探索ARKit API的最新更新,并跟随我们演示如何在应用程序中利用手跟踪和场景几何。 ![](/assets/images/20230609LearnAboutVisionOS/RediscoverARKit.webp) * [Meet ARKit for spatial computing](https://developer.apple.com/videos/play/wwdc2023/10082/) * [Evolve your ARKit app for spatial experiences](https://developer.apple.com/videos/play/wwdc2023/10091/) ## 设计同学需要关注的visionOS 了解如何为空间计算设计出色的应用程序、游戏和体验。发现全新的输入和组件。潜入深度和规模。增加沉浸的时刻。创建空间音频音景。寻找合作和联系的机会。并帮助人们在探索全新世界的同时保持对周围环境的脚踏实地。无论这是您第一次设计空间体验,还是您多年来一直在构建完全沉浸式的应用程序,了解如何使用visionOS创建神奇的英雄时刻,迷人的音景,以人为中心的UI等等。 ![](/assets/images/20230609LearnAboutVisionOS/DesignforvisionOS.webp) * [Principles of spatial design](https://developer.apple.com/videos/play/wwdc2023/10072/) * [Design for spatial user interfaces](https://developer.apple.com/videos/play/wwdc2023/10076/) * [Design for spatial input](https://developer.apple.com/videos/play/wwdc2023/10073/) * [Explore immersive sound design](https://developer.apple.com/videos/play/wwdc2023/10271/) * [Design considerations for vision and motion](https://developer.apple.com/videos/play/wwdc2023/10078/) ## visionOS开发工具包 Apple提供了一套全面的工具来帮助您为visionOS构建出色的应用程序、游戏和体验。了解如何在Xcode中开始您的第一个visionOS项目,探索工具和测试的更新,了解如何在您的3D开发工作流程中利用Reality Composer Pro,并了解如何使用Unity的创作工具为空间计算创造出色的体验。 ## Xcode开发需要关注的 用Xcode开始为visionOS开发。我们将向您展示如何将visionOS目的地添加到您现有的项目或构建一个全新的应用程序,在Xcode预览中的原型,并从Reality Composer Pro导入内容。我们还将分享如何使用visionOS模拟器来评估您对各种模拟场景和照明条件的体验。了解如何创建测试和可视化,以探索空间内容的碰撞、遮挡和场景理解,并优化该内容的性能和效率。 ![](/assets/images/20230609LearnAboutVisionOS/ExploredevelopertoolsforvisionOS.webp) * [What's new in Xcode 15](https://developer.apple.com/videos/play/wwdc2023/10165/) * [Develop your first immersive app](https://developer.apple.com/videos/play/wwdc2023/10203/) * [Meet RealityKit Trace](https://developer.apple.com/videos/play/wwdc2023/10099/) * [Explore rendering for spatial computing](https://developer.apple.com/videos/play/wwdc2023/10095/) * [Optimize app power and performance for spatial computing ](https://developer.apple.com/videos/play/wwdc2023/10100/) * [Meet Core Location for spatial computing](https://developer.apple.com/videos/play/wwdc2023/10146/) ## Reality Composer Pro 发现预览和准备3D内容的新方法为您的visionOS应用程序。本月晚些时候,现实作曲家专业利用美元的力量来帮助您撰写,编辑和预览资产,如3D模型,材料和声音。我们将向您展示如何利用此工具为您的应用程序创建身临其境的内容,向对象添加材料,并将您的现实作曲家Pro内容带入Xcode中。我们还将带您了解Apple平台上通用场景描述(USD)的最新更新。 ![](/assets/images/20230609LearnAboutVisionOS/MeetRealityComposerPro.webp) * [Meet Reality Composer Pro](https://developer.apple.com/videos/play/wwdc2023/10083/) * [Explore materials in Reality Composer Pro](https://developer.apple.com/videos/play/wwdc2023/10202/) * [Work with Reality Composer Pro content in Xcode](https://developer.apple.com/videos/play/wwdc2023/10273/) * [Explore the USD ecosystem](https://developer.apple.com/videos/play/wwdc2023/10086/) ## 学习Unity ![](/assets/images/20230609LearnAboutVisionOS/GetstartedwithUnity.webp) ## TestFlight and App Store Connect ![](/assets/images/20230609LearnAboutVisionOS/LearnaboutTestFlightandAppStoreConnect.webp) [Explore App Store Connect for spatial computing](https://developer.apple.com/videos/play/wwdc2023/10012/) ## 游戏和音视频多媒体 了解如何使用visionOS在游戏和媒体体验中创造真正身临其境的时刻。游戏和媒体可以利用全方位的沉浸感来讲述令人难以置信的故事,并以一种新的方式与人们建立联系。我们将向您展示可用的途径,让您开始与visionOS的游戏和叙事开发。学习使用RealityKit有效渲染3D内容的方法,探索视觉和运动的设计考虑因素,并了解如何创建完全身临其境的体验,将人们带入一个新的世界 ![](/assets/images/20230609LearnAboutVisionOS/Buildgamesandmediaexperiences1.webp) * [Build great games for spatial computing](https://developer.apple.com/videos/play/wwdc2023/10096/) * [Explore rendering for spatial computing](https://developer.apple.com/videos/play/wwdc2023/10095/) * [Design considerations for vision and motion](https://developer.apple.com/videos/play/wwdc2023/10078/) * [Create immersive Unity apps](https://developer.apple.com/videos/play/wwdc2023/10088/) * [Bring your Unity VR app to a fully immersive space ](https://developer.apple.com/videos/play/wwdc2023/10093/) * [Discover Metal for immersive apps](https://developer.apple.com/videos/play/wwdc2023/10089/) 声音也可以极大地增强你的visionOS应用程序和游戏的体验-无论你添加一个效果的按钮按下或创建一个完全身临其境的音景。了解Apple设计师如何选择声音和构建声景,在窗口、体量和空间中创造富有质感的沉浸式体验。我们将分享如何在您的应用程序中丰富声音的基本交互,当您在空间上放置音频线索时,变化重复的声音,并在您的应用程序中构建声音愉悦的时刻。 ![](/assets/images/20230609LearnAboutVisionOS/Buildgamesandmediaexperiences2.webp) * [Explore immersive sound design](https://developer.apple.com/videos/play/wwdc2023/10271/) 如果您的应用程序或游戏具有媒体内容,我们有一系列的会议,旨在帮助您更新您的视频管道,并为visionOS建立一个伟大的播放体验。了解如何扩展交付管道以支持3D内容,并获得应用程序中空间媒体流的提示和技术。我们还将向您展示如何使用为visionOS提供视频播放的框架和api创建引人入胜和身临其境的播放体验。 ![](/assets/images/20230609LearnAboutVisionOS/Buildgamesandmediaexperiences3.webp) * [Deliver video content for spatial experiences](https://developer.apple.com/videos/play/wwdc2023/10071/) * [Create a great spatial playback experience](https://developer.apple.com/videos/play/wwdc2023/10070/) ## 协作、共享和生产力 共享和协作是visionOS的核心部分,通过在应用程序和游戏中提供体验,让人们感觉仿佛置身于同一个空间。默认情况下,人们可以通过FaceTime通话与他人共享任何应用程序窗口,就像他们在Mac上一样。但是当你采用GroupActivities框架时,你可以创建下一代协作体验。 通过了解您可以在应用程序中创建的共享活动类型,开始在Apple Vision Pro上设计和构建SharePlay。了解如何在体验中的参与者之间建立共享上下文,并了解如何通过支持空间人物角色在应用程序中支持更有意义的交互。 ![](/assets/images/20230609LearnAboutVisionOS/Buildforcollaborationsharingandproductivity.webp) * [Design spatial SharePlay experiences](https://developer.apple.com/videos/play/wwdc2023/10075/) * [Build spatial SharePlay experiences](https://developer.apple.com/videos/play/wwdc2023/10087/) ## web相关和创建3D模型相关 ![](/assets/images/20230609LearnAboutVisionOS/Buildwebexperiences1.webp) ![](/assets/images/20230609LearnAboutVisionOS/Buildwebexperiences2.webp) ## 如果让我们的运行在iPhone和iPad的app运行在visionOS上 了解如何在visionOS中运行现有的ipad和iOS应用程序。探索iPad和iOS应用程序如何在这个平台上运行,了解框架依赖关系,并了解专为iPad设计的应用程序交互。当您准备好将现有应用程序提升到一个新的水平时,我们将向您展示如何优化共享空间的iPad和iPhone应用程序体验,并帮助您改善视觉效果。 ![](/assets/images/20230609LearnAboutVisionOS/RunyouriPadandiPhoneappsinvisionOS.webp) * [Run your iPad and iPhone apps in the Shared Space](https://developer.apple.com/videos/play/wwdc2023/10090/) * [Enhance your iPad and iPhone apps for the Shared Space](https://developer.apple.com/videos/play/wwdc2023/10094/) 上边这里作为一个iOS开发必看 # 总结 整理了一些学习visionOS的全部资料和视频,希望大家能在新的visionOS上开辟新的世界,就像我们在iOS起步的时候那样. [Learn about visionOS官方地址](https://developer.apple.com/visionos/learn/) URL: https://sunyazhou.com/2023/06/batchconvertheictopng/index.html.md Published At: 2023-06-07 02:01:00 +0000 # Shell脚本批量将heic转png和webp # 前言 ![](/assets/images/20230607BatchConvertHEICtoPNG/chatgpt.webp) 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! ## 如何批量将HECI图片转成png 最近公司搞活动需要我们组织小组业余爱好并记录美好生活,避免不了拍一些照片,我手机忘记了设置所以拍摄出来的照片都是HEIC(H.265)编码的格式,然而公司需要的是把这些图片转成png或者jpeg上传,所以就因为了今天这批技术文章 ### 编写脚本HECI转PNG 先看下之前的效果 ![](/assets/images/20230607BatchConvertHEICtoPNG/heic.webp) 转换完成后的效果 ![](/assets/images/20230607BatchConvertHEICtoPNG/pngs.webp) 下面来编写脚本实现 作为一个很多年都不怎么学习使用脚本的我面对这个一个突如其来的需求比较麻木,喵神曾经说过:"对于一些不擅长的技术很多人刚接触的时候会有一种恐惧感,这种恐惧源于未知!"。那么怎么解决自己不了解的技术难题的呢?--`ChatGPT` 先打开[ChatGPT](https://chat.openai.com/), 输入如下: ``` txt 写个shell脚本 指定 Mac 中Downloads目录下的所有HEIC格式一次批量转成png ``` 当使用Shell脚本将`Mac`中`Downloads`目录下的所有`HEIC`格式文件批量转换为`PNG`格式时,可以使用`sips`命令来完成。以下是一个示例脚本: ``` sh #!/bin/bash # 定义Download目录路径 download_dir="$HOME/Downloads" # 转换HEIC文件为PNG格式 convert_heic_to_png() { local file_path="$1" local file_name="$(basename "$file_path")" local file_extension="${file_name##*.}" local file_name_no_extension="${file_name%.*}" if [[ "$file_extension" == "HEIC" || "$file_extension" == "heic" ]]; then png_file_path="$download_dir/$file_name_no_extension.png" sips -s format png "$file_path" --out "$png_file_path" echo "转换完成: $png_file_path" fi } # 遍历Download目录下的文件 find "$download_dir" -type f | while read -r file; do convert_heic_to_png "$file" done ``` 请将上述脚本保存为一个文件(例如`convert_heic_to_png.sh`),然后打开终端,执行以下命令来给脚本文件添加执行权限: ``` sh chmod +x convert_heic_to_png.sh ``` 之后,可以通过以下命令运行脚本: ``` sh ./convert_heic_to_png.sh ``` 脚本将会遍历Download目录下的所有文件,并将HEIC格式的文件转换为PNG格式,转换后的PNG文件将存储在相同目录下。脚本会输出每个转换完成的PNG文件路径。注意,脚本仅在Mac系统上有效,且需要安装Xcode命令行工具以获取`sips`命令。 ### 编写脚本HECI转webp 当使用Shell脚本将Mac中Downloads目录下的所有HEIC格式文件批量转换为WebP格式时,可以使用`cwebp`命令来完成。以下是一个示例脚本: ``` sh #!/bin/bash # 定义Downloads目录路径 downloads_dir="$HOME/Downloads" # 转换HEIC文件为WebP格式 convert_heic_to_webp() { local file_path="$1" local file_name="$(basename "$file_path")" local file_extension="${file_name##*.}" local file_name_no_extension="${file_name%.*}" if [[ "$file_extension" == "HEIC" || "$file_extension" == "heic" ]]; then webp_file_path="$downloads_dir/$file_name_no_extension.webp" cwebp -q 80 "$file_path" -o "$webp_file_path" echo "转换完成: $webp_file_path" fi } # 遍历Downloads目录下的文件 find "$downloads_dir" -type f | while read -r file; do convert_heic_to_webp "$file" done ``` 请将上述脚本保存为一个文件(例如`convert_heic_to_webp.sh`),然后打开终端,执行以下命令来给脚本文件添加执行权限: ``` sh chmod +x convert_heic_to_webp.sh ``` 之后,可以通过以下命令运行脚本: ``` sh ./convert_heic_to_webp.sh ``` 脚本将会遍历Downloads目录下的所有文件,并将HEIC格式的文件转换为WebP格式,转换后的WebP文件将存储在相同目录下。脚本会输出每个转换完成的WebP文件路径。注意,脚本仅在Mac系统上有效,且需要安装`webp`库以获取`cwebp`命令。你可以通过Homebrew来安装`webp`库: ``` sh brew install webp ``` 至于如何转换其它类型 我应该不用再说了吧! # 总结 简直被ChatGPT的神奇力量炸裂,我不咋会写脚本,这太好用了.分享给大家. [点击这里下载本文的脚本文件](https://github.com/sunyazhou13/BatchConvertImagesShells) URL: https://sunyazhou.com/2023/06/learnswiftuichapter1/index.html.md Published At: 2023-06-04 05:32:00 +0000 # SwiftUI第一章学习总结 ![](/assets/images/20230604LearnSwiftUIChapter1/swiftuilogo.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! # SwiftUI课程 最近在听B站以为来自祖国宝岛台湾省的一个女博主(声音很嗲dia)讲解SwiftUI课程,讲的不错把学习的内容记录下来: ## 主要内容包含 ![](/assets/images/20230604LearnSwiftUIChapter1/finalproject.gif) 1-5 第一章补充:some View、排版规则、设计细节 - SwiftUI 新手入门 * SwiftUI基本架构 * View 和 调整器(就是一些视图的函数方法) * @State 属性封装器 * 变形和转场动画 SwiftUI默认有转场和变形的显示动画 * View的身份 * SwiftUI 如何排版和 View的带下类型 尤其是 `some View`是咋回事 讲的比较不错 ## 代码记录 ``` swift // // ContentView.swift // FoodPicker // // Created by sunyazhou on 2023/4/16. // import SwiftUI struct ContentView: View { let food = ["汉堡", "沙拉", "披萨", "意大利面", "鸡腿便当", "刀削面", "火锅", "牛肉面", "关东煮"] @State private var selectedFood: String? var body: some View { VStack(spacing: 30) { Image("dinner") .resizable() .aspectRatio(contentMode: .fit) Text("今天吃什么?") .bold() if selectedFood != .none { Text(selectedFood ?? "") .font(.largeTitle) .bold() .foregroundColor(.green) .id(selectedFood) .transition(.asymmetric( insertion:.opacity .animation(.easeInOut(duration: 0.5).delay(0.2)), removal:.opacity .animation(.easeInOut(duration: 0.4)))) } Button { // withAnimation { // } selectedFood = food.shuffled().filter {$0 != selectedFood }.first } label: { Text(selectedFood == .none ? "告诉我!": "换一个").frame(width: 200, alignment: .center) .animation(.none, value: selectedFood) .transformEffect(.identity) }.padding(.bottom, -15) Button { // withAnimation { // selectedFood = .none // } selectedFood = .none } label: { Text("重置").frame(width: 200) }.buttonStyle(.bordered) } .padding() .frame(maxHeight: .infinity) .background(Color(.secondarySystemBackground)) .font(.title) .buttonStyle(.borderedProminent) .buttonBorderShape(.capsule) .controlSize(.large) .animation(.easeInOut(duration: 0.6), value: selectedFood) } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } } ``` 这里有几个比较重要的内容 ![](/assets/images/20230604LearnSwiftUIChapter1/ViewSizeType1.webp) ![](/assets/images/20230604LearnSwiftUIChapter1/ViewSizeType2.webp) * 1.动态字体大小 Dynamic type [Human Interface Guildlines](https://developer.apple.com/design/human-interface-guidelines/) * 2.屏幕缩放系数 [屏幕大小和 Scale factor](https://iosref.com/res ) 这里的一些动画很好,这里不展开细节讨论了,记录一些代码demo我放在下方链接 # 总结 工作时间很紧张,周末有时间记录一些重要容易被遗忘的内容,很水,希望大家不要介意. [本文demo](https://github.com/sunyazhou13/FoodPicker) [1-5 第一章补充:some View、排版规则、设计细节 - SwiftUI 新手入门 ](https://www.bilibili.com/video/BV1CG411776w/?p=6&spm_id_from=pageDriver) URL: https://sunyazhou.com/2023/04/cocoapodsuserguide/index.html.md Published At: 2023-04-26 03:22:00 +0000 # CocoaPods完全使用指南 ![](/assets/images/20201010PodSpec/cocoapods.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! 在我的技术认知里, Cocoapods已经成为每一位iOS开发者必备的技能之一,然后这么多年过去了,在我认为它已经过时了的时候居然还有人对这个东西玩的不透彻,今天我就把我这么多年使用的经验高阶部分全部都拿出来跟不熟悉这个工具的同行分享一下. 对于大多数软件开发团队来说,依赖管理工具必不可少,它能针对开源和私有依赖进行安装与管理,从而提升开发效率,降低维护成本。针对不同的语言与平台,其依赖管理工具也各有不同,例如`npm`管理`Javascript`、`Gradle `、`Maven` 管理`Jar` 包、`pip `管理 `Python `包,`Bundler`、`RubyGems`等等。本文聚焦于` iOS `方面,对 `CocoaPods `的使用和部分原理进行阐述。 ## 简单易用的 CocoaPods 对于 iOSer 来说,CocoaPods 并不陌生,几乎所有的 iOS 工程都会有它的身影。CocoaPods 采用 Ruby 构建,它是 Swift 和 Objective-C Cocoa 项目的依赖管理工具。在 MacOS 上,推荐使用默认的 Ruby 进行安装 (以下操作均在 CocoaPods 1.10.1、Ruby 2.7.2 进行): ``` objc sudo gem install cocoapods ``` 如果安装成功,便可以使用 pod 的相关命令了。针对一个简单的项目来说,只需三步便可引入其他的依赖: * 1.创建 Podfile 文件( CocoaPods 提供了 pod init 命令创建) * 2.对 Podfile 文件进行编写,添加依赖的库,版本等信息。 * 3.在命令行执行 `pod install` 命令 顺利的话,这时在项目目录下会出现以下文件: * .xcworkspace:CocoaPods 将项目分为了主工程与依赖工程(Pods)。与 .xcodeproj 相比 .xcworkspace 对于管理多个项目的能力更强,你也可以将复杂的大型应用转换为以 .xcworkspace 构建的多个兄弟项目,从而更轻松的维护和共享功能。 * Podfile.lock:记录并跟踪依赖库版本,将依赖库锁定于某个版本。 * Pods 文件夹:存放依赖库代码。 * Pods/Manifest.lock:每次 pod install 时创建的 `Podfile.lock`的副本,用于比较这两个文件。一般来说, Podfile.lock 会纳入版本控制管理,而 Pods 文件夹则不会纳入版本控制变更;这意味着 Podfile.lock 表示项目应该依赖的库版本信息,而 Manifest.lock 则代表本地 Pods 的依赖库版本信息。在 pod install 后会将脚本插入到 Build Phases,名为 `[CP] Check Pods Manifest.lock`,从而保证开发者在运行 app 之前能够更新 Pods,以确保代码是最新的。 ## pod install vs. pod update * `pod install`:在每一次编辑 Podfile 以添加、更新或删除 pod 时使用。它会下载并安装新的 Pod,并将其版本信息写入 Podfile.lock 中。 * `pod outdated`:列出所有比 Podfile.lock 中当前记录的版本 newer 版本的 pod。 * `pod update [PODNAME]`:CocoaPods 会查找 newer 版本的 PODNAME,同时将 pod 更新到可能的最新版本(须符合 Podfile 限制)。若没有 PODNAME,则会将每一个 pod 更新到可能的最新版本。 一般来说,每次编辑 Podfile 时使用 `pod install`,仅在需要更新某个 pod 版本(所有版本)时才使用 pod update。同时,需提交 Podfile.lock 文件而不是 Pods 文件夹来达到同步所有 pod 版本的目的。 > newer 代表更加新的,若采用中文理解起来比较别扭。 ## Podfile 语法规范 Podfile 描述了一个或多个 Xcode 项目的 target 依赖关系,它是一种 DSL,了解它对我们使用好 CocoaPods 是一个必不可少的步骤。下面列出其相关的语法规范: #### Root Options `install!`:指定 CocoaPods 安装 Podfile 时使用的安装方法和选项。如: ``` sh install! 'cocoapods', :deterministic_uuids => false, :integrate_targets => false ``` * `:clean`:根据 podspec 和项目支持平台的指定,清理所有不被 pod 使用的文件,默认为 true。 * `:deduplicate_targets`:是否对 pod target 进行重复数据删除,默认为 true。 * `:deterministic_uuids`:创建 pod project 是否产生确定性 UUID,默认为 true。 * `:integrate_targets`:是否继承到用户项目中,为 false 会将 Pod 下载并安装到到 project_path/Pods 目录下,默认为 true。 * `:lock_pos_sources`:是否锁定 pod 的源文件,当 Xcode 尝试修改时会提示解锁文件,默认为 true。 * `:warn_for_multiple_pod_sources`:当多个 source 包含同名同版本 pod 时是否发出警告,默认为 true。 * `:warn_for_unused_master_specs_repo`:如果没有明确指出 master specs repo 的 git 是否发出警告,默认为 true。 * `:share_schemes_for_development_pods`:是否为开发中的 pod 分享 schemes,默认为 false。 * `:disable_input_output_paths`:是否禁用 CocoaPods 脚本阶段的输入输出路径(Copy Frameworks 和 Copy Resources),默认为 false。 * `:preserve_pod_file_structure`:是否保留所有 pod 的文件结构,默认为 false。 * `:generate_multiple_pod_projects`:是否为每一个 pod target 生成 一个 project,生成与 Pods/Pods 文件夹中,默认为 false。 * `:incremental_installation`:仅对自上次安装的 target 与其关联的 project 的变更部分进行重新生成,默认为 false。 * `:skip_pods_project_generation`:是否跳过生成 Pods.xcodeproj 并仅进行依赖项解析与下载,默认为 false。 `ensure_bundler!`:当 bundler 版本不匹配时发出警告。 ``` ruby ensure_bundler! '~> 2.0.0' ``` #### Dependencies `pod`:指定项目的依赖项 * 依赖版本控制:`=`、`>`、`>=`、`<`、`<=` 为字面意思;`~> 0.1.2` 表示 `0.1.2 <= currVersion < 0.2` 之间的符合要求的最新版本版本。 * Build configurations:默认依赖安装在所有的构建配置中,但也可仅在指定构建配置中启用。 * Modular Headers:用于将 pod 转换为 module 以支持模块,这时在 Swift 中可以不用借助 `bridging-header` 桥接就可以直接导入,简化了 Swift 引用 Objective-C 的方式;也可以采用 `use_modular_headers!` 进行全局的变更。 * Source:指定具有依赖项的源,同时会忽略全局源。 * Subspecs:默认会安装所有的 subspecs,但可制定安装某些 subspecs。 * Test Specs:默认不会安装 test specs,但可选择性安装 test specs。 * Local path:将开发的 pod 与其客户端一起使用,可采用 path。 * 指定某个特殊或者更为先进的 pod 版本 ``` ruby # 依赖版本控制 pod 'Objection', '~> 0.9'  # Build configurations pod 'PonyDebugger', :configurations => ['Debug', 'Beta']  # Modular Headers pod 'SSZipArchive', :modular_headers => true  # Source pod 'PonyDebugger', :source => 'https://github.com/CocoaPods/Specs.git' # Subspecs pod 'QueryKit', :subspecs => ['Attribute', 'QuerySet']  # Test Specs pod 'AFNetworking', :testspecs => ['UnitTests', 'SomeOtherTests'] # Local path pod 'AFNetworking', :path => '~/Documents/AFNetworking' # 指定某个特殊或者更为先进的 Pod 版本 pod 'AFNetworking', :git => 'https://github.com/gowalla/AFNetworking.git', :branch => 'dev' pod 'AFNetworking', :git => 'https://github.com/gowalla/AFNetworking.git', :tag => '0.7.0' pod 'AFNetworking', :git => 'https://github.com/gowalla/AFNetworking.git', :commit => '082f8319af' # 指定某个 podspec pod 'JSONKit', :podspec => 'https://example.com/JSONKit.podspec' ``` `inherit`:设置当前 target 的继承模式。 `:complete` 继承父级 target 的所有行为,`:none` 不继承父级 target 的任何行为,`:search_paths` 仅继承父级的搜索路径。 ``` ruby target 'App' do target 'AppTests' do inherit! :search_paths end end ``` `target`:与 Xcode 中的 target 相对应,block 中是 target 的依赖项。 默认情况下,target 包含在父级 target 定义的依赖项,也即` inherit! `为 `:complete`。关于 `:complete`和 `:search_paths`,`:complete` 会拷贝父级 target 的 pod 副本,而 `:search_paths` 则只进行 `FRAMEWORK_SEARCH_PATHS` 和 `HEADER_SEARCH_PATHS` 的相关拷贝,具体可通过比对 Pods/Target Support Files 的相关文件得以验证,一般在 `UnitTests` 中使用,以减少多余的 `install_framework` 过程。 ``` ruby target 'ShowsApp' do   pod 'ShowsKit'   # 拥有 ShowsKit 和 ShowTVAuth 的拷贝   target 'ShowsTV' do     pod 'ShowTVAuth'   end   # 拥有 Specta 和 Expecta 的拷贝   # 并且能够通过 ShowsApp 进行访问 ShowsKit, 相当于 ShowsApp 是 ShowsTests 的宿主APP   target 'ShowsTests' do     inherit! :search_paths     pod 'Specta'     pod 'Expecta'   end end ``` `abstract_target`:定义 `abstract_target`,方便 target 进行依赖继承,在 CocoaPods 1.0 版本之前为 `link_with`。 ``` ruby abstract_target 'Networking' do pod 'AlamoFire' target 'Networking App 1' target 'Networking App 2' end ``` `abstract`:表示当前 target 是抽象的,不会链接到 Xcode 的 target 中。 `script_phase`:添加脚本阶段 在执行完 `pod install` 之后 CocoaPods 会将脚本添加到对应的 `target build phases`。 ``` ruby target 'App' do script_phase { :name => 'scriptName' # 脚本名称,         :script => 'echo "nihao"' # 脚本内容,         :execution_position => :before_compile / :after_compile         :shell_path => '/usr/bin/ruby' # 脚本路径         :input_files => ['/input/filePath'], # 输入文件         :output_files => ['/outpput/filePath'] # 输出文件 } end ``` #### Target configuration `platform`:指定其构建平台。 默认值为 iOS 4.3、OSX 10.6、tvOS 9.0 和 watchOS 2.0。CocoaPods 1.0 之前的版本为 xcodeproj ``` ruby platform :ios, '4.0' ``` `project`:指定包含 target 的 Xcode project。这一般在 workspace 存在多个 xcode project 中使用: ``` ruby # 在 FastGPS Project 中可以找到一个名为 MyGPSApp 的 target target 'MyGPSApp' do project 'FastGPS' ... end ``` `inhibit_all_warnings!`:禁止所有警告。如果针对单个 Pod,则可以采用: ``` ruby pod 'SSZipArchive', :inhibit_warnings => true pod 'SSZipArchive', :inhibit_warnings => true ``` `user_modular_headers!`:将所有 Pod 模块化。如果针对单个 Pod,则可以采用: ``` ruby pod 'SSZipArchive', :modular_headers => true pod 'SSZipArchive', :modular_headers => false ``` `user_frameworks!`:采用 framework 而不是 .a 文件的静态库。 可以通过 `:linkage` 指定使用静态库还是动态库: ``` ruby use_frameworks!:linkage => :dynamic / :static ``` `supports_swift_versions`:指定 target definition 支持的 swift 版本要求 ``` ruby supports_swift_versions '>= 3.0', '< 4.0' ``` #### Workspace `workspace`:指定包含所有项目的 Xcode workspace。 #### Sources `sources`:Podfile 从指定的源列表中进行检索。sources 默认存储在 ~/.cocoapods/repos 中,是全局的而非按 target definition 存储。当有多个相同的 Pod 时,优先采用检索到的 Pod 的第一个源,因此当指定另一个来源时,则需显示指定 CocoaPods 的源。 ``` ruby source 'https://github.com/artsy/Specs.git' source 'https://github.com/CocoaPods/Specs.git' ``` #### Hooks `plugin`:指定在安装期间使用的插件。 ``` ruby plugin 'cocoapods-keys', :keyring => 'Eidolon' plugin 'slather' ``` `pre_install`:在下载后和在安装 Pod 前进行更改。 ``` ruby pre_install do |installer| # Do something fancy! end ``` `pre_integrate`:在 project 写入磁盘前进行更改。 ``` ruby pre_integrate do |installer| # perform some changes on dependencies end ``` `post_install`:对生成 project 写入磁盘前进行最后的修改。 ``` ruby post_install do |installer| installer.generated_projects.each do |project| project.targets.each do |target| target.build_configurations.each do |config| config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '11.0' end end end end ``` `post_integrate`:在 project 写入磁盘后进行最后更改。 ``` ruby post_integrate do |installer| # some change after project write to disk end ``` ## podspec 语法规范 podspec = pod Specification,意为 pod 规范,它是一个 Ruby 文件。包含了 Pod 的库版本详细信息,例如应从何处获取源、使用哪些文件、要应用构建设置等信息;也可以看作该文件是整个仓库的索引文件,了解它对我们知道 Pod 库是如何组织、运作的提供了很大帮助。podspec 的 DSL 提供了极大的灵活性,文件可通过 `pod spec create` 创建。 #### Root | 名称 | 用途 | 必需 | | :------| :------: | :------ | | `name` | pod 名称 | required | | `version` | pod 版本,遵循语义化版本控制 | required | | `swift_version` | 支持的 Swift 版本 | | | `cocoapods_version` | 支持的 CocoaPods 版本 | | | `authors` | pod 维护者的姓名和电子邮件,用“, ”进行分割 | required | | `license` | pod 的许可证 | required | | `homepage` | pod 主页的 URL | required | | `source` | 源地址,即源文件的存放地址,支持多种形式源 | required | | `summary` | pod 的简短描述 | required | | `prepare_command` | 下载 pod 后执行的 bash 脚本 | | | `static_framework` | 是否采用静态 framework 分发 | | | `deprecated` | 该库是否已被弃用 | | | `deprecated_in_favor_of` | 该库名称已被弃用,取而代之 | | ``` ruby Pod::Spec.new do |s|   s.name             = 'CustomPod'   s.version          = '0.1.0'   s.summary          = 'A short description of CustomPod.'   s.swift_versions   = ['3.0', '4.0', '4.2']   s.cocoapods_version  =  '>= 0.36'   s.author           = { 'nihao' => 'XXXX@qq.com' }   s.license          = { :type => 'MIT', :file => 'LICENSE' }   s.homepage         = 'https://github.com/XXX/CustomPod' # Supported Key # :git=> :tag, :branch, :commit,:submodules # :svn=> :folder, :tag,:revision # :hg=>:revision # :http=> :flatten, :type, :sha256, :sha1,:headers   s.source           = { :git => 'https://github.com/XX/CustomPod.git', :tag => s.version.to_s }   s.prepare_command  =  'ruby build_files.rb'   s.static_framework = true   s.deprecated       = true   s.deprecated_in_favor_of  =  'NewMoreAwesomePod' end ``` #### Platform `platform`:pod 支持的平台,留空意味着 pod 支持所有平台。当支持多平台时应该用 `deployment_target` 代替。 ``` ruby spec.platform = :osx, '10.8' ``` `deployment_target`:允许指定支持此 pod 的多个平台,为每个平台指定不同的部署目标。 ``` ruby spec.ios.deployment_target = '6.0' spec.osx.deployment_target = '10.8' ``` #### Build settings `dependency`:基于其他 pods 或子规范的依赖 ``` ruby spec.dependency 'AFNetworking', '~> 1.0', :configurations => ['Debug'] ``` `info_plist`:加入到生成的 Info.plist 的键值对,会对 CocoaPods 生成的默认值进行覆盖。仅对使用 framework 的框架有影响,对静态库无效。对于应用规范,这些值将合并到应用程序主机的 `Info.plist`;对于测试规范,这些值将合并到测试包的 Info.plist。 ``` ruby spec.info_plist = { 'CFBundleIdentifier' => 'com.myorg.MyLib', 'MY_VAR' => 'SOME_VALUE' } ``` `requires_arc`:允许指定哪些 source_files 采用 ARC,不使用 ARC 的文件将具有 `-fno-objc-arc` 编译器标志 ``` ruby spec.requires_arc = false spec.requires_arc = 'Classes/Arc' spec.requires_arc = ['Classes/*ARC.m', 'Classes/ARC.mm'] ``` `frameworks`:使用者 target 需要链接的系统框架列表 ``` ruby spec.ios.framework = 'CFNetwork' spec.frameworks = 'QuartzCore', 'CoreData' ``` `weak_frameworks`:使用者 target 需要弱链接的框架列表 ``` ruby spec.weak_framework = 'Twitter' spec.weak_frameworks = 'Twitter', 'SafariServices' ``` `libraries`:使用者 target 需要链接的系统库列表 ``` ruby spec.ios.library = 'xml2' spec.libraries = 'xml2', 'z' ``` `compiler_flags`:应传递给编译器的 flags ``` ruby spec.compiler_flags = '-DOS_OBJECT_USE_OBJC=0', '-Wno-format' ``` `pod_target_xcconfig`:将指定 flag 添加到最终 pod 的 xcconfig 文件 ``` ruby spec.pod_target_xcconfig = { 'OTHER_LDFLAGS' => '-lObjC' } ``` `user_target_xcconfig`:🙅 将指定 flag 添加到最终聚合的 target 的 xcconfig,不推荐使用此属性,因为会污染用户的构建设置,可能会导致冲突。 ``` ruby spec.user_target_xcconfig = { 'MY_SUBSPEC' => 'YES' } ``` `prefix_header_contents`:🙅 在 Pod 中注入的预编译内容,不推荐使用此属性,因为其会污染用户或者其他库的预编译头。 ``` ruby spec.prefix_header_contents = '#import ', '#import ' ``` `prefix_header_file`:预编译头文件,false 表示不生成默认的 CocoaPods 的与编译头文件。🙅 不推荐使用路径形式,因为其会污染用户或者其他库的预编译头。 ``` ruby spec.prefix_header_file = 'iphone/include/prefix.pch' spec.prefix_header_file = false ``` `module_name`:生成的 framrwork / clang module 使用的名称,而非默认名称。 ``` ruby spec.module_name = 'Three20' ``` `header_dir`:存储头文件的目录,这样它们就不会被破坏。 ``` ruby spec.header_dir = 'Three20Core' ``` `header_mappings_dir`:用于保留头文件文件夹的目录。如未提供,头文件将被碾平。 ``` ruby spec.header_mappings_dir = 'src/include' ``` `script_phases`:该属性允许定义脚本在 pod 编译时执行,其作为 `xcode build` 命令的一部分执行,还可以利用编译期间所设置的环境变量。 ``` ruby spec.script_phases = [     { :name => 'Hello World', :script => 'echo "Hello World"' },     { :name => 'Hello Ruby World', :script => 'puts "Hello World"', :shell_path => '/usr/bin/ruby' },   ] ``` #### File patterns 文件模式指定了库的所有文件管理方式,如源代码、头文件、framework、libaries、以及各种资源。其文件模式通配符形式可参考[链接](https://guides.cocoapods.org/syntax/podspec.html#group_file_patterns) `source_files`:指定源文件 ``` ruby spec.source_files = 'Classes/**/*.{h,m}', 'More_Classes/**/*.{h,m}' ``` `public_header_files`:指定公共头文件,这些头文件与源文件匹配,并生成文档向用户提供。如果未指定,则将 source_files 中的所有头文件都包含生成。 ``` ruby spec.public_header_files = 'Headers/Public/*.h' ``` `project_header_files`:指定项目头文件,与公共头文件相对应,以排除不应向用户项目公开且不应用于生成文档的标头,且不会出现在构建目录中。 ``` ruby spec.project_header_files = 'Headers/Project/*.h' ``` `private_header_files`:私有头文件,与公共头文件对应,以排除不应向用户项目公开且不应用于生成文档的标头,这些头文件会出现在产物中的 PrivateHeader 文件夹中。 ``` ruby spec.private_header_files = 'Headers/Private/*.h' ``` `vendered_frameworks`:pod 附加的 framework 路径 ``` ruby spec.ios.vendored_frameworks = 'Frameworks/MyFramework.framework' spec.vendored_frameworks = 'MyFramework.framework', 'TheirFramework.xcframework' ``` `vendered_libraries`:pod 附加的 libraries 路径 ``` ruby spec.ios.vendored_library = 'Libraries/libProj4.a' spec.vendored_libraries = 'libProj4.a', 'libJavaScriptCore.a' ``` `on_demand_resources`:根据 [Introducing On demand Resources](https://developer.apple.com/videos/play/wwdc2015/214/)按需加载资源,不推荐与主工程共享标签,默认类别为 `category => :download_on_demand` ``` ruby s.on_demand_resources = {   'Tag1' => { :paths => ['file1.png', 'file2.png'], :category => :download_on_demand } } s.on_demand_resources = {   'Tag1' => { :paths => ['file1.png', 'file2.png'], :category => :initial_install } } ``` `resources`:为 pod 构建的 bundle 的名称和资源文件,其中 key 为 bundle 名称,值代表它们应用的文件模式。 ``` ruby spec.resource_bundles = { 'MapBox' => ['MapView/Map/Resources/*.png'],     'MapBoxOtherResources' => ['MapView/Map/OtherResources/*.png'] } ``` `exclude_files`:排除的文件模式列表 ``` ruby spec.ios.exclude_files = 'Classes/osx' spec.exclude_files = 'Classes/**/unused.{h,m}' ``` `preserve_paths`:下载后不应删除的文件。默认情况下,CocoaPods 会删除与其他文件模式不匹配的所有文件 ``` ruby spec.preserve_path = 'IMPORTANT.txt' spec.preserve_paths = 'Frameworks/*.framework' ``` `module_map`:pod 继承为 framework 时使用的模块映射文件,默认为 true,CocoaPods 根据 公共头文件创建 module_map 文件。 ``` ruby spec.module_map = 'source/module.modulemap' spec.module_map = false ``` #### Subspecs `subspec`:子模块的规范;实行双重继承:specs 自动继承所有 subspec 作为依赖项(除非指定默认 spec);subspec 继承了父级的属性; ``` ruby # 采用不同源文件的 Specs, CocoaPods 自动处理重复引用问题 subspec 'Twitter' do |sp|   sp.source_files = 'Classes/Twitter' end subspec 'Pinboard' do |sp|   sp.source_files = 'Classes/Pinboard' end # 引用其他子规范 s.subspec "Core" do |ss|     ss.source_files  = "Sources/Moya/", "Sources/Moya/Plugins/"     ss.dependency "Alamofire", "~> 5.0"     ss.framework  = "Foundation"   end   s.subspec "ReactiveSwift" do |ss|     ss.source_files = "Sources/ReactiveMoya/"     ss.dependency "Moya/Core"     ss.dependency "ReactiveSwift", "~> 6.0"   end   s.subspec "RxSwift" do |ss|     ss.source_files = "Sources/RxMoya/"     ss.dependency "Moya/Core"     ss.dependency "RxSwift", "~> 5.0"   end end # 嵌套子规范 Pod::Spec.new do |s|   s.name = 'Root'   s.subspec 'Level_1' do |sp|     sp.subspec 'Level_2' do |ssp|     end   end end ``` `default_subspecs`:默认子规范数组名称,不指定将全部子规范作为默认子规范,`:none` 表示不需要任何子规范。 ``` ruby spec.default_subspec = 'Core' spec.default_subspecs = 'Core', 'UI' spec.default_subspecs = :none ``` `scheme`:用以给指定 scheme configuration 添加拓展 ``` ruby spec.scheme = { :launch_arguments => ['Arg1'] } spec.scheme = { :launch_arguments => ['Arg1', 'Arg2'], :environment_variables => { 'Key1' => 'Val1'} } ``` `test_spec`:测试规范,在 1.8 版本支持。可参考:[CocoaPods 1.8 Beta](https://blog.cocoapods.org/CocoaPods-1.8.0-beta/) `requires_app_host`:是否需要宿主 APP 运行测试,仅适用于测试规范。 `app_host_name`:必要时作用于应用程序的应用程序规范名称 `app_spec`:宿主 APP 规范 ``` ruby Pod::Spec.new do |s|   s.name         = 'CannonPodder'   s.version      = '1.0.0'   # ...rest of attributes here   s.app_spec 'DemoApp' do |app_spec|     app_spec.source_files = 'DemoApp/**/*.swift'     # Dependency used only by this app spec.     app_spec.dependency 'Alamofire'   end   s.test_spec 'Tests' do |test_spec|     test_spec.requires_app_host = true     # Use 'DemoApp' as the app host.     test_spec.app_host_name = 'CannonPodder/DemoApp'     # ...rest of attributes here     # This is required since 'DemoApp' is specified as the app host.     test_spec.dependency 'CannonPodder/DemoApp'   end end ``` #### Multi-Platform support 存储特定于某一个平台的值,分别为 ios、osx、macOS、tvos、watchos: ``` ruby spec.resources = 'Resources/**/*.png' spec.ios.resources = 'Resources_ios/**/*.png' ``` ## Pod 的开发流程 了解完 Podfile 和 podspec 的相关的规范之后,那么开发自己的 pod 应该是一件驾轻就熟的事。 #### Spec Repo Spec Repo 是 podspec 的仓库,即是存储相关的 podspec 文件的地方。本地源存储于 ~/.cocoapods/repos中,它从 git 上拉取并完全保留目录结构。可以发现, Master Specs Repo 的现在目录结构有些特殊;以往版本的 Master Spec Repo 是完全在同一目录下的,但若大量文件在同一目录中会导致了 [Github 下载慢](https://github.com/CocoaPods/CocoaPods/issues/4989#issuecomment-193772935) 的问题。为解决这个问题,采用散列表形式处理。具体方式为对名称进行 MD5 计算得到散列值,取前三位作为目录前缀,以对文件分散化。初次之外,CocoaPods 后续还采用 CDN 以及 trunk 进一步加快下载速度,有兴趣可以参考 [CocoaPods Source 管理机制](http://chuquan.me/2022/01/07/source-analyze-principle/)。 如:`md5("CJFoundation") => 044d913fdd5a52b303222c357521f744`;`CJFoundation` 则在 /Specs/0/4/4 目录中 ![](/assets/images/20230426CocoaPodsUserGuide/1.webp) #### Create 只需利用  `pod lib create [PodName] ` 命令便可以快速创建一个自己的 pod 。填写好使用平台、使用语言、是否包含 Demo、测试框架等信息,CocoaPods 会从默认的 Git 地址中拉取一份 pod 模版,同时也可以通过 `--template-url=URL` 指定模版地址。在执行完后,整个文件结构如下: ``` swift tree CustomPod -L 2 CustomPod ├── CustomPod │   ├── Assets // 存放资源文件 │   └── Classes │       └── RemoveMe.[swift/m] // 单一文件以确保最初编译工作 ├── CustomPod.podspec // Pod 的 spec 文件, 是一个 Pod 依赖的索引以及规范信息 ├── Example // 用作演示/测试的示例项目 │   ├── CustomPod │   ├── CustomPod.xcodeproj │   ├── CustomPod.xcworkspace │   ├── Podfile │   ├── Podfile.lock │   ├── Pods │   └── Tests ├── _Pods.xcodeproj -> Example/Pods/Pods.xcodeproj // 指向 Pods 项目的以获得 Carthage 支持 ├── LICENSE // 许可证 └── README.md  // 自述文件 ``` #### Development 将源文件和资源分别放入 Classes / Assets 文件夹中,或者按你喜欢的方式组织文件,并在 podspec 文件中编辑相应项。如果你有任何想使用的配置项,可参考前面的podsepc 语法规范 。 一般来说,开发 Pod 一般都是作为本地 Pod 被其他 Project 所依赖进行开发,无论是使用 example 文件夹的 project 或者其他的 Project。 `pod 'Name', :path => '~/CustomPod/'` #### Testing 通过 `pod lib lint `以验证 Pod 仓库的使用是否正常。 #### Release 前面提到过 podspec 可以看作是整个仓库的索引文件,有了这个文件也就能组织起一个 Pod。因此官方的源以及私有源都只需要 podspec 即可,而其他文件则应推送到 podspec 中 source 中指定仓库,这个仓库应该是你自创建的。 在准备发布推送源代码时,需要更新版本号以及在 git 上打上 tag,这是为了进行版本号匹配,因为默认情况下的 podspec 文件中: ``` ruby s.source = { :git => 'https://github.com/XXX/CustomPod.git', :tag => s.version.to_s } ``` 可能你的工作流操作如下: ``` sh $ cd ~/code/Pods/NAME $ edit NAME.podspec # set the new version to 0.0.1 # set the new tag to 0.0.1 $ pod lib lint $ git add -A && git commit -m "Release 0.0.1." $ git tag '0.0.1' $ git push --tags ``` 存有几种方式推送 podspec 文件: * 1.推送到[公共仓库](https://github.com/CocoaPods/Specs),需要用到的 trunk 子命令,更多可以参考 [Getting setup with Trunk](https://guides.cocoapods.org/making/getting-setup-with-trunk): ``` sh # 通过电子邮箱进行注册 pod trunk register orta@cocoapods.org 'Orta Therox' --description='macbook air'  # 将指定podspec文件推送到公共仓库中 pod trunk push [NAME.podspec]  # 添加其他人作为协作者 pod trunk add-owner ARAnalytics kyle@cocoapods.org  ``` * 2.推送到私有源,例如 [Artsy/Specs](https://github.com/artsy/Specs),需要用到 repo 子命令,更多可以参考 [Private Pods](https://guides.cocoapods.org/making/private-cocoapods): ``` sh # 将私有源地址添加到本地 pod repo add REPO_NAME SOURCE_URL  # 检查私有源是否安装成功并准备就绪 cd ~/.cocoapods/repos/REPO_NAME pod repo lint . # 将Pod的podspec添加到指定REPO_NAME中 pod repo push REPO_NAME SPEC_NAME.podspec ``` * 3.不推送到任何源中,若能存在以 URL 方式检索到 podspec文件,则可用该 URL,一般采用仓库地址,例如: ``` ruby pod 'AFNetworking', :git => 'https://github.com/XXX/CustomPod.git' ``` #### Semantic Versioning 语义化版本控制顾名思义是一种语义上的版本控制,它不要求强制遵循,只是希望开发者能够尽量遵守。如果库之间依赖关系过高,可能面临版本控制被锁死的风险(可能需要对每一个依赖库改版才能完成某次升级);如果库之间依赖关系过于松散,又将无法避免版本的混乱(可能库兼容性不再能支持以往版本),语义化版本控制正是作为这个问题的解决方案之一。无论在 CocoaPods 中,还是 Swift Packager Manager 上,官方都希望库开发者的的版本号能遵循这一原则: 例如,给定版本号 `MAJOR.MINOR.PATCH`: * 1.`MAJOR`:进行不兼容的 API 更改时进行修改 * 2.`MINOR`:向后兼容的方式添加新功能时进行修改 * 3.`PATCH`:进行向后兼容的错误修复时进行修改 先行版本号以及版本编译信息可以添加到 `MAJOR.MINOR.PATCH` 后面以作为延伸。 ## CocoaPods 原理浅析 #### CococaPods 核心组件 CocoaPods 被 Ruby 管理,其核心部分也被分为一个一个组件。下载源码,可以看到 Gemfile 文件如下,其依赖了若干个 `gem`,有意思的是 `cp_gem` 函数,通过 `SKIP_UNRELEASED_VERSIONS` 与 `path`来控制是否采用本地的 gem 路径,实现了 DEVELOPMENT 与 RELEASE 环境的切换。 ``` ruby SKIP_UNRELEASED_VERSIONS = false # Declares a dependency to the git repo of CocoaPods gem. This declaration is # compatible with the local git repos feature of Bundler. def cp_gem(name, repo_name, branch = 'master', path: false)   return gem name if SKIP_UNRELEASED_VERSIONS   opts = if path            { :path => "../#{repo_name}" }          else            url = "https://github.com/CocoaPods/#{repo_name}.git"            { :git => url, :branch => branch }          end   gem name, opts end source 'https://rubygems.org' gemspec group :development do   cp_gem 'claide',                'CLAide'   cp_gem 'cocoapods-core',        'Core'   cp_gem 'cocoapods-deintegrate', 'cocoapods-deintegrate'   cp_gem 'cocoapods-downloader',  'cocoapods-downloader'   cp_gem 'cocoapods-plugins',     'cocoapods-plugins'   cp_gem 'cocoapods-search',      'cocoapods-search'   cp_gem 'cocoapods-trunk',       'cocoapods-trunk'   cp_gem 'cocoapods-try',         'cocoapods-try'   cp_gem 'molinillo',             'Molinillo'   cp_gem 'nanaimo',               'Nanaimo'   cp_gem 'xcodeproj',             'Xcodeproj'   gem 'cocoapods-dependencies', '~> 1.0.beta.1'   ... end ``` 这些组件相对独立,被分成一个一个 Gem 包,在 [Core Components](https://guides.cocoapods.org/contributing/components.html) 中,可以找到对这些组件的简要描述。同时也可以到 CocoaPods 的 Github 中去看详细文档。 ![](/assets/images/20230426CocoaPodsUserGuide/2.webp) * `CocoaPods`:命令行支持与安装程序,也会处理 CocoaPods 的所有用户交互。 * `cocoapods-core`:对模版文件的解析,如 Podfile、.podspec 等文件。 * `CLAide`:一个简单的命令解析器,它提供了一个快速创建功能齐全的命令行界面的 API。 * `cocoapods-downloader`:用于下载源码,为各种类型的源代码控制器(HTTP/SVN/Git/Mercurial) 提供下载器。它提供 tags、commites、revisions、branches 以及 zips 文件的下载与解压缩操作。 * `Monlinillo`:CocoaPods:对于依赖仲裁算法的封装,它是一个具有前项检察的回溯算法。不仅在 pods 中,Bundler 和 RubyGems 也是使用这一套仲裁算法。 * `Xcodeproj`:通过 Ruby 来对 Xcode projects 进行创建于修改。如:脚本管理、libraries 构建、Xcode workspece 和配置文件的管理。 * `cocoapods-plugins`:插件管理,其中有 pod plugins 命令帮助你获取的可用插件列表以及开发一个新插件等功能,具体可用 pod plugins --help 了解。 #### pod install 做了什么 执行 `pod install --verbose`,会显示 pod install 过程中的更多 debugging 信息。下文主要参考[整体把握 CocoaPods 核心组件 ](https://www.desgard.com/2020/08/17/cocoapods-story-2.html) 经过消息转发与 CLAide 命令解析,最终调用了 CocoaPods/lib/cocoapods/installer.rb 的 install! 函数,主要流程图如下: ![](/assets/images/20230426CocoaPodsUserGuide/3.webp) ``` ruby def install! prepare resolve_dependencies download_dependencies validate_targets clean_sandbox if installation_options.skip_pods_project_generation? show_skip_pods_project_generation_message run_podfile_post_install_hooks else integrate end write_lockfiles perform_post_install_actions end ``` #### 1. Install 环境准备(prepare) ``` ruby def prepare   # 如果检测出当前目录是 Pods,直接 raise 终止   if Dir.pwd.start_with?(sandbox.root.to_path)     message = 'Command should be run from a directory outside Pods directory.'     message << "\n\n\tCurrent directory is #{UI.path(Pathname.pwd)}\n"     raise Informative, message   end   UI.message 'Preparing' do     # 如果 lock 文件的 CocoaPods 主版本和当前版本不同,将以新版本的配置对 xcodeproj 工程文件进行更新     deintegrate_if_different_major_version     # 对 sandbox(Pods) 目录建立子目录结构     sandbox.prepare     # 检测 PluginManager 是否有 pre-install 的 plugin     ensure_plugins_are_installed!     # 执行插件中 pre-install 的所有 hooks 方法     run_plugins_pre_install_hooks   end end ``` 在 prepare 阶段会完成 `pod install` 的环境准备,包括目录结构、版本一致性以及 `pre_install` 的 hook。 #### 2. 解决依赖冲突(resolve dependencies) ``` ruby def resolve_dependencies     # 获取 Sources     plugin_sources = run_source_provider_hooks     # 创建一个 Analyzer     analyzer = create_analyzer(plugin_sources)     # 如果带有 repo_update 标记     UI.section 'Updating local specs repositories' do         # 执行 Analyzer 的更新 Repo 操作         analyzer.update_repositories     end if repo_update?     UI.section 'Analyzing dependencies' do         # 从 analyzer 取出最新的分析结果,@analysis_result,@aggregate_targets,@pod_targets         analyze(analyzer)         # 拼写错误降级识别,白名单过滤         validate_build_configurations     end     # 如果 deployment? 为 true,会验证 podfile & lockfile 是否需要更新     UI.section 'Verifying no changes' do         verify_no_podfile_changes!         verify_no_lockfile_changes!     end if deployment?     analyzer end ``` 通过 Podfile、Podfile.lock 以及 manifest.lock 等生成 Analyzer 对象,其内部会使用个 Molinillo 算法解析得到一张依赖关系表,进行一系列的分析与依赖冲突解决。 #### 3. 下载依赖文件(download dependencies) ``` ruby def download_dependencies   UI.section 'Downloading dependencies' do     # 构造 Pod Source Installer     install_pod_sources     # 执行 podfile 定义的 pre install 的 hooks     run_podfile_pre_install_hooks     # 根据配置清理 pod sources 信息,主要是清理无用 platform 相关内容     clean_pod_sources   end end ``` 经过前面分析与解决依赖冲突后,这是会进行依赖下载。会根据依赖信息是否被新添加或者修改等信息进行下载,同时下载后也会在本地留有一份缓存,其目录在 ~/Library/Caches/CocoaPods 。 #### 4. 验证 targets(validate targets) ``` ruby def validate_targets     validator = Xcode::TargetValidator.new(aggregate_targets, pod_targets, installation_options)     validator.validate! end def validate!     verify_no_duplicate_framework_and_library_names     verify_no_static_framework_transitive_dependencies     verify_swift_pods_swift_version     verify_swift_pods_have_module_dependencies     verify_no_multiple_project_names if installation_options.generate_multiple_pod_projects? end ``` * `verify_no_duplicate_framework_and_library_names`:验证是否有重名的 framework / library * `verify_no_static_framework_transitive_dependencies`:验证动态库是否有静态链接库依赖。个人认为,这个验证是不必要的,起码不必要 error。 * `verify_swift_pods_swift_version`:验证 Swift pod 的 Swift 版本配置且相互兼容 * `verify_swift_pods_have_module_dependencies`:验证 Swift pod 是否支持 module * `verify_no_multiple_project_names`:验证没有重名的 project 名称 #### 5. 生成工程(Integrate) ``` ruby def integrate     generate_pods_project     if installation_options.integrate_targets?         # 集成用户配置,读取依赖项,使用 xcconfig 来配置         integrate_user_project     else         UI.section 'Skipping User Project Integration'     end end def generate_pods_project     # 创建 stage sanbox 用于保存安装前的沙盒状态,以支持增量编译的对比     stage_sandbox(sandbox, pod_targets)     # 检查是否支持增量编译,如果支持将返回 cache result     cache_analysis_result = analyze_project_cache     # 需要重新生成的 target     pod_targets_to_generate = cache_analysis_result.pod_targets_to_generate     # 需要重新生成的 aggregate target     aggregate_targets_to_generate = cache_analysis_result.aggregate_targets_to_generate     # 清理需要重新生成 target 的 header 和 pod folders     clean_sandbox(pod_targets_to_generate)     # 生成 Pod Project,组装 sandbox 中所有 Pod 的 path、build setting、源文件引用、静态库文件、资源文件等     create_and_save_projects(pod_targets_to_generate, aggregate_targets_to_generate,                                 cache_analysis_result.build_configurations, cache_analysis_result.project_object_version)     # SandboxDirCleaner 用于清理增量 pod 安装中的无用 headers、target support files 目录     SandboxDirCleaner.new(sandbox, pod_targets, aggregate_targets).clean!     # 更新安装后的 cache 结果到目录 `Pods/.project_cache` 下     update_project_cache(cache_analysis_result, target_installation_results) end ``` 将之前版本仲裁的所有组件通过 project 文件的形式组织起来,并对 project 中做一些用户指定的配置。 #### 6. 写入依赖(write lockfiles) ``` ruby def write_lockfiles   @lockfile = generate_lockfile   UI.message "- Writing Lockfile in #{UI.path config.lockfile_path}" do     # No need to invoke Sandbox#update_changed_file here since this logic already handles checking if the     # contents of the file are the same.     @lockfile.write_to_disk(config.lockfile_path)   end   UI.message "- Writing Manifest in #{UI.path sandbox.manifest_path}" do     # No need to invoke Sandbox#update_changed_file here since this logic already handles checking if the     # contents of the file are the same.     @lockfile.write_to_disk(sandbox.manifest_path)   end end ``` 将依赖更新写入 Podfile.lock 与 Manifest.lock #### 7. 结束回调(perform post install action) ``` ruby def perform_post_install_actions   # 调用 HooksManager 执行每个插件的 post_install 方法    run_plugins_post_install_hooks   # 打印过期 pod target 警告   warn_for_deprecations   # 如果 pod 配置了 script phases 脚本,会主动输出一条提示消息   warn_for_installed_script_phases   # 警告移除的 master specs repo 的 specs   warn_for_removing_git_master_specs_repo   # 输出结束信息 `Pod installation complete!`   print_post_install_message end ``` 最后的收尾工作,进行 post install action 的 hook 执行以及一些 warning 打印。 ## CocoaPods + Plugins 早在 2013 年,CocoaPods 就添加了对插件的支持,以添加不符合依赖管理和生态系统增长为主要目标的功能。CocoaPods Plugins 可以:在 install 前后添加 hook、添加新命令到 pod、以及利用 Ruby 动态性做任何事。下面介绍一下常见的插件: * [cocoapods-binary](https://github.com/leavez/cocoapods-binary):一个比较早期的二进制插件库,是诸多二进制方案的灵感来源 * [cocoapods-repo-update](https://github.com/wordpress-mobile/cocoapods-repo-update):自动化 pod repo update * [cocoapods-integrate-flutter](https://github.com/upgrad/cocoapods-integrate-flutter):将 flutter 与现有 iOS 应用程序集成 * [cocoapods-uploader](https://github.com/alibaba-archive/cocoapods-uploader):上传文件/目录到远程仓库 > 许多插件可能许久未维护,读者使用需自行斟酌。 ## 不太常见概念 CocoaPods 的配置内容几乎包含了 Xcode Build 的方方面面,因此存在许多不太常见的概念,在此做一个链接聚合以供参考。 * Clang Module / module_map / umbrella header:Clang Module 是 Clang 16.0.0 中引入的概念,用以解决 #include / #import 头文件引入导致的相关问题;module_map 是用以描述 clang module 与 header 的关系;umbrella header 则是 module_map 中的语法规范,表示指定目录中的头文件都应包含在模块中。 * [Modules](https://clang.llvm.org/docs/Modules.html#introduction) * [Clang Module](http://chuquan.me/2021/02/11/clang-module/) * [LLVM 中的 Module](https://www.stephenw.cc/2017/08/23/llvm-modules/) * Hmap / Xcode Header / CocoaPods Headers Header Map 是一组头文件信息映射表,用 .hmap 后缀表示,整体结构以 Key-Value 形式存储;Key为头文件名称、Value 为 头文件物理地址。 Xcode Phases - Header 在构建配置中分为 public、private 与 project ,用以与 target 关联;其中 public 、private 就复制到最终产物的 header 和 PrivateHeaders 中,而 project 头文件不对外使用,则不会放到最终产物。 * [一款可以让大型iOS工程编译速度提升50%的工具](https://tech.meituan.com/2021/02/25/cocoapods-hmap-prebuilt.html) * [What are build phases?](https://help.apple.com/xcode/mac/current/#/dev50bab713d) * [Xcconfig](https://nshipster.com/xcconfig/): 一种配置文件,用以对构建设置进行声明与管理,比如区分不同的开发环境等。 * [On demand resource](https://developer.apple.com/videos/play/wwdc2015/214/):WWDC 2015 引入的概念,对资源文件的按需加载。 # 总结 Cocoapods发展这么多年却依然服役于现有的iOS工程,足以说明其包管理的重要性,有很多SDK厂商用它做SDK制品工程的管理,有很多业务团队用它做工程组件化编译,凡此种种都足以说明这个工具是一个iOS开发者必备的专业工具,有很多细节希望大家认真挖掘. 参考列表: [Cocoapods.org官方网站](https://cocoapods.org/) [深入理解 CocoaPods](https://objccn.io/issue-6-4/) [系统理解 iOS 库与框架](http://chuquan.me/2021/02/14/understand-ios-library-and-framework/) [Cocoapods script phases](https://swiftunwrap.com/article/cocoapods-script-phases/) [CocoaPods Podfile 解析原理 ](http://chuquan.me/2021/12/24/podfile-analyze-principle/) [Semantic Versioning 2.0.0](https://semver.org/) [一款可以让大型iOS工程编译速度提升50%的工具](https://tech.meituan.com/2021/02/25/cocoapods-hmap-prebuilt.html) [CocoaPods Source 管理机制](http://chuquan.me/2022/01/07/source-analyze-principle/#more) [版本管理工具及 Ruby 工具链环境](https://www.desgard.com/2020/06/11/cocoapods-story-1.html#podfilelock) [整体把握 CocoaPods 核心组件 ](https://www.desgard.com/2020/08/17/cocoapods-story-2.html) [工程效率优化:CocoaPods优化](https://binlogo.github.io/post/gong-cheng-xiao-lu-you-hua-cocoapods-you-hua/) [pod仓库的常用命令](https://www.sunyazhou.com/2023/04/podcommands/) [如何在pod中的podspec使用XCAssets ](https://www.sunyazhou.com/2023/03/podxcassets/) [Pod spec集成第三framework和.a工作记录](https://www.sunyazhou.com/2020/10/PodSpec/) URL: https://sunyazhou.com/2023/04/podcommands/index.html.md Published At: 2023-04-03 09:10:00 +0000 # pod仓库的常用命令 ![](/assets/images/20201010PodSpec/cocoapods.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! ## 添加私有源站的仓库 ``` sh pod repo add sunyazhou-specs https://www.sunyazhou.com/xxproject/Specs.git --verbose ``` ## 单独更新某个pod spec的仓库索引 ``` sh pod repo update sunyazhou-specs --verbose ``` ## push本地 repo 的podspec到远端私有源站 ``` sh pod repo push sunyazhou-specs xxxxlib.podspec --allow-warnings --use-libraries --verbose --skip-import-validation ``` 这里的push操作会自动触发git push.把podspec推送到远端索引仓库,后面加了一堆参数是为了去除警告方便编译通过. 如果是手动的话需要在spec仓库 创建 **库名**/**版本号**/**xxxlib.spec**文件 URL: https://sunyazhou.com/2023/03/podxcassets/index.html.md Published At: 2023-03-22 02:08:00 +0000 # 如何在pod中的podspec使用XCAssets ![](/assets/images/20201010PodSpec/cocoapods.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! # 背景 最近这几年,移动端工程的开发模式逐渐变成面向pod开发,就是工程大了以后每个模块和业务就变成了单独的pod. 在这种使用pod的背景下,我们的开发资源例如图片、文本文件、plist、音频文件等等会被创建后放到相应的pod下的bundle目录里 当放到bundle下后就不支持@2x和@3x的后缀名支持了,取图片的时候注意下下面代码 ``` objc NSBundle *mainBundle = [NSBundle bundleForClass:self.class]; NSString *resourcePath = [mainBundle pathForResource:@"YZTools" ofType:@"bundle"]; NSBundle *resourceBundle = [NSBundle bundleWithPath:resourcePath] ?: mainBundle; NSString *imagePath = [resourceBundle pathForResource:@"power" ofType:@"jpg"]; UIImage *image = [UIImage imageWithContentsOfFile:imagePath]; ``` > 1.pod不出意外的时候基本都会被编译成framework,那对应bundle最后编译完后会放到framework的目录下这时候就不能用传统的mainBundle去取了,因为它的默认bundle已经不是主工程bundle了,应该取自当前某个核心class所在的目录下的bundle. > 2.并且图片名如果带@2x或者@3x 直接取得时候必须得写上绝对名称 如下例子 > > ``` objc > [resourceBundle pathForResource:@"power@2x" ofType:@"jpg"]; > ``` 上述显然不能满足我们的需求. ![](/assets/images/20230322PodXCAssets/1.webp) ## 图片都是带2x3x的图,怎么从默认的pod里面取到相关的图片呢?. 这时候我们要做几件事 * 1. 创建XCAssets * 2. 在podspec中声明好相关名称 * 3. 创建为图片 创建好category 从这个pod的的XCAssets取出 #### 创建XCAssets ##### 如下图 ![](/assets/images/20230322PodXCAssets/2.webp) ##### podspec中声明 ``` ruby spec.resource_bundles = {'YZToolsAssets' => ['Resources/*.xcassets']} ``` 这里的YZToolsAssets和YZTools.podspec里的spec.resource_bundles 中的'YZToolsAssets'要完全对应. 下图中是相关的文档 ![](/assets/images/20230322PodXCAssets/6.webp) 这种[podspec文档](https://guides.cocoapods.org/syntax/podspec.html#resource_bundles)中说明演示的不清晰,大家需要深入学习和挖掘, 显然这个文档根本没说XCAssets咋加的. ##### 为图片获取添加分类 ``` objc #import NS_ASSUME_NONNULL_BEGIN @interface UIImage (YZBundleImage) /// 从YZTools的YZToolsAssets取图片 /// - Parameter imageName: 图片名称 + (UIImage *)yzToolsImageNamed:(NSString *)imageName; @end //这里的YZToolsAssets和YZTools.podspec里的spec.resource_bundles = {'YZToolsAssets' 完全对应 NSString *kYZToolsAssets = @"YZToolsAssets"; @implementation UIImage (YZBundleImage) + (NSBundle *)yzImageBundle { static NSBundle *imageBundle = nil; if (!imageBundle) { NSBundle *mainBundle = [NSBundle mainBundle]; NSString *resourcePath = [mainBundle pathForResource:kYZToolsAssets ofType:@"bundle"]; imageBundle = [NSBundle bundleWithPath:resourcePath] ?: mainBundle; } NSAssert([imageBundle bundlePath].length > 0, @"内部imageBundle路径不能为空"); return imageBundle; } + (UIImage *)yzToolsImageNamed:(NSString *)imageName { NSBundle *imageBundle = [self yzImageBundle]; UIImage *image = [UIImage imageNamed:imageName inBundle:imageBundle compatibleWithTraitCollection:nil]; return image; } @end NS_ASSUME_NONNULL_END ``` 在使用的时候如下代码: ``` objc UIImage *image = [UIImage yzToolsImageNamed:@"power"]; ``` 结果如下图: ![](/assets/images/20230322PodXCAssets/3.webp) 这里需要注意下这里面拿到的是`mainBundle`. #### 当编译完后最终的XCAssets会变成一个bundle 在.app/目录下 ![](/assets/images/20230322PodXCAssets/4.webp) ``` sh /var/containers/Bundle/Application/F3C2809A-A5E4-4808-A2AA-5962D4BE6AA1/bodianplayer.app/YZToolsAssets.bundle ``` ![](/assets/images/20230322PodXCAssets/5.webp) 可以看到这里的图片素材已经被加密变成一个叫做Assets.car的文件,这说明我们的资源已经被加密打包,轻易不会被别的app找到. # 总结 这里的XCAssets在最终被包装成bundle放在主app的目录下,最核心的方式是通过pod写好`resource_bundles`,并且记录好你给它的名字 这里就给大家提供demo了,都是一些技巧. URL: https://sunyazhou.com/2023/03/flutterformtextfield/index.html.md Published At: 2023-03-18 06:50:00 +0000 # Flutter中的Form表单 ![](/assets/images/20230312FlutterLifeCycle/flutter0.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! # 笔记 在学习flutter中处理用户名和密码输入的问题时,Flutter这边轻松搞定,学完后发现flutter内置的能力非常强大.下面是记录用户名密码输入的简单demo. ![](/assets/images/20230318FlutterFormTextfield/flutterInput.gif) ``` dart import 'package:flutter/material.dart'; ///创建 void main () { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: "hello flutter", home: Scaffold( appBar: AppBar( title: Text("sunyazhou.com"), ), body: ContentWidget(), ), ); } } class LoginWidgetState extends State { String username = ""; String password = ""; GlobalKey formGlobalKey = GlobalKey(); @override Widget build(BuildContext context) { return Form( key: formGlobalKey, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ TextFormField( decoration: InputDecoration( icon: Icon(Icons.people), labelText: "用户名", ), onSaved: (value) { print("执行了username的 onSaved:$value"); this.username = value!; }, validator: (value) { if (value == null || value.length == 0) { return "用户名不能为空!"; } return null; }, ), TextFormField( obscureText: true, decoration: InputDecoration( icon: Icon(Icons.lock), labelText: "密码", ), onSaved: (value) { print("执行了password的 onSaved:$value"); this.password = value!; }, validator: (value) { if (value == null || value.length == 0) { return "密码不能为空!"; } return null; }, ), SizedBox(height: 20,), Container( width: double.infinity, height: 44, child: ElevatedButton( child: Text("登录", style: TextStyle(fontSize: 20, color: Colors.white),), onPressed: () { print("注册按钮被点击"); formGlobalKey.currentState?.validate(); formGlobalKey.currentState?.save(); print("username:$username, password:$password"); }, ), ), ], ), ); } } ``` 上面的代码不但具备输入框的功能,还具备输入框内的内容校验,当发生错误的时候 可以使用`formGlobalKey`拿到当前`State`调用`validate()`函数来触发输入框的校验方法. ``` dart formGlobalKey.currentState?.validate(); ``` 这步的操作原理大概是 1. 声明GlobalKey 2. 把声明的GlobalKey 的实例传入到 `Form`中 3. 通过调用`formGlobalKey.currentState?.validate();`函数来调用其表单内子控件的方法`validator`来校验 4. 最后提交服务端登操作校验提交内容 # 总结 不得不说flutter还是有他的优势的,通过表单控件完成对children的方法注入.从而实现协议约束的形式, 代码简洁高效. 下面附上学习其他控件的记录 ``` dart class ContentWidget extends StatelessWidget { @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.all(20.0), child: LoginWidget(), ); } } class LoginWidget extends StatefulWidget { @override State createState() { return LoginWidgetState(); } } class RegisterWidget extends StatefulWidget { @override State createState() { return RegisterWidgetState(); } } class RegisterWidgetState extends State { final textEditingController = TextEditingController(); @override void initState() { // TODO: implement initState textEditingController.text = "默认值"; textEditingController.addListener(() { print("监听到值的变化: ${textEditingController.text}"); }); } @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.all(20.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ TextField( decoration: InputDecoration( icon: Icon(Icons.people), labelText: "username", hintText: "请输入用户名", border: OutlineInputBorder( borderSide: BorderSide(width: 1), ), // filled: true, // fillColor: Colors.purple, ), onChanged: (value) { print("当前值 $value"); }, onSubmitted: (value) { print("最后提交值:$value"); }, controller: textEditingController, ), ], ), ); } } class RadiusImageDemo extends StatelessWidget { @override Widget build(BuildContext context) { return Center( child: ClipRRect( borderRadius: BorderRadius.circular(16), child: Image.network("https://www.sunyazhou.com/assets/images/avatar.jpg", width: 150, height: 150, ), ), ); } } class CircleImageDemo extends StatelessWidget { @override Widget build(BuildContext context) { return ClipOval( child: Image.network("https://www.sunyazhou.com/assets/images/avatar.jpg", width: 150, height: 150, ), ); } } class AssertImageDemo extends StatelessWidget { @override Widget build(BuildContext context) { return Image.asset("assets/images/3.webp"); } } class NetworkImageDemo extends StatelessWidget { @override Widget build(BuildContext context) { return Center( child: Container( width: 300, height: 300, color: Colors.red, child: Image.network("https://www.sunyazhou.com/assets/images/20230312FlutterLifeCycle/flutter3.webp", // fit: BoxFit.cover, repeat: ImageRepeat.repeatY, ), ), ); } } class ButtonDemo extends StatelessWidget { @override Widget build(BuildContext context) { return Column( children: [ ElevatedButton( child: Text("ElevatedButton"), onPressed: () => print("ElevatedButton click"), ), OutlinedButton( child: Text("OutlinedButton"), onPressed: () => print("OutlinedButton click"), ), FloatingActionButton( child: Text("FloatingActionButton"), onPressed: () => print("FloatingActionButton click"), ), ], ); } } class TextRichDemo extends StatelessWidget { @override Widget build(BuildContext context) { return Text.rich( TextSpan( children: [ TextSpan( text: "sunyazhou.com", style: TextStyle(fontSize: 30, fontWeight: FontWeight.bold, color: Colors.red) ), TextSpan( text: "sunyazhou", style: TextStyle(fontSize: 18,color: Colors.blue) ), TextSpan(text: "\n本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. \n本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或使用,\n请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,这样您将能在第一时间获取本站信息.",style: TextStyle(fontSize: 20, color: Colors.deepOrangeAccent)), ], ), textAlign: TextAlign.center, ); } } class TextDemo extends StatelessWidget { @override Widget build(BuildContext context) { return Text("本文具有强烈的个人感情色彩,\n如有观看不适,请尽快关闭. \n本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或使用," ,style: TextStyle( fontSize: 20, color: Colors.lightBlue, ), textAlign: TextAlign.center, // maxLines: 2, overflow: TextOverflow.ellipsis, // textScaleFactor: 2, ); } } ``` URL: https://sunyazhou.com/2023/03/circularsliderview/index.html.md Published At: 2023-03-17 12:38:00 +0000 # 使用SwiftUI绘制环形 Slider # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! ## SwiftUI绘制环形 Slider 最近看到一篇文章讲述简单的用SwiftUI绘制一个圆形Slider,经过亲手实践果然很简单,记录一下代码实现和效果. ![](/assets/images/20230317CircularSliderView/slider.gif) ``` swift // // CircularSliderView.swift // CircleSliderDemo // // Created by sunyazhou on 2023/3/16. // import Foundation import SwiftUI struct CircularSliderView: View { @Binding var progress: Double @State private var rotationAngle = Angle(degrees: 0) private var minValue = 0.0 private var maxValue = 1.0 init(value progress: Binding, in bounds: ClosedRange = 0...1) { self._progress = progress self.minValue = Double(bounds.first ?? 0) self.maxValue = Double(bounds.last ?? 1) self.rotationAngle = Angle(degrees: progressFraction * 360.0) } private var progressFraction: Double { return ((progress - minValue) / (maxValue - minValue)) } private func changeAngle(location: CGPoint) { // 为位置创建一个向量(在 iOS 上反转 y 坐标系统) let vector = CGVector(dx: location.x, dy: -location.y) //计算向量的角度 let angleRadians = atan2(vector.dx, vector.dy) // 将角度转换为 0 到 360 的范围(而不是负角度) let positiveAngle = angleRadians < 0.0 ? angleRadians + (2.0 * .pi) : angleRadians // 根据角度更新滑块进度值 progress = ((positiveAngle / (2.0 * .pi)) * (maxValue - minValue )) + minValue rotationAngle = Angle(radians: positiveAngle) } var body: some View { GeometryReader { gr in let radius = (min(gr.size.width, gr.size.height) / 2.0) * 0.9 let sliderWidth = radius * 0.1 VStack(spacing: 0) { ZStack { Circle() //外圆 .stroke(Color(hue: 0.0, saturation: 0.0, brightness: 0.9), lineWidth: 20.0) .overlay() { Text("\(progress, specifier: "%.2f")") .font(.system(size: radius * 0.6, weight: .bold, design: .rounded)) } Circle() //进度条 .trim(from: 0, to: progressFraction) .stroke(Color(hue: 0.0, saturation: 0.5, brightness: 0.9), style: StrokeStyle(lineWidth: sliderWidth, lineCap: .round)) .rotationEffect(Angle(degrees: -90)) Circle() //旋钮 .fill(Color.white) .shadow(radius: sliderWidth * 0.3) .frame(width: sliderWidth, height: sliderWidth) .offset(y: -radius) .rotationEffect(rotationAngle) .gesture( DragGesture(minimumDistance: 0.0) .onChanged() { value in changeAngle(location: value.location) } ) } .frame(width: radius * 2.0, height: radius * 2.0, alignment: .center) .padding(radius * 0.1) } .onAppear { self.rotationAngle = Angle(degrees: progressFraction * 360.0) } } } } ``` # 总结 SwiftUI写代码很快,api非常多,比UIKit便携了很多,也简单了很多,我的习惯是别人的东西拿来消化吸收的同时必须出demo. [本文demo](https://github.com/sunyazhou13/CircleSliderDemo) [在 SwiftUI 中创建一个环形 Slider ](https://mp.weixin.qq.com/s/DUFEB5aOTx1jurPP4gP0MQ) URL: https://sunyazhou.com/2023/03/flutterlifecycle/index.html.md Published At: 2023-03-12 06:38:00 +0000 # Flutter的有状态StatefulWidget生命周期 ![](/assets/images/20230312FlutterLifeCycle/flutter0.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! ## Flutter的声明周期 最近在负责波点音乐的相关开发,由于波点是用flutter写的,所以周末不得不做一些Flutter开发的功课来弥补自己在移动端技术栈的缺失. Flutter的声明周期主要是`StatefulWidget`和`State`之间配合 下面从祖传的hello world开始 ``` dart import 'package:flutter/material.dart'; ///创建 void main () { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: "hello flutter", home: Scaffold( appBar: AppBar( title: Text("sunyazhou.com"), ), body: ContentWidget(), ), ); } } class ContentWidget extends StatefulWidget { ContentWidget(){ print("ContentWidget构造函数被调用"); } @override State createState() { print("createState被调用"); return ContentWidgetState(); } } class ContentWidgetState extends State { int counter = 0; ContentWidgetState() { print("ContentWidgetState构造函数被调用"); } @override void initState() { // TODO: implement initState super.initState(); print("ContentWidgetState的 initState被调用"); } @override void didChangeDependencies() { super.didChangeDependencies(); print("ContentWidgetState的 didChangeDependencies被调用"); } @override void didUpdateWidget(covariant ContentWidget oldWidget) { super.didUpdateWidget(oldWidget); print("ContentWidgetState的 didUpdateWidget被调用"); } @override Widget build(BuildContext context) { print("ContentWidgetState的 build被调用"); return Center( child: Column ( mainAxisAlignment: MainAxisAlignment.center, children: [ ElevatedButton(onPressed: (){ setState(() { counter++; }); }, child: Text("计数+1")), Text("hello world $counter", style: TextStyle(fontSize: 30),), ], ), ); } } ``` ![](/assets/images/20230312FlutterLifeCycle/flutter1.webp) 打印如下: ``` sh flutter: ContentWidget构造函数被调用 flutter: createState被调用 flutter: ContentWidgetState构造函数被调用 flutter: ContentWidgetState的 initState被调用 flutter: ContentWidgetState的 didChangeDependencies被调用 flutter: ContentWidgetState的 build被调用 flutter: ContentWidget构造函数被调用 flutter: ContentWidgetState的 didUpdateWidget被调用 flutter: ContentWidgetState的 build被调用 ``` #### didUpdateWidget `didUpdateWidget()` 这个函数父类更新才会被调用 #### 当点击后的结果 当每次点击就会每次都调用 build ![](/assets/images/20230312FlutterLifeCycle/flutter2.gif) ``` sh flutter: ContentWidgetState的 build被调用 flutter: ContentWidgetState的 build被调用 flutter: ContentWidgetState的 build被调用 flutter: ContentWidgetState的 build被调用 flutter: ContentWidgetState的 build被调用 flutter: ContentWidgetState的 build被调用 flutter: ContentWidgetState的 build被调用 flutter: ContentWidgetState的 build被调用 ``` 下面是flutter的生命周期函数图 ![](/assets/images/20230312FlutterLifeCycle/flutter3.webp) > 图片引用自[Flutter(七)之有状态的StatefulWidget](https://zhuanlan.zhihu.com/p/83782208),如果有版权问题请联系我. # 总结 这个生命周期和iOS中的UIViewController很像. 利用周末的时间学习一些新的技术, 秉承边战斗边学习的态度. [参考](https://www.geeksforgeeks.org/life-cycle-of-flutter-widgets/) URL: https://sunyazhou.com/2023/03/whimsical/index.html.md Published At: 2023-03-08 03:09:00 +0000 # whimsical模块设计 # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! # 背景 在很多开发中经常会对模块进行技术设计,其中用到很多绘图的工具,最近发现一个很好的国外在线设计工具 [https://whimsical.com/](https://whimsical.com/) > 需要科学上网才能访问. # 总结 收藏开发过程中一些工具方便提高工作效率 URL: https://sunyazhou.com/2023/03/safecast/index.html.md Published At: 2023-03-06 02:28:00 +0000 # objc中的类型安全转换 ![](/assets/images/20230306SafeCast/cast.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! # CAST `Cast`也叫柯里化,就是一种简单的类型转换过程.在iOS中我们经常对某种数据类型进行强制转换.每次不得不写一些臃肿的代码,于是大家习惯写成宏来check 某实例变量是否数据某class. 下面代码分享出来经常用到宏 利用objc运行时提供的动态特性来处理常用的类型check. ``` objc #ifdef __cplusplus extern "C" { #endif id YZSafeCast(id obj, Class classType); #ifdef __cplusplus } #endif #ifndef YZ_SAFE_CAST /// 安全类型转换(柯里化) #define YZ_SAFE_CAST(obj, asClass) YZSafeCast(obj, [asClass class]) #endif ``` 实现文件 ``` objc #import #import "YZSafeCast.h" id YZSafeCast(id obj, Class classType) { if ([obj isKindOfClass:classType]) { return obj; } return classType ? nil : obj; } ``` 使用的时候如下 ``` objc ``` ## 总结 记录一些常用且找的时候不是马上能找到的技巧 [demo](https://github.com/sunyazhou13/SafeCastDemo) URL: https://sunyazhou.com/2023/02/nsdateistoday/index.html.md Published At: 2023-02-13 11:39:00 +0000 # iOS中NSDate是否是今天Today ![](/assets/images/20230213NSDateIsToday/date.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! ## 背景 每天的iOS开发过程中经常会出现Check一些逻辑是否每天仅发生一次,一般我们的通用是用NSDate判断是否是今天,然后对NSDate做持久化存储 eg: `MMKV`、或者`NSUserDefault`,但随着工程的日渐庞大,我们逐渐关注一些细节和代码 的耗时. 首先来看一下几种不同的逻辑判断`NSDate`isToday的代码实现 * 1.系统NSCalendar日历 ``` objc NSDate *date = [NSDate date]; //这里取当前日期,正常应该做为参数传入NSDate BOOL inToday = [[NSCalendar currentCalendar] isDateInToday:date] ``` * 2.增加更多参数调用NSCalendar ``` objc - (BOOL)isToday { NSCalendar *cal = [NSCalendar currentCalendar]; NSDateComponents *components = [cal components:(NSCalendarUnitEra|NSCalendarUnitYear|NSCalendarUnitMonth|NSCalendarUnitDay) fromDate:[NSDate date]]; NSDate *today = [cal dateFromComponents:components]; components = [cal components:(NSCalendarUnitEra|NSCalendarUnitYear|NSCalendarUnitMonth|NSCalendarUnitDay) fromDate:self]; NSDate *otherDate = [cal dateFromComponents:components]; return [today isEqualToDate:otherDate]; } ``` * 3.通过比较时间 ``` objc - (BOOL)isTodayWithDate:(NSDate *)date { NSDate *selfBegin = [self dateByBeginDay]; NSDate *dateBegin = [date dateByBeginDay]; if (fabs([selfBegin timeIntervalSinceDate:dateBegin]) < 1.0e-6) { return YES; } return NO; } - (NSDate *)dateByBeginDay { //前一天的结束 16:00为结束 8小时时区 今天开始 也就是说 前一天的00:00:00 unsigned int flags = NSCalendarUnitYear | NSCalendarUnitMonth | NSCalendarUnitDay | NSCalendarUnitHour | NSCalendarUnitMinute | NSCalendarUnitSecond; NSDateComponents *parts = [[NSCalendar currentCalendar] components:flags fromDate:self]; [parts setHour:0]; [parts setMinute:0]; [parts setSecond:0]; return [[NSCalendar currentCalendar] dateFromComponents:parts]; } ``` 这几种都是检测是否是当天的代码 #### 故事的开始 今天工作中Review代码 对NSDate的检查判断是否是today的耗时问题产生了分歧,工程师解决问题的方式很简单,实践代码证明是否耗时. 我选中了 第一种和第三种做测试.代码如下: ![](/assets/images/20230213NSDateIsToday/result.webp) ``` sh 2023-02-13 19:59:08.855078+0800 NSDateSpeedDemo[1837:197213] NSCalendar耗时:0.011064 2023-02-13 19:59:11.108141+0800 NSDateSpeedDemo[1837:197213] NSDate (YZUtils)耗时:0.030793 ``` 完整的代码如下: ``` objc - (IBAction)didSysDateClick:(id)sender { NSDate *date = [NSDate date]; CFTimeInterval startTime = CACurrentMediaTime(); for (int i = 0; i < 1000; i++) { __unused BOOL inToday = [[NSCalendar currentCalendar] isDateInToday:date]; } CFTimeInterval endTime = CACurrentMediaTime(); NSString *log = [NSString stringWithFormat:@"NSCalendar耗时:%f",endTime - startTime]; NSLog(@"%@", log); self.l1.text = log; } - (IBAction)didOnCusDateClick:(id)sender { NSDate *date = [NSDate date]; CFTimeInterval startTime = CACurrentMediaTime(); for (int i = 0; i < 1000; i++) { __unused BOOL inToday = [date isTodayWithDate:date]; self.l2.text = [NSString stringWithFormat:@"SysDate:%d",i]; } CFTimeInterval endTime = CACurrentMediaTime(); NSString *log = [NSString stringWithFormat:@"NSDate (YZUtils)耗时:%f",endTime - startTime]; NSLog(@"%@", log); self.l2.text = log; } ``` # 总结 这时间上还是差至少1x左右,如果频繁的操作确实会影响一些性能. [demo](https://github.com/sunyazhou13/NSDateSpeedDemo) URL: https://sunyazhou.com/2023/02/webpenhancement/index.html.md Published At: 2023-02-05 09:09:00 +0000 # 博客的图片资源优化 # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! ## 背景 最近周末有点时间,对博客的资源图片进行了整体优化,优化后图片资源减少了一半.要不然以目前我写文章的速度,不出几年连同资源很快就会超过1G.如果超过1G的话github pages将停止对仓库提供支持,需要额外购买仓库空间. 前一阵偶然发现自己的博客图片太多了运行起来慢 加载也慢,资源文件优化减负逐渐成为迫在眉睫的焦虑任务. 然后尝试一下webp图片和png对比发现,简直就是降维打击,通用的图片,在webp下27k,在png下80k+,这完全就是几何倍数的优化.再回头看看画质,虽然色彩没有之前鲜艳了不过也不是一些重要的图片,没有必要搞得那么高清.于是萌生优化博客资源的想法. 想法有了 可是面对的是自己写了好多年的资源 难不成要挨个替换吗? 作为NB的工程师必须得用脚本把所有的图片进行批处理,全部转成 webp 于是我使用 brew 安装了 libwebp的库 ``` sh brew install jpeg-turbo brew install libpng brew install libtiff brew install webp ``` 经过一顿操作终于安装好了 webp命令 如果把其他图片转webp 应该使用`cwebp`命令(codec 编码),反之`dwebp`就是decodec解码 工具完成后开始整活 `touch` 一个`webp.sh` 遍历所有资源目录 然后执行转码操作,转完后.顺便把之前的`png`,`jpg`等通通删除掉. ``` sh #!/bin/sh for dir in `ls .` do if [ -d $dir ] then echo $dir cd $dir `for file in *.png *.jp*g *.PNG ; do cwebp -q 80 "$file" -o "${file%.*}.webp"; done` rm -rf *.png *.jp*g cd .. fi done #读取第一个参数 read_dir $1 #for file in *.png *.jp*g *.PNG ; do cwebp -q 80 "$file" -o "${file%.*}.webp"; done ``` 这个脚本放到`/assets/images`目录执行 ![](/assets/images/20230205WebpEnhancement/webp1.webp) 剩下的工作就是找到左右post文章的markdown统一更改图片后缀 ![](/assets/images/20230205WebpEnhancement/webp2.webp) 然后通过sourceTree进行最后的校对 review一遍改动防止改错,这个过程很快,虽然很多 但是图片的后缀修改十分简单容易识别. ![](/assets/images/20230205WebpEnhancement/webp3.webp) 最后build博客 部署到远端即可 # 总结 就目前这个案例,手动改肯定是十分累并且全是体力活,一定要学会驾驭技术,用技术去解决实际问题. URL: https://sunyazhou.com/2023/02/jekyllpost/index.html.md Published At: 2023-02-02 02:21:00 +0000 # 如何使用jekyll发布一篇文章 # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! ## jekyll 我的博客开始使用的是hexo,后来[`喵神`](https://onevcat.com/) 把博客换成以jekyll的形式,我还是很喜欢的.可是之前hexo要写一篇文章直接就可以用 ``` sh hexo new "202300202XXXPaper" ``` 的形式 通过模版生成一个markdown文件. > 具体使用一看看[hexo 指令](https://hexo.io/zh-cn/docs/commands.html) 然而新的[https://github.com/cotes2020/jekyll-theme-chirpy/ 主题](https://github.com/cotes2020/jekyll-theme-chirpy/) 在喵大的简化版本里面还是比较简单实用的,唯独缺少了 如何快速写文章的操作. 经过和喵神邮件请教 ![](/assets/images/20230202JekyllPost/email.webp) 喵神不但回复了我并表示 他不是很经常写文章,这种操作他都是复制一下原来的也不麻烦.邮件末尾喵神给出来一个非常实用的 stackoverflow的答案. ![](/assets/images/20230202JekyllPost/answer.webp) 在我的博客中有一个`Gemfile`文件 ``` sh source "https://rubygems.org" gem "jekyll", ">=3.8.6" # Official Plugins group :jekyll_plugins do gem "jekyll-paginate" gem "jekyll-redirect-from" gem "jekyll-seo-tag", "~> 2.6.1" gem 'jekyll-compose' //新增这个 end group :test do gem "html-proofer" end ``` 然后执行一次 ``` sh bundle install ``` 这样会保证所需要的类库全部load, 这里可以配置 rubychina的镜像或者科学上网 剩下的就是每次发文章执行 ``` sh $ bundle exec jekyll post "My New Post" ``` > `$`这个符号不要复制哈,这是表示你用shell终端执行的命令. 生成后这玩意会自动标识年月格式 ``` sh bundle exec jekyll post "jekyllpost" ``` ![](/assets/images/20230202JekyllPost/post.webp) ### 疑问 这里生成的markdown没有hexo中的模版那样能自定义,我没找到 如果你感兴趣可以一起研究一下. [Why isn't there a "jekyll post" command to create posts like in hexo?](https://stackoverflow.com/questions/43416113/why-isnt-there-a-jekyll-post-command-to-create-posts-like-in-hexo) # 总结 这个命令工具非常适合我这种经常发表文章的使用,希望能帮助一些使用jekyll的伙伴. URL: https://sunyazhou.com/2023/02/swift-defer/index.html.md Published At: 2023-02-01 02:10:58 +0000 # Swift中的defer关键字 # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! ## defer关键字 在swift中有一个关键字非常 类似 try catch finally中的`finally`,在一个代码块中执行完成后 执行最后的收尾代码完成一些收尾工作. 例如: 清理工作、回收资源 跟 swift 文档举的例子类似,defer一个很适合的使用场景就是用来做清理工作。文件操作就是一个很好的例子: 关闭文件 ``` swift func foo() { let fileDescriptor = open(url.path, O_EVTONLY) defer { close(fileDescriptor) } // use fileDescriptor... } ``` 在例如: dealloc 手动分配的空间,最后销毁内存 ``` swift func foo() { let valuePointer = UnsafeMutablePointer.allocate(capacity: 1) defer { valuePointer.deallocate(capacity: 1) } // use pointer... } ``` #### 简单理解 我可以简单的把`defer`关键字理解为 return之前要调用的最后一个函数.无论是switch还是其它条件导致函数return返回,如果我们想在return之前做一些收尾工作那么`defer`非常合适. ``` swift func foo() { ... defer { //这里代码块在return之前调用 } ... return; } ``` 如果我们使用多个defer的话 会按照 defer压栈顺序执行.非不要不建议要加多个`defer` ``` swift func foo() { print("1") defer { print("6") } print("2") defer { print("5") } print("3") defer { print("4") } } ``` 一个 scope 可以有多个 defer,顺序是像栈一样倒着执行的:每遇到一个 defer 就像压进一个栈里,到 scope 结束的时候,后进栈的先执行。如下面的代码,会按 1、2、3、4、5、6 的顺序 print 出来。 ## 官方定义 > You use a defer statement to execute a set of statements just before code execution leaves the current block of code. This statement lets you do any necessary cleanup that should be performed regardless of how execution leaves the current block of code—whether it leaves because an error was thrown or because of a statement such as return or break. For example, you can use a defer statement to ensure that file descriptors are closed and manually allocated memory is freed. > A defer statement defers execution until the current scope is exited. This statement consists of the defer keyword and the statements to be executed later. The deferred statements may not contain any code that would transfer control out of the statements, such as a break or a return statement, or by throwing an error. Deferred actions are executed in the reverse of the order that they’re written in your source code. That is, the code in the first defer statement executes last, the code in the second defer statement executes second to last, and so on. The last defer statement in source code order executes first. ``` swift func processFile(filename: String) throws { if exists(filename) { let file = open(filename) defer { close(file) } while let line = try file.readline() { // Work with the file. } // close(file) is called here, at the end of the scope. } } ``` The above example uses a defer statement to ensure that the `open(_:)` function has a corresponding call to `close(_:)`. > You can use a defer statement even when no error handling code is involved. [Specifying Cleanup Actions](https://docs.swift.org/swift-book/LanguageGuide/ErrorHandling.html) ## 总结 2023年swift能多写一些了,希望记录一些经常忘记的内容,方便使用时及时翻阅查找. URL: https://sunyazhou.com/2022/12/FinalSummary/index.html.md Published At: 2022-12-24 06:54:00 +0000 # 2022年终总结 ![](/assets/images/20221231FinalSummary/2022F1.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! ``` txt 年终将至,作业催交. 州司临门,病毒被秒. 诸事皆顺,时光渐少. 励精图治,地上逍遥. ``` 这是坚持写年终总结的第5个年头了,如果非要给今年的总结写个主题的话我肤浅的认为`每天都活在自我否定后的不断妥协中的一年`最为贴切. * [2021年终总结](https://www.sunyazhou.com/2021/12/FinalSummary/) * [2020年终总结](https://www.sunyazhou.com/2020/12/FinalSummary/) * [2019年终总结](https://www.sunyazhou.com/2019/12/FinalSummary/) * [2018年终总结](https://www.sunyazhou.com/2018/12/FinalSummary/) * [2017年终总结](https://www.sunyazhou.com/2017/12/FinalSummary/) > 真的是有量的积累,才有质的飞跃. ## 2022回顾 * 关于生活 * 关于工作 * 关于学习 * 关于理想 * 关于好物 ## 关于生活 疫情3年多了,直到这个月初才真正的放开,经过3针疫苗后,放开的第一周不幸中招.还好康复的很快. 2022年的春节是在北京过的,这是自参加工作以来唯一一次被疫情逼迫不得不在北京过年.说到底还是因为穷人的世界处处都是困难.就在与此同时 俄罗斯和乌克兰展开了旷日持久的战争. 此时此刻2022年12月24日晚,战争仍在继续,随着逐步放开的疫情管控,疫情终将被消灭.可是这战争何日休战上不明朗,美国的`特朗普`走了,来了`拜登biden`当总统,这老头还真不如特朗普那两把刷子,但是他其中的一把刷子向中国刷来,另一把刷子向着北约俄罗斯刷去. ![](/assets/images/20221231FinalSummary/2022Biden.webp) > 拜登英文名字biden,以下统称biden 在此种操作之下,我们的汽油价格从平常的6.x元涨到目前的8.x元(92号汽油),而且从2月俄乌开战到现在仍然没有下降的趋势. 我的生活受到或多或少的影响,各种科技公司集体的口号永远都是一个`降本增效`,裁员的声浪遍地哀嚎,我所处的IT行业是重灾区.刚回北京没多久到目前都已经换了三家东家了. 给我的感受是这是时代的浪潮,当初的选择可能将会导致灾难发生的可能性概率高低的差别,但并不会决定最终的结果.在这种情况下的实体经济遭到了前所未有的打击,经济不景气的同时各行各业都在渐少运营成本,能在这种时代的背景下活下去的就是王者. 于是引发了我对这三年多的疫情模式和处境产生了思考,得出了一个结论-`这几年能活的体面就好,工资多少不重要,活下去很重要`,生活总是喜忧参半,得抻悠着过. 这简直就是对自己的各种否定后的感受,这不是生活`好不好过`的问题,是生活`能不能过`的问题. 回到biden的第一把刷子挥向中国后,投资的各种资本撤资的撤资,导致各种科技公司股票狂跌,降薪裁员都在路上.让我不得不佩服biden这老头的刷子确实挺猛,刷出了新境界的同时也干残了美国的科技巨头,从twitter被马斯克(musk)收购后裁员75%,紧随其后的是Meta就是那Facebook,再后是google和各种曾经风光无限的美国科技公司. 这轮操作完后biden的刷子刷完墙脏的同时还整自己一身油漆,真是刷的漂亮. #### 纪念图灵 在之前我承诺过英镑发行后,我会买几张收藏用以纪念推动计算机行业发展的先驱-阿兰图灵. ![](/assets/images/20221231FinalSummary/2022F2.webp) 是他鼓励了我的灵魂,让我对计算机科学的发展不断开拓进取,努力革新. 今年夏天去招商银行,预约得到了2张崭新的50英镑,正面是英国女王,背面是这位计算机先驱的灵魂人物图灵.这英镑确实比人民币质量好,上边还有芯片电路.咱也不知道干啥用的反正是400多RMB兑换一张面值50英镑. 看到这里的各位觉得我的年终总结还算有那么一点价值的话请帮忙转发,我打算把另一张50英镑送出给关注我的博客的哥几个,用以勉励各位让计算机行业发展的更好.当然这个操作要看哪位老哥学识高深技术卓越博客出众对技术的发展有卓越得贡献者哈. #### 做菜 不知道从什么时候起养成了一个习惯,闲着无聊的时候喜欢反省自己的各种缺陷,然后不断总结错误和不足.为了验证自己是不是秉承`行胜于言`的理念.从一道东北最考验技术的菜开始练起-锅包肉. 怎么才算是完整的做好呢? 我在某视频app上反复观看几个喜欢做菜分享的博主,经过反复10次的观看和总结我发现,东北菜还得东北厨师做. 那什么才是好菜的标准呢? 好吃就是标准.现在都流行老菜新做,有的说锅包肉橙汁好吃,有的说番茄汁好吃,最原始的锅包肉做法是咸鲜口的不是糖醋口的.不管啥汁啥口味,您觉得好吃就好. 这是第一次试验的样子. ![](/assets/images/20221231FinalSummary/2022F3.webp) 这是第二次实验的样子. ![](/assets/images/20221231FinalSummary/2022F4.webp) ... 不行了在往后就没发展示了.不过经过熟能生巧的不断练习发现,这道菜我有信心能做到完美,就是这菜太费材料,食材也着实不便宜,要是有足够的食材练手那做好它就是时间的问题. 经过动手实践我做个技术总结: * 糊(hú四声)的调制,最好提前泡好水淀粉(土豆淀粉),然后必须要调整到非牛顿流体的状态. * 里脊肉必须攥干水分,不能出现血水,否则炸出来后会发黑.肉片要攥到发白的状态为最佳 * 高油温炸制,如果是自己家燃气灶的话油温最好到达180°以上(就是油冒烟的状态),第一遍炸制到金黄状态,第二第三遍要等油温再次升高(到冒烟状态)进行复炸,这步叫`闯油`,就是趁着油温升高放里复炸.最终经过几遍闯油操作后锅包肉会变成枣黄色基本完成 * 剩下就是调整汁技巧了,这玩意还分烹汁和握汁.具体操作大家看网络上的视频教程就会知道我不过多介绍了. 经过上述的操作,我认为最重要的就是手法和认真程度,勤加练习. 在这里我推荐大家买一种叫做风车牌的淀粉 ![](/assets/images/20221231FinalSummary/2022F5.webp) 如果经常做建议买2.5KG以上的,这道菜十分费淀粉.别问为什么,这都是交的学费.别的淀粉基本都没有它纯度高. 由于我的勤加练习我在一次同事钓鱼活动娱乐后去吃饭,席间有一道菜叫做`那年秋天的茄子` 吃完我感觉不错我要回去复刻出来.下面是饭店的样子 ![](/assets/images/20221231FinalSummary/2022F7.webp) 下面是我复刻后的样子. ![](/assets/images/20221231FinalSummary/2022F6.webp) 这道菜很好吃,成本非常廉价,我可以非常负责告诉你这盘菜食材+调料成本不超过10元,麻烦就麻烦在如何熬制那个拔丝状态的糊. 吃过就知道了,一点不油腻,我做的很油腻不是很成功还得多练练. 2022年的生活就介绍到这里吧,都是一堆琐碎的记忆和一些扯淡的经验以及不成熟的政治观念各位见笑了. ## 关于工作 年初的时候在旷视负责人脸识别的美颜SDK开发,这家公司绝对是一家没有人文主义精神和艺术气质的公司.组内同事的歧视感比较强总认为自己非常NB咱也不知道哪里来的自豪感,如果后续同行找工作还是不要找这家了吧,创始人自称清华姚班毕业觉的自己出身高贵,傲气较重给我的直观感受是清华是好清华,通过观察这些人带领的公司情况觉得,怎么把公司搞得这么乌烟瘴气,瞅瞅招聘公司这些人都是啥人,有的人还不错,有的人却很一般,有的人还不知道自己是垃圾的垃圾,总想着把别人整走,而不是想着怎么把队友带好,团队带好. 我给这个公司最后的评价是“脚踏实地一点,别拿着那点所谓的AI说事了,这只是一个哪家都会技术,让自己活下去,创造的技术革新,否则你打不过商汤都是有原因的”.也许这就是传说中的`卷`吧! 能卷的留下不能卷的就走吧!如果一家公司进入这种状态的话能走就走吧!不值得付出过多的努力,因为它的状态就注定产出低增效低没有收入只有投入.比较搞笑的是技术面试到入职一共5面,4面技术,这4面中,3面算法,3面算法中2个都是二分查找.我不吹牛逼在这负责的说这边面试官算法只会二分查找,如果我要让他写个二叉树层序遍历锯齿遍历或者LRU之类的他基本写不出来吧!总之我还是在算法上做了很多功课的. 我之前把清华的兄弟分为两派. * 求真务实派 * 徒有虚名派 经过亲身鉴定,此属于徒有虚名派,鉴定完毕! #### 近况 我目前就职于TME腾讯音乐娱乐集团下的酷我音乐团队,这是经过这两年多的疫情裁员潮下我能找到为数不多的公司了. TME下面的产品 * QQ音乐 * 酷狗音乐 * 酷我音乐 * 懒人听书 * 酷我畅听 * 波点音乐 ... 由于音乐是有版权的,所以在TME下的产品大部分都是收费的.公司目前发展都是走降本增效的路线 #### 说说我的感受 早些年在百度工作的时候团队和团队之间是封闭的,几乎没有技术交流的机会,即便某些团队做出了很酷炫的功能或者很有技术含量的技术架构和工具也很难和兄弟团队甚至百度内部其它团队共享成果.这一点我很反感,一家公司内部分裂的好像N家小公司,互相不通气,封闭闭门造车,完全不开源至少兄弟团队共享技术成果是一件十分糟糕的事情,这也导致百度为什么直到现在依然是这样,因为大家都在考虑自己团队的ORI,这就是典型的走下坡路的表现,如果Robin看到我这篇年终总结记得改一改公司这个很不好的点.至少让百度的股票升一个百分点不是问题. 在TME虽然情况也是这样,但相对有很多改善,Q音的功能我们可以随时拿来,Q音提供内部技术支持,能保证技术共享,我想也许这就是腾讯为什么能比百度强的原因吧! ![](/assets/images/20221231FinalSummary/2022F8.webp) 这已经是职业生涯里赶上的第二家上市公司了.我没股票哈. #### 说说团队 总体感觉这边的团队人都挺好的 * 没有新人排斥感 * 大家虽然薪资不高,但团队内部气氛很好 * 老大没有很不好的架子,还是一个合格的leader 现在的环境下能遇到这样的团队已经非常nice了. #### 说说技术 我们团队对技术的追求还是很好的. 自从加入到这个团队后,把在快手团队的好的内容我基本都带到了这个团队 * 开启合理化分支管理 * 因地制宜的代码review * 探讨技术 * 讨论流行的技术方案和设计技巧 * 对编程的热爱同时把平常的难点集中攻破. * 对一些有价值和高技术含量的开发会做技术分享 说一件比较搞笑的事情,在进入到团队开发2个月多的时候遇到一个非常奇葩的问题,有一个组内的小伙写动画错误的把`CAReplicatorLayer`里的`instanceCount`设置成了`MAXFLOAT`。 导致手机播放歌曲的时候直接死机重启,为了解决这个问题我用了一天的时间,不断的二分法缩小代码差距.因为debug一次的时间包含重启手机要3分钟.最终锁定该bug. 这是我从业至今为止遇到最奇葩的bug必须mark一下. ## 关于学习 这一年的上半年接触了人脸识别技术和AI相关,了解了一些机器学习相关的技术.下半年在做音乐类app, 当然音乐也不是那么好做的,首先得会数字信号处理.谈到信号处理就离不开傅里叶变换.因为懂了这些后才能做更好的功能,比如播放的每一首歌转换成各种格式的时候需要把格式转成原始的数据采样,比如 AAC或者mp3我想转成无陨音质,这中间需要把封装好的格式转回PCM(脉冲调制编码),经过编码器封装成其它格式.这中间肯定不止是会点音视频技术就完事了,还要负责对数据频域转时域,经过FFT(快速傅里叶变换)后生成多声道信号样本方便对音频做频带动画.比如加权平均算法,A计权算法,加窗函数的拟合处理,滤波处理等等.大部分人应该不接触这领域都会感到很晦涩,我也一样. #### 数字信号处理 所以我在B站上学花了点时间学习完了一门 数字信号处理的课程 ![](/assets/images/20221231FinalSummary/2022F9.webp) 这门课程学完之后感觉和没学一样,因为它全是各种公式推导并没有代码也没有工具的使用教程,只是能作为一个入门的基础知识参考,比如低通滤波器、高通滤波器、带通滤波器、带阻滤波器、FIR滤波器、BF半带滤波器等等各种滤波算法、再比如一些拉普拉斯定理,希尔波特定理等等都是偏学术和基础,根本不适合一个实战程序员学习. #### 音视频课程 * 快手 · 音视频技术入门课 * 快手 · 移动端音视频开发实战 这两门课程我都保证100%学完,但是学完并不代表就会了,还得变战斗边学习. ![](/assets/images/20221231FinalSummary/2022F10.webp) ![](/assets/images/20221231FinalSummary/2022F11.webp) 学完的证书留个纪念,虽然这说明不了什么, 这两门课程非常适合iOS转C/C++学习音视频.如果我推荐的话一定是10颗星. #### 目前正在学习 侯捷C++ ![](/assets/images/20221231FinalSummary/2022F12.webp) 因为音视频入门的门槛就是C/C++,为了能和技术契合我这边正在努力跟进学习. #### 看书 ![](/assets/images/20221231FinalSummary/2022Books.webp) 这一年都在学习这两本书. 毛爷爷写的挺好,我找工作的时候时常翻看,他鼓舞了我的信心. 那本就是工具书,时常翻阅细节.没啥可说的,在一项技术面前,如果没有灵活掌控驾驭它,千万不要说自己会了,承认吧你不会. ### 小结 做技术真的需要不断学习,而且我还认为技术是需要变战斗边学习最好.这边要用那边要学,两者结合才能勤实践,技术才能有所长进. 可以这样直白的说,即便大学你学了一遍C++你好多年不用它==0. 回首面试的时候自己复习了算法复习N遍.发一张图聊表纪念. ![](/assets/images/20221231FinalSummary/2022F13.webp) ## 关于理想 我的理想是造一所解决卡中国脖子技术大学,然而现实是我这理想过于荒谬.但是有一个人,他总能实现我的理想-曹德旺 他创办的福耀科技大学几乎是把我的梦想变成了他的现实.我承认一个连生活温饱都解决不了的人造一所解决卡中国技术脖子的大学的事情是可笑的.他投资100个亿.这妥妥就是按照我的标准设计了大学,哎,我承认自己过于垃圾. 我记得去年我就说许家印花重金造车,我说这有钱人有钱吧他不会花.王思聪有钱也不会花.就解决卡中国脖子这事你花一个100w都比王健林的小目标牛逼多了.不知道这帮人有钱的想法和世界都是啥样的. ![](/assets/images/20221231FinalSummary/2022F14.webp) 总之希望曹德旺的人设不会崩塌,希望他把大学办好,他办不好我来办,在我有生之年一定解决卡中国脖子的34项技术.[34项技术点击这里查看](https://www.sunyazhou.com/2019/12/FinalSummary/) 以目前咱曹大爷的操作,指日可待. 这里我不得不拿出咱曹大爷说的话 ![](/assets/images/20221231FinalSummary/2022F15.webp) 没有消费能力+1 ## 关于好物 #### MagSafe磁吸充电宝 ![](/assets/images/20221231FinalSummary/2022F16.webp) 如果你经常出门,拥有iPhone12 以上机型,我建议买个这玩意,不用繁琐的充电线,直接吸在手机背面无线充电.这玩意出门绝对使用. #### 8位堂游戏手柄 ![](/assets/images/20221231FinalSummary/2022F17.webp) 在业余时间我经常喜欢玩玩益智游戏和原始的街机游戏,有一个模拟器叫做[RetroArch](https://www.retroarch.com/) ![](/assets/images/20221231FinalSummary/2022F18.webp) 这个模拟器可以在各种平台玩,就是配置起来很麻烦,你需要先去西瓜视频或者B站看看配置教程.业余时间玩玩游戏还是很不错的有益于身心健康的事情,当然还是工作和家庭重要,分清主次就好.合理安排时间. #### 稚晖君发明的Quark-N ![](/assets/images/20221231FinalSummary/2022F19.webp) 这玩意是最小的卡片电脑你信吗.这来自于华为的天才少年出品.[稚晖君的B站主页](https://space.bilibili.com/20259914),喜欢的可以点开看看.全是稚晖君的出品,如果我告诉你他开源了全部硬件和原软件包含硬件的设计图纸和电路你也很难造出来你相信吗? 这个卡片电脑能装乌班图linux,非常适合学习. # 总结 这一年过得,前半年很无聊,后半年还好,只是不知道裁员当下的公司到底想裁员到什么时候,希望大家和我一样努力学习自己的技术,把自己的技术玩到出神入化的状态,打破资本裁员的镰刀,构建自己的防空系统,在危险来临之前能有恃无恐,早日实现财富自由. 活的体面不要凉,未来的路还很长. URL: https://sunyazhou.com/2022/12/gradientlayeranimation/index.html.md Published At: 2022-12-07 08:00:00 +0000 # 模仿Q音径向渐变动画 ![](/assets/images/20221207RadialGradientlayer/RadialCenter.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! 先上成品看看效果 ![](/assets/images/20221207RadialGradientlayer/final.gif) # 深入CAGradientLayer 最近开发功能,视觉设计同学对QQ音乐的桌面歌词预览图的流体渐变动画很感兴趣,想让开发这边实现这个效果. ![](/assets/images/20221207RadialGradientlayer/qqmusicanimation1.gif) 仔细观察歌词背景( 若不是我左右眼都是5.0 我第一次看到QQ音乐的效果我以为没动画.幼稚被教育了),会有一个类似柔光的效果像个灯光一样照射并移动.视觉设计同学把这个效果称为`流体过渡动画`. 为了研究这个效果我深入了解了一下`CAGradientLayer`,发现这里面有几个重要的类型和大家介绍一下 `CAGradientLayer`中有一个成员变量叫`type` ``` objc @property(copy) CAGradientLayerType type; //objc中的成员变量 ``` ``` swift open var type: CAGradientLayerType //swift中的成员变量 ``` 这里拿Objc举例 * kCAGradientLayerAxial 这种叫做轴向梯度或者线性渐变 * kCAGradientLayerRadial 这种叫做径向渐变 * kCAGradientLayerConic 这种叫做锥形渐变 #### kCAGradientLayerAxial 这种Linear (Axial) Gradients 如图 ![](/assets/images/20221207RadialGradientlayer/linear.webp) ``` objc // Objective C gradientLayer.type = kCAGradientLayerAxial; gradientLayer.colors = @[ (id)[UIColor colorWithRed: 48.0/255.0 green: 35.0/255.0 blue: 174.0/255.0 alpha: 1.0].CGColor, (id)[UIColor colorWithRed: 200.0/255.0 green: 109.0/255.0 blue: 215.0/255.0 alpha: 1.0].CGColor ]; ``` ``` swift // Swift gradientLayer.type = .axial; gradientLayer.colors = [ UIColor(red: 48.0/255.0, green: 35.0/255.0, blue: 174.0/255.0, alpha: 1.0).cgColor, UIColor(red: 200.0/255.0, green: 109.0/255.0, blue: 215.0/255.0, alpha: 1.0).cgColor ] ``` 为了研究明白这几种类型有啥区别之前我们要复习一下渐变layer的开始点和结束点. ##### Start Point and End Point 这个开始点和结束点可以改变渐变方向. 默认 `startPoint = (0.5, 0)`,`endPoint = (0.5, 1.0)` 如果把这个渐变变成左右横向的可以参考下面图示和代码. 可以参考下图 ![](/assets/images/20221207RadialGradientlayer/corners.webp) ![](/assets/images/20221207RadialGradientlayer/LinearHorizontal.webp) 示例代码如下: ``` objc // Objective C // Set type (Axial is already the default value) gradientLayer.type = kCAGradientLayerAxial; // Set the colors (these need to be CGColor's, not UIColor's) gradientLayer.colors = @[ (id)[UIColor colorWithRed: 48.0/255.0 green: 35.0/255.0 blue: 174.0/255.0 alpha: 1.0].CGColor, (id)[UIColor colorWithRed: 200.0/255.0 green: 109.0/255.0 blue: 215.0/255.0 alpha: 1.0].CGColor ]; // Set the start and end points gradientLayer.startPoint = CGPointMake(0, 0); gradientLayer.endPoint = CGPointMake(1, 0); ``` ``` swift // Swift // Set type (Axial is already the default value) gradientLayer.type = CAGradientLayerType.axial // Set the colors (these need to be CGColor's, not UIColor's) gradientLayer.colors = [ UIColor(red: 48.0/255.0, green: 35.0/255.0, blue: 174.0/255.0, alpha: 1.0).cgColor, UIColor(red: 200.0/255.0, green: 109.0/255.0, blue: 215.0/255.0, alpha: 1.0).cgColor ] // Set the start and end points gradientLayer.startPoint = CGPoint(x: 0, y: 0) gradientLayer.endPoint = CGPoint(x: 1, y: 0) ``` ##### 多颜色和位置控制 研究明白渐变方向先别着急,我们需要了解一下多个颜色的控制和渐变段的位置是如何设置的. gradientLayer的成员变量`colors`是个数组,可以接收多个颜色值.通常我们使用2个颜色做渐变,如果复杂的话可以设置多个 下面代码示例多种颜色渐变和响应的代码 ![](/assets/images/20221207RadialGradientlayer/rainbow.webp) ``` objc gradientLayer.colors = @[ (id)[UIColor blueColor].CGColor, (id)[UIColor orangeColor].CGColor, (id)[UIColor greenColor].CGColor, (id)[UIColor redColor].CGColor, (id)[UIColor purpleColor].CGColor ]; ``` 下图是演示关键渐变的位置设置 ![](/assets/images/20221207RadialGradientlayer/locations.webp) 位置设置代码如下 ``` objc // ObjC gradientLayer.locations = @[ @0, // blueColor @0.1, // orangeColor @0.6, // greenColor @0.7, // redColor @1 // purpleColor ]; ``` ``` swift // Swift gradientLayer.locations = [ 0, // blueColor 0.1, // orangeColor 0.6, // greenColor 0.7, // redColor 1 // purpleColor ] ``` 可以简单理解为`locations`实际控制的是渐变的位置大小,相对于前后的距离(也可能是各种方向). #### Radial Gradients径向渐变 明白了颜色和位置 下面我们看看 什么是径向渐变? 当我们使用`kCAGradientLayerRadial `类型的时候 我们需要关注一下径向渐变需要的开始点和结束点. 下图演示的是一个椭圆的渐变layer.当然可以设置圆形. ![](/assets/images/20221207RadialGradientlayer/RadialCenter.webp) ``` objc // Objective C // Set the type gradientLayer.type = kCAGradientLayerRadial; gradientLayer.colors = @[ (id)[UIColor colorWithRed: 0.0/255.0 green: 101.0/255.0 blue: 255.0/255.0 alpha: 1.0].CGColor, (id)[UIColor colorWithRed: 0.0/255.0 green: 40.0/255.0 blue: 101.0/255.0 alpha: 1.0].CGColor ]; // Start in the center gradientLayer.startPoint = CGPointMake(0.5, 0.5); // End at the outer edge of the view gradientLayer.endPoint = CGPointMake(0, 0.75); ``` ``` swift // Swift // Set type to radial gradientLayer.type = CAGradientLayerType.radial // Set the colors gradientLayer.colors = [ UIColor(red: 0.0/255.0, green: 101.0/255.0, blue: 255.0/255.0, alpha: 1.0).cgColor, UIColor(red: 0.0/255.0, green: 40.0/255.0, blue: 101.0/255.0, alpha: 1.0).cgColor ] // Start point of first color in the middle of the view gradientLayer.startPoint = CGPoint(x: 0.5, y: 0.5) // End points to the edges of the view gradientLayer.endPoint = CGPoint(x: 0, y: 0.75) ``` #### 锥形渐变kCAGradientLayerConic > 锥形渐变仅支持`@available(iOS 12.0, *)` ![](/assets/images/20221207RadialGradientlayer/conic.webp) 注意观察开始点和结束点位置 ``` objc // Objective C gradientLayer.type = kCAGradientLayerConic; // Set the colors gradientLayer.colors = @[ (id)[UIColor blueColor].CGColor, (id)[UIColor colorWithRed: 50.0/255.0 green: 251.0/255.0 blue: 255.0/255.0 alpha: 1.0].CGColor, (id)[UIColor blackColor].CGColor ]; // Start point of first color in the middle of the view gradientLayer.startPoint = CGPointMake(0.5, 0.5); // End points to the edges of the view gradientLayer.endPoint = CGPointMake(0.5, 0); ``` ``` swift // Swift gradientLayer.type = CAGradientLayerType.conic gradientLayer.colors = [ UIColor.blue, UIColor(red: 50.0/255.0, green: 251.0/255.0, blue: 255.0/255.0, alpha: 1.0).cgColor, UIColor.black ] gradientLayer.startPoint = CGPoint(x: 0.5, y: 0.5) gradientLayer.endPoint = CGPoint(x: 0.5, y: 0) ``` ![](/assets/images/20221207RadialGradientlayer/finaldemo.webp) #### 实现Q音效果的思路 我们先观察一下QQ音乐的效果 ![](/assets/images/20221207RadialGradientlayer/qqmusicanimation1.gif) 我们的思路 ![](/assets/images/20221207RadialGradientlayer/qqmusicanimation2.gif) * 创建一个径向渐变图层 * 放在视图外部通过加`CABasicAnimation`实现`position.x`从右向左移动动画 * 注意颜色配置 * 移动结束为止一定要在屏幕外部 下面看下 实现思路示意图 ![](/assets/images/20221207RadialGradientlayer/qqmusicanimation3.gif) 代码如下 ``` swift var backgroundView: UIView! var gradientLayer: CAGradientLayer! ... override func viewDidLoad() { super.viewDidLoad() self.backgroundView = UIView(frame: .zero) let bgColor = UIColor(red: 231.0/255, green: 223.0/255, blue: 239.0/255, alpha: 1) //要想过渡自然必须保证背景颜色和渐变主颜色一致 self.backgroundView.backgroundColor = bgColor self.view.addSubview(self.backgroundView) self.backgroundView.snp.makeConstraints { make in make.centerX.equalTo(self.view) make.centerY.equalTo(self.view) make.size.equalTo(CGSize(width: 360, height: 70)) } //径向渐变layer self.gradientLayer = CAGradientLayer() self.gradientLayer.frame = CGRect(x: 360 * 1.15, y: -70, width: 360 * 1.15, height: 70 * 2) self.gradientLayer.contentsScale = UIScreen.main.scale self.gradientLayer.startPoint = CGPoint(x: 0.5, y: 0.5) self.gradientLayer.endPoint = CGPoint(x: 0, y: 1) self.gradientLayer.type = .radial self.gradientLayer.locations = [0.25, 1] self.gradientLayer.colors = [UIColor(red: 203.0/255, green: 190.0/255, blue: 224.0/255, alpha: 1).cgColor, bgColor.cgColor] self.backgroundView.layer.addSublayer(self.gradientLayer) self.backgroundView.layer.cornerRadius = 5 self.backgroundView.layer.maskedCorners = [.layerMinXMinYCorner,.layerMinXMaxYCorner,.layerMaxXMinYCorner,.layerMaxXMaxYCorner]; self.backgroundView.layer.masksToBounds = true } ``` 添加动画效果 ``` swift private func addPositionAnimation () { if ((self.gradientLayer.animationKeys()?.contains("kAnimationKey")) != nil) { return; } let width = CGRectGetWidth(self.backgroundView.frame) let gradientWidth = CGRectGetWidth(self.gradientLayer.frame) let locationAniamtion: CABasicAnimation = CABasicAnimation(keyPath: "position.x") locationAniamtion.fromValue = gradientWidth + self.gradientLayer.anchorPoint.x * width locationAniamtion.toValue = -gradientWidth locationAniamtion.duration = 7 locationAniamtion.repeatCount = Float.infinity locationAniamtion.fillMode = .forwards; self.gradientLayer.add(locationAniamtion, forKey: "kAnimationKey") } ``` 实现完成不超过80行代码,去掉无用冗余代码也就40行代码 ##### 遇到问题 * 1.颜色不同很奇怪 * 2.超出范围后要截掉 ##### 解决问题1颜色不同很奇怪 ``` swift let bgColor = UIColor(red: 231.0/255, green: 223.0/255, blue: 239.0/255, alpha: 1) //要想过渡自然必须保证背景颜色和渐变主颜色一致 self.backgroundView.backgroundColor = bgColor ... self.gradientLayer.colors = [UIColor(red: 203.0/255, green: 190.0/255, blue: 224.0/255, alpha: 1).cgColor, bgColor.cgColor] ``` ##### 解决问题2超出范围后要截掉 ``` swift self.backgroundView.layer.cornerRadius = 5 //设置倒角半径 self.backgroundView.layer.maskedCorners = [.layerMinXMinYCorner,.layerMinXMaxYCorner,.layerMaxXMinYCorner,.layerMaxXMaxYCorner]; //设置圆角方向 self.backgroundView.layer.masksToBounds = true //超出屏幕截掉 ``` 这里我用了一个iOS11以后的api `maskedCorners`可以导不同方向的角.很多人都会有疑问,设置倒角半径和`masksToBounds` 容易触发离屏渲染,导致带来额外的开销. 如果你有这个疑问请参考一下我的[UIView不同方向的导角](https://www.sunyazhou.com/2018/05/HowToCreateTopBottomRoundedCornersForViews/) 下面看下做完的效果 ![](/assets/images/20221207RadialGradientlayer/final.gif) ## 总结 首先声明这里不是为了炫耀技术,这个东西也没有多少技术含量,只是缺少我们不断探究技术的精神,在这里例子中我们学到如何利用径向渐变实现2D下类似流体灯光的效果.这个过程中希望大家能学到有用的技术知识,好了文章就写到这里,Demo和参考我放在下文的链接里面,感兴趣可以看看,感谢观看. [本文Demo](https://github.com/sunyazhou13/RadialGradientDemo) [引用CAGradientLayer Explained](https://ikyle.me/blog/2020/cagradientlayer-explained) [引用Location](https://www.cnblogs.com/YouXianMing/p/3793913.html) URL: https://sunyazhou.com/2022/12/thesunyazhoutheoryii/index.html.md Published At: 2022-12-03 03:32:00 +0000 # 《The Sunyazhou Theory Ⅱ》的诞生 ![](/assets/images/20221203TheSunyazhouTheoryII/thesunyazhoutheoryii.webp) # 前言 由于基础知识薄弱,我所做到的内容仅限于学习和观察到的一些事实,未能上升为理论学说. ## 非著名的《The Sunyazhou Theory II》 未来自动驾驶必须且一定是眼睛(摄像头)+耳朵(雷达超声波或激光)一起用才是最优解,计算机视觉+激光雷达,来实现安全车规级自动驾驶。 计算机视觉最多只能识别物体表面,经过计算结果指纹比对方式才能得知已识别到已知物体密度和质量(识别到未知物体则是盲区),目前还做不到感知物体远近距离的精确到毫米级,即便做到了也是理论级,绝非物理级,而超声波雷达则可以快速感知,达到物理级感知,这才是它存在的合理性。 -- 综上所述,我成立孙亚洲第二理论,未来自动驾驶一定眼睛耳朵一起用。 目前以我肤浅的认知还没出现这个系统,我应该享有`优先命名权`: * `TSTⅡCVLRS` (The Sunyazhou Theory Ⅱ Computer Vision Laser Radar System) 孙亚洲第二理论计算机视觉激光雷达系统. ![](/assets/images/20221203TheSunyazhouTheoryII/laserladar1.webp) > 注: > 计算机视觉是一种图形图像识别技术科学 > 激光雷达是一种发射激光束探测位置的雷达系统 ## 验证理论 未来汽车领域几十年的发展中会逐渐验证 # 总结 有些想法可能稚嫩可笑, 我喜欢行胜于言,用时代发展来验证我此刻的结论是否正确. 附: 目前国内各家造车新势力雷达使用情况 ![](/assets/images/20221203TheSunyazhouTheoryII/laserladar2.webp) [参考自 中国市场教激光雷达公司做生意](https://www.ithome.com/0/658/343.htm) [孙亚洲理论](https://www.sunyazhou.com/2020/02/SunyazhouTheory/) URL: https://sunyazhou.com/2022/11/swiftuipropertywrapper/index.html.md Published At: 2022-11-25 10:45:00 +0000 # SwiftUI属性包装器:State、Binding、ObservableObject、EnvironmentObject ![](/assets/images/20221125SwiftUIPropertyWrapper/swiftUIPropertyWrappers.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! ## 主要内容 本文主要讲述SwiftUI中的属性包装器,这些包装器都是用来数据绑定的,作为视图的唯一真值来源,四种方式在实现功能上有细微差别。最后会进行总结比较 * State * Binding * ObservableObject * EnvironmentObject #### 1.@State SwiftUI管理声明为`state`的存储属性。当值发生变化时,`SwiftUI`会更新视图层次结构中依赖于该值的部分。使用`@State`作为存储在视图层次结构中的给定值的唯一真值来源。 `@State`修饰的属性虽然是存储属性,但是我们可以进行读写操作。 父视图和子视图进行传递该属性只能是值传递。 需要在属性名称前加上一个美元符号$来获得这个值。因为它是投影属性 代码: ``` swift struct ContentView: View { @State private var str: String = "" var body: some View { VStack { TextField("Placeholder", text: $str) Text("\(str)") } } } ``` 说明: 1. 在str上设置了@State修饰,那么我在文本输入框中输入的数据,就会传入到str中 2. 同时str又绑定在文本视图上,所以会将文本输入框输入的文本显示到文本视图上 3. 这就是数据绑定的快捷实现。 > 注意: > * 不要在视图层次结构中实例化视图的位置初始化视图的状态属性,因为这可能与SwiftUI提供的存储管理冲突。 > * 为了避免这种情况,总是将state声明为private,并将其放在视图层次结构中需要访问该值的最高视图中。 > * 然后与任何也需要访问的子视图共享该状态,可以直接用于只读访问,也可以作为读写访问的绑定。 #### 2. Binding `@State`修饰的属性是值传递,因此在父视图和子视图之间传递属性时。子视图针对属性的修改无法传递到父视图上。 `Binding`修饰后会将属性会变为一个引用类型,视图之间的传递从值传递变为了引用传递,将父视图和子视图的属性关联起来。这样子视图针对属性的修改,会传递到父视图上。 需要在属性名称前加上一个美元符号$来获得这个值。因为它是投影属性 下面代码在主视图上添加一个`BtnView`视图,视图上添加一个按钮,按钮点击后修改`isShowText`变量。这里的变量通过传入参数与主视图的`isShowText`进行绑定。绑定到主视图的`isShowText`变量上。主视图的变量用来决定文本视图的隐藏和显示。 示例代码: ``` swift struct BtnView: View { @Binding var isShowText: Bool var body: some View { Button { isShowText.toggle() } label: { Text("点击") } } } struct ContentView: View { @State private var isShowText: Bool = true var body: some View { VStack { if(isShowText) { Text("点击后会被隐藏") } else { Text("点击后会被显示").hidden() } BtnView(isShowText: $isShowText) } } } ``` 说明: 1. 按钮在BtnView视图中,并且通过点击,修改isShowText的值。 2. 将BtnView视图添加到ContentView上作为它的子视图。并且传入isShowText。 3. 此时的传值是指针传递,会将点击后的属性值传递到父视图上。 4. 父视图拿到后也作用在自己的属性,因此他的文本视图会依据该属性而隐藏或显示 5. 如果将`@Binding`改为`@State`,会发现点击后不起作用。这是因为值传递子视图的更改不会反映到父视图上 #### 3.@ObservableObject 对实例进行监听,其用处和@State非常相似,只不过必须是对象,而且这个被监听的对象可以被多个视图使用。需要注意用法 ``` swift class DelayedUpdater: ObservableObject { @Published var value = 0 init() { for i in 1...10 { DispatchQueue.main.asyncAfter(deadline: .now() + Double(i)) { self.value += 1 } } } } struct ContentView: View { @ObservedObject var updater = DelayedUpdater() var body: some View { VStack { Text("\(updater.value)").padding() } } } ``` 说明: 1. 绑定的数据是一个对象。 2. 被修饰的对象,其类必须遵守ObservableObject协议 3. 此时这个类中被@Published修饰的属性都会被绑定 4. 使用@ObservedObject修饰这个对象,绑定这个对象。 5. 被@Published修饰的属性发生改变时,SwiftUI就会进行更新。 6. 这里当value值会随着时间发生改变。所以updater对象也会发生改变。此时文本视图的内容就会不断更新。 #### 4.@EnvironmentObject 在多视图中,为了避免数据的无效传递,可以直接将数据放到环境中,供多个视图进行使用。 ``` swift struct EnvView: View { @EnvironmentObject var updater: DelayedUpdater var body: some View { Text("\(updater.value)") } } struct BtnvView: View { @EnvironmentObject var updater: DelayedUpdater var body: some View { Text("\(updater.value)") } } struct ContentView: View { let updater = DelayedUpdater() var body: some View { VStack { EnvView().environmentObject(updater) BtnvView().environmentObject(updater) } } } ``` 说明: * 给属性添加@EnvironmentObject修改,就将其放到了环境中。 * 其他视图中想要获取该属性,可以通过.environmentObject从环境中获取。 * 可以看到分别将EnvView和BtnvView的属性分别放到了环境中 * 之后我们ContentView视图中获取数据时,可以直接通过环境获取。 * 不需要将数据传递到ContentView,而是直接通过环境获取,这样避免了无效的数据传递,更加高效 * 如果是在多层级视图之间进行传递,会有更明显的效果。 # 总结 * @State将属性和视图进行绑定,是唯一真实数据源。子视图和父视图之间是值传递 * @Binding在子视图和父视图之间是指针传递 * @ObservableObject只能监听对象,并且可以在多个视图中监听 * @EnvironmentObject将数据放到环境中,更适用在多视图中 [参考SwiftUI教程(七)属性包装器](https://juejin.cn/post/7112984613102092325) [SwiftUI教程系列文章汇总](https://juejin.cn/post/7110918270743478279) URL: https://sunyazhou.com/2022/09/howtousensoptions/index.html.md Published At: 2022-09-16 09:02:00 +0000 # NS-OPTIONS的用法 # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! #### 定义: ``` objc typedef NS_OPTIONS(NSUInteger, MyOption) { MyOptionNone = 0, //二进制0000,十进制0 MyOption1 = 1 << 0,//0001,1 MyOption2 = 1 << 1,//0010,2 MyOption3 = 1 << 2,//0100,4 MyOption4 = 1 << 3,//1000,8 }; ``` #### 使用 ``` objc //声明定义枚举变量 MyOption option = MyOption1 | MyOption2;//0001 | 0010 = 0011,3 //检查是否包含某选型 if (option & MyOption3) { //0011 & 0100 = 0000 //包含MyOption3 } else { //不包含MyOption3 } //增加选项 option = option | MyOption4;//0011 | 1000 = 1011, 11 //减少选项 option = option & (~MyOption4);//1011 & (~1000) = 1011 & 0111 = 0011, 3 //除了MyOption2以外都恢复到默认 option = option & MyOption2 // 相当于擦除 MyOption2以外的所有值只保留MyOption2 option &= MyOption2 // 也相当于 option = MyOption2 ``` #### 枚举示例代码片段(可复制使用) ``` objc typedef NS_OPTIONS(NSUInteger, YZOptionsFlag) { YZOptionsFlagNone = 0, //二进制0000,十进制0 YZOptionsFlagNormal = 1 << 0, //0001,1 常规状态 下面以此类推 YZOptionsFlag1 = 1 << 1, //0010,2 YZOptionsFlag2 = 1 << 2, // YZOptionsFlag3 = 1 << 3, // YZOptionsFlag4 = 1 << 4, // YZOptionsFlag5 = 1 << 5, // YZOptionsFlag6 = 1 << 6, // YZOptionsFlag7 = 1 << 7, // // YZOptionsFlag = 1 << 8, // // YZOptionsFlag = 1 << 9, // // // YZOptionsFlag = 0 << 16, // // YZOptionsFlag = 1 << 16, // YZOptionsFlag = 2 << 16, // YZOptionsFlag = 3 << 16, // // YZOptionsFlag = 0 << 20, // // YZOptionsFlag = 1 << 20, // YZOptionsFlag = 2 << 20, // YZOptionsFlag = 3 << 20, // YZOptionsFlag = 4 << 20, // YZOptionsFlag = 5 << 20, // YZOptionsFlag = 6 << 20, // YZOptionsFlag = 7 << 20, // // YZOptionsFlag = 0 << 24, // YZOptionsFlag = 3 << 24, // YZOptionsFlag = 7 << 24, } API_AVAILABLE(ios(4.0)); ``` URL: https://sunyazhou.com/2022/09/fbmemorychecktool/index.html.md Published At: 2022-09-16 02:11:00 +0000 # FB内存检测工具分享 ![](/assets/images/20220916FBMemoryCheckTool/FBMemoryProfiler.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! ## 讨论 如果申请一大块内存没有被release这属于内存泄漏,并不属于内存竞争导致的持有,根本原因是没有正确的release. 在常规流程下(正常 alloc,release或者 malloc free,亦或是 new delete)合理使用内存,造成内存泄漏的最主要原因是资源的竞争造成的相互持有.这是诱因导致 资源没有被正常释放. 此工具擅长解决 相互持有关系不释放内存问题,精确到实例类型和内存地址,并直观看到 找到相关对象. ## 内存检测工具-介绍 * FBMemoryProfiler > FBMemoryProfiler 是几个组件的结合。其中包括 FBAllocationTracker 和 FBRetainCycleDetector。 可视化工具,直接嵌入到 App 中,可以起到在 App 中直接查看内存使用情况,并筛选潜在泄漏对象的作用 * FBAllocationTracker > 主要用于快速检测潜在的内存泄漏对象,并提供给 FBRetainCycleDetector 进行检测 这是一个用来主动追踪所有 NSObject 的子类的内存分配和释放操作的工具。 > FBAllocationTracker 用于检测应用在运行时所有实例的分配。它的原理其实就是用 method swizzling 替换原本的 alloc 方法。这样就可以记录下所有的实例分配了。 > 在需要的时候调用 currentAllocationSummary 方法,就可以得到当前整体的实例分配情况(前提是在 main 中初始化过,下面有介绍): ``` objc NSArray *summaries = [[FBAllocationTrackerManager sharedManager] currentAllocationSummary]; ``` * FBRetainCycleDetector > FBRetainCycleDetector 接受一个运行时的实例,然后从这个实例开始遍历它所有的属性,逐级递归。 如果发现遍历到重复的实例,就说明存在循环引用,并给出报告。 ``` objc FBRetainCycleDetector *detector = [FBRetainCycleDetector new]; [detector addCandidate:myObject]; NSSet *retainCycles = [detector findRetainCycles]; NSLog(@"%@", retainCycles); ``` ## 代码检测循环引用-原理 在运行时中检测对象 的内存布局,实例地址 ``` objc const char *class_getIvarLayout(Class cls); const char *class_getWeakIvarLayout(Class cls); ``` > support for Objective-C++ 代码中使用 ``` objc FBRetainCycleDetector *detector = [[FBRetainCycleDetector alloc] initWithConfiguration:nil]; [detector addCandiate:myObject]; NSSet *> *retainCycles = [detector findRetainCycles]; NSLog(@"%@", retainCycles); ``` > 这里的 `myObject `,就是我们所怀疑的实例变量 `FBObjectiveCGraphElement` 是所有用来查找对象类型的基类。所有的查找对象都基于它实现。该类并不需要外部的调用,主要是供内部查询使用。其提供的功能主要是: * 提供初始化方法封装`object`(即调用`addCandiate`传入的`object`) * 获取所有该对象所持有对象`- (NSSet *)allRetainedObjects;`。 基类`FBObjectiveCGraphElement`所获取的对象类型是通过`associated object`所持有的对象。 `associated object`对象的获取是通过`Facebook`自身的`fishhook`去`hook`原先的`objc_setAssociatedObject`和`objc_removeAssociatedObjects`来实现对象的持有标记。 * 提供过滤接口`- (NSSet *)filterObjects:(nullable NSArray *)objects`;,过滤接口主要是与`FBObjectGraphConfiguration`相结合使用,`FBObjectGraphConfiguration`会在下文介绍。 `FBObjectGraphConfiguration ` 是提供过滤相关白名单的类,相关的配置 其余的不就在这里过多介绍了. #### findRetainCycles查询方式 - DFS深度优先 这里查找对象的方式用的是深度优先遍历搜索 ![](/assets/images/20220916FBMemoryCheckTool/retainCycle.gif) ## 如何使用 ![](/assets/images/20220916FBMemoryCheckTool/retainCycle1.gif) 示例场景分析 ![](/assets/images/20220916FBMemoryCheckTool/retainCycle2.webp) 示例代码 ``` objc @property (nonatomic, strong) NSTimer *timer; @property(copy,nonatomic)NSString *name; self.timer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(handleTimer) userInfo:nil repeats:YES]; - (void)handleTimer { self.name = @"123"; } ``` 参考 [FBRetainCycleDetector分析](https://www.jianshu.com/p/bdce04214cf3) [automatic-memory-leak-detection-on-ios](https://engineering.fb.com/2016/04/13/ios/automatic-memory-leak-detection-on-ios/) URL: https://sunyazhou.com/2022/08/nsstringenum/index.html.md Published At: 2022-08-22 11:23:00 +0000 # iOS开发中的字符串枚举 # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! # 枚举 在`Objective-C`中并没有一个专门的类型来定义字符串枚举.我们常用的枚举都是整形,并且自动伸长累加+1. ``` objc typedef NS_ENUM(NSInteger, YZAnimationType) { YZAnimationTypeDefault = 0, YZAnimationType1 = 1, YZAnimationType2 = 2, YZAnimationType3 = 3, YZAnimationType4 = 4, YZAnimationTypeCount, }; ``` 在C++中有专门的枚举类,但是在iOS的objc中并没有C++中的专门枚举类,今天就来学习一个常量字符串字面量定义的枚举类型 我们新建一个class, 就叫`YZEnumConst.h`,然后 写上以下代码 ``` objc #import typedef NSString * const kComponentMessage NS_STRING_ENUM; FOUNDATION_EXPORT kComponentMessage const kComponentMessageXXXXX; ``` 在`YZEnumConst.m`中 写上 ``` objc kComponentMessage const kComponentMessageXXXXX = @"ComponentMessageXXXXX"; ``` 这样通过类型别名的形式就构成了 objc中的字符串枚举类型. > 注意: 声明必须在`.h`中,实现必须在`.m`中,这样才不会造成找不到符号编译报错. 这里大家会注意到有个关键字 `NS_STRING_ENUM `和`FOUNDATION_EXPORT` * `NS_STRING_ENUM `代表 类型 专用于枚举字符串 * `FOUNDATION_EXPORT`代表 对外暴漏声明 的字符串常量. 结合以上使用规则我们可以参考一下苹果内部的定义,例如动画常用的差时器常量. ``` objc typedef NSString * CAMediaTimingFunctionName NS_TYPED_ENUM; CA_EXTERN CAMediaTimingFunctionName const kCAMediaTimingFunctionLinear API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)); CA_EXTERN CAMediaTimingFunctionName const kCAMediaTimingFunctionEaseIn API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)); CA_EXTERN CAMediaTimingFunctionName const kCAMediaTimingFunctionEaseOut API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)); CA_EXTERN CAMediaTimingFunctionName const kCAMediaTimingFunctionEaseInEaseOut API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)); CA_EXTERN CAMediaTimingFunctionName const kCAMediaTimingFunctionDefault API_AVAILABLE(macos(10.6), ios(3.0), watchos(2.0), tvos(9.0)); ``` `CA_EXTERN `定义在``中 ``` objc #ifndef CA_EXTERN # define CA_EXTERN extern __attribute__((visibility("default"))) #endif ``` 这里的实现是参照苹果的差时器字符串枚举实现的. # 总结 开发过程中记录一些有价值的知识点,当使用时才能更加快速的完成工作,提高工作效率. [参考 初探 `NS_STRING_ENUM`](https://juejin.cn/post/6844903638226173966) URL: https://sunyazhou.com/2022/08/checkcellscrolloutscreen/index.html.md Published At: 2022-08-01 14:08:00 +0000 # 检查Cell是否滚动出屏幕之外 # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! ## 如何检测一个cell滑动到屏幕以外? ``` objc //判断cell视图是否在屏幕上.不在的话停止播放 - (void)scrollViewDidScroll:(UIScrollView *)scrollView{ if (_currentPlayIndexPath) { CGRect cellR = [self.tableViewrectForRowAtIndexPath:_currentPlayIndexPath]; if(scrollView.contentOffset.y > cellR.origin.y + cellR.size.height || scrollView.contentOffset.y < cellR.origin.y - scrollView.frame.size.height){ _currentPlayIndexPath = nil; //做一些 滚动出屏幕以外的逻辑代码 } NSLog(@"-------:%@",NSStringFromCGPoint(scrollView.contentOffset)); } } ``` URL: https://sunyazhou.com/2022/07/iosinterviewproblems/index.html.md Published At: 2022-07-14 00:58:00 +0000 # iOS面试问题记录 # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! ## NSObject Category问题 如果一个给NSObject对象扩展Category, 下面的代码能调用吗? ``` objc #import @interface NSObject (Test) + (void)test; @end @implementation NSObject (Test) - (void)test { NSLog(@"11111"); } @end ``` 调用代码 ``` objc - (void)viewDidLoad { [super viewDidLoad]; [NSObject test]; } ``` 打印结果 ``` sh 2022-07-14 09:03:41.039406+0800 UIViewTest[1700:16744] 11111 ``` #### 答案 因为对象的`isa`指针最终指向元类,元类也就是自己的实例,所以会调用到实例方法。 ## Block问题 下面打印结果是什么样的,地址指针谁和谁相同. ``` objc __block int a = 10; NSLog(@"begin %d, %p",a,&a); dispatch_async(dispatch_get_main_queue(), ^{ NSLog(@"in block %d, %p",a,&a); }); a = 20; NSLog(@"end %d, %p",a,&a); ``` 结果 ``` sh 2022-07-14 09:09:04.562941+0800 UIViewTest[2035:23455] begin 10, 0x30998ff08 2022-07-14 09:09:04.563160+0800 UIViewTest[2035:23455] end 20, 0x600003973538 2022-07-14 09:09:04.652175+0800 UIViewTest[2035:23455] in block 20, 0x600003973538 ``` ## 线程问题 ``` objc - (void)viewDidLoad { [super viewDidLoad]; dispatch_queue_t queue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL); dispatch_async(queue, ^{ sleep(5); NSLog(@"1"); }); dispatch_sync(queue, ^{ sleep(3); NSLog(@"2"); }); sleep(1); NSLog(@"3"); } ``` 打印结果 ``` sh 2022-07-14 09:12:46.798861+0800 UIViewTest[2179:26391] 1 2022-07-14 09:12:49.801871+0800 UIViewTest[2179:26296] 2 2022-07-14 09:12:50.804045+0800 UIViewTest[2179:26296] 3 ``` 解答 同步任务的顺序会依据dispatch_sync()函数所在的代码行属于那个线程.如果是主线程则会阻塞主线程等待任务执行完成.所以会打印 1和2. 最后打印 3 因为 前面的代码块被同步队列中的任务所阻塞. # 总结 开发中的细节问题要善于研其根究其本, 善于学习和积累是成为一个优秀开发者的必要条件. URL: https://sunyazhou.com/2022/07/iosoom/index.html.md Published At: 2022-07-11 03:04:00 +0000 # iOS中的OOM ![](/assets/images/20220711iOSCrashType/kernel.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! ## OOM 在iOS开发中,可能会经常看到app用着用着就崩溃了,而在后台查看崩溃栈的时候,找不到崩溃日志。其实这大多数的可能是系统产生了低内存崩溃,也就是`OOM`(还有一种可能是主线程卡死,导致`watchdog`杀掉了应用),而低内存崩溃的日志,往往都是以`JetsamEvent`开头的,日志中有内存页大小(`pageSize`),CPU时间(`cpuTime`)等字段。 #### 什么是OOM? 什么是`OOM`呢,它是`out-of-memory`的缩写,字面意思就是内存超过了限制。它是由于 `iOS` 的`Jetsam`机制造成的一种`另类`Crash,它不同于常规的`Crash`,通过`Signal`捕获等Crash监控方案无法捕获到`OOM`事件。 当然还会有`FOOM`这样的词,代表的是`Foreground-out-of-memory`,是指`App`在前台因消耗内存过多引起系统强杀。这也就是本文要讨论的。后台出现`OOM`不一定都是app本身造成的,大多数是因为当前在前台的App占用内存过大,系统为了保证前台应用正常运行,把后台应用清理掉了。 #### 什么是Jetsam机制? `Jetsam`机制可以理解为操作系统为了控制内存资源过度使用而采用的一种管理机制。`Jetsam`是一个独立运行的进程,每一个进程都有一个内存阈值,一旦超过这个阈值Jetsam就会立刻杀掉这个进程。 #### 为什么要设计Jetsam机制? 首先设备的内存是有限制的,并不是无限大的,所以内存资源非常重要。系统进程及用户使用的其他`app`的进程都会争抢这个资源。由于`iOS`不支持交换空间,一旦触发低内存事件,`Jetsam`就会尽可能多的释放应用占用的内存,这样在`iOS`系统上出现系统内存不足时,应用就会被系统终止。 #### 空间交换 物理内存不够使用该怎么办呢?像一些桌面操作系统,会有内存`交换空间`,在window上称为`虚拟内存`。它的机制是,在需要时能将物理内存中的一部分交换到硬盘上去,利用硬盘空间扩展内存空间。 #### iOS不支持交换空间 但iOS并不支持`交换空间`,大多数移动设备都不支持`交换空间`。移动设备的大容量存储器通常是闪存,它的读写速度远远小于电脑所使用的硬盘,这就导致在移动设备上就算使用了`交换空间`,也并不能提升性能。其次,移动设备的容量本身就经常短缺、内存的读写寿命也有限,所以在这种情况下还拿闪存来做内存交换,就有点奢侈了。 需要注意的是,网上有少出文章说iOS没有虚拟内存机制,实际上指的是iOS没有`交换空间机制`。 #### 典型app内存类型 当内存不足的时候,系统会按照一定策略来腾出更多空间供使用,比较常见的做法是将一部分低优先级的数据挪到磁盘上,这个操作称为`Page Out`。之后当再次访问到这块数据的时候,系统会负责将它重新搬回内存空间中,这个操作称为`Page In`。 #### Clean Memory `Clean Memory`是指那些可以用以`Page Out`的内存,只读的内存映射文件,或者是App所用到的`frameworks`。每个`frameworks`都有`_DATA_CONST`段,通常他们都是`Clean`的,但如果用runtime进行swizzling,那么他们就会变`Dirty`。 #### Dirty Memory `Dirty Memory`是指那些被App写入过数据的内存,包括所有堆区的对象、图像解码缓冲区,同时,类似`Clean memory`,也包括App所用到的`frameworks`。每个`framework`都会有`_DATA`段和`_DATA_DIRTY`段,它们的内存是`Dirty`的。 值得注意的是,在使用`framework`的过程中会产生`Dirty Memory`,使用单例或者全局初始化方法是减少`Dirty Memory`不错的方法,因为单例一旦创建就不会销毁,全局初始化方法会在类加载时执行。 #### Compressed Memory 由于闪存容量和读写寿命的限制,iOS 上没有`交换空间`机制,取而代之使用`Compressed memory`。 `Compressed memory`是在内存紧张时能够将最近使用过的内存占用压缩至原有大小的一半以下,并且能够在需要时解压复用。它在节省内存的同时提高了系统的响应速度,特点总结起来如下: * Shrinks memory usage 减少了不活跃内存占用 * Improves power efficiency 改善电源效率,通过压缩减少磁盘IO带来的损耗 * Minimizes CPU usage 压缩/解压十分迅速,能够尽可能减少 CPU 的时间开销 * Is multicore aware 支持多核操作 例如,当我们使用`Dictionary`去缓存数据的时候,假设现在已经使用了3页内存,当不访问的时候可能会被压缩为1页,再次使用到时候又会解压成3页。 > 本质上,`Compressed memory`也是`Dirty memory`。 因此, `memory footprint ` = `dirty size` + `compressed size`,这也就是我们需要并且能够尝试去减少的内存占用。 #### Memory Warning 相信对于`MemoryWarning`并不陌生,每一个`UIViewController`都会有一个`didReceivedMemoryWarning`的方法。 当使用的内存是一点点上涨时,而不是一下子直接把内存撑爆。在达到内存临界点之前,系统会给各个正在运行的应用发出内存警告,告知app去清理自己的内存。而内存警告,并不总是由于自身app导致的。 内存压缩技术使得释放内存变得复杂。内存压缩技术在操作系统层面实现,对进程无感知。有趣的是如果当前进程收到了内存警告,进程这时候准备释放大量的误用内存,如果访问到过多的压缩内存,再解压缩内存的时候反而会导致内存压力更大,然后出现`OOM`,被系统杀掉。 > 我们对数据进行缓存的目的是想减少 CPU 的压力,但是过多的缓存又会占用过大的内存。在一些需要缓存数据的场景下,可以考虑使用`NSCache`代替`NSDictionary`,`NSCache`分配的内存实际上是`Purgeable Memory`,可以由系统自动释放。这点在`Effective Objective 2.0`一书中也有推荐`NSCache`与`NSPureableData`的结合使用既能让系统根据情况回收内存,也可以在内存清理的同时移除相关对象。 ##### **出现OOM前一定会出现Memory Warning么**? 答案是不一定,有可能瞬间申请了大量内存,而恰好此时主线程在忙于其他事情,导致可能没有经历过`Memory Warning`就发生了OOM。当然即便出现了多次`Memory Warning`后,也不见得会在最后一次`Memory Warning`的几秒钟后出现`OOM`。之前做`extension`开发的时候,就经常会出现`Memory Warnning`,但是不会出现`OOM`,再操作一两分钟后,才出现`OOM`,而在这一两分钟内,没有再出现过`Memory Warning`。 当然在内存警告时,处理内存,可以在一定程度上避免出现`OOM`。 #### 如何确定OOM的阈值? 不同设备OOM的阈值是不同的。那我们该如何知道OOM的阈值呢? ##### 方法1 当我们的`App`被`Jetsam`机制杀死的时候,在手机中会生成系统日志,在手机`系统设置`-`隐私`-`分析`中,可以得到`JetSamEvent`开头的日志。这些日志中就可以获取到一些关于`App`的内存信息,例如我当前用的iPhone12,在日志中的前部分看到了`pageSize`,而查找`per-process-limit`一项(并不是所有日志都有,可以找有的),用该项的`rpages * pageSize`即可得到`OOM`的阈值。 ``` json {"bug_type":"298","timestamp":"2022-07-10 01:18:15.51 +0800","os_version":"iPhone OS 15.5 (19F77)","incident_id":"893A5949-F274-434F-938F-96DF562C9486"} { "crashReporterKey" : "1fccb167681199b571738b0f60a42574dace79ac", "kernel" : "Darwin Kernel Version 21.5.0: Thu Apr 21 21:51:27 PDT 2022; root:xnu-8020.122.1~1\/RELEASE_ARM64_T8101", "product" : "iPhone13,2", "incident" : "893A5949-F274-434F-938F-96DF562C9486", "date" : "2022-07-10 01:18:15.51 +0800", "build" : "iPhone OS 15.5 (19F77)", "timeDelta" : 5, "memoryStatus" : { "compressorSize" : 55911, "compressions" : 121377965, "decompressions" : 84151972, "zoneMapCap" : 1394786304, "largestZone" : "APFS_4K_OBJS", "largestZoneSize" : 41566208, "pageSize" : 16384, "uncompressed" : 139405, "zoneMapSize" : 242122752, "memoryPages" : { "active" : 55880, "throttled" : 0, "fileBacked" : 47550, "wired" : 51160, "anonymous" : 63802, "purgeable" : 489, "inactive" : 52901, "free" : 8686, "speculative" : 2571 } }, "largestProcess" : "WeChat", "genCounter" : 0, "processes" : [ { "uuid" : "c5bfd6df-d788-3dd4-a585-3ad5aa26b390", "states" : [ "daemon", "idle" ], "purgeable" : 0, "age" : 111572686953, "fds" : 25, "coalition" : 3457, "rpages" : 84, "priority" : 0, "physicalPages" : { "internal" : [ 3, 68 ] }, "freeze_skip_reason:" : "out-of-budget", "pid" : 85307, "cpuTime" : 0.007986, "name" : "EnforcementService", "lifetimeMax" : 87 } ... ``` 那么当前这个MemoryTest的内存阈值就是pageSize * rpages / 1024 / 1024 = xx MB。 ##### 方法2 通过Xcode进行DEBUG时,当使用的内存超出限制的时候,系统会抛出 EXC_RESOURCE_EXCEPTION 异常。 ##### 方法3 首先,我们可以通过方法得到当前应用程序占用的内存 通过探测系统可用内存的方式 判断 相关代码请各位搜索一下其它网络平台将会比这更全面 ##### 方法4(适用于iOS13系统) iOS13系统`os/proc.h`中提供了新的API,可以查看当前可用内存 ``` objc #import extern size_t os_proc_available_memory(void); + (CGFloat)availableSizeOfMemory { if (@available(iOS 13.0, *)) { return os_proc_available_memory() / 1024.0 / 1024.0; } // ... } ``` #### 源码探究Jetsam的具体实现 iOS/MacOS的内核都是XNU,同时XNU是开源的。我们可以在开源的XNU内核源码中 XNU的内核内层为`Mach`层,`Mach`作为微内核,是仅提供基础服务的一个薄层,如处理器管理和调度及IPC(进程间通信)。`XNU`的第二个主要部分是`BSD`层。我们可以将其看成围绕`mach层`的一个外环,`BSD`为最终用户的应用程序提供变成接口,其职责包括进程管理,文件系统和网络。 内存管理中各种常见的`JetSam`时间也是由`BSD`产生的,所以,我们从`bsd_init`这个函数作为入口,来探究一下原理。 `bsd_init`中基本都是在初始化各种子系统,比如虚拟内存管理等等 ##### BSD初始化`bsd_init` 跟内存相关的包括如下几步: ``` c //1. 初始化BSD内存Zone,这个Zone是基于Mach内核的zone kmeminit(); //2.iOS上独有的特性,内存和进程的休眠的常驻监控线程 #if CONFIG_FREEZE #ifndef CONFIG_MEMORYSTATUS #error "CONFIG_FREEZE defined without matching CONFIG_MEMORYSTATUS" #endif /* Initialise background freezing */ bsd_init_kprintf("calling memorystatus_freeze_init\n"); memorystatus_freeze_init(); #endif //3.iOS独有,JetSAM(即低内存事件的常驻监控线程) #if CONFIG_MEMORYSTATUS /* Initialize kernel memory status notifications */ rticle/details/104004692 ``` 这里面的`memorystatus_freeze_init()`和`memorystatus_init()`两个方法都是调用`kern_memorystatus.c`里面暴露的接口,主要的作用就是从内核中开启两个优先级最高的线程,来监控整个系统的内存情况。 `CONFIG_FREEZE`涉及到的功能,当启用这个宏时,内核会对进程进行冷冻而不是`Kill`。涉及到进程休眠相关的代码,暂时不在本文讨论范围内。 回到`iOS`的`OOM`崩溃话题上,我们只需要关注`memorystatus_init()`方法即可。 #### 知识点介绍 * 内核里面对于所有的进程都有一个优先级的分布,通过一个数组维护,数组的每一项是一个进程的列表。这个数组的大小则是`JETSAM_PRIORITY_MAX + 1`。 ``` c #define MEMSTAT_BUCKET_COUNT (JETSAM_PRIORITY_MAX + 1) typedef struct memstat_bucket { TAILQ_HEAD(, proc) list; // 一个TAILQ_HEAD的双向链表,用来存放这个优先级下面的进程 int count; // 进程的个数 } memstat_bucket_t; memstat_bucket_t memstat_bucket[MEMSTAT_BUCKET_COUNT];//优先级队列(里面包含不同优先级的结构) ``` * 在`kern_memorystatus.h`中,我们可以找到`JETSAM_PRIORITY_MAX`值以及进程优先级相关的定义: ``` c #define JETSAM_PRIORITY_REVISION 2 #define JETSAM_PRIORITY_IDLE_HEAD -2 /* The value -1 is an alias to JETSAM_PRIORITY_DEFAULT */ #define JETSAM_PRIORITY_IDLE 0 #define JETSAM_PRIORITY_IDLE_DEFERRED 1 /* Keeping this around till all xnu_quick_tests can be moved away from it.*/ #define JETSAM_PRIORITY_AGING_BAND1 JETSAM_PRIORITY_IDLE_DEFERRED #define JETSAM_PRIORITY_BACKGROUND_OPPORTUNISTIC 2 #define JETSAM_PRIORITY_AGING_BAND2 JETSAM_PRIORITY_BACKGROUND_OPPORTUNISTIC #define JETSAM_PRIORITY_BACKGROUND 3 #define JETSAM_PRIORITY_ELEVATED_INACTIVE JETSAM_PRIORITY_BACKGROUND #define JETSAM_PRIORITY_MAIL 4 #define JETSAM_PRIORITY_PHONE 5 #define JETSAM_PRIORITY_UI_SUPPORT 8 #define JETSAM_PRIORITY_FOREGROUND_SUPPORT 9 #define JETSAM_PRIORITY_FOREGROUND 10 #define JETSAM_PRIORITY_AUDIO_AND_ACCESSORY 12 #define JETSAM_PRIORITY_CONDUCTOR 13 #define JETSAM_PRIORITY_HOME 16 #define JETSAM_PRIORITY_EXECUTIVE 17 #define JETSAM_PRIORITY_IMPORTANT 18 #define JETSAM_PRIORITY_CRITICAL 19 #define JETSAM_PRIORITY_MAX 21 /* TODO - tune. This should probably be lower priority */ #define JETSAM_PRIORITY_DEFAULT 18 #define JETSAM_PRIORITY_TELEPHONY 19 ``` 其中数值越大,优先级越高。后台应用程序优先级`JETSAM_PRIORITY_BACKGROUND`是`3`,低于前台应用程序优先级`JETSAM_PRIORITY_FOREGROUND` 10,而`SpringBoard`(桌面程序)位于`JETSAM_PRIORITY_HOME` 16。 * JetSam出现的原因 ``` c #define JETSAM_REASON_INVALID 0 #define JETSAM_REASON_GENERIC 1 #define JETSAM_REASON_MEMORY_HIGHWATER 2 #define JETSAM_REASON_VNODE 3 #define JETSAM_REASON_MEMORY_VMPAGESHORTAGE 4 #define JETSAM_REASON_MEMORY_PROCTHRASHING 5 #define JETSAM_REASON_MEMORY_FCTHRASHING 6 #define JETSAM_REASON_MEMORY_PERPROCESSLIMIT 7 #define JETSAM_REASON_MEMORY_DISK_SPACE_SHORTAGE 8 #define JETSAM_REASON_MEMORY_IDLE_EXIT 9 #define JETSAM_REASON_ZONE_MAP_EXHAUSTION 10 #define JETSAM_REASON_MEMORY_VMCOMPRESSOR_THRASHING 11 #define JETSAM_REASON_MEMORY_VMCOMPRESSOR_SPACE_SHORTAGE 12 ``` ##### 源码逻辑流程 1. JetSam线程初始化完毕,从外部接收到内存压力 2. 如果接收到的内存压力是当前物理内存达到限制时,同步触发per-process-limit类型的OOM,退出流程 3. 如果接受到的内存压力是其他类型时,则唤醒`JetSam`线程,判断`kill_under_pressure_cause`值为`kMemorystatusKilledVMThrashing`,`kMemorystatusKilledFCThrashing`,`kMemorystatusKilledZoneMapExhaustion`时,或者当前可用内存`memorystatus_available_pages`小于阈值`memorystatus_available_pages_pressure`时,进入`OOM`逻辑。 4. 遍历优先级最低的每个进程,根据`phys_footprint`,判断当前进程是否高于阈值,如果没有超过阈值的,则据需查找下一个次低优先级的进程,直到找到后,触发`high-water`类型`OOM` 5. 此时先回一个收优先级较低的进程或正常情况下随时可回收的进程,再次走到`4`的判断逻辑 6. 当所有低优先级进程或正常情况下课随时可回收的进程都被杀掉后,如果`memorystatus_available_pages`依然小于阈值,先杀掉后台的进程,每杀掉一个进程,判断一下`memorystatus_available_pages`是否还小于阈值,如果已经小于阈值了,则挂起线程,等待唤醒 7. 当所有后台进程都被杀掉后,调用`memorystatus_kill_top_process_aggressive`,杀掉前台的进程,挂起线程,等待唤醒 8. 如果上面的`memorystatus_kill_top_process_aggressive`没有杀掉任何进程,就通过LRU杀死`Jetsam`队列中的第一个进程,挂起线程,等待唤醒 #### 如何判定发生了OOM facebook和微信的Matrix都是采用的排除法。在Matrix初始化的时候调用checkRebootType`方法,来判定是否发生了OOM,具体流程如下: 1. 如果当前设备正在DEBUG,则直接返回,不继续执行。 2. 上次打开app是否发生了普通的崩溃,如果不是继续执行 3. 上次打开app后,是用户是否主动退出的应用(监听`UIApplicationWillTerminateNotification`消息),如果不是继续执行 4. 上次打开app后,是否调用`exit`相关的函数(通过`atexit`函数监控),如果不是继续执行 5. 上次打开app后,app是否挂起`suspend`或者执行`backgroundFetch`,如果此时没有被看门狗杀死,则是一种`OOM`,Matrix起名叫`Suspend OOM`,如果不是继续执行 6. app的uuid是否变化了,如果不是继续执行 7. 上次打开app后,系统是否升级了,如果不是继续执行 8. 上次打开app后,设备是否重启了,如果不是继续执行 9. 上次打开app时,app是否处于后台,如果是,则触发了`Background OOM`,如果不是继续执行 10. 上次打开app后,app是否处于前台,是否主线程卡死了,如果没有卡死,则说明触发了`Foreground OOM`。 # 总结 平时我们谈论的大部分都是FOOM,因为如果我们的程序在后台,优先级很低,即便我们不占用大量的内存,也可能会由于前台应用程序占用了大量的内存,而把我们在后台的程序杀掉。这是系统的机制,我们没有太多的办法.针对于FOOM,我们需要着重关注`dirty pages`和`IOKit mappings`,当然注意系统做的缓存,例如图片、字体等。针对于OOM问题监控与解决,可以参考[Matrix](https://github.com/Tencent/matrix)和[OOMDetector](https://github.com/Tencent/OOMDetector)两个开源库 [实践方案 快影iOS端如何实现OOM率下降80%+](https://www.gushiciku.cn/pl/aHa7) URL: https://sunyazhou.com/2022/07/ioscrashtype/index.html.md Published At: 2022-07-11 01:53:00 +0000 # iOS中的崩溃类型 ![](/assets/images/20220711iOSCrashType/kernel.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! ## 崩溃类型 崩溃通常是指操作系统向正在运行的程序发送的信号,所以我们在查看崩溃日志时,常常看到如下错误摘要:Application received signal SIGSEGV。一般来说,常见的崩溃类型有以下几种: ### 1.`EXC_BAD_ACCESS` 野指针引起的崩溃,访问了一个已经释放的内存而导致,向已经释放的对象或向它发送消息时,`EXC_BAD_ACCESS`就会出现。造成`EXC_BAD_ACCESS`最常见的原因是,在初始化方法中初始化变量时用错了所有权修饰符,这会导致对象过早地被释放。举个例子,在`viewDidLoad`方法中为`UIViewController`创建了一个包含元素的`NSArray`,却将该数组的所有权修饰符设成了`assign`而不是`strong`。现在在`viewWillAppear`中,若要访问已经释放掉的对象时,就会得到名为`EXC_BAD_ACCESS`的崩溃。 这个崩溃发生时,查看崩溃日志,却往往得不到有用的栈信息。还好,有一个方法用来解决这个问题:`NSZombieEnabled`。 这是一个环境变量,用来调试与内存相关的问题,跟踪对象的释放过程。启用了`NSZombieEnabled`的话,它会用一个僵尸实现来替换你的默认的`dealloc`实现,也就是在引用计数降到0时,该僵尸实现会将该对象转换成僵尸对象。僵尸对象的作用是在你向它发送消息时,它会显示一段日志并自动跳入调试器。 所以,当在应用中启用`NSZombie`而不是让应用直接崩溃时,一个错误的内存访问就会变成一条无法识别的消息发送给僵尸对象。僵尸对象会显示接收到的消息,然后跳入调试器,这样你就可以查看到底哪时出了问题。 可以在`Xcode`的`scheme`页面中设置`NSZombieEnabled`环境变量。点击`Product`的`Edit Scheme`打开该页面,然后勾选`Enable Zombie Objects`复选框,如图所示: ![](/assets/images/20220711iOSCrashType/1.webp) 僵尸在RAC出现以前作用很大。但自从有了ARC,如果你在对象的所有权方面比较注意,那么通常不会碰到内存相关的崩溃。 ### 2.`SIGSEGV` 段错误信息(`SIGSEGV`)是操作系统产生的一个更严重的问题。当硬件出现错误、访问不可读的内存地址或向受保护的内存地址写入数据时,就会发生这个错误。 硬件错误这一情况并不常见。当要读取保存在`RAM`中的数据,而该位置的`RAM`硬件有问题时,你会收到`SIGSEGV`。`SIGSEGV`更多是出现在后两种情况。默认情况下,代码页不允许进行写操作。当应用中的某个指针指向代码页并试图修改指向位置的值时,你会收到`SIGSEGV`。当要读取一个指针的值,而它被初始化成指向无效内存地址的垃圾值时,你也会收到`SIGSEGV`。 `SIGSEGV`错误调试起来更困难,而导致`SIGSEGV`的最常见原因是不正确的类型转换。要避免过度使用指针或尝试手动修改指针来读取私有数据结构。如果你那样做了,而在修改指针时没有注意内存对齐和填充问题,就会收到`SIGSEGV`。 ### 3.`SIGBUS` 总线错误信号(`SIGBUG`)代表无效内存访问,即访问的内存是一个无效的内存地址。也就是说,那个地址指向的位置根本不是物理内存地址(它可能是某个硬件芯片的地址)。 ### 4.`SIGTRAP` `SIGTRAP`代表陷阱信号。它并不是一个真正的崩溃信号。它会在处理器执行trap指令发送。`LLDB`调试器通常会处理此信号,并在指定的断点处停止运行。如果你收到了原因不明的`SIGTRAP`,先清除上次的输出,然后重新进行构建通常能解决这个问题。 ### 5.`EXC_ARITHETIC` 当要除零时,应用会收到`EXC_ARITHMETIC`信号。这个错误应该很容易解决。 ``` objc int result = 10/0; //0不能作为除数 否则crash ``` ### 6.`SIGILL` `SIGILL`代表`signal illegal instruction`(非法指令信号)。当在处理器上执行非法指令时,它就会发生。执行非法指令是指,将函数指针会给另外一个函数时,该函数指针由于某种原因是坏的,指向了一段已经释放的内存或是一个数据段。有时你收到的是`EXC_BAD_INSTRUCTION`而不是`SIGILL`,虽然它们是一回事,不过`EXC_*`等同于此信号不依赖体系结构。 ### 7. `SIGABRT` `SIGABRT`代表`SIGNAL ABORT`(中止信号)。当操作系统发现不安全的情况时,它能够对这种情况进行更多的控制;必要的话,它能要求进程进行清理工作。在调试造成此信号的底层错误时,并没有什么妙招。`Cocos2d`或`UIKit`等框架通常会在特定的前提条件没有满足或一些糟糕的情况出现时调用C函数`abort`(由它来发送此信号)。当`SIGABRT`出现时,控制台通常会输出大量的信息,说明具体哪里出错了。由于它是可控制的崩溃,所以可以在`LLDB`控制台上键入`bt`命令打印出回溯信息。 ### 8.`看门狗超时` 这种崩溃通常比较容易分辨,因为错误码是固定的0x8badf00d。在iOS上,它经常出现在执行一个同步网络调用而阻塞主线程的情况。因此,永远不要进行同步网络调用。 # 总结 crash log类型不限于上述这些,结合实际跟踪将会找到影响的崩溃信息.这里只做参考使用. 如果想了解更加详细请参考gnu的[源码](http://fxr.watson.org/fxr/source/osfmk/mach/exception_types.h?v=xnu-2050.18.24) [参考](https://zhuanlan.zhihu.com/p/269371735) [理解 iOS 异常类型](https://juejin.cn/post/6844903866128039944) [浅谈 iOS 中的 Crash 捕获与防护 ](http://shevakuilin.com/ios-crashprotection/) URL: https://sunyazhou.com/2022/07/commonsuperview/index.html.md Published At: 2022-07-05 00:12:00 +0000 # iOS寻找两个UIView的最近的公共父类 ![](/assets/images/20220701ReverseList/algorithm.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! # 实现代码 ``` objc //调用 - (void)viewDidLoad { [super viewDidLoad]; Class commonClass1 = [self commonClass1:[ViewA class] andClass:[ViewC class]]; NSLog(@"%@",commonClass1); // 输出:2022-07-03 17:36:01.868966+0800 两个UIView的最近公共父类[84288:2458900] ViewD } // 获取所有父类 - (NSArray *)superClasses:(Class)class { if (class == nil) { return @[]; } NSMutableArray *result = [NSMutableArray array]; while (class != nil) { [result addObject:class]; class = [class superclass]; } return [result copy]; } //我们将一个路径中的所有点先放进NSSet中.因为NSSet的内部实现是一个hash表,所以查询元素的时间的复杂度变成O(1),我们一共有N个节点,所以总时间复杂度优化到了O(N) - (Class)commonClass2:(Class)classA andClass:(Class)classB{ NSArray *arr1 = [self superClasses:classA]; NSArray *arr2 = [self superClasses:classB]; NSSet *set = [NSSet setWithArray:arr2]; for (NSUInteger i =0; i &res) { if (!root) {return;} //前序 res.push_back(root->val); preorder(root->left,res); preorder(root->right,res); //中序 inorder(root->left, res); res.push_back(root->val); inorder(root->right,res); //后序 postorder(root->left, res); postorder(root->right, res); res.push_back(root->val); } vector inorderTraversal(TreeNode* root) { vector ans; inorder(root, ans); return ans; } }; ``` [144. 二叉树的前序遍历](https://leetcode.cn/problems/binary-tree-preorder-traversal/) [94. 二叉树的中序遍历](https://leetcode.cn/problems/binary-tree-inorder-traversal/) [145. 二叉树的后序遍历](https://leetcode.cn/problems/binary-tree-postorder-traversal/) [引用自codetop](https://codetop.cc/home) URL: https://sunyazhou.com/2022/07/islandscount/index.html.md Published At: 2022-07-04 09:50:00 +0000 # 岛屿数量 ![](/assets/images/20220701ReverseList/algorithm.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! # 如题 给你一个由 `'1'`(陆地)和 `'0'`(水)组成的的二维网格,请你计算网格中岛屿的数量。 岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。 此外,你可以假设该网格的四条边均被水包围。 #### 示例1 ``` sh 输入:grid = [ ["1","1","1","1","0"], ["1","1","0","1","0"], ["1","1","0","0","0"], ["0","0","0","0","0"] ] 输出:1 ``` #### 示例2 ``` sh 输入:grid = [ ["1","1","0","0","0"], ["1","1","0","0","0"], ["0","0","1","0","0"], ["0","0","0","1","1"] ] 输出:3 ``` ## 实现代码 使用深度优先DFS ``` c++ class Solution { private: void dfs(vector>& grid, int r, int c) { int nr = grid.size(); int nc = grid[0].size(); grid[r][c] = '0'; if (r - 1 >= 0 && grid[r-1][c] == '1') dfs(grid, r - 1, c); if (r + 1 < nr && grid[r+1][c] == '1') dfs(grid, r + 1, c); if (c - 1 >= 0 && grid[r][c-1] == '1') dfs(grid, r, c - 1); if (c + 1 < nc && grid[r][c+1] == '1') dfs(grid, r, c + 1); } public: int numIslands(vector>& grid) { int nr = grid.size(); if (!nr) return 0; int nc = grid[0].size(); int num_islands = 0; for (int r = 0; r < nr; ++r) { for (int c = 0; c < nc; ++c) { if (grid[r][c] == '1') { ++num_islands; dfs(grid, r, c); } } } return num_islands; } }; ``` [200. 岛屿数量](https://leetcode.cn/problems/number-of-islands/) [引用自codetop](https://codetop.cc/home) URL: https://sunyazhou.com/2022/07/longestpalindrome/index.html.md Published At: 2022-07-04 09:41:00 +0000 # 最长回文子串 ![](/assets/images/20220701ReverseList/algorithm.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! # 如题 给你一个字符串`s`,找到`s`中最长的回文子串。 #### 示例1 ``` sh 输入:s = "babad" 输出:"bab" 解释:"aba" 同样是符合题意的答案。 ``` #### 示例2 ``` sh 输入:s = "cbbd" 输出:"bb" ``` ## 实现代码 ``` c++ //中心扩展算法 class Solution { public: pair expandAroundCenter(const string& s, int left, int right) { while (left >= 0 && right < s.size() && s[left] == s[right]) { --left; ++right; } return {left + 1, right - 1}; } string longestPalindrome(string s) { int start = 0, end = 0; for (int i = 0; i < s.size(); ++i) { auto [left1, right1] = expandAroundCenter(s, i, i); auto [left2, right2] = expandAroundCenter(s, i, i + 1); if (right1 - left1 > end - start) { start = left1; end = right1; } if (right2 - left2 > end - start) { start = left2; end = right2; } } return s.substr(start, end - start + 1); } }; ``` [5. 最长回文子串](https://leetcode.cn/problems/longest-palindromic-substring/) [引用自codetop](https://codetop.cc/home) URL: https://sunyazhou.com/2022/07/mergesortarray/index.html.md Published At: 2022-07-04 09:25:00 +0000 # 合并两个有序数组 ![](/assets/images/20220701ReverseList/algorithm.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! # 如题 给你两个按`非递减顺序`排列的整数数组`nums1`和`nums2`,另有两个整数`m`和`n` ,分别表示`nums1`和`nums2`中的元素数目。 请你`合并` `nums2` 到`nums1`中,使合并后的数组同样按`非递减顺序`排列。 > 注意:最终,合并后数组不应由函数返回,而是存储在数组`nums1`中。为了应对这种情况,`nums1`的初始长度为`m + n`,其中前`m`个元素表示应合并的元素,后`n`个元素为 `0`,应忽略。`nums2`的长度为`n`。 #### 示例1 ``` sh 输入:nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3 输出:[1,2,2,3,5,6] 解释:需要合并 [1,2,3] 和 [2,5,6] 。 合并结果是 [1,2,2,3,5,6] ,其中斜体加粗标注的为 nums1 中的元素。 ``` #### 示例2 ``` sh 输入:nums1 = [1], m = 1, nums2 = [], n = 0 输出:[1] 解释:需要合并 [1] 和 [] 。 合并结果是 [1] 。 ``` #### 示例3 ``` sh 输入:nums1 = [0], m = 0, nums2 = [1], n = 1 输出:[1] 解释:需要合并的数组是 [] 和 [1] 。 合并结果是 [1] 。 注意,因为 m = 0 ,所以 nums1 中没有元素。nums1 中仅存的 0 仅仅是为了确保合并结果可以顺利存放到 nums1 中. ``` ## 实现代码 ``` c++ class Solution { public: void merge(vector& nums1, int m, vector& nums2, int n) { for (int i = 0; i != n; ++i) { nums1[m + i] = nums2[i]; } sort(nums1.begin(),nums1.end()); } }; ``` [88. 合并两个有序数组](https://leetcode.cn/problems/merge-sorted-array/) [引用自codetop](https://codetop.cc/home) URL: https://sunyazhou.com/2022/07/commonancestor/index.html.md Published At: 2022-07-04 09:12:00 +0000 # 二叉树的最近公共祖先 ![](/assets/images/20220701ReverseList/algorithm.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! # 如题 给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。 [百度百科](https://baike.baidu.com/item/%E6%9C%80%E8%BF%91%E5%85%AC%E5%85%B1%E7%A5%96%E5%85%88/8918834?fr=aladdin)中最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(`一个节点也可以是它自己的祖先`)。” #### 示例1 ![](/assets/images/20220704CommonAncestor/1.webp) ``` sh 输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1 输出:3 解释:节点 5 和节点 1 的最近公共祖先是节点 3 。 ``` #### 示例2 ![](/assets/images/20220704CommonAncestor/2.webp) ``` sh 输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4 输出:5 解释:节点 5 和节点 4 的最近公共祖先是节点 5 。因为根据定义最近公共祖先节点可以为节点本身。 ``` #### 示例3 ``` sh 输入:root = [1,2], p = 1, q = 2 输出:1 ``` ## 实现代码 ``` c++ struct TreeNode { int val; TreeNode *left; TreeNode *right; TreeNode(int x) : val(x), left(NULL), right(NULL) {} }; class Solution { public: unordered_map father; unordered_map vis; //周游过的 void dfs(TreeNode *root) { if (root->left != nullptr) { father[root->left->val] = root; dfs(root->left); } if (root->right != nullptr) { father[root->right->val] = root; dfs(root->right); } } TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) { father[root->val] = nullptr; dfs(root); while (p != nullptr) { vis[p->val] = true; p = father[p->val]; } while (q != nullptr) { if (vis[q->val]) { return q; } q = father[q->val]; } return nullptr; } }; ``` [236. 二叉树的最近公共祖先](https://leetcode.cn/problems/lowest-common-ancestor-of-a-binary-tree/) [引用自codetop](https://codetop.cc/home) URL: https://sunyazhou.com/2022/07/searchrotatearray/index.html.md Published At: 2022-07-04 06:53:00 +0000 # 搜索旋转排序数组 ![](/assets/images/20220701ReverseList/algorithm.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! # 如题 整数数组`nums`按升序排列,数组中的值 互不相同 。 在传递给函数之前,`nums` 在预先未知的某个下标 `k`(`0 <= k < nums.length`)上进行了 `旋转`,使数组变为 `[nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]]`(下标 `从 0 开始` 计数)。例如, `[0,1,2,4,5,6,7]` 在下标 `3` 处经旋转后可能变为 `[4,5,6,7,0,1,2]` 。 给你 旋转后 的数组 `nums` 和一个整数 `target` ,如果 `nums` 中存在这个目标值 `target` ,则返回它的下标,否则返回 `-1` 。 你必须设计一个时间复杂度为 `O(log n)` 的算法解决此问题。 #### 示例1 ``` sh 输入:nums = [4,5,6,7,0,1,2], target = 0 输出:4 ``` #### 示例2 ``` sh 输入:nums = [4,5,6,7,0,1,2], target = 3 输出:-1 ``` #### 示例3 ``` sh 输入:nums = [1], target = 0 输出:-1 ``` ## Answer ``` c++ class Solution { public: int search(vector& nums, int target) { int n = (int)nums.size(); if (!n) { return -1; } if (n == 1) { return nums[0] == target? 0 : -1; } int left = 0, right = n -1; while (left <= right) { int mid = (right + left) /2; if (nums[mid] == target) { return mid; } if (nums[0] <= nums[mid]) { if (nums[0] <= target && target < nums[mid]) { right = mid -1; } else { left = mid + 1; } } else { if (nums[mid] < target && target <= nums[n-1]) { left = mid +1; } else { right = mid -1; } } } return -1; } }; ``` [33. 搜索旋转排序数组](https://leetcode.cn/problems/search-in-rotated-sorted-array/) [引用自codetop](https://codetop.cc/home) URL: https://sunyazhou.com/2022/07/zigzaglevelorder/index.html.md Published At: 2022-07-04 06:19:00 +0000 # 二叉树的锯齿形层序遍历 ![](/assets/images/20220701ReverseList/algorithm.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! # 如题 给你二叉树的根节点`root`,返回其节点值的`锯齿形层序遍历`。(即先从左往右,再从右往左进行下一层遍历,以此类推,层与层之间交替进行)。 #### 示例1 ![](/assets/images/20220704ZigzagLeveOrder/1.webp) ``` sh 输入:root = [3,9,20,null,null,15,7] 输出:[[3],[20,9],[15,7]] ``` #### 示例2 ``` sh 输入:root = [1] 输出:[[1]] ``` #### 示例3 ``` sh 输入:root = [] 输出:[] ``` ## 实现代码 ``` c++ struct TreeNode { int val; TreeNode *left; TreeNode *right; TreeNode() : val(0), left(nullptr), right(nullptr) {} TreeNode(int x) : val(x), left(nullptr), right(nullptr) {} TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {} }; class Solution { public: vector> zigzagLevelOrder(TreeNode* root) { vector> ans; if (root == nullptr) { return ans; } queue nodeQueue; nodeQueue.push(root); bool isOrderLeft = true; while(!nodeQueue.empty()) { deque levelList; int n = nodeQueue.size(); for (int i = 0; i < n; i++) { auto node = nodeQueue.front(); nodeQueue.pop(); if (isOrderLeft) { levelList.push_back(node->val); } else { levelList.push_front(node->val); } if (node->left) { nodeQueue.push(node->left); } if (node->right) { nodeQueue.push(node->right); } } ans.emplace_back(vector{levelList.begin(),levelList.end()}); isOrderLeft = !isOrderLeft; } return ans; } }; ``` [103. 二叉树的锯齿形层序遍历](https://leetcode.cn/problems/binary-tree-zigzag-level-order-traversal/) [引用自codetop](https://codetop.cc/home) URL: https://sunyazhou.com/2022/07/cyclelinktable/index.html.md Published At: 2022-07-04 06:09:00 +0000 # 环形链表 ![](/assets/images/20220701ReverseList/algorithm.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! # 如题 给你一个链表的头节点`head`,判断链表中是否有环。 如果链表中有某个节点,可以通过连续跟踪`next`指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数`pos`来表示链表尾连接到链表中的位置(索引从 0 开始). > 注意:`pos`不作为参数进行传递 。仅仅是为了标识链表的实际情况。 如果链表中存在环 ,则返回`true`。 否则,返回`false`。 #### 示例1 ![](/assets/images/20220704CycleLinkTable/1.webp) ``` sh 输入:head = [3,2,0,-4], pos = 1 输出:true 解释:链表中有一个环,其尾部连接到第二个节点。 ``` #### 示例2 ![](/assets/images/20220704CycleLinkTable/2.webp) ``` sh 输入:head = [1,2], pos = 0 输出:true 解释:链表中有一个环,其尾部连接到第一个节点。 ``` #### 示例3 ![](/assets/images/20220704CycleLinkTable/3.webp) ``` sh 输入:head = [1], pos = -1 输出:false 解释:链表中没有环。 ``` ## 实现代码 ``` c++ struct ListNode { int val; ListNode *next; ListNode(int x) : val(x), next(NULL) {} }; class Solution { public: bool hasCycle(ListNode *head) { unordered_set seen; while (head != nullptr) { if (seen.count(head)){ return true; } seen.insert(head); head = head->next; } return false; } }; ``` [141. 环形链表](https://leetcode.cn/problems/linked-list-cycle/) [引用自codetop](https://codetop.cc/home) URL: https://sunyazhou.com/2022/07/binarytreelevelorder/index.html.md Published At: 2022-07-04 02:10:00 +0000 # 二叉树的层序遍历 ![](/assets/images/20220701ReverseList/algorithm.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! # 如题 给你二叉树的根节点`root`,返回其节点值的`层序遍历`.(即逐层地,从左到右访问所有节点). #### 示例1 ![](/assets/images/20220704BinaryTreeLevelOrder/1.webp) ``` sh 输入:root = [3,9,20,null,null,15,7] 输出:[[3],[9,20],[15,7]] ``` #### 示例2 ``` sh 输入:root = [1] 输出:[[1]] ``` #### 示例3 ``` sh 输入:root = [] 输出:[] ``` ## 实现代码 ``` c++ struct TreeNode { int val; TreeNode *left; TreeNode *right; TreeNode() : val(0), left(nullptr), right(nullptr) {} TreeNode(int x) : val(x), left(nullptr), right(nullptr) {} TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {} }; class Solution { public: vector> levelOrder(TreeNode* root) { vector> ret; if(root == nullptr) { return ret; } queue q; q.push(root); while(!q.empty()) { int levelSize = q.size(); ret.push_back(vector()); for (int i = 1; i <= levelSize; ++i) { auto node = q.front(); q.pop(); ret.back().push_back(node->val); if (node->left) { q.push(node->left); } if (node->right) { q.push(node->right); } } } return ret; } }; ``` [102. 二叉树的层序遍历](https://leetcode.cn/problems/binary-tree-level-order-traversal/) [引用自codetop](https://codetop.cc/home) URL: https://sunyazhou.com/2022/07/twosum/index.html.md Published At: 2022-07-04 02:03:00 +0000 # 两数之和 ![](/assets/images/20220701ReverseList/algorithm.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! # 如题 给定一个整数数组`nums`和一个整数目标值`target`,请你在该数组中找出`和为目标值` `target`的那`两个`整数,并返回它们的数组下标。 你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。 你可以按任意顺序返回答案。 #### 示例1 ``` sh 输入:nums = [2,7,11,15], target = 9 输出:[0,1] 解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。 ``` #### 示例2 ``` sh 输入:nums = [3,2,4], target = 6 输出:[1,2] ``` #### 示例3 ``` sh 输入:nums = [3,3], target = 6 输出:[0,1] ``` ## Answer ``` c++ class Solution { public: vector twoSum(vector& nums, int target) { int n = nums.size(); for (int i = 0; i < n; ++i) { for (int j = i+1; j < n; ++j) { if (nums[i] + nums[j] == target) { return {i,j}; } } } return {}; } }; ``` [1. 两数之和](https://leetcode.cn/problems/two-sum/) [引用自codetop](https://codetop.cc/home) URL: https://sunyazhou.com/2022/07/mergetwolists/index.html.md Published At: 2022-07-01 07:38:00 +0000 # 合并两个有序链表 ![](/assets/images/20220701ReverseList/algorithm.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! # 如题 将两个升序链表合并为一个新的`升序`链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。 #### 示例1 ![](/assets/images/20220701MergeTwoLists/mergelinklist.webp) ``` sh 输入:l1 = [1,2,4], l2 = [1,3,4] 输出:[1,1,2,3,4,4] ``` #### 示例2 ``` sh 输入:l1 = [], l2 = [] 输出:[] ``` #### 示例3 ``` sh 输入:l1 = [], l2 = [0] 输出:[0] ``` ## Answer ``` c++ struct ListNode { int val; ListNode *next; ListNode() : val(0), next(nullptr) {} ListNode(int x) : val(x), next(nullptr) {} ListNode(int x, ListNode *next) : val(x), next(next) {} }; class Solution { public: ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) { if (list1 == nullptr) { return list2; } else if (list2 == nullptr) { return list1; } else if (list1->val < list2->val) { list1->next = mergeTwoLists(list1->next, list2); return list1; } else { list2->next = mergeTwoLists(list2->next, list1); return list2; } } }; ``` [21. 合并两个有序链表](https://leetcode.cn/problems/merge-two-sorted-lists/) [引用自codetop](https://codetop.cc/home) URL: https://sunyazhou.com/2022/07/maxsubarray/index.html.md Published At: 2022-07-01 07:29:00 +0000 # 最大子数组和 ![](/assets/images/20220701ReverseList/algorithm.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! # 如题 给你一个整数数组`nums`,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。 `子数组`是数组中的一个连续部分。 #### 示例1 ``` sh 输入:nums = [-2,1,-3,4,-1,2,1,-5,4] 输出:6 解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。 ``` #### 示例2 ``` sh 输入:nums = [1] 输出:1 ``` #### 示例3 ``` sh 输入:nums = [5,4,-1,7,8] 输出:23 ``` ## Answer ``` c++ class Solution { public: int maxSubArray(vector& nums) { int pre = 0, maxAns = nums[0]; for (const auto &x: nums) { pre = max( pre + x, x); maxAns = max(maxAns,pre); } return maxAns; } }; ``` [53. 最大子数组和](https://leetcode.cn/problems/maximum-subarray/) [引用自codetop](https://codetop.cc/home) URL: https://sunyazhou.com/2022/07/threesum/index.html.md Published At: 2022-07-01 06:55:00 +0000 # 三数之和 ![](/assets/images/20220701ReverseList/algorithm.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! # 如题 给你一个包含`n`个整数的数组`nums`,判断`nums`中是否存在三个元素`a`,`b`,`c` ,使得 `a + b + c = 0`?请你找出所有和为`0`且不重复的三元组。 > 注意:答案中不可以包含重复的三元组。 #### 示例1 ``` sh 输入:nums = [-1,0,1,2,-1,-4] 输出:[[-1,-1,2],[-1,0,1]] ``` #### 示例2 ``` sh 输入:nums = [] 输出:[] ``` #### 示例3 ``` sh 输入:nums = [0] 输出:[] ``` ## Answer ``` c++ class Solution { public: vector> threeSum(vector& nums) { int n = nums.size(); sort(nums.begin(),nums.end()); vector> ans; for (int first = 0; first < n; ++first) { //check if (first > 0 && nums[first] == nums[first - 1]) { continue; } int third = n -1; int target = -nums[first]; for (int second = first + 1; second < n; ++second) { if (second > first + 1 && nums[second] == nums[second - 1]) { continue; } while (second < third && nums[second] + nums[third] > target) { --third; } if (second == third) { break; } if (nums[second] + nums[third] == target) { ans.push_back({nums[first],nums[second],nums[third]}); } } } return ans; } }; ``` [15. 三数之和](https://leetcode.cn/problems/3sum/) [引用自codetop](https://codetop.cc/home) URL: https://sunyazhou.com/2022/07/kthlargest/index.html.md Published At: 2022-07-01 06:44:00 +0000 # 数组中的第K个最大元素 ![](/assets/images/20220701ReverseList/algorithm.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! # 如题 给定整数数组`nums`和整数`k`,请返回数组中第`k`个最大的元素。 请注意,你需要找的是数组排序后的第`k`个最大的元素,而不是第`k`个不同的元素。 #### 示例1 ``` sh 输入: [3,2,1,5,6,4] 和 k = 2 输出: 5 ``` #### 示例2 ``` sh 输入: [3,2,3,1,2,4,5,5,6] 和 k = 4 输出: 4 ``` ## Answer ``` c++ class Solution { public: void maxHeapify(vector &nums, int i, int heapsize) { int left = i * 2+1, right = i * 2+2, largest = i; if (left < heapsize && nums[left] > nums[largest]) { largest = left; } if (right < heapsize && nums[right] > nums[largest]) { largest = right; } if (largest != i) { swap(nums[i], nums[largest]); maxHeapify(nums, largest, heapsize); } } void buildMaxHeap(vector &nums, int heapsize){ for (int i = heapsize/2; i >= 0; --i) { maxHeapify(nums, i , heapsize); } } //堆化 int findKthLargest(vector nums, int k){ int heapsize = nums.size(); buildMaxHeap(nums, heapsize); for (int i = nums.size() - 1; i >= nums.size() - k + 1; --i) { swap(nums[0],nums[i]); --heapsize; maxHeapify(nums, 0, heapsize); } return nums[0]; } }; ``` [215. 数组中的第K个最大元素](https://leetcode.cn/problems/kth-largest-element-in-an-array/) [引用自codetop](https://codetop.cc/home) URL: https://sunyazhou.com/2022/07/lrucache/index.html.md Published At: 2022-07-01 06:35:00 +0000 # LRU 缓存 ![](/assets/images/20220701ReverseList/algorithm.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! # 如题 请你设计并实现一个满足`LRU(最近最少使用)缓存` 约束的数据结构。 实现`LRUCache`类: * `LRUCache(int capacity)`以 `正整数` 作为容量`capacity`初始化LRU缓存 * `int get(int key)` 如果关键字 `key` 存在于缓存中,则返回关键字的值,否则返回 `-1`。 * `void put(int key, int value)`如果关键字`key`已经存在,则变更其数据值`value`;如果不存在,则向缓存中插入该组`key-value`。如果插入操作导致关键字数量超过`capacity`,则应该`逐出`最久未使用的关键字。 函数`get`和`put`必须以`O(1)`的平均时间复杂度运行。 #### 示例 ``` sh 输入 ["LRUCache", "put", "put", "get", "put", "get", "put", "get", "get", "get"] [[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]] 输出 [null, null, null, 1, null, -1, null, -1, 3, 4] 解释 LRUCache lRUCache = new LRUCache(2); lRUCache.put(1, 1); // 缓存是 {1=1} lRUCache.put(2, 2); // 缓存是 {1=1, 2=2} lRUCache.get(1); // 返回 1 lRUCache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3} lRUCache.get(2); // 返回 -1 (未找到) lRUCache.put(4, 4); // 该操作会使得关键字 1 作废,缓存是 {4=4, 3=3} lRUCache.get(1); // 返回 -1 (未找到) lRUCache.get(3); // 返回 3 lRUCache.get(4); // 返回 4 ``` ## Answer ``` c++ struct DLinkedNode { //基础结构 int key, value; DLinkedNode* prev; DLinkedNode* next; DLinkedNode(): key(0),value(0), prev(nullptr), next(nullptr) {} DLinkedNode(int _key, int _value): key(_key),value(_value), prev(nullptr), next(nullptr) {} }; class LRUCache { private: unordered_map cache; DLinkedNode* head; DLinkedNode* tail; int size; int capacity; public: LRUCache(int _capacity): capacity(_capacity),size(0) { head = new DLinkedNode(); tail = new DLinkedNode(); head->next = tail; //后继连接尾部 tail->prev = head; //前驱连接头部 } void addToHead(DLinkedNode* node) { node->prev = head; node->next = head->next; head->next->prev = node; head->next = node; } void moveToHead(DLinkedNode* node) { removeNode(node); addToHead(node); } void removeNode(DLinkedNode* node) { node->next->prev = node->prev; node->prev->next = node->next; } DLinkedNode* removeTail(){ DLinkedNode *node = tail->prev; removeNode(node); return node; } int get(int key) { if (!cache.count(key)) { return -1; } DLinkedNode* node = cache[key]; moveToHead(node); return node->value; } void put(int key, int value) { if (!cache.count(key)) { //如果不存在 DLinkedNode* node = new DLinkedNode(key,value); cache[key] = node; addToHead(node); ++size; if (size > capacity) { DLinkedNode *node = removeTail(); cache.erase(node->key); delete node; --size; } } else { DLinkedNode *node = cache[key]; moveToHead(node); node->value = value; } } }; /** * Your LRUCache object will be instantiated and called as such: * LRUCache* obj = new LRUCache(capacity); * int param_1 = obj->get(key); * obj->put(key,value); */ ``` [146. LRU 缓存](https://leetcode.cn/problems/lru-cache/) [引用自codetop](https://codetop.cc/home) URL: https://sunyazhou.com/2022/07/longestsubstring/index.html.md Published At: 2022-07-01 05:21:00 +0000 # 无重复字符的最长子串 ![](/assets/images/20220701ReverseList/algorithm.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! # 如题 给定一个字符串 `s`,请你找出其中不含有重复字符的 最长子串 的长度。 #### 示例1 ``` sh 输入: s = "abcabcbb" 输出: 3 解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。 ``` #### 示例2 ``` sh 输入: s = "bbbbb" 输出: 1 解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。 ``` #### 示例3 ``` sh 输入: s = "pwwkew" 输出: 3 解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。   请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。 ``` ## Answer ``` c++ class Solution { public: int lengthOfLongestSubstring(string s) { unordered_set charSets;//哈希集合,记录每个字符是否出现过 int n = s.size(); int rk = -1, ans = 0;//右指针,初始值为 -1,相当于我们在字符串的左边界的左侧,还没有开始移动 for (int i = 0; i < n; ++i) {//枚举左指针的位置,初始值隐性地表示为 -1 if (i != 0) { charSets.erase(s[i-1]);// 左指针向右移动一格,移除一个字符 } while (rk + 1 < n && !charSets.count(s[rk +1 ])) { charSets.insert(s[rk + 1]);// 不断地移动右指针 ++rk; } ans = max(ans, rk - i + 1);// 第 i 到 rk 个字符是一个极长的无重复字符子串 } return ans; } }; ``` [3. 无重复字符的最长子串](https://leetcode.cn/problems/longest-substring-without-repeating-characters/) [引用自codetop](https://codetop.cc/home) URL: https://sunyazhou.com/2022/07/reverselist/index.html.md Published At: 2022-07-01 04:36:00 +0000 # 反转链表 ![](/assets/images/20220701ReverseList/algorithm.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! # 如题 给你单链表的头节点`head`,请你反转链表,并返回反转后的链表。 ![](/assets/images/20220701ReverseList/1.webp) ``` sh 输入:head = [1,2,3,4,5] 输出:[5,4,3,2,1] ``` ## Answer ``` c++ //Definition for singly-linked list. struct ListNode { int val; ListNode *next; ListNode() : val(0), next(nullptr) {} ListNode(int x) : val(x), next(nullptr) {} ListNode(int x, ListNode *next) : val(x), next(next) {} }; class Solution { public: ListNode* reverseList(ListNode* head) { ListNode *prev = nullptr; ListNode *curr = head; while (curr){ ListNode *next = curr->next; curr->next = prev; prev = curr; curr = next; } return prev; } }; ``` [206. 反转链表](https://leetcode-cn.com/problems/reverse-linked-list/) [引用自codetop](https://codetop.cc/home) URL: https://sunyazhou.com/2022/04/Mos/index.html.md Published At: 2022-04-25 01:28:00 +0000 # macos鼠标软件MOS ![](/assets/images/20220425Mos/mos.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! ## 推荐一个好用的mac上使用鼠标的软件 免费开源mac上使用windows鼠标滑动自然的软件[mos](https://mos.caldis.me/) 可以让windows上的鼠标滚轮滑动像苹果触摸板一样丝滑 [点此下载](https://github.com/Caldis/Mos/releases/download/3.3.2/Mos.Versions.3.3.2.dmg) URL: https://sunyazhou.com/2022/04/BranchManage/index.html.md Published At: 2022-04-14 00:50:00 +0000 # 开发分支管理模型 ![](/assets/images/20220414BranchManage/git.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! ## 一种适合客户端开发的分支管理模型 ![](/assets/images/20220414BranchManage/BranchGuide.webp) 首先`DEV`代表开发分支 首先`RB`代表发版分支 > 注意此名称和思路借鉴之前快手开发的内部分支管理. 当开发完成提测后自动开出下个版本的RB和DEV分支.这样循环往复.实现迭代的管理 ### 大家关心的问题 #### RB的代码修改如果DEV想用如何处理? RB分支修改后和如何同步给DEV分支,如果常规开发的话在代码Review的前提下.可以从RB 提Merge Reuqest到DEV. eg: `RB1.6.0` Merge to `DEV1.6.1`. 如果只是几个简单的commit 的话 我建议RB上的提交自己手动执行`git cherry-pick commitIDXXX`的形式到DEV分支.(也就是说你要切到DEVxxx分支 然后执行 `git cherry-pick commitIDXXX`) #### 发布完成后 RB分支如何处理 理论上发布完成后需要做2件事 1. 合并到`master`后 并打`tag`. 2. 删除`RBxxx`分支 > 如果上述操作完成后,RB的修改如果dev想用但是RB分支被删除了的话可以直接从master merge代码到DEV. #### RB分支的 目的是? 1. 只接受bug的修改 2. 不接受需求开发,不能 合并DEV > 注意: RB不能合并DEV,只能DEV Merge RB! > 注意: RB不能合并DEV,只能DEV Merge RB! > 注意: RB不能合并DEV,只能DEV Merge RB! ## 总结 分支管理各家都有自己的管理方式,没有谁比谁更好,只有那些管理方式更适合. -- 本文撰写自 [sunyazhou](https://https://www.sunyazhou.com/) [中] sunyazhou.com 此材料受版权保护 URL: https://sunyazhou.com/2022/04/YZ3DMenu/index.html.md Published At: 2022-04-13 01:50:00 +0000 # 开源YZ3DMenu导航菜单 ![](/assets/images/20220413YZ3DMenu/3dmenu.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! 上图先看效果 ![开源项目](/assets/images/20220413YZ3DMenu/3DMenuDemo1.gif) ![实践完成](/assets/images/20220413YZ3DMenu/3DMenuDemo2.gif) 这个菜单是个3D菜单,之前在一个叫cosmos的app中出现,之前一直想找个时间把这个功能搞出来开源.之后出现了一个类似微博的开源项目[Cosmos](https://github.com/zhnnnnn/ZHNCosmos) 这一阵子有时间 把这个组件重写一遍. ## 实现原理 * 通过tabbar的按钮触发(内部封装pan手势)时机,开始拖拽中、结束或取消... * 添加新window 并在window上覆盖了 Blur模糊和容器,以及封装的仪表盘菜单视图 * 滑动过程中改动菜单视图(仪表盘菜单)的m34动画属性实现 倾斜滑动. ![内部变量](/assets/images/20220413YZ3DMenu/3DMenuDemo3.gif) 下面是框架的代码结构设计 ![](/assets/images/20220413YZ3DMenu/3DMenuDesign.webp) 代码比较多 我这里没有给大家详细列,我把代码 放在了 [github](https://github.com/sunyazhou13/YZ3DMenu)上感兴趣的可以点击查看一下. ## 总结 之前一直想把这个重写一下,一直没有抽出时间,后续打算用swift重写一个新的轮子,希望这个开源组件能帮到你,有问题可以下方留言, 很久没有写博客了.2022年要坚持写一些硬核技术. [本文demo地址](https://github.com/sunyazhou13/YZ3DMenu) -- 摘录来自 [参考Cosmos](https://github.com/zhnnnnn/ZHNCosmos) [中] zhnnnnn 本文材料受版权保护 URL: https://sunyazhou.com/2022/04/CVPixelBufferRef/index.html.md Published At: 2022-04-06 01:50:00 +0000 # 深入理解CVPixelBufferRef ![](/assets/images/20220406CVPixelBufferRef/Cover.webp) 在iOS里,我们经常能看到`CVPixelBufferRef`这个类型,在`Camera`采集返回的数据里得到一个`CMSampleBufferRef`,而每个`CMSampleBufferRef`里则包含一个`CVPixelBufferRef`,在视频硬解码的返回数据里也是一个`CVPixelBufferRef`。 顾名思义,`CVPixelBufferRef`是一种像素图片类型,由于`CV`开头,所以它是属于`CoreVideo`模块的。 iOS喜欢在对象命名前面用缩写表示它属于的模块,比如`CF`代表`CoreFoundation`,`CG`代表`CoreGraphic`,`CM`代表 `CoreMedia`。既然属于`CoreVideo`那么它就和视频处理相关了。 它是一个`C`对象,而不是`Objective-C`对象,所以它不是一个类,而是一个类似`Handle`的东西。从代码头文件的定义来看 `CVPixelBufferRef`就是用`CVBufferRef` `typedef`而来的,而`CVBufferRef`本质上就是一个`void *`,至于这个`void *`具体指向什么数据只有系统才知道了。 所以我们看到 所有对`CVPixelBufferRef`进行操作的函数都是纯`C`函数,这很符合iOS `CoreXXXX`系列`API` 的风格。 比如`CVPixelBufferGetWidth`,`CVPixelBufferGetBytesPerRow` 通过API可以看出来,`CVPixelBufferRef`里包含很多图片相关属性,比较重要的有`width`,`height`,`PixelFormatType`等。 由于可以有不同的`PixelFormatType`,说明他可以支持多种位图格式,除了常见的`RGB32`以外,还可以支持比如`kCVPixelFormatType_420YpCbCr8BiPlanarFullRange`,这种`YUV`多平面的数据格式,这个类型里 `BiPlanar`表示双平面,说明它是一个`NV12`的`YUV`,包含一个Y平面和一个UV平面。通过`CVPixelBufferGetBaseAddressOfPlane`可以得到每个平面的数据指针。在得到`Address`之前需要调用`CVPixelBufferLockBaseAddress`,这说明`CVPixelBufferRef`的内部存储不仅是内存也可能是其它外部存储,比如现存,所以在访问前要`lock`下来实现地址映射,同时`lock`也保证了没有读写冲突。 由于是C对象,它是不受 ARC 管理的,就是说要开发者自己来管理引用计数,控制对象的生命周期,可以用`CVPixelBufferRetain`,`CVPixelBufferRelease`函数用来加减引用计数,其实和`CFRetain`和`CFRelease`是等效的,所以可以用`CFGetRetainCount`来查看当前引用计数。 如果要显示`CVPixelBufferRef`里的内容,通常有以下几个思路。 把`CVPixelBufferRef`转换成`UIImage`,就可以直接赋值给`UIImageView`的`image`属性,显示在UIImageView上,示例代码 ``` objc + (UIImage*)uiImageFromPixelBuffer:(CVPixelBufferRef)p { CIImage* ciImage = [CIImage imageWithCVPixelBuffer:p]; CIContext* context = [CIContext contextWithOptions:@{kCIContextUseSoftwareRenderer : @(YES)}]; CGRect rect = CGRectMake(0, 0, CVPixelBufferGetWidth(p), CVPixelBufferGetHeight(p)); CGImageRef videoImage = [context createCGImage:ciImage fromRect:rect]; UIImage* image = [UIImage imageWithCGImage:videoImage]; CGImageRelease(videoImage); return image; } ``` 从代码可以看出来,这个转换有点复杂,中间经历了多个步骤,所以性能是很差的,只适合偶尔转换一张图片,用于调试截图等,用于显示视频肯定是不行的。 另一个思路是用OpenGL来渲染,`CVPixelBufferRef`是可以转换成一个`openGL texture`的,方法如下: ``` c CVOpenGLESTextureRef pixelBufferTexture; CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault, _textureCache, pixelBuffer, NULL, GL_TEXTURE_2D, GL_RGBA, width, height, GL_BGRA, GL_UNSIGNED_BYTE, 0, &pixelBufferTexture); ``` 其中,`_textureCache`代表一个`Texture`缓存,每次生产的`Texture`都是从缓存获取的,这样可以省掉反复创建`Texture`的开销,`_textureCache`要实现创建好,创建方法如下 ``` c CVOpenGLESTextureCacheCreate(kCFAllocatorDefault, NULL, _context, NULL, &_textureCache); ``` 其中`_context`是`openGL`的 `context`,在`iOS`里就是 `EAGLContext *` `pixelBufferTexture`还不是`openGL`的`Texture`,调用`CVOpenGLESTextureGetName`才能获得在`openGL`可以使用的`Texture ID`。 当获得了 `Texture ID`后就可以用`openGL`来绘制了,这里推荐用 `GLKView`来做绘制 ``` c glUseProgram(_shaderProgram); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, textureId); glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glDrawArrays(GL_TRIANGLE_FAN, 0, 4); ``` 当然这不是全部代码,完整的绘制`openGL`代码还有很多,`openGL`是著名的啰嗦冗长,还有`openGL` `Context`创建`shader`编译`DataBuffer`加载等。 本质上这段代码是为了把`Texture`的内容绘制到 `openGL的frame buffer`里,然后再把`frame buffer`贴到 `CAEAGLayer`。 这个从`CVPixelBufferRef`获取的`texture`,和原来的`CVPixelBufferRef`对象共享同一个存储,就是说如果改变了`Texture`的内容,那么`CVPixelBufferRef`的内容也会改变。利用这一点我们就可可以用`openGL`的绘制方法向`CVPixelBufferRef`对象输出内容了。比如可以给`CVPixelBufferRef`的内容加图形特效打水印等。 除了从系统API里获得`CVPixelBufferRef`外,我们也可以自己创建`CVPixelBufferRef` ``` objc +(CVPixelBufferRef)createPixelBufferWithSize:(CGSize)size { const void *keys[] = { kCVPixelBufferOpenGLESCompatibilityKey, kCVPixelBufferIOSurfacePropertiesKey, }; const void *values[] = { (__bridge const void *)([NSNumber numberWithBool:YES]), (__bridge const void *)([NSDictionary dictionary]) }; OSType bufferPixelFormat = kCVPixelFormatType_32BGRA; CFDictionaryRef optionsDictionary = CFDictionaryCreate(NULL, keys, values, 2, NULL, NULL); CVPixelBufferRef pixelBuffer = NULL; CVPixelBufferCreate(kCFAllocatorDefault, size.width, size.height, bufferPixelFormat, optionsDictionary, &pixelBuffer); CFRelease(optionsDictionary); return pixelBuffer; } ``` 创建一个`BGRA`格式的`PixelBuffer`,注意`kCVPixelBufferOpenGLESCompatibilityKey`和`kCVPixelBufferIOSurfacePropertiesKey`这两个属性,这是为了实现和`openGL`兼容,另外有些地方要求`CVPixelBufferRef`必须是`IO Surface`。 `CVPixelBufferRef`是iOS视频采集处理编码流程的重要中间数据媒介和纽带,理解`CVPixelBufferRef有助于写出高性能可靠的视频处理。 要进一步理解`CVPixelBufferRef`还需要学习`YUV`,`color range`,`openGL`等知识。 引用自[深入理解CVPixelBufferRef](https://zhuanlan.zhihu.com/p/24762605?utm_source=ZHShareTargetIDMore&utm_medium=social&utm_oi=28280635785216) URL: https://sunyazhou.com/2021/12/FinalSummary/index.html.md Published At: 2021-12-26 00:00:00 +0000 # 2021年终总结 ![](/assets/images/20211231FinalSummary/2021F1.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! 年底了,交作业,这一年无业游民蹉跎了不少.回冰城生活了一年又双叒叕的回到了曾今奋斗的根据地-首都北京.别问为什么`生活所迫`. 这一年过很不顺利,家人生病,外祖母离世,工作不稳定,疫情常态化延续... 几乎没有好的消息.年初的时候疫情爆发,导致过年的时候疫情严重封城. 自己在同学家就地过年. ### 诗和远方 离开快手的时候同事说这是洲哥的诗和远方.见笑了哈哈,今天的洲哥得为诗和远方付出代价了. > 有时候不被嘲笑的梦想不值得去实现. ``` txt "少鹏": 洲哥要坚持写博客呀! "我":嗯, 我一定会坚持写好! ``` 感谢我还有这样同事. ## 2021回顾 * 关于生活 * 关于工作 * 关于学习 * 关于游戏 * 关于修车 ## 关于生活 自去年`金毛特朗普`执政最后期限美国疫情一直失控,导致这个世界跟着止步不前,他的继任者`拜登`上台后,疫情依然没有得到有效的控制的同时开始肆意放水狂印钞票致使美金通货膨胀严重. 严重影响了世界各国经济,致使最终影响到了我的收入(每个人的收入). 后疫情时代的挽歌依然不停地持续发酵,我的生活也随着疫情的波动起起伏伏从未稳定.回家后的我变得沉默寡言,对技术的热爱从未因环境的变化而衰减. ![](/assets/images/20211231FinalSummary/2021F2.webp) 为了应对疫情,国家免费接种疫苗,截止本文撰写时间截止,我已经全部完成了3针的疫苗接种.保护了自身和家人的安全. #### 最开心的事 今年最开心的事莫过于拿到新房的房本 ![](/assets/images/20211231FinalSummary/2021F3.webp) 下班后衣服一扔沙发开始做晚饭, 我没想到厨房是我度过时光最多的地方. ![](/assets/images/20211231FinalSummary/2021F5.webp) 我搞的跟炼金术师一样,添油加醋,油温,烹饪. ![](/assets/images/20211231FinalSummary/2021F4.webp) 吃完晚饭我会去家门口哈西站广场遛弯,思考一下人生. ![](/assets/images/20211231FinalSummary/2021F9.webp) 至于烹饪的事情还是挺佩服大学李月辉老师,我即便如何尝试也未曾学会摆盘儿,每次都看到老师逢年过节下厨,最后朋友圈share一下,高端操作.以后有机会我当面请教请教. 7月的时候去了一趟齐齐哈尔扎龙自然保护区,看一看我女朋友家亲戚的别墅. ![](/assets/images/20211231FinalSummary/2021F6.webp) 别墅是好呀,菜园子成了点缀别墅的点睛之笔. 重点是看仙鹤哈 ![](/assets/images/20211231FinalSummary/2021F10.webp) 我分享一下大概 路线和路途方便兄弟们去自驾 从哈尔滨西站出发到扎龙自然保护区如果自驾话走高速大概3个小时10分钟.门票几十块钱很便宜. 需要沿着环城高速经过松花江服务区后走绥满高速大庆方向,大庆好大,几乎2个半小时的路程在大庆的疆域范围内,途径肇东、安达、大庆让胡路区的红旗水库,大庆林甸县... 最后到达齐齐哈尔扎龙服务区. 高速距离大概300公里左右.费用全程来回+600块钱足够了. > 重点提醒一下蚊子多,以及带防晒装备就行了. #### 回家一年的感受 我想我有必要在这跟各位聊聊回家这一年有什么收获或者感受. * 感受了家乡的风土人情 * 很多人都生活在解决温饱的水平线之间 * 家里车牌不限号可以自由的开车了 * 不用交房租了 * 在自己的房子里买东西再也不用担心搬家要不要考虑搬走了方不方便了 * 工作环境差、包容性较差. * 家乡的消费不比北京低多少,但是收入可是差太多了 * 医疗水平不高,跟北京完全不一样 * 家里没有末位淘汰制,教师、公务员、体制内工作的人更受欢迎和被大众所接受. * 家里更看重学历不看重能力,双一流院校毕业的硕士以上有可能有工作机会.其它的免谈. 家乡城市属于3线,房地产是支柱性产业,曾今的哈飞等老牌重工业产业处于衰落期,政府政策贯彻落实到执行等于画大饼.给我最深刻的感受是在老家的年轻人或者中年人大部分都处于`对未来失去希望`勉强过活的状态. 只有少部分做生意买卖的人能发家致富并安稳的生活在这座城市里. 其余的人都是过着过一天算一天的躺平状态. 至于职业规划都是空谈.更不用提如何建设和谐社会发展经济和高科技产业了. 人口每年流出率 33%+. 这是回家后的一些微观感受. 生活还是需要继续的,思考了很久,并认清了现实和所处的环境,还是觉得一线城市适合自己的职业发展. 当然看到这里你可以嘲笑我的愚蠢,没关系,我也一样嘲笑自己无知的鲁莽和对城市发展的误判.但我认为如上的所有感受是坐在办公室里无法真切体会和亲身实践的.这并不是什么坏事,是教育自己成长和增长见识的务实体现.如果等自己中年后再来感受这些我相信比我的遭遇感受更痛苦吧!但我希望各位不要重蹈我的覆辙.做出明智的抉择和好的职业发展才是人生赢家. 即便生活如此不顺利,但身心得到了前所未有的舒畅和改善.我变得越来越愿意和承受各种痛苦的经历,心态变得良好.这是在北京7年多的工作经历中前所未有的. 消费观念、生活态度、心理预期、接受能力、承认平庸的自己等等. 为了感受一下大家经常讨论的公考我做了一次尝试、结果可想而知这东西需要经常的锻炼、不是一朝一夕就能行的.但是收获很多, 例如:申论中针对社会现象的描述以及给出相应合理可被公众所接受且务实可行的方案. 虽然我们有时候站在一个老百姓的角度考虑问题可以肆无忌惮,但是站在一个公务人员的角度考虑问题需要遵守`永远为人民服务`为第一原则,阐述和写出可行性措施和最优解.并且要求公务人员的文字书写能力,逻辑组织能力都是很大的挑战.几乎可以说`千里挑一`.我们生活中所看到的官方通知,或者其它类似的文本消息.这些都是需要经过专业的训练和语言组织能力训练斟酌N遍.review N遍后 第一时间发出的.所以我们看到的时候才挑不出任何问题.那大家有没有想过谁来撰写这些内容呢!是公务科员类的人员, 大家虽然觉得是动动嘴就搞定的事,其实不知道的是为了动动嘴付出了多少努力.就训练写一篇声明稿件 我反复练习10+次,也依然不能完美写好!(可能是我不擅长干文职类的工作). 说真的自从高中高考毕业到我考试答申论作文,我已经十几年没再写过1200字以上的作文了.至于章法、文字功底、写作手法早已经随着互联网的发展交给百度去完成了. 不过考后我发现我对自己的申论总体来说还是很满意的虽然3类文,但是得了57分已经超出了我对自己的预期了. ![](/assets/images/20211231FinalSummary/2021F7.webp) ## 关于工作 #### 专利 在快手工作的时候申请的专利终于在2021年公示了 ![](/assets/images/20211231FinalSummary/2021F8.webp) #### 回北京继续打拼 与其说回北京打拼不如说是逃离北漂失败, 不管怎样我还是能接受大家对我的各种评论的.这说明我的职业适合一线城市.这是大部分人无法逃离的.还是熟悉的地铁,匆忙的路人. 由于互联网的浪潮下 "双减"政策影响,国家反垄断的打压,资本市场预冷,增量用户市场转为存量市场.导致IT行业现在步履艰难.这是好事也是坏事.好事是这个行业将会不断被打磨成科学的可持续发展的行业带动经济发展.坏事是找工作的难度和标准变得越来越困难和更高. 对于一个iOS来说其实工作中用到的算法比较少.但是这个行业不知道从什么时候开始变得面试必须问算法. #### 说说我对算法的理解 * 懂比不懂好 * 仁者见仁智者见智 * 面试为啥问算法 ##### 为什么懂比不懂好? 因为即便你之前大学学过《算法与数据结构》,但是开发中也或多或少使用一些算法,我认为最难得不是算法,是懂了算法之后要学以致用,把它落实到实际的工作中或者工程中解决实际问题,这才是我们学好它的目的. 如果都不了解它,那团队的同事不能和你保持在同一段位上,人家说啥你听不懂你说这样好吗? ##### 仁者见仁智者见智 举个例子: 如果开发中我们要做个类似根据个人喜好推荐内容的推荐系统,如果你不懂一些数学上的概念或者算法,你根本不知道 有一个叫[欧几里得距离](https://zh.wikipedia.org/wiki/%E6%AC%A7%E5%87%A0%E9%87%8C%E5%BE%97%E8%B7%9D%E7%A6%BB)的概念能帮我们实现这套系统. 它的公式可以帮我们解决一些内容的距离是不是我们喜欢的内容,虽然我们看到的是一堆公式或者是一堆搞不懂的算法表达式.总之这是算法的范畴能帮我们解决实际问题的科学依据. **因为我们对一些未知的知识缺乏探索的精神导致我们总为自己不学习高精尖技术找借口**. 所以我说对于学习算法不同的人有不同的理解 ##### 面试为啥问算法? 首先我们要明白一点是为什么有的公司要面试过程中引入算法,`是为了重塑这个有技术含量的职业`,`是为了教育从业者学习算法才能变得更有价值`,因为我们的从业门槛低端的话薪资也不会高,那么高的薪资体系下就要求从业者有更高水平的发挥.为了balance技术和职业水平的矛盾,加入算法考核是一个科学的举措. 下面我再说一下面试iOS开发者过程中问算法,这个事情是个趋势,如果问法是从通过算法解决工程中遇到的哪些实际问题,从务实的角度考虑我是赞同的. 如果是从只为了检验面试者有没有这个能力考一些不切实际的算法的角度考虑我是反对并极力抨击的. 这种面试等于浪费时间.对于一个iOS开发者它每天都不用你问的内容,相当于你俩说话都不在一个频道上,背离了 算法的初衷.这种问法我是不推荐的. 总之学习算法是程序员的必备技能,书到用时方恨少,学以致用是学习它的最高境界. 继续回来说工作哈. 这一年其实总的来看在工作内容和方向上自己需要有所改变和提升,最重要的是行胜于言,不能每年都墨迹XX要提升结果第二年年终总结还是待提升,要务实去做,实际行动起来才行.要`求真务实,实事求是`.禁止摸鱼 ![](/assets/images/20211231FinalSummary/BanTouchFish.webp) ## 关于学习 今年学了几门付费课程也强烈推荐给各位,希望在后续的时光中能坚持持续学习,学习科学技术. * iOS开发高手课 学习了3遍 * 数据结构与算法之美 * Swift核心技术与实践 * 鸿蒙OS ![](/assets/images/20211231FinalSummary/2021F11iOS.webp) 这门课程几乎全是干货,每一个iOS都应该作为科普课程学习. ![](/assets/images/20211231FinalSummary/2021F11Algorithm.webp) 这个数据结构与算法我也强烈建议各位认真学习,数据结构从`表`到`树`再到`图`,最后算法由浅入深.想想算法导论你买了后翻看了几页. ![](/assets/images/20211231FinalSummary/2021F11Swift.webp) swift这是我学习第3遍了,swift5.x之后变动的东西太多了,不得不重新学习一遍,目前学习进度60%以上了. ![](/assets/images/20211231FinalSummary/2021F11HarmonyOS.webp) 这门课程作为国产操作系统的标配,自从华为被美国打压后,国人痛定思痛,从基础科学研发搞起.专攻卡脖子的技术,为了完成我2019年描述的年终总结,2022年这门课程我必须认真学完. #### 学习小结 已经连续4年写年终总结了,我也在持续不断的完善自己知识的盲区范围,我应该敏锐的握住这个社会在发展过程中的趋势,这样我就可以抢先一步学一些别人还不掌握的技能,那么在未来就不愁有立足之地. ## 关于游戏 之前在北京的时候买了一个NEOGEO mini的小型游戏机,在北京北漂久了防止抑郁买一些小玩意打发一些心情.自从回家后被二姐家外甥女`恩慧`抢去了,全当送你了唉. 初中时我第一次接触到PS one(PlayStation ONE),那时候我就觉得索尼做游戏行业很NB,可是即便是工作了好多年依然舍不得买个PS玩. 错过了 * PS2 * PS3 * PS4 * PS4 slim * PS4 Pro PS5我不能再错过了,有能力就下手吧,刚好公积金按月支取到账加上双11,买了人生中第一个像样的游戏机. ![](/assets/images/20211231FinalSummary/2021F12PS5.webp) 国行版本,港版账号备份可玩港服游戏 买完后我只能说被教育了,游戏好贵呀!贵不是重点,重点一个游戏100G+,可怜的原机游戏最多能下2~3个.想扩容可是一个PCIe4.0的SSD m.2格式的固态硬盘 2000+RMB. 如果买齐了所有装备这玩意1w+,果断放弃.实力已经不允许我玩PS5了. 至于游戏还是很好玩的 * 神秘海域4 * 恶魔之魂重制版 * 原机带的机器人非常好玩 * 双人成行 这些都是我觉得非常经典的游戏 ## 关于修车 从封面大家也能看到我的破车. 说来话长,我只能奉劝各位买车不买二手就行了.我列了一下这车从我买到手换的零件清单 * 刹车盘+刹车片 前面一套 * 马牌UC6 (215/55 R16)轮胎4只 * 发动机节气门 * 发电机皮带 * 油气分离器 * 碳罐电磁阀 * 正时链条+链板一套 * 涡轮增压管 * 汽车蓄电瓶 * 氙气大灯+海拉5透镜+海拉4安定器一套 * 4个火花塞我换2次了 * 空调滤+空气滤+机油滤 * Power Flex 后下支臂聚氨酯强化衬套 * 后备箱衬板 * 天窗开关 * 自动挡档把开关一套 * MIB 682E 车机 * 后翻表倒车 翻标一套 * 汽车脚垫一套 * 前排左右车窗银色镀铬压条 * 前支臂铝座 * 发动机底部树脂护板 * 变速箱连接发动机 机爪垫 * 轮胎挡泥板 x 4 * 进气道清理积碳 * 主驾侧后视镜总成全套 * 前风挡玻璃 * 后备箱气动支撑杆 x2 * 变速箱油 * 刹车油 * 防冻液 * 全车减震器 x 4 * 前减震弹簧 x 2 好了我列到这里,我基本可以修理EA888发动机了.从一个码农成为为半个大众修理工. > 下一步我打算整个破笔记本装上win elsa软件,专业检修大众、奥迪、保时捷等大众系列专修. 我带大家认识下这些机械部件,以下全部是我的车换过的配件. ![](/assets/images/20211231FinalSummary/2021F13BrakeDisc.webp) 前驱刹车盘 x2 ![](/assets/images/20211231FinalSummary/2021F13BrakeBlock.webp) 刹车片 x2 ![](/assets/images/20211231FinalSummary/2021F13TireUC6.webp) 马牌轮胎 x4 ![](/assets/images/20211231FinalSummary/2021F13ThrottleValve.webp) 发动机节气门 > 冬天的时候仪表盘报EPC灯闪烁点亮,怠速抖动不稳,经过检测发现节气门信号不可信,冬天节气门里面有冷空气形成的水珠,冷热交替容易造成进气管道有水珠出现,凉车的时候东北温度低容易结冰,造成节气门开合不畅,长年累月造成节气门节气门电机疲劳,其实这玩意我非常想把它拆下来自己维修一下,这个东西1700+rmb.很贵换一个不值得.后来考虑这东西关键可别在路上坏了换个新的吧! ![](/assets/images/20211231FinalSummary/2021F13DynamoBelt.webp) 发电机皮带,我看着这个东西要坏,实际拆下来没啥事也让我换新的了,旧的依然放在车上防止路上再出问题的时候有个备件更换,东西没多少钱但却十分重要. ![](/assets/images/20211231FinalSummary/2021F13Oil-waterSeparator.webp) 上面这个部件叫油气分离器或者叫油水分离器,发动机会产生高温气体,因为发动机内部有机油,机油产生的蒸汽中有水分子,为了能让机油和水分离开,这个部件就显得十分重要,我更换的时候其实没有坏,只是之前的部件漏油,整的发动机全是油污,我以为发动机漏油了呢结果一看这玩意上边全是油.换了改进型的,减少烧机油的可能. ![](/assets/images/20211231FinalSummary/2021F13ValveEGR.webp) 这个东西叫碳罐电磁阀,在节气门附近,主要是负责把汽油油箱产生的挥发蒸汽送入到燃烧室燃烧,如果这个东西坏的话最明显的表现就是常通油耗高(当然汽油都挥发了掉了能不费油吗).这个部件也没坏,只是我去4S店也看不出来这个东西有问题,只是换新的后汽油节省了30%.所以换新的零部件是起作用的. ![](/assets/images/20211231FinalSummary/2021F13TimingChain.webp) 正时链条,我的是EA888 二代,这里只是引用一下图, 这个链条8~10w公里必须换,我很积极的在84000公里的时候直接把它换了,当时正时链条被拉长了很多几乎到了更换它的预设阈值范围,防止提前坏就换了. 这个东西相当于发动机核心的部件了,连链板等相关部件一起换掉2400块. ![](/assets/images/20211231FinalSummary/2021F13PressureInlet.webp) 发动机最里面有个连接涡轮增压的管道,因为油气分离器漏油导致这个管里面全是油,顺手把它换掉,其实这玩意换不换没啥必要,只是我不太像看到我的发动机全是裸露在外面油乎乎的样子. ![](/assets/images/20211231FinalSummary/2021F13LeadAcidBattery.webp) 风帆铅酸蓄电池(好马配好鞍,好车配风帆.),自冬天在路上EPC灯亮起我把车停路边,最后发现节气门信号不可信后一起把汽车电瓶也换了,原来的电瓶应该好多年没换了都鼓包了,我直接换新的了. ![](/assets/images/20211231FinalSummary/2021F13Lights.webp) 这是2020年升级的整套氙气大灯. ![](/assets/images/20211231FinalSummary/2021F13SparkPlug.webp) 这是2.0T的火花塞,是我自己从某猫上买的,博世的,顺手买了一套棘轮扳手工具箱,自己找个停车场 自己换的. 我非常自豪的说这玩意其实自己动手就可以解决4个火花塞才200不到,原来的火花塞2w公里4s店就建议更换,我的车2w公里后 自己换的新的,旧的拆下来后里面有积碳,我用洁厕灵泡了1天把火花塞清理的干干净净,后续还能对付用. ![](/assets/images/20211231FinalSummary/2021F13OilFilter.webp) 机油滤清器,这玩意网上买才30多(4s店卖100+),赶上某活动自己有车的兄弟们可以多买几个囤着,这个东西是个消耗品,大众车每5000公里保养的时候就换它.这个我买了3个在曼牌旗舰店买的. > 这里我强调一件事,有些配件并不一定原厂的比其它品牌的好,这得分是什么零件.这个机滤只要你买的品牌正规商店买的应该差不了,这就和买笔记本的内存条你说买联想的内存条好还是买金士顿亦或是三星、闪迪、威刚的好呢?肯定非联想的呀!毕竟它不是专业做内存条的. 还有空调滤芯和空气滤芯我也是囤积了好几个. * 空调滤芯是在车内负责车内空调的空气过滤 一般`马勒`品牌比较出名 * 空气滤芯是在发动机舱里面负责发动机进气空气过滤 > 不懂车的小伙伴看好了别搞混,这俩东西不一样 > 以上俗称3滤芯都可以自己更换,机油滤清器得有专门的工具哈. ![](/assets/images/20211231FinalSummary/2021F13PowerFlex.webp) 这个东西叫聚氨酯强化衬套,我的车全车如果换下来得7000+.为了尝试这东西到底行不行2020年大年初一赶上同学的单位(一汽大众4s店)休班人少,我自己拿着这两个衬套去4s店自己换的. 大众现在的车底盘都是通用的配件,几乎所有的零件大部分都可以通用,下面我们来看看这玩意底盘可对应的零件号,以及价格 ![](/assets/images/20211231FinalSummary/2021F13MQB1.webp) ![](/assets/images/20211231FinalSummary/2021F13MQB2.webp) 我换的是后下支臂`PFR85-510`衬套,因为原车的已经老化到不行了,车很松散,开起来感觉车不自然.这玩意如果换的话必须连那两个下支臂一起换. 这一点我不得不批评一下大众,这个底盘设计的扩展性和改装性很差.不过换完后车底盘立马开起来不一样了.底盘很紧很硬,回到了新车状态. > 注意这玩意必须得用专业设备更换,而且更换完后必须做4轮定位,因为要改动的是它的悬挂系统中的偏心螺丝,这个螺丝专用于调整车辆侧倾角的位置. 更换这个衬套最难的是需要压出原来的衬套,最好有专业设备,否则不太好拆卸.我利用修理厂的压力机和一些套筒工具勉强把这玩意拆下来. ![](/assets/images/20211231FinalSummary/2021F13Lining.webp) 上边这图的后备箱底部是一个车厢可拆卸内衬板,为啥要换它呢?夏天的时候帮亲戚农村操办喜事拉了一车厢的菜.导致某些液体渗漏到了车后备箱,一股难闻的味道,这东西洗还没法洗,搞的我只能换个新的了. ![](/assets/images/20211231FinalSummary/2021F13ClearstorySwitch.webp) 天窗开关,这个东西很少有坏的,我的车是行驶一段时间天窗自己打开一会儿并关闭,一开始我以为迈腾是不是有二氧化碳浓度检测,当检测到车内二氧化碳气体过高会自动触发天窗开关把车外空气交换一下避免驾驶者昏迷困倦,结果我把这个事情跟我同学一聊他说大众应该给你点专利费,这个是个通病,开关时间长了触点氧化接触不良.于是更换了一个. 一汽大众你看好了,我申请个专利你看看这专利费是不是交一交. ![](/assets/images/20211231FinalSummary/2021F13ShiftGears.webp) 传说中的DSG(单身狗,大傻瓜)档把手球,原来的已经磨包浆了,换个新的. ![](/assets/images/20211231FinalSummary/2021F13MIB682E.webp) 这个mib车机系统, 是我觉得是这个老车上最应该升级的部件,这玩意很贵,连翻标倒车并支持CarPlay功能一共5500+. 老车有个很差劲的地方是 车机系统卡顿,功能单一破旧,对新兴技术跟进慢,这个也不能怪老车毕竟它生产的时候还没有iPhone 5s. 但是扩展性这一块做的很差.如果未来汽车谁能既产硬件又产软件并且能提供一套完整的接入车机方案的话那么这将是一个非常好的创业方向.正常情况这个东西想接入汽车是不行的因为内部是有加密协议的,不通用,我买的这个是破解完的波兰进口.破解完就不需要密码了,直接即插即用.只是有些功能这车功能不支持没法用.我买它的主要目的是有一个好的听音乐的系统并支持carplay. 想实现这两点的可行性方案是: * 换一套好的车机系统 * 换一套好的音响 最后选择了车机,因为有导航. ![](/assets/images/20211231FinalSummary/2021F13VWlogo.webp) 光换车机是不行的,必须也连同翻标倒车的摄像头一起换掉,于是也顺手换掉了翻标倒车.这个东西就是封面上那个logo它是大众的独特设计之一,倒车的时候logo会张开伸出一个摄像头,平常我们按动它的时候是开启后备箱.不用担心它会被按坏,它非常结实. ![](/assets/images/20211231FinalSummary/2021F13PressBar.webp) 这个是车窗银色镀铬条,专业名称应该叫车窗压条,为啥换呢,我的车原来主驾位置的橡胶老化了,已经无法挡住雨水了,这个是和玻璃窗紧贴在一起的部件,于是就换了. ![](/assets/images/20211231FinalSummary/2021F13AluminumBase.webp) 下支臂铝座支架,前侧底部下支臂和车身接触的地方橡胶老化,然后还不能单独更换胶套,只能一套一起更换,所以直接换了.这就是我说大众的车扩展性不强的主要原因,那个铝座胶套完好就橡胶老化了导致底盘开车的时候方向盘摆动响应程度不够.其实这个最好单独更换聚氨酯强化衬套会好一些,考虑成本 这俩单独更换其实都没有一个聚氨酯强化衬套贵. ![](/assets/images/20211231FinalSummary/2021F13EngineShield.webp) 发动机下护板,这个东西是保护发动机油底壳的树脂材料,防止磕碰硬物导致发动机受伤.树脂的没有异响,如果是钢制的容易变形后影响底盘稳定性. ![](/assets/images/20211231FinalSummary/2021F13EngineMounting.webp) 发动机连接变速箱的机脚胶,这个东西出现了异响,于是我就更换了新的. 左上角的是连接变速箱和发动机的. ![](/assets/images/20211231FinalSummary/2021F13MudGuard.webp) 车轮挡泥板,买车的时候已经破的不成样子了,直接换了. ![](/assets/images/20211231FinalSummary/2021F13Studdle.webp) 后备箱气动支撑杆,这个东西冬天容易失去弹性,有可能和里面使用的油液有关系,不过换了1年后问题依旧,后续也不打算换了就这样了. ![](/assets/images/20211231FinalSummary/2021F13TransmissionOil.webp) 我的车是要求8万公里更换变速箱油的否则将会失去质保,质保10年16w公里,目前还剩3年质保. ![](/assets/images/20211231FinalSummary/2021F13BrakeOil.webp) 刹车油,这个是我冬天发现自己的车刹车刹不住了我去店里检测一下刹车油水分子过多红灯预警.必须更换否则影响安全. ![](/assets/images/20211231FinalSummary/2021F13Antifreeze.webp) 防冻液,这个买车的时候没有更换,怕水箱太脏早早更换了,现在还剩半瓶放在车里面呢. ![](/assets/images/20211231FinalSummary/2021F13ShockAbsorbe.webp) 这车今年夏天走了一些烂路,发现减震器4个全部漏油,全部更换,同时更换了减震弹簧. ![](/assets/images/20211231FinalSummary/2021F13Spring.webp) 减震弹簧已经被应力压低了1cm多,一起换掉了避免车不安全. #### 修车小结 以上就是我买的二手车后全部的零件更换,看到这么多零件大家也终于知道二手车为啥卖的便宜了吧!因为这些配件到了更换的时候了.这么多配件加在一起再加上之前的买车钱都足够买一辆新高尔夫顶配了. 我现在的感受是这车几乎被我修复到了巅峰状态,虽然花了不少钱和时间,也让我从一个程序员变成了多面手.有些时候修车是有乐趣的.当个爱好培养挺好. ## 总结 ![](/assets/images/20211231FinalSummary/final.webp) 年终总结很零碎,也没啥技术含量,这一年过得很麻木,学习内容较少,其实我还私下里学习了一遍java发现还是不能形成学习体系并在工作中使用它,争取后续学习完其它框架后做点东西.回家的经历改变了我的看法,也教育了我应该继续扎根在北京继续成为一名`新一代的农民工`,从0~1的努力工作才能好好的生活. 2022年见. URL: https://sunyazhou.com/2021/12/iOSLessonsStudyNotes1/index.html.md Published At: 2021-12-23 01:30:00 +0000 # iOS开发高手课01-建立你自己的iOS开发知识体系 ![](/assets/images/20210722iOSLessonsStudyNotes1/studycover.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! 首先致敬一下iOS领域开发者-戴铭,我也非常推荐大家买它的[课程](https://time.geekbang.org/column/intro/161),本篇文章主要是学习时记录的笔记,记录大佬讲述的内容. ## 学习资料 Awesome 系列,就是专门用来搜集各类资料的,其中 [iOS 的 Awesome](https://github.com/vsouza/awesome-ios) 里面,就涉及了 iOS 开发的各个方面。 ## iOS 的知识体系 * 基础 * 原理 * 应用开发 * 原生与前端 ### 基础模块 iOS 开发者需要掌握的整个基础知识,按照 App 的开发流程(开发、调试测试、发布、上线)进行了划分,如下图所示: ![](/assets/images/20210722iOSLessonsStudyNotes1/basicmodule.webp) ### 应用开发 ![](/assets/images/20210722iOSLessonsStudyNotes1/applicationdevelopmodule.webp) ### 原理模块 ![](/assets/images/20210722iOSLessonsStudyNotes1/principlemodule.webp) ### 原生与前端 ![](/assets/images/20210722iOSLessonsStudyNotes1/nativeandfemudule.webp) ### 汇总 ![](/assets/images/20210722iOSLessonsStudyNotes1/final.webp) # 总结 iOS开发务必求精,理解原理和细节,同时开阔眼界,紧跟技术前沿. URL: https://sunyazhou.com/2021/12/WordFrepuency/index.html.md Published At: 2021-12-12 08:08:08 +0000 # 如何计算文本中某些单词出现的频率 # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! 最近在学习一些Swift中的语法,有些比较有意思的内容我都记了笔记 ## 先看问题 ### 如何读入一个文本文件,确定所有单词的使用频率并从高到低排序,打印出所有单词及其频率的排序列表? > 这道题目出自计算机科学史上的著名事件,是当年 Communications of the ACM 杂 志“Programming Pearls”专栏的作者 Jon Bentley 向计算机科学先驱 Donald Knuth 提出的挑战 问题抽象 * 文本字符串 * 切分成单个单词的字符串数组 * 遍历数组,过滤不需要统计的单词 * 统计单词使用频率并打印 #### 传统编程范式解法 ``` swift import UIKit let words = "Blogging is about going through a memory stack of searching and storing knowledge, references, keyword indexing, and so on. Because my brain started like a memory stack, from empty stack to stack overflow, later use heap to store knowledge, found that memory is also OOM. Then I used reference tags to store iOS developing-related articles, tutorials, tricks, tricks, practice code, etc., which made my browser tabs look like they were poisoned. Later, I only remembered a few keywords to Google related articles. Sometimes some people post articles and delete them, so I can't find them. In addition, I have accumulated some skills and shared some skills. So I decided to blog about it. My skills are way, way out of line with yours truly. So I should write more and record the bit by bit of my growth process. Even though my views are filled with too many narrow theories and immature arguments, I must know that today is not yesterday. Otherwise I would have been sorry for wasting my time and ashamed for doing nothing." let NON_WORDS = ["a", "of", "and", "!"] func wordFrep(word: String) -> [String: Int] { var wordDict:[String: Int] = [:] let wordList = words.split(separator: " "); for word in wordList { let lowercaseWord = word.lowercased() if !NON_WORDS.contains(lowercaseWord) { if let count = wordDict[lowercaseWord] { wordDict[lowercaseWord] = count + 1 } else { wordDict[lowercaseWord] = 1 } } } return wordDict } print(wordFrep(word: words)) ``` 结果: ``` sh ["in": 1, "on.": 1, "must": 1, "store": 2, "it.": 1, "blogging": 1, "views": 1, "with": 2, "arguments,": 1, "later": 1, "write": 1, "developing-related": 1, "from": 1, "out": 1, "to": 5, "theories": 1, "my": 6, "keywords": 1, "though": 1, "today": 1, "practice": 1, "more": 1, "can\'t": 1, "been": 1, "oom.": 1, "nothing.": 1, "post": 1, "storing": 1, "i": 8, "look": 1, "yesterday.": 1, "too": 1, "also": 1, "find": 1, "line": 1, "bit": 2, "the": 1, "growth": 1, "browser": 1, "tags": 1, "remembered": 1, "stack,": 1, "skills": 2, "started": 1, "process.": 1, "wasting": 1, "knowledge,": 2, "which": 1, "by": 1, "would": 1, "going": 1, "articles,": 1, "searching": 1, "some": 3, "references,": 1, "google": 1, "tutorials,": 1, "truly.": 1, "sorry": 1, "were": 1, "used": 1, "for": 2, "found": 1, "even": 1, "empty": 1, "blog": 1, "many": 1, "delete": 1, "addition,": 1, "about": 2, "tricks,": 2, "know": 1, "narrow": 1, "filled": 1, "stack": 3, "heap": 1, "related": 1, "code,": 1, "indexing,": 1, "only": 1, "articles": 1, "way,": 1, "way": 1, "record": 1, "later,": 1, "keyword": 1, "ios": 1, "not": 1, "accumulated": 1, "tabs": 1, "have": 2, "skills.": 1, "memory": 3, "them,": 1, "shared": 1, "sometimes": 1, "then": 1, "so": 4, "through": 1, "reference": 1, "poisoned.": 1, "them.": 1, "brain": 1, "ashamed": 1, "immature": 1, "etc.,": 1, "articles.": 1, "overflow,": 1, "people": 1, "is": 3, "made": 1, "otherwise": 1, "time": 1, "they": 1, "are": 2, "few": 1, "doing": 1, "because": 1, "that": 2, "decided": 1, "should": 1, "yours": 1, "like": 2, "use": 1] ``` #### 函数式编程解法 ``` swift import UIKit let words = "Blogging is about going through a memory stack of searching and storing knowledge, references, keyword indexing, and so on. Because my brain started like a memory stack, from empty stack to stack overflow, later use heap to store knowledge, found that memory is also OOM. Then I used reference tags to store iOS developing-related articles, tutorials, tricks, tricks, practice code, etc., which made my browser tabs look like they were poisoned. Later, I only remembered a few keywords to Google related articles. Sometimes some people post articles and delete them, so I can't find them. In addition, I have accumulated some skills and shared some skills. So I decided to blog about it. My skills are way, way out of line with yours truly. So I should write more and record the bit by bit of my growth process. Even though my views are filled with too many narrow theories and immature arguments, I must know that today is not yesterday. Otherwise I would have been sorry for wasting my time and ashamed for doing nothing." let NON_WORDS = ["a", "of", "and", "!"] func wordFrep2(words: String) -> [String: Int] { var wordDict: [String: Int] = [:] let wordList = words.split(separator: " ") wordList.map { $0.lowercased()} .filter{ !NON_WORDS.contains($0)} .forEach { wordDict[$0] = (wordDict[$0] ?? 0) + 1 } return wordDict } print(wordFrep2(words: words)) ``` 结果: ``` sh ["storing": 1, "were": 1, "also": 1, "many": 1, "like": 2, "filled": 1, "used": 1, "process.": 1, "skills.": 1, "more": 1, "find": 1, "record": 1, "tricks,": 2, "found": 1, "oom.": 1, "later": 1, "which": 1, "yours": 1, "ashamed": 1, "tabs": 1, "though": 1, "can\'t": 1, "blog": 1, "way,": 1, "reference": 1, "out": 1, "post": 1, "write": 1, "i": 8, "store": 2, "ios": 1, "is": 3, "they": 1, "brain": 1, "growth": 1, "with": 2, "overflow,": 1, "wasting": 1, "would": 1, "time": 1, "even": 1, "to": 5, "articles,": 1, "tags": 1, "developing-related": 1, "made": 1, "doing": 1, "browser": 1, "then": 1, "people": 1, "later,": 1, "accumulated": 1, "yesterday.": 1, "know": 1, "going": 1, "the": 1, "through": 1, "addition,": 1, "my": 6, "remembered": 1, "theories": 1, "look": 1, "them.": 1, "articles.": 1, "stack": 3, "narrow": 1, "should": 1, "so": 4, "google": 1, "too": 1, "bit": 2, "heap": 1, "articles": 1, "it.": 1, "keyword": 1, "today": 1, "way": 1, "some": 3, "code,": 1, "started": 1, "keywords": 1, "shared": 1, "indexing,": 1, "use": 1, "arguments,": 1, "few": 1, "stack,": 1, "blogging": 1, "nothing.": 1, "are": 2, "views": 1, "must": 1, "references,": 1, "poisoned.": 1, "empty": 1, "decided": 1, "sorry": 1, "searching": 1, "by": 1, "knowledge,": 2, "not": 1, "for": 2, "about": 2, "them,": 1, "been": 1, "delete": 1, "line": 1, "because": 1, "from": 1, "memory": 3, "related": 1, "that": 2, "on.": 1, "truly.": 1, "etc.,": 1, "tutorials,": 1, "in": 1, "have": 2, "practice": 1, "skills": 2, "only": 1, "immature": 1, "otherwise": 1, "sometimes": 1] ``` ### 问题引申 * 找到一个字符串里面某个字符数组里面第一个出现的字符的位置。比如“Hello, World”,[“a”, “e”, “i”, “o”, “u”],那 e 是在字符串第一个出现的字符,位置是 1, 返回 1 * 提示:zip函数 > 这个问题面试中问的最多! ``` swift let source = "Hello world" let target: [Character] = ["a","e","i","o","u"] zip(0.. 参考[Swift - zip函数使用详解(附样例)](https://www.hangge.com/blog/cache/detail_1829.html) # 总结 经过本篇文章学习你学会了? * 如何解决文本中计算单词出现的频率 * 如何找到一个字符串里面某个字符数组里面第一个出现的字符的位置 * swift中的zip()函数使用 技术要求我们不断学习,周末的时光不能浪费. 学习了一个下午,把重要的内容都记录了下来,如果你看的不是很懂建议认真学习一遍swift就会了. URL: https://sunyazhou.com/2021/10/SwiftSubSets/index.html.md Published At: 2021-10-16 08:30:00 +0000 # swift中求一个集合中子集 # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! 最近在学习一些Swift中的语法,有些比较有意思的内容我都记了笔记 ## 问题 给定一个集合,求这个集合中有多少真子集? #### 方法一 ``` swift import UIKit func getSubSets(set: Set) -> Array> { let count = 1 << set.count //set.count 不能超过64 否则将超过int最大数限制 let elements = Array(set) var subsets = [Set]() for i in 0..() for j in 0..> j) & 1) == 1 { subset.insert(elements[j]) } } subsets.append(subset) } return subsets } let testSet: Set = ["S","Y","Z"] for subSet in getSubSets(set: testSet) { print(subSet) } ``` 得出结果 ``` sh [] ["Y"] ["Z"] ["Y", "Z"] ["S"] ["Y", "S"] ["S", "Z"] ["Z", "Y", "S"] ``` #### 方法二 ``` swift func getSubSets(_ set: Set) -> Array> { let elements = Array(set) return getSubSetsDetail(elements, index: elements.count - 1, count: elements.count) } func getSubSetsDetail(_ elements: Array, index: Int, count: Int) -> Array> { var subSets = Array>() if index == 0 { subSets.append(Set()) var subset = Set() subset.insert(elements[0]) subSets.append(subset) return subSets } subSets = getSubSetsDetail(elements, index: index - 1, count: count) for subset in subSets { var currentSubset = Set(subset) currentSubset.insert(elements[index]) subSets.append(currentSubset) } return subSets } let testSet: Set = ["S","Y","Z"] for subSet in getSubSets(testSet) { print(subSet) } ``` 输出 ``` sh [] ["Y"] ["Z"] ["Y", "Z"] ["S"] ["S", "Y"] ["S", "Z"] ["S", "Y", "Z"] ``` # 总结 记录一些学习知识,防止遗忘. URL: https://sunyazhou.com/2021/06/CheckNSStringIsPureInteger/index.html.md Published At: 2021-06-23 00:30:00 +0000 # 如何判断NSString是纯数字类型 ![](/assets/images/20210623CheckNSStringIsPureInteger/pureinteger.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! ## 遇到的问题? 在iOS开发过程中是否有这样的需求,单纯判断NSString中是否是纯数字,如下面代码 ``` objc NSString *str1 = @"10003600"; NSString *str2 = @"ffdec500063143bf91f509255cb87cda"; ``` 我第一时间想到用正则匹配数字并连续,但是这样貌似不是便捷方式. 经过知识索引搜索,我找到了如下解决方式 ``` objc NSString *str = @"ffdec500063143bf91f509255cb87cda";//@"10003600"; NSScanner *scanner = [NSScanner scannerWithString:str1]; NSInteger intVal; BOOL result = ([scanner scanInteger:&intVal] && [scanner isAtEnd]); if (result) { NSLog(@"是纯整形"); } else { NSLog(@"非纯整形"); } ``` 使用`NSScanner `类来处理, 我猜测实现原理是逐个字符串`迭代便利`逐个字符探测并到达最后的长度时来判定当前字符串是纯数字类型. 为了方便起见我写了一个demo和 category方便大家使用. `.h`和`.m`文件 ``` objc #import "NSString+NumberTypeCheck.h" #import @interface NSString (NumberTypeCheck) /// 字符串是否是纯Int类型 - (BOOL)isPureInt; /// 字符串是否是纯NSInteger类型 - (BOOL)isPureInteger; /// 字符串是否是纯CGFloat(Double)类型 - (BOOL)isPureCGFloat; @end @implementation NSString (NumberTypeCheck) - (BOOL)isPureInt { NSScanner *scanner = [NSScanner scannerWithString:self]; int intVal; BOOL result = ([scanner scanInt:&intVal] && [scanner isAtEnd]); return result; } - (BOOL)isPureInteger { NSScanner *scanner = [NSScanner scannerWithString:self]; NSInteger intVal; BOOL result = ([scanner scanInteger:&intVal] && [scanner isAtEnd]); return result; } - (BOOL)isPureCGFloat { NSScanner *scanner = [NSScanner scannerWithString:self]; CGFloat floatVal; BOOL result = ([scanner scanDouble:&floatVal] && [scanner isAtEnd]); return result; } @end ``` # 总结 遇到问题需要不断探索,保持持续学习求真务实的态度. [demo地址](https://github.com/sunyazhou13/NSScanner) URL: https://sunyazhou.com/2021/04/WCDBPractice/index.html.md Published At: 2021-04-06 10:58:36 +0000 # WCDB实践记录 ![](/assets/images/20210406WCDBPractice/wcdb.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! ### 最近在忙啥? 洲哥最近在跟 WCDB 斗智斗勇,公司做 IM,一套很老的代码需要维护,为了打好底层基础,解决上层业务问题决定按批次从原来的 FMDB 迁移到 WCDB. ### 遇到的问题 在使用 WCDB 的过程中遇到了一个很低级的错误,就是 `where` 语句后面接的判断条件问题 先来看下代码和 业务问题的场景复现 ``` objc - (BOOL)updateMsgHeight:(SYMessage *)msg toTable:(NSString *)tableName { if (tableName.length == 0) { return NO; } if (msg.messageId.length == 0) { return NO; } BOOL result = [[self dataBase] updateRowsInTable:[self tablenameByID:tableName] onProperty:SYMessage.chatMsgHeight withValue:msg.chatMsgHeight where:{SYMessage.messageId == msg.messageId && SYMessage.type = msg.type}]; return result; } ``` ![](/assets/images/20210406WCDBPractice/chatlist.gif) `cell`的高度不对劲 经过 [FLEX工具](https://github.com/FLEXTool/FLEX) 查看数据库文件,发现数据库中的`chatMsgHeight`值都是一样的. ![](/assets/images/20210406WCDBPractice/chatlist2.gif) 经过我仔细排查了所有 SQL 语句发现有一处这样的调用 在控制台. ``` sh UPDATE msg_10003600 SET chatMsgHeight=? WHERE 1.000000 ``` 它正确的形式应该是这样的 ``` sh SQL: UPDATE msg_10003600 SET chatMsgHeight=? WHERE ((messageId='936542df77de41778139a42b4f4be296') AND (type=2)) ``` 想都不用想`SQL`语句 `where` 后的条件 一直为 `true` ,才会出现这种低级到不能在低级的错误. 纠正原有代码如下: ``` objc - (BOOL)updateMsgHeight:(SYMessage *)msg toTable:(NSString *)tableName { if (tableName.length == 0) { return NO; } if (msg.messageId.length == 0) { return NO; } BOOL result = [[self dataBase] updateRowsInTable:[self tablenameByID:tableName] onProperty:SYMessage.chatMsgHeight withValue:msg.chatMsgHeight where:{SYMessage.messageId == msg.messageId && SYMessage.type == msg.type}]; return result; } ``` 这个问题出在了`SYMessage.type == msg.type`而不是`SYMessage.type = msg.type`. > `==` 恒等于 > `=` 赋值 > WCDB 并没有提示错误因为这不属于错误,这属于正常的代码赋值,C++编译器也不会报错. 所以这个`坑` 小伙伴们还是注意.虽然明明是我少写了等号导致的,毕竟 WCDB 不是编译器不能为我们纠正`词法分析`以及`语法分析`和`语义分析`亦或是`文法分析`等 编译原理的知识错误. ### 说说使用 WCDB 后的真实体验 快! 方便!代码简洁! 我把聊天功能涉及到的聊天会话列表、消息列表、等核心模块全部迁移到 WCDB. 还有个坑需要小伙伴们注意、FMDB 迁移到 WCDB 一定切记 `要做就做彻底`、一次全部替换、否则很容易出现队列的死锁竞争.不信邪的可以试试. ### 开发文档归类 [WCDB - 腾讯开源的移动数据库框架](https://www.bookstack.cn/read/tencent-wcdb/66f893c12ef91f78.md) 这个连接国内打开比较快 [FLEX调试工具](https://github.com/FLEXTool/FLEX) 可以实时查看 app 中各种 UIView、网络、对象、内存、沙盒文件等等. # 总结 下一步准备看看WCDB源码实现,和分享一些经常使用的技巧, WCDB 让我重新学习了一遍数据库知识. URL: https://sunyazhou.com/2021/02/UTIsOfficalFileExtensionName/index.html.md Published At: 2021-02-21 10:40:31 +0000 # UTIs苹果官方的文件扩展名全集 ![](/assets/images/20210221UTIsOfficalFileExtensionName/UTIs.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! ## 文件扩展名 最近开发过程中遇到了一个很低级的问题,有点类似网盘app里面要区分各种文件类型,然后找出对应的文件归属icon并显示,做为一个极客精神的iOS开发者,我想找到一种文件扩展名集合并不需要自己命名枚举值,这时我想起2015年左右开发百度网盘时调研过的airdrop,其中苹果规定了分享文件到其它手机需要提供的`文件唯一标识(UTI)`,于是我找了一下文档记录一下如下的文件类型列表. 苹果的CoreServices.framework中有一个`UTCoreTypes.h`头文件记录了所有的文件类型. 下面是我去掉所有注释的代码.当然这些仅限于苹果内部可识别的类型,那些不太常用的类型文件或者自定义扩展名文件类型在这里就不赘述了,如果需要搞个通配类型即可,或者单独处理. ``` objc #ifndef __UTCORETYPES__ #define __UTCORETYPES__ #ifndef __COREFOUNDATION__ #include #endif #include #if PRAGMA_ONCE #pragma once #endif #ifdef __cplusplus extern "C" { #endif CF_ASSUME_NONNULL_BEGIN #pragma mark - Abstract base types extern const CFStringRef kUTTypeItem API_DEPRECATED_WITH_REPLACEMENT("UTTypeItem", ios(3.0, API_TO_BE_DEPRECATED), macos(10.4, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeContent API_DEPRECATED_WITH_REPLACEMENT("UTTypeContent", ios(3.0, API_TO_BE_DEPRECATED), macos(10.4, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeCompositeContent API_DEPRECATED_WITH_REPLACEMENT("UTTypeCompositeContent", ios(3.0, API_TO_BE_DEPRECATED), macos(10.4, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeMessage API_DEPRECATED_WITH_REPLACEMENT("UTTypeMessage", ios(3.0, API_TO_BE_DEPRECATED), macos(10.4, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeContact API_DEPRECATED_WITH_REPLACEMENT("UTTypeContact", ios(3.0, API_TO_BE_DEPRECATED), macos(10.4, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeArchive API_DEPRECATED_WITH_REPLACEMENT("UTTypeArchive", ios(3.0, API_TO_BE_DEPRECATED), macos(10.4, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeDiskImage API_DEPRECATED_WITH_REPLACEMENT("UTTypeDiskImage", ios(3.0, API_TO_BE_DEPRECATED), macos(10.4, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); #pragma mark - Concrete base types extern const CFStringRef kUTTypeData API_DEPRECATED_WITH_REPLACEMENT("UTTypeData", ios(3.0, API_TO_BE_DEPRECATED), macos(10.4, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeDirectory API_DEPRECATED_WITH_REPLACEMENT("UTTypeDirectory", ios(3.0, API_TO_BE_DEPRECATED), macos(10.4, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeResolvable API_DEPRECATED_WITH_REPLACEMENT("UTTypeResolvable", ios(3.0, API_TO_BE_DEPRECATED), macos(10.4, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeSymLink API_DEPRECATED_WITH_REPLACEMENT("UTTypeSymLink", ios(3.0, API_TO_BE_DEPRECATED), macos(10.4, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeExecutable API_DEPRECATED_WITH_REPLACEMENT("UTTypeExecutable", ios(8.0, API_TO_BE_DEPRECATED), macos(10.5, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeMountPoint API_DEPRECATED_WITH_REPLACEMENT("UTTypeMountPoint", ios(3.0, API_TO_BE_DEPRECATED), macos(10.4, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeAliasFile API_DEPRECATED_WITH_REPLACEMENT("UTTypeAliasFile", ios(3.0, API_TO_BE_DEPRECATED), macos(10.4, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeAliasRecord API_DEPRECATED("The Alias Manager is obsolete.", ios(3.0, API_TO_BE_DEPRECATED), macos(10.4, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeURLBookmarkData API_DEPRECATED_WITH_REPLACEMENT("UTTypeURLBookmarkData", ios(8.0, API_TO_BE_DEPRECATED), macos(10.10, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); #pragma mark - URL types extern const CFStringRef kUTTypeURL API_DEPRECATED_WITH_REPLACEMENT("UTTypeURL", ios(3.0, API_TO_BE_DEPRECATED), macos(10.4, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeFileURL API_DEPRECATED_WITH_REPLACEMENT("UTTypeFileURL", ios(3.0, API_TO_BE_DEPRECATED), macos(10.4, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); #pragma mark - Text types extern const CFStringRef kUTTypeText API_DEPRECATED_WITH_REPLACEMENT("UTTypeText", ios(3.0, API_TO_BE_DEPRECATED), macos(10.4, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypePlainText API_DEPRECATED_WITH_REPLACEMENT("UTTypePlainText", ios(3.0, API_TO_BE_DEPRECATED), macos(10.4, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeUTF8PlainText API_DEPRECATED_WITH_REPLACEMENT("UTTypeUTF8PlainText", ios(3.0, API_TO_BE_DEPRECATED), macos(10.4, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeUTF16ExternalPlainText API_DEPRECATED_WITH_REPLACEMENT("UTTypeUTF16ExternalPlainText", ios(3.0, API_TO_BE_DEPRECATED), macos(10.4, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeUTF16PlainText API_DEPRECATED_WITH_REPLACEMENT("UTTypeUTF16PlainText", ios(3.0, API_TO_BE_DEPRECATED), macos(10.4, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeDelimitedText API_DEPRECATED_WITH_REPLACEMENT("UTTypeDelimitedText", ios(8.0, API_TO_BE_DEPRECATED), macos(10.10, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeCommaSeparatedText API_DEPRECATED_WITH_REPLACEMENT("UTTypeCommaSeparatedText", ios(8.0, API_TO_BE_DEPRECATED), macos(10.10, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeTabSeparatedText API_DEPRECATED_WITH_REPLACEMENT("UTTypeTabSeparatedText", ios(8.0, API_TO_BE_DEPRECATED), macos(10.10, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeUTF8TabSeparatedText API_DEPRECATED_WITH_REPLACEMENT("UTTypeUTF8TabSeparatedText", ios(8.0, API_TO_BE_DEPRECATED), macos(10.10, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeRTF API_DEPRECATED_WITH_REPLACEMENT("UTTypeRTF", ios(3.0, API_TO_BE_DEPRECATED), macos(10.4, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); #pragma mark - Markup languages extern const CFStringRef kUTTypeHTML API_DEPRECATED_WITH_REPLACEMENT("UTTypeHTML", ios(3.0, API_TO_BE_DEPRECATED), macos(10.4, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeXML API_DEPRECATED_WITH_REPLACEMENT("UTTypeXML", ios(3.0, API_TO_BE_DEPRECATED), macos(10.4, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); #pragma mark - Programming languages extern const CFStringRef kUTTypeSourceCode API_DEPRECATED_WITH_REPLACEMENT("UTTypeSourceCode", ios(3.0, API_TO_BE_DEPRECATED), macos(10.4, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeAssemblyLanguageSource API_DEPRECATED_WITH_REPLACEMENT("UTTypeAssemblyLanguageSource", ios(8.0, API_TO_BE_DEPRECATED), macos(10.10, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeCSource API_DEPRECATED_WITH_REPLACEMENT("UTTypeCSource", ios(3.0, API_TO_BE_DEPRECATED), macos(10.4, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeObjectiveCSource API_DEPRECATED_WITH_REPLACEMENT("UTTypeObjectiveCSource", ios(3.0, API_TO_BE_DEPRECATED), macos(10.4, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeSwiftSource API_DEPRECATED_WITH_REPLACEMENT("UTTypeSwiftSource", ios(9.0, API_TO_BE_DEPRECATED), macos(10.11, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(2.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeCPlusPlusSource API_DEPRECATED_WITH_REPLACEMENT("UTTypeCPlusPlusSource", ios(3.0, API_TO_BE_DEPRECATED), macos(10.4, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeObjectiveCPlusPlusSource API_DEPRECATED_WITH_REPLACEMENT("UTTypeObjectiveCPlusPlusSource", ios(3.0, API_TO_BE_DEPRECATED), macos(10.4, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeCHeader API_DEPRECATED_WITH_REPLACEMENT("UTTypeCHeader", ios(3.0, API_TO_BE_DEPRECATED), macos(10.4, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeCPlusPlusHeader API_DEPRECATED_WITH_REPLACEMENT("UTTypeCPlusPlusHeader", ios(3.0, API_TO_BE_DEPRECATED), macos(10.4, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeJavaSource API_DEPRECATED("Java support is no longer provided by this operating system. Install a JDK to use Java.", ios(3.0, API_TO_BE_DEPRECATED), macos(10.4, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); #pragma mark - Scripting languages extern const CFStringRef kUTTypeScript API_DEPRECATED_WITH_REPLACEMENT("UTTypeScript", ios(8.0, API_TO_BE_DEPRECATED), macos(10.10, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeAppleScript API_DEPRECATED_WITH_REPLACEMENT("UTTypeAppleScript", ios(8.0, API_TO_BE_DEPRECATED), macos(10.10, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeOSAScript API_DEPRECATED_WITH_REPLACEMENT("UTTypeOSAScript", ios(8.0, API_TO_BE_DEPRECATED), macos(10.10, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeOSAScriptBundle API_DEPRECATED_WITH_REPLACEMENT("UTTypeOSAScriptBundle", ios(8.0, API_TO_BE_DEPRECATED), macos(10.10, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeJavaScript API_DEPRECATED_WITH_REPLACEMENT("UTTypeJavaScript", ios(8.0, API_TO_BE_DEPRECATED), macos(10.10, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeShellScript API_DEPRECATED_WITH_REPLACEMENT("UTTypeShellScript", ios(8.0, API_TO_BE_DEPRECATED), macos(10.10, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypePerlScript API_DEPRECATED_WITH_REPLACEMENT("UTTypePerlScript", ios(8.0, API_TO_BE_DEPRECATED), macos(10.10, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypePythonScript API_DEPRECATED_WITH_REPLACEMENT("UTTypePythonScript", ios(8.0, API_TO_BE_DEPRECATED), macos(10.10, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeRubyScript API_DEPRECATED_WITH_REPLACEMENT("UTTypeRubyScript", ios(8.0, API_TO_BE_DEPRECATED), macos(10.10, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypePHPScript API_DEPRECATED_WITH_REPLACEMENT("UTTypePHPScript", ios(8.0, API_TO_BE_DEPRECATED), macos(10.10, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); #pragma mark - Serialized data types extern const CFStringRef kUTTypeJSON API_DEPRECATED_WITH_REPLACEMENT("UTTypeJSON", ios(8.0, API_TO_BE_DEPRECATED), macos(10.10, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypePropertyList API_DEPRECATED_WITH_REPLACEMENT("UTTypePropertyList", ios(8.0, API_TO_BE_DEPRECATED), macos(10.10, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeXMLPropertyList API_DEPRECATED_WITH_REPLACEMENT("UTTypeXMLPropertyList", ios(8.0, API_TO_BE_DEPRECATED), macos(10.10, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeBinaryPropertyList API_DEPRECATED_WITH_REPLACEMENT("UTTypeBinaryPropertyList", ios(8.0, API_TO_BE_DEPRECATED), macos(10.10, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); #pragma mark - Composite content types extern const CFStringRef kUTTypePDF API_DEPRECATED_WITH_REPLACEMENT("UTTypePDF", ios(3.0, API_TO_BE_DEPRECATED), macos(10.4, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeRTFD API_DEPRECATED_WITH_REPLACEMENT("UTTypeRTFD", ios(3.0, API_TO_BE_DEPRECATED), macos(10.4, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeFlatRTFD API_DEPRECATED_WITH_REPLACEMENT("UTTypeFlatRTFD", ios(3.0, API_TO_BE_DEPRECATED), macos(10.4, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeTXNTextAndMultimediaData API_DEPRECATED("The Multilingual Text Engine is obsolete.", ios(3.0, API_TO_BE_DEPRECATED), macos(10.4, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeWebArchive API_DEPRECATED_WITH_REPLACEMENT("UTTypeWebArchive", ios(3.0, API_TO_BE_DEPRECATED), macos(10.4, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); #pragma mark - Image content types extern const CFStringRef kUTTypeImage API_DEPRECATED_WITH_REPLACEMENT("UTTypeImage", ios(3.0, API_TO_BE_DEPRECATED), macos(10.4, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeJPEG API_DEPRECATED_WITH_REPLACEMENT("UTTypeJPEG", ios(3.0, API_TO_BE_DEPRECATED), macos(10.4, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeJPEG2000 API_DEPRECATED("JPEG2000 is no longer supported by this operating system.", ios(3.0, API_TO_BE_DEPRECATED), macos(10.4, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeTIFF API_DEPRECATED_WITH_REPLACEMENT("UTTypeTIFF", ios(3.0, API_TO_BE_DEPRECATED), macos(10.4, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypePICT API_DEPRECATED("QuickDraw is obsolete.", ios(3.0, API_TO_BE_DEPRECATED), macos(10.4, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeGIF API_DEPRECATED_WITH_REPLACEMENT("UTTypeGIF", ios(3.0, API_TO_BE_DEPRECATED), macos(10.4, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypePNG API_DEPRECATED_WITH_REPLACEMENT("UTTypePNG", ios(3.0, API_TO_BE_DEPRECATED), macos(10.4, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeQuickTimeImage API_DEPRECATED("The QuickTime Image file format is obsolete.", ios(3.0, API_TO_BE_DEPRECATED), macos(10.4, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeAppleICNS API_DEPRECATED_WITH_REPLACEMENT("UTTypeICNS", ios(3.0, API_TO_BE_DEPRECATED), macos(10.4, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeBMP API_DEPRECATED_WITH_REPLACEMENT("UTTypeBMP", ios(3.0, API_TO_BE_DEPRECATED), macos(10.4, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeICO API_DEPRECATED_WITH_REPLACEMENT("UTTypeICO", ios(3.0, API_TO_BE_DEPRECATED), macos(10.4, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeRawImage API_DEPRECATED_WITH_REPLACEMENT("UTTypeRAWImage", ios(8.0, API_TO_BE_DEPRECATED), macos(10.10, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeScalableVectorGraphics API_DEPRECATED_WITH_REPLACEMENT("UTTypeSVG", ios(8.0, API_TO_BE_DEPRECATED), macos(10.10, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeLivePhoto API_DEPRECATED_WITH_REPLACEMENT("UTTypeLivePhoto", ios(9.1, API_TO_BE_DEPRECATED), macos(10.12, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(2.1, API_TO_BE_DEPRECATED)); #pragma mark - Audiovisual content types extern const CFStringRef kUTTypeAudiovisualContent API_DEPRECATED_WITH_REPLACEMENT("UTTypeAudiovisualContent", ios(3.0, API_TO_BE_DEPRECATED), macos(10.4, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeMovie API_DEPRECATED_WITH_REPLACEMENT("UTTypeMovie", ios(3.0, API_TO_BE_DEPRECATED), macos(10.4, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeVideo API_DEPRECATED_WITH_REPLACEMENT("UTTypeVideo", ios(3.0, API_TO_BE_DEPRECATED), macos(10.4, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeAudio API_DEPRECATED_WITH_REPLACEMENT("UTTypeAudio", ios(3.0, API_TO_BE_DEPRECATED), macos(10.4, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeQuickTimeMovie API_DEPRECATED_WITH_REPLACEMENT("UTTypeQuickTimeMovie", ios(3.0, API_TO_BE_DEPRECATED), macos(10.4, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeMPEG API_DEPRECATED_WITH_REPLACEMENT("UTTypeMPEG", ios(3.0, API_TO_BE_DEPRECATED), macos(10.4, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeMPEG2Video API_DEPRECATED_WITH_REPLACEMENT("UTTypeMPEG2Video", ios(8.0, API_TO_BE_DEPRECATED), macos(10.10, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeMPEG2TransportStream API_DEPRECATED_WITH_REPLACEMENT("UTTypeMPEG2TransportStream", ios(8.0, API_TO_BE_DEPRECATED), macos(10.10, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeMP3 API_DEPRECATED_WITH_REPLACEMENT("UTTypeMP3", ios(3.0, API_TO_BE_DEPRECATED), macos(10.4, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeMPEG4 API_DEPRECATED_WITH_REPLACEMENT("UTTypeMPEG4Video", ios(3.0, API_TO_BE_DEPRECATED), macos(10.4, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeMPEG4Audio API_DEPRECATED_WITH_REPLACEMENT("UTTypeMPEG4Audio", ios(3.0, API_TO_BE_DEPRECATED), macos(10.4, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeAppleProtectedMPEG4Audio API_DEPRECATED_WITH_REPLACEMENT("UTTypeAppleProtectedMPEG4Audio", ios(3.0, API_TO_BE_DEPRECATED), macos(10.4, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeAppleProtectedMPEG4Video API_DEPRECATED_WITH_REPLACEMENT("UTTypeAppleProtectedMPEG4Video", ios(8.0, API_TO_BE_DEPRECATED), macos(10.10, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeAVIMovie API_DEPRECATED_WITH_REPLACEMENT("UTTypeAVI", ios(8.0, API_TO_BE_DEPRECATED), macos(10.10, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeAudioInterchangeFileFormat API_DEPRECATED_WITH_REPLACEMENT("UTTypeAIFF", ios(8.0, API_TO_BE_DEPRECATED), macos(10.10, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeWaveformAudio API_DEPRECATED_WITH_REPLACEMENT("UTTypeWAV", ios(8.0, API_TO_BE_DEPRECATED), macos(10.10, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeMIDIAudio API_DEPRECATED_WITH_REPLACEMENT("UTTypeMIDI", ios(8.0, API_TO_BE_DEPRECATED), macos(10.10, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypePlaylist API_DEPRECATED_WITH_REPLACEMENT("UTTypePlaylist", ios(8.0, API_TO_BE_DEPRECATED), macos(10.10, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeM3UPlaylist API_DEPRECATED_WITH_REPLACEMENT("UTTypeM3UPlaylist", ios(8.0, API_TO_BE_DEPRECATED), macos(10.10, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); #pragma mark - Directory types extern const CFStringRef kUTTypeFolder API_DEPRECATED_WITH_REPLACEMENT("UTTypeFolder", ios(3.0, API_TO_BE_DEPRECATED), macos(10.4, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeVolume API_DEPRECATED_WITH_REPLACEMENT("UTTypeVolume", ios(3.0, API_TO_BE_DEPRECATED), macos(10.4, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypePackage API_DEPRECATED_WITH_REPLACEMENT("UTTypePackage", ios(3.0, API_TO_BE_DEPRECATED), macos(10.4, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeBundle API_DEPRECATED_WITH_REPLACEMENT("UTTypeBundle", ios(3.0, API_TO_BE_DEPRECATED), macos(10.4, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypePluginBundle API_DEPRECATED_WITH_REPLACEMENT("UTTypePluginBundle", ios(8.0, API_TO_BE_DEPRECATED), macos(10.10, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeSpotlightImporter API_DEPRECATED_WITH_REPLACEMENT("UTTypeSpotlightImporter", ios(8.0, API_TO_BE_DEPRECATED), macos(10.10, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeQuickLookGenerator API_DEPRECATED_WITH_REPLACEMENT("UTTypeQuickLookGenerator", ios(8.0, API_TO_BE_DEPRECATED), macos(10.10, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeXPCService API_DEPRECATED_WITH_REPLACEMENT("UTTypeXPCService", ios(8.0, API_TO_BE_DEPRECATED), macos(10.10, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeFramework API_DEPRECATED_WITH_REPLACEMENT("UTTypeFramework", ios(3.0, API_TO_BE_DEPRECATED), macos(10.4, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); #pragma mark - Application and executable types // Abstract executable types extern const CFStringRef kUTTypeApplication API_DEPRECATED_WITH_REPLACEMENT("UTTypeApplication", ios(3.0, API_TO_BE_DEPRECATED), macos(10.4, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeApplicationBundle API_DEPRECATED_WITH_REPLACEMENT("UTTypeApplicationBundle", ios(3.0, API_TO_BE_DEPRECATED), macos(10.4, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeApplicationFile API_DEPRECATED_WITH_REPLACEMENT("Classic applications are obsolete.", ios(3.0, API_TO_BE_DEPRECATED), macos(10.4, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeUnixExecutable API_DEPRECATED_WITH_REPLACEMENT("UTTypeUnixExecutable", ios(8.0, API_TO_BE_DEPRECATED), macos(10.10, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); // Other platform binaries extern const CFStringRef kUTTypeWindowsExecutable API_DEPRECATED_WITH_REPLACEMENT("UTTypeEXE", ios(8.0, API_TO_BE_DEPRECATED), macos(10.10, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeJavaClass API_DEPRECATED("Java support is no longer provided by this operating system. Install a JDK to use Java.", ios(8.0, API_TO_BE_DEPRECATED), macos(10.10, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeJavaArchive API_DEPRECATED("Java support is no longer provided by this operating system. Install a JDK to use Java.", ios(8.0, API_TO_BE_DEPRECATED), macos(10.10, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); // Misc. binaries extern const CFStringRef kUTTypeSystemPreferencesPane API_DEPRECATED_WITH_REPLACEMENT("UTTypeSystemPreferencesPane", ios(8.0, API_TO_BE_DEPRECATED), macos(10.10, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); #pragma mark - Archival and compression types extern const CFStringRef kUTTypeGNUZipArchive API_DEPRECATED_WITH_REPLACEMENT("UTTypeGZIP", ios(8.0, API_TO_BE_DEPRECATED), macos(10.10, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeBzip2Archive API_DEPRECATED_WITH_REPLACEMENT("UTTypeBZ2", ios(8.0, API_TO_BE_DEPRECATED), macos(10.10, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeZipArchive API_DEPRECATED_WITH_REPLACEMENT("UTTypeZIP", ios(8.0, API_TO_BE_DEPRECATED), macos(10.10, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); #pragma mark - Document types extern const CFStringRef kUTTypeSpreadsheet API_DEPRECATED_WITH_REPLACEMENT("UTTypeSpreadsheet", ios(8.0, API_TO_BE_DEPRECATED), macos(10.10, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypePresentation API_DEPRECATED_WITH_REPLACEMENT("UTTypePresentation", ios(8.0, API_TO_BE_DEPRECATED), macos(10.10, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeDatabase API_DEPRECATED_WITH_REPLACEMENT("UTTypeDatabase", ios(8.0, API_TO_BE_DEPRECATED), macos(10.4, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); #pragma mark - Messages, contacts, and calendar types extern const CFStringRef kUTTypeVCard API_DEPRECATED_WITH_REPLACEMENT("UTTypeVCard", ios(3.0, API_TO_BE_DEPRECATED), macos(10.4, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeToDoItem API_DEPRECATED_WITH_REPLACEMENT("UTTypeToDoItem", ios(8.0, API_TO_BE_DEPRECATED), macos(10.10, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeCalendarEvent API_DEPRECATED_WITH_REPLACEMENT("UTTypeCalendarEvent", ios(8.0, API_TO_BE_DEPRECATED), macos(10.10, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeEmailMessage API_DEPRECATED_WITH_REPLACEMENT("UTTypeEmailMessage", ios(8.0, API_TO_BE_DEPRECATED), macos(10.10, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); #pragma mark - Internet locations extern const CFStringRef kUTTypeInternetLocation API_DEPRECATED_WITH_REPLACEMENT("UTTypeInternetLocation", ios(8.0, API_TO_BE_DEPRECATED), macos(10.10, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); #pragma mark - Miscellaneous types extern const CFStringRef kUTTypeInkText API_DEPRECATED("The Ink framework is obsolete.", ios(3.0, API_TO_BE_DEPRECATED), macos(10.4, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeFont API_DEPRECATED_WITH_REPLACEMENT("UTTypeFont", ios(8.0, API_TO_BE_DEPRECATED), macos(10.10, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeBookmark API_DEPRECATED_WITH_REPLACEMENT("UTTypeBookmark", ios(8.0, API_TO_BE_DEPRECATED), macos(10.10, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTType3DContent API_DEPRECATED_WITH_REPLACEMENT("UTType3DContent", ios(8.0, API_TO_BE_DEPRECATED), macos(10.10, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypePKCS12 API_DEPRECATED_WITH_REPLACEMENT("UTTypePKCS12", ios(8.0, API_TO_BE_DEPRECATED), macos(10.10, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeX509Certificate API_DEPRECATED_WITH_REPLACEMENT("UTTypeX509Certificate", ios(8.0, API_TO_BE_DEPRECATED), macos(10.10, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeElectronicPublication API_DEPRECATED_WITH_REPLACEMENT("UTTypeEPUB", ios(8.0, API_TO_BE_DEPRECATED), macos(10.10, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); extern const CFStringRef kUTTypeLog API_DEPRECATED_WITH_REPLACEMENT("UTTypeLog", ios(8.0, API_TO_BE_DEPRECATED), macos(10.10, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(1.0, API_TO_BE_DEPRECATED)); CF_ASSUME_NONNULL_END #ifdef __cplusplus } #endif #endif /* __UTCORETYPES__ */ ``` | Identifier (Constant) 文件类型 | Conforms to | Tags | Comments| |-----|-----|-----|-----| | public.item (kUTTypeItem) | - | | Base type for the physical hierarchy. | | public.content (kUTTypeContent) | - | | Base type for all document content. | | public.composite-content (kUTTypeCompositeContent) | public.content | | Base type for mixed content. For example, a PDF file contains both text and special formatting data. | | public.data (kUTTypeData) | public.item | | Base physical type for byte streams (flat files, pasteboard data, and so on). | | public.database | - | | Base functional type for databases. | | public.calendar-event | - | | Base functional type for scheduled events. | | public.message (kUTTypeMessage) | - | | Base type for messages (email, IM, and so on). | | public.presentation | public.composite-content | | Base type for presentations. | | public.contact (kUTTypeContact) | - | | Base type for contact information. | | public.archive (kUTTypeArchive) | - | | Base type for an archive of files and directories. | | public.disk-image (kUTTypeDiskImage) | public.archive | | Base type for items mountable as a volume. | | public.text (kUTTypeText) | public.content, public.data | | Base type for all text, including text with markup information (HTML, RTF, and so on). | | public.plain-text (kUTTypePlainText) | public.text | .txt, text/plain | Text of unspecified encoding, with no markup. Equivalent to the MIME type text/plain | | public.utf8-plain-text (kUTTypeUTF8PlainText) | public.plain-text | 'utf8', NSStringPBoardType | Unicode-8 | | public.utf16-external-plain-​text (kUTTypeUTF16ExternalPlain​Text) | public.plain-text | 'ut16' | Unicode-16 with byte-order mark (BOM), or if BOM is not present, an external representation byte order (big-endian). | | public.utf16-plain-text (kUTTypeUTF16PlainText) | public.plain-text | 'utxt' | Unicode-16, native byte order, with an optional byte-order mark (BOM). | | com.apple.traditional-mac-​plain-text | public.plain-text | 'TEXT' | Classic Mac OS text. | | public.rtf (kUTTypeRTF) | public.text | 'RTF ', .rtf, text/rtf, NeXT Rich Text Format 1.0 pasteboard type, NSRTFPBoardType | Rich Text. | | com.apple.ink.inktext (kUTTypeInkText) | public.data | | Opaque InkText data. | | public.html (kUTTypeHTML) | public.text | 'HTML', .html, .htm, text/html, Apple HTML pasteboard type | HTML text. | | public.xml (kUTTypeXML) | public.text | .xml, text/xml | XML text. | | public.source-code (kUTTypeSourceCode) | public.plain-text | | Generic source code. | | public.c-source (kUTTypeCSource) | public.source-code | .c | C source code. | | public.objective-c-source (kUTTypeObjectiveCSource) | public.source-code | .m | Objective-C source code. | | public.c-plus-plus-source (kUTTypeCPlusPlusSource) | public.source-code | .cp, .cpp, .c++, .cc, .cxx | C++ source code. | | public.objective-c-plus-​plus-source (kUTTypeObjectiveC​PlusPlusSource) | public.source-code | .mm | Objective-C++ source code. | | public.c-header (kUTTypeCHeader) | public.source-code | .h | C header file. | | public.c-plus-plus-header (kUTTypeCPlusPlusHeader) | public.source-code | .hpp, .h++ , .hxx | C++ header file. | | com.sun.java-source (kUTTypeJavaSource) | public.source-code | .java, .jav | Java source code | | public.script | public.source-code | | Base type for scripting language source code. | | public.assembly-source | public.source-code | .s | Assembly language source code. | | com.apple.rez-source | public.source-code | .r | Rez source code. | | public.mig-source | public.source-code | .defs, .mig | Mig definition source code. | | com.apple.symbol-export | public.source-code | .exp | Symbol export list. | | com.netscape.javascript-​source | public.source-code, public.executable | .js, .jscript, .javascript, text/javascript | JavaScript. | | public.shell-script | public.script | .sh, .command | Shell script. | | public.csh-script | public.shell-script | .csh | C-shell script. | | public.perl-script | public.shell-script | .pl, .pm, text/x-perl-script | Perl script. | | public.python-script | public.shell-script | .py, text/x-python-script | Python script. | | public.ruby-script | public.shell-script | .rb, .rbw, text/ruby-script | Ruby script. | | public.php-script | public.shell-script | .php, .php3, .php4, .ph3, .ph4, .phtml, text/x-php-script, text/php, application/php | PHP script. | | com.sun.java-web-start | public.xml | .jnlp, application/x-java, jnlp-file, application/jnlp | Java web start. | | com.apple.applescript.text | public.script | .applescript | AppleScript text. | | com.apple.applescript.​script | public.data | .scpt, 'osas' | AppleScript. | | public.object-code | public.data, public.executable | .o | Object code. | | com.apple.mach-o-binary | public.data, public.executable | | Mach-O binary. | | com.apple.pef-binary | public.data, public.executable | | PEF (CFM-based) binary | | com.microsoft.windows-​executable | public.data, public.executable | .exe, application/x-msdownload | Microsoft Windows application. | | com.microsoft.windows-​dynamic-link-library | public.data, public.executable | .dll, application/x-msdownload | Microsoft dynamic link library. | | com.sun.java-class | public.data, public.executable | .class | Java class. | | com.sun.java-archive | public.data, public.executable, public.archive | .jar , application/java-archive | Java archive. | | com.apple.quartz-​composer-composition | public.data, public.executable | .qtz , application/x-quartzcomposer | Quartz Composer composition. | | org.gnu.gnu-tar-archive | public.data, public.archive | .gtar, application/x-gtar | GNU archive. | | public.tar-archive | org.gnu.gnu-tar-archive | .tar, application/x-tar, application/tar | Tar archive. | | org.gnu.gnu-zip-archive | public.data, public.archive | .gz, .gzip, application/x-gzip, application/gzip | Gzip archive. | | org.gnu.gnu-zip-tar-archive | org.gnu.gnu-zip-archve | .tgz | Gzip tar archive. | | com.apple.binhex-archive | public.data, public.archive | .hqx, application/mac-binhex40, application/mac-binhex, application/binhex | BinHex archive. | | com.apple.macbinary-​archive | public.data, public.archive | .bin, application/x-macbinary, application/macbinary | MacBinary archive. | | public.url (kUTTypeURL) | public.data | 'url ' | Uniform Resource Locator. | | public.file-url (kUTTypeFileURL) | public.url | 'furl' | File URL. | | public.url-name | - | 'urln' | URL name. | | public.vcard (kUTTypeVCard) | public.data, public.content | 'vCrd', .vcf, .vcard, text/directory, text/vcard, text/x-vcard, Apple Vcard, pasteboard type | vCard (electronic business card). | | public.image (kUTTypeImage) | public.data, public.content | | Base type for images. | | public.fax | public.image | | Base type for fax images. | | public.jpeg (kUTTypeJPEG) | public.image | 'JPEG', .jpg, .jpeg, image/jpeg | JPEG image. | | public.jpeg-2000 (kUTTypeJPEG2000) | public.image | 'jp2 ', .jp2, image/jp2 | JPEG 2000 image. | | public.tiff (kUTTypeTIFF) | public.image | 'TIFF', .tif, .tiff, image/tiff, NeXT TIFF v4.0 pasteboard type, NSTIFFPBoardType | TIFF image. | | public.camera-raw-image | public.image | | Base type for digital camera raw image formats. | | com.apple.pict (kUTTypePICT) | public.image | 'PICT', .pic, .pct, .pict, image/pict, image/x-pict, image/x-macpict | PICT image | | com.apple.macpaint-image | public.image | .pntg, 'PNTG' | MacPaint image. | | public.png (kUTTypePNG) | public.image | 'PNGf', .png, image/png | PNG image | | public.xbitmap-image | public.image | .xbm, image/x-quicktime | X bitmap image. | | com.apple.quicktime-image (kUTTypeQuickTimeImage) | public.image | 'qtif', .qif, .qtif, image/x-quicktime | QuickTime image. | | com.apple.icns (kUTTypeAppleICNS) | public.image | 'icns', .icns | Mac OS icon image. | | com.apple.txn.text-​multimedia-data (kUTTypeTXNTextAnd​MultimediaData) | public.data, public.composite-​content | 'txtn' | MLTE (Textension) format for mixed text and multimedia data. | | public.audiovisual-​content (kUTTypeAudioVisual​Content) | public.data, public.content | | Base type for any audiovisual content. | | public.movie | public.audiovisual-​content | | Base type for movies (video with optional audio or other tracks). | | public.video (kUTTypeVideo) | public.movie | | Base type for video (no audio). | | com.apple.quicktime-movie (kUTTypeQuickTimeMovie) | public.movie | 'MooV', .mov, .qt, video/quicktime | QuickTime movie. | | public.avi | public.movie | .avi, .vfw, 'Vfw ', video/avi, video/msvideo, video/x-msvideo | AVI movie. | | public.mpeg (kUTTypeMPEG) | public.movie | 'MPG ', 'MPEG', .mpg, .mpeg, .m75, .m15, video/mpg, video/mpeg, video/x-mpg, video/x-mpeg | MPEG-1 or MPEG-2 content. | | public.mpeg-4 (kUTTypeMPEG4) | public.movie | 'mpg4', .mp4, video/mp4, video/mp4v | MPEG-4 content. | | public.3gpp | public.movie | .3gp, .3gpp, '3gpp', video/3gpp, audio/3gpp | 3GPP movie. | | public.3gpp2 | public.movie | .3g2 , .3gp2 , '3gp2', video/3gpp2, audio/3gpp2 | 3GPP2 movie. | | public.audio (kUTTypeAudio) | public.audiovisual-​content | | Base type for audio (no video). | | public.mp3 (kUTTypeMP3) | public.audio | 'MPG3', 'mpg3', 'Mp3 ', 'MP3 ', 'mp3!', 'MP3!', .mp3, audio/mpeg, audio/mpeg3, audio/mpg, audio/mp3, audio/x-mpeg, audio/x-mpeg3, audio/x-mpg, audio/x-mp3 | MPEG-3 audio. | | public.mpeg-4-audio (kUTTypeMPEG4Audio) | public.audio, public.mpeg4 | 'M4A ', .m4a | MPEG-4 audio. | | com.apple.protected-​mpeg-4-audio (kUTTypeAppleProtected​MPEG4Audio) | public.audio | 'M4P ', 'M4B ', .m4p, .m4b | Protected MPEG-4 audio. (iTunes music store format) | | public.ulaw-audio | public.audio | .au, .ulw, .snd, 'ULAW', audio/basic, audio/au, audio/snd | μLaw audio. | | public.aifc-audio | public.audio | .aifc, .aiff, .aif, 'AIFC', audio/aiff, audio/x-aiff | AIFF-C audio. | | public.aiff-audio | public.audio | .aiff, .aif, 'AIFF', audio/aiff, audio/x-aiff | AIFF audio. | | com.apple.coreaudio-​format | public.audio | .caf, 'caff' | Core Audio format. | | public.directory (kUTTypeDirectory) | public.item | | Base type for directories. | | public.folder (kUTTypeFolder) | public.directory | | A plain folder (that is, not a package). | | public.volume (kUTTypeVolume) | public.folder | | A volume. | | com.apple.package (kUTTypePackage) | public.directory | | A package (that is, a directory presented to the user as a file). | | com.apple.bundle (kUTTypeBundle) | public.directory | 'BNDL', .bundle | A directory with an internal structure specified by Core Foundation Bundle Services. . | | public.executable | - | | Base type for executable data. | | com.apple.application (kUTTypeApplication) | public.executable | | Base type for applications and other launchable files. | | com.apple.application-​bundle (kUTTypeApplicationBundle) | com.apple.package, com.apple.bundle, com.apple.application | 'APPL', .app | Application bundle. | | com.apple.application-file (kUTTypeApplicationFile) | com.apple.application public.data | 'APPL' | Application file. | | com.apple.deprecated-​application-file | com.apple.application​-file | 'APPC', 'APPD', 'APPE', 'appe', 'CDEV', 'cdev', 'dfil' | Deprecated application file. | | com.apple.plugin | com.apple.bundle, com.apple.package | .plugin | Plugin. | | com.apple.metadata-​importer | com.apple.plugin | .mdimporter | Spotlight importer plugin. | | com.apple.dashboard-​widget | com.apple.bundle, com.apple.package | .wdgt | Dashboard widget. | | public.cpio-archive | public.data | .cpio | CPIO archive. | | com.pkware.zip-archive | public.data, public.archive | .zip, application/zip | Zip archive. | | com.apple.webarchive (kUTTypeWebArchive) | public.data, public.composite-​content | | Web Kit webarchive format. | | com.apple.framework (kUTTypeFramework) | com.apple.bundle | 'FMWK', .framework | Framework. | | com.apple.rtfd (kUTTypeRTFD) | com.apple.package, public.composite-​content | .rtfd | Rich Text Format Directory. That is, Rich Text with content embedding, on-disk format. | | com.apple.flat-rtfd (kUTTypeFlatRTFD) | public.data, public.composite-​content | NeXT RTFD pasteboard type, NSRTFDPBoardType | Rich Text with content embedding, pasteboard format. | | com.apple.resolvable (kUTTypeResolvable) | - | | Items that the Alias Manager can resolve. | | public.symlink (kUTTypeSymLink) | public.item, com.apple.resolvable | | UNIX-style symlink. | | com.apple.mount-point (kUTTypeMountPoint) | public.item, com.apple.resolvable | | A volume mount point | | com.apple.alias-record (kUTTypeAliasRecord) | public.data, com.apple.resolvable | 'alis' | Alias record. | | com.apple.alias-file (kUTTypeAliasFile) | public.data, com.apple.resolvable | | Alias file. | | public.font | public.data | | Base type for fonts. | | public.truetype-font | public.font | | TrueType font. | | com.adobe.postscript-font | public.font | | PostScript font. | | com.apple.truetype-​datafork-suitcase-font | public.truetype-font | .dfont, 'dfon' | TrueType data fork font. | | public.opentype-font | public.font | .otf, 'OTTO' | PostScript OpenType font. | | public.truetype-ttf-font | public.truetype-font | .ttf | TrueType OpenType font. | | public.truetype-collection-​font | public.font | .ttc, 'ttcf' | TrueType collection font. | | com.apple.font-suitcase | public.font | .suit, 'FFIL', 'ffil', 'sfnt', 'tfil' | Font suitcase. | | com.adobe.postscript-lwfn​-font | com.adobe.postscript-​font | 'LWFN' | PostScript Type 1 outline font. | | com.adobe.postscript-pfb-​font | com.adobe.postscript-​font | .pfb | PostScriptType1 outline font. | | com.adobe.postscript.pfa-​font | com.adobe.postscript-​font | .pfa | PostScriptType 1 outline font. | | com.apple.colorsync-profile | public.data | .icc, .icm, .pf , 'prof' | ColorSync profile. | | Identifier | Conforms to | Comments | | public.filename-extension | public.case-insensitive-text | Filename extension. | | public.mime-type | public.case-insensitive-text | MIME type. | | com.apple.ostype | public.text | Four-character code (type OSType). | | com.apple.nspboard-type | public.text | NSPasteboard type. | | Identifier (Constant) | Conforms to | Tags | Comments | | com.adobe.pdf (kUTTypePDF) | public.data, public.composite-​content | 'PDF ', .pdf, application/pdf, Apple PDF pasteboard type | PDF data. | | com.adobe.postscript | public.data | .ps, application/postscript | PostScript data. | | com.adobe.encapsulated-​postscript | com.adobe.postscript | .eps, NeXT Encapsulated PostScript v1.2 pasteboard type | Encapsulated PostScript. | | com.adobe.photoshop-​image | public.image | .psd, '8BPS, ' image/x-photoshop, image/photoshop, image/psd, application/photoshop | Adobe Photoshop document. | | com.adobe.illustrator.ai-​image | public.image | .ai | Adobe Illustrator document. | | com.compuserve.gif (kUTTypeGIF) | public.image | 'GIFf', .gif, image/gif | GIF image. | | com.microsoft.bmp (kUTTypeBMP) | public.image | 'BMP ', 'BMPf', .bmp | Windows bitmap image. | | com.microsoft.ico (kUTTypeICO) | public.image | .ico | Windows icon image. | | com.microsoft.word.doc | public.data | 'W8BN', .doc, application/msword | Microsoft Word data. | | com.microsoft.excel.xls | public.data | 'XLS8', .xls, application/vnd.ms-excel | Microsoft Excel data. | | com.microsoft.powerpoint.​ppt | public.data, public.presentation | .ppt, 'SLD8', application/mspowerpoint | Microsoft PowerPoint presentation. | | com.microsoft.waveform-​audio | public.audio | .wav, .wave, '.WAV', 'WAVE', audio/wav, audio/wave | Waveform audio. | | com.microsoft.advanced-​systems-format | public.audiovisual-​content | .asf , 'ASF_', video/x-ms-asf | Microsoft Advanced Systems format. | | com.microsoft.windows-​media-wm | public.movie, com.microsoft.advanced-​systems-format | .wm, video/x-ms-wm | Windows media. | | com.microsoft.windows-​media-wmv | public.movie, com.microsoft.advanced-​systems-format | .wmv, video/x-ms-wmv | Windows media. | | com.microsoft.windows-​media-wmp | public.movie, com.microsoft.advanced-​systems-format | .wmp, video/x-ms-wmp | Windows media. | | com.microsoft.windows-​media-wma | public.audio, com.microsoft.advanced-​systems-format | .wma, video/x-ms-wma | Windows media audio. | | com.microsoft.advanced-​stream-redirector | public.xml, public.audiovisual-​content | .asx, 'ASX_', video/x-ms-asx | Advanced Stream Redirector. | | com.microsoft.windows-​media-wmx | public.audio, com.microsoft.advanced-​stream-redirector | .wmx , video-x-ms-wmx | Windows media. | | com.microsoft.windows-​media-wvx | public.audio, com.microsoft.advanced-​stream-redirector | .wvx, video-x-ms-wvx | Windows media. | | com.microsoft.windows-​media-wax | public.audio, com.microsoft.advanced-​stream-redirector | .wax, video-x-ms-wax | Windows media audio. | | com.apple.keynote.key | com.apple.package, public.presentation | .key | Apple Keynote document. | | com.apple.keynote.kth | com.apple.package, public.composite-​content | .kth | Apple Keynote theme. | | com.truevision.tga-image | public.image | .tga, 'TPIC', image/targa, image/tga, application/tga | TGA image. | | com.sgi.sgi-image | public.image | .sgi, '.SGI', image/sgi | Silicon Graphics image. | | com.ilm.openexr-image | public.image | .exr | OpenEXR image. | | com.kodak.flashpix.image | public.image | .fpx, image/fpx, application/vnd.fpx | FlashPix image. | | com.j2.jfx-fax | public.fax | .jfx | J2 fax. | | com.js.efx-fax | public.fax | .efx, image/efax | eFax fax. | | com.digidesign.sd2-audio | public.audio | .sd2, 'Sd2f' | Digidesign Sound Designer II audio. | | com.real.realmedia | public.movie | .rm, 'PNRM', application/vnd.rn-realmedia | RealMedia. | | com.real.realaudio | public.audio | .ram, .ra , 'PNRA', audio/vnd.rn-realaudio, audio/x-pn-realaudio | RealMedia audio. | | com.real.smil | public.xml | .smil, application/smil | Real synchronized multimedia integration language. | | com.allume.stuffit-archive | public.data, public.archive | .sit, .sitx, application/x-stuffit, application/x-sit , application/stuffit | Stuffit archive. | # 总结 希望这篇文章帮助哪些需要区分类型的兄弟们. [官方文档Uniform Type Identifiers Reference](file:///Users/sunyazhou/Downloads/System-Declared%20Uniform%20Type%20Identifiers.htm). [如何在macOS上设置打开文件类型 ](https://www.file-extensions.org/article/set-default-app-for-opening-files-with-no-extension-on-mac) [软件地址]() URL: https://sunyazhou.com/2021/01/TextGradient/index.html.md Published At: 2021-01-21 20:00:29 +0000 # 滚动文本设置渐变颜色 ![](/assets/images/20210121TextGradient/gradientcover.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! 最近看到搜狐发表了一篇文章,其中有一段是文本如何添加渐变颜色,使用`非mask`的方式.因为mask的方式非常耗费性能,因为mask会触发离屏渲染.今天的demo中我使用了原来的demo做为例子 ## 实现滚动字幕中增加 渐变颜色 核心实现很简单,[带你实现完整的 iOS 视频弹幕系统](https://mp.weixin.qq.com/s/4pWrwmZBEbrca2uxIt3o6w)中并没有给出相关demo.只是把相关弹幕的实现思路大概说说.所以做为一个iOS开发要善于动手写代码实现和验证它的思路. 其实我内心是很讨厌 搜狐的这篇有头没尾的技术文章,无非就是写写思路,代码实现的demo一个也不放出来显然不厚道. 这里面我们实现的比较简单.就是算出文本的size然后用 CoreGraphicContext画出一张图片. 核心代码如下 ``` objc + (UIImage *)gradientFromColor:(UIColor *)fromeColor toColor:(UIColor *)toColor andSize:(CGSize)imageSize { if (fromeColor == nil) { fromeColor = [UIColor clearColor]; } if (toColor == nil) { toColor = [UIColor clearColor]; } NSArray* gradientColors = [NSArray arrayWithObjects: (id)fromeColor.CGColor, (id)toColor.CGColor, nil]; CGFloat scale = [UIScreen mainScreen].scale; UIGraphicsBeginImageContextWithOptions(imageSize, NO, scale); CGContextRef context = UIGraphicsGetCurrentContext(); CGContextSaveGState(context); CGColorSpaceRef colorSpace = CGColorGetColorSpace([fromeColor CGColor]); CGGradientRef gradient = CGGradientCreateWithColors(colorSpace, (CFArrayRef)gradientColors, NULL); CGPoint start = CGPointMake(0.0, 0.0); CGPoint end = CGPointMake(imageSize.width, 0.0); CGContextDrawLinearGradient(context, gradient, start, end, kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation); UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); CGGradientRelease(gradient); CGContextRestoreGState(context); UIGraphicsEndImageContext(); return image; } ``` 这里面需要着重强调就是 两行代码 ``` objc //获取渐变颜色的色彩空间 CGColorSpaceRef colorSpace = CGColorGetColorSpace([fromeColor CGColor]); //然后根据渐变数组调用 线性渐变,至于放心可以在代码中找到 start 和end的CGPoint处设置 NSArray* gradientColors = [NSArray arrayWithObjects: (id)fromeColor.CGColor, (id)toColor.CGColor, nil]; CGGradientRef gradient = CGGradientCreateWithColors(colorSpace, (CFArrayRef)gradientColors, NULL); ``` 下面是我这边实现的逻辑展示 demo我会放到下面大家自行下载. ![](/assets/images/20210121TextGradient/gradienttextscroll.gif) # 总结 2021年第二篇.时间紧凑,有时间仔细研究一下 搜狐的弹幕系统 写个demo. [本文demo下载](https://github.com/sunyazhou13/UIScrollTextNewDemo) [参考 带你实现完整的 iOS 视频弹幕系统](https://mp.weixin.qq.com/s/4pWrwmZBEbrca2uxIt3o6w) URL: https://sunyazhou.com/2021/01/SafeAreaEdges/index.html.md Published At: 2021-01-18 13:43:41 +0000 # 获取UIWindow的边界距离 # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! 2021第一篇发布记录开发中遇到的费时间问题 ### 新工程经常遇到解决刘海屏的距离问题 写了一个工具类用于记录边界 ``` objc #import #define YZAreaInsets [YZUtilTool yz_safeAreaInsets] @interface YZUtilTool : NSObject + (UIEdgeInsets)yz_safeAreaInsets; @end @implementation YZUtilTool + (UIEdgeInsets)yz_safeAreaInsets { UIWindow *window = [UIApplication sharedApplication].windows.firstObject; if (![window isKeyWindow]) { UIWindow *keyWindow = [UIApplication sharedApplication].keyWindow; if (CGRectEqualToRect(keyWindow.bounds, [UIScreen mainScreen].bounds)) { window = keyWindow; } } if (@available(iOS 11.0, *)) { UIEdgeInsets insets = [window safeAreaInsets]; return insets; } return UIEdgeInsetsZero; } @end ``` 记录过程代码. URL: https://sunyazhou.com/2020/12/FinalSummary/index.html.md Published At: 2020-12-30 23:30:00 +0000 # 2020年终总结 ![](/assets/images/20201231FinalSummary/ShulanServiceArea.webp) 2020年8月 舒兰服务区.结束北漂回家的路上. # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! > 七年辛苦北漂之身,一日还乡重归故人. > 倘若有天若有人问, 征尘仆仆烟尘滚滚. > 回首成败暂且不论,我给自己打十几分. > 言不求实语不求真, 纵为利刃依默无闻. 2020年了 这是搬砖的第7个年头,记录生活交出年终总结. 每年的这个时候我会沉下心来想想这一年又经历了什么.写年终总结是每一个技术工程师对IT技术求真务实的执着和热衷学习的态度,它考验着我们建造技术博客的初心,磨砺我们的灵魂纵情向前. 打工人要学会自娱自乐,写总结这个东西,我是学习着iOS技术大佬 [唐巧](https://blog.devtang.com/)和 iOS领域顶级开发者[喵神](https://onevcat.com/), 看着两位出色的iOS 开发者 每年都会在自己的博客上发布年总结,作为一个后来者我应该向他们学习记录个人的成长. ## 2020回顾 今年事件如下: * 疫情 * 政治 * 复工 * 快手极速版 * 卖摩托车 * 离职结束北漂 * 回家 * 盛歌 * 我认为的伟大 * 孙亚洲第二理论 * 好物 * 总结 ### 疫情 ![](/assets/images/20201231FinalSummary/vaccine.webp) 从2019年我写完年终总结不久,一种疫情疾病开始成扩散的形式在中华大地上快速传播,最后才知道是新冠肺炎病毒,2020年初愈加严重,记得当时我们X6团队去[唇辣号重庆老火锅(上地七街店)]()聚餐吃火锅,大家讨论的话题是如何买口罩.过年回家的时候开车回家路过京哈高速沿线,各个服务区人心慌慌. 直到正月初四 城市小区以及乡镇村屯开始封路,口罩,消毒液,一次性手套,护目镜等医疗防疫用品紧俏脱销. ![](/assets/images/20201231FinalSummary/epidemicpreventionmaterials.webp) 事态发展较为严重,初八开始小区全面封锁,就这样这一年开始了漫长防疫战争拉开了序幕,直至本文书写之时,全球除中国外,还有很多国家依然陷入疫情防疫不利甚至失控的局面,美国较为严重、英国新冠病毒变异传播、冷冻食品频频检车出病毒附着、环境传染成为可能.面对着种种不利的消息使疫情严重拖累导致工业生产迟滞,影响我们生活中的方方面面,对我们直接的影响是一些依赖进口物资短缺,生活成本上升,就业压力增加,经济发展下行,人均收入减少明显. 这一年中 2~3月份新冠肺炎顶峰、4~5月份二线城市再度爆发、7~8月份 相对平静常控,年末育苗进入第三阶段临床试验. 也许这次疫情 让我们科技行业终于打破常规,使居家办公成为一种可能.从2月份爆发开始,我就逐步在家里打开电脑coding.但是事实证明,还是集中办公效率高,分散式办公相对低效. 这一年我国的防疫运动着实让国人肯定和赞许,世界上除了中国以外其它国家几乎防疫疫情屡屡爆出不利的消息.美利坚的金毛特朗普总统 差点把世界搞成一锅粥. 最可笑的是美国这个资本主义国家每天新冠肺炎检测确诊人数取决于每天生产+进口试剂盒的生产数量.因为没有那么多试剂盒可用所以能确诊的人数是固定的. 希望疫情尽快过去.让我们的生活能一如既往.毕竟我们还是小老百姓指着身上这点本事吃饭, 但我坚定不移的相信科学和习大大的领导下这个国家将成为历史不朽的丰碑. 那句"还要为人民的对美好的生活的向往,继续奋斗"依然回荡在我脑海里.这句话来自--《习近平七年的知青岁月》. ### 政治 ![](/assets/images/20201231FinalSummary/cutetrump.webp) 曾几何时一个纯粹的IT人开始关注国家政治,其实本质上不是关心国家政治,而是国际政治变动影响着我们的钱袋子的收入.我也从不关心美国到底是谁当总统、但是没想到无论是谁当总统对中国都是一种不好的局面、封锁华为科技企业,卡脖子技术导致国内企业倒闭破产,中兴不就是这么一个被卡脖子打倒的企业吗?那些在中兴工作的人一定多多少少是你的同学或者前同事亦或是你曾认识的某个人. 我能认知的世界在变大,但我实际生活的圈子却在变小。世界的变动非常剧烈,在中美争霸背景下,被时代洪流的裹挟向前的我们,其实很难对抗宏观层面的规律。而对这个世界的认知范围,往往决定了我们在这一变局下会去往何方。而与此同时我所从事的iOS开发领域也差点被特朗普搞得丢了饭碗,华为是一家android手机厂商之一, 而与之对应移动阵营iOS 苹果手机厂商仅此一家. 如果特朗普拿中国华为开涮,我国也拿苹果对怼,那将是每一个iOS开发者**黑暗前的黎明**.两个大国非要像小孩吵架一样,你拿我一块橡皮我就拿你一只铅笔,最后伤害的是我们这些劳苦大众,不过好在这种危机局面并没有上演但不保证后续不会出现.在这种矛盾的局面中我只能随遇而安相机而动. 不知拜登上台会不会改善国际政治的局面.我只想好好的coding,求拜登总统给个饭碗. 经常打开快手会出现一些生活在美洲的华人,比较有意思的是他们对美国有如下词语的形容: * 特朗普不叫特朗普叫金毛 * 新冠病毒不叫新冠肺炎 叫小冠 * 感染新冠肺炎死亡的人 叫 嘎嘣装袋 虽然这些词汇都是中文魅力中的黑色幽默,也客观的反映了在美华人客观的生活状态.在美国的同胞其实深受着黑裔以及印度裔或者美洲裔的歧视以及区别对待.我想说的是这种歧视并不限于北美,在首都北京也一样区分北京土著和外埠人口,这种歧视也许从未消除还与日俱增,我所在的三线城市还区分本地和外县来划请界限. 我一直都怀疑**人生而平等** 和 **人分三六九等**是不是悖论. 虽然找不到合适的答案 ,但是希望未来的自己保持一个关心国家前途命运发展的积极心态.就像喵神说的**被时代洪流裹挟向前的我们**别无先择,学会直面惨淡的人生吧! ### 复工 由于疫情的发展被有效控制,根据[孙亚洲理论](https://www.sunyazhou.com/2020/02/SunyazhouTheory/)这个国家发展需要生产力原理,我们投身于国家科技领域建设的科技企业开始响应国家的号召回京复工.在家办公了1个月,我谈谈我的感受.我很机智的有2件事,第一件事是19年买了一个小破车过年回家的时候驱车1500公里从北京开回老家完美的闪避开了人群聚集的疫情区域,第二件事情是我带着工作用的笔记本电脑测试机等全套设备得以在家办公没有麻烦同事帮忙邮寄设备. 回到北京的时候已经是三月中旬了,回京的路上我很疲惫 开车开了18个小时,中途还不能去亲戚家休息,怕麻烦亲戚朋友被隔离.然后就是到租住的地方隔离14天.14天后开始分批次上班. 在家办公期间又经历了团队调整.快手联合创始人**一笑**开视频会议跟我们全体极速版同事说明了未来公司如何发展,极速版如何归属,人员如何如何... 老实说我是把工作的笔记本电脑放在窗台上看着一位新高层leader的到来介绍一下自己然后各位默不作声,我只是悄悄的问了一个很low的问题 **Thomas**中文名叫啥. 似乎我都习惯了这种互联网公司团队浪潮般的更迭. 不断更换上层leader意味着要重新和上司建立信任关系,往往这个过程比较漫长. 我在快手工作2年半换了3个老大.我在快手工作时间最长的工位是D601会议室中的一个偏僻的角落.我在快手做过从**0.5到1**的app是快手极速版.我在快手。。。额 。过的还算快乐. 下面这张图片是我度过漫长的隔离期复工的第一天拍摄的图片 ![](/assets/images/20201231FinalSummary/returntowork1.webp) 公司上班的人很少 ![](/assets/images/20201231FinalSummary/returntowork2.webp) 以往我都是去公司吃早餐.疫情 的时候公司给每位同学配发了一个隔离罩,要把工作餐拿到工位吃. 剩下的就是公司每个人 每周都能领到一周的口罩,直到我离职的时候为止依然没有停止发放.这确实是一种科技公司的人文精神. ### 快手极速版 这是我见证过的DAU日活从0~5000w 的app没有之一.从19年开始做极速版 到后来团队被归属给X6.再到后来拆分成单列上下滑团队.我亲身经历的它的成长.2019年末快手独家冠名了春晚,我们被告知要签署一份秘密协议,协议的内容大概是说春晚的项目不能泄露.是公司机密,签署协议的人员必须对家人甚至爱人都要保密.违反协议将被公司罚款300w元. 当时负责的直播业务可能比较重要.元旦的时候第一次预演习承接元旦直播效果不错. 开始承接春晚项目前夕的动员大会上我是这样的. ![](/assets/images/20201231FinalSummary/meeting.webp) 干活的时候是这样的 ![](/assets/images/20201231FinalSummary/NewYearsDay.gif) 最后元旦还算比较成功 ![](/assets/images/20201231FinalSummary/meetingresult.webp) 我希望这些照片能经得起时光的沉淀.虽然啥用没有,全当博客里不可或缺的材料吧. ### 卖摩托车 在北京工作这么多年我是一路换了N辆电动车,要么是电动车总坏,要么是充电比较费事.导致我最后下决心买一辆好的摩托车代步来缓解北漂生活的压抑感. ![](/assets/images/20201231FinalSummary/motor1.webp) ![](/assets/images/20201231FinalSummary/motor2.webp) 这辆摩托车是我买过最好的一辆没有之一,无奈只骑了3000多公里后卖了.因为老家不允许摩托车上路.没办法把它带回去.不过还好最后成交价没赔多少.自己也算对它有个交代. ### 离职结束北漂 从参加工作以来我一直都有一个愿望,什么时候能不用这么漂泊,回到自己的家乡,找一份工作安静的生活.至少不用像北漂一样如此煎熬.可是对于一个毕业生来说首要的任务是增长知识和技能和生活的自给自足,渐渐的这个愿望变成了梦寐以求的梦想伴随每一个北漂人如影随形. 2018年我入职了快手.负责直播iOS开发.在就职期间我遇到了很多不一样的同事,每个人都技术出色.参加快手十三期入职培训(快手中学),CEO宿华 给我们讲述创业的艰辛,以及背后不出名却又默默支持快手的投资人.我也许清楚的记得华哥说:"快手要做一家有温度的公司,要让每个人像阳光一样得到普惠."华哥的朴实感有一种人格魅力,我希望能传承一下华哥的这种朴实无华,即便他依然是中国2018年胡润富豪榜排行84位的富翁,我也会有时候在电梯里看到这位CEO,即便他如何有钱却依然开着一辆别克GL6老实商务车办公,有时候下班也经常能在后厂村路上看到他骑车摩拜单车回家.这也导致我经常穿着华瑞棉林甸鞋的主要原因.不是因为低调的学习这位CEO,而是因为我穷. 现在浮躁的我只有一个![](/assets/images/20201231FinalSummary/getrich.webp)想法 印象中最深刻的是server端的leader 李伟博大佬给大家分享 一个视频从用户手机到观看者能看到的视频内容中间都经历了哪些处理过程. 因为这是每一个快手员工应该了解的过程.从视频生产到后端处理,再到消费视频.让我们每个人都储备好未来在快手工作的基础知识.我受益匪浅,我进入百度的工作3年的时间里除了志波大概给新人讲解一下百度网盘的app端设计外,没有人愿意讲述一下一个关键字搜索到自然语言分析再到后端搜索引擎处理再到返回给用户能看到的信息内容是怎么样的处理过程.从这一点上看百度还是需要向后来者学习借鉴一下经验. 由于工作团队经常变动,自己总被安排(技术不出众),想想自己的7年北漂生涯,晚上经常睡不着觉.由于加班没有充足的时间睡觉,头发开始逐渐变白.加之北京政府一直对外埠人来京就业持高压打击态度,人权不平等、路权不平等、但是纳税很平等、反正就是各种好事外埠人员几乎都没有北京人同等权利,区别对待.如此种种,房价持续高位不下,户口问题一直很难落实,在北京除了枯燥996上班以外任何自驾旅游的权利对我们都是奢侈的.毕竟没有**对价**我承认,可是也不能让我们连个车都没有就有点过分了,上班我们租房、放假都没有心情消费、北漂被当成**人肉干电池**对待.我最担心的就是每年租房换地儿,连个车都没有着实不方便.这种被压迫的生活状态持续了多年.终于今年我无法承受北京不允许外地车进入,每年只允许办理12次进京证.这让我备受煎熬,我依然清楚的记得下班后打羽毛球打到很晚打不到车回家的场面. 渐渐地各种事情会引发我对**在北京工作的思考**,思考自己到底想得到什么.不就是为了得到自己想要的生活吗?目前这种形势持续下去,钱可能很多但是也是会被国家的高房价一举收割回到解放前的状态.虽然北京生活的大家都这样,但这不是我想要的生活.权衡了很久加上工作上的变动让我失望,于是提了离职申请开始休假回老家. ![](/assets/images/20201231FinalSummary/JinghaExpressway.webp) 我原来从未像离开北京这样的开心,那种喜悦前所未有. > 回首北漂的每一步,我都走的好孤独. ---- 沿着京哈高速, 开着我的战车,一路向祖国的东北方向. ![](/assets/images/20201231FinalSummary/route1.webp) 回家行程前段目的地是辽宁省沈阳市铁西区北大营附近的同学家,经过这一年的疫情防控,全国的小区控制比较严格,严禁外地人员流动,我出发前计划问好了小区可以进出,才动身出发. 辽宁省真的是风光无限.路过 绥中、兴城、塔山等服务区的时候 我记录下了这一路的美好记忆,最后在盘锦服务区休息时拍下了下面这张照片. 路过塔山服务区,那座双塔造型的建筑横跨京哈高速也许是这一路唯一一座地标吧! ![](/assets/images/20201231FinalSummary/panjinservicearea.webp) 回家行程后段路程需要从吉林方向绕行,走吉黑高速,舒兰、五常 然后回冰城哈尔滨. 中途路过 开源、铁岭服务区很好、服务区建设的很现代化,如果大家有自驾经过的话一定要到这两个服务区看一看. ![](/assets/images/20201231FinalSummary/route2.webp) 这中途会路过美丽的长白山脚下**雾凇岛**,风景很好.以后有机会了一定自驾去仔细看看. ![](/assets/images/20201231FinalSummary/provincialtollstations.webp) 最后途径新发省界收费站进入哈尔滨市,这一路走了2天.行程大概1500公里. ### 回家 ![秋天的哈尔滨清晨](/assets/images/20201231FinalSummary/home1.webp) 回家后的第一件事是整理一下家里物品.房子入户很久了没人打理.下一步的计划是复习基础知识准备面试找工作.在离职后的一段时间微信上经常收到前同事和同行的微信,大家问我同一个问题,为啥放弃北京这么好的工作和待遇回老家?我其实想在这里正面回复一下,每个人都要经历或多或少的换工作经历,IT这一行从来都是一个高风险、高收益、高失业率的行业,**我牺牲了高薪和好的待遇换来了自己想要的生活**,所以付出这些都是值得的.即便回家后的公司都很low,待遇很差,甚至曾经在北京不需要考虑的问题回家后都是很难的事情,但是三线城市就这样,纵然下这个决定不是很明智,但和在北京比起来好多了,再也不租房子了,幸福感指数高了,下午5点多可以下班了,挣的钱甚至不如北京的1/3、1/4,我有更多的时间在自己的爱好上了.当然也失去了和很多优秀的杰出的同事一起工作的机会, 每个人都有选择自己生活的方式,也许这就是我愚蠢的选择方式吧! 希望自己把业余时间放在学习和爱好上,保持持续学习的热情和不断更新博客写出更多优秀的文章. 只有接受三线城市的各种不好才能等来它带给我们的好吧! 我认为这是自己对生活的态度和面对那些困难的挑战,这种挑战就是面对不利的工作环境、不优厚的待遇、不多的薪资收入、不理想的职业发展... 看到这的小伙伴甚至会嘲笑我的这种毫无进取的生活态度.是的我更愿意做一个直面惨淡人生的人.参加工作的这么多年自己也从一个无知小白成为无知大白.自己的学习历程和工作经历让我成长为一个愿意接受挑战的人. 在北京挣快钱的日子到头了,回到二三线城市要适应挣慢钱的节奏才能走的更远.在没有找到更有发展前景的职业之前这就是我目前的认知和想法.总之自己不是那种碌碌无为还认为自己平凡可贵的人.我会感到没有成就而羞耻. 在快手工作的时候打羽毛球认识了一位球友同事程波波,这位来自江西景德镇毕业于清华大学的老哥让我看到了不寻常的一面.每次看他的朋友圈我都会评论一句风景真好,因为他总利用周末的时间去旅行,让我最深刻的是他的回复,他回复说:"**打工人要学会自娱自乐**". **打工人也可以有点乐趣**.打工人,这句话鼓励了我的灵魂. 也许我的离开流漏出了北漂人心里面最深刻的痛,在这里我想勉励一下北漂的各位,我们所从事的行业是一个伟大的行业,只有优秀的人、杰出的人、肯努力的人才能够出头.让我们互相勉励让计算机科学与技术领域变得更好. ### 盛歌 年少不听李宗盛,听懂已是不惑年. 他的歌声经得起时光的沉淀,唱出了我心目中的痛. ![](/assets/images/20201231FinalSummary/JonathanLee.webp) 其实有些歌的歌词写得太真实又有诗意,让我这个伪歌迷听的深入人心. * 寂寞的恋人啊 * 你们 * 伤心地铁 * 我是真的爱你 * 问 * 远行 * 不必在乎我是谁 李宗盛的声音低沉有力,干脆利索.离开北京最后的一个月几乎每天上下班路上被他的音乐鼓励着灵魂. ### 我认为的伟大 曾几何时"求真务实,实事求是 "成了自己的人生信条.可是自己从未做过任何伟大有成就的事情.睡不着觉的时候经常想当自己真的有一天暴富了该干啥.是肆意的纵情挥霍财富还是选择踏实的继续工作,最后发现还是踏实的继续工作,否则生活将失去了意义.如果真的走狗屎运暴富了成了百万富翁也要去帮助一下身边那些亲戚朋友或兄弟姐妹.因为似乎某种意义的上暴富或者有钱意味着自己要有能力驾驭那些财富,否则也会因为驾驭不了这么多财富而膨胀过度的变成一无所有.如果是我,我当然可以选择买几套房作为投资,但是身边的兄弟姐妹连房子这些基本的生活设施都没有,作为有钱的你袖手旁观我认为这是一种失败的人生活法.这种纵然有钱却对社会没有贡献的富有过于狭隘.李嘉诚就是其中之一.这种认知的伟大**仁者见仁,智者见智**. ##### 为什么要在这里谈论伟大? 因为发生在身边这样真实的故事太多被刺激了神经,很多人确实驾驭不了那些别人给予的财富成为悲剧.这是促使我时刻提醒自己.**人生最大的悲剧是自己的实力还配不上自己的梦想,还要去攀比**.希望活出真实的自己. ### 《孙亚洲第二理论》 在我大学毕业时我给自己立下了三个目标: * 1.**不啃老** * 2.**先能自给自足解决温饱** * 3.**如果前两个实现了,再去研究实现发家致富** 经过7年多的工作实践我在这里正式宣布这三条目标将作为衡量一个优秀大学毕业生脱贫致富的标准规范.我把这套方法论称为**《孙亚洲第二理论》**,**《The 2nd Theory Of SunYazhou》**. > 听完我瞎扯的理论,你学废了吗? ### 好物 ##### 书法桌 疫情期间买了一套北美黑桃木的[书法桌](https://detail.tmall.com/item.htm?id=592669199819&spm=a1z09.2.0.0.248d2e8dpNMOm5&_u=v2qkte5648b&sku_properties=10187648:21959)用于学习.我比较喜欢那把太师椅.坐起来并不舒服是为了提醒自己生活总是这样让我们不舒服. ![](/assets/images/20201231FinalSummary/tablesandchairs.webp) 实木的家具会有点稍贵,太师椅+书法桌 大概花费 ¥5000+ RMB. ##### 大众灯光升级 由于去年买了一辆破车,开高速的时候夜晚灯光很差劲,差点压倒轮胎,比较危险,所以建议开大众车的小伙伴可以看看升级一下灯光,这种升级是完全符合验车法规的放心.[大众氙气灯光升级](https://detail.tmall.com/item.htm?id=562699335539&spm=a1z09.2.0.0.248d2e8dCkJmgu&_u=v2qkte5851d&sku_properties=3986737:6070426;122216751:20213),我升级了进口海拉5透镜,这玩意非常贵.还有欧司朗顶级D1s 6000K色温的氙气灯泡,以及海拉4安定器. ![](/assets/images/20201231FinalSummary/autobulb.webp) ![](/assets/images/20201231FinalSummary/magotan1990.webp) 升级灯光绝对是出于不得已为之的操作,这辆战车虽然老旧一些,也陪我两次往返老家和北京.在长达6000+公里零事故的安全驾驶区间确实没有把我扔到路上我对此感到很欣慰.今年五一换了4只马牌轮胎,和一个mib 682E的车机,希望它未来照顾好这辆战车的主人. #### 扫地机器人 解放双手扫地神器,我比较辣不愿意打扫屋子,作为热爱3C的 程序员来说,一个电动的扫地机器人居家必不可少.方便打扫房间. ![](/assets/images/20201231FinalSummary/floormoppingrobot.webp) ### 总结 今年的年终总结并没有像去年一样记录流水账一样,也没有提及生活和学习相关,主要是想换换想法思考一下这文章的故事情节.更没有提及自己的第二个五年计划进行的情况,我想第二个五年计划我们看结果. 2020我最大的收获是结束了北漂选择了自己想要的生活,2021要努力迈进,幸福是奋斗出来的,争取早日实现第二个五年计划. URL: https://sunyazhou.com/2020/12/iOSsynchronousTimeWithServer/index.html.md Published At: 2020-12-05 21:12:31 +0000 # 解决iOS系统时间被修改的问题 ![](/assets/images/20201206iOSsynchronousTimeWithServer/iOSsynchronousTimeWithServerCover.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! 本文将用到的科普知识如下: * GMT:(Greenwich Mean Time)格林尼治标准时间。这是以英国格林尼治天文台观测结果得出的时间,这是英国格林尼治当地时间,这个地方的当地时间过去被当成世界标准的时间。 * UT:(Universal Time)世界时。根据[原子钟](https://baike.baidu.com/item/%E5%8E%9F%E5%AD%90%E9%92%9F/765460)计算出来的时间 * UTC:(Coordinated Universal Time)太阳所处的位置变化跟地球的自转相关,过去人们认为地球自转的速率是恒定的,但在1960年这一认知被推翻了,人们发现地球自转的速率正变得越来越慢,而时间前进的速率还是恒定的,所以UTC不再被认为可以用来精准的描述时间了。我们需要继续寻找一个匀速前进的值。抬头看天是我们从宏观方向去寻找答案,科技的发展让我们在微观方面取得了更深的认识,于是有聪明人根据微观粒子原子的物理属性,建立了原子钟,以这种原子钟来衡量时间的变化,原子钟50亿年才会误差1秒,这种精读已经远胜于GMT了。这个原子钟所反映的时间,也就是我们现在所使用的UTC(Coordinated Universal Time )标准时间。 上述摘自: [iOS关于时间的处理](https://mp.weixin.qq.com/s/cSZUNMuqk6DL3-nctyxzcw?) ## 场景描述 最近开发过程中QA同学提了一个bug, 当手机日期时间修改后 发现页面时间显示异常, 这种问题非常经典, 也就是**iOS关于时间的处理**. ## 我们对时间的认识 **时间是线性的**,即任意一个时刻,这个地球上只有一个绝对时间值存在,只不过因为时区或者文化的差异,处于同一时空的我们对同一时间的表述或者理解不同。比如,北京的20:00和东京的21:00其实是同一个绝对的时间值。 > 可以理解为 以一个标准点作为标准点.通过时区微调 来实现全球各个国家的日期显示. ## iOS几种获取时间的方式 #### 1.NSDate 代码实现 ``` objc - (void)timeIntervalSinceReferenceDate { NSDate *date = [NSDate date]; NSLog(@"date = %lf", date.timeIntervalSinceReferenceDate); } ``` `NSDate`对象封装单个时间点,与任何特定的日历系统或时区无关.日期对象是不可变的,表示相对于绝对参考日期(`2001年1月1日00:00:00 UTC`)的不变时间间隔,它是以UTC为标准的。 NSDate输出结果: ``` sh 2020-12-06 12:28:55.795929+0800 ZGTimeDemo[12177:134289] date = 628921735.795845 ``` 下面计算一下:628921735.795845/365/86400 = 19.942977,今年是2020年,距离2001年正好是19年. 如果我们直接打印NSDate ``` objc NSDate *date = [NSDate date]; NSLog(@"%@",date); ``` 则会输出 ``` sh 2020-12-06 06:51:04 +0000 ``` 可见NSDate输出的是绝对的UTC时间,而北京时间的时区为UTC+8,上面的输出+8个小时,刚好就是我当前的时间了。所以正常`UTC + 时区`才是真正的时间日期. 至于时区加减请参考下图. ![](/assets/images/20201206iOSsynchronousTimeWithServer/iOSsynchronousTimeWithServerZone.webp) **注意: NSDate是受手机系统时间控制的,当你修改了手机上的时间显示,NSDate获取当前时间的输出也会随之改变。在我们做App的时候,明白这一点,就知道NSDate并不可靠,因为用户可能会修改它的值**. #### 2.函数CFAbsoluteTimeGetCurrent() > 官方文档: 绝对时间是相对于绝对参考日期(格林尼治标准时间2001年1月1日00时00分)以秒计算的。正值表示引用日期之后的日期,负值表示引用日期之前的日期。例如,绝对时间-32940326相当于1999年12月16日17:54:34。重复调用这个函数不能保证单调递增的结果。系统时间可能由于与外部时间引用同步或由于显式的用户更改时钟而减少。 `CFAbsoluteTimeGetCurrent()`的概念和NSDate非常相似,只不过参考点是:以GMT为标准的,2001年一月一日00:00:00这一刻的时间绝对值。 **注意:CFAbsoluteTimeGetCurrent()也会跟着当前设备的系统时间一起变化,也可能会被用户修改.** #### 3.`gettimeofday()` ``` objc int gettimeofday(struct timeval * __restrict, void * __restrict); ``` 这个函数获取的是UNIX time. ``` objc struct timeval now; struct timezone tz; gettimeofday(&now, &tz); NSLog(@"gettimeofday: %ld", now.tv_sec); ``` ``` sh gettimeofday: 1607238723 ``` ##### UNIX time又是什么呢? Unix time是以UTC 1970年1月1号 00:00:00为基准时间,当前时间距离基准点偏移的秒数。上述API返回的值是1607238723,表示当前时间距离UTC 1970年1月1号 00:00:00一共过了1607238723秒。 `Unix time`也是平时我们使用较多的一个时间标准,在Mac的终端可以通过以下命令转换成可阅读的时间: ``` sh date -r 1607238723 ``` 输出 ``` sh 2020年12月 6日 星期日 15时12分03秒 CST ``` **注意:`gettimeofday()`,`NSDate`,`CFAbsoluteTimeGetCurrent`这三个都是受当前设备的系统时间影响.只不过是参考的时间基准点不一样而已。我们和服务器通讯的时候一般使用NIX time.** #### 5.`mach_absolute_time()` 在我们的iPhone上刚好有一个这样的值存在,它就是CPU的时钟周期数(ticks),这个`tick`的数值可以用来描述时间,而`mach_absolute_time()`返回的就是CPU已经运行的`tick`的数量。将这个`tick`数经过一定的转换就可以变成秒数,或者纳秒数.这样就和时间直接关联了.不过这个`tick`数,在每次手机重启之后,会重新开始计数,而且iPhone锁屏进入休眠之后`tick`也会暂停计数. **注意: `mach_absolute_time()`不会受系统时间影响,只受设备重启和休眠行为影响** #### 6.`CACurrentMediaTime()` `CACurrentMediaTime()`就是将上面`mach_absolute_time()`的CPU`tick`数转化成秒数的结果。以下代码: ``` objc double mediaTime = CACurrentMediaTime(); NSLog(@"CACurrentMediaTime: %f", mediaTime); ``` ``` sh 2020-12-06 15:34:59.808799+0800 ZGTimeDemo[19731:281283] CACurrentMediaTime: 17789.582767 ``` 返回的就是开机后设备一共运行了(设备休眠不统计在内)多少秒. 这个API等同于下面代码: ``` objc NSTimeInterval systemUptime = [[NSProcessInfo processInfo] systemUptime]; ``` **注意:`CACurrentMediaTime()`也不会受系统时间影响,只受设备重启和休眠行为影响.** #### 7.sysctl() iOS系统还记录了上次设备重启的时间。可以通过如下API调用获取: ``` objc #include - (long)bootTime { #define MIB_SIZE 2 int mib[MIB_SIZE]; size_t size; struct timeval boottime; mib[0] = CTL_KERN; mib[1] = KERN_BOOTTIME; size = sizeof(boottime); if (sysctl(mib, MIB_SIZE, &boottime, &size, NULL, 0) != -1) { return boottime.tv_sec; } return 0; } ``` 返回的值是上次设备重启的Unix time。 **注意:这个API返回的值也会受系统时间影响,用户如果修改时间,值也会随着变化.** ## 客户端和服务器之间的时间同步 一般我们发起请求的时候都是在公参中带上本地时间,如果有一些比较敏感的接口会遇到用户更改系统时间的异常case导致异常.为了防止用户通过断网修改系统时间,来影响客户端的逻辑我们通常都这样做. * 获取服务器某一时刻`A`的时间; * 记录获取到时刻`A`时的本地时间`B`; * 需要用到时间时,获取当前本地时间`C`,当`C`-`B`作为时间间隔`D`,则`A` + `D` 则是当前服务器的时间. 这里要准确做到客户端时间和服务器时间一致,很关键的问题就是`B`和`C`不能受系统时间的影响,要解决这个问题,要依靠iOS的接口--**系统运行时间** 首先: 我们要依靠服务端给一个准确的时间戳.每次同步记录一个得到服务端时间戳B.我们就是要用运行的时间差来解决时间校时问题. 获取系统当前运行了多长时间方法: ``` objc //get system uptime since last boot - (NSTimeInterval)uptime { struct timeval boottime; int mib[2] = {CTL_KERN, KERN_BOOTTIME}; size_t size = sizeof(boottime); struct timeval now; struct timezone tz; gettimeofday(&now, &tz); double uptime = -1; if (sysctl(mib, 2, &boottime, &size, NULL, 0) != -1 && boottime.tv_sec != 0) { uptime = now.tv_sec - boottime.tv_sec; uptime += (double)(now.tv_usec - boottime.tv_usec) / 1000000.0; } return uptime; } ``` **注意:这个函数返回的是秒.和server返回的unix time可能要乘以1000**.(1s = 1000ms) `gettimeofday()`和`sysctl()`都会受系统时间影响,但他们二者做一个减法所得的值,就和系统时间无关了.这样就可以避免用户修改时间了。当然用户如果关机,过段时间再开机,会导致我们获取到的时间慢与服务器时间,真实场景中,慢于服务器时间往往影响较小,我们一般担心的是客户端时间快于服务器时间. 以下这段代码也可以做到不被修改`local_absolute_n_clock ()`返回秒 ``` c++ namespace { mach_timebase_info_data_t init_mach_timebase_info() { mach_timebase_info_data_t info; mach_timebase_info(&info); return info; } } int64_t CTimestamp::local_absolute_n_clock() { static mach_timebase_info_data_t sTimebaseInfo = init_mach_timebase_info(); int64_t t = mach_absolute_time(); return t * sTimebaseInfo.numer / sTimebaseInfo.denom; } CTimestamp::CTimestamp() { m_base_tm = time(0)*(1000*1000*1000); m_base_clock = local_absolute_n_clock(); } ``` # 总结 本篇问题的解决难点的关键在于如果获取本地的时间,我们这里取的是`系统运行时间进行的差值计算法`.我没有尝试过 休眠 退后台等逻辑消耗的时长.但是我认为如果要做好工具类,要尝试计算后台消耗的时间计时时长,可以也可以通过系统运行时间的差值运算得到准确的时间. 本篇重点: ABCD同步时间算法 主要依赖于服务端给的时间作为基准点. 另一个难点怎么获取系统运行时间做差值计算 来解决系统时间被用户修改后时间不准的问题. 硬核代码 并没有写成相关工具类,小伙伴们可以自行实现比较简单我就不写demo了.本篇文章也是我第一次切换到jekyll 第一次发布文章,如果本文对你有帮助可以收藏 参考: [iOS关于时间的处理](https://mp.weixin.qq.com/s/cSZUNMuqk6DL3-nctyxzcw?) [客户端和服务器的时间同步问题解决](https://www.jianshu.com/p/61e6385f8cf6) URL: https://sunyazhou.com/2020/10/WebviewSystemLanguage/index.html.md Published At: 2020-10-27 13:57:30 +0000 # 解决iOS调用系统相册不显示中文问题 # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! 最近开发遇到一个bug,在h5中点击选择iOS系统相册,显示的是英文的 ![](/assets/images/2020107WebviewSystemLanguage/WebviewSystemLanguage1.webp) ## 解决方式 在Xcode的plist中加入如下代码 ``` xml CFBundleAllowMixedLocalizations ``` 也可以 在 info.plist里面添加`Localized resources can be mixed` `YES`表示是否允许应用程序获取框架库内语言。 ![](/assets/images/2020107WebviewSystemLanguage/WebviewSystemLanguage2.webp) 然后运行效果: ![](/assets/images/2020107WebviewSystemLanguage/WebviewSystemLanguage3.webp) # 总结 感谢大家观看 到位挠挠了 URL: https://sunyazhou.com/2020/10/XcodeSourceEditorNotWork/index.html.md Published At: 2020-10-16 16:05:42 +0000 # 修复Xcode Source Editor在masOS的扩展中不显示 ![](/assets/images/20201016XcodeSourceEditorNotWork/XcodeSourceEditorCover.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! ## 问题描述 这几天要对代码进行对齐发现经常用的 XAlign插件不起作用了,一看设置中发现 扩展中没有了Xcode Source Editor ![](/assets/images/20201016XcodeSourceEditorNotWork/XcodeSourceEditor.webp) 通过网络上查询找到一篇靠谱的方式 特记录下来 终端输入如下 即可出来 ``` sh $ PATH=/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support:"$PATH" $ lsregister -f /Applications/Xcode.app ``` 引起的原因 当Xcode的多个副本在同一台机器上时,扩展可能会完全停止工作。在这种情况下,Apple Developer Relations建议重新注册你的Xcode主拷贝到Launch Services(最简单的方法是暂时将lsregister的位置先添加到PATH中): 参考 [https://nshipster.com/xcode-source-extensions/](https://nshipster.com/xcode-source-extensions/) # 总结 记录经常遇到的问题. URL: https://sunyazhou.com/2020/10/PodSpec/index.html.md Published At: 2020-10-10 07:52:18 +0000 # Pod spec集成第三framework和.a工作记录 ![](/assets/images/20201010PodSpec/cocoapods.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! 最近开发过程经常遇到工程中集成第三方工程中的时候 使用pod的方式集成.总忘记链接 第三方库的方式和podspec的写法.所以记录下来容易忘记的内容 ## podspec 创建 我们在测试工程目录下创建一个目录 起名叫 demoframeworks 然后在这个目录下执行如下: ``` sh pod spec create spec名称 ``` > `spec名称` 自己起个名字哈 本地会生成一个spec模板 然后用文本编辑器编译一下spec文件,这里我们拿声网的sdk举例. ``` ruby Pod::Spec.new do |spec| spec.name = "specdemo" spec.version = "0.0.1" spec.summary = "test pod spec" spec.description = "demo test测试" spec.homepage = "https://www.sunyazhou.com/" spec.license = "MIT" spec.author = { "東引甌越" => "https://www.sunyazhou.com/" } spec.source = { :git => "git@gitee.com:sunyazhou/sunyazhou13.github.io-images.git"} #加载第三方framework写法 spec.vendored_frameworks = 'AgoraRtcCryptoLoader.framework','AgoraRtcEngineKit.framework','AgoraRtmKit.framework','AgoraSigKit.framework' #加载第三方.a #spec.vendored_libraries = 'libProj4.a', 'libJavaScriptCore.a' #系统内置动态库的依赖 spec.frameworks = 'Photos','PhotosUI','CoreMedia','Foundation','CoreGraphics','CoreMotion','QuartzCore','MobileCoreServices','Security','CoreText','VideoToolbox','CoreTelephony','AudioToolbox','SystemConfiguration','AVFoundation', 'CoreLocation','AdSupport','OpenGLES','CoreML' #内置静态库的依赖 spec.libraries = "iconv", "c++", "z.1.1.3" ,"z","resolv" ,"sqlite3","icucore","z.1.2.5" end ``` 然后在Podfile内容里面添加 ``` sh target 'PodSpecDemo' do # Comment the next line if you don't want to use dynamic frameworks use_frameworks! # 添加这行 pod 'specdemo', :path=>'./demoframeworks' end ``` 注意这里的`specdemo`是我们给集成本地pod起的名字 最好和创建的spec名字保持一致. 然后 pod install 最后工程就变成了我们想要的样子 ![](/assets/images/20201010PodSpec/cocoapods1.webp) # 总结 记录经常忘记的知识点 防止着急用的时候各种找,更多spec的写法 [参考官方的api](https://guides.cocoapods.org/syntax/podspec.html#vendored_libraries) [Demo工程点击这里下载](https://github.com/sunyazhou13/PodSpecDemo) 工程中移除了framework 因为github不允许上传超过100m以上的文件.很坑 大家下载后看下写法就好了. URL: https://sunyazhou.com/2020/10/XcodeBuildXcconfigFile/index.html.md Published At: 2020-10-04 11:58:03 +0000 # 使用Xcode配置文件来管理不同的环境设置 ![](/assets/images/20201004XcodeBuildXcconfigFile/XcodeBuildConfigrationFile1.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! ## 背景 最近工程遇到了一个环境切换的问题,想到了 *.xcconfig 文件的用处. 查了一圈搜索引擎大家的搞法真是各种抄袭.遇到的问题没有一个能正式解决的 下面是我遇到的问题,我尝试解决一下. * 创建xcconfig后 cocoapods有警告 * xcconfig继承 需要注意的点 * 解决完警告 编译打印问题 ### 创建 ![](/assets/images/20201004XcodeBuildXcconfigFile/xcconfig1.webp) 下面这里默认勾选tagget (Xcode 不会默认勾选) ![](/assets/images/20201004XcodeBuildXcconfigFile/xcconfig2.webp) 创建完了 选择我们自己的配置 ![](/assets/images/20201004XcodeBuildXcconfigFile/xcconfig3.webp) #### 先说第一个警告问题 搞完后我们来看下pod install后出现的警告 ![](/assets/images/20201004XcodeBuildXcconfigFile/xcconfig4.webp) ``` sh [!] CocoaPods did not set the base configuration of your project because your project already has a custom config set. In order for CocoaPods integration to work at all, please either set the base configurations of the target `XcodeConfigDemo` to `Target Support Files/Pods-XcodeConfigDemo/Pods-XcodeConfigDemo.debug.xcconfig` or include the `Target Support Files/Pods-XcodeConfigDemo/Pods-XcodeConfigDemo.debug.xcconfig` in your build configuration (`XcodeConfigDemo/DemoDebug.xcconfig`). [!] CocoaPods did not set the base configuration of your project because your project already has a custom config set. In order for CocoaPods integration to work at all, please either set the base configurations of the target `XcodeConfigDemo` to `Target Support Files/Pods-XcodeConfigDemo/Pods-XcodeConfigDemo.release.xcconfig` or include the `Target Support Files/Pods-XcodeConfigDemo/Pods-XcodeConfigDemo.release.xcconfig` in your build configuration (`XcodeConfigDemo/DemoRelease.xcconfig`). ``` 先说如何解决 当我们生成了自己的.xcconfig文件后默认是cocoapods的配置,让我们改成了自己的并没有管理cocoapod.所以cocoapods对工程的build setting有可能因为我们的改动而不生效,为了解决这个问题我们需要在自己的xcconfig中导入cocoapods的 xcconfig ![](/assets/images/20201004XcodeBuildXcconfigFile/xcconfig5.webp) ![](/assets/images/20201004XcodeBuildXcconfigFile/xcconfig6.webp) 这里我顺便声明了2个变量 在 debug和release配置里,为了下面测试变量在工程中使用. ``` sh // debug SUNYAZHOU_COM = @"https://www.sunyazhou.com/" SYZ_TEST = @"https://xxxxx.com/" #include "../Pods/Target Support Files/Pods-XcodeConfigDemo/Pods-XcodeConfigDemo.release.xcconfig" #include "DemoCommon.xcconfig" //release SUNYAZHOU_COM = @"https://www.sunyazhou.com/" SYZ_TEST = @"https://xxxxx.com/" #include "../Pods/Target Support Files/Pods-XcodeConfigDemo/Pods-XcodeConfigDemo.debug.xcconfig" #include "DemoCommon.xcconfig" ``` ok 下面 pod install后 警告就没了. #### xcconfig继承 需要注意的点 这里我加了一个通用的 DemoCommon.xcconfig 配置,为了向外输出公共的宏变量. ![](/assets/images/20201004XcodeBuildXcconfigFile/xcconfig7.webp) `GCC_PREPROCESSOR_DEFINITIONS ` . 表示继承通用环境变量 要加入预处理,即加上这句,代码中才可以调到相关的宏定义 ``` sh GCC_PREPROCESSOR_DEFINITIONS = $(inherited) SUNYAZHOU_COM='$(SUNYAZHOU_COM)' SYZ_TEST='$(SYZ_TEST)' ``` 这里需要注意多个变量的格式: ``` sh GCC_PREPROCESSOR_DEFINITIONS = $(inherited)空格(不能加换行)+SUNYAZHOU_COM='$(SUNYAZHOU_COM)'+空格(不能加换行)SYZ_TEST='$(SYZ_TEST)' ``` 我这里遇到的坑是加了20多个变量, 写了一堆.最后发现不像上边的格式那样就编译不过找不到变量. #### 解决完警告 编译打印问题 ![](/assets/images/20201004XcodeBuildXcconfigFile/xcconfig8.webp) ``` sh Unexpected '@' in program ``` 出现这个问题是因为宏变量没有转义 必须将下面的变量 ``` sh //转换前 SUNYAZHOU_COM = @"https://www.sunyazhou.com/" SYZ_TEST = @"https://xxxxx.com/" //转换后 SUNYAZHOU_COM = @"https:\/\/www.sunyazhou.com/" SYZ_TEST = @"https:\/\/xxxxx.com/" ``` ![](/assets/images/20201004XcodeBuildXcconfigFile/xcconfig9.webp) 转换完能编译过了 但是还是有警告,应该是没转义对 不过 能正常输出了. 哪位高手如果知道可以留言给我 多谢! # 总结 有些知识不经常使用容易忘记, xcconfig就是这样.工程 demo我已经附在了下方 感兴趣的同学可以下载. [本文demo](https://github.com/sunyazhou13/XcodeConfigDemo) [参考Mattt 大佬的 Xcode Build Configuration Files](https://nshipster.com/xcconfig/) [Using Xcode Configuration (.xcconfig) to Manage Different Build Settings](https://www.appcoda.com/xcconfig-guide/) URL: https://sunyazhou.com/2020/09/iOSinterviewPerformanceOptimization/index.html.md Published At: 2020-09-22 09:42:48 +0000 # 阿里、字节:一套高效的iOS面试题之性能优化 ![](/assets/images/20200721iOSinterviewAnswers/iOSInterviewQuestionsAlbumCover.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! 本篇我们来讲一下 [阿里、字节:一套高效的iOS面试题](https://mp.weixin.qq.com/s/bDnsaD__ZpdHIk3_So382w) 中的性能优化相关的问题. ## 性能优化 主要的优化如下: 1. 如何做启动优化,如何监控 2. 如何做卡顿优化,如何监控 3. 如何做耗电优化,如何监控 4. 如何做网络优化,如何监控 首先优化要从多维度进行才有较大的收益 这里推荐大家认真分析一下自己的工程并研读一下戴铭老师的[如何对 iOS 启动阶段耗时进行分析](https://ming1016.github.io/2019/12/07/how-to-analyze-startup-time-cost-in-ios/) 文章 必须要从多维度分析并入手. 运行时初始化过程 分为: * 加载类扩展 * 加载 C++静态对象 * 调用+load 函数 * 执行 main 函数 * Application 初始化,到 applicationDidFinishLaunchingWithOptions 执行完 初始化帧渲染,到 viewDidAppear 执行完,用户可见可操作。 # 总结 性能优化部分 并没有标准的答案,所以分享给大家一篇重要的文章作为抓手和参考,只要达到预期的优化目的并保证程序稳定即可. URL: https://sunyazhou.com/2020/09/UIViewGraphic/index.html.md Published At: 2020-09-20 11:40:47 +0000 # 阿里、字节:一套高效的iOS面试题之视图&图形 ![](/assets/images/20200721iOSinterviewAnswers/iOSInterviewQuestionsAlbumCover.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! 本篇我们来讲一下 [阿里、字节:一套高效的iOS面试题](https://mp.weixin.qq.com/s/bDnsaD__ZpdHIk3_So382w) 中的视图&图形相关的问题. ## 视图&图像相关 主要问题列表如下: 1. AutoLayout的原理,性能如何 2. UIView & CALayer的区别 3. 事件响应链 4. drawrect & layoutsubviews调用时机 5. UI的刷新原理 6. 隐式动画 & 显示动画区别 7. 什么是离屏渲染 8. imageName&imageWithContentsOfFile区别 9. 多个相同的图片,会重复加载吗 10. 图片是什么时候解码的,如何优化 11. 图片渲染怎么优化 12. 如果GPU的刷新率超过了iOS屏幕60Hz刷新率是什么现象,怎么解决 ### 1.AutoLayout的原理,性能如何? #### AutoLayout的原理 > 来历 一般大家都会认为Auto Layout这个东西是苹果自己搞出来的,其实不然,早在1997年Alan Borning, Kim Marriott, Peter Stuckey等人就发布了《Solving Linear Arithmetic Constraints for User Interface Applications》论文([论文地址:http://constraints.cs.washington.edu/solvers/uist97.html](http://constraints.cs.washington.edu/solvers/uist97.html))提出了在解决布局问题的Cassowary constraint-solving算法实现,并且将代码发布在他们搭建的[Cassowary网站上http://constraints.cs.washington.edu/cassowary/](http://constraints.cs.washington.edu/cassowary/)。后来更多开发者用各种语言来写Cassowary,比如说pybee用python写的https://github.com/pybee/cassowary。自从它发布以来JavaScript,.NET,JAVA,Smalltall和C++都有相应的库。2011年苹果将这个算法运用到了自家的布局引擎中,美其名曰Auto Layout。 论文下载链接比较慢,我下载了一份[Cassowary原文放到了我的博客 大家可以自由下载](/assets/images/20200920UIViewGraphic/Cassowary.pdf). **AutoLayout的原理就是用Cassowary算法来将布局问题抽象成线性不等式,并分解成多个位置间的约束** 因为多了计算视图大小frame的过程,所以性能肯定没有指定Frame坐标要快. 详细的原理以及高阶原理请参考戴铭老师的文章 [戴铭老师写的 深入剖析Auto Layout,分析iOS各版本新增特性](http://www.starming.com/2015/11/03/deeply-analyse-autolayout/) #### 性能如何? 下面是[WWDC2018 High Performance Auto Layout](https://developer.apple.com/videos/play/wwdc2018/220/)中对比的iOS12和iOS11下分别使用自动布局的性能对比现场. ![](/assets/images/20200920UIViewGraphic/HighPerformanceAutoLayoutiOS11iOS12Compare.gif) 经过实验得出如下图标结论: ![](/assets/images/20200920UIViewGraphic/HighPerformanceAutoLayoutResult.webp) iOS12之前,视图嵌套的数量对性能的影响是呈指数级增长的,而iOS12优化之后对性能的影响是线性增长,对性能消耗不大。 无论如何优化也肯定不如CGRectFrame那样的设置更加直接,性能更好. ### 2.UIView & CALayer的区别 | 区别 | UIView | CALayer | | :------| :------: | :------: | | 继承父类 | UIView:UIResponder:NSObject | CALayer:NSObject | | 用途 | 可以处理触摸事件 | 不处理用户的交互,不参与响应事件传递 | | 两者关系 | 有一个CALayer成员变量 eg: view.layer | 是UIView的成员变量 | | 分工 | 处理交互层事件并包装各种图形的简单设置 | 底层渲染图形,支持动画 | ### 3.事件响应链 下面这篇文章我已经在前几篇将runloop的时候提了不止一次,前列建议阅读,快手的同事大部分都以这个理解为标准 [iOS触摸事件全家桶](https://mp.weixin.qq.com/s/9rvSRt4kfpy7e87EJoaJOQ) ### 4. drawrect & layoutsubviews调用时机 `layoutSubviews:`(相当于layoutSubviews()函数)在以下情况下会被调用: 1. init初始化不会触发layoutSubviews。 2. addSubview会触发layoutSubviews。 3. 设置view的Frame会触发layoutSubviews (frame发生变化触发)。 4. 滚动一个UIScrollView会触发layoutSubviews。 5. 旋转Screen会触发父UIView上的layoutSubviews事件。 6. 改变一个UIView大小的时候也会触发父UIView上的layoutSubviews事件。 7. 直接调用setLayoutSubviews。 `drawrect:`(drawrect()函数)在以下情况下会被调用: 1. `drawrect:`是在UIViewController的`loadView:`和`ViewDidLoad:`方法之后调用. 2. 当我们调用`[UIFont的 sizeToFit]`后,会触发系统自动调用`drawRect:` 3. 当设置UIView的contentMode或者Frame后会立即触发触发系统调用`drawRect:` 4. 直接调用`setNeedsDisplay`设置标记 或`setNeedsDisplayInRect:`的时候会触发`drawRect:` > 知识点扩充: 当我们操作drawRect方法的时候实际是在操作内存中存放视图的backingStore区域,用于后续图形的渲染操作,如果不理解可以看下[UIView的渲染过程](https://www.sunyazhou.com/2017/10/16/20171016UIViewRendering/). ### 5.UI的刷新原理 这个问题我不知道问的是不是iOS离屏渲染过程,我来简单的回到一下这个吧 iOS 的`MainRunloop` 是一个60fps 的回调,也就是说16.7ms(毫秒)会绘制一次屏幕在这过程中要完成以下的工作: * view的缓冲区创建 * view内容的绘制(如果重写了 drawRect) * 接收和处理系统的触摸事件 我们看到的UI图形实际上是CPU和GPU不断配合工作的结果.经过[UIView的渲染过程](https://www.sunyazhou.com/2017/10/16/20171016UIViewRendering/) 后我们的UI会不间断的接收系统图给我们的事件. 由于主线程的runloop 一直在回调,我们的UI就得到了刷新的窗口,是渲染还是处理事件都是因为runloop不断工作的结果.前几篇我们学过 main线程的runloop默认是启动的.因为我们响应交互. 不知道我这样回答是否满足这个问题的答案.如果回答的不对烦请下方评论区留言 告知我将持续改进. ### 6.隐式动画 & 显示动画区别 隐式动画一直存在 如需关闭需设置 显式动画是不存在,如需显式 要开启 只需要观察动画执行完成的结果 比如: 一个简单UIView的frame移动 如果从A点移动到B点 移动完成 回到原始位置就是隐式动画 Core Animation 是显式动画.因为它既可以直接对其layer属性做动画,也可以覆盖默认的图层行为. ### 7.imageName&imageWithContentsOfFile区别 | 区别 | UIView | imageWithContentsOfFile | | :------| :------: | :------: | | 不同点 | 会图片缓存到内存中 | 无缓存 | ### 8.什么是离屏渲染 ![](/assets/images/20200920UIViewGraphic/CoreAnimationPipeline.webp) [iOS离屏渲染的深入研究](https://zhuanlan.zhihu.com/p/72653360) ### 9.多个相同的图片,会重复加载吗 不会,GPU有 像素点缓存的mask. ### 10.图片是什么时候解码的,如何优化 是加载到内存中,从UIImge->CGImage->CGImageSourceCreateWithData(data) 创建ImageSource变成bitmap位图,这些工作都是CoreAnimation在图片被加载到内存中存在在backingStore里,送给GPU流水线处理之前被解码. #### 如何优化 自己手动操作图片的编码API CGImageSource开头的哪些,根据合理利用时机和操作系统资源调整出一套缓存小加载快的库. 参考[PINRemoteImage](https://github.com/pinterest/PINRemoteImage)或者[YYWebImage](https://github.com/ibireme/YYWebImage)开源 ### 11.图片渲染怎么优化 可以从阴影,圆角入手.帧率,电量,图片的锯齿等等. [iOS开发-视图渲染与性能优化](https://www.jianshu.com/p/748f9abafff8) ### 12.如果GPU的刷新率超过了iOS屏幕60Hz刷新率是什么现象,怎么解决 现象是 图形清晰,场景逼真,但是一般arm芯片的GPU 刷新超过60Hz一定会超级费电,手机发热导致降频.FPS降低,因为低能耗电量不足,无法支持GPU高刷新率 解决办法只能用xcode自带工具检测,看渲染过程哪里可以优化. # 总结 简单回答了一些图形相关的问题,大部分都是iOS离屏渲染,这个地方大家要认真学习.很多资料看起来比较耗时. URL: https://sunyazhou.com/2020/09/GCD/index.html.md Published At: 2020-09-19 11:09:28 +0000 # 阿里、字节:一套高效的iOS面试题之多线程 ![](/assets/images/20200721iOSinterviewAnswers/iOSInterviewQuestionsAlbumCover.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! 本篇我们来讲一下 [阿里、字节:一套高效的iOS面试题](https://mp.weixin.qq.com/s/bDnsaD__ZpdHIk3_So382w) 中的多线程相关的问题. ## 多线程 这一篇我们来解答下多线程问题,主要以GCD为主: * iOS开发中有多少类型的线程?分别对比 * GCD有哪些队列,默认提供哪些队列 * GCD有哪些方法api * GCD主线程 & 主队列的关系 * 如何实现同步,有多少方式就说多少 * `dispatch_once`实现原理 * 什么情况下会死锁 * 有哪些类型的线程锁,分别介绍下作用和使用场景 * NSOperationQueue中的`maxConcurrentOperationCount`默认值 * NSTimer、CADisplayLink、`dispatch_source_t` 的优劣 ### 1.iOS开发中有多少类型的线程?分别对比 | 线程类型 | 对比 | 备注 | | :------| :------: | :------: | | `pthread_t` | 跨平台C语言标准库中的多线程框架 | 过于底层使用很麻烦,需要封装使用. | | GCD(Grand Central Dispatch) | iOS5后苹果推出的双核CPU优化的多线程框架,对A5以后的CPU有很多底层优化,C函数的形式调用 有点面向过程,不能直接设置并发数,需要写一些代码曲线方式实现并发 | 推荐使用 | | NSOperation & NSOperationQueue | 更加面向对象 可以设置并发数量 | GCD 的封装 | > 苹果底层库经过自己多年实践没有问题才会推荐给上层使用, eg:siri. 所以NSOperation实际上是苹果的ver1.0的多线程SDK,对GCD封装和`pthread_t`的封装. ### 2.GCD有哪些队列,默认提供哪些队列 * 1.主线程串行队列 * 2.全局并行队列 * 3.自定义队列(可自行设置`串/并`的参数`DISPATCH_QUEUE_SERIAL`和`DISPATCH_QUEUE_CONCURRENT`) 下面我整理了一个表格: | 队列类型 | 对应函数 | 系统默认提供/自定义 | 优先级 | | :------| :------: | :------: | :------: | | 主线程串行队列(mian)| `dispatch_get_main_queue()` | 系统 | | | 全局并行队列(global)| `dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)` | 系统 | 系统提供参数设置 | | 自定义并行队列(Concurrent)| `dispatch_queue_create("com.sunyazhou.self.queue.concurrent", DISPATCH_QUEUE_CONCURRENT)` | 自定义 | | | 自定义串行队列(Serial)| `dispatch_queue_create("com.sunyazhou.self.queue.serial", DISPATCH_QUEUE_SERIAL)` | 自定义 | | `dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)` 其中的第一个参数就是队列的优先级,具体对于优先级QOS如下: | GCD全局队列优先级宏定义 | 对应枚举数值 | 对应Qos | | :------| :------: | :------ | | `DISPATCH_QUEUE_PRIORITY_HIGH` | 2 | `QOS_CLASS_USER_INITIATED` | | `DISPATCH_QUEUE_PRIORITY_DEFAULT` | 0 | `QOS_CLASS_DEFAULT` | | `DISPATCH_QUEUE_PRIORITY_LOW` | -2 | `QOS_CLASS_UTILITY` | | `DISPATCH_QUEUE_PRIORITY_BACKGROUND` | `INT16_MIN` | `QOS_CLASS_BACKGROUND` | > 其中`dispatch_get_global_queue `的第二个参数flag只是一个苹果予保留字段,通常我们传0(你可以试试传1应该队列创建失败) ### 3.GCD有哪些方法api * 队列相关API ``` objc dispatch_get_main_queue(void) //获取主线程队列 dispatch_get_global_queue(intptr_t identifier, uintptr_t flags) //获取全局队列 dispatch_queue_create(const char *_Nullable label,dispatch_queue_attr_t _Nullable attr) //创建自定义队列 (一般大家都用域名倒置来区分队列的唯一标识,苹果对标识符是否一致在iOS10后有优化请注意.) ``` * 执行API ``` objc dispatch_async(dispatch_queue_t queue, dispatch_block_t block) //在某队列开启异步线程 block{}花括号内的代码将在某队列异步运行 dispatch_sync(dispatch_queue_t queue, DISPATCH_NOESCAPE dispatch_block_t block) //在某队列开启同步线程 block{}花括号内的代码将在某队列同步运行 dispatch_after(dispatch_time_t when, dispatch_queue_t queue, dispatch_block_t block) //GCD定时器 多久后执行 block dispatch_once(dispatch_once_t *predicate, DISPATCH_NOESCAPE dispatch_block_t block) //单次操作 (单位时间内只允许一个线程进入操作系统的临界区,一般创建单利时使用)这个变量可以区分冷热启动. dispatch_apply(size_t iterations, dispatch_queue_t DISPATCH_APPLY_QUEUE_ARG_NULLABILITY queue, DISPATCH_NOESCAPE void (^block)(size_t)) //向队列中追加任务操作并等待处理执行结束. dispatch_barrier_async() //将自己的任务插入到队列之后,不会等待自己的任务结束,它会继续把后面的任务插入到队列,然后等待自己的任务结束后才执行后面任务 dispatch_barrier_sync() //将自己的任务插入到队列的时候,需要等待自己的任务结束之后才会继续插入被写在它后面的任务,然后执行它们 ``` * 调度组API ``` objc dispatch_group_create(void) //创建GCD 调度组 dispatch_group_async(dispatch_group_t group, dispatch_queue_t queue,dispatch_block_t block) //调度组开启异步线程 dispatch_group_enter() //调度组信号量 需要和leave成对出现. dispatch_group_leave() //调度组信号量 需要和enter成对出现. dispatch_group_notify() //调度组任务完成通知调用方 操作(一般都回到主线程) dispatch_group_wait() //整个调度组 阻塞操作.只等待不做结束处理 ``` * 信号量API ``` objc dispatch_semaphore_create(intptr_t value) //创建信号量 (可以理解为是线程锁) dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout) //信号-1 dispatch_semaphore_signal(dispatch_semaphore_t dsema) //信号+1 ``` * 调度资源API ``` objc dispatch_source_create() dispatch_source_set_timer() dispatch_source_set_event_handler() dispatch_activate() dispatch_resume() dispatch_suspend() dispatch_source_cancel() dispatch_source_testcancel() dispatch_source_set_cancel_handler() dispatch_notify() dispatch_get_context() dispatch_set_contex() dispatch_queue_set_specific() 给队列设置标识 dispatch_queue_get_specific() 取出队列标识 dispatch_get_specific() 查询线程标识 ... ``` ### 4.GCD主线程 & 主队列的关系 提交到主队列的任务在主线程执行. ### 5.如何实现同步,有多少方式就说多少 * `dispatch_sync(dispatch_queue_t queue, DISPATCH_NOESCAPE dispatch_block_t block)` 在某队列开启同步线程 * dispatch_barrier_sync() 障碍锁的方式同步 * dispatch_group_create() + dispatch_group_wait() * dispatch_apply() 插队追加 操作同步 * dispatch_semaphore_create() + dispatch_semaphore_wait() 信号量锁 * 串行NSOperationQueue队列并发数为1的时候 [NSOpertaion start] 启动任务即使同步操作 (NSOperationQueue.maxConcurrentOperationCount = 1) * `pthread_mutex`底层锁函数 * 上层应用层封装的NSLock * NSRecursiveLock 递归锁,这个锁可以被同一线程多次请求,而不会引起死锁。这主要是用在循环或递归操作中 * NSConditionLock & NSCondition 条件锁 * @synchronized 同步操作 单位时间内只允许一个线程进入临界区 * dispatch_once() 单位时间内只允许一个线程进入临界区 ... ### 6.`dispatch_once`实现原理 这个问题问的很傻吊也很高超.因为要解释清楚所有步骤需要记住里面所有代码 我认为这个问题应该从操作系统层面回答, 这个问题的核心是操作系统返回状态决定的,**单位时间内操作系统只允许一个线程进入临界区,进入临界区的线程会被标记** 回归到代码就是 ``` sh dispatch_once(dispatch_once_t *val, dispatch_block_t block) |_____dispatch_once_f(val, block, _dispatch_Block_invoke(block)) |_______&l->dgo_once // &l->dgo_once 地址中存储的值。显然若该值为DLOCK_ONCE_DONE,即为once已经执行过 ``` `dgo_once`是`dispatch_once_gate_s`的成员变量 ``` objc typedef struct dispatch_once_gate_s { union { dispatch_gate_s dgo_gate; uintptr_t dgo_once; }; } dispatch_once_gate_s, *dispatch_once_gate_t; ``` 有个内联函数`static inline bool _dispatch_once_gate_tryenter(dispatch_once_gate_t l)` 这个内联函数返回一个 原子性操作的结果 ``` return os_atomic_cmpxchg(&l->dgo_once, DLOCK_ONCE_UNLOCKED,(uintptr_t)_dispatch_lock_value_for_self(), relaxed) ``` 比较+交换 的原子操作。比较 `&l->dgo_once` 的值是否等于 `DLOCK_ONCE_UNLOCKED` 这样就实现了我们的执行1次的GCD API. [dispatch_once的底层实现](https://juejin.im/post/6844904143753052174) ### 7.什么情况下会死锁 造成死锁的主要是 线程信息不对称,出现A等B的同时 B也在等A的情况. ``` objc /// 在主线程中执行这句代码 dispatch_sync(dispatch_get_main_queue(), ^{ NSLog(@"这里死锁了"); }); ``` 主线程一直不会执行完,追加到主线程同步执行的任务显然惨死.卡住主线程无法自拔. 其它的情况 都是资源产生竞争或者调用lock的函数没有调用unlock导致,异步线程 先后调用等产生的较多. ### 8.有哪些类型的线程锁,分别介绍下作用和使用场景 | 锁类型 | 使用场景 | 备注 | | :------| :------: | :------ | | `pthread_mutex` | 互斥锁 | `PTHREAD_MUTEX_NORMAL`,`#import `| | OSSpinLock | 自旋锁 | 不安全,iOS 10 已启用 | | `os_unfair_lock ` | 互斥锁 | 替代 OSSpinLock | | `pthread_mutex`(recursive) | 递归锁 | `PTHREAD_MUTEX_RECURSIVE`,`#import `| | `pthread_cond_t` | 条件变量 | `#import `| | `pthread_rwlock ` | 读写锁 | 读操作重入,写操作互斥 | | @synchronized | 互斥锁 | 性能差,且无法锁住内存地址更改的对象 | | NSLock | 互斥锁 | 封装 `pthread_mutex` | | NSRecursiveLock | 递归锁 | 封装`pthread_mutex`(recursive)| | NSCondition | 条件锁| 封装 `pthread_cond_t ` | | NSConditionLock | 条件锁 | 可以指定具体条件值 封装 `pthread_cond_t `| ### 9.NSOperationQueue中的maxConcurrentOperationCount默认值 默认值 -1. 这个值操作系统会根据资源使用的综合开销情况设置. ### 10.`NSTimer、CADisplayLink、`dispatch_source_t` 的优劣 | 定时器类型 | 优势 | 劣势 | | :------| :------: | :------ | | NSTimer |使用简单 | 依赖 Runloop,具体表现在 无 Runloop 无法使用、NSRunLoopCommonModes、不精确 | | CADisplayLink | 依赖屏幕刷新频率出发事件,最精.最合适做UI刷新 | 若屏幕刷新被影响,事件也被影响、事件触发的时间间隔只能是屏幕刷新 duration 的倍数、若事件所需时间大于触发事件,跳过数次、不能被继承 | | `dispatch_source_t` |不依赖 Runloop | 依赖线程队列,使用麻烦 使用不当容易Crash | # 总结 今天这篇多线程 也算是一个objc开发者的知识总结,这里面问到的知识大部分和队列线程关系比较多. 高阶一些的搞法并没有. 比如:如何停掉`dispatch_source_t `的定时器.再比如 为什么要存在`dispatch_source`. 下一篇我们讲解一下 视图&图像相关文章. URL: https://sunyazhou.com/2020/09/Block/index.html.md Published At: 2020-09-17 14:34:10 +0000 # 阿里、字节:一套高效的iOS面试题之Block ![](/assets/images/20200721iOSinterviewAnswers/iOSInterviewQuestionsAlbumCover.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! ## block 这一篇我们来研究一下objc的block并回答一下面试中的下列问题: 1.block的内部实现,结构体是什么样的 2.block是类吗,有哪些类型 3.一个int变量被 `__block` 修饰与否的区别?block的变量截获 4.block在修改NSMutableArray,需不需要添加`__block` 5.怎么进行内存管理的 6.block可以用strong修饰吗 7.解决循环引用时为什么要用`__strong`、`__weak`修饰 8.`block`发生`copy`时机 9.`Block`访问对象类型的`auto`变量时,在`ARC`和`MRC`下有什么区别 在回答所有问题之前我们需要了解一些block背景相关的知识. 如下: \- 如何查看Block的内部实现,也就是说转换成背后真正的c/c++代码的block是什么样的?以及转换格式或者原理等. \-关于变量的作用域 #### Objective-C 转 C++的方法 下面我写了个示例`TestClass.m`类其中block代码如下 OC代码: ``` objc @interface TestClass () @end @implementation TestClass - (void)testMethods { void (^blockA)(int a) = ^(int a) { NSLog(@"%d",a); }; if (blockA) { blockA(1990); } } @end ``` 经过上述转换操作我们在TestClass.cpp中最下面发现如下代码 C++代码 ``` objc // @interface TestClass () /* @end */ // @implementation TestClass struct __TestClass__testMethods_block_impl_0 { struct __block_impl impl; struct __TestClass__testMethods_block_desc_0* Desc; __TestClass__testMethods_block_impl_0(void *fp, struct __TestClass__testMethods_block_desc_0 *desc, int flags=0) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; static void __TestClass__testMethods_block_func_0(struct __TestClass__testMethods_block_impl_0 *__cself, int a) { NSLog((NSString *)&__NSConstantStringImpl__var_folders_wx_b8tcry0j24dbhr7zlzjq3v340000gn_T_TestClass_ee18d3_mi_0,a); } static struct __TestClass__testMethods_block_desc_0 { size_t reserved; size_t Block_size; } __TestClass__testMethods_block_desc_0_DATA = { 0, sizeof(struct __TestClass__testMethods_block_impl_0)}; static void _I_TestClass_testMethods(TestClass * self, SEL _cmd) { void (*blockA)(int a) = ((void (*)(int))&__TestClass__testMethods_block_impl_0((void *)__TestClass__testMethods_block_func_0, &__TestClass__testMethods_block_desc_0_DATA)); if (blockA) { ((void (*)(__block_impl *, int))((__block_impl *)blockA)->FuncPtr)((__block_impl *)blockA, 1990); } } ``` 上面的代码生成是通过如下操作: 打开终端,cd到TestClass.m所在文件夹,使用如下命令 ``` c++ clang -rewrite-objc TestClass.m ``` 就会在当前文件夹内自动生成对应的TestClass.cpp文件 > 注意: 如果提示clang没有的话 需要安装, 输入如下 ``` sh brew install clang-format 或者 brew link clang-forma 然后输入 下面命令测试是否好使 clang-format --help ``` 通过上述代码我们发现Block的其实是一个结构体类型 底层实现 会根据 `__`**类名**`__`**方法名**`_`block`_`impl`_`**下标** (0代表这个方法或者这个类中第0个block 下面如果还有将会 第1个block 第2个...) ``` objc struct __类名__方法名_block_impl_下标 ``` #### 关于变量的作用域 c语言的函数中可能使用的参数变量种类 * 参数类型 * 自动变量(局部变量) * 静态变量(静态局部变量) * 静态全局变量 * 全局变量 由于存储区域特殊,这其中有三种变量是可以在任何时候以任何状态调用的. * 静态变量 * 静态全局变量 * 全局变量 而其他两种,则是有各自相应的作用域,超过作用域后,会被销毁. --- ### 1.block的内部实现,结构体是什么样的 看了上面的背景知识我们来回到一下这个问题 block的内部实现如下: ``` objc struct __TestClass__testMethods_block_impl_0 { struct __block_impl impl; //成员变量 struct __TestClass__testMethods_block_desc_0* Desc; //desc 结构体声明 // 构造函数 // fp 函数指针 // desc 静态全局变量初始化的 __main_block_desc_ 结构体实例指针 // flags block 的负载信息(引用计数和类型信息),按位存储. __TestClass__testMethods_block_impl_0(void *fp, struct __TestClass__testMethods_block_desc_0 *desc, int flags=0) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; //将来被调用的block内部的代码:block值被转换为C的函数代码 //这里,*__cself 是指向Block的值的指针,也就相当于是Block的值它自己(相当于C++里的this, OC里的self) //__cself 是指向__TestClass__testMethods_block_impl_0结构体实现的指针 //Block结构体就是__TestClass__testMethods_block_impl_0结构体.Block的值就是通过__TestClass__testMethods_block_impl_0构造出来的 static void __TestClass__testMethods_block_func_0(struct __TestClass__testMethods_block_impl_0 *__cself, int a) { NSLog((NSString *)&__NSConstantStringImpl__var_folders_wx_b8tcry0j24dbhr7zlzjq3v340000gn_T_TestClass_9f58f7_mi_0,a); } static struct __TestClass__testMethods_block_desc_0 { size_t reserved; size_t Block_size; } __TestClass__testMethods_block_desc_0_DATA = { 0, sizeof(struct __TestClass__testMethods_block_impl_0)}; static void _I_TestClass_testMethods(TestClass * self, SEL _cmd) { void (*blockA)(int a) = ((void (*)(int))&__TestClass__testMethods_block_impl_0((void *)__TestClass__testMethods_block_func_0, &__TestClass__testMethods_block_desc_0_DATA)); if (blockA) { ((void (*)(__block_impl *, int))((__block_impl *)blockA)->FuncPtr)((__block_impl *)blockA, 1990); } } ``` 可以看得出来`__TestClass__testMethods_block_impl_0`有3个部分组成 * impl 函数指针指向`__TestClass__testMethods_block_impl_0` ``` objc struct __block_impl { void *isa; int Flags; int Reserved; //今后版本升级所需的区域 void *FuncPtr; //函数指针 }; ``` * Desc 指向`__TestClass__testMethods_block_impl_0`的Desc指针,用于描述当前这个block的附加信息的,包括结构体的大小等等信息. ``` objc static struct __TestClass__testMethods_block_desc_0 { size_t reserved; //今后升级版本所需区域 size_t Block_size; //block的大小 } __TestClass__testMethods_block_desc_0_DATA = { 0, sizeof(struct __TestClass__testMethods_block_impl_0)}; ``` * `__TestClass__testMethods_block_impl_0()`构造函数,也就是该block的具体实现 ``` objc __TestClass__testMethods_block_impl_0(void *fp, struct __TestClass__testMethods_block_desc_0 *desc, int flags=0) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } ``` 此结构体中 * isa指针保持这所属类的结构体的实例的指针. * `struct __TestClass__testMethods_block_impl_0`相当于Objective-C类对象的结构体 * `_NSConcreteStackBlock`相当于Block的结构体实例,也就是说**block其实就是Objective-C对于闭包的对象实现** 讲到这里block的内部实现你看懂了吗?结构体是什么样的你记住了吗? 其实看着繁琐 细心观察代码会发现还是比较简单的. ### 2.block是类吗,有哪些类型? block也算是个类,因为它有isa指针,block.isa的类型包括 * _NSConcreteGlobalBlock 跟全局变量一样,设置在程序的数据区域(.data)中 * _NSConcreteStackBlock栈上(前面讲的都是栈上的 block) * _NSConcreteMallocBlock 堆上 > 这个isa可以按位运算 ### 3.一个int变量被 `__block` 修饰与否的区别?block的变量截获 #### 被`__block` 修饰与否的区别 用一段示例代码来解答这个问题吧: ``` objc __block int a = 10; int b = 20; PrintTwoIntBlock block = ^(){ a -= 10; printf("%d, %d\n",a,b); }; block();//0 20 a += 20; b += 30; printf("%d, %d\n",a,b);//20 50 block();/10 20 ``` 通过`__block`修饰`int` `a`,block体中对这个变量的引用是指针拷贝,它会作为block结构体构造参数传入到结构体中且复制这个变量的指针引用,从而达到可以修改变量的作用. `int` `b`没有被`__block`修饰,block内部对`b`是值copy.所以在block内部修改`b`不影响外部b的变化. #### block的变量截获 通过如下代码我们来观察要一下变量的捕获 ``` objc blk_t blk; { id array = [NSMutableArray new]; blk = [^(id object){ [array addObject:object]; NSLog(@"array count = %ld",[array count]); } copy]; } blk([NSObject new]); blk([NSObject new]); blk([NSObject new]); ``` 输出打印 ``` sh block_demo[28963:1629127] array count = 1 block_demo[28963:1629127] array count = 2 block_demo[28963:1629127] array count = 3 ``` 我们把上面的代码翻译成C++看下 ``` c++ struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; id array;//截获的对象 __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, id _array, int flags=0) : array(_array) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; ``` 在Objc中,C结构体里不能含有被`__strong`修饰的变量,因为编译器不知道应该何时初始化和废弃C结构体。但是Objc的运行时库能够准确把握`Block`从栈复制到堆,以及堆上的block被废弃的时机,在实现上是通过`__TestClass__testMethods_block_copy_0`函数和`__TestClass__testMethods_block_dispose_0`函数进行的 ``` objc static void __TestClass__testMethods_block_copy_0(struct __TestClass__testMethods_block_impl_0*dst, struct __TestClass__testMethods_block_impl_0*src) { _Block_object_assign((void*)&dst->array, (void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/); } static void __TestClass__testMethods_block_dispose_0(struct __TestClass__testMethods_block_impl_0*src) { _Block_object_dispose((void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/); } ``` * `_Block_object_assign`相当于retain操作,将对象赋值在对象类型的结构体成员变量中. * `_Block_object_dispose`相当于release操作. 这两个函数调用的时机是在什么时候呢? | 函数 | 被调用时机 | | ------ | ------ | | `__TestClass__testMethods_block_copy_0 ` | 从栈复制到堆时 | | `__TestClass__testMethods_block_dispose_0 ` | 堆上的Block被废弃时 | ##### 什么时候栈上的Block会被复制到堆呢? * 调用block的copy函数时。 * Block作为函数返回值返回时。 * 将Block赋值给附有`__strong`修饰符id类型的类或者Block类型成员变量时。 * 方法中含有usingBlock的Cocoa框架方法或者GCD的API中传递Block时。 ##### 什么时候Block被废弃呢? * 堆上的Block被释放后,谁都不再持有Block时调用dispose函数。 以上就是变量被block捕获的内容 --- ### 4.`block`在修改`NSMutableArray`,需不需要添加`__block` * 如修改`NSMutableArray`的存储内容的话,是不需要添加`__block`修饰的。 * 如修改`NSMutableArray`对象的本身,那必须添加`__block`修饰。 ### 5.怎么进行内存管理的? 在上面Block的构造函数`__TestClass__testMethods_block_impl_0`中的isa指针指向的是&_NSConcreteStackBlock,它表示当前的Block位于栈区中. | block内存操作 | 存储域/存储位置 | copy操作的影响 | | :------: | :------: | :------: | | _NSConcreteGlobalBlock |程序的数据区域 | 什么也不做 | | _NSConcreteStackBlock | 栈 | 从栈拷贝到堆 | | _NSConcreteMallocBlock| 堆 | 引用计数增加 | * 全局Block:`_NSConcreteGlobalBlock`的结构体实例设置在程序的数据存储区,所以可以在程序的任意位置通过指针来访问,它的产生条件: * 记述全局变量的地方有block语法时. * block不截获的自动变量. > 以上两个条件只要满足一个就可以产生全局Block. [参考](https://juejin.im/post/6844903474312773646#heading-13) * 栈Block:`_NSConcreteStackBlock`在生成Block以后,如果这个Block不是全局Block,那它就是栈Block,生命周期在其所属的变量作用域内.(也就是说如果销毁取决于所属的变量作用域).如果Block变量和`__block`变量复制到了堆上以后,则不再会受到变量作用域结束的影响了,因为它变成了堆Block. * 堆Block:`_NSConcreteMallocBlock`将栈block复制到堆以后,block结构体的isa成员变量变成了`_NSConcreteMallocBlock`。 ### 6.block可以用strong修饰吗? 在ARC中可以,因为在ARC环境中的block只能在堆内存或全局内存中,因此不涉及到从栈拷贝到堆中的操作. 在MRC中不行,因为要有拷贝过程.如果执行copy用strong的话会crash, `strong`是ARC中引入的关键字.如果使用retain相当于忽视了block的copy过程. ### 7.解决循环引用时为什么要用`__strong`、`__weak`修饰? 首先因为block捕获变量的时候 结构体构造时传入了self,造成了默认的引用关系,所以一般在block外部对操作对象会加上`__weak`,在Block内部使用`__strong`修饰符的对象类型的自动变量,那么当Block从栈复制到堆的时候,该对象就会被Block所持有,但是持有的是我们上面加了`__weak`所以行程了比消此长的链条,刚好能解决block延迟销毁的时候对外部对象生命周期造成的影响.如果不这样做很容易造成循环引用. ### 8.block发生copy时机? 在ARC中,编译器将创建在栈中的block会自动拷贝到堆内存中,而block作为方法或函数的参数传递时,编译器不会做copy操作. * 调用block的copy函数时。 * Block作为函数返回值返回时。 * 将Block赋值给附有`__strong`修饰符id类型的类或者Block类型成员变量时。 * 方法中含有usingBlock的Cocoa框架方法或者GCD的API中传递Block时。 ### 9.Block访问对象类型的auto变量时,在ARC和MRC下有什么区别? ARC下会对这个对象强引用,MRC下不会 [详细请参考](https://juejin.im/post/6844903474312773646) URL: https://sunyazhou.com/2020/09/iOSinterviewAnswers5/index.html.md Published At: 2020-09-02 11:23:24 +0000 # 阿里、字节:一套高效的iOS面试题之Runloop&KVO ![](/assets/images/20200721iOSinterviewAnswers/iOSInterviewQuestionsAlbumCover.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! [前几篇](https://www.sunyazhou.com/tags/iOS%E9%9D%A2%E8%AF%95%E9%A2%98/)我们一路讲了内存,关联对象、ARC、AutoreleasePool、weak对象, NSNotifacionCenter等, 今天这一篇我们来讲一下 Runloop和KVO 本章的主要回答的问题如下: #### Runloop * app如何接收到触摸事件的 * 为什么只有主线程的runloop是开启的 * 为什么只在主线程刷新UI * PerformSelector和runloop的关系 * 如何使线程保活 #### KVO * 实现原理 * 如何手动关闭kvo * 通过KVC修改属性会触发KVO么 * 哪些情况下使用kvo会崩溃,怎么防护崩溃 * kvo的优缺点 ## Runloop 作为一个合格的iOS开发者必须对runloop有一个更深入的了解,下面我们来回答一下 相关问题 ### 1.app如何接收到触摸事件的 回答这个问题前请认真阅读一下 [iOS触摸事件全家桶](https://mp.weixin.qq.com/s/9rvSRt4kfpy7e87EJoaJOQ) ![](/assets/images/20200902iOSinterviewAnswers/runloop_event_receive.webp) 通过上图可以看出整个流程就是 我们app启动默认会通过machPort监听端口的方式 来接受IOHIDEvent 来接收和处理触摸事件. ### 2.为什么只有主线程的runloop是开启的 mian()函数中调用UIApplicationMain,这里会创建一个主线程,用于UI处理,为了让程序可以一直运行并接收事件,所以在主线程中开启一个runloop,让主线程常驻. ### 3.为什么只在主线程刷新UI 我们所有用到的UI都是来自于UIKit这个基础库.因为objc不是一门线程安全的语言所以存在多线程读写不同步的问题,如果使用加锁的方式操作系统开销很大,会耗费大量的系统资源(内存、时间片轮转、cpu处理速度...),加上上面讲到的系统事件的接收处理都在主线程,如果UI异步线程的话 还会存在 同步处理事件的问题,所以多点触摸手势等一些事件要保持和UI在同一个线程相对是最优解. 另一方面是 屏幕的渲染是 60帧(60Hz/秒), 也就是1秒钟回调60次的频率,(iPad Pro 是120Hz/秒),我们的runloop 理想状态下也会按照时钟周期 回调60次(iPad Pro 120次), 这么高频率的调用是为了 屏幕图像显示能够垂直同步 不卡顿.在异步线程的话是很难保证这个处理过程的同步更新. 即便能保证的话 相对主线程而言 系统资源开销 线程调度等等将会占据大部分资源和在同一个线程只专门干一件事有点得不偿失. ### 4.PerformSelector和runloop的关系 当调用NSObect的 performSelector:相关的时候,内部会创建一个timer定时器添加到当前线程的runloop中,如果当前线程没有启动runloop,则该方法不会被调用. 开发中遇到最多的问题就是这个performSelector: 导致对象的延迟释放,这里开发过程中注意一下,可以用单次的NSTimer替代. 详细可以参考[Runloop与performSelector](https://juejin.im/post/6844903781755256840) ### 5.如何使线程保活? 想要线程保活的话就开启该线程的runloop即可,注意:在NSThread执行的方法中添加while(true){},这样是模拟runloop的运行原理,结合GCD的信号量,在{}代码块中处理任务. 但是注意 开启runloop的方法要正确 如下代码 ``` objc //测试开启线程 - (void)memoryTest { for (int i = 0; i < 100000; ++i) { NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil]; [thread start]; [self performSelector:@selector(stopThread) onThread:thread withObject:nil waitUntilDone:YES]; } } //线程停止 - (void)stopThread { CFRunLoopStop(CFRunLoopGetCurrent()); NSThread *thread = [NSThread currentThread]; [thread cancel]; } //运行线程的runloop 注意 意添加的那个空port,否则会出现内存泄露 - (void)run { @autoreleasepool { NSLog(@"current thread = %@", [NSThread currentThread]); NSRunLoop *runLoop = [NSRunLoop currentRunLoop]; if (!self.emptyPort) { self.emptyPort = [NSMachPort port]; } [runLoop addPort:self.emptyPort forMode:NSDefaultRunLoopMode]; [runLoop runMode:NSRunLoopCommonModes beforeDate:[NSDate distantFuture]]; } } //下列代码用于模拟线程内部做的一些耗时任务 - (void)printSomething { NSLog(@"current thread = %@", [NSThread currentThread]); [self performSelector:@selector(printSomething) withObject:nil afterDelay:1]; } //模拟手动点击按钮 让 runloop停掉 - (void)stopButtonDidClicked:(id)sender { [self performSelector:@selector(stopRunloop) onThread:self.thread withObject:nil waitUntilDone:YES]; } - (void)stopRunloop { CFRunLoopStop(CFRunLoopGetCurrent()); } ``` 详细请参考:[iOS开发深入研究Runloop与线程保活](https://allluckly.cn/%E6%8A%95%E7%A8%BF/tuogao55) ## KVO 在开发过程中我们经常使用KVO,下面解答一下KVO相关的问题. ### KVO的实现原理 通过`runtime`派生子类的方式 复写相关需要KVO监听的属性,在该属性setter之前和之后调用NSObject的监听方法,这样KVO就实现了属性变换前后的回调. KVO派生的子类具体格式应该是:`NSKVONotifying_+类名`的类 eg: NSKVONotifying_Person 下面示例代码为Person类的name添加KVO的模拟实验 ``` objc - (void)setName:(NSString *)name{ _NSSetObjectValueAndNotify(); } void _NSSetObjectValueAndNotify { [self willChangeValueForKey:@"name"]; [super setName:name]; [self didChangeValueForKey:@"name"]; } - (void)didChangeValueForKey:(NSString *)key{ [observe observeValueForKeyPath:key ofObject:self change:nil context:nil]; } ``` 问题来了如何动态创建类呢? ``` objc //动态创建XXCustomClass Class customClass = objc_allocateClassPair([NSObject class], "XXCustomClass", 0); // 添加实例变量 class_addIvar(customClass, "age", sizeof(int), 0, "i"); // 动态添加方法 class_addMethod(customClass, @selector(hahahha), (IMP)hahahha, "V@:"); //需要实现的方法 void hahahha(id self, SEL _cmd) { NSLog(@"hahahha===="); } - (void)hahahha{ } //最后注册到运行时环境 objc_registerClassPair(customClass); ``` > [V@:表示方法的参数和返回值](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html#//apple_ref/doc/uid/TP40008048-CH100-SW1) 具体原理以及自定义实现KVO可以参考[KVO详解及底层实现](https://cloud.tencent.com/developer/article/1136759) ### 如何手动关闭KVO? 被观察的对象复写如下方法 返回`NO`即可关闭KVO ``` objc + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key { return NO; } ``` 如果关闭后还想触发 KVO的话 修改需要手动调用在变量setter的前后 主动调用 `willChangeValueForKey:`和`didChangeValueForKey:` ### 通过KVC修改属性会触发KVO么? 会的 ### 哪些情况下使用kvo会崩溃,怎么防护崩溃? 使用不当 会crash,比如: \- 添加和移出不是成对出现且存在多线程添加KVO的情况,经常遇到的crash是移出 \- 内存dealloc的时候 或者对象销毁前没有正确移出Observer 如何防护? 1.注意移出对象 匹配 2.内存野指针问题,一定要在对象销毁前移出观察者 3.可以使用第三方库BlockKit添加KVO,blockkit内部会自动移除Observer避免crash. ### KVO的优缺点 优点: \- 方便两个对象间同步状态(keypath)更加方便,一般都是在A类要观察B类的属性的变化. \- 非侵入式的得到某内部对象的状态改变并作出响应.(就是在不改变原来对象类的代码情况下即可做出对该对象的状态变化进行监听) \- 可以嵌入更改前后的两个时机的状态. \- 可以通过Keypaths对嵌套对象的监听. 缺点: \- 需要手动移除观察者,不移除容易造成crash. \- 注册和移出成对匹配出现. \- keypath参数的类型String, 如果对象的成员变量被重构而变化字符串不会被编译器识别而报错. \- 实现观察的方式是复写NSObjec的相关KVO的方法,应该更加面向protocol的方式会更好. ## 总结 这一篇我们讲了 runloop和KVO相关的内容,这里面最负责的当属runloop如何处理触摸手势事件.建议认真研读相关链接文章.这样才有一个对runloop更深刻的理解, 下一篇我们讲一下Block,敬请期待. URL: https://sunyazhou.com/2020/09/iOSinterviewAnswers4/index.html.md Published At: 2020-09-01 10:15:27 +0000 # 阿里、字节:一套高效的iOS面试题之NSNotification相关 ![](/assets/images/20200721iOSinterviewAnswers/iOSInterviewQuestionsAlbumCover.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! 前3篇中已经讲完了内存管理,今天我们继续完成[阿里、字节:一套高效的iOS面试题](https://juejin.im/post/6844904064937902094)的通知部分. 主要内容包含如下: * 实现原理(结构设计、通知如何存储的、name&observer&SEL之间的关系等) * 通知的发送时同步的,还是异步的 * NSNotificationCenter接受消息和发送消息是在一个线程里吗?如何异步发送消息 * NSNotificationQueue是异步还是同步发送?在哪个线程响应 * NSNotificationQueue和runloop的关系 * 如何保证通知接收的线程在主线程 * 页面销毁时不移除通知会崩溃吗 * 多次添加同一个通知会是什么结果?多次移除通知呢 * 下面的方式能接收到通知吗?为什么 ``` objc // 发送通知 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotification:) name:@"TestNotification" object:@1]; // 接收通知 [NSNotificationCenter.defaultCenter postNotificationName:@"TestNotification" object:nil]; ``` 在解释这些内容之前 强烈建议认真研读一下这篇 [一文全解iOS通知机制(经典收藏)](https://juejin.im/post/6844904082516213768)文章 了解一下大概 所有的问题就迎刃而解了. ## 实现原理(结构设计、通知如何存储的、name&observer&SEL之间的关系等 首先通知中心结构大概分为如下几个类 * `NSNotification` 通知的模型 name、object、userinfo. * `NSNotificationCenter`通知中心 负责发送`NSNotification` * `NSNotificationQueue`通知队列 负责在某些时机触发 调用`NSNotificationCenter`通知中心 `post`通知 通知是结构体通过双向链表进行数据存储 ``` objc // 根容器,NSNotificationCenter持有 typedef struct NCTbl { Observation *wildcard; /* 链表结构,保存既没有name也没有object的通知 */ GSIMapTable nameless; /* 存储没有name但是有object的通知 */ GSIMapTable named; /* 存储带有name的通知,不管有没有object */ ... } NCTable; // Observation 存储观察者和响应结构体,基本的存储单元 typedef struct Obs { id observer; /* 观察者,接收通知的对象 */ SEL selector; /* 响应方法 */ struct Obs *next; /* Next item in linked list. */ ... } Observation; ``` 主要是以`key` `value`的形式存储,这里需要重点强调一下 通知以 `name`和`object`两个纬度来存储相关通知内容,也就是我们添加通知的时候传入的两个不同的方法. ![](/assets/images/20200901iOSinterviewAnswers/NCTable.webp) ![](/assets/images/20200901iOSinterviewAnswers/NCTable2.webp) 简单理解`name`&`observer`&`SEL`之间的关系就是`name`作为`key`, `observer`作为观察者对象,当合适时机触发就会调用`observer`的`SEL`.这基本很简单,如果觉得我说的不准确可以看下文章开头的文章. ## 通知的发送时同步的,还是异步的 同步发送.因为要调用消息转发.所谓异步,指的是**非实时发送**而是**在合适的时机发送**,并没有开启异步线程. ## NSNotificationCenter接受消息和发送消息是在一个线程里吗?如何异步发送消息 是的, 异步线程发送通知则响应函数也是在异步线程. 异步发送通知可以开启异步线程发送即可. ## NSNotificationQueue是异步还是同步发送?在哪个线程响应 ``` objc // 表示通知的发送时机 typedef NS_ENUM(NSUInteger, NSPostingStyle) { NSPostWhenIdle = 1, // runloop空闲时发送通知 NSPostASAP = 2, // 尽快发送,这种时机是穿插在每次事件完成期间来做的 NSPostNow = 3 // 立刻发送或者合并通知完成之后发送 }; ``` | | NSPostWhenIdle | NSPostASAP | NSPostNow | | ------| ------ | ------ | ------ | | NSPostingStyle | 异步发送 | 异步发送 | 同步发送 | `NSNotificationCenter`都是同步发送的,而这里介绍关于`NSNotificationQueue`的异步发送,从线程的角度看并不是真正的异步发送,或可称为**延时发送**,它是利用了`runloop`的时机来触发的. 异步线程发送通知则响应函数也是在异步线程,主线程发送则在主线程. ## NSNotificationQueue和runloop的关系 `NSNotificationQueue`依赖`runloop`. 因为通知队列要在runloop回调的某个时机调用通知中心发送通知.从下面的枚举值就能看出来 ``` objc // 表示通知的发送时机 typedef NS_ENUM(NSUInteger, NSPostingStyle) { NSPostWhenIdle = 1, // runloop空闲时发送通知 NSPostASAP = 2, // 尽快发送,这种时机是穿插在每次事件完成期间来做的 NSPostNow = 3 // 立刻发送或者合并通知完成之后发送 }; ``` ## 如何保证通知接收的线程在主线程 如果想在主线程响应异步通知的话可以用如下两种方式 1.系统接受通知的API指定队列 ``` objc - (id )addObserverForName:(nullable NSNotificationName)name object:(nullable id)obj queue:(nullable NSOperationQueue *)queue usingBlock:(void (^)(NSNotification *note))block ``` 2.`NSMachPort`的方式 通过在主线程的runloop中添加machPort,设置这个port的delegate,通过这个Port其他线程可以跟主线程通信,在这个port的代理回调中执行的代码肯定在主线程中运行,所以,在这里调用NSNotificationCenter发送通知即可 ## 页面销毁时不移除通知会崩溃吗? iOS9.0之前,会crash,原因:通知中心对观察者的引用是unsafe_unretained,导致当观察者释放的时候,观察者的指针值并不为nil,出现野指针. iOS9.0之后,不会crash,原因:通知中心对观察者的引用是weak。 ## 多次添加同一个通知会是什么结果?多次移除通知呢 多次添加同一个通知,会导致发送一次这个通知的时候,响应多次通知回调。 多次移除通知不会产生crash。 ## 下面的方式能接收到通知吗?为什么 ``` objc // 发送通知 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotification:) name:@"TestNotification" object:@1]; // 接收通知 [NSNotificationCenter.defaultCenter postNotificationName:@"TestNotification" object:nil]; ``` 不能 首先我们看下通知中心存储通知观察者的结构 ``` objc // 根容器,NSNotificationCenter持有 typedef struct NCTbl { Observation *wildcard; /* 链表结构,保存既没有name也没有object的通知 */ GSIMapTable nameless; /* 存储没有name但是有object的通知 */ GSIMapTable named; /* 存储带有name的通知,不管有没有object */ ... } NCTable; // Observation 存储观察者和响应结构体,基本的存储单元 typedef struct Obs { id observer; /* 观察者,接收通知的对象 */ SEL selector; /* 响应方法 */ struct Obs *next; /* Next item in linked list. */ ... } Observation; ``` `nameless`与`named`的具体数据结构如下: ![](/assets/images/20200901iOSinterviewAnswers/NCTable.webp) ![](/assets/images/20200901iOSinterviewAnswers/NCTable2.webp) 当添加通知监听的时候,我们传入了`name`和`object`,所以,观察者的存储链表是这样的: `named`表:`key(name)` : `value`->`key(object)` : `value(Observation)` 因此在发送通知的时候,如果只传入`name`而并没有传入`object`,是找不到`Observation`的,也就不能执行观察者回调. # 总结 经过今天的 复习又重新认识了iOS中的通知中心,希望大家经常温故而知新. 下一篇我们开始讲解 `Runloop`& `KVO` URL: https://sunyazhou.com/2020/08/iOSinterviewAnswers3/index.html.md Published At: 2020-08-31 16:52:25 +0000 # 阿里、字节:一套高效的iOS面试题之runtime相关问题3 ![](/assets/images/20200721iOSinterviewAnswers/iOSInterviewQuestionsAlbumCover.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! # runtime相关问题之内存部分的关联属性或者hook相关的Method Swizzle 经过前两期内容 我们这期来讲一下 内存部分的剩余问题 主要包含如下: 1. `Method Swizzle`注意事项 2. 属性修饰符atomic的内部实现是怎么样的?能保证线程安全吗 3. iOS 中内省的几个方法有哪些?内部实现原理是什么 4. `class`、`objc_getClass`、`object_getclass` 方法有什么区别? ## `Method Swizzle`注意事项 1. **需要注意的是交换方法实现后的副作用**, `method_exchangeImplementations()`.交换方法函数最终会以`objc_msgSend()`方式调用,副作用主要集中在第一个参数 如下示例 ``` objc objc_msgSend(payment, @selector(quantity)) ``` 方法交换后再去调用quantity方法将有可能会crash.解决这种副作用的方式是使用`method_setImplementation()`来替换原来的交换方式,这样才最为合理, 具体原理请参照 [Objc 黑科技 - Method Swizzle 的一些注意事项](https://www.ctolib.com/topics-103098.html) 2. **避免交换父类方法** 如果当前类没有实现被交换的方法且父类实现了,此时父类的实现会被交换,若此父类的多个继承者都在交换时会引起多次交换导致混乱,同时调用父类方法有可能因为找不到方法签名而crash. 所以交换前都应该check能否为当前类添加被交换的函数的新的实现IMP,这个过程大概分为3步骤 * `class_addMethod` check能否添加方法 ``` objc BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types) ``` > 给类cls的SEL添加一个实现IMP, 返回YES则表明类cls并未实现此方法,返回NO则表明类已实现了此方法。注意:添加成功与否,完全由该类本身来决定,与父类有无该方法无关。 * `class_replaceMethod` 替换类cls的SEL的函数实现为imp ``` objc class_replaceMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types) ``` * `method_exchangeImplementations` 最终方法交换 ``` objc method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2) ``` 3. 交换方法应在+load方法 这个前面讲消息转发的时候讲过,+load不是消息转发的方式实现的且在运行时初始化过程中类被加载的时候调用,而且父类,当前类,category,子类等 都会调用一次.所以这里最适合写方法交换的hook(Method Swizzle). 4. 交换的分类方法应该添加自定义前缀,避免冲突 这个毫无疑问,方法名称一样的时候会出现,分类的方法会覆盖类中同名的方法. [method swizzling你应该注意的点](https://blog.csdn.net/weixin_34168700/article/details/88762738) ## 属性修饰符atomic的内部实现是怎么样的?能保证线程安全吗? ### atomic内部实现 ``` objc id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) { ... id *slot = (id*) ((char*)self + offset); if (!atomic) return *slot; // Atomic retain release world spinlock_t& slotlock = PropertyLocks[slot]; slotlock.lock(); id value = objc_retain(*slot); slotlock.unlock(); return objc_autoreleaseReturnValue(value); } ``` ``` objc static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy) { ... if (!atomic) { oldValue = *slot; *slot = newValue; } else { spinlock_t& slotlock = PropertyLocks[slot]; slotlock.lock(); oldValue = *slot; *slot = newValue; slotlock.unlock(); } objc_release(oldValue); } ``` `property` 的 `atomic` 是采用 `spinlock_t`自旋锁实现的. ### 能保证线程安全吗? `atomic`通过这种方法.在运行时仅仅是保证了`set`,`get`方法的原子性.所以使用atomic并不能保证线程安全。 ## iOS 中内省的几个方法有哪些?内部实现原理是什么? 首先要明白一个名词 `introspection` 反省,内省的意思,在iOS开发中我们会称它为反射. 内省方法 例如常用的`NSObject`中的`isKindOfClass:` 通过实例对象判断`class`这就是一种内省方法或者叫反射方法,但我认为`NSClassFromString()`这个应该也算一种反射方法. ### iOS 中内省的几个方法 我们从NSObject.h中看下吧 ``` objc - (BOOL)isKindOfClass:(Class)aClass; //判断是否是这个类或者这个类的子类的实例 - (BOOL)isMemberOfClass:(Class)aClass; //判断是否是这个类的实例 - (BOOL)conformsToProtocol:(Protocol *)aProtocol; //判断是否遵守某个协议 + (BOOL)conformsToProtocol:(Protocol *)protocol; //判断某个类是否遵守某个协议 - (BOOL)respondsToSelector:(SEL)aSelector; //判读实例是否有这样方法 + (BOOL)instancesRespondToSelector:(SEL)aSelector; //判断类是否有这个方法 ... ``` ### 内部实现原理 1.`isKindOfClass:` ``` objc + (BOOL)isKindOfClass:(Class)cls { for (Class tcls = self->ISA(); tcls; tcls = tcls->superclass) { if (tcls == cls) return YES; } return NO; } - (BOOL)isKindOfClass:(Class)cls { for (Class tcls = [self class]; tcls; tcls = tcls->superclass) { if (tcls == cls) return YES; } return NO; } ``` 类方法是通过ISA()函数拿到指向元类的存储isa指针数据的地址bit位按位与上相关掩码的方式判断当前是否是某个类的子类. 实例方法是通过`objc_object::getIsa()`函数通过存储的`tag_ext`表形式拿到isa对于的class来取出class平check来实现的. 2.`isMemberOfClass:` ``` objc + (BOOL)isMemberOfClass:(Class)cls { return self->ISA() == cls; } - (BOOL)isMemberOfClass:(Class)cls { return [self class] == cls; } ``` 这俩方法非常简单直接 拿到isa指针对比 3.`conformsToProtocol:` ``` objc + (BOOL)conformsToProtocol:(Protocol *)protocol { if (!protocol) return NO; for (Class tcls = self; tcls; tcls = tcls->superclass) { if (class_conformsToProtocol(tcls, protocol)) return YES; } return NO; } - (BOOL)conformsToProtocol:(Protocol *)protocol { if (!protocol) return NO; for (Class tcls = [self class]; tcls; tcls = tcls->superclass) { if (class_conformsToProtocol(tcls, protocol)) return YES; } return NO; } ``` 两个方法最终还是去isa->data()->protocols 拿到相关协议然后判断是否存在相关协议 如下代码: ``` objc BOOL class_conformsToProtocol(Class cls, Protocol *proto_gen) { protocol_t *proto = newprotocol(proto_gen); if (!cls) return NO; if (!proto_gen) return NO; mutex_locker_t lock(runtimeLock); checkIsKnownClass(cls); ASSERT(cls->isRealized()) for (const auto& proto_ref : cls->data()->protocols) { protocol_t *p = remapProtocol(proto_ref); if (p == proto || protocol_conformsToProtocol_nolock(p, proto)) { return YES; } } return NO; } ``` > 这里可以清晰的看到for循环 取出相关protocol指针 然后通过指针和传入的参数生成的`proto`对比 4.`respondsToSelector:` ``` objc + (BOOL)respondsToSelector:(SEL)sel { return class_respondsToSelector_inst(self, sel, self->ISA()); } - (BOOL)respondsToSelector:(SEL)sel { return class_respondsToSelector_inst(self, sel, [self class]); } ``` 这个源码比较麻烦 我简单叙述一下吧 实际上调用栈比较深就是一直寻找到当前实例能响应哪些方法,当前类没有就去父类,父类没有则直到元类. ``` sh respondsToSelector: |__ class_respondsToSelector_inst() |__ lookUpImpOrNil() |__ lookUpImpOrForward() 返回IMP结果 ``` 这就是整个消息转发的过程 就不在这里赘述了.感兴趣回看一下[第二章](https://www.sunyazhou.com/2020/08/08/20200808iOSinterviewAnswers/) 消息转发部分 我上述列举了一些常用的内省方法,其它的都方法基本没什么特别之处都是拿到isa各种操作内部的获取相关属性的函数返回结. ## `class`、`objc_getClass`、`object_getclass` 方法有什么区别? 我用xcode随便建了一个demo 打印一下viewcontrooller的内容 ``` objc @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; Class cls1 = [self class]; Class cls2 = object_getClass(cls1); Class cls3 = objc_getClass(object_getClassName([self class])); NSLog(@"%p",cls1); NSLog(@"%p",cls2); NSLog(@"%p",cls3); } @end ``` 输出 ``` sh 2020-08-31 16:15:48.150285+0800 ClassDemo[5582:55836] 0x10205b3b0 2020-08-31 16:15:48.150456+0800 ClassDemo[5582:55836] 0x10205b3d8 2020-08-31 16:15:48.150575+0800 ClassDemo[5582:55836] 0x10205b3b0 ``` 我简单列举了一张表格 | | `class` | `object_getclass()` | `objc_getClass()` | | :-----: | :-----: | :-----: | :-----: | | 传入参数 | N/a | id类型 | 类名的字符串 | | 操作对象 | obj | 这个id的isa指针所指向的Class |这个类的类对象 | | 实例对象时 | 和`object_getclass()`一致 |和`class`一致 | N/a | | 类对象/元类对象时 | 返回的消息对象本身 | 返回的是下一个对象 | N/a | > 原因:因为class返回的是self,而object_getClass返回的是isa指向的对象 # 总结 以上就是"一套高效的iOS面试题之我整理的答案之runtime相关问题3"中的内存剩余部分,问题答案虽然简短 但是每道题都问的非常到位,下一期我们讲一下 通知部分 争取用最快时间内把所有问题都整理出来答案. [参考](https://www.codenong.com/cs106358283/) URL: https://sunyazhou.com/2020/08/iOSinterviewAnswers2/index.html.md Published At: 2020-08-08 14:54:07 +0000 # 阿里、字节:一套高效的iOS面试题之runtime相关问题2 ![](/assets/images/20200721iOSinterviewAnswers/iOSInterviewQuestionsAlbumCover.webp) # 前言 > 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! 本篇我们来讲一下 [阿里、字节:一套高效的iOS面试题](https://mp.weixin.qq.com/s/bDnsaD__ZpdHIk3_So382w) 中的runtime相关问题部分的内存管理相关的内容. # runtime相关问题之 内存管理 基本内容包括: * weak的实现原理?SideTable的结构是什么样的 * 关联对象的应用?系统如何实现关联对象的 * 关联对象的如何进行内存管理的?关联对象如何实现weak属性 * Autoreleasepool的原理?所使用的的数据结构是什么 * ARC的实现原理?ARC下对retain, release做了哪些优化 * ARC下哪些情况会造成内存泄漏 ## weak的实现原理?SideTable的结构是什么样的 先说结论: * `weak表`其实是一个hash(哈西)表.`Key`是所指对象的地址,`Value`是`weak`指针的地址数组.实现原理是通过新旧表的更新指针方式,对weak对象单独存储于`SideTable`中的`weak_table_t`(类型) `weak_table`表中,通过函数`objc_initWeak()`->`storeWeak()`函数中的新旧`SideTable`(结构体)表来实现 * `SideTable`是一个结构体,内部主要有引用计数表和弱引用表两个成员,内存存储的其实都是对象的地址和引用计数和weak变量的地址,而不是对象本身的数据,它的结构如下 ``` objc struct SideTable { spinlock_t slock; RefcountMap refcnts; weak_table_t weak_table; SideTable() { memset(&weak_table, 0, sizeof(weak_table)); } ~SideTable() { _objc_fatal("Do not delete SideTable."); } void lock() { slock.lock(); } void unlock() { slock.unlock(); } void forceReset() { slock.forceReset(); } // Address-ordered lock discipline for a pair of side tables. template static void lockTwo(SideTable *lock1, SideTable *lock2); template static void unlockTwo(SideTable *lock1, SideTable *lock2); }; ``` ### weak实现原理 实现原理概括分为3个时机 * 1.初始化 * 2.添加引用 * 3.释放 #### 1.初始化时候 `runtime`会调用`objc_initWeak`函数,初始化一个新的`weak`指针指向对象的地址. 我们引入一段测试代码 ``` objc NSObject *obj = [[NSObject alloc] init]; id __weak obj1 = obj; ``` 当我们初始化一个weak变量时,`runtime`会调用`NSObject.mm`中的`objc_initWeak()`函数。这个函数在Clang中的声明如下: ``` objc id objc_initWeak(id *location, id newObj) { if (!newObj) { // 查看对象实例是否有效 无效对象直接导致指针释放 *location = nil; return nil; } // 这里传递了三个 bool 数值 old, new, crash.使用 template 进行常量参数传递是为了优化性能 return storeWeak (location, (objc_object*)newObj); } ``` 可以看出,这个函数仅仅是一个深层函数的调用入口,而一般的入口函数中,都会做一些简单的判断(例如 `objc_msgSend` 中的缓存判断),这里判断了其指针指向的类对象是否有效,无效直接释放,不再往深层调用函数。否则,object将被注册为一个指向value的`__weak`对象。而这事应该是`objc_storeWeak`函数干的. > 注意: `objc_initWeak`函数有一个前提条件:就是object必须是一个没有被注册为`__weak`对象的有效指针。而value则可以是null,或者指向一个有效的对象. #### 2.添加引用时 `objc_initWeak`函数会调用 `objc_storeWeak() `函数,`objc_storeWeak() `则会调用`storeWeak()`函数, `storeWeak()`的作用是更新指针指向,创建对应的弱引用表 模板 ``` c // HaveOld: true - 变量有值 ,false - 需要被及时清理,当前值可能为 nil // HaveNew: true - 需要被分配的新值,当前值可能为nil, false - 不需要分配新值 // CrashIfDeallocating: true - 说明 newObj 已经释放或者 newObj 不支持弱引用,该过程需要暂停,false - 用 nil 替代存储 template ``` weak实现函数 **该过程用来更新弱引用指针的指向**. ``` objc static id storeWeak(id *location, objc_object *newObj) { ASSERT(haveOld || haveNew); if (!haveNew) ASSERT(newObj == nil); // 初始化 previouslyInitializedClass 指针. Class previouslyInitializedClass = nil; id oldObj; // 声明两个 SideTable,① 新旧散列创建 SideTable *oldTable; SideTable *newTable; //获得新值和旧值的锁存位置(用地址作为唯一标示),通过地址来建立索引标志,防止桶重复,下面指向的操作会改变旧值. if (haveOld) { oldObj = *location;// 更改指针,获得以 oldObj 为索引所存储的值地址 oldTable = &SideTables()[oldObj]; } else { oldTable = nil; } if (haveNew) { newTable = &SideTables()[newObj];// 更改新值指针,获得以 newObj 为索引所存储的值地址 } else { newTable = nil; } // 加锁操作,防止多线程中竞争冲突 SideTable::lockTwo(oldTable, newTable); // 避免线程冲突重处理,location 应该与 oldObj 保持一致,如果不同,说明当前的 location 已经处理过 oldObj 可是又被其他线程所修改 if (haveOld && *location != oldObj) { SideTable::unlockTwo(oldTable, newTable); goto retry; } // 防止弱引用间死锁,并且通过 +initialize 初始化构造器保证所有弱引用的 isa 非空指向 if (haveNew && newObj) { Class cls = newObj->getIsa();// 获得新对象的 isa 指针 // 判断 isa 非空且已经初始化 if (cls != previouslyInitializedClass && !((objc_class *)cls)->isInitialized()) { SideTable::unlockTwo(oldTable, newTable);/ 解锁 class_initialize(cls, (id)newObj); //如果该类已经完成执行 +initialize 方法是最理想情况,如果该类 +initialize 在线程中,例如 +initialize 正在调用 storeWeak 方法,需要手动对其增加保护策略,并设置 previouslyInitializedClass 指针进行标记 previouslyInitializedClass = cls; goto retry; //重试 } } // ② 清除旧值 if (haveOld) { weak_unregister_no_lock(&oldTable->weak_table, oldObj, location); } // ③ 分配新值 if (haveNew) { newObj = (objc_object *) weak_register_no_lock(&newTable->weak_table, (id)newObj, location, crashIfDeallocating); //如果弱引用被释放 weak_register_no_lock 方法返回 nil,在引用计数表中设置若引用标记位 if (newObj && !newObj->isTaggedPointer()) { //弱引用位初始化操作,引用计数那张散列表的weak引用对象的引用计数中标识为weak引用 newObj->setWeaklyReferenced_nolock(); } //之前不要设置 location 对象,这里需要更改指针指向 *location = (id)newObj; } else { // 没有新值,则无需更改 } SideTable::unlockTwo(oldTable, newTable); return (id)newObj; } ``` ##### SideTable SideTable就是一个结构体,内部主要有引用计数表和弱引用表两个成员,内存存储的其实都是对象的地址和引用计数和weak变量的地址,而不是对象本身的数据. > 主要用于管理对象的引用计数和 weak 表. 我们来看图 ![](/assets/images/20200808iOSinterviewAnswers/SideTableStructure.webp) > 操作系统维护64个SideTable,通过对象的地址位置hash之后模64(就是%64求余数)找到指定的SideTable 每个SideTable维护了一个RefcountMap的引用计数表,key就是对象地址,value就是此对象的引用计数 ``` objc struct SideTable { spinlock_t slock; //保证原子操作的自旋锁 RefcountMap refcnts; //引用计数的 hash 表 weak_table_t weak_table; //weak 引用全局 hash 表 ... }; ``` * slock 防止竞争的自旋锁 * refcnts 协助对象的 isa 指针的`extra_rc`共同引用计数的变量 ##### weak表 弱引用hash表,`weak_table_t`类型的结构体,存储某个实例对象相关的所有弱引用信息. 定义如下: ``` objc struct weak_table_t { weak_entry_t *weak_entries; // 保存了所有指向指定对象的 weak 指针 size_t num_entries; // 存储空间 uintptr_t mask; // 参与判断引用计数辅助量 uintptr_t max_hash_displacement; // hash key 最大偏移值 }; ``` 这是一个全局弱引用hash表。使用不定类型对象的地址作为`key`,用`weak_entry_t`类型结构体对象作为`value`,其中的`weak_entries` 成员,即为弱引用表入口. 其中`weak_entry_t`是存储在弱引用表中的一个内部结构体,它负责维护和存储指向一个对象的所有弱引用hash表。其定义如下: ``` objc typedef DisguisedPtr weak_referrer_t; struct weak_entry_t { DisguisedPtr referent; union { struct { weak_referrer_t *referrers; uintptr_t out_of_line_ness : 2; uintptr_t num_refs : PTR_MINUS_2; uintptr_t mask; uintptr_t max_hash_displacement; }; struct { // out_of_line_ness field is low bits of inline_referrers[1] weak_referrer_t inline_referrers[WEAK_INLINE_COUNT]; }; }; ... }; ``` 其中`DisguisedPtr`类型的`referent`变量是**对泛型对象的指针的封装**,通过这个`泛型类`来解决内存泄露的问题. 注释中有个很重要的`out_of_line`成员,它代表最低的有效位,当它为0的时候,`weak_referrer_t `成员将扩展为多行静态的`hask table`. 其中`weak_referrer_t ` 是一个二维`objc_object`的别名(typedef),通过一个二维指针地址偏移,用下标作hash的`key`,做成了一个弱引用的散列。 那么`weak_entry_t`中的各成员`out_of_line`、`num_refs`、`mask` 、`max_hash_displacement` 在有效位未生效的时候有什么作用? * `out_of_line`:最低有效位,也是标志位。当标志位 0 时,增加引用表指针纬度。 * `num_refs`: 引用数值。这里记录弱引用表中引用有效数字,因为弱引用表使用的是静态 hash 结构,所以需要使用变量来记录数目。 * `mask`:计数辅助量。 * `max_hash_displacement`:`hash`元素上限阀值。 > 其实 `out_of_line` 的值通常情况下是等于零的,所以弱引用表总是一个`objc_objective`指针二维数组。一维 `objc_objective`指针可构成一张弱引用散列表,通过第三纬度实现了多张散列表,并且表数量为 `WEAK_INLINE_COUNT`. 以上是weak表的实现原理. #### 3.释放 释放时,调用`clearDeallocating`函数。`clearDeallocating`函数首先根据对象地址获取所有`weak`指针地址的数组,然后遍历这个数组把其中的数据设为`nil`,最后把这个`entry`从`weak`表中删除,最后清理对象的记录. ##### 当weak引用指向的对象被释放时,又是如何去处理weak指针的呢?当释放对象时,其基本流程如下: * 1.调用`objc_release` * 2.因为对象的引用计数为0,所以执行`dealloc` * 3.在dealloc中,调用了`_objc_rootDealloc`函数 * 4.在`_objc_rootDealloc`中,调用了`object_dispose`函数 * 5.调用`objc_destructInstance` * 6.最后调用`objc_clear_deallocating` 重点看对象被释放时调用的`objc_clear_deallocating`函数。该函数实现如下: ``` objc void objc_clear_deallocating(id obj) { ASSERT(obj); if (obj->isTaggedPointer()) return; obj->clearDeallocating(); } ``` 调用了`clearDeallocating()`,点击源码进去追踪发现,它最终是使用了迭代器来取`weak`表的`value`,然后调用`weak_clear_no_lock()`查找对应`value`,将该`weak`指针置空. `weak_clear_no_lock()`函数的实现如下: ``` objc void weak_clear_no_lock(weak_table_t *weak_table, id referent_id) { objc_object *referent = (objc_object *)referent_id; weak_entry_t *entry = weak_entry_for_referent(weak_table, referent); if (entry == nil) { /// XXX shouldn't happen, but does with mismatched CF/objc //printf("XXX no entry for clear deallocating %p\n", referent); return; } // zero out references weak_referrer_t *referrers; size_t count; if (entry->out_of_line()) { referrers = entry->referrers; count = TABLE_SIZE(entry); } else { referrers = entry->inline_referrers; count = WEAK_INLINE_COUNT; } for (size_t i = 0; i < count; ++i) { objc_object **referrer = referrers[i]; if (referrer) { if (*referrer == referent) { *referrer = nil; } else if (*referrer) { _objc_inform("__weak variable at %p holds %p instead of %p. " "This is probably incorrect use of " "objc_storeWeak() and objc_loadWeak(). " "Break on objc_weak_error to debug.\n", referrer, (void*)*referrer, (void*)referent); objc_weak_error(); } } } weak_entry_remove(weak_table, entry); } ``` `objc_clear_deallocating()`该函数的动作如下: * 1.从weak表中获取废弃对象的地址为键值的记录 * 2.将包含在记录中的所有附有 weak修饰符变量的地址,赋值为nil * 3.将weak表中该记录删除 * 4.从引用计数表中删除废弃对象的地址为键值的记录 [参考](https://www.jianshu.com/p/13c4fb1cedea) ## 关联对象的应用?系统如何实现关联对象的 ### 关联对象的应用? 一般应用在`category`(分类)中为 当前类 添加关联属性,因为不能直接添加成员变量,但是可以通过runtime的方式间接实现添加成员变量的效果。 当我们在`category`中声明如下代码: ``` objc @interface ClassA : NSObject (Category) @property (nonatomic, strong) NSString *property; @end ``` 实际上`@property`这个objc标准库的内建关键字帮我们实现了 setter和 getter,但是在category中并不能帮我们声明成员变量 `property` 我们需要通过runtime提供的两个C函数的api间接实现 动态添加 成员变量`property`. * `objc_setAssociatedObject()` * `objc_getAssociatedObject()` ``` objc #import "ClassA+Category.h" #import @implementation ClassA (Category) - (NSString *) property { return objc_getAssociatedObject(self, _cmd); } - (void)setProperty:(NSString *)categoryProperty { objc_setAssociatedObject(self, @selector(property), categoryProperty, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } @end ``` 看到上面的关联方法,我们来仔细研究一下下面经常使用的关联属性相关的API ``` c void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy); id objc_getAssociatedObject(id object, const void *key); void objc_removeAssociatedObjects(id object); ``` 1. `objc_setAssociatedObject()`以键值对形式添加关联对象 2. `objc_getAssociatedObject()`根据 key 获取关联对象 3. `objc_removeAssociatedObjects()`移除所有关联对象 `objc_setAssociatedObject()`的调用栈 ``` objc void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) └── SetAssocHook.get()(object, key, value, policy) └── void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) ``` 上述调用栈中的`_object_set_associative_reference()`函数实际完成了设置关联对象的任务: ``` c++ void _object_set_associative_reference(id object, const void *key, id value, uintptr_t policy) { if (!object && !value) return; if (object->getIsa()->forbidsAssociatedObjects()) _objc_fatal("objc_setAssociatedObject called on instance (%p) of class %s which does not allow associated objects", object, object_getClassName(object)); DisguisedPtr disguised{(objc_object *)object}; ObjcAssociation association{policy, value}; association.acquireValue(); { AssociationsManager manager; AssociationsHashMap &associations(manager.get()); if (value) { auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{}); if (refs_result.second) { object->setHasAssociatedObjects(); } auto &refs = refs_result.first->second; auto result = refs.try_emplace(key, std::move(association)); if (!result.second) { association.swap(result.first->second); } } else { ... } } association.releaseHeldValue(); } ``` 省略的很多代码,上述代码中就是应用场景,上面调用的类`AssociationsManager`就是我们下面要讲的系统如何实现关联对象的原理. ### 系统如何实现关联对象的(关联对象实现原理) 实现关联对象技术的核心对象 有如下这么几个: 1. AssociationsManager 2. AssociationsHashMap 3. ObjectAssociationMap 4. ObjcAssociation > 其中Map同我们平时使用的字典类似。通过`key`-`value`的形式对应存值. 下面我们通过源码来一探究竟 #### `objc_setAssociatedObject()`函数 runtime源码 ``` sh void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) { _object_set_associative_reference(object, key, value, policy); } ``` > 源码调用过程有hook函数,有点长,这里我简化一下,直接调用核心的函数 下面看下`_object_set_associative_reference()`函数的代码实现 ``` objc void _object_set_associative_reference(id object, const void *key, id value, uintptr_t policy) { if (object->getIsa()->forbidsAssociatedObjects()) _objc_fatal("objc_setAssociatedObject called on instance (%p) of class %s which does not allow associated objects", object, object_getClassName(object)); DisguisedPtr disguised{(objc_object *)object}; ObjcAssociation association{policy, value}; //4. 我们用到的ObjcAssociation association.acquireValue(); { AssociationsManager manager; //1. 我们用到的AssociationsManager AssociationsHashMap &associations(manager.get()); //2.我们上面列举的AssociationsHashMap if (value) { auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{}); //3.我们用到的ObjectAssociationMap if (refs_result.second) { object->setHasAssociatedObjects(); } auto &refs = refs_result.first->second; auto result = refs.try_emplace(key, std::move(association)); if (!result.second) { association.swap(result.first->second); } } else { auto refs_it = associations.find(disguised); if (refs_it != associations.end()) { auto &refs = refs_it->second; auto it = refs.find(key); if (it != refs.end()) { association.swap(it->second); refs.erase(it); if (refs.size() == 0) { associations.erase(refs_it); } } } } } association.releaseHeldValue(); } ``` 上述代码可以找到我们实现关联对象技术的核心对象. 下面我们分别介绍一下几个核心对象的内部实现. ##### AssociationsManager ``` objc typedef DenseMap ObjectAssociationMap; typedef DenseMap, ObjectAssociationMap> AssociationsHashMap; class AssociationsManager { using Storage = ExplicitInitDenseMap, ObjectAssociationMap>; static Storage _mapStorage; public: AssociationsManager() { AssociationsManagerLock.lock(); } ~AssociationsManager() { AssociationsManagerLock.unlock(); } AssociationsHashMap &get() { return _mapStorage.get(); } static void init() { _mapStorage.init(); } }; ``` `AssociationsManager`内部有一个`get()`函数返回一个`AssociationsHashMap`对象 ##### AssociationsHashMap `AssociationsHashMap` 是`DenseMap`的typedef(可以理解为别名) 只不过它被定义成符合某些`元组`的条件的`DenseMap`类型 实际上 `AssociationsHashMap` 用与保存从对象的 `disguised_ptr_t `到 `ObjectAssociationMap`的映射,这个数据结构保存了当前对象对应的所有关联对象 ``` objc typedef DenseMap ObjectAssociationMap; typedef DenseMap, ObjectAssociationMap> AssociationsHashMap; ``` 这里的`ObjectAssociationMap `是另一类型的typedef,里面存着`ObjcAssociation`类型的对象指针的key,value形式. 下面再看下 `ObjcAssociation` ,这是一个C++的类对象,最关键的`ObjcAssociation`包含了`policy`以及`value`. ``` c++ class ObjcAssociation { uintptr_t _policy; id _value; public: ObjcAssociation(uintptr_t policy, id value) : _policy(policy), _value(value) {} ObjcAssociation() : _policy(0), _value(nil) {} ObjcAssociation(const ObjcAssociation &other) = default; ObjcAssociation &operator=(const ObjcAssociation &other) = default; ObjcAssociation(ObjcAssociation &&other) : ObjcAssociation() { swap(other); } inline void swap(ObjcAssociation &other) { std::swap(_policy, other._policy); std::swap(_value, other._value); } inline uintptr_t policy() const { return _policy; } inline id value() const { return _value; } ... }; ``` ##### 关联对象在内存中以什么形式存储的? 示例代码 ``` objc int main(int argc, const char * argv[]) { @autoreleasepool { NSObject *obj = [NSObject new]; objc_setAssociatedObject(obj, @selector(hello), @"Hello", OBJC_ASSOCIATION_RETAIN_NONATOMIC); } return 0; } ``` 这个调用函数`objc_setAssociatedObject(OBJC_ASSOCIATION_RETAIN_NONATOMIC, @"Hello")`在内存中是这样的存储结构 ![](/assets/images/20200808iOSinterviewAnswers/AssociationOrder.webp) ##### `objc_setAssociatedObject()` 我们回头来详细分解一下`objc_setAssociatedObject()`函数中的真实实现部分,`_object_set_associative_reference()` 这个函数需要传入`(id object, const void *key, id value, uintptr_t policy)`,这么几个参数,我们拿第3个`value`参数来分解. 我们分解为2步 1. `value != nil` 设置或者更新关联对象的值 2. `value == nil` 删除一个关联对象. 下面是具体是代码解释 **注意看代码注释!!!** ``` objc void _object_set_associative_reference(id object, const void *key, id value, uintptr_t policy) { // 判空 if (!object && !value) return; // 判断本类对象是否允许关联其他对象.如果允许则进入代码块 if (object->getIsa()->forbidsAssociatedObjects()) _objc_fatal("objc_setAssociatedObject called on instance (%p) of class %s which does not allow associated objects", object, object_getClassName(object)); // 将被关联的对象封装成DisguisedPtr方便在后边hash表中的管理,它的作用就像是一个指针 DisguisedPtr disguised{(objc_object *)object}; // 将需要关联的对象,封装成ObjcAssociation,方便管理 ObjcAssociation association{policy, value}; // 处理policy为retain和copy的修饰情况, association.acquireValue(); { // 获取关联对象管理者对象 AssociationsManager manager; // 根据管理者对象获取对应关联表(HashMap) AssociationsHashMap &associations(manager.get()); if (value) { // 如果这个disguised存在于ObjectAssociationMap()中,则替换,如果不存在则初始化后在插入 // 这里说明一下,我们关联的对象关系存在于ObjectAssociationMap中,而 // ObjectAssociationMap有多个,所以,这一步是对ObjectAssociationMap的一个管理,下边才是对我们要关联的对象的操作 auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{}); // 如果这是此对象第一次被关联 if (refs_result.second) { // 修改isa_t中的has_assoc字段,标记其被关联状态 object->setHasAssociatedObjects(); } // 这里才是对我们要关联的对象操作 auto &refs = refs_result.first->second; // 想map中插入key value对 auto result = refs.try_emplace(key, std::move(association)); // 这里没有看懂,为什么没有第二个就要交换一下.. if (!result.second) { association.swap(result.first->second); } } else { // value为空, 并且在associations中有记录,则进行擦除操作 auto refs_it = associations.find(disguised); if (refs_it != associations.end()) { auto &refs = refs_it->second; auto it = refs.find(key); if (it != refs.end()) { association.swap(it->second); refs.erase(it); if (refs.size() == 0) { associations.erase(refs_it); } } } } } // release the old value (outside of the lock). association.releaseHeldValue(); } ``` ##### `objc_setAssociatedObject()`函数的作用是什么? ``` c++ inline void objc_object::setHasAssociatedObjects() { if (isTaggedPointer()) return; retry: isa_t oldisa = LoadExclusive(&isa.bits); isa_t newisa = oldisa; if (!newisa.nonpointer || newisa.has_assoc) { ClearExclusive(&isa.bits); return; } newisa.has_assoc = true; if (!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)) goto retry; } ``` 它会将`isa`结构体中的标记位`has_assoc`标记为`true`,也就是表示当前对象有关联对象,如下图`isa`中的各个标记位都是干什么的. ![](/assets/images/20200808iOSinterviewAnswers/isa.webp) ##### `objc_getAssociatedObject()` 这个函数的调用栈如下 ``` sh id objc_getAssociatedObject(id object, const void *key) └── id _object_get_associative_reference(id object, const void *key); ``` 通过上面我们介绍,理解这个函数相当简单了 ``` objc id _object_get_associative_reference(id object, const void *key) { ObjcAssociation association{}; { AssociationsManager manager; //1 AssociationsHashMap &associations(manager.get()); //1 AssociationsHashMap::iterator i = associations.find((objc_object *)object); //2 if (i != associations.end()) { ObjectAssociationMap &refs = i->second; ObjectAssociationMap::iterator j = refs.find(key); if (j != refs.end()) { association = j->second; association.retainReturnedValue(); } } } return association.autoreleaseReturnedValue(); } ``` 1. 通过`AssociationsManager`拿到`AssociationsHashMap`哈西表 2. 通过哈西表寻找关联对象 3. 剩下的就是更新对象是否初次创建等标记 然后返回对象 ##### `objc_removeAssociatedObjects()` 调用栈如下: ``` sh void objc_removeAssociatedObjects(id object) └── void _object_remove_assocations(id object) ``` 代码具体实现 ``` objc void objc_removeAssociatedObjects(id object) { if (object && object->hasAssociatedObjects()) { _object_remove_assocations(object); } } ``` > check对象是否为nil 且 关联对象是否存在 然后调用实现跟上边的get差不多 ``` objc void _object_remove_assocations(id object) { ObjectAssociationMap refs{}; { AssociationsManager manager; AssociationsHashMap &associations(manager.get()); AssociationsHashMap::iterator i = associations.find((objc_object *)object); if (i != associations.end()) { refs.swap(i->second); associations.erase(i); } } // release everything (outside of the lock). for (auto &i: refs) { i.second.releaseHeldValue(); } } ``` 通过`AssociationsManager` -> `AssociationsHashMap` -> object 是否存在,如果存在就**擦除**.- > releaseHeldValue()是否对象 #### 小结 关联对象的应用和系统如何实现关联对象的大概顺序如下: `AssociationsManager`关联对象管理器->`AssociationsHashMap`哈希映射表->`ObjectAssociationMap`关联对象指针->`ObjcAssociation`关联对象 ## 关联对象的如何进行内存管理的?关联对象如何实现weak属性? ### 关联对象的如何进行内存管理的? 当我调用关联对象函数`objc_setAssociatedObject()`的时候会调用如下函数: `_object_set_associative_reference(id object, const void *key, id value, uintptr_t policy)`,这里面有个方法 ``` objc ObjcAssociation association{policy, value}; // retain the new value (if any) outside the lock. association.acquireValue(); ``` 这里的 `policy`就是具体绝对内存使用retain还是其它相关的内存枚举. ``` objc enum { OBJC_ASSOCIATION_SETTER_ASSIGN = 0, OBJC_ASSOCIATION_SETTER_RETAIN = 1, OBJC_ASSOCIATION_SETTER_COPY = 3, // NOTE: both bits are set, so we can simply test 1 bit in releaseValue below. OBJC_ASSOCIATION_GETTER_READ = (0 << 8), OBJC_ASSOCIATION_GETTER_RETAIN = (1 << 8), OBJC_ASSOCIATION_GETTER_AUTORELEASE = (2 << 8) }; ``` 通过 acquireValue()函数判断使用那种内存关键字. ``` objc inline void acquireValue() { if (_value) { switch (_policy & 0xFF) { case OBJC_ASSOCIATION_SETTER_RETAIN: _value = objc_retain(_value); break; case OBJC_ASSOCIATION_SETTER_COPY: _value = ((id(*)(id, SEL))objc_msgSend)(_value, @selector(copy)); break; } } } ``` ### 关联对象如何实现weak属性? 首先说一下 这个问题问的非常有技术含量,完全考验iOS开发者对底层了解的程度. 在为NSObject对象绑定 associated object 时可以指定如下依赖关系: ``` objc typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) { OBJC_ASSOCIATION_ASSIGN = 0, //弱引用 OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, //强引用,非原子操作 OBJC_ASSOCIATION_COPY_NONATOMIC = 3, //先 copy,然后强引用 OBJC_ASSOCIATION_RETAIN = 01401, //强引用,原子操作 OBJC_ASSOCIATION_COPY = 01403 //先 copy,然后强引用,原子操作 }; ``` 根据上述的枚举我们发现一个很奇怪的问题,这里的枚举中并没有`OBJC_ASSOCIATION_WEAK`这样的选项. 基于上述的代码介绍我们知道`Objective-C`在底层使用`AssociationsManager`统一管理各个对象的 `associated objects`关联对象.然后通过`static key`(一般是一个固定值)去访问对应的`associated object`关联对象.然后在`dealloc`的时候调用`擦除函数`(`associations.erase() `)来解除对这些关联对象的引用: ``` sh dealloc object_dispose objc_destructInstance _object_remove_assocations // 移除必要的associated objects ``` 也就是说,在`NSObject`对象的内存空间里,并没有为 `associated objects`(关联对象) 分配任何变量. 我们知道weak变量和 assign变量的区别是:weak指向的对象销毁的时候,`Objective-C`会自动帮我们设置`nil`,而`assign`却不能. 这个逻辑是如何实现的呢? `Runtime`在底层维护一个`weak`表(也就是本文开头讲的`SlideTable`中的`weak_table_t` `weak_tabl`),每每分配一个`weak`指针并赋值有效对象的地址时,会将对象地址和`weak`指针地址注册到`weak`表中,其中对象地址作为`key`;当对象被废弃时,可根据对象地址快速寻找到指向它的所有`weak` 指针,这些`weak`指针会被赋值`0`(即`nil`)并移出`weak表。 所以,实现`weak`引用(而非`assign`引用)的前提是存在一个`__weak`指针指向到被引用对象的地址,只有这样,当对象被销毁时,指针才能被`runtime`找到然后被设置为`nil`;`NSObject`对象和其`associated object`关联对象的关系,并不存在指针这样的**中间媒介**,因此只存在`OBJC_ASSOCIATION_ASSIGN`选项,而不存在`OBJC_ASSOCIATION_WEAK`选项. #### 那我们怎么解决为关联对象实现weak属性呢? 可以通过曲线救国的方式声明一个`class`类 持有一个weak的成员变量,然后通过 实例化 我们自定义的class的实例,然后把这个实例作为关联对象即可. 声明封装weak对象的类 ``` objc @interface WeakAssociatedObjectWrapper : NSObject @property (nonatomic, weak) id object; @end @implementation WeakAssociatedObjectWrapper @end ``` 调用 ``` objc @interface UIView (ViewController) @property (nonatomic, weak) UIViewController *vc; @end @implementation UIView (ViewController) - (void)setVc:(UIViewController *)vc { WeakAssociatedObjectWrapper *wrapper = [WeakAssociatedObjectWrapper new]; wrapper.object = vc; objc_setAssociatedObject(self, @selector(vc), wrapper, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (UIViewController *)vc { WeakAssociatedObjectWrapper *wrapper = objc_getAssociatedObject(self, _cmd); return wrapper.object; } @end ``` > 看明白没有,曲线救国.代码引入自[Weak Associated Object](https://zhangbuhuai.com/post/weak-associated-object.html) [关联对象参考](https://draveness.me/ao/) ## Autoreleasepool的原理?所使用的的数据结构是什么? 在ARC下我们使用`@autoreleasepool{}` 关键字 把需要自动管理的代码块圈起来 ,这个过程就是在使用一个`AutoReleasePool` ``` objc @autoreleasepool { <#statements#> //代码块 } ``` 以上代码编译器 最终会把它改写成下面的样子 ``` c void *context = objc_autoreleasePoolPush(); ``` 既然有压栈一定就有 出栈操作`objc_autoreleasePoolPop(context)`; * `objc_autoreleasePoolPush()` * `objc_autoreleasePoolPop()` 这俩函数都是对`AutoreleasePoolPage`的封装,自动释放机制的核心就是这个类 ### `AutoreleasePoolPage` `AutoreleasePoolPage`是个C++的类 ![](/assets/images/20200808iOSinterviewAnswers/autoreleasepoolpage.webp) * **AutoreleasePool**并没有单独的结构,而是由若干个`AutoreleasePoolPage`以`双向链表`的形式组合成的,根据上图可以看出,这个双向链表有`前驱parent `和`后继child `. * **AutoreleasePool**是按`线程`一一对应的(thread 成员变量) * **AutoreleasePoolPage**就是自动释放池存储对象的数据结构每个Page占用`4KB`内存,本身的成员变量占用`56`字节,剩下的空间用来存放调用了`autorelease`方法的对象地址,同时将一个哨兵插入到Page中,这个哨兵其实就是一个空地址 * 当一个page被占满以后会新建一个新的`AutoreleasePoolPage`对象,并插入哨兵标记.
 具体代码如下: ``` objc class AutoreleasePoolPage { # define EMPTY_POOL_PLACEHOLDER ((id*)1) # define POOL_BOUNDARY nil static pthread_key_t const key = AUTORELEASE_POOL_KEY; static uint8_t const SCRIBBLE = 0xA3; // 0xA3A3A3A3 after releasing static size_t const SIZE = #if PROTECT_AUTORELEASEPOOL PAGE_MAX_SIZE; // must be multiple of vm page size #else PAGE_MAX_SIZE; // size and alignment, power of 2 #endif static size_t const COUNT = SIZE / sizeof(id); magic_t const magic; id *next; pthread_t const thread; AutoreleasePoolPage * const parent; AutoreleasePoolPage *child; uint32_t const depth; uint32_t hiwat; }; ``` * `magic` 检查校验完整性的变量 * `next` 指向新加入的autorelease对象 * `thread` page当前所在的线程,AutoreleasePool是按线程一一对应的(结构中的thread指针指向当前线程) * `parent` 父节点 指向前一个page * `child` 子节点 指向下一个page * `depth` 链表的深度,节点个数 * `hiwat` high water mark 数据容纳的一个上限 * `EMPTY_POOL_PLACEHOLDER` 空池占位 * `POOL_BOUNDARY` 是一个边界对象 nil,之前的源代码变量名是 `POOL_SENTINEL`哨兵对象,用来区别每个page即每个 AutoreleasePoolPage 边界 * `PAGE_MAX_SIZE` = 4096, 为什么是4096呢?其实就是虚拟内存每个扇区4096个字节,4K对齐的说法。 * `COUNT` 一个page里对象数 下面看下工作机制图 ![](/assets/images/20200808iOSinterviewAnswers/autoreleasepoolworkflow.gif) > 这张图来自快手同事 周学运,如果大佬看到这张图的话希望能允许授权给我使用哈. 根据上面的示意图我们大概明白, `AutoreleasePoolPage`是以栈的形式存在,并且内部对象通过进栈出栈来对应着`objc_autoreleasePoolPush`和`objc_autoreleasePoolPop` 如果嵌套AutoreleasePool 就是通过`哨兵对象`来标识,每次更新链表的next和`前驱``后继`来完成表的创建销毁. ![](/assets/images/20200808iOSinterviewAnswers/autoreleasepoolpage1.webp) 当我们对一个对象发送一条`autorelease`消息的时候实际上就是将这个对象加入到当前`AutoreleasePoolPage`的栈顶`next`指针指向的位置 > 这里只拿了一张page举例. #### 小结 * 自动释放池是有N张`AutoreleasePoolPage`组成,每张page 4K大小, AutoreleasePoolPage是c++的类, AutoreleasePoolPage以双向链表连接起来形成一个自动释放池 * 当对象调用 autorelease 方法时,会将对象加入 AutoreleasePoolPage 的栈中 * pop 时是传入边界对象(哨兵对象),然后对page 中的对象发送release 的消息 [自动释放池原理](https://www.jianshu.com/p/0afda1f23782) [AutoreleasePool底层实现原理](https://juejin.im/post/6844903609428115470) ## ARC的实现原理?ARC下对retain, release做了哪些优化 ARC自动引用计数,是苹果objc4引入的编译器自动在适当位置 帮助实例对象进行 自动retain后者release的一套机制. 它的实现原理就是在编译层面插入相关代码,帮助补全MRC时代需要开发者手动填写的和管理的对象的相关内存操作的方法. 为了解释清楚具体实现原理 ,我找到一篇有代码示例的文章,从代码编译成汇编过程中 编译器做了很多优化工作. 更新`isa指针`的信息. [理解 ARC 实现原理](https://juejin.im/post/6844903847622606861#heading-4) 这里有个点需要跟大家说一下, 上文 中我们讲了SlideTable,但是还是有不懂得地方下面我们来通过isa串联起来 isa的组成 ``` c union isa_t { Class cls; uintptr_t bits; struct { uintptr_t nonpointer : 1;//->表示使用优化的isa指针 uintptr_t has_assoc : 1;//->是否包含关联对象 uintptr_t has_cxx_dtor : 1;//->是否设置了析构函数,如果没有,释放对象更快 uintptr_t shiftcls : 33; // MACH_VM_MAX_ADDRESS 0x1000000000 ->类的指针 uintptr_t magic : 6;//->固定值,用于判断是否完成初始化 uintptr_t weakly_referenced : 1;//->对象是否被弱引用 uintptr_t deallocating : 1;//->对象是否正在销毁 uintptr_t has_sidetable_rc : 1;//1->在extra_rc存储引用计数将要溢出的时候,借助Sidetable(散列表)存储引用计数,has_sidetable_rc设置成1 uintptr_t extra_rc : 19; //->存储引用计数 }; }; ``` 其中`nonpointer`、`weakly_referenced`、`has_sidetable_rc`和`extra_rc`都是 `ARC`有直接关系的成员变量,其他的大多也有涉及到。 ### retain,release做了哪些优化 大概可以分为如下 * TaggedPointer 指针优化 * !newisa.nonpointer:未优化的 isa 的情况下retain或者release * newisa.nonpointer:已优化的 isa , 这其中又分 extra_rc 溢出区别 我把相关代码站在下面并且把结论输出出来. | 内存操作 | objc_retain | objc_release | | :------:| :------: | :------: | | TaggedPointer | 值存在指针内,直接返回 | 直接返回 false。 | | !nonpointer | 未优化的`isa`,使用`sidetable_retain()` | 未优化的`isa`执行`sidetable_release` | | nonpointer| 已优化的`isa`,这其中又分`extra_rc`溢出和未溢出的两种情况 | 已优化的`isa`,分下溢和未下溢两种情况 | |nonpointer已优化isa的extra_rc | objc_retain | objc_release | | ------| ------ | ------ | | 未溢出时 |`isa.extra_rc`+1 | NA | |溢出时|将`isa.extra_rc`中一半值转移至`sidetable`中,然后将`isa.has_sidetable_rc`设置为`true`,表示使用了`sidetable`来计算引用次数|NA| |未下溢|NA|extra_rc--| |下溢|NA|从`sidetable`中借位给`extra_rc`达到半满,如果无法借位则说明引用计数归零需要进行释放,其中借位时可能保存失败会不断重试| > NA -> non available 不可获得 下面我们看下retain源码 ``` objc ALWAYS_INLINE id objc_object::rootRetain(bool tryRetain, bool handleOverflow) { if (isTaggedPointer()) return (id)this; // 如果是 TaggedPointer 直接返回 bool sideTableLocked = false; bool transcribeToSideTable = false; isa_t oldisa; isa_t newisa; do { transcribeToSideTable = false; oldisa = LoadExclusive(&isa.bits); // 获取 isa newisa = oldisa; if (slowpath(!newisa.nonpointer)) { ClearExclusive(&isa.bits);// 未优化的 isa 部分 if (!tryRetain && sideTableLocked) sidetable_unlock(); if (tryRetain) return sidetable_tryRetain() ? (id)this : nil; else return sidetable_retain(); } if (slowpath(tryRetain && newisa.deallocating)) { // 正在被释放的处理 ClearExclusive(&isa.bits); if (!tryRetain && sideTableLocked) sidetable_unlock(); return nil; } // extra_rc 未溢出时引用计数++ uintptr_t carry; newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry); // extra_rc++ // extra_rc 溢出 if (slowpath(carry)) { // newisa.extra_rc++ overflowed if (!handleOverflow) { ClearExclusive(&isa.bits); return rootRetain_overflow(tryRetain); // 重新调用该函数 入参 handleOverflow 为 true } // 保留一半引用计数,准备将另一半复制到 side table. if (!tryRetain && !sideTableLocked) sidetable_lock(); sideTableLocked = true; transcribeToSideTable = true; newisa.extra_rc = RC_HALF; newisa.has_sidetable_rc = true; } // 更新 isa 值 } while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits))); if (slowpath(transcribeToSideTable)) { sidetable_addExtraRC_nolock(RC_HALF); // 将另一半复制到 side table side table. } if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock(); return (id)this; } ``` `release`源码 ``` objc ALWAYS_INLINE bool objc_object::rootRelease(bool performDealloc, bool handleUnderflow) { if (isTaggedPointer()) return false; bool sideTableLocked = false; isa_t oldisa; isa_t newisa; retry: do { oldisa = LoadExclusive(&isa.bits); newisa = oldisa; if (slowpath(!newisa.nonpointer)) { ClearExclusive(&isa.bits);// 未优化 isa if (sideTableLocked) sidetable_unlock(); return sidetable_release(performDealloc);// 入参是否要执行 Dealloc 函数,如果为 true 则执行 SEL_dealloc } newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry); // extra_rc-- if (slowpath(carry)) { // donot ClearExclusive() goto underflow; } // 更新 isa 值 } while (slowpath(!StoreReleaseExclusive(&isa.bits, oldisa.bits, newisa.bits))); if (slowpath(sideTableLocked)) sidetable_unlock(); return false; underflow: // 处理下溢,从 side table 中借位或者释放 newisa = oldisa; if (slowpath(newisa.has_sidetable_rc)) { // 如果使用了 sidetable_rc if (!handleUnderflow) { ClearExclusive(&isa.bits);// 调用本函数处理下溢 return rootRelease_underflow(performDealloc); } size_t borrowed = sidetable_subExtraRC_nolock(RC_HALF); // 从 sidetable 中借位引用计数给 extra_rc if (borrowed > 0) { // extra_rc 是计算额外的引用计数,0 即表示被引用一次 newisa.extra_rc = borrowed - 1; // redo the original decrement too bool stored = StoreReleaseExclusive(&isa.bits, oldisa.bits, newisa.bits); // 保存失败,恢复现场,重试 if (!stored) { isa_t oldisa2 = LoadExclusive(&isa.bits); isa_t newisa2 = oldisa2; if (newisa2.nonpointer) { uintptr_t overflow; newisa2.bits = addc(newisa2.bits, RC_ONE * (borrowed-1), 0, &overflow); if (!overflow) { stored = StoreReleaseExclusive(&isa.bits, oldisa2.bits, newisa2.bits); } } } // 如果还是保存失败,则还回 side table if (!stored) { sidetable_addExtraRC_nolock(borrowed); goto retry; } sidetable_unlock(); return false; } else { // Side table is empty after all. Fall-through to the dealloc path. } } // 没有使用 sidetable_rc ,或者 sidetable_rc 计数 == 0 的就直接释放 // 如果已经是释放中,抛个过度释放错误 if (slowpath(newisa.deallocating)) { ClearExclusive(&isa.bits); if (sideTableLocked) sidetable_unlock(); return overrelease_error(); } // 更新 isa 状态 newisa.deallocating = true; if (!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)) goto retry; if (slowpath(sideTableLocked)) sidetable_unlock(); // 执行 SEL_dealloc 事件 __sync_synchronize(); if (performDealloc) { ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc); } return true; } ``` ### 小结 到这里可以知道 引用计数分别保存在`isa.extra_rc`和`sidetable`中,当`isa.extra_rc`溢出时,将一半计数转移至`sidetable`中,而当其下溢时,又会将计数转回。当二者都为空时,会执行释放流程 ## ARC下哪些情况会造成内存泄漏 * block中的循环引用 * NSTimer的循环引用 * addObserver的循环引用 * delegate的强引用 * 大次数循环内存爆涨 * 非OC对象的内存处理(需手动释放) # 总结 以上就是我们讨论上述一套面试题的 runtime相关问题之 内存管理部分,下一篇讲把剩余的问题收一下尾 感谢各位支持 URL: https://sunyazhou.com/2020/07/iOSinterviewAnswers1/index.html.md Published At: 2020-07-06 09:52:47 +0000 # 阿里、字节:一套高效的iOS面试题之runtime相关问题1 ![](/assets/images/20200721iOSinterviewAnswers/iOSInterviewQuestionsAlbumCover.webp) # 前言 > 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! 记得过年时候 [有一个微信公众号](https://mp.weixin.qq.com/s/bDnsaD__ZpdHIk3_So382w) 的面试题引起了我的关注,但是只有问题没有答案,由于最近半年时间太忙了,博客几乎停更了一个季度,所以今天我打算把这个面试题的答案 整理一下,方便后续iOS开发者需要时可时长关注.期间如果有解答不清楚或者不对之处还请各位指正. # 面试题的结构分类和细化 * runtime相关问题 1. runtime结构模型 2. 内存管理 3. 关联属性或者hook相关的Method Swizzle * NSNotification相关 1. 参考GNUStep源码 2. NSNotification实现原理 相关 * Runloop & KVO 1. runloop 2. KVO * Block 1. Block实现原理和注意事项相关 * 多线程 1. GCD相关和一些多线程概念 * 视图&图像相关 1. 视图UI布局方案 2. 视图渲染相关 * 性能优化 * 开发证书 * 架构设计 1. 各种设计模式 2. 自己的设计 * 其他问题 1. 方法调用和切面编程等 * 系统基础知识 * 数据结构与算法 ## runtime相关问题 [objc-runtime源码地址](https://github.com/RetVal/objc-runtime) [objc4官方源码地址](https://opensource.apple.com/tarballs/objc4/) ### 结构模型 #### 介绍下runtime的内存模型(isa、对象、类、metaclass、结构体的存储信息等) ##### 对象 OC中的对象指向的是一个`objc_object`指针类型,`typedef struct objc_object *id;`从它的结构体中可以看出,它包括一个isa指针,指向的是这个对象的类对象,一个对象实例就是通过这个isa找到它自己的Class,而这个Class中存储的就是这个实例的方法列表、属性列表、成员变量列表等相关信息的。 ``` objc /// Represents an instance of a class. struct objc_object { Class _Nonnull isa OBJC_ISA_AVAILABILITY; }; /// A pointer to an instance of a class. typedef struct objc_object *id; ``` 这个objc_object 的实现比较长 在这里[查看](https://github.com/RetVal/objc-runtime/blob/master/runtime/objc-private.h) #### 类 在OC中的类是用Class来表示的,实际上它指向的是一个`objc_class`的指针类型,`typedef struct objc_class *Class;` 对应的结构体如下: ``` objc struct objc_class { Class _Nonnull isa OBJC_ISA_AVAILABILITY; #if !__OBJC2__ Class _Nullable super_class OBJC2_UNAVAILABLE; const char * _Nonnull name OBJC2_UNAVAILABLE; long version OBJC2_UNAVAILABLE; long info OBJC2_UNAVAILABLE; long instance_size OBJC2_UNAVAILABLE; struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE; struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE; struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE; struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE; #endif } ``` ##### class和 object 小结 从结构体中定义的变量可知,OC的`Class`类型包括如下 数据(即:元数据`metadata`):`super_class`(父类类对象); name(类对象的名称); version、info(版本和相关信息); instance_size(实例内存大小); ivars(实例变量列表); methodLists(方法列表); cache(缓存); protocols(实现的协议列表); 当然也包括一个isa指针,这说明Class也是一个对象类型,所以我们称之为类对象, 这里的isa指向的是元类对象(metaclass),元类中保存了创建类对象(Class)的类方法的全部信息。 ![Objective-C的对象原型继承链](/assets/images/20200721iOSinterviewAnswers/class_inherit.webp) [Objective-C的对象原型继承链]() 从图中可知,最终的基类`NSObject`的元类对象`isa`指向的是自己本身,从而形成一个闭环。 元类(`Meta Class`):是一个类对象的类,即:Class的类,这里保存了类方法等相关信息。 我们再看一下类对象中存储的方法、属性、成员变量等信息的结构体 `objc_ivar_list`:存储了类的成员变量, 可以通过`object_getIvar`或`class_copyIvarList`获取; 另外这两个方法是用来获取类的属性列表的`class_getProperty`和`class_copyPropertyList`,属性和成员变量是有区别的。 ``` objc struct objc_ivar { char * _Nullable ivar_name OBJC2_UNAVAILABLE; char * _Nullable ivar_type OBJC2_UNAVAILABLE; int ivar_offset OBJC2_UNAVAILABLE; #ifdef __LP64__ int space OBJC2_UNAVAILABLE; #endif } OBJC2_UNAVAILABLE; struct objc_ivar_list { int ivar_count OBJC2_UNAVAILABLE; #ifdef __LP64__ int space OBJC2_UNAVAILABLE; #endif /* variable length structure */ struct objc_ivar ivar_list[1] OBJC2_UNAVAILABLE; } ``` `objc_method_list`:存储了类的方法列表,可以通过`class_copyMethodList`获取。 结构体如下: ``` objc struct objc_method { SEL _Nonnull method_name OBJC2_UNAVAILABLE; char * _Nullable method_types OBJC2_UNAVAILABLE; IMP _Nonnull method_imp OBJC2_UNAVAILABLE; } OBJC2_UNAVAILABLE; struct objc_method_list { struct objc_method_list * _Nullable obsolete OBJC2_UNAVAILABLE; int method_count OBJC2_UNAVAILABLE; #ifdef __LP64__ int space OBJC2_UNAVAILABLE; #endif /* variable length structure */ struct objc_method method_list[1] OBJC2_UNAVAILABLE; } ``` `objc_protocol_list`:储存了类的协议列表,可以通过`class_copyProtocolList`获取。 结构体如下: ``` objc struct objc_protocol_list { struct objc_protocol_list * _Nullable next; long count; __unsafe_unretained Protocol * _Nullable list[1]; }; ``` 此问题参考[介绍下runtime的内存模型(isa、对象、类、metaclass、结构体的存储信息等)](https://developer.aliyun.com/ask/282811) #### 为什么要设计metaclass? 先说结论: 为了更好的**复用传递消息**.metaclass只是需要**实现复用消息传递**为目的工具.而Objective-C所有的类默认都是同一个MetaClass(通过isa指针最终指向metaclass). 因为Objective-C的特性基本上是照搬的Smalltalk,Smalltalk中的MetaClass的设计是Smalltalk-80加入的.所以Objective-C也就有了metaclass的设计. > 本质上因为Smalltalk的面向对象的亮点是它的**消息发送机制**. 回答这个问题之前我们先回看一下上边的Objective-C的对象原型继承链![Objective-C的对象原型继承链](/assets/images/20200721iOSinterviewAnswers/class_inherit2.webp) 通过上图我们明白如下 重点内容: * **实例的实例方法函数存在类结构体中** * **类方法函数存在metaclass结构体中** 而Objective-C的方法调用(消息)就会根据对象去找isa指针指向的Class对象中的方法列表找到对应的方法。 > isa 指向的类就是我们创建实例的类型. 通过[Why is MetaClass in Objective-C?](https://www.jianshu.com/p/ea7c42e16da8)文章我们了解到一个十分重要的概念,python和**Objective-C不太一样的是,并不是每一个类都有一个MetaClass,而是Objective-C所有的类默认都是同一个MetaClass.** ##### Smalltalk中的metaclass Smalltalk,被公认为历史上第二个面向对象的语言,其亮点是它的**消息发送机制**。 Smalltalk中的MetaClass的设计是Smalltalk-80加入的。而之前的Smalltalk-76,并不是每个类有一个MetaClass,而是所有类的isa指针都指向一个特殊的类,叫做Class(这种设计之后也被Java借鉴了)。 而每个类都有自己MetaClass的设计,加入的原因是,因为Smalltalk里面,类是对象,而对象就可以响应消息,那么类的消息的响应的方法就应该由类的类去存储,而每个MetaClass就持有每个类的类方法。 ###### 每个MetaClass的isa指针指向什么? 如果MetaClass再有MetaClass,那么这个关系将无穷无尽。Smalltalk里的解决方案是,指向同一个叫MetaClass的类。 ###### MetaClass的isa指针指向什么? 指向他的实例,也就是实例的isa指向MetaClass,同时MetaClassisa指向实例,相互指着。 那么Smalltalk的继承关系,其实和Objective-C的很像了(后面有class的是前者的MetaClass)。 ![](/assets/images/20200721iOSinterviewAnswers/class_inherit2_smaltalk.webp) ###### 这时候产生了一个重要的问题,假如去掉MetaClass,把类方法放到也类里面是否可行? 这个问题,我思索许久,发现其实是一个对面向对象的哲学思想问题,要对这个问题下结论,不得不重新讲讲面向对象 ##### 从Smalltalk重新认识面向对象 以前谈到面向对象,总会提到,面向对象三特征:封装、继承、多态。但其实,面向对象中也分流派,如C++这种来自Simula的设计思想的,更注重的是类的划分,因为方法调用是静态的。而如Objective-C这种借鉴Smalltalk的,更注重的是消息传递,是动态响应消息。 而面向对象三种特征,更基于的是类的划分而提出的。 这两种思想最大的不同,我认为是自上而下和自下而上的思考方式。 * 类的划分,要求类的设计者是以一个很高的层次去设计这个类,提取出类的特性和本质,进行类的构建。知道类型才可以去发送消息给对象。 * 消息传递,要求的是类的设计者以消息为起点去构建类,也就是对外界的变化进行响应,而不关心自身的类型,设计接口。尝试理解消息,无法处理则进行特殊处理。 在此不讨论两种方式的优劣之分,而着重讲讲Smalltalk这种设计。 消息传递对于面向对象的设计,其实在于给出一种对消息的解决方案。而面向对象优点之一的复用,在这种设计里,更多在于复用解决方案,而不是单纯的类本身。这种思想就如设计组件一般,关心接口,关心组合而非类本身。其实之所以有MetaClass这种设计,我的理解并不是先有MetaClass,而是在万物都是对象的Smalltalk里,向对象发送消息的基本解决方案是统一的,希望复用的。而实例和类之间用的这一套通过isa指针指向的Class单例中存储方法列表和查询方法的解决方案的流程,是应该在类上复用的,而MetaClass就顺理成章出现罢了。 ##### 为什么要设计metaclass小结 ###### 回到一开始那个问题,为什么要设计MetaClass,去掉把类方法放到类里面行不行? 我的理解是,可以,但不Smalltalk。这样的设计是C++那种自上而下的设计方式,类方法也是类的一种特征描述。而Smalltalk的精髓正在于消息传递,复用消息传递才是根本目的,而MetaClass只不过是因此需要的一个工具罢了。 参考[Why is MetaClass in Objective-C?](https://www.jianshu.com/p/ea7c42e16da8) #### **class_copyIvarList()** & **class_copyPropertyList()**区别 先说结论: * **class_copyIvarList()** 能获取到所有的成员变量,包括 花括号内的变量(`.h`和`.m`都包括). * **class_copyPropertyList()** 只能获取到 以`@property`关键字 声明的中属性(`.h`和`.m`都包括) 区别: * `class_copyIvarList()`获取默认是带下划线的变量 * `class_copyPropertyList()`获取默认是不带下划线的变量名称. > 但是以上两个方法都只能获取到当前类的属性和变量(也就是说获取不到父类的属性和变量) ___ 举例说明: 我们声明一个`ClassA` 通过 调试代码实现 ``` objc #import #import @interface ClassA : NSObject { int _a; int _b; int _c; CGFloat d; //不推荐这样写 } @property (nonatomic, strong) NSArray *arrayA; @property (nonatomic, copy ) NSString *stringA; @property (nonatomic, assign) dispatch_queue_t testQueue; @end @implementation ClassA @end ``` 如果是通过`class_copyIvarList()`函数获取则打印如下结果. ``` sh --- class_copyIvarList ↓↓↓--- _a _b _c d _arrayA _stringA _testQueue --------------END---------------- ``` 如果是通过`class_copyPropertyList()`函数获取则打印如下结果. ``` sh --- class_copyPropertyList ↓↓↓--- arrayA stringA testQueue --------------END---------------- ``` debug代码如下: ``` objc - (void)printIvarOrProperty { NSLog(@"--- class_copyPropertyList ↓↓↓---"); ClassA *classA = [[ClassA alloc] init]; unsigned int propertyCount; objc_property_t *result = class_copyPropertyList(object_getClass(classA), &propertyCount); for (unsigned int i = 0; i < propertyCount; i++) { objc_property_t objc_property_name = result[i]; NSLog(@"%@",[NSString stringWithFormat:@"%s", property_getName(objc_property_name)]); } free(result); NSLog(@"--------------END----------------"); NSLog(@"--- class_copyIvarList ↓↓↓---"); Ivar *iv = class_copyIvarList(object_getClass(classA), &propertyCount); for (unsigned int i = 0; i < propertyCount; i++) { Ivar ivar = iv[i]; NSLog(@"%@",[NSString stringWithFormat:@"%s", ivar_getName(ivar)]); } free(iv); NSLog(@"--------------END----------------"); } ``` 以上[demo点击这里下载](https://github.com/sunyazhou13/IvarAndPropertyDemo) ___ 下面我们看下[objc的源码](https://github.com/sunyazhou13/objc-runtime) 以下代码位于`objc-runtime-new.mm`中 ``` c++ /*********************************************************************** * class_copyPropertyList. Returns a heap block containing the * properties declared in the class, or nil if the class * declares no properties. Caller must free the block. * Does not copy any superclass's properties. * Locking: read-locks runtimeLock **********************************************************************/ objc_property_t * class_copyPropertyList(Class cls, unsigned int *outCount) { if (!cls) { if (outCount) *outCount = 0; return nil; } mutex_locker_t lock(runtimeLock); checkIsKnownClass(cls); ASSERT(cls->isRealized()); auto rw = cls->data(); property_t **result = nil; unsigned int count = rw->properties.count(); if (count > 0) { result = (property_t **)malloc((count + 1) * sizeof(property_t *)); count = 0; for (auto& prop : rw->properties) { result[count++] = ∝ } result[count] = nil; } if (outCount) *outCount = count; return (objc_property_t *)result; } ``` 通过源码我们可以看到 ``` c auto rw = cls->data(); rw->properties; //通过rw直接拿到properties ``` 通过rw直接拿到properties,然后便利拿出想要的 以`@property`关键字 声明变量名称. `properties `详细内容 还请异步运行时源码看下这里篇幅限制就不啰嗦了. --- ``` c++ /*********************************************************************** * class_copyIvarList * fixme * Locking: read-locks runtimeLock **********************************************************************/ Ivar * class_copyIvarList(Class cls, unsigned int *outCount) { const ivar_list_t *ivars; Ivar *result = nil; unsigned int count = 0; if (!cls) { if (outCount) *outCount = 0; return nil; } mutex_locker_t lock(runtimeLock); ASSERT(cls->isRealized()); if ((ivars = cls->data()->ro->ivars) && ivars->count) { result = (Ivar *)malloc((ivars->count+1) * sizeof(Ivar)); for (auto& ivar : *ivars) { if (!ivar.offset) continue; // anonymous bitfield result[count++] = &ivar; } result[count] = nil; } if (outCount) *outCount = count; return result; } ``` 这里就一个关键点 ``` c ivars = cls->data()->ro->ivars ``` 拿到ivars. 由于这两者拿到的成员不一样所以两个API就会有区别. #### `class_rw_t` 和 `class_ro_t` 的区别 先说结论: * 两个结构体都存放着当前类的属性、实例变量、方法、协议等. * `class_ro_t`存放的是编译期间就确定的. * 而`class_rw_t`是在runtime时才确定,它会先将`class_ro_t`的内容拷贝过去,然后再将当前类的分类的这些属性、方法等拷贝到其中。所以可以说`class_rw_t`是`class_ro_t`的超集,当然实际访问类的方法、属性等也都是访问的`class_rw_t`中的内容. ___ ##### 下面我来深入了解两者具体是什么 首先我们需要了解它俩的由来,在`objc_class`我们知道有一个成员变量叫`isa`,我们这里要介绍的是`objc_class`的另一成员变量`bits`. `objc_class`的结构如下: ![objc_class的结构](/assets/images/20200721iOSinterviewAnswers/objc_class_struct.webp) `bits` 用来存储类的属性,方法,协议等信息。它是一个`class_data_bits_t`类型 `class_data_bits_t` 如下: ``` objc struct class_data_bits_t { uintptr_t bits; // method here } ``` 这个结构体只有一个`64bit`的成员变量`bits`,先来看看这`64bit`分别存放的什么信息: ![](/assets/images/20200721iOSinterviewAnswers/objc_class_bits.webp) * `is_swift` : 第一个bit,判断类是否是Swift类 * `has_default_rr` :第二个bit,判断当前类或者父类含有默认的`retain/release/autorelease/retainCount/_tryRetain/_isDeallocating/retainWeakReference/allowsWeakReference` 方法 * `require_raw_isa` :第三个bit, 判断当前类的实例是否需要`raw_isa` * `data` : 第4-48位,存放一个指向class_rw_t结构体的指针,该结构体包含了该类的属性,方法,协议等信息。至于为何只用44bit来存放地址 ##### `class_rw_t` 和` class_ro_t` 先来看看两个结构体的内部成员变量 ``` objc struct class_rw_t { uint32_t flags; uint32_t version; const class_ro_t *ro; method_array_t methods; property_array_t properties; protocol_array_t protocols; Class firstSubclass; Class nextSiblingClass; }; ``` ``` objc struct class_ro_t { uint32_t flags; uint32_t instanceStart; uint32_t instanceSize; uint32_t reserved; const uint8_t * ivarLayout; const char * name; method_list_t * baseMethodList; protocol_list_t * baseProtocols; const ivar_list_t * ivars; const uint8_t * weakIvarLayout; property_list_t *baseProperties; }; ``` `class_rw_t`结构体内有一个指向`class_ro_t`结构体的指针. 每个类都对应有一个`class_ro_t`结构体和一个`class_rw_t`结构体。在编译期间,`class_ro_t`结构体就已经确定,`objc_class`中的`bits`的`data`部分存放着该结构体的地址。在`runtime`运行之后,具体说来是在运行`runtime`的`realizeClass` 方法时,会生成`class_rw_t`结构体,该结构体包含了`class_ro_t`,并且更新`data`部分,换成`class_rw_t`结构体的地址。 用两张图来说明这个过程: 类的`realizeClass`运行之前: ![](/assets/images/20200721iOSinterviewAnswers/before_bits.webp) 类的`realizeClass`运行之后: ![](/assets/images/20200721iOSinterviewAnswers/after_bits.webp) 细看两个结构体的成员变量会发现很多相同的地方,他们都存放着当前类的属性、实例变量、方法、协议等等。区别在于:`class_ro_t`存放的是编译期间就确定的;而`class_rw_t`是在`runtime`时才确定,它会先将`class_ro_t`的内容拷贝过去,然后再将当前类的分类的这些属性、方法等拷贝到其中。所以可以说`class_rw_t`是`class_ro_t`的超集,当然实际访问类的方法、属性等也都是访问的`class_rw_t`中的内容 属性(property)存放在`class_rw_t`中,实例变量(ivar)存放在`class_ro_t`中。 详细内容请 参考资料[Objective-C runtime - 属性与方法](http://vanney9.com/2017/06/05/objective-c-runtime-property-method/) #### category如何被加载的,两个category的load方法的加载顺序,两个category的同名方法的加载顺序 结论: 1. category 是 这样 `realizeClass ` -> `methodizeClass()` -> `attachCategories()` 一步步被加载的. 2. 主类与分类的加载顺序是:**主类优先于分类加载,无关编译顺序**. 3. 分类间的加载顺序取决于编译的顺序:**编译在前则先加载,编译在后则后加载**. --- ##### category如何被加载的 我在运行时的源码 `objc-runtime-new.mm`中找到如下: ``` objc static Class realizeClassWithoutSwift(Class cls, Class previously) { ... // Attach categories 被加载 methodizeClass(cls, previously); return cls; } ``` `realizeClass ` -> `methodizeClass()` -> `attachCategories()` 核心是在methodizeClass()函数中实现的. ``` c static void methodizeClass(Class cls) { runtimeLock.assertLocked(); bool isMeta = cls->isMetaClass(); auto rw = cls->data(); auto ro = rw->ro; ... property_list_t *proplist = ro->baseProperties; if (proplist) { rw->properties.attachLists(&proplist, 1); } ... // Attach categories. category_list *cats = unattachedCategoriesForClass(cls, true /*realizing*/); attachCategories(cls, cats, false /*don't flush caches*/); ... if (cats) free(cats); } ``` 通过上述代码我们发现`ro->baseProperties;` , baseProperties 在前,category 在后, ``` objc property_list_t *proplist = ro->baseProperties; if (proplist) { rw->properties.attachLists(&proplist, 1); } ``` 但决定顺序的是 rw->`properties.attachLists ()`这个方法. ``` c /// category 被附加进去 void attachLists(List* const * addedLists, uint32_t addedCount) { if (addedCount == 0) return; if (hasArray()) { // many lists -> many lists uint32_t oldCount = array()->count; uint32_t newCount = oldCount + addedCount; setArray((array_t *)realloc(array(), array_t::byteSize(newCount))); array()->count = newCount; // 将旧内容移动偏移量 addedCount 然后将 addedLists copy 到起始位置 /* struct array_t { uint32_t count; List* lists[0]; }; */ memmove(array()->lists + addedCount, array()->lists, oldCount * sizeof(array()->lists[0])); memcpy(array()->lists, addedLists, addedCount * sizeof(array()->lists[0])); } else if (!list && addedCount == 1) { // 0 lists -> 1 list list = addedLists[0]; } else { // 1 list -> many lists List* oldList = list; uint32_t oldCount = oldList ? 1 : 0; uint32_t newCount = oldCount + addedCount; setArray((array_t *)malloc(array_t::byteSize(newCount))); array()->count = newCount; if (oldList) array()->lists[addedCount] = oldList; memcpy(array()->lists, addedLists, addedCount * sizeof(array()->lists[0])); } } ``` 所以 category 的属性总是在前面的,baseClass的属性被往后偏移了。 ##### 两个category的load方法的加载顺序 ``` txt A class’s +load method is called after all of its superclasses’ +load methods. 一个类的+load方法在其父类的+load方法后调用 A category +load method is called after the class’s own +load method. 一个Category的+load方法在被其扩展的类的自有+load方法后调用 ``` 结论: 主类与分类的加载顺序是:**主类优先于分类加载,无关编译顺序**. ##### 两个category的同名方法的加载顺序 应用程序 image 镜像加载到内存中时, `Category` 解析的过程,注意下面的 `while(i--)` 循环 这里倒序将 `category` 中的协议 方法 属性添加到了` rw = cls->data() `中的 `methods/properties/protocols`中。 ``` objc static void attachCategories(Class cls, category_list *cats, bool flush_caches) { if (!cats) return; if (PrintReplacedMethods) printReplacements(cls, cats); bool isMeta = cls->isMetaClass(); // fixme rearrange to remove these intermediate allocations method_list_t **mlists = (method_list_t **) malloc(cats->count * sizeof(*mlists)); property_list_t **proplists = (property_list_t **) malloc(cats->count * sizeof(*proplists)); protocol_list_t **protolists = (protocol_list_t **) malloc(cats->count * sizeof(*protolists)); // Count backwards through cats to get newest categories first int mcount = 0; int propcount = 0; int protocount = 0; int i = cats->count; bool fromBundle = NO; while (i--) { auto& entry = cats->list[i]; method_list_t *mlist = entry.cat->methodsForMeta(isMeta); if (mlist) { mlists[mcount++] = mlist; fromBundle |= entry.hi->isBundle(); } property_list_t *proplist = entry.cat->propertiesForMeta(isMeta, entry.hi); if (proplist) { proplists[propcount++] = proplist; } protocol_list_t *protolist = entry.cat->protocols; if (protolist) { protolists[protocount++] = protolist; } } auto rw = cls->data(); // 注意下面的代码,上面采用倒叙遍历方式,所以后编译的 category 会先add到数组的前部 prepareMethodLists(cls, mlists, mcount, NO, fromBundle); rw->methods.attachLists(mlists, mcount); free(mlists); if (flush_caches && mcount > 0) flushCaches(cls); rw->properties.attachLists(proplists, propcount); free(proplists); rw->protocols.attachLists(protolists, protocount); free(protolists); } ``` 所以结论是:分类间的加载顺序取决于编译的顺序:编译在前则先加载,编译在后则后加载 这个问题网上有很多例子 就不多在这举例了. #### `category` & `extension`区别,能给NSObject添加Extension吗,结果如何 ##### `category` * 运行时添加分类属性/协议/方法 * 分类添加的方法会“覆盖”原类方法,因为方法查找的话是从头至尾,一旦查找到了就停止了 * 同名分类方法谁生效取决于编译顺序,image 读取的信息是倒叙的,所以编译越靠后的越先读入 * 名字相同的分类会引起编译报错; ##### `extension` * 编译时决议 * 只以声明的形式存在,多数情况下就存在于 .m 文件中; * 不能为系统类添加扩展 可以给类添加成员变量,但是是私有的 可以給类添加方法,但是是私有的 添加的属性和方法是类的一部分,在编译期就决定的。在编译器和头文件的@interface和实现文件里的@implement一起形成了一个完整的类。 伴随着类的产生而产生,也随着类的消失而消失 > **必须有类的源码才可以给类添加extension**!!! ##### `category` & `extension`区别 * Category的小括号中有名字,而Extension没有; * Category只能扩充方法,不能扩充成员变量和属性; * 如果Category声明了声明了一个属性,那么Category只会生成这个属性的set,get方法的声明,也就不是会实现.所以对于系统一些类,如nsstring,就无法添加类扩展 不能给NSObject添加Extension,因为在extension中添加的方法或属性必须在源类的文件的.m文件中实现才可以,即:你必须有一个类的源码才能添加一个类的`extension` ##### 能给NSObject添加Extension吗,结果如何? 不能 因为没有NSObject的.m源码文件. > 如果能的话那应该不叫Extension.或者我们自己通过运行时的api自己造一套ExtensionDIY.结果就是你用的根本不能称为`Extension`,而是api调用而已. #### 消息转发机制,消息转发机制和其他语言的消息机制优劣对比 > 前言: 了解消息转发之前我们有必要了解一些Objectivce-C中的消息传递机制 ##### 消息传递机制 在Objectivce-C中,我们通过`实例变量(对象)`或者`类方法名`调用一个方法,那么我们实际上是在发送一条消息 ``` objc id returnValue = [someObject messageName:parameter]; //实例调用方式 id returnValue = [ClassA messageName:parameter]; //类调用方式 ``` 上述`someObject`和`ClassA`是接受者(receiver),`messageName:`是选择器(`selector`),选择器和参数合起来称为消息(`message`)。编译器看到此消息后,将其转换为一条标准的c语言函数调用,所调用的函数乃是消息传递机制中的核心函数:`objc_msgSend()`。 ``` c void objc_msgSend(id self, SEL cmd, ...) ``` 第一个参数代表接受者,第二个参数代表选择子,后续参数就是消息中的那些参数 编译器会把刚才的那个例子中的消息转换为如下函数: ``` objc id returnValue = objc_msgSend(someObject, @selector(messageName:),parameter); id returnValue = objc_msgSend(ClassA, @selector(messageName:),parameter); ``` `objc_msgSend()`函数会依据接受者与选择器的类型来调用适当的方法.为来完成此操作,该方法需要在接受者所属的类中搜寻其“方法列表”(也就是上文我们说的`class_ro_t`中的method_list)。找到则跳到现实代码,否则,就沿着继承体系继续向上查找,如果还没有则执行消息转发操作。对于其他的“边界情况”,则需要交由Objective-c运行环境的另一些函数来处理: ``` c objc_msgSend_stret //待发送的消息返回结构体时 objc_msgSend_fpret //消息返回的是浮点型 objc_msgSendSuper //如果要给超类发送消息 ``` ##### 消息转发机制 结合上边的消息传递机制,在Objective-C中如果给一个对象发送一条它无法处理的消息,就会进入下图描述的消息转发(Message Forwarding)流程 ![](/assets/images/20200721iOSinterviewAnswers/methodforward.webp) 在objc中消息转发需要经历3个阶段 `resolveInstanceMethod` -> `forwardingTargetForSelectoer` -> `forwardInvocation` ->`消息未能处理`。 * 第一阶段:**动态方法解析(Dynamic Method Resolution)**也就是在所属的类中先征询接受者,看其是否能动态加方法,来处理当前这个**未知选择器** * 第二阶段:**替换消息接收者快速转发** * 第三阶段:**完全消息转发机制** ##### 第一阶段:**动态方法解析(Dynamic Method Resolution)** 对象在受到无法解读的消息后,首先将调用其所属类的下列类方法: ``` objc + (BOOL)resolveClassMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0); + (BOOL)resolveInstanceMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0); ``` > 这俩方法在NSObject.h中 返回一个`Boolean`类型,表示这个类是否能新增一个实例方法以处理选择器. 在 消息转发过程中,我们可以使用`resolveInstanceMethod:`动态的将一个方法添加到一个类中. 例下面示例代码: ``` objc @implementation MyClass + (BOOL)resolveInstanceMethod:(SEL)aSEL { if (aSEL == @selector(resolveThisMethodDynamically)) { class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:"); return YES; } return [super resolveInstanceMethod:aSEL]; } @end ``` 这里我们用到一个运行时函数`class_addMethod()`. ``` c BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types) { if (!cls) return NO; mutex_locker_t lock(runtimeLock); return ! addMethod(cls, name, imp, types ?: "", NO); } ``` * `class_addMethod()`最后一个参数叫做`types`,是一个描述方法的参数类型的字符串. * `v`代表`void` * `@`代表对象或者说`id类型` * `:`(这个冒号)代表方法选择器SEL 具体代表什么不是我们瞎写的,得按照苹果的这个标准 [Objective-C Runtime Programming Guide->Type Encodings](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html#//apple_ref/doc/uid/TP40008048-CH100-SW1) 上面的`dynamicMethodIMP`,返回值是`void`,两个入参分别是`id`和`SEL`,所以描述这个方法的参数类型的字符串就是`v@:` 这个阶段的意义是为一个类动态提供方法实现,严格来说,还没进入消息转发流程。 `resolveInstanceMethod:` 控制这下面两个方法是否会被调用 * `respondsToSelector:` * `instancesRespondToSelector:` > 也就是说,如果`resolveInstanceMethod:`返回了`YES`,那么`respondsToSelector:`和`instancesRespondToSelector:`都会返回`YES`. ##### 第二阶段:替换消息接收者(快速转发) 如果第一阶段中`resolveInstanceMethod:`返回NO,就会调用`forwardingTargetForSelector:`询问是否把消息转发给另一个对象.消息的接收者就改变了。 ``` objc - (id)forwardingTargetForSelector:(SEL)aSelector { return someOtherObject; } ``` ##### 第三阶段:完全消息转发机制 如果第二阶段的`forwardingTargetForSelector:`返回了`nil`,这就进入了所谓完全消息转发的机制。 首先调用`methodSignatureForSelector:`为要转发的消息返回正确的签名: ``` objc - (void)forwardInvocation:(NSInvocation *)anInvocation { NSLog(@"forwardInvocation"); SomeOtherObject *someOtherObject = [SomeOtherObject new]; if ([someOtherObject respondsToSelector:[anInvocation selector]]) { [anInvocation invokeWithTarget:someOtherObject]; } else { [super forwardInvocation:anInvocation]; } } ``` 上面代码是将消息转发给其他对象,其实这与第二阶段中示例代码做的事情是一样的。区别就在于这个阶段会有一个`NSInvocation`对象。[`NSInvocation`](https://developer.apple.com/documentation/foundation/nsinvocation?language=objc)是一个用来存储和转发消息的对象。它包含了一个Objective-C消息的所有元素:一个target,一个selector,参数和返回值。每个元素都可以被直接设置。 > `NSInvocation`可以简单理解为一个对象把我们用到 selector方法和对象都存储了一下,然后哪个是指向我们需要调用的指针对象. 所以不同与第二阶段,在这个阶段你可以: * 把消息存储,在你觉得合适的时机转发出去,或者不处理这个消息。 * 修改消息的target,selector,参数等 * 多次转发这个消息,转发给多个对象 显然在这个阶段,你可以对一个OC消息做更多的事情 --- ##### 消息转发机制和其他语言的消息机制优劣对比 这个目前没有深入其它编程语言的运行时层面,比如C的底层或者C++的底层或者Java的底层消息传递这里提供 [一个android的类似消息转发的文章](探索 Android App Bundle) #### 在方法调用的时候,方法查询-> 动态解析-> 消息转发 之前做了什么 Objective-C 实例对象执行方法步骤 1. 获取 receiver 对应的类 Class 2. 在 Class 缓存列表中(就是`objc_class`里的`cache_t`到`class_ro_t`的方法list)根据选择子`selector`查找`IMP` 3. 若缓存中没有找到,则在方法列表中继续查找. 4. 若方法列表没有,则从父类查找,重复以上步骤. 5. 若最终没有找到,则进行消息转发操作. * 方法查询之前 要知道 receiver和 selector.主要是要明确我们是哪个实例调用了哪个方法. * 动态解析解析之前要 在所属的类中先征询接受者,看其是否能动态加方法,来处理当前这个未知选择器. * 消息转发 之前 要询问是否把消息转发给另一个对象. > 如果更深入的而理解 那应该是 objc_msgSend() 为啥是汇编实现的,上面的那些方法 调用之前 汇编的哪些指令被执行 这里找到两篇文章可以参考一下 [深入了解Objective-C消息发送与转发过程](https://chipengliu.github.io/2019/06/02/objc-msgSend-forward/) [汇编语言编写的,其中具体过程细节](https://chipengliu.github.io/2019/04/07/objc-msg-armd64/) #### `IMP`、`SEL`、`Method`的区别和使用场景 * `IMP` : 是方法的具体实现(指针) * `SEL` :方法名称 * `Method `:是objc_method类型指针,它是一个结构体 ,如下: ``` objc struct objc_method { SEL _Nonnull method_name OBJC2_UNAVAILABLE; char * _Nullable method_types OBJC2_UNAVAILABLE; IMP _Nonnull method_imp OBJC2_UNAVAILABLE; } ``` 使用场景 * 例如 Button添加Target和Selector的时候.或者 实现类的`swizzle`的时候会用到,通过`class_getInstanceMethod(class, SEL)`来获取类的方法`Method`,其中用到了SEL作为方法名 * 例如 给类动态添加方法,此时我们需要调用class_addMethod(Class, SEL, IMP, types),该方法需要我们传递一个方法的实现函数IMP,例如: ``` objc static void funcName(id receiver, SEL cmd, 方法参数...) { // 方法具体的实现 } ``` > SEL相当于 方法的类型 关键字. #### `load`、`initialize`方法的区别什么?在继承关系中他们有什么区别 在Objective-C的类被加载和初始化的时候, 类 是 可以收到 方法回调的. ``` objc - (void)load; - (void)initialize; ``` ##### `+load` `+ load`方法是在这个文件(就是你复写的子类化的class)被程序装载时调用,只要是在Xcode `Compile Sources`中出现的文件总是会被装载,这与这个类是否被用到无关,因此+load方法总是在`main()`函数之前调用. 调用时机比较早,运行环境有不确定因素。具体说来,在iOS上通常就是App启动时进行加载,但当load调用的时候,并不能保证所有类都加载完成且可用,必要时还要自己负责做auto release处理。 > 补充上面一点,对于有依赖关系的两个库中,被依赖的类的+load会优先调用。但在一个库之内,父、子类、类别之间调用有顺序,不同类之间调用顺序是不确定的。 * 关于继承:对于一个类而言,没有+load方法实现就不会调用,不会考虑对NSObject的继承,就是不会沿用父类的+load。 * 父类和本类的调用:父类的方法优先于子类的方法。一个类的+load方法不用写明`[super load]`,父类就会收到调用。 * 本类和Category的调用:本类的方法优先于类别(Category)中的方法。Category的+load也会收到调用,但顺序上在本类的+load调用之后。 * 不会直接触发initialize的调用。 #### `+initialize` `+initialize`方法是在类或它的子类收到第一条消息之前被调用的,这里所指的消息包括实例方法和类方法的调用,并且只会调用一次。`initialize`方法实际上是一种惰性(lazy load)调用,也就是说如果一个类一直没被用到,那它的initialize方法也不会被调用,这一点有利于节约资源. runtime 使用了发送消息 `objc_msgSend` 的方式对 `+initialize` 方法进行调用。也就是说 `+initialize` 方法的调用与普通方法的调用是一样的,走的都是`发送消息的流程`。换言之,如果子类没有实现 +initialize 方法,那么继承自父类的实现会被调用;如果一个类的分类实现了 `+initialize` 方法,那么就会对这个类中的实现造成覆盖(override)。 * initialize的自然调用是在第一次主动使用当前类的时候。 * 在initialize方法收到调用时,运行环境基本健全。 * 关于继承:和load不同,即使子类不实现initialize方法,会把父类的实现继承过来调用一遍,就是会沿用父类的+initialize。(沿用父类的方法中,self还是指子类) * 父类和本类的调用:子类的+initialize将要调用时会激发父类调用的+initialize方法,所以也不需要在子类写明[super initialize]。(本着除主动调用外,只会调用一次的原则,如果父类的+initialize方法调用过了,则不会再调用) * 本类和Category的调用:Category中的+initialize方法会覆盖本类的方法,只执行一个Category的+initialize方法。 下面是我整理的一个表格希望对解释这俩方法有帮助: | | + load | + initialize | | ------| ------ | ------ | | 调用方式 | 直接使用函数内存地址 | objc_msgSend()方式 | | 调用时机 | 被程序装载时调用main()函数之前,就是被添加到runtime时 | 在本类或它的子类收到第一条消息之前被调用 | | 是否被系统单次调用(除主动调用外) | 是 | 是 | | 运行时环境是否稳定| 不确定 | 稳定 | | 线程是否安全 | 默认是安全的(已加锁) | 安全(已加锁 ) | | 特性 | 由于非`objc_msgSend()`方式调用就使得 +load 方法拥有了一个非常有趣的特性,那就是子类、父类和分类中的 +load 方法的实现是被区别对待的。也就是说如果子类没有实现 +load 方法,那么当它被加载时 runtime 是不会去调用父类的 +load 方法的。同理,当一个类和它的分类都实现了 +load 方法时,两个方法都会被调用 | +initialize 方法的调用与普通方法的调用是一样的,如果子类没有实现 +initialize 方法,那么继承自父类的实现会被调用;如果一个类的分类实现了 +initialize 方法,那么就会对这个类中的实现造成覆盖 | 参考[类方法load和initialize的区别](https://cloud.tencent.com/developer/article/1355957) ##### 在继承关系中他们有什么区别 super的方法会成功调用,但是这是多余的,因为runtime会自动对父类的+load方法进行调用,而+initialize则会随子类自动激发父类的方法(如Apple文档中所言)不需要显示调用。另一方面,如果父类中的方法用到的self(像示例中的方法),其指代的依然是类自身,而不是父类 #### 说说消息转发机制的优劣 优点: * 利用消息转发机制可以无代码侵入的实现多重代理,让不同对象可以同时代理同个回调,然后在各自负责的区域进行相应的处理,降低了代码的耦合程度。 * 使用 @synthesize 可以为 @property 自动生成 getter 和 setter 方法(现 Xcode 版本中,会自动生成),而 @dynamic 则是告诉编译器,不用生成 getter 和 setter 方法。当使用 @dynamic 时,我们可以使用消息转发机制,来动态添加 getter 和 setter 方法。当然你也用其他的方法来实现。 缺点: * Objective-C本身不支持多继承,这是因为消息机制名称查找发生在运行时而非编译时,很难解决多个基类可能导致的二义性问题,但是可以通过消息转发机制在内部创建多个功能的对象,把不能实现的功能给转发到其他对象上去,这样就做出来一种多继承的假象。转发和继承相似,可用于为OC编程添加一些多继承的效果,一个对象把消息转发出去,就好像他把另一个对象中放法接过来或者“继承”一样。消息转发弥补了objc不支持多继承的性质,也避免了因为多继承导致单个类变得臃肿复杂。 # 总结 本篇讲述的面试题中的**runtime相关问题**之**结构模型**部分。下一章打算继续讲一下 **runtime相关问题**之**内存管理**,这样循序渐进把相关面试的文章都讲完. 这里不得不说 这样的面试确实很有挑战,顺便 我也喷一下阿里 头条希望厚道一点,有问题可以但是也要有答案.这件事 让我观察出 这两家公司干事 有头没尾,能善始未能善终. URL: https://sunyazhou.com/2020/04/CocoapodsProblems/index.html.md Published At: 2020-04-10 07:13:59 +0000 # Cocoapods清华镜像 # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! ## Cocoapod疑难杂症 这几天开发 总遇到疑难杂症 是因为我的cocoapods升级到了1.9.1,导致各种问题 然后还被和谐,无奈找到如下解决cocoapods各种问题的解决方式 对于旧版的 CocoaPods 可以使用如下方法使用 tuna 的镜像: ``` sh $ pod repo remove master $ pod repo add master https://mirrors.tuna.tsinghua.edu.cn/git/CocoaPods/Specs.git $ pod repo update ``` 新版的 CocoaPods 不允许用`pod repo add`直接添加master库了,但是依然可以: ``` sh $ cd ~/.cocoapods/repos $ pod repo remove master $ git clone https://mirrors.tuna.tsinghua.edu.cn/git/CocoaPods/Specs.git master ``` 最后进入自己的工程,在自己工程的`PodFile`第一行加上: ``` ruby source 'https://mirrors.tuna.tsinghua.edu.cn/git/CocoaPods/Specs.git' ``` # 总结 折腾了很久不如找对地方 [参考CocoaPods 镜像使用帮助](https://mirrors.tuna.tsinghua.edu.cn/help/CocoaPods/) URL: https://sunyazhou.com/2020/04/MasonryTricks/index.html.md Published At: 2020-04-07 11:54:39 +0000 # 使用Masonry处理UIView的safeArea边界布局问题 # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! ## 背景 iOS11之后推出的safeArea 用于处理刘海屏幕的问题.如果自己处理起来可能比较 麻烦 又需要判断 版本又需要判断 API的可用性. 清明假期 在家没事写个demo 解决如何更快捷处理屏幕的边界问题,比如 视图要布局在iOS导航栏底部 和 `Home Indicator`. 先看下图: ![](/assets/images/20200407MasonryTricks/SafeArea1.gif) 如果使用更少的代码实现在安全区域内部 展示某个View. ## 代码实现 这里我们借助Masonry最新库提供的支持API ``` objc - (void)viewDidLoad { [super viewDidLoad]; [self.subViewA mas_makeConstraints:^(MASConstraintMaker *make) { if (@available(iOS 11.0, *)) { make.top.equalTo(self.view.mas_safeAreaLayoutGuideTop); make.left.equalTo(self.view.mas_safeAreaLayoutGuideLeft); make.bottom.equalTo(self.view.mas_safeAreaLayoutGuideBottom); make.right.equalTo(self.view.mas_safeAreaLayoutGuideRight); } else { make.top.equalTo(self.mas_topLayoutGuideBottom); make.left.right.equalTo(self.view); make.bottom.equalTo(self.mas_bottomLayoutGuideTop); } }]; } ``` > 这里看到safeArea仅仅支持 iOS11以上 那么 iOS11一下 我们可以借出如上述代码 > `self.mas_topLayoutGuideBottom`和`self.mas_bottomLayoutGuideTop` 这个是`self`指的是`UIViewController` 下面我们不用SafeArea来使用一下 如下api 1. 顶部区域 * `mas_topLayoutGuide`和`mas_topLayoutGuideBottom`都是 顶到屏幕 刘海屏底部 也就是说和 safeAreaTop一样,如下图: ![](/assets/images/20200407MasonryTricks/mas_topLayoutGuide&mas_topLayoutGuideBottom.webp) * `mas_topLayoutGuideTop` 顶到屏幕顶部(忽略刘海屏,也就是说被刘海盖住),如下图示: ![](/assets/images/20200407MasonryTricks/mas_topLayoutGuideTop.webp) 2. 底部区域 * `mas_bottomLayoutGuide`和`mas_bottomLayoutGuideTop` 都是在`Home条`的上面 ,如下图: ![](/assets/images/20200407MasonryTricks/mas_bottomLayoutGuide&mas_bottomLayoutGuideTop.webp) * `mas_bottomLayoutGuideBottom` 直接推底,撑到屏幕边缘,如下图: ![](/assets/images/20200407MasonryTricks/mas_bottomLayoutGuideBottom.webp) #### 如果想实现和safeArea一样的搞法 可以这样写 ``` objc [self.subViewA mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(self.mas_topLayoutGuide); make.left.right.equalTo(self.view); make.bottom.equalTo(self.mas_bottomLayoutGuide); }]; ``` > !!!注意 LayoutGuide 紧紧适用于` ios(7.0,11.0)`,也就是说11之后 必须使用safeArea才精准. 附上一张搞完的效果图 ![](/assets/images/20200407MasonryTricks/LayoutGuideFullsceen.webp) # 总结 借入Masory 可以更加方便快捷的实现我们想要的布局效果并且不用写宏区分是否是刘海屏或者其他屏幕,因为我们操控的实际上就是 安全区内部的范围. 没事得多关注开源代码时长写个demo实验一下. 所以想实现上文中最佳实践的代码应该是 ``` objc [self.subViewA mas_makeConstraints:^(MASConstraintMaker *make) { if (@available(iOS 11.0, *)) { make.top.equalTo(self.view.mas_safeAreaLayoutGuideTop); make.left.equalTo(self.view.mas_safeAreaLayoutGuideLeft); make.bottom.equalTo(self.view.mas_safeAreaLayoutGuideBottom); make.right.equalTo(self.view.mas_safeAreaLayoutGuideRight); } else { make.top.equalTo(self.mas_topLayoutGuideBottom); make.left.right.equalTo(self.view); make.bottom.equalTo(self.mas_bottomLayoutGuideTop); } }]; ``` [本文Demo](https://github.com/sunyazhou13/MasonryTrickDemo) URL: https://sunyazhou.com/2020/03/NSURLProtocol/index.html.md Published At: 2020-03-20 11:34:22 +0000 # (转)深度理解 NSURLProtocol ![](/assets/images/20200320NSURLProtocol/NSURLProtocol.webp) # 前言 本文经由微信`知识小集 `公众号授权并转载自[FiTeen博客](https://blog.fiteen.top/2020/hijacking-webview-request-with-nsprotocol),如果版权问题请与我联系sunyazhou13@163.com.转载此文目的是为了记录iOS开发中的重要知识点,防止原文博客寻找起来麻烦. 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! ## NSURLProtocol 是什么 NSURLProtocol 是 Foundation 框架中[URL Loading System](https://developer.apple.com/documentation/foundation/url_loading_system?language=objc)的一部分。它可以让开发者可以在不修改应用内原始请求代码的情况下,去改变 URL 加载的全部细节。换句话说,NSURLProtocol 是一个被 Apple 默许的中间人攻击。 虽然 NSURLProtocol 叫`Protocol`,却不是协议,而是一个**抽象类**。 既然 NSURLProtocol 是一个抽象类,说明它无法被实例化,那么它又是如何实现网络请求拦截的? 答案就是通过**子类化**来定义新的或是已经存在的 URL 加载行为。如果当前的网络请求是可以被拦截的,那么开发者只需要将一个自定义的 NSURLProtocol 子类注册到 App 中,在这个子类中就可以拦截到所有请求并进行修改。 那么到底哪些网络请求可以被拦截? ## NSURLProtocol 使用场景 前面已经说了,NSURLProtocol 是 URL Loading System 的一部分,所以它可以拦截所有基于 URL Loading System 的网络请求: * NSURLSession * NSURLConnection * NSURLDownload * NSURLResponse * NSHTTPURLResponse * NSURLRequest * NSMutableURLRequest 相应的,基于它们实现的第三方网络框架[AFNetworking](https://github.com/AFNetworking/AFNetworking)和[Alamofire](https://github.com/Alamofire/Alamofire)的网络请求,也可以被 NSURLProtocol 拦截到。 但早些年基于 CFNetwork 实现的,比如[ASIHTTPRequest](https://github.com/pokeb/asi-http-request),其网络请求就无法被拦截。 另外,**UIWebView也是可以被NSURLProtocol拦截的,但WKWebView不可以**。(因为 WKWebView 是基于 WebKit,并不走 C socket。) 因此,在实际应用中,它的功能十分强大,比如: * 重定向网络请求,解决 DNS 域名劫持的问题 * 进行全局或局部的网络请求设置,比如修改请求地址、header 等 * 忽略网络请求,使用 H5 离线包或是缓存数据等 * 自定义网络请求的返回结果,比如过滤敏感信息 下面来看一下 NSURLProtocol 的相关方法。 ## NSURLProtocol 的相关方法 ### 创建协议对象 ``` objc // 创建一个 URL 协议实例来处理 request 请求 - (instancetype)initWithRequest:(NSURLRequest *)request cachedResponse:(NSCachedURLResponse *)cachedResponse client:(id)client; // 创建一个 URL 协议实例来处理 session task 请求 - (instancetype)initWithTask:(NSURLSessionTask *)task cachedResponse:(NSCachedURLResponse *)cachedResponse client:(id)client; ``` ### 注册和注销协议类 ``` objc // 尝试注册 NSURLProtocol 的子类,使之在 URL 加载系统中可见 + (BOOL)registerClass:(Class)protocolClass; // 注销 NSURLProtocol 的指定子类 + (void)unregisterClass:(Class)protocolClass; ``` ### 确定子类是否可以处理请求 子类化 NSProtocol 的首要任务就是告知它,需要控制什么类型的网络请求。 ``` objc // 确定协议子类是否可以处理指定的 request 请求,如果返回 YES,请求会被其控制,返回 NO 则直接跳入下一个 protocol + (BOOL)canInitWithRequest:(NSURLRequest *)request; // 确定协议子类是否可以处理指定的 task 请求 + (BOOL)canInitWithTask:(NSURLSessionTask *)task; ``` ### 获取和设置请求属性 NSURLProtocol 允许开发者去获取、添加、删除 request 对象的任意元数据。这几个方法常用来处理请求无限循环的问题。 ``` objc // 在指定的请求中获取与指定键关联的属性 + (id)propertyForKey:(NSString *)key inRequest:(NSURLRequest *)request; // 设置与指定请求中的指定键关联的属性 + (void)setProperty:(id)value forKey:(NSString *)key inRequest:(NSMutableURLRequest *)request; // 删除与指定请求中的指定键关联的属性 + (void)removePropertyForKey:(NSString *)key inRequest:(NSMutableURLRequest *)request; ``` ### 提供请求的规范版本 如果你想要用特定的某个方式来修改请求,可以用下面这个方法。 ``` objc // 返回指定请求的规范版本 + (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request; ``` ### 确定请求是否相同 ``` objc // 判断两个请求是否相同,如果相同可以使用缓存数据,通常只需要调用父类的实现 + (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b; ``` ### 启动和停止加载 这是子类中最重要的两个方法,不同的自定义子类在调用这两个方法时会传入不同的内容,但共同点都是围绕 `protocol` 客户端进行操作. ``` objc // 开始加载 - (void)startLoading; // 停止加载 - (void)stopLoading; ``` ### 获取协议属性 ``` objc // 获取协议接收者的缓存 - (NSCachedURLResponse *)cachedResponse; // 接受者用来与 URL 加载系统通信的对象,每个 NSProtocol 的子类实例都拥有它 - (id)client; // 接收方的请求 - (NSURLRequest *)request; // 接收方的任务 - (NSURLSessionTask *)task; ``` NSURLProtocol 在实际应用中,主要是完成两步:拦截 URL 和 URL 转发。先来看如何拦截网络请求。 ## 如何利用 NSProtocol 拦截网络请求 ### 创建 NSURLProtocol 子类 这里创建一个名为`HTCustomURLProtocol`的子类。 ``` objc @interface HTCustomURLProtocol : NSURLProtocol @end ``` ### 注册 NSURLProtocol 的子类 在合适的位置注册这个子类。对基于 NSURLConnection 或者使用`[NSURLSession sharedSession]`初始化对象创建的网络请求,调用 `registerClass` 方法即可。 ``` objc [NSURLProtocol registerClass:[NSClassFromString(@"HTCustomURLProtocol") class]]; // 或者 // [NSURLProtocol registerClass:[HTCustomURLProtocol class]]; ``` 如果需要全局监听,可以设置在 `AppDelegate.m` 的 `didFinishLaunchingWithOptions:` 方法中。如果只需要在单个 UIViewController 中使用,记得在合适的时机注销监听: ``` objc [NSURLProtocol unregisterClass:[NSClassFromString(@"HTCustomURLProtocol") class]]; ``` 如果是基于 NSURLSession 的网络请求,且不是通过`[NSURLSession sharedSession]`方式创建的,就得配置 NSURLSessionConfiguration 对象的 `protocolClasses` 属性。 ``` objc NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration]; sessionConfiguration.protocolClasses = @[[NSClassFromString(@"HTCustomURLProtocol") class]]; ``` ### 实现 NSURLProtocol 子类 实现子类分为五个步骤: > 注册 → 拦截 → 转发 → 回调 → 结束 以拦截 UIWebView 为例,这里需要重写父类的这五个核心方法。 ``` objc // 定义一个协议 key static NSString * const HTCustomURLProtocolHandledKey = @"HTCustomURLProtocolHandledKey"; // 在拓展中定义一个 NSURLConnection 属性。通过 NSURLSession 也可以拦截,这里只是以 NSURLConnection 为例。 @property (nonatomic, strong) NSURLConnection *connection; // 定义一个可变的请求返回值, @property (nonatomic, strong) NSMutableData *responseData; // 方法 1:在拦截到网络请求后会调用这一方法,可以再次处理拦截的逻辑,比如设置只针对 http 和 https 的请求进行处理。 + (BOOL)canInitWithRequest:(NSURLRequest *)request { // 只处理 http 和 https 请求 NSString *scheme = [[request URL] scheme]; if ( ([scheme caseInsensitiveCompare:@"http"] == NSOrderedSame || [scheme caseInsensitiveCompare:@"https"] == NSOrderedSame)) { // 看看是否已经处理过了,防止无限循环 if ([NSURLProtocol propertyForKey:HTCustomURLProtocolHandledKey inRequest:request]) { return NO; } // 如果还需要截取 DNS 解析请求中的链接,可以继续加判断,是否为拦截域名请求的链接,如果是返回 NO return YES; } return NO; } // 方法 2:【关键方法】可以在此对 request 进行处理,比如修改地址、提取请求信息、设置请求头等。 + (NSURLRequest *) canonicalRequestForRequest:(NSURLRequest *)request { // 可以打印出所有的请求链接包括 CSS 和 Ajax 请求等 NSLog(@"request.URL.absoluteString = %@",request.URL.absoluteString); NSMutableURLRequest *mutableRequest = [request mutableCopy]; return mutableRequest; } // 方法 3:【关键方法】在这里设置网络代理,重新创建一个对象将处理过的 request 转发出去。这里对应的回调方法对应 协议方法 - (void)startLoading { // 可以修改 request 请求 NSMutableURLRequest *mutableRequest = [[self request] mutableCopy]; // 打 tag,防止递归调用 [NSURLProtocol setProperty:@YES forKey:HTCustomURLProtocolHandledKey inRequest:mutableRequest]; // 也可以在这里检查缓存 // 将 request 转发,对于 NSURLConnection 来说,就是创建一个 NSURLConnection 对象;对于 NSURLSession 来说,就是发起一个 NSURLSessionTask。 self.connection = [NSURLConnection connectionWithRequest:mutableRequest delegate:self]; } // 方法 4:主要判断两个 request 是否相同,如果相同的话可以使用缓存数据,通常只需要调用父类的实现。 + (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b { return [super requestIsCacheEquivalent:a toRequest:b]; } // 方法 5:处理结束后停止相应请求,清空 connection 或 session - (void)stopLoading { if (self.connection != nil) { [self.connection cancel]; self.connection = nil; } } // 按照在上面的方法中做的自定义需求,看情况对转发出来的请求在恰当的时机进行回调处理。 #pragma mark- NSURLConnectionDelegate - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error { [self.client URLProtocol:self didFailWithError:error]; } #pragma mark - NSURLConnectionDataDelegate // 当接收到服务器的响应(连通了服务器)时会调用 - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response { self.responseData = [[NSMutableData alloc] init]; // 可以处理不同的 statusCode 场景 // NSInteger statusCode = [(NSHTTPURLResponse *)response statusCode]; // 可以设置 Cookie [self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed]; } // 接收到服务器的数据时会调用,可能会被调用多次,每次只传递部分数据 - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data { [self.responseData appendData:data]; [self.client URLProtocol:self didLoadData:data]; } // 服务器的数据加载完毕后调用 - (void)connectionDidFinishLoading:(NSURLConnection *)connection { [self.client URLProtocolDidFinishLoading:self]; } // 请求错误(失败)的时候调用,比如出现请求超时、断网,一般指客户端错误 - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error { [self.client URLProtocol:self didFailWithError:error]; } ``` 上面用到的一些 NSURLProtocolClient 方法: ``` objc @protocol NSURLProtocolClient // 请求重定向 - (void)URLProtocol:(NSURLProtocol *)protocol wasRedirectedToRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)redirectResponse; // 响应缓存是否合法 - (void)URLProtocol:(NSURLProtocol *)protocol cachedResponseIsValid:(NSCachedURLResponse *)cachedResponse; // 刚接收到 response 信息 - (void)URLProtocol:(NSURLProtocol *)protocol didReceiveResponse:(NSURLResponse *)response cacheStoragePolicy:(NSURLCacheStoragePolicy)policy; // 数据加载成功 - (void)URLProtocol:(NSURLProtocol *)protocol didLoadData:(NSData *)data; // 数据完成加载 - (void)URLProtocolDidFinishLoading:(NSURLProtocol *)protocol; // 数据加载失败 - (void)URLProtocol:(NSURLProtocol *)protocol didFailWithError:(NSError *)error; // 为指定的请求启动验证 - (void)URLProtocol:(NSURLProtocol *)protocol didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge; // 为指定的请求取消验证 - (void)URLProtocol:(NSURLProtocol *)protocol didCancelAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge; @end ``` ## 补充内容 ### 使用 NSURLSession 时的注意事项 如果在 NSURLProtocol 中使用 NSURLSession,需要注意: * 拦截到的 request 请求的 HTTPBody 为 nil,但可以借助 HTTPBodyStream 来获取 body; * 如果要用 `registerClass` 注册,只能通过 `[NSURLSession sharedSession]` 的方式创建网络请求。 ### 注册多个 NSURLProtocol 子类 当有多个自定义 NSURLProtocol 子类注册到系统中的话,会按照他们注册的反向顺序依次调用 URL 加载流程,也就是最后注册的 NSURLProtocol 会被优先判断。 对于通过配置 NSURLSessionConfiguration 对象的 `protocolClasses` 属性来注册的情况,`protocolClasses` 数组中只有第一个 NSURLProtocol 会起作用,后续的 NSURLProtocol 就无法拦截到了。 所以 [OHHTTPStubs](https://github.com/AliSoftware/OHHTTPStubs) 在注册 NSURLProtocol 子类的时候是这样处理的: ``` objc + (void)setEnabled:(BOOL)enable forSessionConfiguration:(NSURLSessionConfiguration*)sessionConfig { // Runtime check to make sure the API is available on this version if ([sessionConfig respondsToSelector:@selector(protocolClasses)] && [sessionConfig respondsToSelector:@selector(setProtocolClasses:)]) { NSMutableArray * urlProtocolClasses = [NSMutableArray arrayWithArray:sessionConfig.protocolClasses]; Class protoCls = HTTPStubsProtocol.class; if (enable && ![urlProtocolClasses containsObject:protoCls]) { // 将自己的 NSURLProtocol 插入到 protocolClasses 的第一个,进行拦截 [urlProtocolClasses insertObject:protoCls atIndex:0]; } else if (!enable && [urlProtocolClasses containsObject:protoCls]) { // 拦截完成后移除 [urlProtocolClasses removeObject:protoCls]; } sessionConfig.protocolClasses = urlProtocolClasses; } else { NSLog(@"[OHHTTPStubs] %@ is only available when running on iOS7+/OSX9+. " @"Use conditions like 'if ([NSURLSessionConfiguration class])' to only call " @"this method if the user is running iOS7+/OSX9+.", NSStringFromSelector(_cmd)); } } ``` ## 如何拦截 WKWebView 虽然 NSURLProtocol 无法直接拦截 WKWebView,但其实还是有解决方案的。就是使用 `WKBrowsingContextController` 和 `registerSchemeForCustomProtocol`。 ``` objc // 注册 scheme Class cls = NSClassFromString(@"WKBrowsingContextController"); SEL sel = NSSelectorFromString(@"registerSchemeForCustomProtocol:"); if ([cls respondsToSelector:sel]) { // 通过 http 和 https 的请求,同理可通过其他的 Scheme 但是要满足 URL Loading System [cls performSelector:sel withObject:@"http"]; [cls performSelector:sel withObject:@"https"]; } ``` 但由于这涉及到了私有方法,直接引用无法过苹果的机审,所以使用的时候需要对字符串做下处理,比如对方法名进行算法加密处理等,实测也是可以通过审核的。 总之,NSURLProtocol 非常强大,无论是优化 App 的性能,还是拓展功能,都具有很强的可塑空间,但在使用的同时,又要多关注它带来的问题。尽管它在很多框架或者知名项目中都已经得以应用,其奥义依然值得开发者们去深入研究。 # 总结 我认真看了作者的文章 强烈推荐iOS开发小伙伴学习一下. URL: https://sunyazhou.com/2020/02/SunyazhouTheory/index.html.md Published At: 2020-02-07 04:59:11 +0000 # 《孙亚洲理论》的诞生 ![](/assets/images/20200207SunyazhouTheory/thesunyazhoutheory.webp) # 前言 由于基础知识薄弱,我所做到的内容仅限于学习和观察到的一些事实,未能上升为理论学说. ## 非著名的《The Sunyazhou Theory》 国家要想发展必须从提高生产力上入手,技术是目前为止人类发现唯一解决生产力问题的有力工具。 所以我把生产力分为三个阶段: 1. 解放生产力 2. 提高生产力 3. 突破生产力 我把它称为**`《孙亚洲理论》`**英文**`《The Sunyazhou Theory》`**,目前没有任何权威杂志发表。 ## 验证理论 这种理论证明的客观事实是,发展中国家,发展阶段一定是: 1. 小规模制造可用商品 2. 优化并提升工艺水平流水线作业 3. 再到被新的产品取缔淘汰的过程 所以活在当下,无论上学还是工作,__技术人员远比普通人员有优势,因为它验证了我的理论学说,从事提高生产力的职业。__ 我希望多年后大家提起这个道理的时候能想到,啊这就是非著名的《孙亚洲理论》. # 总结 过年回家总有人问在北京工作工资高如何如何,我就简单发表一下我的看法并扯淡的形成了自己的理论学说. URL: https://sunyazhou.com/2019/12/FinalSummary/index.html.md Published At: 2019-12-30 23:30:00 +0000 # 2019年终总结 ![](/assets/images/20191231FinalSummary/AlanTuring.webp) 2019年 [阿兰图灵](https://baike.baidu.com/item/%E8%89%BE%E4%BC%A6%C2%B7%E9%BA%A6%E5%B8%AD%E6%A3%AE%C2%B7%E5%9B%BE%E7%81%B5/3940576?fromtitle=%E9%98%BF%E5%85%B0%C2%B7%E5%9B%BE%E7%81%B5&fromid=10961384)(计算机的先驱缔造者)将出现在未来的50英镑纸币的封面,这将是计算机发展史上的里程碑,是他破解了德国的著名密码系统Enigma,帮助盟军取得了二战的胜利.没有他就没有今天的计算机. # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! > 望极天涯博渐卑,而立三十我是谁. > 压年抑岁匆匆过,回头碌碌变无为. > 人言北漂薪资高,力图迈进皆煎熬. > 星辰大海尽在此,诗与远方路迢迢. 年终了,墨守成规交出2019的年终总结 ## 2019回顾 今年事件如下: * 工作 * 装修 * 日本之旅 * 生活 * 读书 * 买摩托车 * 买汽车 * 健康 * 五年计划 * 总结 ### 工作 今年工作很稳定,一直在快手干活,只是从6月份开始被调整去做一个新的app`快手极速版`,目前来看还不错,由于去年我所在的直播团队获得了`业务突破奖`.公司发了`500w`的奖金,显然这没我们什么事,只是出去团建的档次或许能提高一下. 今年有一件愉快的事情是我分享的一下AVAudioSesion相关文章,给个奖励,激励我不断学习专研技术. ![](/assets/images/20191231FinalSummary/share1.webp) ![](/assets/images/20191231FinalSummary/share2.webp) > 感谢我手官方,如此关怀备至. 公司在逐渐壮大,不知不觉中好像我也成了一名老员工,毕竟我来的时候工号1807号,现在目测得到10000+了.为了支撑公司业务,我不得不听从公司安排到其它业务线去做事情.心中有一种落差是从一个500w的团队去一个不是很好的团队,当时还充满了各种X数.即便各种委屈也只能作为一块砖哪里用哪里搬,当时就觉得也许干两个月就得凉凉,没想到不但没凉搞的还不错. 刚去开搞的时候那种连续几天工程编译不过的痛苦还浮现在脑海中,那种痛苦就是这个东西要是搞不定明天就得离职. 深处这个高风险高收益高失业率的行业上,连续2年以上不换工作已经太难了.这个行业几乎每天晚上12点睡觉早晨9点到公司. 这的确是拿生命来换钱,每次去理发店我都要求理发小哥先帮我剪一下白头发.如果你没做好心里准备的时候我不建议你选择IT行业,开发不是一件小事情,干到中年你就知道啥叫失业. 从百度,金山再到快手,这几年的变化让我不断产生新的想法. __快手是我在北京的最后一家公司,没有哪家能像它一样接地气.__ 就在我写这篇文章的时候,我参与开发的`快手极速版`已经登上了App Store top1. ![](/assets/images/20191231FinalSummary/nebula.webp) 我2019年的年终OKR就是要把快手极速版送上App Store top1的宝座,今天实现了. ### 装修 2019年初 开发商交房之后办理了入户手续,然后就开始准备着手装修.入户的时候心情很激动毕竟花了100w+ 的房子开发商没跑路,也没烂尾. 入户的时候需要缴纳各种费用 2w+多点吧.开发商被我们业主维权维的还算良心,免除供热费和其它零碎的费用小几千块钱. 但是 入户依赖各种问题 我就不说了 买房如果你有钱 就买别墅,没钱就买二手房,买新房除非你财富自由和时间经历都多,否则你会慢慢体会到什么叫`磨练灵魂`. 装修是我女朋友父母帮忙全权搞的,全部都是自己家人做的,不能说多好,毕竟不专业,但是能用就行了 我就这标准. 全都下来 17w多吧 下图是装完最后上理石包口的场景. ![](/assets/images/20191231FinalSummary/decoration1.gif) ![](/assets/images/20191231FinalSummary/decoration2.gif) 下面是装修完最后的一张图 ![](/assets/images/20191231FinalSummary/decoration3.gif) 具体的装修套路我大概了解一二供后续装修的各位参考: 1. 规划设计 2. 改水改电 3. 刷墙固地固(这是一种胶防止墙面腐蚀防潮防碱的东西) 4. 包好下水管道,静音棉捆扎 5. 地面铺砖包口厨房洗手间 瓷砖上墙 6. 集成吊顶 7. 刷大白墙面 8. 买家电 厨房 集成灶还是吸油烟机灶台. 9. 厨房橱柜,卧室木柜等等 木工的活 10.木门定制 11.理石包口、包括厨房灶台板、过门石、地脚线等 12. 收工准备开始买各种家电沙发洗衣机 冰箱电视等等. 基本就是上述套路 不完善哈. ##### 装修需要注意的地方 > 最重要的是理石包口,我家用的比较贵,家里亲戚干的 得10000+左右整体,因为我地脚线都用的天然石,这玩意太贵了 260+/m,不过确实好看,我建议大家把窗口用这种高档的理石包裹上,踢脚线用一些便宜的几十块钱的港石就行了,这样看起来高档还不贵. #### 领我感动的一件事 我在快手上关注一个叫`华图聊装修`的大龙哥,这个用户我们经常聊天,聊快手,作为一个iOS研发我能做的就是把app做好,保证主播和观众能顺畅的使用快手开播或者看直播业务,这是我的工作,这个主播跟我说 如果没有快手 我的装修公司有可能今年就干黄了,感谢快手官方每个月能认识几个老铁来找他装修. 我突然感觉我在的这家公司是有温度的,他拯救了至少我看到 这样一家小的企业. 这里不是打广告 是我觉得他干装修比我上面说的要专业,大家找不找他无所谓,我觉得只要大家想装修的时候不被坑多学习学习这些方面的知识很重要, 他的快手号大家打开快手搜索即可. ### 日本之旅 今年五一以后,直播团队迎来了第一次团建,经过大家投票决定去日本,但是每个人需要自己补贴费用至少5000+以上,由于以前也经历过这样的事情,没有买房,所以当时也没敢报名,现在打算出去看看,长长见识.看看资本主义. 北京首都国际机场直飞日本东京羽田机场 ![](/assets/images/20191231FinalSummary/japen_travel1.webp) > 出乎我意料的是这飞机是沿着朝鲜和韩国的38线飞过去的. 到达东京坐大巴,日本的车牌让我震惊了 ![](/assets/images/20191231FinalSummary/japen_travel2.webp) 简单几个数字就能代表一个车牌. 上车后 我发现一下非常实用的设计 大巴的杯架 ![](/assets/images/20191231FinalSummary/japen_travel4.gif) 不说咱崇洋媚外,就说这玩意为啥国内公交车没有吧!这玩意又没技术含量,这么人性化设计简直太难得了. 东京湾的港口以及工厂基本说明了,日本之所以发达的原因 ![](/assets/images/20191231FinalSummary/japen_travel5.gif) 日本属于英联邦国家,道路左侧通行,一辆辆卡车擦的锃亮. ![](/assets/images/20191231FinalSummary/japen_travel6.webp) 右舵的司机位置, 我只听过 日本司机戴白手套,这回见到了. 我还见到了 这司机把相同颜色的行李箱分组摆放,这点真是不一样,方便色盲的游客快速找到自己的行李箱. 去的第一个景点是`平和公园`,没有拍照可惜了,空气湿度很好. 从随处可见的蝴蝶就能看出来,环境不错. ![](/assets/images/20191231FinalSummary/japen_travel7.gif) 到达`山梨县`温泉酒店 ![](/assets/images/20191231FinalSummary/japen_travel9.webp) 这个小县里的药妆店 ![](/assets/images/20191231FinalSummary/japen_travel10.webp) 让我感到资本主义落后社会主义兴盛的唯一客观特征是`电线杆子`在日本随处可见. 日本山梨县交警队 ![](/assets/images/20191231FinalSummary/japen_travel8.webp) 日本的送餐摩托车 ![](/assets/images/20191231FinalSummary/japen_travel11.webp) 看这整齐的车队,比起国内的送餐车除了不环保以外,其实还是很板正的. 啥时候国内能这样整齐的配备送餐工具车那我的祖国基本已经进入了发达国家的行列.这种车在日本被成为`大脚摩托`.前面还是分体式独立悬挂设计.备箱至少能送超过20人的餐食吧,一箱油200公里. 简直就是发达国家的标配. 日式温泉酒店 ![](/assets/images/20191231FinalSummary/japen_travel12.webp) 被子叠起来就桌子一放就能吃饭 ,省空间 省地方,是挺方便. 第二天去富士山,富士山掠影 ![](/assets/images/20191231FinalSummary/japen_travel13.webp) 回来的路上,看到每家房前屋后都有几罐燃气. ![](/assets/images/20191231FinalSummary/japen_travel14.webp) 日本的居民用车`K-CAR` 它不能超过660cc排量.小到非常实用. ![](/assets/images/20191231FinalSummary/japen_travel15.webp) 忍野八海 清澈的池塘,让我不禁想起 毛爷爷的那句**鹰击长空鱼翔浅底** ![](/assets/images/20191231FinalSummary/japen_travel20.webp) 下午我们抵达东京 去 天皇皇居外面参观 ![](/assets/images/20191231FinalSummary/japen_travel16.webp) > 他叫[楠木正成](https://baike.baidu.com/item/%E6%A5%A0%E6%9C%A8%E6%AD%A3%E6%88%90),相当于日本的诸葛亮. 日本也有类似我们的三国时期被称为[战国时代](https://baike.baidu.com/item/%E6%A5%A0%E6%9C%A8%E6%AD%A3%E6%88%90)1467—1585年或1615年,也就是 丰臣秀吉、织田信长、德川家康 ,最后以德川幕府取得最终的胜利建立今天的日本天皇时代. 行程的第三天我们自由行 来到的 日本东京繁华的接头`新宿` ![](/assets/images/20191231FinalSummary/japen_travel19.webp) 日本的街头 人行横道原来是 横向 竖向 对角都可以. 第三天我们自由行 晚上去了`六本目之丘`,俯瞰东京塔和繁华的东京夜景 ![](/assets/images/20191231FinalSummary/japen_travel18.webp) 最后一天 去东京港 看一看 曾经日本在美国军舰密苏里号上签字宣布无条件投降的东京湾. ![](/assets/images/20191231FinalSummary/japen_travel17.webp) 由于篇幅较长 这里简短的介绍到这里. #### 总结日本之旅-建设好我的祖国 去了一趟顿感深深的被资本主义教育了, 日本的街头鳞次栉比的房舍,整齐的送餐摩托,干净直饮的街头水龙头,污水沟的水清澈见底,房前屋后的清洁燃气,排量不超过 660cc的家用车,垃圾分类桶,东京湾水陆两栖的公交车,新宿街头横向对角的人行横道. 无论哪里都透漏出日本人环保和以实用为原则的生活原则. 非吾崇洋媚外,确实得师夷长技,尝想此处皆受灵魂之考问,我国尚处社会主义初级阶段,日本的移动网络建设不怎么好,互联网发展不如我们,移动支付不如我们,电动汽车不如我们,这些都是今天的日本需要像我们学习的.日本大多数人都认为中国的邓小平的改革开放没有错,这是一个『金钱至上』的社会,日本也一样.我相信在未来10~20年中国的一定能超过日本 经过了这次日本之旅深深地教育了我,一定要建设好我的祖国,要从自己擅长的专业抓手做起,做到极致这就是对国家和人民的贡献.我的旅行不是走马观花,我的照片就是记录历史. ### 生活 今年北漂已经过了第6个年头了,在北京的生活 虽然工资高很多,但是我过的很压抑,这种压抑感前所未有,我可以这样说,我每天除了工作连放假都不知道去哪里,没有一点乐趣,这种状态每天都在持续.业余爱好不提也罢,年终的时候工作几乎忙到回家走路都能睡着.睡不着觉的时候都在考虑这是不是我在 北京的最后一周.北漂不值得,没做好充分的准备不要来,除非你被生活逼的走投无路. 从现在30岁看6年前的自己那肯定是强多了,但要是看5年前、4年前、3年前的自己,没有最差只有更差.我不想成为房奴,希望2020能彻底拜托房奴的身份,贷款目前还剩10w就全部还完了.还得撸起袖子加油干. 今年把户口迁到了哈尔滨.从此脱离农村户口,还是前几年我的年终总结里说的,户口对于我们这样的年轻人简直太重要了,你没农村户口你永远理解不了啥叫小农经济的思想的思想根深蒂固,并且形成一定的思维定势以后你就不想把户口迁移到城市里.城市户口对年轻人买房贷款、子女入学、社会服务体系等太重要了.__小时候在农村生活,总听到什么劳动人民很光荣,农民很光荣,可是我没见哪个城里人回来当农民,农民不是光荣吗?他们怎么不回来做一些光荣的事情呢?长大后明白了他们不可能不光荣,因为他们生活在社会底层,你说他们不光荣那是阶级歧视,可是大家干的事都是城里人的事,谁愿意回去种地.__ 迁移户口的流程大概是这样的 1. 提供身份证 原件+复印件 2. 提供户口 户主页和本人页+户主页复印件+本人页复印件 3. 提供学历证书 原件+复印件 并且提供学历查询二维码也就是去学信网查询的确认单 4. 购房合同原件+复印件 5. 购房发票原件 6. 我的小区是爱达88要去南岗区王岗派出所 7. 找户籍民警 整理材料出具迁移意见 8. 找派出所所长签字 9. 到柜台办理 10. 我顺道把身份证也一起换了地址 > 如果是本省基本不需要准迁证 直接迁移不用回原籍,如果是外省则需要在原籍派出所办理申请迁出手续,然后到想迁入地的派出所办理接收手续.操作很费事还得两地折腾. 整套户口办理完需要2个小时左右,身份证大概需要半个月以上 身份证办理完到我回到北京,把所有用到的各种证件银行卡、支付宝微信、工作居住证、居住证等等全部办理了更新身份信息的操作. 全套办理一圈发现换个户口换个身份证太麻烦了. ### 读书 今天几乎没有太多时间读书了,看的最多的是迈腾维修说明书 买了谭浩强最新出的C程序设计 ![](/assets/images/20191231FinalSummary/cprogram.webp) 买了鸟哥最新出的linux ![](/assets/images/20191231FinalSummary/LinuxNew.webp) 这两本书基本陪我走过了一半的职业生涯,家里装修完了,这些书都要放到书架上珍藏起来. 据说最新版本的linux使用的已经不是centos6了,是centos7操作系统了.有时间把树莓派装上,感觉很棒. ### 买摩托车 2019年北京出了限制性政策,说不符合标准的电动车2年后禁止上路,于是我把我的小牛N1s卖了,换了一辆摩托车代步,为了摩托又现去海淀驾校增驾的摩托车. ![](/assets/images/20191231FinalSummary/motocycle1.webp) ![](/assets/images/20191231FinalSummary/motocycle2.webp) 买个踏板就够用了, 这摩托叫 django 150(姜戈) ,是济南轻骑制造.目前安全骑行3000公里,没有任何问题,价格2w+.买车的时候正好赶上国家执行150cc以下排量的摩托车免税.这辆车希望能骑回哈尔滨.用以纪念北京工作的这几年. 如果你对摩托车不了解可以下载个叫`哈罗摩托`的app看下 就了解了. 为了增驾摩托车,又从科目一考到科目四,从报考到练车再到拿本用时11天.非常到位,不过夏天练摩托车是真热 摩托车(2轮)科目二 练习 ![](/assets/images/20191231FinalSummary/drivin_license_exam1.gif) 摩托车(3轮)科目二 练习 ![](/assets/images/20191231FinalSummary/drivin_license_exam2.gif) ##### 摩托车考试流程 * 科目一理论 1. 100道题 * 科目二 1. 起步 2. 定点停车坡道起步 3. 单边桥 4. 绕桩 * 科目三上路实测 1. 人行横道减速 2. 调头 3. 换档加速 4. 打开右转向灯减速停车 * 科目四理论 1. 100道题 如果是报考三轮摩托的驾驶本后边是D,两轮摩托车是E, D向下兼容,也就是说你考完D本直接就能开2轮摩托. 如果你是C本 比如:C1 当你考完2轮摩托车你的驾驶本会变成 C1E或者C2E, (C1代表手动挡,C2代表自动挡),原来的驾驶本回收重新印发新的驾驶本并且区分E或者D是实习期到多久.基本问题都不大,实习期不能上高速. 其它的A本或者B本不知道,没测试过大家可以网上查查. 关于扣分,当你拥有 C1E的时候 你要倍加小心, 因为你一共拥有12分不区分 C还是E还是D,只要你开车扣分是一样的,拥有一辆车和一辆摩托车和你有两辆汽车是一样的就是12分多一分没有,如果你骑摩托车扣了12分那你汽车就别想开了,赶紧准备考试科目一科目四. 摩托车科目二也不那么容易过,中途比如绕桩、单边桥只要脚点地就算不合格考试失败, 坡道起步最多停留30秒并且脚只允许点一次地面. ### 买汽车 对于一个拿驾照2年多的我还从未尝试正经开过车,没有车练习怕以后用到的时候手足无措,今年没想到经过同学介绍碰到了一个合适的二手车,车况不错.于是坐飞机回去就买了. ![](/assets/images/20191231FinalSummary/magotan1.webp) ![](/assets/images/20191231FinalSummary/magotan2.webp) ![](/assets/images/20191231FinalSummary/magotan3.webp) ![](/assets/images/20191231FinalSummary/magotan4.webp) 他叫`迈腾`B7L,2013年12月左右出厂的第7代迈腾(目前最新出的是迈腾B8.5后边带字母)我接手前 这车实表跑了8w左右.全车4s店保养,没出过大毛病.迈腾2013豪华版2.0TSI,我以10.5w买下. 办理牌照和保险以及保养全部搞完11w左右. 买到手第一件事加装倒车翻标摄像头,也就是大众车标翻盖伸出来一个摄像头用于倒车影像. ![](/assets/images/20191231FinalSummary/magotan5.webp) 开到北京周末闲暇的时候洗洗车打打蜡跟新的一样. ![](/assets/images/20191231FinalSummary/magotan6.webp) 说说买车的过程吧, 原车主一开始以为我不会买他的车也就没当回事,后来发现我连车都不看直接交易这个过程就有点认真了回去直接把全部证件都拿来了,下午4点办理完各种保险 第二天把车一顿保养,上100w的商保,开车回北京,期间感谢同学姚志强,一路办牌照压牌等等手续都板正的帮我整利索了,我这初中同学是在哈尔滨平房一个叫诚杰一汽大众4s店工作,所以在有人办事真快真方便.他在4s店我买车没理由不选择大众.原来的车主小伙送了我一套雪地胎很棒. 这车买回来的一些小毛病 1. 前风挡玻璃被石子打过 我自己在天猫上买了玻璃修复工具,自己动手解决了 2. 后排座椅塑料插口坏了,我在网上话几十块钱买个新的装上了非常好使. 3. 汽车轮胎, 由于原车主送了一套雪地胎,原车的4条邓禄普还能对付5000公里没问题,最近把雪地胎换上了然后把原车的邓禄普卖了200块钱. 4. 换了新的火花塞,机油、空滤以及刹车油、玻璃水. > 其它啥毛病没有,这车的变速箱DSG湿式双离合还在质保期内(10年或者16w公里) 2020年过年回家把发发动机机油尺处渗油的问题解决一下,然后刹车片刹车盘都换换.过完年回来看看买一套新的马牌轮胎换上. 买了这个车虽然在北京不怎么开,但是它带给我很多乐趣,有时间的时候我去擦擦车,买一些汽车养护用品,自己能修理的地方就自己 DIY搞定很有成就感. ### 健康 2019年对30年的牙齿进行了一次大保养,连续跑了20多趟北京口腔医院,终于把牙齿修理的非常板正. 每年体检,有问题及时就医治疗,2019年医疗保险至少为我节省了5000多,今年的医疗花费开支2w+. ### 五年计划 2019年相当于第一个"五年计划"的收尾,由于2018年的五年计划完成70%左右,所以这一年还是沉淀沉淀,静下心来争取100%完成再开始新计划。 2020开始制订新的"五年计划",争取2025年底完成如下: 1. 在哈尔滨或者其它城市买一套小户型的房产一室一厅一卫,带车位 全款结清 2. 投资岳父岳母开个洗车店,争取不让他们出去打工 3. (如果可能)创建一所大学,设立30+个专业并设立实验室,专攻卡中国脖子的35项关键技术!即便不能解决卡脖子技术也要为这些尖端科技培养人才做出努力,一定要建设好我的祖国 [这篇文章](https://mp.weixin.qq.com/s/ms2TbtZyEHYqoQGHQrJY5A)介绍了目前卡中国脖子的34项尖端科技. ##### 光刻机 制造芯片的光刻机,其精度决定了芯片性能的上限。在“十二五”科技成就展览上,中国生产的最好的光刻机,加工精度是90纳米。这相当于2004年上市的奔腾四CPU的水准。而国外已经做到了十几纳米。 光刻机里有两个同步运动的工件台,一个载底片,一个载胶片。两者需始终同步,误差在2纳米以下。两个工作台由静到动,加速度跟导弹发射差不多。在工作时,相当于两架大飞机从起飞到降落,始终齐头并进一架飞机上伸出一把刀,在另一架飞机的米粒上刻字,不能刻坏了。 > 来源:《这些“细节”让中国难望顶级光刻机项背》 (科技日报4月19日) ##### 芯片 低速的光芯片和电芯片已实现国产,但高速的仍全部依赖进口。国外最先进芯片量产精度为10纳米,我国只有28纳米,差距两代。据报道,在计算机系统、通用电子系统、通信设备、内存设备和显示及视频系统中的多个领域中,我国国产芯片占有率为0。 > 来源:《中兴的“芯”病,中国的心病》 ##### 操作系统 普通人看到中国IT业繁荣,认为技术差距不大,实则不然。3家美国公司垄断手机和个人电脑的操作系统。数据显示,2017年安卓系统市场占有率达85.9%,苹果IOS为14%。其他系统仅有0.1%。这0.1%,基本也是美国的微软的Windows和黑莓。没有谷歌铺路,智能手机不会如此普及,而中国手机厂商免费利用安卓的代价,就是随时可能被“断粮”。 > 来源:《丧失先机,没有自研操作系统的大国之痛》 #### 航空发动机短舱 飞机上安放发动机的舱室,俗称“房子”,是航空推进系统最重要的核心部件之一,其成本约占全部发动机的1/4左右。短舱需要将发动机包覆,减少飞行阻力;其进气道还要具有防、除冰的能力;飞行中,要保护发动机不受干扰正常工作;在地面,需要做到方便发动机的维护和维修,一旦短舱有损,飞行中可能会引起发动机严重事故。短舱越大技术难度越高。我国在这一重要领域尚属空白。查阅所有公开资料,我国尚无自主研制短舱的专门机构,相关院校似乎也没有设置相关的学科。 > 来源:《居者无其屋,国产航空发动机的短舱之困》 ##### 触觉传感器 触觉传感器是工业机器人核心部件。精确、稳定的严苛要求,拦住了我国大部分企业向触觉传感器迈进的步伐,目前国内传感器企业大多从事气体、温度等类型传感器的生产。在一个有着100多家企业的行业中,几乎没有传感器制造商进行触觉传感器的生产。日本阵列式传感器能在10厘米×10厘米大小的基质中分布100个敏感元件,售价10万元,而国内产品多为一点式,一般100元一个。 > 来源:《传感器疏察,被愚钝的机器人“国产触觉”》 ##### 真空蒸镀机 OLED面板制程的“心脏”。日本Canon Tokki独占高端市场,掌握着该产业的咽喉。业界对它的年产量预测通常在几台到十几台之间。有钱也买不到,说的就是它。Canon Tokki能把有机发光材料蒸镀到基板上的误差控制在5微米内(1微米相当于头发直径的1%),没有其他公司的蒸镀机能达到这个精准度。目前我国还没有生产蒸镀机的企业,在这个领域我们没什么发言权。 > 来源:《真空蒸镀机匮缺:高端显示屏上的阴影》 ##### 手机射频器件 一块手机的主板上,1/3的空间是射频电路。手机发展趋势是更轻薄,功耗更小,频段更多,带宽更大,这就向射频芯片提出了挑战。射频芯片将数字信号转化成电磁波,4G手机要支持十几个频段,信息带宽几十兆。2018年,射频芯片市场150亿美元;高端市场基本被Skyworks、Qorvo和 博通3家垄断,高通也占一席之地。射频器件的另一个关键元件——滤波器,国内外差距更大。手机使用的高端滤波器,几十亿美元的市场,完全归属Qorvo等国外射频器件巨头。中国是世界最大的手机生产国,但造不了高端的手机射频器件。这需要材料、工艺和设计经验的踏实积累。 > 来源:《射频器件:仰给于人的手机尴尬》 ##### iCLIP技术 iCLIP是一种新兴的实验技术,是研发创新药的最关键的技术之一。它的发明,让人们抛弃精密的观测仪器,也能确定RNA(核糖核酸)和蛋白质在哪个位置“交汇”,甚至可以读出位点“密码”。iCLIP技术难,犹如万千人海中找一个人,要从几十亿个碱基对找到一个或几个确定的结合点,精确度可想而知。国外研究团队已在此领域展开“技术竞赛”,研究论文以几个月为周期轮番上演。国内实验室却极少有成熟经验。 > 来源:《“靶点”难寻,国产创新药很迷惘》 ##### 重型燃气轮机 燃气轮机广泛应用于舰船、火车和大型电站。我国具备轻型燃机自主化能力;但重燃仍基本依赖引进。国际上大的重燃厂家,主要是美国GE、日本三菱、德国西门子、意大利安萨尔多4家。与中国合作都附带苛刻条件:设计技术不转让,核心的热端部件制造技术也不转让,仅以许可证方式许可本土制造非核心部件。没有自主化能力,意味着我国能源安全的重要一环,仍然受制于人,存在被“卡脖子”的风险。 > 来源:《“命门火衰”,重型燃气轮机的叶片之殇》 ##### 激光雷达 激光雷达是个传感器,自带光源,主动发出激光,感知周围环境,像蝙蝠通过超声波定位一样。它是自动驾驶汽车的必备组件,决定着自动驾驶行业的进化水平。但在该领域,国货几乎没有话语权。目前能上路的自动驾驶汽车中,凡涉及激光雷达者,使用的几乎都是美国Velodyne的产品,其激光雷达产品是行业标配,占八成以上市场份额。 > 来源:《激光雷达昏聩,让自动驾驶很纠结》 #### 适航标准 一款航空发动机要想获取一张放飞证,必须经过一套非常严格的“适航”标准体系验证,涵盖设计、制造、验证和管理。但目前在国际上,以FAA和欧洲航空安全局(EASA)的适航审定影响力最大,认可度最高。尽管在规章要求层面,中国与FAA基本一致,但由于国产航空发动机型号匮乏,缺乏实际工程实践经验,使我国适航规章缺少相应的技术支撑。实际型号的适航验证工作,成为被卡在别国空域之外的关隘。 > 来源:《适航标准:国产航发又一道难迈的坎儿》 ##### 高端电容电阻 电容和电阻是电子工业的黄金配角。中国是最大的基础电子元件市场,一年消耗的电阻和电容,数以万亿计。但最好的消费级电容和电阻,来自日本。电容市场一年200多亿美元,电阻也有百亿美元量级。所谓高端的电容电阻,最重要的是同一个批次应该尽量一致。日本这方面做得最好,国内企业差距大。国内企业的产品多属于中低端,在工艺、材料、质量管控上,相对薄弱。 > 来源:《没有这些诀窍,我们够不着高端电容电阻》 #### 核心工业软件 中国的核心工业软件领域,基本还是“无人区”。工业软件缺位,为智能制造带来了麻烦。工业系统复杂到一定程度,就需要以计算机辅助的工业软件来替代人脑计算。譬如,芯片设计生产“必备神器”EDA工业软件,国产EDA与美国主流EDA工具相较,设计原理上并无差异,但软件性能却存在不小差距,主要表现在对先进技术和工艺支持不足,和国外先进EDA工具之间存在“代差”。国外EDA三大巨头公司Cadence、Synopsys及Mentor,占据了全球该行业每年总收入的70%。发展自主工业操作系统+自主工业软件体系,刻不容缓。 > 来源:《核心工业软件:智能制造的中国“无人区”》 ##### ITO靶材 ITO靶材不仅用于制作液晶显示器、平板显示器、等离子显示器、触摸屏、电子纸、有机发光二极管,还用于太阳能电池和抗静电镀膜、EMI屏蔽的透明传导镀膜等,在全球拥有广泛的市场。ITO膜的厚度因功能需求而有不同,一般在30纳米至200纳米。在尺寸的问题上,国内ITO靶材企业一直鲜有突破,而后端的平板显示制造企业也要仰人鼻息。烧结大尺寸ITO靶材,需要有大型的烧结炉。国外可以做宽1200毫米、长近3000毫米的单块靶材,国内只能制造不超过800毫米宽的。产出效率方面,日式装备月产量可达30吨至50吨,我们年产量只有30吨——而进口一台设备价格要花一千万元,这对国内小企业来说无异于天价。 每年我国ITO靶材消耗量超过1千吨,一半左右靠进口,用于生产高端产品。 > 来源:《烧不出大号靶材,平板显示制造仰人鼻息》 #### 核心算法 中国已经连续5年成为世界第一大机器人应用市场,但高端机器人仍然依赖于进口。由于没有掌握核心算法,国产工业机器人稳定性、故障率、易用性等关键指标远不如工业机器人“四大家族”发那科(日本)、ABB(瑞士)、安川(日本)、库卡(德国)的产品。核心算法差距过大,导致国产机器人稳定性不佳,故障率居高不下。算法的差距不只体现在核心控制器上,更拖慢了伺服系统响应的速度。 机器人每完成一个动作,需要核心控制器、伺服驱动器和伺服电机协同作战。对于单台伺服系统,国产机器人动态与静态精度都很高,但高端机器人一般同时有6台以上伺服系统,用传统的控制方法难以取得好的控制效果。 > 来源:《算法不精,国产工业机器人有点“笨”》 ##### 航空钢材 无论起飞还是降落,起落架都是支撑飞机的唯一部件,尤其是在飞机降落阶段,其承载的载荷不仅仅来自机身重量,还有飞机垂直方向的巨大冲力。因此,起落架的材料强度必须十分优异,只能依靠特种钢材才行。目前使用范围最广的是美国的300M钢,该材料采用真空热处理技术,避免了渗氢,零件表面光亮,无氧化脱碳、增碳和晶界氧化等缺陷,提高了表面质量。而国内用于制作起落架的国产超强度钢材有时会出现点状缺陷、硫化物夹杂、粗晶、内部裂纹、热处理渗氢等问题,这些问题都与冶炼过程中纯净度不够有关系。所以我国在高纯度熔炼技术方面与美国还有较大差距,存在很大提升空间。 > 来源:《航空钢材不过硬,国产大飞机起落失据》 ##### 铣刀 随着我国近年来高铁的迅猛建设,钢轨养护问题也愈加让业内专家忧心。若养护不到位,不仅折损生命周期,还存在高风险隐患。我国自主创新研发的双动力电驱铣磨维护机器人装备——被称为钢轨‘急救车’的铣磨车可为钢轨“保驾护航”。但铣磨车最核心部件铣刀仍需从国外进口。铣刀的材料是一种超硬合金材料。对其中金属成分我们已然了解,但就是不知人家是怎么配比、合成的,如同琢磨某种中药的祖传秘方、各种药材比例是多少,都不甚明了。 > 来源:《为高铁钢轨“整容”,国产铣刀难堪重任》 ##### 高端轴承钢 作为机械设备中不可或缺的核心零部件,轴承支撑机械旋转体,降低其摩擦系数,并保证其回转精度。无论飞机、汽车、高铁,还是高精密机床、仪器仪表,都需要轴承。这就对其精度、性能、寿命和可靠性提出了高要求。而我国的制轴工艺已经接近世界顶尖水平,但材质——也就是高端轴承用钢几乎全部依赖进口。 高端轴承用钢的研发、制造与销售基本上被世界轴承巨头美国铁姆肯、瑞典SKF所垄断。前几年,他们分别在山东烟台、济南建立基地,采购中国的低端材质,运用他们的核心技术做成高端轴承,以十倍的价格卖给中国市场。炼钢过程中加入稀土,就能使原本优质的钢变得更加“坚强”。但怎么加,这是世界轴承巨头们的核心秘密。 > 来源:《高端轴承钢,难以补齐的中国制造业短板》 ##### 高压柱塞泵 液压系统是装备制造业的关键部件之一,一切工程领域,凡是有机械设备的场合,都离不开液压系统。高压柱塞泵是高端液压装备的核心元件,被称作液压系统的“心脏”。中国液压工业的规模在2017年已经成为世界第二,但产业大而不强,尤其是额定压力35MPa以上高压柱塞泵,90%以上依赖进口。国内生产的液压柱塞泵与外国品牌相比,在技术先进性、工作可靠性、使用寿命、变量机构控制功能和动静态性能指标上都有较大差距,基本相当于国外上世纪90年代初水平。 > 来源:《高压柱塞泵,鲠在中国装备制造业咽喉的一根刺》 ##### 航空设计软件 自上世纪80年代后,世界航空业就迈入数字化设计的新阶段,现在已经达到离开软件就无法设计的高度依赖程度。设计一架飞机至少需要十几种专业软件,全是欧美国家产品。国内设计单位不仅要投入巨资购买软件,而且头戴钢圈,一旦被念“紧箍咒”,整个航空产业将陷入瘫痪。据媒体报道,设计歼-10飞机时,主起落架主承力结构的整个金属部件是委托国外制造。但造完之后,起落架的收放出现问题,有5毫米的误差,只好重新订货制造。仅仅是这一点点的误差,影响了歼-10首飞推迟了八九个月。没有全数字化的软件支撑,任何一点细微的误差,都可能成为制造业的梦魇。 > 来源:《航空软件困窘,国产飞机设计戴上“紧箍咒”》 ##### 光刻胶 我国虽然已成为世界半导体生产大国,但面板产业整体产业链仍较为落后。目前,LCD用光刻胶几乎全部依赖进口,核心技术至今被TOK、JSR、住友化学、信越化学等日本企业所垄断。就拿在国际上具有一定竞争实力的京东方来说,目前已建立17个面板显示生产基地,其中有16个已经投产。但京东方用于高端面板的光刻胶,仍然由国外企业提供。光刻胶主要成分有高分子树脂、色浆、单体、感光引发剂、溶剂以及添加剂,开发所涉及的技术难题众多,需从低聚物结构设计和筛选、合成工艺的确定和优化、活性单体的筛选和控制、色浆细度控制和稳定、产品配方设计和优化、产品生产工艺优化和稳定、最终使用条件匹配和宽容度调整等方面进行调整。因此,要自主研发生产,技术难度非常之高。 来源:《中国半导体产业因光刻胶失色》 ##### 高压共轨系统 电控柴油高压共轨系统相当于柴油发动机的“心脏”和“大脑”,其品质的好坏,严重影响发动机的使用。柴油机产业是推动一个国家经济增长、社会运行的重要装备基础。中国是全球柴油发动机的主要市场和生产国家,而在国内的电控柴油机高压共轨系统市场,德国、美国和日本等企业占据了绝大份额。和国外先进公司的产品相比,国产高压共轨系统在性能、功能、质量及一致性上还存在一定的差距,成本上的优势也不明显。 > 来源:《高压共轨不中用,国产柴油机很受伤》 ##### 透射式电镜 冷冻电镜可以拍摄微观结构高清3d“彩照”,是生命科学研究的利器,透射式电镜的生产能力是冷冻电镜制造能力的基础之一。目前世界上生产透射电镜的厂商只有3家,分别是日本电子、日立、FEI,国内没有一家企业生产透射式电镜。匹配冷冻电镜使用的工具都需要原装,零件坏了找不到人修理,只能等待零件邮寄到货后进行更换。对于中国的冷冻电镜使用者们来说,这样的体验可能还要持续不短的时间。 > 来源:《我们的蛋白质3D高清照片仰赖舶来的透射式电镜》 ##### 掘进机主轴承 主轴承,有全断面隧道掘进机的“心脏”之称,承担着掘进机运转过程的主要载荷,是刀盘驱动系统的关键部件,工作所处状况十分恶劣。与直径仅有几百毫米的传统滚动轴承相比,掘进机主轴承直径一般为几米,是结构最复杂的一种轴承,制造需要上百道工序。就掘进机整机制造能力而言,国产掘进机已接近世界最先进水平,但最关键的主轴承全部依赖进口。德国的罗特艾德、IMO、FAG和瑞典的SKF占据市场。 > 来源:《自家的掘进机却不得不用别人的主轴承》 ##### 微球 微球,直径是头发粗细的三十分之一。手机屏幕里,每平方毫米要用一百个微球,撑起了两块玻璃面板,相当于骨架,在两块玻璃面板的缝隙里,再灌进液晶。少了它,你正盯着的液晶屏幕将无法生产。没有微球,芯片生产、食品安全检测、疾病诊断、生物制药、环境监测……许多行业都会陷入窘境。仅微电子领域,中国每年就要进口价值几百亿元人民币的微球。2017年中国大陆的液晶面板出货量达到全球的33%,产业规模约千亿美元,位居全球第一。但这面板中的关键材料——间隔物微球,以及导电金球,全世界只有日本一两家公司可以提供。这些材料也像芯片一样,给人卡住了脖子。 > 来源:《微球:民族工业不能承受之轻》 ##### 水下连接器 除了船舶、遥感卫星,海底观测网已成为第三种海洋观测平台——通过它,人类可以深入到水下观测和认识海洋。如果将各类缆系观测平台比作胳膊、腿,水下连接器就好比关节,对海底观测网系统的建设、运行和维护有着不可替代的作用。目前我国水下连接器市场基本被外国垄断。一旦该连接器成为禁运品,整个海底观测网的建设和运行将被迫中断。 > 来源:《水下连接缺国产利器,海底观测网傍人篱壁》 ##### 燃料电池关键材料 国外的燃料电池车已实现量产,但我国车用燃料电池还处在技术验证阶段。我国车用燃料电池的现状是——几乎无部件生产商,无车用电堆生产公司,只有极少量商业运行燃料电池车。多项关键材料,决定着燃料电池的寿命和性能。这些材料我国并非完全没有,有些实验室成果甚至已达到国际水平。但是,没有批量生产线,燃料电池产业链依然梗阻。关键材料长期依赖国外,一旦遭遇禁售,我国的燃料电池产业便没有了基础支撑。 > 来源:《少了三种关键材料,燃料电池商业化难成文章》 ##### 高端焊接电源 我国是海洋大国,拥有300多万平方公里海域,正在大力发展高端海洋资源开发和海洋维权装备。海里的设备一旦出现开裂等故障,需要用有工业制造“缝纫机”之称的焊接装备修补。深海焊接的实现靠水下机器人。虽然我国是全球最大焊接电源制造基地,年产能已超1000万台套,但高端焊接电源基本上仍被国外垄断。我国水下机器人焊接技术一直难以提升,原因是高端焊接电源技术受制于人。国外焊接电源全数字化控制技术已相对成熟, 国内的仍以模拟控制技术为主。 > 来源:《国产焊接电源“哑火”,机器人水下作业有心无力》 #### 锂电池隔膜 作为新能源车的“心脏”,国产锂离子电池(以下简称锂电池)目前“跳”得还不够稳。电池四大核心材料中,正负极材料、电解液都已实现了国产化,唯独隔膜仍是短板。高端隔膜技术具有相当高的门槛,不仅要投入巨额的资金,还需要有强大的研发和生产团队、纯熟的工艺技术和高水平的生产线。高端隔膜目前依然大量依赖进口。 > 来源:《一层隔膜两重天:国产锂电池尚需拨云见日》 ##### 医学影像设备元器件 目前国产医学影像设备的大部分元器件依赖进口,至少要花10年、20年才能达到别人的现有水平。在传统医学成像(CT、磁共振等)上,中国最早的专利比美国平均晚20年。在专利数量上,美国是我国的10倍。这意味着整个产业已经完全掌握在国外企业的手里了,所有的知识产权,所有的原创成果,所有的科研积累都在国外,中国只占很少的一部分。 > 来源:《拙钝的探测器模糊了医学影像》 ##### 超精密抛光工艺 超精密抛光工艺在现代制造业中有多重要,其应用的领域能够直接说明问题:集成电路制造、医疗器械、汽车配件、数码配件、精密模具、航空航天。“它是技术灵魂”。美日牢牢把握了全球市场的主动权,其材料构成和制作工艺一直是个谜。换言之,购买和使用他们的产品,并不代表可以仿制甚至复制他们的产品。 > 来源:《通往超精密抛光工艺之巅,路阻且长》 ##### 环氧树脂 碳纤维质量能比金属铝轻,但强度却高于钢铁,还具有耐高温、耐腐蚀、耐疲劳、抗蠕变等特性,其中一个关键的复合辅材就是环氧树脂。但目前国内生产的高端碳纤维,所使用的环氧树脂全部都是进口的。目前,我国已能生产T800等较高端的碳纤维,但日本东丽掌握这一技术的时间是上世纪90年代。相比于碳纤维,我国高端环氧树脂产业落后于国际的情况更为严重。 > 来源:《环氧树脂韧性不足,国产碳纤维缺股劲儿》 ##### 高强度不锈钢 用于火箭发动机的钢材需具备多种特性,其中高强度是必须满足的重要指标。然而,不锈钢的强度和防锈性能,却是鱼和熊掌般难以兼得的矛盾体。火箭发动机材料如果如果严重生锈,将带来很大影响。完全依靠材料自身实现高强度和防锈性能兼备,这是世界性难题。现在,我国航天材料大多用的是国外上世纪六七十年代用的材料,发达国家在生产过程中会严格控制杂质含量,如果纯度不达标,便重新回炉,但国内厂家往往缺乏这种严谨的态度。 > 来源:《去不掉的火箭发动机“锈疾”》 ##### 数据库管理系统 目前全世界最流行的两种数据库管理系统是Oracle和MySQL,都是甲骨文公司旗下的产品。竞争者还有IBM公司以及微软公司的产品等。甲骨文、IBM、微软和Teradata几家美国公司,占了大部分市场份额。数据库管理系统国货也有市场份额,但只是个零头,其稳定性、性能都无法让市场信服,银行、电信、电力等要求极端稳妥的企业,不会考虑国货。 > 来源:《数据库管理系统:中国还在寻找“正确打开方式”》 ##### 扫描电镜 扫描电子显微镜,一种高端的电子光学仪器,它被广泛地应用于材料、生物、医学、冶金、化学和半导体等各个研究领域和工业部门,被称为“微观相机”目前我国科研与工业部门所用的扫描电镜严重依赖进口,每年我国花费超过1亿美元采购的几百台扫描电镜中,主要产自美、日、德和捷克等国。国产扫描电镜只占约5%—10%。 > 来源:《扫描电镜“弱视”,工业制造难以明察秋毫》 __也许习大大看完我的第二个五年计划也会潸然泪下.__ 我觉得相比许家印富豪造车,不如他投资一下我开个大学干我这个想法,比他造车更有意义,这关乎于国家前途的命运。然而我还是个不能解决温饱的工人阶级谈这个有点纸上谈兵人微言轻。 其实我清楚的知道第3个计划有点扯淡,但我不能没有梦想.万一实现了呢. 我认为我们总听到身边人说中国这不如外国那不如外国,但具体哪里不如外国我们一时半会还说不清楚.看了这34+项卡脖子技术就知道了我们国家的短板在哪,我们这一代人所需要努力的地方在哪里.我们最大的悲剧是不知自己和其它强国的具体差距到底在哪里,我相信看完[这篇文章](https://mp.weixin.qq.com/s/ms2TbtZyEHYqoQGHQrJY5A)你将有所收获. ### 总结 每当写年终总结的时候别人都会给予我羡慕的目光,我想说我只是挣扎在温饱线上勉强度日.在没有`对价`的条件下只能直面惨淡的人生.2020年得继续奋斗.不但要为了国家和人民对美好生活的向往,还要为了自己诗和远方. 年华尚在,岁月尚好. URL: https://sunyazhou.com/2019/12/Chisel/index.html.md Published At: 2019-12-06 11:09:25 +0000 # 简单了解LLDB调试工具chisel ![](/assets/images/20191206Chisel/lldb1.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! 最近比较懒 Xcdeo每次启动的时候都会报一个找不到某某python文件的log在lldb中,为了一探究竟,花了点时间简单修复了 # chisel是啥? 这个问题我要是回答完都会被iOS开发者笑话死,这个工具facebook都开源很久了,只是自己以前研究过,换个电脑懒得布置环境.一直没当回事,这几天开发遇到了控制台问题顺手看到了,想来整理一下. 首先这个工具的的地址在[https://github.com/facebook/chisel](https://github.com/facebook/chisel) 这里面提供的安装方法如下: ``` sh brew update brew install chisel ``` > 通过 brew 安装完就行了 然后它会告诉你看下用户home目录有个隐藏文件 `.lldbinit`如果没有就创建一下 ``` sh touch .lldbinit open .lldbinit ``` ![](/assets/images/20191206Chisel/lldbinit.webp) 创建完 把脚本命令搞进去 这里我就不说官方的写法了 跟屎一样一点不好使 我贴一下我的添加方法. ![](/assets/images/20191206Chisel/lldb2.webp) 这是我用本地电脑安装完成之后的路径图 ``` sh ### Chisel LLDB add by sunyazhou command script import /usr/local/Cellar/chisel/1.8.1/libexec/fblldb.py ``` 本地的 .lldbinit 文件中加入上代码 记得必须对应上路径.然后重启Xcode运行即可在控制台使用无问题 由于这个文件会被Reveal改动 所以经常会有各种lldb工具修改这个文件 ``` sh ### Chisel LLDB add by sunyazhou command script import /usr/local/Cellar/chisel/1.8.1/libexec/fblldb.py ### ### LLDB https://github.com/DerekSelander/LLDB command script import /Users/sunyazhou/LLDB-master/lldb_commands/dslldb.py ### ### Reveal LLDB commands support - DO NOT MODIFY command script import /Users/sunyazhou/Library/Application\ Support/Reveal/RevealServerCommands.py ### ``` ## chisel用法 * `pvc`:查看当前控制器状态 * `pviews`:查看UIWindow及其子视图层级关系 * `presponder`:打印一个对象的响应链关系 * `pclass`:根据内存地址打印相关信息 * `visualize`:使用mac系统preview程序查看UIImage、CGImage、UIView、CALayer、NSData(of an UIImage)、UIColor、CIColor。 * `show`/`hide`:显示or隐藏一个UIView * `mask`/`umask`:给一个UIView或CALayer添加一个半透明蒙版 * `border`/`unborder`:给指定的UIView或CALayer添加边框或移除边框用于调试,记得执行后紧接着执行caflush * `caflush`:刷新界面UI,类似于前面介绍的flush * `bmessage`:添加一个断点,即使这个函数在子类没有实现(比如说在UIViewController中想在viewWillAppear中打断点,但是很可能没有实现父类方法,就可以通过bmessage [UIViewController viewWillAppear:]添加) * `wivar`:相当于kvo,监听一个变量,例如wivar self _subviews * `taplog`:开启点击log功能,当点击某个控件时会打印相关控件的信息 * `paltrace`: 打印指定view的自动布局信息,比如:paltrace self.view * `ptv`:打印当前界面中的UITableView,相对应的还有pcells打印当前界面中的UITableViewCell * `pdata`:Data的string解码 * `vs`:搜索指定的view并加上半透明蒙版(包含子命令),例如:vs 0x13a9efe00 就可以标注出对应的控件 * `slowanim`/`unslowanim`:降低(或取消)动画速度,默认0.1 ,可以在任意断点或Xcode暂定执行slowanim即可,方便动画调试 演示效果 ![](/assets/images/20191206Chisel/lldb3_chisel.webp) ## 其它调试lldb有哪些? [lldb_commands](https://github.com/DerekSelander/LLDB) 是另一个第三方的lldb扩展库,其中提供了很多实用的文件操作 `ls`:显示指定路径的目录或文件列表 `pexecutable`:打印当前可执行文件所在位置 `dumpenv`:查看环境信息,比如说沙盒地址 `yoink`:拷贝指定目录的文件到mac的临时目录 `keychain`:查看keychain信息 # 总结 工欲善其事 必先利其器,作为一个iOS开发如果想做到极致就需要我们好的工具都需要试试,这样能不断提高生产效率 [参考 iOS开发调试概览](https://www.cnblogs.com/kenshincui/p/11953536.html) URL: https://sunyazhou.com/2019/12/MathGraphicTool/index.html.md Published At: 2019-12-04 18:12:58 +0000 # 图形示意绘制工具 ![](/assets/images/20191204MathGraphicTool/MathGraphic.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! 以前经常写一些动画需要描述示意图形的运动图.这样能生动形象的表述我所讲述的内容到底是什么样的 最近发现了一个网站,专门做数学图形绘制 # [https://www.geogebra.org/](https://www.geogebra.org/) 这里数学上用的东西都是可以直接拿来使用的,比如画个坐标系 表述一下 动画运动轨迹等 ![](/assets/images/20191204MathGraphicTool/MathGraphicOverView.gif) URL: https://sunyazhou.com/2019/09/MasonryPanViewDemo/index.html.md Published At: 2019-09-26 20:05:20 +0000 # 使用Masonry约束实现简单的高级拖拽视图 ![](/assets/images/20190926MasonryPanViewDemo/panviewdemo.gif) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! ## 背景 最近开发遇到一个上图的需求,做一个挂件能四处拖动并且上边还时不时的展示一个tips气泡的`Label`,在尽量使用少的代码来实现这个功能,作为我手一名iOS开发人员必须严格考究这个需求,显然这有点麻烦,一贯懒惰我的实在不想计算哪个边哪个角甚至滑动到哪里的`frame`,计算frame这既听起来可笑又觉的没什么技术含量. 为了让代码量少并且能满足需求,我选择使用Masonry来实现这个功能 ## 开搞 首先我搞起之前我建议大家看下[土土哥](http://tutuge.me/)的[有趣的Autolayout示例1~5Masonry实现文章](http://tutuge.me/tags/Masonry/),本文也是参考土土哥的文章学习写出的,见笑了各位,个人认为土土哥的文章简直就是Masonry自动布局的样板教程,强烈建议入门的小伙伴或者高手经常复写. 下面的图是土土哥实现的demo ![](/assets/images/20190926MasonryPanViewDemo/tutugeMasonry1.gif) ![](/assets/images/20190926MasonryPanViewDemo/tutugeMasonry2.gif) 但我的问题是怎么保证那个tip的气泡label左右拖拽能辗转腾挪的允许logo图像之间有`旷量移动` ## 代码实现旷量移动 首先我们创建一个demo,很简单VC的demo就行 创建相关绿色背景视图和图像imageView以及tipLabel的气泡视图,具体代码我贴了出来,我就不啰嗦如何创建其它视图了xib拖拽一下就行了. ``` objc #import "ViewController.h" #import @interface ViewController () @property (weak, nonatomic ) IBOutlet UIView *greenView; @property (weak, nonatomic ) IBOutlet UIImageView *widgetView; @property (weak, nonatomic ) IBOutlet UILabel *bubbleTitleLabel; @property (nonatomic, strong) MASConstraint *leftConstraint; //左侧约束变量 @property (nonatomic, strong) MASConstraint *topConstraint; //顶部约束变量 @end ``` 这里可以看到有两个`leftConstraint `和`topConstraint `的约束全局变量,这两个就是实现拖拽的时候改变约束的偏移量来实现的.具体代码如下 ``` objc CGFloat screenWidth = [UIScreen mainScreen].bounds.size.width; CGFloat screenHeight = [UIScreen mainScreen].bounds.size.height; [self.widgetView mas_makeConstraints:^(MASConstraintMaker *make) { // 设置边界条件约束,保证内容可见,优先级1000 make.left.greaterThanOrEqualTo(self.greenView.mas_left); make.right.lessThanOrEqualTo(self.greenView.mas_right); make.top.greaterThanOrEqualTo(self.greenView.mas_top); make.bottom.lessThanOrEqualTo(self.greenView.mas_bottom); self.leftConstraint = make.centerX.equalTo(self.greenView.mas_left).with.offset(screenWidth - 20).priorityHigh(); // 优先级要比边界条件低 self.topConstraint = make.centerY.equalTo(self.greenView.mas_top).with.offset(screenHeight - 100).priorityHigh(); // 优先级要比边界条件低 make.width.height.mas_equalTo(@100); }]; ``` 上边的`greaterThanOrEqualTo`和`lessThanOrEqualTo`都是限制挂件的可滑动范围,而最后的`make.centerX/Y.equalTo`是限制挂件的默认位置,我让它默认在右下角,所以通过偏移量移动过去 > 注意:这里有个坑就是因为这个东西能四处滑动 所以基本需要锁定`left`和`top`,我发现只有通过offset移动才能确定最初位置,如果equalTo直接写成xxxview的bottom或者right是滑动不了的,仔细思考一下masonry就知道为啥了. 然后添加手势并实现相关滑动事件即可实现滑动 ``` objc UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panWithGesture:)]; [self.greenView addGestureRecognizer:pan]; ... - (void)panWithGesture:(UIPanGestureRecognizer *)pan { CGPoint touchPoint = [pan locationInView:self.greenView]; self.leftConstraint.offset = touchPoint.x; self.topConstraint.offset = touchPoint.y; } ``` #### 旷量Label的约束 ``` objc [self.bubbleTitleLabel mas_remakeConstraints:^(MASConstraintMaker *make) { make.height.equalTo(@26); make.bottom.equalTo(self.widgetView.mas_top); make.left.greaterThanOrEqualTo(self.greenView.mas_left).offset(0); make.right.lessThanOrEqualTo(self.greenView.mas_right).offset(0); make.centerX.lessThanOrEqualTo(self.widgetView.mas_right).offset(10); make.centerX.greaterThanOrEqualTo(self.widgetView.mas_left).offset(-10); }]; ``` 想要实现旷量移动必须增加更多的约束限制 这里就增加了 ``` objc make.centerX.lessThanOrEqualTo(self.widgetView.mas_right).offset(10); make.centerX.greaterThanOrEqualTo(self.widgetView.mas_left).offset(-10); ``` 这样就实现了 左右超过滑动便宜还依然控制着tip的label左右移动范围. # 总结 经过工作中遇到的问题实例,学习了一些Masonry的技巧,希望和大家分享,demo我已放到下面 喜欢自行下载学习. [demo下载](https://github.com/sunyazhou13/PanViewDemo) URL: https://sunyazhou.com/2019/09/UITraitCollection/index.html.md Published At: 2019-09-16 18:49:05 +0000 # UITraitCollection详解 ![](/assets/images/20190916UITraitCollection/UITraitCollection1.webp) # 前言 本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! ## 先说问题 最近在适配iOS13 有个`Dark Mode`的暗黑模式, 为了适配这个模式不得不在UIView和UIViewController以及UIWindow中复写如下方法 ``` objc - (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection { [super traitCollectionDidChange:previousTraitCollection]; } ``` 这里有个`UITraitCollection`的类以前从来没有仔细研究,今天详细研究一下. > Trait 特性 特点 显然 这个类是一个UIKit中用于处理苹果手机的一些特性的储存与UI相关的配置, 大家有没有想过如果你在iOS修改通用中的某些系统设置,比如(下图) 对比度、全局字体大小,这个我们开发人员怎么处理. ![](/assets/images/20190916UITraitCollection/UITraitCollection2.gif) 这些系统的特性修改就放到这个`UITraitCollection`中,这个类也就是我们经常在VC和View中经常用到而大家往往容易忽略的,下面简单记录一下这些特性都有哪些 ## UITraitCollection API介绍 #### 判断当前设备时 iPhone/iPad/tv/carPlay 的配置 ``` objc + (UITraitCollection *)traitCollectionWithUserInterfaceIdiom:(UIUserInterfaceIdiom)idiom; @property (nonatomic, readonly) UIUserInterfaceIdiom userInterfaceIdiom; // unspecified: UIUserInterfaceIdiomUnspecified ``` #### 关于布局方向的配置 ``` objc + (UITraitCollection *)traitCollectionWithLayoutDirection:(UITraitEnvironmentLayoutDirection)layoutDirection API_AVAILABLE(ios(10.0)); @property (nonatomic, readonly) UITraitEnvironmentLayoutDirection layoutDirection API_AVAILABLE(ios(10.0)); // unspecified: UITraitEnvironmentLayoutDirectionUnspecified ``` #### 图片 Scale 的配置 ``` objc + (UITraitCollection *)traitCollectionWithDisplayScale:(CGFloat)scale; @property (nonatomic, readonly) CGFloat displayScale; // unspecified: 0.0 ``` #### 布局 Size Class 的配置 ``` objc + (UITraitCollection *)traitCollectionWithHorizontalSizeClass:(UIUserInterfaceSizeClass)horizontalSizeClass; @property (nonatomic, readonly) UIUserInterfaceSizeClass horizontalSizeClass; // unspecified: UIUserInterfaceSizeClassUnspecified + (UITraitCollection *)traitCollectionWithVerticalSizeClass:(UIUserInterfaceSizeClass)verticalSizeClass; @property (nonatomic, readonly) UIUserInterfaceSizeClass verticalSizeClass; // unspecified: UIUserInterfaceSizeClassUnspecified ``` #### Force Touch 是否可用的配置 ``` objc + (UITraitCollection *)traitCollectionWithForceTouchCapability:(UIForceTouchCapability)capability API_AVAILABLE(ios(9.0)); @property (nonatomic, readonly) UIForceTouchCapability forceTouchCapability API_AVAILABLE(ios(9.0)); // unspecified: UIForceTouchCapabilityUnknown ``` #### 全局字体大小的配置 ``` objc + (UITraitCollection *)traitCollectionWithPreferredContentSizeCategory:(UIContentSizeCategory)preferredContentSizeCategory API_AVAILABLE(ios(10.0)); @property (nonatomic, copy, readonly) UIContentSizeCategory preferredContentSizeCategory API_AVAILABLE(ios(10.0)); // unspecified: UIContentSizeCategoryUnspecified ``` #### 色域的配置 ``` objc + (UITraitCollection *)traitCollectionWithDisplayGamut:(UIDisplayGamut)displayGamut API_AVAILABLE(ios(10.0)); @property (nonatomic, readonly) UIDisplayGamut displayGamut API_AVAILABLE(ios(10.0)); // unspecified: UIDisplayGamutUnspecified ``` #### 是否开启高对比度的配置 ``` objc + (UITraitCollection *)traitCollectionWithAccessibilityContrast:(UIAccessibilityContrast)accessibilityContrast API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos); @property (nonatomic, readonly) UIAccessibilityContrast accessibilityContrast API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos); // unspecified: UIAccessibilityContrastUnspecified ``` #### 全局字重的配置 ``` objc + (UITraitCollection *)traitCollectionWithLegibilityWeight:(UILegibilityWeight)legibilityWeight API_AVAILABLE(ios(13.0), tvos(13.0), watchos(6.0)); @property (nonatomic, readonly) UILegibilityWeight legibilityWeight API_AVAILABLE(ios(13.0), tvos(13.0), watchos(6.0)); // unspecified: UILegibilityWeightUnspecified ``` #### 主题的配置 ``` objc + (UITraitCollection *)traitCollectionWithUserInterfaceStyle:(UIUserInterfaceStyle)userInterfaceStyle API_AVAILABLE(tvos(10.0)) API_AVAILABLE(ios(12.0)) API_UNAVAILABLE(watchos); @property (nonatomic, readonly) UIUserInterfaceStyle userInterfaceStyle API_AVAILABLE(tvos(10.0)) API_AVAILABLE(ios(12.0)) API_UNAVAILABLE(watchos); // unspecified: UIUserInterfaceStyleUnspecified ``` ### 如何获取 UITraitCollection `UITraitCollection`本身是一个配置的集合,每个 `UIView`/`UIViewController`都有自己的 `UITraitCollection`对象,并将自己的`UITraitCollection`传递给子`UIView`/`UIViewController`作为默认值。 * 可以通过 `UIView`/`UIViewController`的属性 `traitCollection` 获取到当前视图的 UITraitCollection 对象 ``` objc - (void)viewDidLoad { [super viewDidLoad]; self.traitCollection //拿到当前得 } ``` * 可以通过子类重写如下方法的方式监控 traitCollection 属性的变化 ``` objc - (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection { [super traitCollectionDidChange:previousTraitCollection]; } ``` * 获取全局的`UITraitCollection` ``` objc [UITraitCollection currentTraitCollection]; ``` ## 技巧 如果要在UIViewController中更新状态栏 当设置完style的时候可以调用 ``` objc [self setNeedsStatusBarAppearanceUpdate]; ``` # 总结 通过简单的学习`UITraitCollection`又深刻学习了一下这个类,希望以后能记录一下学过的知识. URL: https://sunyazhou.com/2019/09/OpenGLESDemo1/index.html.md Published At: 2019-09-06 10:19:48 +0000 # 从零学习OpenGLES的纹理渲染 ![](/assets/images/20190906OpenGLESDemo1/sunyazhou_logo_glsl.webp) # 前言 很久没用OpenGL了 记录一下学习GLSL代码和实现 ## Objctive-C代码 ``` objc #import "ViewController.h" #import //顶点结构体类型 typedef struct { GLKVector3 positionCoord; // (x,y,z) GLKMatrix2 textureCoord; // (u, v) } SenceVertex; @interface ViewController () @property (nonatomic, assign) SenceVertex *vertices; //顶点数组 @property (nonatomic, strong) EAGLContext *context; @end @implementation ViewController #pragma mark - #pragma mark - override methods 复写方法 - (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = [UIColor whiteColor]; [self commonInit]; } #pragma mark - #pragma mark - private methods 私有方法 - (void)commonInit { // 创建上下文 使用 2.0版本 self.context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2]; [EAGLContext setCurrentContext:self.context]; //创建顶点数组 self.vertices = malloc(sizeof(SenceVertex) * 4); //4个顶点 {% raw %} self.vertices[0] = (SenceVertex){{-1, 1, 0},{ 0, 1 }}; //左上角 self.vertices[1] = (SenceVertex){{-1, -1, 0},{0 ,0}}; //左下角 self.vertices[2] = (SenceVertex){{1, 1, 0},{1, 1}}; //右上角 self.vertices[3] = (SenceVertex){{1, -1, 0},{1, 0}}; //右下角 {% endraw %} //创建一个展示纹理的layer CAEAGLLayer *layer = [CAEAGLLayer layer]; layer.frame = CGRectMake(0, 100, self.view.frame.size.width, self.view.frame.size.width); layer.contentsScale = [[UIScreen mainScreen] scale]; //设置缩放比例,不设置的话,纹理会失真 [self.view.layer addSublayer:layer]; // 绑定纹理到输出layer [self bindRenderLayer:layer]; // 读取纹理 NSString *imagePath = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:@"logo.webp"]; UIImage *image = [UIImage imageWithContentsOfFile:imagePath]; GLuint textureID = [self createTextureWithImage:image]; // 设置视口尺寸 glViewport(0, 0, self.drawableWidth, self.drawableHeight); // 编译链接 着色器 GLuint program = [self programWithShaderName:@"glsl"]; glUseProgram(program); // 获取shader 中的参数 然后传数据进去 GLuint positionSlot = glGetAttribLocation(program, "Position"); //获取顶点着色器的位置 GLuint textureCoordsSlot = glGetAttribLocation(program, "TextureCoords"); //获取顶点着色器中的纹理坐标 GLuint textureSlot = glGetUniformLocation(program, "Texture"); //获取片元着色器纹理变量 //将纹理 ID 传给着色器程序 glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, textureID); glUniform1i(textureSlot, 0); // 将textureSlot 赋值为 0, 而 0 与 GL_TEXTURE0 对应,这里如果写1,就是GL_TEXTURE1 // 创建顶点缓存 GLuint vertexBuffer; glGenBuffers(1, &vertexBuffer); glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer); GLsizeiptr bufferSizeBytes = sizeof(SenceVertex) * 4; glBufferData(GL_ARRAY_BUFFER, bufferSizeBytes, self.vertices, GL_STATIC_DRAW); // 设置顶点数据 glEnableVertexAttribArray(positionSlot); glVertexAttribPointer(positionSlot, 3, GL_FLOAT, GL_FALSE, sizeof(SenceVertex), NULL + offsetof(SenceVertex, positionCoord)); // 设置纹理数据 glEnableVertexAttribArray(textureCoordsSlot); glVertexAttribPointer(textureCoordsSlot, 2, GL_FLOAT, GL_FALSE, sizeof(SenceVertex), NULL + offsetof(SenceVertex, textureCoord)); // 开始绘制 glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); // 将绑定 [self.context presentRenderbuffer:GL_RENDERBUFFER]; //删除顶点缓存 glDeleteBuffers(1, &vertexBuffer); vertexBuffer = 0; } //绑定图像要输出的 layer - (void)bindRenderLayer:(CALayer *)layer { GLuint frameBuffer; //帧缓冲 GLuint renderBuffer; //渲染缓冲 //绑定渲染缓冲到 输出的layer glGenRenderbuffers(1, &renderBuffer); glBindRenderbuffer(GL_RENDERBUFFER, renderBuffer); [self.context renderbufferStorage:GL_RENDERBUFFER fromDrawable:layer]; //将渲染缓冲附着在帧缓冲上 glGenFramebuffers(1, &frameBuffer); glBindFramebuffer(GL_FRAMEBUFFER, frameBuffer); glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderBuffer); } // 通过一个图片 创建纹理 - (GLuint)createTextureWithImage:(UIImage *)image { // 将 UIImage 转换为 CGImageRef CGImageRef cgImageRef = [image CGImage]; GLuint width = (GLuint)CGImageGetWidth(cgImageRef); GLuint height = (GLuint)CGImageGetHeight(cgImageRef); CGRect rect = CGRectMake(0, 0, width, height); // 绘制图片 CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); void *imageData = malloc(width * height * 4); CGContextRef context = CGBitmapContextCreate(imageData, width, height, 8, width * 4, colorSpace, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big); CGContextTranslateCTM(context, 0, height); CGContextScaleCTM(context, 1.0f, -1.0f); CGColorSpaceRelease(colorSpace); CGContextClearRect(context, rect); CGContextDrawImage(context, rect, cgImageRef); // 生成纹理 GLuint textureID; glGenTextures(1, &textureID); glBindTexture(GL_TEXTURE_2D, textureID); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, imageData); // 将图片数据写入纹理缓存 // 设置如何把纹素映射成像素 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); // 解绑 glBindTexture(GL_TEXTURE_2D, 0); // 释放内存 CGContextRelease(context); free(imageData); return textureID; } // 将一个顶点着色器和片元着色器挂在到一个着色器程序上, 并返回程序的 id - (GLuint)programWithShaderName:(NSString *)shaderName { // 编译两个着色器 GLuint vertexShader = [self compileShaderWithName:shaderName type:GL_VERTEX_SHADER]; GLuint fragmentShader = [self compileShaderWithName:shaderName type:GL_FRAGMENT_SHADER]; // 挂载 shader 到 program 上 GLuint program = glCreateProgram(); glAttachShader(program, vertexShader); glAttachShader(program, fragmentShader); // 链接 program glLinkProgram(program); // 检查链接是否成功 GLint linkSuccess; glGetProgramiv(program, GL_LINK_STATUS, &linkSuccess); if (linkSuccess == GL_FALSE) { GLchar messages[256]; glGetProgramInfoLog(program, sizeof(messages), 0, &messages[0]); NSString *messageString = [NSString stringWithUTF8String:messages]; NSAssert(NO, @"program链接失败:%@", messageString); exit(1); } return program; } // 编译一个 shader,并返回 shader 的 id - (GLuint)compileShaderWithName:(NSString *)name type:(GLenum)shaderType { // 查找 shader 文件 NSString *shaderPath = [[NSBundle mainBundle] pathForResource:name ofType:shaderType == GL_VERTEX_SHADER ? @"vsh" : @"fsh"]; // 根据不同的类型确定后缀名 NSError *error; NSString *shaderString = [NSString stringWithContentsOfFile:shaderPath encoding:NSUTF8StringEncoding error:&error]; if (!shaderString) { NSAssert(NO, @"读取shader失败"); exit(1); } // 创建一个 shader 对象 GLuint shader = glCreateShader(shaderType); // 获取 shader 的内容 const char *shaderStringUTF8 = [shaderString UTF8String]; int shaderStringLength = (int)[shaderString length]; glShaderSource(shader, 1, &shaderStringUTF8, &shaderStringLength); // 编译shader glCompileShader(shader); // 查询 shader 是否编译成功 GLint compileSuccess; glGetShaderiv(shader, GL_COMPILE_STATUS, &compileSuccess); if (compileSuccess == GL_FALSE) { GLchar messages[256]; glGetShaderInfoLog(shader, sizeof(messages), 0, &messages[0]); NSString *messageString = [NSString stringWithUTF8String:messages]; NSAssert(NO, @"shader编译失败:%@", messageString); exit(1); } return shader; } #pragma mark - #pragma mark - public methods 公有方法 #pragma mark - #pragma mark - getters and setters 设置器和访问器 // 获取渲染缓存宽度 - (GLint)drawableWidth { GLint backingWidth; glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &backingWidth); return backingWidth; } // 获取渲染缓存高度 - (GLint)drawableHeight { GLint backingHeight; glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &backingHeight); return backingHeight; } #pragma mark - #pragma mark - life cycle 视图的生命周期 - (void)dealloc { if ([EAGLContext currentContext] == self.context) { [EAGLContext setCurrentContext:nil]; } //释放结构体内存的数组 需要手动free if (_vertices) { free(_vertices); _vertices = nil; } } @end ``` ### 顶点着色器 ``` shade attribute vec4 Position; attribute vec2 TextureCoords; varying vec2 TextureCoordsVarying; void main (void) { gl_Position = Position; TextureCoordsVarying = TextureCoords; } ``` ### 片元着色器 ``` shade precision mediump float; uniform sampler2D Texture; varying vec2 TextureCoordsVarying; void main (void) { vec4 mask = texture2D(Texture, TextureCoordsVarying); gl_FragColor = vec4(mask.rgb, 1.0); } ``` # 总结 最近一有时间就尽量研习图形图像相关的技术,本文具有强烈的个人感情色彩,如有观看不适,请尽快关闭. 本文仅作为个人学习记录使用,也欢迎在许可协议范围内转载或分享,请尊重版权并且保留原文链接,谢谢您的理解合作. 如果您觉得本站对您能有帮助,您可以使用RSS方式订阅本站,感谢支持! [Demo](https://github.com/sunyazhou13/GLSLDemo1) [学习参考](http://www.lymanli.com/2019/02/17/ios-opengles-render-texture/) URL: https://sunyazhou.com/2019/07/LoadingAnimationI/index.html.md Published At: 2019-07-26 11:13:44 +0000 # 做一个简单的Loading动画 ![](/assets/images/20190726LoadingAnimationI/CircleLoadingAnimation.webp) # 前言 由于最近工作忙到坐地铁回家都能睡着,博客没能及时更新,今天抽个时间写个加载动画,废话不多说上图. ![](/assets/images/20190726LoadingAnimationI/CircleLoadingAnimation.gif) (颜色可以自定义哈,非常简单,小白自己可以随便改) # 开始 创建一个UIView的子类`UILoadingView`(名字最好不要带`UI`开头哈,我这是为了玩 大家理解就行),然后添加两个接口 ``` objc @interface UILoadingView : UIView - (void)startLoading; //1 - (void)stopLoading; //2 @end ``` > 1. 开始动画 > 2. 结束动画 实现的.m文件中我们需要用到`CAReplicatorLayer`,主要是看上边的小圆点比较多,用`CAReplicatorLayer`可以帮助我们自动创建多个小圆点实例. ``` objc @interface UILoadingView () @property(nonatomic, strong) CAReplicatorLayer *replicatorLayer; @end @implementation UILoadingView - (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { [self setupSubviews]; } return self; } - (void)awakeFromNib { [super awakeFromNib]; [self setupSubviews]; } - (void)setupSubviews { //这里要补全代码 创建需要的视图 继续往下看 } @end ``` ### 创建子视图 首先我们需要创建一个replicatorLayer的实例对象,然后向这个对象上添加一个圆点的,并错开角度.这里用到的知识点基本就是position和bounds啥关系.这里我不想啰嗦了大家自行google就行了.上代码 ``` objc - (void)setupSubviews { if (self.replicatorLayer == nil) { self.replicatorLayer = [CAReplicatorLayer layer]; self.replicatorLayer.backgroundColor = [UIColor clearColor].CGColor; [self.layer addSublayer:self.replicatorLayer]; self.replicatorLayer.bounds = self.bounds; self.replicatorLayer.position = self.center; NSInteger instanceCount = 15; //1 self.replicatorLayer.instanceCount = instanceCount; // self.replicatorLayer.instanceTransform = CATransform3DMakeRotation(M_PI * 2 / instanceCount, 0, 0, 1); //2 self.replicatorLayer.instanceDelay = 1 / (instanceCount * 1.0); //2 } //圆点 CALayer *circle = [CALayer layer]; circle.bounds = CGRectMake(0, 0, 10, 10); circle.cornerRadius = 5; circle.position = CGPointZero; circle.backgroundColor = [self randomColor].CGColor; circle.name = kCircleName; //3 设置layer的唯一标识 [self.replicatorLayer addSublayer:circle]; //小技巧 刚开始的动画不是很自然,那是因为小圆点的初始比例是1,让小圆点的初始比例为0.01 circle.transform = CATransform3DMakeScale(0.01, 0.01, 0.01); //5 } ``` > 1. 1处的代码是让`CAReplicatorLayer`帮我们创建指定数量的实例对象(我们添加的原点就是它需要的也就是说它帮你创建了instanceCount个实例对象) > 2. 2处代码 是设置错开的角度(2π 是 360°, 如果需要一圈创建指定数量的圆点 那么 instanceCount/2π 就是每个圆的角度,这里很重要 认真学习一下.) > 3. 3处代码是给这个layer设置一个唯一标识 一会儿要通过方法找到它,如果 不这样做你就要搞个成员变量去存一下,如果用成员变量存储的话注意内存引用关系,我这里不推荐成员变量的搞法. > 4. 4处代码主要是解决动画不自然,因为添加动画的圆点的初始比例是1,只有第一次开始的时候很愣. #### 找到圆点layer ``` objc - (CALayer *)findCircleLayer { for (CALayer *layer in [self.replicatorLayer sublayers]) { if ([[layer name] isEqualToString:kCircleName]) { return layer; } } return nil; } ``` 我们用的时候调用这个方法 找一下我们添加上去的layer即可 #### 实现对外暴露的方法 ``` objc - (void)startLoading { CALayer *circleLayer = [self findCircleLayer]; if (circleLayer && ![[circleLayer animationKeys] containsObject:kScaleAnimationKey]) { //加动画 CABasicAnimation *scale = [CABasicAnimation animationWithKeyPath:@"transform.scale"]; scale.fromValue = @(1); scale.toValue = @(0.1); scale.duration = 1; scale.repeatCount = HUGE; [circleLayer addAnimation:scale forKey:kScaleAnimationKey]; } } - (void)stopLoading { CALayer *circleLayer = [self findCircleLayer]; if (circleLayer && [[circleLayer animationKeys] containsObject:kScaleAnimationKey]) { [circleLayer removeAnimationForKey:kScaleAnimationKey]; } } ``` > kScaleAnimationKey 常量自行定义即可. #### 支持AutoLayout 目前大家用的比较多的是`Masonry`,所以这里使用`Masonry`自动布局, 使用自动布局主要是是为了方便外部调用的时候外部视图使用了自动布局,那么内部就需要更新相关`layer`的`frame`.具体代码如下 ``` objc - (void)layoutSubviews { [super layoutSubviews]; [CATransaction begin]; [CATransaction setDisableActions:YES]; self.replicatorLayer.bounds = self.bounds; self.replicatorLayer.position = CGPointMake(CGRectGetWidth(self.bounds)/2, CGRectGetHeight(self.bounds)/2); CALayer *circleLayer = [self findCircleLayer]; if (circleLayer) { circleLayer.position = CGPointMake(self.frame.size.width / 2, self.frame.size.height/2 - 40); //距离圆心 40pt } [CATransaction commit]; [self.replicatorLayer layoutSublayers]; } ``` 这里使用了 隐式动画和显式动画相关的知识 ``` objc [CATransaction begin]; [CATransaction setDisableActions:YES]; //... 这里修改相关动画参数 [CATransaction commit]; ``` 因为有可能layer在动画中,如果在动画中一般在这里需要加上`事务`修改这样才会更顺畅自然. 可以看到我上边的代码注释写的`40pt`的距离 实际上是圆心距离我们上边做的原点layer的的距离,大家可自行修改. # 总结 由于最近在研究一[音频波形](https://juejin.im/post/5c1bbec66fb9a049cb18b64c)的动画效果,想实现一个类似网易云音乐的黑胶唱片效果,可是用到的知识有点忘记,用此片文章来回顾复习一下动画的知识,也是为了很久没更新的博客更新一下,记录一下经常忘记的小技巧 Demo我给在下面也把相关的文章参考 放在下方,有兴趣大家可以学习一下. [loading动画Demo](https://github.com/sunyazhou13/UILoadingView) [参考](http://www.devtalking.com/articles/calayer-animation-replicator-animation/) URL: https://sunyazhou.com/2019/05/CentosIptables/index.html.md Published At: 2019-05-28 10:06:24 +0000 # CentOS7安装iptables防火墙 # 前言 最近翻墙总不稳定 最后发现是防火墙配置问题今天 记录一下 ## 配置 CentOS7默认的防火墙不是iptables,而是firewalle. 安装iptable iptable-service ``` sh #先检查是否安装了iptables service iptables status #安装iptables yum install -y iptables #升级iptables yum update iptables #安装iptables-services yum install iptables-services ``` ## 禁用/停止自带的firewalld服务 ``` sh #停止firewalld服务 systemctl stop firewalld #禁用firewalld服务 systemctl mask firewalld ``` ## 设置现有规则 ``` sh #查看iptables现有规则 iptables -L -n #先允许所有,不然有可能会杯具 iptables -P INPUT ACCEPT #清空所有默认规则 iptables -F #清空所有自定义规则 iptables -X #所有计数器归0 iptables -Z #允许来自于lo接口的数据包(本地访问) iptables -A INPUT -i lo -j ACCEPT #开放22端口 iptables -A INPUT -p tcp --dport 22 -j ACCEPT #开放21端口(FTP) iptables -A INPUT -p tcp --dport 21 -j ACCEPT #开放80端口(HTTP) iptables -A INPUT -p tcp --dport 80 -j ACCEPT #开放443端口(HTTPS) iptables -A INPUT -p tcp --dport 443 -j ACCEPT #允许ping iptables -A INPUT -p icmp --icmp-type 8 -j ACCEPT #允许接受本机请求之后的返回数据 RELATED,是为FTP设置的 iptables -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT #其他入站一律丢弃 iptables -P INPUT DROP #所有出站一律绿灯 iptables -P OUTPUT ACCEPT #所有转发一律丢弃 iptables -P FORWARD DROP ``` ## 其他规则设定 ``` sh #如果要添加内网ip信任(接受其所有TCP请求) iptables -A INPUT -p tcp -s 45.96.174.68 -j ACCEPT #过滤所有非以上规则的请求 iptables -P INPUT DROP #要封停一个IP,使用下面这条命令: iptables -I INPUT -s ***.***.***.*** -j DROP #要解封一个IP,使用下面这条命令: iptables -D INPUT -s ***.***.***.*** -j DROP ``` ## 保存规则设定 ``` sh #保存上述规则 service iptables save ``` ## 开启iptables服务 ``` sh #注册iptables服务 #相当于以前的chkconfig iptables on systemctl enable iptables.service #开启服务 systemctl start iptables.service #查看状态 systemctl status iptables.service ``` ## 解决vsftpd在iptables开启后,无法使用被动模式的问题 1.首先在/etc/sysconfig/iptables-config中修改或者添加以下内容 ``` sh #添加以下内容,注意顺序不能调换 IPTABLES_MODULES="ip_conntrack_ftp" IPTABLES_MODULES="ip_nat_ftp" ``` 2.重新设置iptables设置 ``` sh iptables -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT ``` ### 以下为完整设置脚本 ``` sh #!/bin/sh iptables -P INPUT ACCEPT iptables -F iptables -X iptables -Z iptables -A INPUT -i lo -j ACCEPT iptables -A INPUT -p tcp --dport 22 -j ACCEPT iptables -A INPUT -p tcp --dport 21 -j ACCEPT iptables -A INPUT -p tcp --dport 80 -j ACCEPT iptables -A INPUT -p tcp --dport 443 -j ACCEPT iptables -A INPUT -p icmp --icmp-type 8 -j ACCEPT iptables -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT iptables -P INPUT DROP iptables -P OUTPUT ACCEPT iptables -P FORWARD DROP service iptables save systemctl restart iptables.service ``` URL: https://sunyazhou.com/2019/05/CleanerForXcodeBuild/index.html.md Published At: 2019-05-17 16:37:43 +0000 # Cleaner For Xcode编译 ![](/assets/images/20190517CleanerForXcodeBuild/CleanerForXcode.webp) # 前言 最近公司给配发一个最新版的macbook pro, 然后经过一顿折腾以后发现原来的软件 无法 迁移过来或者迁移起来比较费劲.于是就有了这篇文章. ## 背景 由于mac磁盘空间有限,不得不借助一些第三方软件清理磁盘,尤其对于一个iOS开发者来说,固态硬盘128G的mac我只能说公司太抠了,就是能安装个Xcode干活,其余的我觉得我的256G的iPhone X足够应付了 Xcode是占用mac空间最大的app,无论从内存、磁盘io、系统资源 全部都能排第一.因为平时都用真机运行所以Xcode自带的模拟器也排不上用场还占用了一些磁盘空间,几年前有封面的这个软件叫Cleaner For Xcode. 是个开源的并且用React Native写的,我觉得很好用,不过这个开发者不够厚道,在mac的 app store 标价 $0.99。 真的我说这个作者你真不够厚道你都open source 了为啥不打个 release包 广大 mac上的小伙伴用呢.于是 今天有点时间 我配上RN环境 誓死编译出一个app**免费**给大家使用. ## 过程 话说我真不想装RN 太浪费磁盘空间 还浪费时间,迫不得已 经过一番折腾 遇到好多坑 #### 填坑经历 0.45以上的RN 需要 boost 库,这个库 翻墙都很费劲 下载。。。Done. 又要安装 yarn、node、npm、看门狗 watchdag。。。 Done. 编辑错误, Xcode10.12.1 目前最新版 各种编译选项 静态分析 Done. 大概过了 30分钟 终于 折腾 出一个app # 总结 [链接https://pan.baidu.com/s/1BClEjWLHS3htvKXoM11UjQ](https://pan.baidu.com/s/1BClEjWLHS3htvKXoM11UjQ) 提取码: `uhns` 各位拿去不用客气 其实就是了解删除 Xcode每个目录都存储什么缓存 没事删除一下 搞个好看的UI.作者 是有点蛋疼 这么简单的功能 一个shell或者objc几行代码搞定 非要大动干戈用RN. 然后还很不厚道的开源,这种开发者真是蛋疼. URL: https://sunyazhou.com/2019/04/WindowsResources/index.html.md Published At: 2019-04-24 11:26:34 +0000 # Windows装机教程 # 前言 为了解决每次都重装windows电脑系统浪费时间,干脆整理一篇文章记录一下,方便后续修电脑总忘记各种工具 ### PE安装系统教程 PE工具箱制作 主要步骤: * 第一步:制作前的软件、硬件准备:8G及以上U盘一个都可以, 一台可正常可上网的电脑 * 第二步:用电脑店U盘启动制作工具制作启动U盘 * 第三步:下载您需要的系统文件并复制到U盘中 * 第四步:进入BIOS设置U盘启动顺序 * 第五步:在进入WIN10 PE模式分区安装系统即可 * 第六步:系统激活问题 * 第七步:安装驱动问题 ## 制作工具箱教程 首先先下载一个制作启动工具的软件: 下载地址:链接:[http://www.usbrun.com/](http://www.usbrun.com/) ![](/assets/images/20190424WindowsResources/1.webp) 下载精简版本好后 先把杀毒软件关掉 !! 双击安装一下此软件到电脑上 ,安装好软件后打开 ,如果软件提示更新,可以忽略,不需要更新!! 把U盘插上 >【注:U盘会格式化,有资料先拷贝出来保存避免被格式掉了】 要是看不到设备的话, 把U盘重新插一下 ![](/assets/images/20190424WindowsResources/2.webp) 点一键制作然后等制作完成就OK!! 制作完成之后 点一下模拟启动看下U盘能不能启动,能的话就OK。关掉即可 ## 下载系统步骤 下面选1个需要的系统版本下载 (U盘容量够大两个系统都可以下载使用 此PE支持原版系统安装) WIN7 64位(B360B450锐龙二代CPU主板不支持) 系统下载连接:[链接](http://www.jsgho.net/win7/jsy/35178.html)(技术员纯净版) ![](/assets/images/20190424WindowsResources/3.webp) ![](/assets/images/20190424WindowsResources/4.webp) ## WIN7~10 64位 专业版 下载地址:[http://msdn.itellyou.cn/](http://msdn.itellyou.cn/) 可复制此磁力链接使用迅雷新建下载: ed2k://|file|cn_windows_10_business_edition_version_1803_updated_sep_2018_x64_dvd_07b164ed.iso|5229189120|5CC3C32DB198D647DCED4B0EB96B8547|/ 下载参考: ![](/assets/images/20190424WindowsResources/5.webp) 下载好的系统直接拷贝到您刚刚制作好的的U盘里,随便放什么位置都可以。 ![](/assets/images/20190424WindowsResources/6.webp) ## 设置U盘启动步骤 制作好的U盘插上您需要装系统的电脑上,以下主板U盘快捷启动按键 华硕启动快捷键:F8 技嘉、微星、七彩虹、昂达、华擎、映泰 点击:F11 品牌机:惠普、惠普、戴尔、联想、神州 点击:F12 以下华硕主板快捷启动菜单选择进入U盘PE参考图,这是一个开机启动设备选项,我们选择刚刚做好的U盘 ADATA USB Flash Drlve(14800MB)按回车即可 >(注意:选择不带UEFI的选项) ![](/assets/images/20190424WindowsResources/7.webp) 出现U盘启动界面 如下图选择 启动WIN10 PE X 64 ,别的不用去选择。 ![](/assets/images/20190424WindowsResources/8.webp) ## 分区安装系统步骤 进去PE之后 我们就要对新硬盘 进行一个分区操作了 >【如果是老硬盘,有分区的 那可以省略这一步 直接按照下面装系统】 这里采用的是一个三星120G的固态硬盘,__一般为了发回固态最大性能,都要在主板预先开启AHCI模式和分区的时候选择4K对齐__,另外3.0的数据线和主板必须支持3.0的接口。 AHCI模式是主板自带的 新主板都支持 ,如果有的老的主板是IDE的预先设置好,华硕技嘉B250等以上主板 默认都是AHCI模式不需要更改 就要首先打开DG分区工具箱进行分区处理 ,如下图 ![](/assets/images/20190424WindowsResources/9.webp) 点击分区工具 之后 会看到您的硬盘 这时候 可以选择您的新硬盘之后 点击上面的快速分区 如下图 ![](/assets/images/20190424WindowsResources/10.webp) 在新的页面中 你可以选择分几个区 多少容量 都可以自行填写 别的不需要去改变 ,另外右下角 就是4K对齐,只许勾选即可,如下图 > 注:固态硬盘需要选择4K,机械硬盘不用现在 ,选择了会导致进不了系统 ![](/assets/images/20190424WindowsResources/11.webp) 分区好后关闭分区接口窗口看下一步操作 ## 安装系统镜像步骤 打开桌面上的电脑店一键还原,如下图 * ①选择系统镜像文件,等待自动识别和挂载后再次选择系统版本。 * ②选择需要安装系统的分区。[可以根据分区容量格式信息来判断分区] * ③点击执行按钮等待系统安装完成后重启拔掉U盘。 ![](/assets/images/20190424WindowsResources/12.webp) ![](/assets/images/20190424WindowsResources/13.webp) ![](/assets/images/20190424WindowsResources/14.webp) ![](/assets/images/20190424WindowsResources/15.webp) ![](/assets/images/20190424WindowsResources/16.webp) ![](/assets/images/20190424WindowsResources/17.webp) > PS:等进度条走完了 提示重启的时候一定要拔掉U盘 再重启哦!!之后就可以正常进入安装系统过程了,等待大概5-10分钟左右装好重启即可正常使用了 免责条款 > (本作品仅限网友交流安装系统经验,或可作临时安装测试PC硬件之用,请在安装后24小时内删除,若需要长期使用,请购买正版系统及软件。) ## 激活系统步骤 激活工具箱下载连接: `win7`点击这里 链接: `https://pan.baidu.com/s/1iWVZW534JKqAd9mu1B0VzQ` 提取码: `u71b` `win8`链接: `https://pan.baidu.com/s/1M6t2nGwlBM4qXWT_imcI-A` 提取码: tkhb `win10` 链接: `https://pan.baidu.com/s/1Tr-0PYBVmQFR0HNvzZ5yjA` 提取码: a3mt ## 华硕主板网卡驱动安装步骤 教程连接:[http://note.youdao.com/noteshare?id=40345f63671ea936740aa771cca2d438](http://note.youdao.com/noteshare?id=40345f63671ea936740aa771cca2d438) 建议上网其他驱动问题下载:[驱动精灵 标准版](http://www.drivergenius.com/) # 总结 每次装机总忘记一些流程 这里记录下来 [参考 PE安装系统教程](https://note.youdao.com/ynoteshare1/index.html?id=e0f8c30393c4f069555d286020f9d394&type=note) [U盘刻录安装原版系统教程](http://05aebac1.wiz03.com/share/s/05HHH13zK4EY2bE37Q00RO3H1CvO101754vQ2bNyFE2nhALV?tdsourcetag=s_pcqq_aiomsg) [I tell you神奇的镜像下载网站 绝对纯洁](http://msdn.itellyou.cn/) URL: https://sunyazhou.com/2019/04/AVRoutePickerView/index.html.md Published At: 2019-04-17 15:19:52 +0000 # AVRoutePickerView ![](/assets/images/20190417AVRoutePickerView/cover_album.webp) # 前言 最近无意中看了一下AVKit发现内部增加了很多新的内容.其中有个`AVRoutePickerView`的UI控件,打算研究一下. 其实这个很常见就在系统的控制中心 下拉屏幕就能看见 当你连接耳机或者无线蓝牙设备的时候. ![](/assets/images/20190417AVRoutePickerView/RouteChange2.gif) 这里网易云音乐中有实践的例子 ![](/assets/images/20190417AVRoutePickerView/RouteChange1.gif) 这个控件主要用于AirPlay投屏 和音频的线路切换 那么我今天就跟大家一起学习一下这个新的控件 ## 代码实现 导入`#import ` 剩下的就是创建实例调用方法 这里用ViewController做示例 ``` objc @interface ViewController () @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; if (@available(iOS 11.0, *)) { AVRoutePickerView *routerPickerView = [[AVRoutePickerView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)]; routerPickerView.activeTintColor = [UIColor cyanColor]; routerPickerView.delegate = self; [self.view addSubview:routerPickerView]; } else { // Fallback on earlier versions } } //AirPlay界面弹出时回调 - (void)routePickerViewWillBeginPresentingRoutes:(AVRoutePickerView *)routePickerView API_AVAILABLE(ios(11.0)){ NSLog(@"Airplay视图弹出"); } //AirPlay界面结束时回调 - (void)routePickerViewDidEndPresentingRoutes:(AVRoutePickerView *)routePickerView API_AVAILABLE(ios(11.0)){ NSLog(@"Airplay视图弹回"); } @end ``` 添加完之后运行如下 ![](/assets/images/20190417AVRoutePickerView/RouteChange3.gif) `AVRoutePickerView `这个View提供的API 就两个颜色值剩下的啥都没有,啥都改不了,那怎么才能实现网易云音乐那样自定义图标呢? ##### 添加自定义视图 ``` objc UIImageView *imageView = [[UIImageView alloc] initWithFrame:routerPickerView.bounds]; imageView.image = [UIImage imageNamed:@"logo2"]; [routerPickerView addSubview:imageView]; ``` ![](/assets/images/20190417AVRoutePickerView/RouteChange4.gif) 自己加个图标即可. # 总结 此控件只适用于iOS11以后,使用的时候 记得加可用性检测API ``` objc if (@available(iOS 11.0, *)) { //这里写创建视图代码 } ``` 这个控件在多数场景上提升了用户体验,比如音视频类app经常频繁接线控或者蓝牙耳机,那么对这个有要求的可以试试.感谢支持! [demo点击这里下载](https://github.com/sunyazhou13/AVRoutePickerViewDemo) URL: https://sunyazhou.com/2019/03/UIScrollTextDemo/index.html.md Published At: 2019-03-21 09:50:20 +0000 # iOS抖音滚动字幕 ![](/assets/images/20190321UIScrollTextDemo/CAGradientCover.webp) # 前言 很久没更新博客了(家里事情比较多时间太紧迫加上工作时间有限),今天给大家带来的是抖音得滚动字幕,也就是音乐专辑的专辑名称 废话不多说上图 抖音如下 ![](/assets/images/20190321UIScrollTextDemo/scrolltextdemo0.gif) 系统的滚动字幕如下 ![](/assets/images/20190321UIScrollTextDemo/scrolltextdemo4.gif) 本篇完成之后如下 ![](/assets/images/20190321UIScrollTextDemo/scrolltextdemo5.gif) * 支持蒙版渐变模糊 可调节 * 支持富文本字符串用于显示表情或者图片 # 开篇 整个实现比较简单 不超过 200行代码 ![](/assets/images/20190321UIScrollTextDemo/scrolltextdemo1.gif) 首先我们要用到两个CALayer * `CATextLayer` 用于展示文本 * `CAGradientLayer` 用于给文本加蒙版 然后我们新建一个`UIScrollTextView`继承自`UIView`(我这是纯娱乐写成UI前缀大家可自行封装哈.) ``` objc @interface UIScrollTextView : UIView @property (nonatomic, copy ) NSString *text; //1 @property (nonatomic, strong) UIColor *textColor; //2 @property (nonatomic, strong) UIFont *font; //3 @property (nonatomic, strong) NSAttributedString *attrString; //4 /** 渐变开始的距离(0~0.5) 推荐 0.0x eg:0.026, 如果设置成1的时候视图不够长会出现溢出得情况 不推荐超出范围 */ @property (nonatomic, assign) CGFloat fade; //5 @end ``` 对外暴露的接口 * 1.显示的文本内容 * 2.文本颜色 * 3.文本字体 * 4.属性字符串 自行可控颜色字体和样式 * 5.蒙版渐变模糊的 渐变长度 首先大家可以先忽略这些对外暴露的接口 到.m中看实现如下 ``` objc @interface UIScrollTextView () @property (nonatomic, strong) CATextLayer *textLayer; //文本layer @property (nonatomic, strong) CAGradientLayer *gradientLayer; //蒙版渐变layer @property (nonatomic, assign) CGFloat textSeparateWidth; //文本分割宽度 @property (nonatomic, assign) CGFloat textWidth; //文本宽度 @property (nonatomic, assign) CGFloat textHeight; //文本高度 @property (nonatomic, assign) CGRect textLayerFrame; //文本layer的frame @property (nonatomic, assign) CGFloat translationX; //文字位置游标 @end ``` 在`initWithFrame:`;和`awakeFromNib`方法中 初始化一些成员变量 ``` objc - (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { [self configProperty];//初始化成员变量 //1 [self initLayer]; //2 } return self; } - (void)configProperty { _text = @""; _textColor = [UIColor whiteColor]; _font = [UIFont systemFontOfSize:14.0]; self.textSeparateWidth = [kSeparateText calculateSingleLineSizeFromFont:self.font].width; _fade = 0.026; } ``` * 1.configProperty方法 初始化默认值 * 2.initLayer方法创建我们需要的2个layer > configProperty方法 初始化成员变量最好用`_`下划线 这样不会触发`setter`因为我们很多的代码都是写在setter和getter中 #### 初始化Layer 下面我们重点看下`initLayer` ``` objc - (void)initLayer { //文本layer 1 if (self.textLayer == nil) { self.textLayer = [[CATextLayer alloc] init]; } self.textLayer.alignmentMode = kCAAlignmentNatural; //设置文字对齐模式 自然对齐 self.textLayer.truncationMode = kCATruncationNone; //设置截断模式 self.textLayer.wrapped = NO; //是否折行 self.textLayer.contentsScale = [UIScreen mainScreen].scale; if (self.textLayer.superlayer == nil) { [self.layer addSublayer:self.textLayer]; } //渐变 2 self.gradientLayer = [CAGradientLayer layer]; self.gradientLayer.shouldRasterize = YES; self.gradientLayer.rasterizationScale = [UIScreen mainScreen].scale; self.gradientLayer.startPoint = CGPointMake(0.0f, 0.5f); //3 self.gradientLayer.endPoint = CGPointMake(1.0f, 0.5f); //4 id transparent = (id)[UIColor clearColor].CGColor; // 5 id opaque = (id)[UIColor blackColor].CGColor; //5 self.gradientLayer.colors = @[transparent, opaque, opaque, transparent]; // 6 self.gradientLayer.locations = @[@0,@(self.fade),@(1-self.fade),@1]; // 7 self.layer.mask = self.gradientLayer; //8 } ``` 代码`1`处 创建`CATextLayer`和我们创建其它CALayer一样没啥好说的,设置折行、对齐、截断... 代码`2`处 这里重点说一下这个`CAGradientLayer` 代码`3`和`4`处是设置蒙版渐变 的开始方向和结束方向. (以屏幕左下角为原点0,0计算 到屏幕右上角1,1) > 如果开始点是(0.0,0.5)结束点是(1.0,0.5)是横向渐变 > 如果开始点是(0.5,0)结束点是(0.5,1)是纵向渐变 > 这两个点决定了渐变的方向 我们可以把代码去掉运行看下不加蒙版的效果图 如下: ![](/assets/images/20190321UIScrollTextDemo/scrolltextdemo2.gif) 这里我用cyan颜色区域代表视图的大小,文本不加蒙版实际上超出显示范围的. > 注意: 动画不是这个layer自带,是我们自己加的代码,往下看 有代码 代码`5`处代码 是给当前渐变layer加渐变颜色 实现蒙版模糊遮盖的效果 代码`6`处 把对于的颜色的数组中给`gradientLayer.colors` 代码`7`处 对应 代码`6`处 配合使用,就做到了我们两边渐变遮盖的效果 ![](/assets/images/20190321UIScrollTextDemo/scrolltextdemo3.gif) 上图就是我们下面代码的效果,我们加了4个点 ``` objc self.gradientLayer.colors = @[transparent, opaque, opaque, transparent]; // 6 self.gradientLayer.locations = @[@0,@(self.fade),@(1-self.fade),@1]; // 7 ``` #### 更新layer布局 这里我们要在layoutSubviews方法中计算出正确的布局坐标,因为外部有可能使用autolayout布局. ``` objc - (void)layoutSubviews { [super layoutSubviews]; [CATransaction begin]; [CATransaction setDisableActions:YES]; CGFloat textLayerFrameY = CGRectGetHeight(self.bounds)/2 - CGRectGetHeight(self.textLayer.bounds) / 2; self.textLayer.frame = CGRectMake(0, textLayerFrameY, CGRectGetWidth(self.textLayerFrame), CGRectGetHeight(self.textLayerFrame)); self.gradientLayer.frame = self.bounds; [CATransaction commit]; } ``` 这里代码主要是更新gradientLayer 和textLayer的frame. 并 ##### 为什么要用CATransaction? 因为我们要在动画进行中手动改变动画的参数详情参考[设置动画参数](http://jefferyfan.github.io/2016/06/27/programing/iOS/CATransaction/) ### 剩下的主要有3个工作 * 绘制文本layer,就是把想显示的字符串给self.textLayer.string * 添加滚动动画 * 成员变量的setter中调用绘制文本layer和滚动动画方法 ##### 添加drawTextLayer私有方法 ``` objc //拼装文本 - (void)drawTextLayer { self.textLayer.foregroundColor = self.textColor.CGColor; CFStringRef fontName = (__bridge CFStringRef)self.font.fontName; CGFontRef fontRef = CGFontCreateWithFontName(fontName); self.textLayer.font = fontRef; self.textLayer.fontSize = self.font.pointSize; CGFontRelease(fontRef); // 1 self.textLayer.string = [NSString stringWithFormat:@"%@%@%@%@%@",_text,kSeparateText,_text,kSeparateText,_text]; } ``` 这里注意 `1` 处 代码 干了两件事 * 拼接文本 * 给layer.string 格式拼接 __文本+3个空格+文本+3个空格+文本__ ``` objc NSString * const kSeparateText = @" "; //3个空格 ``` > kSeparateText 是个常量 ##### 添加文本滚动动画 ``` objc - (void)startAnimation { if ([self.textLayer animationForKey:kTextLayerAnimationKey]) { [self.textLayer removeAnimationForKey:kTextLayerAnimationKey]; } CABasicAnimation *animation = [CABasicAnimation animation]; animation.keyPath = @"transform.translation.x"; //沿着X轴运动 animation.fromValue = @(self.bounds.origin.x); animation.toValue = @(self.bounds.origin.x - self.translationX); animation.duration = self.textWidth * 0.035f; animation.repeatCount = MAXFLOAT; animation.removedOnCompletion = NO; animation.fillMode = kCAFillModeForwards; animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]; [self.textLayer addAnimation:animation forKey:kTextLayerAnimationKey]; } ``` 这里给`self.textLayer`加一个CABasicAnimation,让它沿着X轴运动,当然如果大家喜欢后续我添加多个方向类似[MarqueeLabel滚动文本](https://github.com/sunyazhou13/MarqueeLabel)一样,不过我觉得[MarqueeLabel滚动文本](https://github.com/sunyazhou13/MarqueeLabel)的实现太复杂,不够接地气,这个简单的动画效果还是要自己写比较靠谱. > 给self.textLayer添加动画即可,相信大家非常了解iOS动画我就不一一介绍了. ##### 成员变量的setter中调用绘制文本layer和滚动动画方法 ``` objc - (void)setText:(NSString *)text { _text = text; //计算单行文本大小 CGSize size = [text calculateSingleLineSizeWithAttributeText:_font]; _textWidth = size.width; _textHeight = size.height; _textLayerFrame = CGRectMake(0, 0, _textWidth * 3 + _textSeparateWidth * 2, _textHeight); _translationX = _textWidth + _textSeparateWidth; [self drawTextLayer]; [self startAnimation]; } ``` 这里每次给当前视图设置相关文本的时候 setter处,就调用一下 渲染文本和动画.这样可以对外部暴露相关接口实时修改实时生效.至于其它的属性字符串,字体等也要最后追加上 ``` objc [self drawTextLayer]; [self startAnimation]; ``` 因为改动对文本大小有影响 文字的计算大小这里用的是 CoreText,支持多行和单行 属性字符串也一样就不写在这里了. 详细代码demo我已经把它写到文章下方 大家自行加载学习即可 最终效果 ![](/assets/images/20190321UIScrollTextDemo/scrolltextdemo5.gif) # 总结 由于业余时间有限,没能1个月稳定更新2偏文章,请各位同仁理解,抖音系列动画需要先写demo然后仔细研究,最终才形成文章.制作不易,本篇也参考了开源代码和一些滚动字幕的库.由于演示的不完美,有些类还有可扩展空间,比如 开始动画和结束动画对外暴露接口,比如像系统那样的自然添加动画组. 本篇感谢开源作者[qiaoshi](https://github.com/sshiqiao/douyin-ios-objectc),因为作者写的不是很完美我学习研究一下,增加了渐变效果和属性字符串得支持. [本篇demo](https://github.com/sunyazhou13/UIScrollTextDemo) URL: https://sunyazhou.com/2019/02/Tools/index.html.md Published At: 2019-02-15 10:40:15 +0000 # Mac上的一些好软件推荐 # 前言 新年第一篇 把自己用的比较好的工具都记录下来 ## 截屏 Snipaste 没当没有网络的时候 或者拿到新mac的时候由于没有什么软件截屏,不得不用QQ或者微信自带的截屏.这就逼着我不得不安装这两个app,有了Snipaste 跟截屏说再见吧 ![](/assets/images/20190215Tools/snipaste.webp) Snipaste是一个小巧强大的截图软件,可以直接对截图进行打码、标注、分享,而不用先保存到本地. 使用方法是按住`Fn`+`F1` 就出现了截屏页面大家可以试试 [下载地址](https://www.snipaste.com/) ## 快捷键查看器CheatSheet CheatSheet可以快速调出当前软件支持的快捷键,长按command即可,简单实用。 ![](/assets/images/20190215Tools/CheatSheet.webp) > 使用之前必须在隐藏策略和安全性那个地方给这个加上访问权限才可以 具体有图片提示按照图片操作即可 [下载地址](https://www.mediaatelier.com/CheatSheet/) ## 下载工具 Folx 迅雷的替代品,支持BT下载,遇到迅雷无法下载的资源时,可以尝试一下Folx,免费版就可以满足基本需求 ![](/assets/images/20190215Tools/Folx.webp) 对于一个曾经在百度网盘搬过砖的程序员 文件上传下载这种最基础的业务必须玩的很溜,当年迅雷有意让我去面试被我拒绝,主要是公司距离住的地方太远,但是从文件下载限速的策略上我觉得是迅雷还是不足以驱动互联网前进的方向,这种第三方的folx完全彻底取代了迅雷. [下载地址](https://mac.eltima.com/cn/torrent-client.html) ## 视频播放器IINA IINA被很多人认为是Mac端的最强视频播放器,简洁无广告,并且功能强大,支持在线字幕的功能,值得尝试 ![](/assets/images/20190215Tools/IINA.webp) 各种使用还行 [下载地址](https://iina.io/) ## Mactracker Mactracker提供了苹果全系列产品的所有信息,包括性能参数、售价等。可以进行检索,还有发布的时间顺序,方便查看。这个软件也有iOS版。 ![](/assets/images/20190215Tools/Mactracker.webp) 做为一个iOS开发者,如果不能实时了解水果公司得各种产品的积极动向我觉得是一种不积极的工作表现,这个软件可以实时的查看苹果目前的所有发不过的型号等等info. [下载地址iTunes](https://itunes.apple.com/cn/app/mactracker/id430255202?mt=12) ## 图片水印批量添加工具 PhotoBulk PhotoBulk可以对图片进行批量处理,比如批量加水印,只要设置好了固定的水印内容、大小、位置,就可以一键生成带水印的图片,十分方便。 ![](/assets/images/20190215Tools/PhotoBulk.webp) [下载地址(收费软件)](https://itunes.apple.com/cn/app/photobulk-watermark-in-batch/id537211143?mt=12) ## 解压缩 Mac对压缩文件的格式支持不足,只支持zip,借助The Unarchiver可以补足这个缺陷,它支持常见的zip、rar、7z等常见格式,可以完成压缩、解压等工作 ![](/assets/images/20190215Tools/TheUnarchiver.webp) [下载地址](https://itunes.apple.com/cn/app/the-unarchiver/id425424353?mt=12) ## mac上管理android的文件管理 handshaker 锤子科技出品的文件管理软件,用于在Mac电脑上管理Android手机内容,功能强大,可以互传文件、分类管理。 ![](/assets/images/20190215Tools/HandShaker.webp) [下载地址](https://www.smartisan.com/apps/#/handshaker) ## mac 视频转gif 工具 GIF Brewery 3 Mac上最强大的GIF图处理工具,可以将视频转为GIF,对尺寸、大小、起止点等进行设置,之前是付费软件,现在免费提供。 ![](/assets/images/20190215Tools/GIFBrewery3.webp) > 这个软件不错 我经常写博客配图就是录制成mov转成gif [下载地址]() ## mac上音量分别控制 Background Music Background Music的功能非常简单,在MacBook中分别控制不同软件的音量,就像Android手机和Windows电脑一样。 ![Background Music](/assets/images/20190215Tools/BackgroundMusic.webp) 我经常用企业IM 但总是因为聊天软件的声音太大 影响体验,没想到还有这种细致入微的app专门解决了这个痛点. [下载地址](https://github.com/kyleneideck/BackgroundMusic/releases) ## 第三方网易云音乐 第三方的网易云音乐播放器,界面经过重新设计,喜欢的朋友可以尝试一下。 ![](/assets/images/20190215Tools/ieaseMusic.webp) 这个第三方的过于花哨 不实用,不如不装. [下载地址](https://github.com/trazyn/ieaseMusic) # 总结 有很多软件没有及时整理 有时间会不定时更新 多谢支持 [喵神的工具集合](https://onevcat.com/apps/) [参考ithome](https://www.ithome.com/0/409/410.htm) URL: https://sunyazhou.com/2018/12/FinalSummary/index.html.md Published At: 2018-12-28 23:04:41 +0000 # 2018年终总结 ![](/assets/images/20181231FinalSummary/banner.webp) # 前言 **埋头苦干尽五年,晨起面镜白发先.** **一年到头不见泪,岁月荏苒冬却寒.** **仿佛回到一三年,北京站前望着天.** **北漂掘金那刻起,人生已无再少年.** ![](/assets/images/20181231FinalSummary/yingxiongbense.gif) _我要是能像你那么潇洒就好了,有时候你能做的事情我却做不到,我相信正义,可是没人相信我..._ __这一年过的像网速一样快,看着年末又到了,我依然得墨守陈规的交出我的2018年终总结__ ## 2018回顾 这一年的清单如下: * 工作 * 演唱会 * 读书 * 技术 * 坚持 * 生活 * 故事 * 百年 * 总结 很精彩... ## 工作 这一年我很动荡,4月份从金山云到了快手.负责开发快手iOS国内版App的直播业务.我是一个不太喜欢换工作的人.从百度一路走来,回头看除了心酸和热泪盈眶... 来到了快手,感觉到这个公司确实很有意思,是参加工作以来我认为最值得去的一个公司,因为我在与一群清华的学子为伍,与北邮的小伙伴肩并肩,这里基本50%都是清华系,老实说,当我面试的时候从上午10点面到下午4点多,我都不知道我是怎么面过的时候,我真的想放弃了,如果不面吐的面试那只能算唠嗑.团队的中坚力量来自于麻省理工学院,以及Facebook来的大佬.这其实是一种收获,一种能与世界顶尖大学的毕业生一起工作的机会,虽然我的工作经历算不上有多好,不过能与这样的小伙伴为伍我应该感到荣幸,当然自己不能太low.他们身上有我值得学习地方,我不喜欢横向对比,只喜欢纵向对比,对比那些比我优秀的人. > 北邮(北京邮电大学)被誉为计算机界的黄埔军校,录入分数仅次于清华几分. 说完了我的感受,我再说一下这家公司的价值观,从我肤浅的认知价值观来看这家公司,确实很值得__尊重__,我只说两点: * 第一:接地气,如果你喜欢抖音,那我只能说你太不切实际,因为抖音的美女瘦身都是AI通过肢体识别实现的,换句话说你在被忽悠,再说抖音的视频中的内容,豪车,豪宅,高贵轻奢的生活,品质高档的餐厅...__我就想问你,中国有多少人能过的上你在抖音视频中能看到的生活,你能吗?你能开着劳斯莱斯有事没事的炫耀吗?__快手不一样,这里你能看到底层的劳动人民为了美好的生活而付出的实践行动,他们没有好的背景,他们真的只有背影,那种背影就是他们拍摄的视频的时候,这就是他们真实生活的写照... * 第二:不像主播倾斜流量.基本不签约明星,名人,让每个普通人能凭借自己的本领脱颖而出,这换句话说就是在用互联网帮助每个人得到公平.(倾斜流量就是把一些高端名人推荐给你让你默认就关注他就跟微博似的最后整的都没法正常说话发东西.全是劈天盖地的牛皮癣广告) > 回头再来看看BAT 哪家敢站出来说自己做到了上边这两点.做到了的在底部评论区扣1. 今年来快手又两件事让我很高兴 * 杭州之旅 * 快手家乡 #### 南下杭州 刚到这家公司不久就赶上了Team Building(团队建设),去杭州 我从未去过杭州,第一站是杭州的海宁 ![](/assets/images/20181231FinalSummary/haining.webp) > 当年孙中山先生就是在这里下车去钱塘江观潮 西塘,在家乡我习惯了23年的低矮平房,从未见过如下得建筑风格,这种徽系的白墙瓦房有一种中国地方的特色 ![](/assets/images/20181231FinalSummary/hangzhou2.webp) > 真是栏杆拍遍吴钩看了,无人会登临意,休说鲈鱼堪脍尽西风季鹰归未... ![](/assets/images/20181231FinalSummary/hangzhou1.webp) ![](/assets/images/20181231FinalSummary/hangzhou3.webp) 这里曾经是__伍子胥(公元前559年—公元前484年,大约是春秋末年)__ 兴修的水利工程,我对春秋末年吴王夫差越王勾践的故事记忆犹新,但对吴国的风土人情的认知仅限于上学时候读的__《史记》__内容描述,从未亲自来到这里.虽然我的认知十分肤浅,但看到了江南风情的别具一格还是令我肃然起敬,从北京到杭州的列车上,途径山东泰山,苏州的姑苏城,上海的长江流域.长江三角洲的确不同于我的家乡东北,这里物流车辆远高于在东北的任何高速,从这一点就可以客观的发现江浙一带经济比东北要发达的多.这一路我几乎一夜没睡,各地都在大兴土木搞基建,火车途径每一个城市附近周边都很明显的发现塔吊楼房,正在施工作业. 江南的风景如画的确如此. __杭州之旅我路过了钱塘江__ ![](/assets/images/20181231FinalSummary/qiantangriver.webp) 这一幕让我不禁的想起初一时候清晨背诵的一首白居易的《钱塘湖春行》 当我身临其境的感受钱塘江的时候,我只有一个感慨,此情此景如果我是白居易我也一样能做出一首诗,这一幕也许在东北几乎是感受不到的.对于一个东北的孩子,让他有诗人的灵感的前提是他需要有这样的环境和人文历史.我喜欢比较我的家乡和我去过的所有地方,在我的家乡冰天雪地,夏季没有像江南的小桥流水人家,没有古道西风瘦马,更没有夕阳西下断肠人在天涯...我的家乡只有黑土地和森林,以及一望无际的平原,唯一一条松花江还只是我们从电视上才能感受到唯一人文地理.地域的文化差距是截然不同的,但是我们的初中依然要学习白居易的《钱塘湖春行》这种以我看来就是为了应付考试的任务文章已经脱离地域特色,变得毫无意义.我并不是偏激,学识渊博是好的,但是,需要了解自己的生活地域和文化我认为比背诵白居易的《钱塘湖春行》更有意义. #### 快手家乡 今年晚些时候, 快手征求全公司员工意见,为一部分员工的家乡树立广告牌,我很幸运,成为了其中之一 ![](/assets/images/20181231FinalSummary/kwaihometown.webp) 我也从未想过有一天我能登上海伦的京都广告牌,成为快手的形象代言人之一,这要是在百度估计即便升到T8也未曾有如此这般的待遇吧! 真的很感谢快手,__这是一家有人文主义精神和艺术气质的公司.__ 工作的内容大概就介绍这么多吧! 现在的工作和团队还是不错的.就像我前边说的那样,与清华的学子为伍,与北邮的小伙伴肩并肩.这一切不是每个人都能在工作中遇到的. ## 演唱会 今年夏天,伍佰在北京五棵松体育馆 凯迪拉克中心开个人演唱会,这么多年我从未参加过任何一个明星真正意义上的演唱会.2013年的时候刘德华在北京开个人演唱会,由于当时没有舍得花钱买票,所以至今都觉得遗憾,当时刘德华的门票 ¥980一张. 如今伍佰的演唱会门票¥680一张,我买了两张,这一次说什么也要去看一看,以前因为工作没多久,花钱的确不敢大手大脚. 首席看台 ![](/assets/images/20181231FinalSummary/chinablue2.webp) ![](/assets/images/20181231FinalSummary/chinablue3.webp) 我记得第一次听伍佰的歌的时候是2000年左右,那时候我寄人篱下在我舅家东胜村,去双胜去上初中初一,有一次去一个叫镇东的地方全班同学都去听文艺演出,我坐在拖拉机的后车斗上,唱着伍佰的《白鸽》那时候还不知道MP3是什么东西,能听上这首录用磁带已经很满足了 ![](/assets/images/20181231FinalSummary/opentheshow.gif) 在伍佰的演唱会上开场的第二首想起的就是这首《白鸽》,唱出我的初中回忆,那个时候很穷,后来才知道还有一首成名曲叫《挪威的森林》. ![](/assets/images/20181231FinalSummary/chinablue1.gif) 当全场一起演唱挪威的森林的时候 那种场合的效果,绝对比KTV好多了.真的是前所未有,老实说 现场的声音算是原声了,这场演唱会真的值得. 伍佰说:"他的歌曲99%都是自己的原创" 不过仔细听过他的歌曲 真的很多经典 这中间伍佰唱了一首我第一感觉我没听过的歌曲,但是很好听,后来才得知叫__《被动》__,推荐给你们听听. ## 读书 这一年严格意义上来说我的眼睛很疼,每天至少14小时对着显示器,所以我列举了一些今年连看带听的书籍. 《晚清的最后十八年4》 ![](/assets/images/20181231FinalSummary/wanqing.webp) 《曾国藩》 ![](/assets/images/20181231FinalSummary/zengguofan.webp) 《晚清重臣李鸿章》 ![](/assets/images/20181231FinalSummary/lepetit.webp) 《毛泽东传》 ![](/assets/images/20181231FinalSummary/maozedong.webp) 《周总理的最后600天》 ![](/assets/images/20181231FinalSummary/zhouzongli.webp) 《普京传记》 ![](/assets/images/20181231FinalSummary/pujing.webp) 《习近平的七年知青岁月》 ![](/assets/images/20181231FinalSummary/xijinping.webp) 《乔布斯传》 ![](/assets/images/20181231FinalSummary/jobs.webp) 《货币战争1~5部》 ![](/assets/images/20181231FinalSummary/currencyWars.webp) 《拿破仑传》 ![](/assets/images/20181231FinalSummary/Napol%C3%A9onBonaparte.webp) 《李嘉诚传》 ![](/assets/images/20181231FinalSummary/superlee.webp) #### 晚清的最后十八年 第4部 去年我读完了1~3部,可是那时候还没有完整版的第4部,这本书俞敏洪都亲自推荐,最后一部绝对殿堂班史诗级没有之一,读完最后一部才了解,原来康有为是个喜欢办事总打脸的人,根本做不到严于利己.原来孙中山其实并没有教科书上那样有多好,反而我觉得辛亥革命得归功于袁世凯,因为晚清是中国历史上像西方文明进军最恢宏得时代,袁世凯主张君主立宪保全皇族脸面,而黎洪元这些后来者为了掩盖造反的事实主张共和,葬送大清王朝最快的一个人是载沣(北京恭亲王府就是这个人的,最后捐给了中国人民政府),就是他曾经去德国,德国沙皇让他下跪他保持了中华民族的尊严没有下跪,得到了德国皇帝得尊重,对这个人还是中国最后一个皇帝溥仪得父亲.总之这些 细节都会在这本书上说到. 从第一部介绍北洋舰队到介绍各种巡洋舰 驱逐舰 护卫舰 鱼雷艇... 这4部书简直完整的记录了中国清朝末期从中日甲午战争到辛亥革命全程记录.值得一看,强烈推荐 如果非要我写出推荐的理由,那我只能拿一张照片说明一下: ![](/assets/images/20181231FinalSummary/Krupp.webp) 这是我今年10月份去天津市津南区小镇站 小站练兵园(袁世凯曾经练兵的地方)拍摄的德国克虏伯公司1860左右生产的野炮. 北洋时期的军舰巡洋舰大部分来自德国克虏伯公司,从这门炮的制造工艺来推测当时的工业制造程度和北洋海军的军事装备基本算是中国近代史亚洲第一的强国了.我们不是打不过日本,我们打不过的是清朝内部的腐败. #### 曾国藩传 如果要问我为什么如此这般推荐曾国藩,我只能说,这是我认为清朝历史上屈指可数的名臣,作为清朝经历过三代皇帝的变更,三朝大臣,恪尽职守兢兢业业,嘉庆皇帝说作为大清帝国的大臣必须得干啥啥行,曾国藩从0开始学习建筑学,从0开始学习西方文明,从0开始学习任何别人能擅长自己不擅长的东西.儒士中的典范,曾子的后人.练习新军评定太平天国起义.麾下基本招纳当时神舟各路领域第一的人才,比如我们化学上的元素周期表,就是当时在曾国藩幕府效力的科学家[徐寿](https://baike.baidu.com/item/%E5%BE%90%E5%AF%BF/3672479?fr=aladdin)所做,如果不是这个人我们今天看到化学元素将是英文符号,不会全部都带金字旁.作为老师,他教出中国近代文明学贯中西的伟大门生[李鸿章](https://zh.wikipedia.org/wiki/%E6%9D%8E%E9%B4%BB%E7%AB%A0),功勋卓著.可是后来的人们因为曾国藩没有处理好[天津教案事件](https://zh.wikipedia.org/wiki/%E5%A4%A9%E6%B4%A5%E6%95%99%E6%A1%88)而倍受争议,我认为不是处理不好,真正的原因是因为__弱国无外交__,是慈禧想让曾国藩来收拾这个烂摊子刻意把这事让曾国藩当替罪羊.换作今天就是,有时候人非圣贤孰能无过,得倍受争议的活着,这显然不是曾国藩的本意,以曾国藩大人的原则和立场绝不是因为这事被别人抛石子而不去为之,相反曾大人敢于直面惨淡的人生,敢于正视淋漓的献血.如果那时候是现在习大大的中国,曾大人也会像外交官王毅一样敢于跟世界任何一个国家平起平坐. 这是我钦佩这位伟人不为人知的一面. 所以为了表达我对曾大人的尊重我送曾大人一副对联: ![](/assets/images/20181231FinalSummary/zengguofan1.webp) __求忠臣必于孝子之门__ __凡秀才当以天下为任__ #### 晚清重臣李鸿章 ![](/assets/images/20181231FinalSummary/lepetit1.webp) 这位慈祥的老人,改变了中国近代史,被西方人誉为"东方的脾斯麦",中国第一位登上美国时代周刊封面人物,中国历史上第一位欧洲考察的人,第一位... 总计创造 了 47个 中国第一. ![](/assets/images/20181231FinalSummary/lepetit2.webp) > 访问香港的李鸿章与香港总督卜力会面,站与李鸿章右侧者为刘学询,1900年7月。 ![](/assets/images/20181231FinalSummary/ChineseMinisterLi%20HongzhangAndPrinceBismarck.webp) \- 为了表示对李鸿章的尊重和敬意,俾斯麦的着装极为庄重,穿上了他极少穿的盛装——德皇所赐玉冕、红鹰大十字宝星,手拿大玉,腰挂宝剑。两人见面后,首先互相问候对方身体如何,交流了各自的身心健康问题。 脾斯麦被誉为 德国的铁血宰相,简直跟李鸿章一模一样经历三朝德皇,欧洲动荡,苦战多年终于换来了德国的统一. ![](/assets/images/20181231FinalSummary/Bismarck%2COttoF%C3%BCrst_von_und%20LiHungChang.webp) > 李鸿章与俾斯麦在首相府阳台 我用了这么多篇幅来介绍这位晚清名臣,我为什么这么崇拜这个人? 对,你问到了重点,我告诉你答案. 在我们的工作生活中,我们所从事的劳动基本都是西方文明的产物吧,计算机,物理化学,各种高科技,电路,哪种我们从几千年的中国的文化洗礼中能找到.我们饱读了中国的四书五经,可是我们反问过自己吗?读了这么多年书,没有从本质或者说从实际的实践中改变或者提过生产力,电脑,智能手机都是西方发明的,我们的时代已经进入了全盘西化的教育模式了 我们学习的英文 这东西我们的古典文学中根本找不到一个字母是能跟英语扯上关系的,我们今天用的汉语拼音是建国后和民国时期的产物. 在我自己的工作中经常会经历或者遭遇一些困难难以在断时间内克服或者解决,这个时候我就在反问自己,我怎么读了这么多年书连一个最简单的计算机理论问题都解决不了,回头想想计算机从发明的那一刻起到现在才几十年,我们从小就没有经历过计算的玩法,原理和制造,从某种意义上来说我们算不算是落后与西方文明,计算机不是中国人发明的,我们先要了解英语,了解和计算打交道的电脑语言,了解如何编写这种语言跟计算机交流,每当这个时候我内心是脆弱的,我的脆弱来自于我不够深入了解计算机内部,从我小的时候读的书都是千字文,跟这个东西扯不上关系,我应该找一个人作为我前进的榜样,这个人是谁呢,是谁在中国历史上第一位敢于学习西方文明,学贯中西,学习西方的先进文化,技术...制造自己的国家的计算机.我觉得这个人非李鸿章莫属,他虽然没有造计算机,但是他每天的工作都是面对着自己极大挑战的任务,他每天要处理洋人的事物,从德国克虏伯公司购买军舰创造北洋舰队,创办江南制造总局制造枪械兵器.用今天的话说,你首先得有车床,得有冶炼金属的技艺,得有技术,这位李大人可谓是中国第一个敢于解放思想观念接受西方文化和教育.学习西方先进文明技术来改造清朝的能臣了. 在西学东进的浪潮中,敢于像西方学习得人,非李鸿章莫属,我心中佩服这样的人,我今天工作中遇到的困难都是来自于西方的文明技术.有时候我们对于一些困难感到恐惧,其实恐惧源于未知,你不知道这么操作会意味着什么感到心理没底,主要的原因是我们还没有完全驾驭这一切.努力弄懂深入研究实践得到结论才是唯一的解决方式. ##### 用实力去赢得别人的尊重 晚清重臣李鸿章为了练军(淮军)去镇压太平天国农民起义,自己回安徽在没有经费和职权得前提下,操练了一只新军,每次镇压太平天国起义总是败多胜少,最后迫于无奈解散了队伍,自己去投奔了恩师 曾国藩,那时候曾国藩也是练习了自己的一只新军-湘军,曾大人也跟李鸿章一样没有经费,但是各路大臣回乡操练新军中,几乎都像李鸿章一样失败,只有曾国藩的湘军不太一样,当李鸿章落魄投奔恩师的时候,曾国藩内心无比高兴,因为在某种重要的决策上他的门生李鸿章原胜于自己,李鸿章投奔门房军营去见恩师,曾国藩故意当什么都没发生,简单吩咐门房说我太忙让 少荃(李鸿章 字少荃)去临营寒舍歇息,待我有空便去探望.在曾国藩的幕府 接纳清朝大臣必须 鸣礼炮 多少响,迎接仪式必须隆重,李鸿章当时还算是一个不大的官员,按照朝廷礼节得 鸣礼炮隆重接待的,在曾国藩这里基本就跟个兵卒来了一样没啥反应,这李鸿章心理十分难受,第二天 一早,左宗棠平定西北叛乱回来,来见曾国藩,曾国藩的迎接仪式远比想象中要隆重,鸣礼炮多少响。。。迎接仪式非常隆重,这一幕李鸿章看眼在里,苦在心里,一个左宗棠连我一半的官职都不到,恩师居然这么隆重接待,我来了恩师当什么都没发生,恩师是带我登上文学殿大学士的人,是我科举考试中对我严加管教,教授我生存本领的人,现在这般场景,怎让我不心酸,哎 看来__我还得有自己的军队,打出几次胜仗,得有实力这样才能得到恩师得认可,才能去赢得别人的尊重__.我现在什么都没有,左宗棠带领一群兵卒都这么嚣张... 李鸿章当时的心酸我看完了这段故事我很理解,我相信看到这里的你也一样感同身受. 其实曾国藩的良苦用心李鸿章当时还没察觉到,作为自己的门生没有谁比曾国藩更了解李鸿章了,曾国藩为了磨平李鸿章的棱角让他受如此这么心酸,就让他尝尝这是啥滋味,让他懂得__人就得用实力去赢得别人的尊重__. 曾国藩和李鸿章的故事一开始很抵触西方文明,到最后疲于应对再到主动出击,再到学贯中西,要不是洋人的船坚炮利那怎么会有李鸿章,怎么会诞生『中国的铁血宰相脾斯麦』.李鸿章在洋务运动中开创了多少个第一,回头再看看我的成长史又开创了多少第一,第一次背井离乡,第一次加入Baidu,比较早的接触iOS开发,第一次用博客记录知识技艺,第一次走出农村用计算机技术改变命运,成为村里人的希望,我完全就是再走李鸿章的老路,希望走在成为圣贤的路上,这条路上一定没有『前方300米有闯红灯拍照』,有前人的经验让我很幸运,但再往后会遇到前人都没经历过得东西,我将成为别人的前人. 故事讲完了 我们再来看看今天的我们,我们心目中按照王阳明的《知行合一》大家也都佩服有实力的人吧. 如果你觉得一个富二代 爹妈在一线二线城市给他买套房 帮他摆平了他需要的一切,这个人还没个正经工作,每天都过着养尊处优的生活你会佩服他吗?这不就是我们身边的一些人嘛? 我相信你一定不佩服他,你佩服他的是他有如此有正事的父母. ![](/assets/images/20181231FinalSummary/5YearsPlan.webp) 我从2013年开始制定自己的第一个五年计划,现在2018年了,我没有给自己一个满意的答卷,看着我的五年计划 勉强完成70%的程度,我很惭愧. 我承认我说了大话,我向那些看到我制定五年计划的各位,以及我的高中老师,深表歉意. 我确实在富二代的面前抬不起头,我的父亲没有正事,一切需要我自己白手起家. 于是我给自己写了句座右铭: __埋头苦干十年,与富二代抬头相见.__ #### 毛泽东传 这是我今年的地四本书,听完感觉毛爷爷的故事,真的不容易,这个人很有气魄和诗人才华.在内外交困的新中国如果从零开始着手准备未来几十年的任务是不容易的,毛爷爷做到.从小生活在地主的家庭中没有因为父亲的小农思想而影响. #### 周总理的最后600天 这位共和国总理的声音你都需要听到,这也是我第一次听到周总理的原声录音,在最后的600多天的日子里,这位总理可谓鞠躬尽瘁日理万机,与江青团伙斗智斗勇,努力纠正文革的错误,把共和国的重任一步一步交给邓小平,没有小平爷爷的改革开放,那你今天也许就不会看到我写的博客文章来总结我的2018,周总理的勤俭值得我去学习,由于医疗条件不是很完备,当时的新中国还没有能力制造一些高级药品,周总理的药都是国外进口,可是这位总理节俭到当药片掉地上很珍惜的捡起来吃,他说,我多么希望我们共和国也能制造这种药去帮助那些像我一样受病痛折磨的人民.在最后的岁月里与病魔坚强抗争. 为了纪念这位伟人,我认真的听完讲解周总理的最后岁月,我很荣幸能在中国国家博物馆里找到总理的几件物品,让我瞻仰一下这位共和国总理的爱戴人民的精神和气质. ![](/assets/images/20181231FinalSummary/hatOfZhouPrimeMinister1.webp) ![](/assets/images/20181231FinalSummary/hatOfZhouPrimeMinister2.webp) 这是周总理参加日内瓦会议的礼帽 也就是下面这张我们在初高中历史书上经常看到的 ![](/assets/images/20181231FinalSummary/hatOfZhouPrimeMinister3.webp) _图片引用自百度百科_ 我相信你看到这位共和国总理也会情不自禁的潸然泪下,他的声音你需要听到. #### 普京传记 这位前克格勃特工,俄罗斯的硬汉,对于苏联集体后的经济重建,强硬铁腕解决俄罗斯寡头的财务霸权,车臣战争的终结者,克里米亚地区的国际精神,敢于在世界上与美国强硬对抗. 普京传记确实记录的很详细,不过 就我个人观点 俄罗斯现在国民经济没有改善多好,依然是两架马车式的发展. 为了了解世界的历史和欧洲的地理我今年 学习很多欧洲的内容, 这本书就是在学习范围内. #### 习近平的七年知青岁月 在我小时候父母经常跟我说学习不好就挑大粪,但是我要说的是这位现任中华人民共和国共和国的主席就是曾经挑过粪,在陕北梁家河一步一步从知青下乡到村支书再到加入中国共产党,恢复高考继续上清华学习,最终成为中华人民共和国的国家主席,这一路走真的很不平凡,这本书让我记忆犹新的一个故事值得我们每个人去学习. 当年习大大 作为知青刚下乡, 由于不够了解农村的百姓的生活,加上陕北十分贫困,知青们需要自己学会做饭,自己学会捡柴烧火,由于第一次没有经验,刚去梁家河的一段日子赶上陕北下雨, 做饭的柴都被雨淋湿了,没办法引火,知青们你瞅瞅我我瞅瞅你,互相不知所措,这种日子实在太过艰难, 习大大下乡的时候满满的一包又一包的都是书,在这段日子里白天干农活晚上吃完饭还要继续学习,由于没有什么吃的晚上学到很晚的时候会觉得饿,就简单 锅里放点水 放里两碎玉米,柴这种资源是有限的 所以煮完的玉米 也不知熟没熟就吃了,这段艰苦的生活我相信在看这篇文章的年轻人也许都理解不了,但是过的确实无比艰难,由于陕北的底线多山和高坡 不能积水加上田地是有限的,习大大带领全村的人民修堤坝蓄水灌溉,然后开垦农田.习大大深知陕北为什么贫穷因为百姓的田地实在太少,只能像大地要粮食,建完堤坝开山拓田地,办沼气池,当年还没有好的机械,全部都是人工挑这粪便装到沼气坑.沼气池需要技术,习大大又探访四川学习办沼气的经验,沼气池刚投入使用之前需要把沼气弄个洞来检测沼气是否能够达标使用,习大大亲自开洞,当时由于池内压力很大,喷了一身粪汤, 看到这的时候我不仅感叹,中国能有以为这样的主席太幸运了,这个人可真是深入基层劳动,敢于干最脏最累的活,一般干这种活的人他肯定知道底层人民的疾苦,他也一定知道如果带领祖国的广大人民脱贫致富. 如果放到现在你让一个年轻人去农村赶上下雨天做饭的柴都淋湿,根本点不着火做饭,这种苦日子真没几年轻人尝过,更别说办沼气池挑粪了. 即便你心目中的政府有多么贪腐,我觉得习大大不会,他尝过太多人没吃过的苦,受到过文革父亲的影响入了N次党才最终成功,这段经历不寻常,如果不了解这位国家领导人的历史,那我们也不会懂得现在的辛福来的多么不容易.现在其实比习大大的过去幸福多了,但是社会主义初级阶段就这样,先能解决温饱,摆脱贫困,然后再去研究奔小康发家致富,我希望习大大的人生经历能让每个人都知道,所以这本书强烈推荐. #### 乔布斯传 作为一名 苹果开发者 如果不去了解创造苹果的人其实是一种悲哀,乔布斯的个人性格非常极端追求绝对机制,如果没有这么刻薄的追求就不会有今天的iPhone, 当然光有这种气质是不够的,需要有技术驱动,沃兹尼亚克就是其中之一.数学天才,第一台 Apple I 就是这个大神手工做的,用芯片叠加,电路设计..... 乔布斯是一个极具 "现实扭曲立场"的人,(接地气一点就是能忽悠),作为孤儿的乔布斯深受养父的影响,他父亲是一个木匠,这个人说:"做什么其实跟做家具是一样的,衣柜的背板不容易被人所看见,但是衣柜的外表质量也要跟内在的背板一样的品质",所以你今天拆开苹果的任何设备看看电路板就知道,黑色PCB印刷,内部电路设置和元器件都十分整齐,真的跟衣柜的背板一样质量. 例如: iPhone 4/4s 这是唯一两部乔布斯在世时发布的最后两样产品,但是现在的库克也许没有100%的遵照乔布斯的人格和习惯以及追求,整的现在的苹果手机国产的质量总出问题.这要是在乔布斯时代我相信这即便有也可以全部换新而且很少能出问题. 由于性格的极端和追求的极致导致 苹果的同事把被乔布斯伤害的同事称为"低通滤波器"(无论如何大声或者激烈大家都很低调平和的处理) 乔布斯的一生是不是传奇我不知道,我觉得至少我通过苹果的这副键盘敲出了一栋100w+楼房. ![](/assets/images/20181231FinalSummary/applekeyboard.webp) 是的 我是一个Apple iOS Developer,这位伟人不为人知的一面的值得学习. #### 货币战争1~5部 这几部货币战争非常好,虽然以小说的形式出现,但是作者自述说如果以真实的材料出现那么银行家基本不会让这本书出版,往往小说的形式可以逃避它. 一部世界经济的必须经典, 英格兰银行...罗斯柴尔德家族...布雷顿森林体系...世界经济的金本位,银本位还有我国的以物资为本位的人民币,这些背后的价值都在这本书中一一列举. #### 拿破仑传 这位活跃在欧洲半个世纪的法兰西缔造者,大约活跃在中国的清朝中晚期,出生在法国南部的科西嘉岛的拿破仑,一路驰骋政府整片西欧领土,没一次的战争都以胜利为结尾,但是因为最后的一次滑铁卢战役而毁于一生,其实我们应该 以"不以成败论英雄"的态度来欣赏这位战斗勇士. 为了更多的了解欧洲,我需要了解那里的人文和地理,所以这本书值得一看 #### 李嘉诚传 最后要说说这位励志伟人李嘉诚,这位李超人简直称霸香港又温文尔雅,其实李超人的祖先时清朝时期的文官拔贡出身(可以理解为有文化有学识的秀才).这位伟人征服全港,无论做人做事都勤勤恳恳踏踏实实,从一家做塑胶花厂商到最后收购港灯,英资企业,汇丰银行的长期合作伙伴,怡和置地,再到香港的房地产...以及收购希尔顿酒店....太多太长一言难尽 这里要说一下这个怡和置地,它的前身是[怡和洋行](https://baike.baidu.com/item/%E6%80%A1%E5%92%8C%E6%B4%8B%E8%A1%8C/5039743?fr=aladdin)(英资企业)由两名苏格兰裔英国人威廉·渣甸(William Jardine,1784年~1843年)及詹姆士·马地臣(James Matheson,一译“孖地臣”,1796年~1878年)在中国广州创办,就是当年林则徐虎门销烟,捣毁东印度公司的鸦片,实际上背后是怡和洋行在掌控,因为林则徐侵犯了怡和洋行的利益在华利益,这位渣甸老板在英国女皇面前游说,挑起了1840年的鸦片战争. ![](/assets/images/20181231FinalSummary/yihe.webp) 怡和洋行旧址 _图片引自百度百科_ 李嘉诚的一生真的从无到有,邻近坎坷,稳扎稳打,一步一步吞并收购. 让我印象深刻的是怡和大班 纽碧坚 跟李嘉诚的合作... 绝对励志的人生奋斗史 值得一看 ## 技术 这一年在技术上投入和收益没有明显提升,不过我给自己的定下的OKR(Object key result一种工作目标的实现方式类似KPI)是每个月保证产出两篇技术文章,至少有一篇质量较高经过这一年的技术写作,还是很有收获的,我从没想过能用博客记录我的生活中的点点滴滴,大多都是技术相关很少写个人生活,除了年终总结以外基本都是iOS相关的技术,不过整体上个人感觉还是很水的,2019年需要比现在更有质量和深度. 为了奖励自己一年的技术进步和对写作的坚持,2018年我买了有一个BOSE降噪耳机(¥1888),作为程序员只有几样东西视为珍宝 * 1.机械键盘 * 2.降噪耳机 这一年坚持学习python和机器学习,但是明显的进步并不多,只是稍稍了解了一些数据挖掘和加工数据.不过目前负责快手的直播业务让我锻炼了很多,每一次开发遇到的问题我都会记下来,业余时间写demo然后发表文章到博客记录世界记录你. 不过对比去年的KPI指标 去年的KPI如下: * swift4 进阶看完 * iOS Core Animation 看完 * Learn AV Foundation 要写几篇博客从上次段的位置续上 * 学会Python和数据挖掘 为机器学习做铺垫 * 多媒体相关技术深耕 * 英语水平再提高一个level 显然我没有完成30%,很惭愧,所以我把目标实现的方式从KPI变成了OKR.这样弹性实现目标以便能更好的实现2019年的目标. 这一年我很败家,买了一堆破烂花了不少钱 __机械键盘__ ![](/assets/images/20181231FinalSummary/keyboard.webp) > 这机械键盘,确实很好,周末在家写代码非常流畅顺手.¥519 __iPhone X 256G 美版__ ![](/assets/images/20181231FinalSummary/iPhoneX.webp) > 同事去美国帮忙代购的,作为一个iOS developer,我也是忍受了4年iPhone6,确实卡的不行了,¥8000 __Apple Watch 3__ ![](/assets/images/20181231FinalSummary/AppleWatch3.webp) > 这个我认为基本没啥用,就能戴在手上看个点儿,其余的功能都用不了,¥3188 __PC__ ![](/assets/images/20181231FinalSummary/PC.webp) > 为了学习机器学习买了个1050Ti,很久没DIY了,整套自己的买的装的.16G DDR4 2400内存,240G+120G两块三星固态硬盘,i5 8400 CPU, 技嘉Z370主板 总造价 ¥5547. > 我已经把旧的那个200+贱卖了. __BOSE 降噪耳机__ ![](/assets/images/20181231FinalSummary/bose.webp) > 双11 打算买个程序员梦想中的耳机 ¥1888 __小牛 N1s 动力版__ ![](/assets/images/20181231FinalSummary/n1s.webp) > 上班的代步工具.¥7399 今年败家花了 ¥26541. 然而这里面我觉得最好用的东西,一个是小牛的这个电动车,一个是降噪耳机,至于手机我觉得对于一个像我这样穿31块钱林甸鞋的人,拿着一个8000多的 iPhone X 无疑是更加鲜明的证明我是一个十足的屌丝.电动车这个东西实在解决了我很多忙,上下班无论去哪里都很方便,降噪耳机则是在工作中能很安静投入的工作.至于机械键盘 显然没啥用,但是苹果的键盘更贵,想想还是算了,也算值得吧.最没啥用的就是PC 和那块手表,这两样东西是我买东西的一个败笔. 买了这么多破烂,花了不少钱,然后我发现有些东西一定要物尽其用.如果花了很多钱却没有发挥有用的价值显然就是一种不理性的消费.. ## 坚持 每年教师节我都会给一位初中的语文老师打电话问候一下,我一直连续坚持了11年.我觉得我的初中学习真的很差,没有给这位老师留下过太好的印象,我想弥补一下,想创造一项记录,这项记录是要做在这位老师的学生中连续坚持一句问候的学生,之不之一我不知道,未来如果能的话我会尽量坚持下去,希望这位老师记得这位学生很平凡,但却做了一件不平凡的事情,希望这位老师记得这位学生来自第二良种场. 在我的人生目标中需要树立一个二良人都佩服的榜样.这位榜样就是发生在这座几乎地图都没有导航路线且被中国忽略的村庄.我梦想有一天衣锦还乡的那一天能为二良建造一座计算机博物馆,在这座博物馆中你可以尽情的学习计算机类的书籍.像我一样用所学的知识改变自己的命运. 因为在我上学的红光农场初中,二良的学生受到了太多不平等不公正的待遇,这不禁让我潸然泪下,为什么我们就在红光农场这座邻近的农场村庄抬不起头来,是因为我们落后,落后就要挨打,就要受到不公正的待遇,为了改变这一切需要我们忍受"韩信的胯下之辱". 在这所中学,老师们只对亲信子弟负责,对该农场的子弟次之,再次才能轮到像二良这种借读的学生中学习好的学生,至于像我这种既无背景也没关系,又学习不好的学生这种歧视真的刻骨铭心,参加工作后也依然没有忘记,为什么同样是中国人还搞这么具体的地域歧视. 我希望我能用自己的实践证明给这位老师看,看看到底是红光的学生坚持11年给您打电话还是二良的学生能坚持11年给你打电话,__人活的要用实力去赢得别人佩服和尊重__,我希望虽然二良和红光地域的经济差距很大,但是每个地域都有其独特性,没有哪里比哪里高低贵贱之分,要相互融合互相尊重. 少一些教育资源的倾斜,让每个学生都能得到公正平等的待遇. ## 生活 我认为人生有两堂选修课 * 1.装修 * 2.做饭 #### 装修 从17年买房到现在房子还没有下来,如果是毛坯房的话,需要自己学会装修. 厨房: 集成灶、消毒柜、蒸烤箱 水槽、净水器、垃圾处理器... 这堂课只有两种人可以逃避 * 有钱人 * 买二手房的人 显然我不是这两种人,既没钱也没买二手房,需要经历有限的装修经费,捉襟见肘的装修. 在房子没下来之前我经常抽出时间了解一些相关的装修经验,我想装完修也许就成了半个装修工. #### 做饭 这是一件只要有一个人需要干的活. 如果不去了解如何下厨做饭,那也许一辈子过的很平庸,如果不去了解毛葱和小葱大葱到底做饭好不好吃,什么样的葱适合做什么样的菜那么人就不会有太多的成长. 显然上面的两堂课我没有逃课. 所谓生活,有的年轻人认为我们不需要做成功人士,享受现在的静好不就可以了么,是的你能享受现在的美好那么一定有人替你砥砺前行,那个人要么是你父母要么是你爱人. 没有被生活折磨过只有两种可能 * 第一 有人替你扛着 * 第二 别着急 还没轮到你 家人总有用钱的时候,在中国 一场大病能摧毁一个中产阶级的家庭.即便是现在的医保力度很大,但是还是经不起大风大浪 在家里的老人总说钱买不来幸福, 我想说:『钱买不来100%的幸福,成长后99%的烦恼都是因为没钱』. 我现在懂得什么叫选择比努力更重要了,努力是为了让自己有更多选择的权利. ## 故事 有一个同学,我4年零2个月没有联系,时间过的真快 一晃 快5年了,我又重新加了一下微信,为了纪念这位同学,我撰写了一篇题材为《船与灯塔》的剧本. 这么多年我总结的真理是:"父母一定要有正事,如果没有正事那子女就得为父母的没正事买单,因为父母没有积攒下儿女未来几十年需要发展的经济基础,显然儿女就得白手起家从零开始". ## 百年 今年的十一假期我去了阔别11年的天津,去看看07年去过的身影,11年前还没有GPS定位的地图,我却依然能找到方向. ![](/assets/images/20181231FinalSummary/sun.webp) 这是我在天津找到家族百年以来唯一留下仅存的几张照片,我曾祖父,曾祖母,我的父亲和姑姑.现在看到这显得无比珍贵. 我的祖先是闯关东去的东北,家谱记载地址:山东省青州府寿光县孙家神庙,甸子北住. 也就是现在的:山东省潍坊市青州市寿光县孙家村 但是我搜索地图发现了至少3处 第一处 ![](/assets/images/20181231FinalSummary/hometown1.webp) 第二处 ![](/assets/images/20181231FinalSummary/hometown2.webp) 第三处 ![](/assets/images/20181231FinalSummary/hometown.webp) 这里最有可能的是第一处 因为靠近河流有河流地面过渡区(甸子),经过百年的地域变化,可能河流流量减少,地表高处水面,时间长了形成村落和耕地. 这里如今已经高铁建成,成为了祖国的蔬菜之乡,我为此感到欣慰,也许1900年左右我的祖先曾经生活在这里. 我的曾祖父来到东北的时候赶上了日本占领东三省,那时候也许迫于生计和民族的沦落,不得不为日本人工作,也就是在现在的黑龙江省海伦市海北镇十三井子村,日本人在这里养军马,前面是东方红水库,我的曾祖父练就一身打铁的手艺,为军马钉马掌,因为东北很冷,路面结冰,防止马打滑,所以钉马掌是其中的任务之一,上边中间这张照片,是当年仅存到现在的一张日本为工匠照的相片. 想想那个时候真是国破山河在,家书抵万金.这位老人也许经历过晚清覆灭,中华民国的初建到军阀混战,在到新中国的成立,历经3个朝代的更迭. 也许换做普通人都活不到一个朝代的覆灭 不过打铁的手艺可是传承3代人,我的爷爷,叔父,现在到我的哥哥,如果在现代应该成为工匠,希望有一天我的计算机博物馆中,能存放着家族打铁工艺的绝唱. 曾祖父那一代人活的不容易. ## 总结 这一年我写了很多技术的文章,我不想在年终总结中还谈论技术,应该记录一些生活的美好回忆. 去年的这个时候我给自己的定下的KPI今年显然没有完成 2019年希望能不断学习,与时俱进,学会更多有用的知识来提高影响力. 最后,幸福是奋斗出来的,还得撸起袖子加油干,得敢于直面惨淡的人生,敢于用实力去赢得别人的尊重,这样才能成为自己羡慕的别人. URL: https://sunyazhou.com/2018/12/AwemeTransition/index.html.md Published At: 2018-12-21 10:12:07 +0000 # iOS抖音的转场动画 # 前言 这几天比较忙,今天给大家带来的是抖音的转场动画实现 废话不多说上图 ![](/assets/images/20181221AwemeTransition/transition.gif) 这里需要用到前一篇文章的上下滑[demo](https://github.com/sunyazhou13/AwemeDemo) 学习这篇文章之前推荐看下喵神的[iOS7中的ViewController转场切换](https://onevcat.com/2013/10/vc-transition-in-ios7/) 如果对转场不是很了解的话可能学习会有一些难度和疑问. ## 转场调用代码 ``` objc - (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath { AwemeListViewController *awemeVC = [[AwemeListViewController alloc] init]; awemeVC.transitioningDelegate = self; //0 // 1 UICollectionViewCell *cell = [collectionView cellForItemAtIndexPath:indexPath]; // 2 CGRect cellFrame = cell.frame; // 3 CGRect cellConvertedFrame = [collectionView convertRect:cellFrame toView:collectionView.superview]; //弹窗转场 self.presentScaleAnimation.cellConvertFrame = cellConvertedFrame; //4 //消失转场 self.dismissScaleAnimation.selectCell = cell; // 5 self.dismissScaleAnimation.originCellFrame = cellFrame; //6 self.dismissScaleAnimation.finalCellFrame = cellConvertedFrame; //7 awemeVC.modalPresentationStyle = UIModalPresentationOverCurrentContext; //8 self.modalPresentationStyle = UIModalPresentationCurrentContext; //9 [self.leftDragInteractiveTransition wireToViewController:awemeVC]; [self presentViewController:awemeVC animated:YES completion:nil]; } ``` `0` 处代码使我们需要把当前的类做为转场的代理 `1` 这里我们要拿出cell这个view `2` 拿出当前Cell的frame坐标 `3` cell的坐标转成屏幕坐标 `4` 设置弹出时候需要cell在屏幕的位置坐标 `5` 设置消失转场需要的选中cell视图 `6` 设置消失转场原始cell坐标位置 `7` 设置消失转场最终得cell屏幕坐标位置 用于消失完成回到原来位置的动画 `8` 设置弹出得vc弹出样式 这个用于显示弹出VC得时候 默认底部使blua的高斯模糊 `9` 设置当前VC的模态弹出样式为当前的弹出上下文 > 5~7 步设置的消失转场动画 下面会讲解 这里我们用的是前面讲上下滑的VC对象 大家不必担心 当它是一个普通的UIViewController即可 ## 实现转场所需要的代理 首先在需要实现`UIViewControllerTransitioningDelegate`这个代理 ``` objc #pragma mark - #pragma mark - UIViewControllerAnimatedTransitioning Delegate - (nullable id )animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source { return self.presentScaleAnimation; //present VC } - (nullable id )animationControllerForDismissedController:(UIViewController *)dismissed { return self.dismissScaleAnimation; //dismiss VC } - (nullable id )interactionControllerForDismissal:(id )animator { return self.leftDragInteractiveTransition.isInteracting? self.leftDragInteractiveTransition: nil; } ``` 这里面我们看到我们分别返回了 * 弹出动画实例`self.presentScaleAnimation` * dismiss动画实例`self.dismissScaleAnimation` * 以及`self.leftDragInteractiveTransition`实例用于负责转场切换的具体实现 所以我们需要在 当前的VC中声明3个成员变量 并初始化 ``` objc @property (nonatomic, strong) PresentScaleAnimation *presentScaleAnimation; @property (nonatomic, strong) DismissScaleAnimation *dismissScaleAnimation; @property (nonatomic, strong) DragLeftInteractiveTransition *leftDragInteractiveTransition; ``` 并在`viewDidLoad:`方法中初始化一下 ``` objc //转场的两个动画 self.presentScaleAnimation = [[PresentScaleAnimation alloc] init]; self.dismissScaleAnimation = [[DismissScaleAnimation alloc] init]; self.leftDragInteractiveTransition = [DragLeftInteractiveTransition new]; ``` 这里我说一下这三个成员都负责啥事 首先`DragLeftInteractiveTransition`类负责转场的 手势 过程,就是pan手势在这个类里面实现,并继承自`UIPercentDrivenInteractiveTransition`类,这是iOS7以后系统提供的转场基类必须在`interactionControllerForDismissal:`代理协议中返回这个类或者子类的实例对象,所以我们生成一个成员变量`self.leftDragInteractiveTransition` 其次是弹出present和消失dismiss的动画类,这俩类其实是负责简单的手势完成之后的动画. 这两个类都是继承自NSObject并实现`UIViewControllerAnimatedTransitioning`协议的类,这个协议里面有 需要你复写某些方法返回具体的动画执行时间,和中间过程中我们需要的相关的容器视图以及控制器的视图实例,当我们自己执行完成之后调用相关的block回答告知转场是否完成就行了. ``` objc @implementation PresentScaleAnimation - (NSTimeInterval)transitionDuration:(id )transitionContext{ return 0.3f; } - (void)animateTransition:(id )transitionContext{ UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey]; if (CGRectEqualToRect(self.cellConvertFrame, CGRectZero)) { [transitionContext completeTransition:YES]; return; } CGRect initialFrame = self.cellConvertFrame; UIView *containerView = [transitionContext containerView]; [containerView addSubview:toVC.view]; CGRect finalFrame = [transitionContext finalFrameForViewController:toVC]; NSTimeInterval duration = [self transitionDuration:transitionContext]; toVC.view.center = CGPointMake(initialFrame.origin.x + initialFrame.size.width/2, initialFrame.origin.y + initialFrame.size.height/2); toVC.view.transform = CGAffineTransformMakeScale(initialFrame.size.width/finalFrame.size.width, initialFrame.size.height/finalFrame.size.height); [UIView animateWithDuration:duration delay:0 usingSpringWithDamping:0.8 initialSpringVelocity:1 options:UIViewAnimationOptionLayoutSubviews animations:^{ toVC.view.center = CGPointMake(finalFrame.origin.x + finalFrame.size.width/2, finalFrame.origin.y + finalFrame.size.height/2); toVC.view.transform = CGAffineTransformMakeScale(1, 1); } completion:^(BOOL finished) { [transitionContext completeTransition:YES]; }]; } @end ``` 很简单. 消失的动画 同上边差不多 ``` objc @interface DismissScaleAnimation () @end @implementation DismissScaleAnimation - (instancetype)init { self = [super init]; if (self) { _centerFrame = CGRectMake((ScreenWidth - 5)/2, (ScreenHeight - 5)/2, 5, 5); } return self; } - (NSTimeInterval)transitionDuration:(id )transitionContext{ return 0.25f; } - (void)animateTransition:(id )transitionContext{ UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey]; // UINavigationController *toNavigation = (UINavigationController *)[transitionContext viewControllerForKey:UITransitionContextToViewControllerKey]; // UIViewController *toVC = [toNavigation viewControllers].firstObject; UIView *snapshotView; CGFloat scaleRatio; CGRect finalFrame = self.finalCellFrame; if(self.selectCell && !CGRectEqualToRect(finalFrame, CGRectZero)) { snapshotView = [self.selectCell snapshotViewAfterScreenUpdates:NO]; scaleRatio = fromVC.view.frame.size.width/self.selectCell.frame.size.width; snapshotView.layer.zPosition = 20; }else { snapshotView = [fromVC.view snapshotViewAfterScreenUpdates:NO]; scaleRatio = fromVC.view.frame.size.width/ScreenWidth; finalFrame = _centerFrame; } UIView *containerView = [transitionContext containerView]; [containerView addSubview:snapshotView]; NSTimeInterval duration = [self transitionDuration:transitionContext]; fromVC.view.alpha = 0.0f; snapshotView.center = fromVC.view.center; snapshotView.transform = CGAffineTransformMakeScale(scaleRatio, scaleRatio); [UIView animateWithDuration:duration delay:0 usingSpringWithDamping:0.8 initialSpringVelocity:0.2 options:UIViewAnimationOptionCurveEaseInOut animations:^{ snapshotView.transform = CGAffineTransformMakeScale(1.0f, 1.0f); snapshotView.frame = finalFrame; } completion:^(BOOL finished) { [transitionContext finishInteractiveTransition]; [transitionContext completeTransition:YES]; [snapshotView removeFromSuperview]; }]; } @end ``` 我们重点需要说一下 转场过渡的类`DragLeftInteractiveTransition`继承自`UIPercentDrivenInteractiveTransition`负责转场过程, 头文件的声明 ``` objc @interface DragLeftInteractiveTransition : UIPercentDrivenInteractiveTransition /** 是否正在拖动返回 标识是否正在使用转场的交互中 */ @property (nonatomic, assign) BOOL isInteracting; /** 设置需要返回的VC @param viewController 控制器实例 */ -(void)wireToViewController:(UIViewController *)viewController; @end ``` 实现 ``` objc @interface DragLeftInteractiveTransition () @property (nonatomic, strong) UIViewController *presentingVC; @property (nonatomic, assign) CGPoint viewControllerCenter; @property (nonatomic, strong) CALayer *transitionMaskLayer; @end @implementation DragLeftInteractiveTransition #pragma mark - #pragma mark - override methods 复写方法 -(CGFloat)completionSpeed{ return 1 - self.percentComplete; } - (void)updateInteractiveTransition:(CGFloat)percentComplete { NSLog(@"%.2f",percentComplete); } - (void)cancelInteractiveTransition { NSLog(@"转场取消"); } - (void)finishInteractiveTransition { NSLog(@"转场完成"); } - (CALayer *)transitionMaskLayer { if (_transitionMaskLayer == nil) { _transitionMaskLayer = [CALayer layer]; } return _transitionMaskLayer; } #pragma mark - #pragma mark - private methods 私有方法 - (void)prepareGestureRecognizerInView:(UIView*)view { UIPanGestureRecognizer *gesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handleGesture:)]; [view addGestureRecognizer:gesture]; } #pragma mark - #pragma mark - event response 所有触发的事件响应 按钮、通知、分段控件等 - (void)handleGesture:(UIPanGestureRecognizer *)gestureRecognizer { UIView *vcView = gestureRecognizer.view; CGPoint translation = [gestureRecognizer translationInView:vcView.superview]; if(!self.isInteracting && (translation.x < 0 || translation.y < 0 || translation.x < translation.y)) { return; } switch (gestureRecognizer.state) { case UIGestureRecognizerStateBegan:{ //修复当从右侧向左滑动的时候的bug 避免开始的时候从又向左滑动 当未开始的时候 CGPoint vel = [gestureRecognizer velocityInView:gestureRecognizer.view]; if (!self.isInteracting && vel.x < 0) { self.isInteracting = NO; return; } self.transitionMaskLayer.frame = vcView.frame; self.transitionMaskLayer.opaque = NO; self.transitionMaskLayer.opacity = 1; self.transitionMaskLayer.backgroundColor = [UIColor whiteColor].CGColor; //必须有颜色不能透明 [self.transitionMaskLayer setNeedsDisplay]; [self.transitionMaskLayer displayIfNeeded]; self.transitionMaskLayer.anchorPoint = CGPointMake(0.5, 0.5); self.transitionMaskLayer.position = CGPointMake(vcView.frame.size.width/2.0f, vcView.frame.size.height/2.0f); vcView.layer.mask = self.transitionMaskLayer; vcView.layer.masksToBounds = YES; self.isInteracting = YES; } break; case UIGestureRecognizerStateChanged: { CGFloat progress = translation.x / [UIScreen mainScreen].bounds.size.width; progress = fminf(fmaxf(progress, 0.0), 1.0); CGFloat ratio = 1.0f - progress*0.5f; [_presentingVC.view setCenter:CGPointMake(_viewControllerCenter.x + translation.x * ratio, _viewControllerCenter.y + translation.y * ratio)]; _presentingVC.view.transform = CGAffineTransformMakeScale(ratio, ratio); [self updateInteractiveTransition:progress]; break; } case UIGestureRecognizerStateCancelled: case UIGestureRecognizerStateEnded:{ CGFloat progress = translation.x / [UIScreen mainScreen].bounds.size.width; progress = fminf(fmaxf(progress, 0.0), 1.0); if (progress < 0.2){ [UIView animateWithDuration:progress delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{ CGFloat w = [UIScreen mainScreen].bounds.size.width; CGFloat h = [UIScreen mainScreen].bounds.size.height; [self.presentingVC.view setCenter:CGPointMake(w/2, h/2)]; self.presentingVC.view.transform = CGAffineTransformMakeScale(1.0f, 1.0f); } completion:^(BOOL finished) { self.isInteracting = NO; [self cancelInteractiveTransition]; }]; }else { _isInteracting = NO; [self finishInteractiveTransition]; [_presentingVC dismissViewControllerAnimated:YES completion:nil]; } //移除 遮罩 [self.transitionMaskLayer removeFromSuperlayer]; self.transitionMaskLayer = nil; } break; default: break; } } #pragma mark - #pragma mark - public methods 公有方法 -(void)wireToViewController:(UIViewController *)viewController { self.presentingVC = viewController; self.viewControllerCenter = viewController.view.center; [self prepareGestureRecognizerInView:viewController.view]; } @end ``` 我们对外提供了一个`wireToViewController:`方法用于外部需要创建转场使用. 前面的代码我们发现有一处 ``` objc [self.leftDragInteractiveTransition wireToViewController:awemeVC]; [self presentViewController:awemeVC animated:YES completion:nil]; ``` 这里就是需要把我们要弹出的上下滑VC实例传进来,进来之后为VC的`self.view`加个`pan`手势, 复写方法中我们可以看到相关开始结束 完成过程的百分比相关方法复写 ``` objc #pragma mark - #pragma mark - override methods 复写方法 -(CGFloat)completionSpeed{ return 1 - self.percentComplete; } - (void)updateInteractiveTransition:(CGFloat)percentComplete { NSLog(@"%.2f",percentComplete); } - (void)cancelInteractiveTransition { NSLog(@"转场取消"); } - (void)finishInteractiveTransition { NSLog(@"转场完成"); } ``` 看是手势 出发前 先检查一下是否如下条件 ``` objc UIView *vcView = gestureRecognizer.view; CGPoint translation = [gestureRecognizer translationInView:vcView.superview]; if(!self.isInteracting && (translation.x < 0 || translation.y < 0 || translation.x < translation.y)) { return; } ``` 拿出手势作用的视图,然后坐标转换,判断当前是否已经开始了动画,如果没开始 或者x坐标 < y坐标是判断当前是否是超过边界范围等等异常case处理. 开始的时候需要注意下 ``` objc //修复当从右侧向左滑动的时候的bug 避免开始的时候从又向左滑动 当未开始的时候 CGPoint vel = [gestureRecognizer velocityInView:gestureRecognizer.view]; if (!self.isInteracting && vel.x < 0) { self.isInteracting = NO; return; } ``` 然后 开始的时候加个蒙版做为view.mask 这样是为了解决tableView 超出contentSize的范围要隐藏 剩下的就是中间过程 __关键的核心代码__ ``` objc [self updateInteractiveTransition:progress]; ``` > 更新转场的进度 这是这个类的自带方法,调用就行了 最后 手势结束 ``` objc CGFloat progress = translation.x / [UIScreen mainScreen].bounds.size.width; progress = fminf(fmaxf(progress, 0.0), 1.0); if (progress < 0.2){ [UIView animateWithDuration:progress delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{ CGFloat w = [UIScreen mainScreen].bounds.size.width; CGFloat h = [UIScreen mainScreen].bounds.size.height; [self.presentingVC.view setCenter:CGPointMake(w/2, h/2)]; self.presentingVC.view.transform = CGAffineTransformMakeScale(1.0f, 1.0f); } completion:^(BOOL finished) { self.isInteracting = NO; [self cancelInteractiveTransition]; }]; }else { _isInteracting = NO; [self finishInteractiveTransition]; [_presentingVC dismissViewControllerAnimated:YES completion:nil]; } //移除 遮罩 [self.transitionMaskLayer removeFromSuperlayer]; self.transitionMaskLayer = nil; ``` > 这里设置0.2的容差 如果你觉得这个应该开放接口设置可自行封装. 当用户取消的话记得调用`cancelInteractiveTransition`方法取消 完成的话调用`finishInteractiveTransition`完成转场 # 总结 整个过程还是比较简单的 如果看过喵神的文章将会更加清晰的了解转场的三个过程 就是 弹出和消失动画 以及一个中间转场过程需要我们熟悉. 优化点: 在原开源工程中的demo转场右滑是有bug的,我做了一下如下判断 ``` objc //修复当从右侧向左滑动的时候的bug 避免开始的时候从又向左滑动 当未开始的时候 CGPoint vel = [gestureRecognizer velocityInView:gestureRecognizer.view]; if (!self.isInteracting && vel.x < 0) { self.isInteracting = NO; return; } ``` `vel`这个变量 其实是判断当我们从右侧划入返回.修复了原来开源的一个bug 还有 原来开源中`tableView`的`contentSize`以外 区域露在外部,我用了一个mask的蒙版遮住了显示在外的区域. 唯一有些许遗憾的地方是抖音的左滑返回时候,有背景遮盖透明的渐变.这里由于时间关系和篇幅限制我没有花足够的时间调研.后续完善,写的不好请大家多多指教 [最终得Demo在这里](https://github.com/sunyazhou13/AwemeDemoTransition) URL: https://sunyazhou.com/2018/12/DetectingPanGestureDirection/index.html.md Published At: 2018-12-06 13:59:46 +0000 # 探测UIPanGesture的滑动方向 # 前言 这几天遇到一个问题 就是拖动手势作用在一个view上的时候 无法区分方向 于是找到stackOverFlow上的答案 记录一下 ``` objc - (void)panRecognized:(UIPanGestureRecognizer *)rec { CGPoint vel = [rec velocityInView:self.view]; if (vel.x > 0) { // user dragged towards the right 向右拖动 } else { // user dragged towards the left 向左拖动 } } ``` [参考](https://stackoverflow.com/questions/11777281/detecting-the-direction-of-pan-gesture-in-ios) URL: https://sunyazhou.com/2018/11/LikeAnimation/index.html.md Published At: 2018-11-27 11:16:14 +0000 # iOS抖音点赞动画实现 # 前言 hi 大家好 又跟大家见面了,今天给大家分享的是抖音的点赞动画的实现, 废话不多说上图 ![](/assets/images/20181127LikeAnimation/likeAnimation1.gif) 本篇文章主要包含技术点: * CAShapeLayer和贝赛尔曲线绘制三角形 * 组合动画的时间技巧 我习惯写完文章的demo都附在文章底部,如果不想看原理的小伙伴可直接跳到底部找demo下载. # 实现原理 首先 我们来详细分解一下这个动画 ![](/assets/images/20181127LikeAnimation/likeAnimation2.gif) > 请仔细观察 我们来看单独的一个动画 ![](/assets/images/20181127LikeAnimation/likeAnimation3.gif) > 请仔细观看 我设置10秒的duration 以便于大家能看清楚 ## 实现原理 从上述两张图中,我们可以看到 它是一个 三角形的贝塞尔曲线 ![](/assets/images/20181127LikeAnimation/likeAnimation4.webp) 这样的动画需要经过: * 2π (360°)旋转一周 * 圆一周一共有六个 三角形的贝赛尔曲线图形形状. * 一个动画组 内部包含缩放动画 从0~1的放大 ,动画如果执行10秒,那么 scale缩放动画执行 10*0.2 = 2秒, 动画组中还包含另一个动画是 从结束位置的动画到结尾消失的位置大小变化直到动画消失. * 沿着圆形每 60°角度 创建一个上图的三角形图形. 说了这么多 实际就是用`CABasicAnimation`的keypath是`path`和`CABasicAnimation`的`keypath`是`transform.scale`的动画组合在一起作用于一个三角形上,并且一共创建6个三角形图形. 结束的时候大概是这样的 ![](/assets/images/20181127LikeAnimation/likeAnimation5.gif) 结束的时候实际上是一个从 上一次动画执行完成的path向 一条线上三个点的path过渡的过程,直到最后隐藏消失. 好下面我们来实现一下这个动画 > 注意: 背景的❤️红心是 一张图不在本篇讲述范围 ## 代码实现 首先我们子类话一个`ZanLikeView`继承自`UIView`并设置底部的图片和点击变换的❤️图片,就是两张UIImageView加手势,当点击的时候区分不同view的tag就知道哪个imageview点击,这样就可以做两张动画不同的效果了,不过这些可以参考demo. 我主要介绍核心代码 创建 `CAShapeLayer`用于做形状图形相关的图形动画. ``` objc CAShapeLayer *layer = [[CAShapeLayer alloc]init]; layer.position = _likeBefore.center; layer.fillColor = [UIColor redColor].CGColor; ``` > 颜色最终可对外暴露接口 for循环每 30°角创建一个上述的三角形.我们需要创建 6个 就循环6次 创建初始位置的贝塞尔path ``` objc CGFloat length = 30; CGFloat duration = 0.5f; for(int i = 0 ; i < 6; i++) { CAShapeLayer *layer = [[CAShapeLayer alloc]init]; layer.position = _likeBefore.center; layer.fillColor = [[UIColor redColor].CGColor; //... 1 //... 2 //... 3 } ``` > 这里我们一共创建6个shapeLayer的实例并填充成颜色,我们这里填充的是红色 其它的颜色可自行封装. > _likeBefore 是我们看到白色的❤️背景视图(UIImageView) 下面 在`//... 1`的地方加入如下代码 ``` objc UIBezierPath *startPath = [UIBezierPath bezierPath]; [startPath moveToPoint:CGPointMake(-2, -length)]; [startPath addLineToPoint:CGPointMake(2, -length)]; [startPath addLineToPoint:CGPointMake(0, 0)]; ``` 这行代码加完就是这样的图形 ![](/assets/images/20181127LikeAnimation/likeAnimation4.webp) 然后创建完成我们需要把path给layer.path. 记得转成CGPath ``` objc layer.path = startPath.CGPath; layer.transform = CATransform3DMakeRotation(M_PI / 3.0f * i, 0.0, 0.0, 1.0); [self.layer addSublayer:layer] ``` > 注: CATransform3DMakeRotation()函数 当x,y,z值为0时,代表在该轴方向上不进行旋转,当值为-1时,代表在该轴方向上进行逆时针旋转,当值为1时,代表在该轴方向上进行顺时针旋转 > 因为我们是需要60°创建一个layer所以需要顺时针 M_PI / 3.0f = 60°. 每循环一次则创建第N个角度`乘`60°. 接着在`//... 2`添加如下代码 ``` objc //动画组 CAAnimationGroup *group = [[CAAnimationGroup alloc] init]; group.removedOnCompletion = NO; group.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; group.fillMode = kCAFillModeForwards; group.duration = duration; //缩放动画 CABasicAnimation *scaleAnim = [CABasicAnimation animationWithKeyPath:@"transform.scale"]; scaleAnim.fromValue = @(0.0); scaleAnim.toValue = @(1.0); scaleAnim.duration = duration * 0.2f; //注意这里是在给定时长的地方前0.2f的时间里执行缩放 ``` > 这里说下duration * 0.2f. 比如我给定 10秒的duration,那么 duration * 0.2 = 2 秒执行缩放. 最后在`//... 3`的代码出加上如下代码 ``` objc //结束点 UIBezierPath *endPath = [UIBezierPath bezierPath]; [endPath moveToPoint:CGPointMake(-2, -length)]; [endPath addLineToPoint:CGPointMake(2, -length)]; [endPath addLineToPoint:CGPointMake(0, -length)]; CABasicAnimation *pathAnim = [CABasicAnimation animationWithKeyPath:@"path"]; pathAnim.fromValue = (__bridge id)layer.path; pathAnim.toValue = (__bridge id)endPath.CGPath; pathAnim.beginTime = duration * 0.2f; pathAnim.duration = duration * 0.8f; [group setAnimations:@[scaleAnim, pathAnim]]; [layer addAnimation:group forKey:nil]; ``` 这几行代码的意识是 从我们上一个layer的path位置开始向我们结束位置的path过渡,并且注意开始时间 `pathAnim.beginTime`是 duration * 0.2也就是说 在上一个动画结束的时间点才开始结束过渡,过渡的时长剩余是duration * 0.8.这样两个连贯在一起的动画就执行完了,最后把动画加到动画组 天加给layer. 下图是从开始到结束点过渡的动画. ![](/assets/images/20181127LikeAnimation/likeAnimation5.gif) 剩余的工作就是做个普通的动画的 基本没什么了. ``` objc [UIView animateWithDuration:0.35f delay:0.0f options:UIViewAnimationOptionCurveEaseIn animations:^{ self.likeAfter.transform = CGAffineTransformScale(CGAffineTransformMakeRotation(-M_PI_4), 0.1f, 0.1f); } completion:^(BOOL finished) { [self.likeAfter setHidden:YES]; self.likeBefore.userInteractionEnabled = YES; self.likeAfter.userInteractionEnabled = YES; }]; ``` #### 技巧 结束动画的开始时间和结束时间控制,恰到好处. # 总结 动画实现的细节需要研究和学习和实践,在这里感谢开源作者的代码给了思路, 我个人通过学习和模仿整理出原理写出代码校验并增加相关对外接口. [点击下载Demo](https://github.com/sunyazhou13/LikeDemo) [直接下载zip](https://github.com/sunyazhou13/LikeDemo/archive/master.zip) URL: https://sunyazhou.com/2018/11/PlayLoadingAnimation/index.html.md Published At: 2018-11-14 14:14:39 +0000 # iOS视频加载动画 # 前言 这几天一直跟开源的抖音demo斗智斗勇,今天跟大家分享的是抖音中或者快手中加载视频的动画 上图看成品 ![](/assets/images/20181114PlayLoadingAnimation/playloading.gif) # 实现原理 首先我创建一个视图 ``` objc @interface ViewController () @property (nonatomic, strong) UIView *playLoadingView; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; //init player status bar self.playLoadingView = [[UIView alloc]init]; self.playLoadingView.backgroundColor = [UIColor whiteColor]; [self.playLoadingView setHidden:YES]; [self.view addSubview:self.playLoadingView]; //make constraintes [self.playLoadingView mas_makeConstraints:^(MASConstraintMaker *make) { make.center.equalTo(self.view); make.width.mas_equalTo(1.0f); //宽 1 dp make.height.mas_equalTo(0.5f); //高 0.5 dp }]; [self startLoadingPlayAnimation:YES]; //调用动画代码 } ``` > 这里我们可以看到 我们实际上创建的是一个 1pt宽度 0.5 pt的宽度 的视图 紧接着动画实现的代码 ``` objc - (void)startLoadingPlayAnimation:(BOOL)isStart { if (isStart) { self.playLoadingView.backgroundColor = [UIColor whiteColor]; self.playLoadingView.hidden = NO; [self.playLoadingView.layer removeAllAnimations]; CAAnimationGroup *animationGroup = [[CAAnimationGroup alloc] init]; animationGroup.duration = 0.5; animationGroup.beginTime = CACurrentMediaTime() + 0.5; animationGroup.repeatCount = MAXFLOAT; animationGroup.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; CABasicAnimation *scaleAnimation = [CABasicAnimation animation]; scaleAnimation.keyPath = @"transform.scale.x"; scaleAnimation.fromValue = @(1.0f); scaleAnimation.toValue = @(1.0f * ScreenWidth); CABasicAnimation *alphaAnimation = [CABasicAnimation animation]; alphaAnimation.keyPath = @"opacity"; alphaAnimation.fromValue = @(1.0f); alphaAnimation.toValue = @(0.5f); [animationGroup setAnimations:@[scaleAnimation, alphaAnimation]]; [self.playLoadingView.layer addAnimation:animationGroup forKey:nil]; } else { [self.playLoadingView.layer removeAllAnimations]; self.playLoadingView.hidden = YES; } } ``` 完事 就这几行代码 搞定 其实核心的只有4行代码 ``` objc CABasicAnimation *scaleAnimation = [CABasicAnimation animation]; scaleAnimation.keyPath = @"transform.scale.x"; scaleAnimation.fromValue = @(1.0f); scaleAnimation.toValue = @(1.0f * ScreenWidth); ``` > 关键在`scaleAnimation.keyPath = @"transform.scale.x";` 这里我们要沿着x做缩放 缩放的得值从 __1~屏幕宽度__, 当然值多大自己可以控制. 如果`@"transform.scale.y"` 则是沿着Y轴缩放 当然 如果写成`@"transform.scale"` 那就X,Y 一起缩放 大家可以试试. # 总结 本篇的动画技巧是 缩放的 `transform.scale.y` 从一个点 做layer缩放 就会出现 加载效果. [最后附上demo](https://github.com/sunyazhou13/PlayLoadingDemo) 感谢大家支持 URL: https://sunyazhou.com/2018/11/AllKeypathOfCALayer/index.html.md Published At: 2018-11-13 11:46:45 +0000 # iOS所有Animation相关可用的Keypath # 前言 在Core Animation中 我们经常使用CABasicAnimation或者它的子类做一些动画 一般情况下我们都要用到Keypath,最近在研究动画,想整理一下所有可用的Keypath在iOS的核心动画中. # CALayer的相关属性 废话不多说 我们上一段代码演示一下 这篇的主题 ``` objc CABasicAnimation * scaleAnimation = [CABasicAnimation animation]; scaleAnimation.keyPath = @"transform.scale.x"; scaleAnimation.fromValue = @(1.0f); scaleAnimation.toValue = @(1.0f * ScreenWidth); ``` 一般我们给一个View的Layer添加animation ``` objc [xxxView.layer addAnimation: scaleAnimation forKey:@"testAnimationName"]; ``` 这里面我们注意到`scaleAnimation.keyPath`它实际上是一个字符串 是一个被外部修改的成员变量的类似的东西,但是我们自己又不能随便想写写啥 这个实际上是一个layer的属性 或者成员变量. ## 全部可修改的keypath有哪些呢? ### CALayer animatable properties 动画有如下这些 ``` nchorPoint backgroundColor backgroundFilters borderColor borderWidth bounds compositingFilter contents contentsRect cornerRadius doubleSided filters frame hidden mask masksToBounds opacity position shadowColor shadowOffset shadowOpacity shadowPath shadowRadius sublayers sublayerTransform transform zPosition ``` 剩下的都是继承自CALayer ### CAEmitterLayer animatable properties: ``` emitterPosition emitterZPosition emitterSize ``` ### CAGradientLayer animatable properties ``` colors locations endPoint startPoint ``` ### CAReplicatorLayer animatable properties ``` instanceDelay instanceTransform instanceRedOffset instanceGreenOffset instanceBlueOffset instanceAlphaOffset ``` ### CAShapeLayer animatable properties ``` fillColor lineDashPhase lineWidth miterLimit strokeColor strokeStart strokeEnd ``` ### CATextLayer animatable properties ``` fontSize foregroundColor ``` ### CATransform3D Key-Value Coding Extensions(KVC的 Keypath) ``` rotation.x rotation.y rotation.z rotation scale.x scale.y scale.z scale translation.x translation.y translation.z ``` #### CGPoint keyPaths ``` x y ``` #### CGSize keyPaths ``` width height ``` ### CGRect keyPaths ``` origin origin.x origin.y size size.width size.height ``` > 还有一些附加可[参考](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CoreAnimation_guide/AnimatableProperties/AnimatableProperties.html), 以及详细内容可以参考[官方文档](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CoreAnimation_guide/Introduction/Introduction.html#//apple_ref/doc/uid/TP40004514-CH1-SW1),以及一些[结构体](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CoreAnimation_guide/Key-ValueCodingExtensions/Key-ValueCodingExtensions.html#//apple_ref/doc/uid/TP40004514-CH12-SW2). 以上就是所有我目前找到的全部动画可用的`keypath`. ## 可动画属性介绍 ### 几何属性(Geometry Properties) |可用 Key Path | 示意 | | ------| ------ | |transform.rotation.x|按x轴旋转的弧度| |transform.rotation.y|按y轴旋转的弧度| |transform.rotation.z|按z轴旋转的弧度| |transform.rotation|按z轴旋转的弧度, 和transform.rotation.z效果一样| |transform.scale.x|在x轴按比例放大缩小| |transform.scale.y|在y轴按比例放大缩小| |transform.scale.z|在z轴按比例放大缩小| |transform.scale|整体按比例放大缩小| |transform.translation.x|沿x轴平移| |transform.translation.y|沿y轴平移| |transform.translation.z|沿z轴平移| |transform.translation| x,y 坐标均发生改变 | |transform | CATransform3D 4xbounds4矩阵| |bounds|layer大小| |position|layer位置| |anchorPoint|锚点位置| |cornerRadius| 圆角大小 | |zPosition |z轴位置 | > 注意: 这里没有frame,layer的 frame 是不支持动画的,我们可以通过改变position和bounds来代替frame ### Layer内容 (Layer Content) |可用 Key Path | 示意 | | ------| ------ | |contents | Layer内容,呈现在背景颜色之上| ### 阴影属性 (Shadow Properties) |可用 Key Path | 示意 | | ------| ------ | |shadowColor|阴影颜色| |shadowOffset| 阴影偏移距离 | |shadowOpacity|阴影透明度| |shadowRadius|阴影圆角| |shadowPath|阴影轨迹| ### 透明度 (Opacity Property) |可用 Key Path | 示意 | | ------| ------ | |opacity| 透明度| ### 遮罩 (Mask Properties) |可用 Key Path | 示意 | | ------| ------ | |mask| | ### ShapeLayer属性 (ShapeLayer) |可用 Key Path | 示意 | | ------| ------ | | fillColor | 填充颜色 | | strokeColor | 描边颜色 | | strokeStart | 描边颜色开始 从无到有 | | strokeEnd | 描边颜色结束 从有到无 | | lineWidth |路径的线宽| | miterLimit | 相交长度的最大值 | | lineDashPhase | 虚线样式 | # 总结 以上是我 搜集整理 到的所有keypath仅供参考 多年前 我走在 辉煌国际到西二旗的大街上 脑袋里还在思考 为什么animation的这种keypath总是搞成字符串 整整就容易写错.今天自己的这篇文章给了答案,答案是 KVC的成员变量并没有直接获取变量名,而是要写成 字符串的变量名.内容通过字符串去做一些事情. URL: https://sunyazhou.com/2018/11/AwemeAlbumAnimation/index.html.md Published At: 2018-11-08 11:52:06 +0000 # iOS抖音右下角专辑动画 # 前言 前两天分享了 抖音 上下滑切换 ,今天给和大家分享的是抖音右小角底部的专辑动画 上图看下 ![](/assets/images/20181108AwemeAlbumAnimation/final.gif) 再看下抖音的 ![](/assets/images/20181108AwemeAlbumAnimation/AlbumAnimation.gif) # 具体实现思路 首先需要3涨素材 这个在demo中就可以找到哈 在文章底部demo中有 1. ContrainerView 2. Background Layer 3. Album (UIImageView) ![](/assets/images/20181108AwemeAlbumAnimation/album1.webp) 我们首先写个 `MusicAlbumView` 继承自UIView ``` objc @interface MusicAlbumView : UIView @property (nonatomic, strong) UIImageView *album; // 开始动画 rate 动画时间系数 - (void)startAnimation:(CGFloat)rate; // 重置视图 删除所有已添加的动画组 - (void)resetView; @end ``` ### 并提供两个接口 * 一个开始动画 * 一个重置动画 `album` 成员变量 是为了给外部加载网络图片使用 所以暴露在.h中, 例如下面的调用 ``` objc __weak __typeof(self) wself = self; //加载网络图 [self.musicAlbum.album sd_setImageWithURL:[NSURL URLWithString:@"https://www.sunyazhou.com/images/logo2.jpg"] completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL) { if(!error) { wself.musicAlbum.album.image = image; } }]; ``` ### 下面我们来看下内部如何封装 首先我们要创建背景 ``` objc - (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { self.noteLayers = [NSMutableArray array]; //专辑背景容器视图 self.albumContainer =[[UIView alloc]initWithFrame:self.bounds]; [self addSubview:self.albumContainer]; } return self; } ``` > 这里初始化的数组是为下面装动画layer使用 方便 Reset的时候 移除所有layer和动画 一个产品背景容器UIView + 一个产品背景Layer + 一个个人头像背景UIImageView 我们依次把下面代码写在`[self addSubview:self.albumContainer]`底部 添加唱片背景 ``` objc //添加唱片icon的layer CALayer *backgroudLayer = [CALayer layer]; backgroudLayer.frame = self.bounds; backgroudLayer.contents = (id)[UIImage imageNamed:@"music_cover"].CGImage; [self.albumContainer.layer addSublayer:backgroudLayer]; ``` 头像视图 ``` objc //放在唱片内部的图片 CGFloat w = CGRectGetWidth(frame) / 2.0f; CGFloat h = CGRectGetHeight(frame) / 2.0f; CGRect albumFrame = CGRectMake(w / 2.0f, h / 2.0f, w, h); self.album = [[UIImageView alloc]initWithFrame:albumFrame]; self.album.contentMode = UIViewContentModeScaleAspectFill; [self.albumContainer addSubview:self.album]; self.album.layer.cornerRadius = h / 2.0f; self.album.layer.masksToBounds = YES; ``` 然后居中对齐. #### 给`self.albumContainer.layer`加旋转 我们在外部调用startAnimation:方法的时候 给`self.albumContainer.layer`添加旋转动画旋转 ``` objc - (void)startAnimation:(CGFloat)rate { CABasicAnimation* rotationAnimation; rotationAnimation = [CABasicAnimation animationWithKeyPath:@"transform.rotation.z"]; rotationAnimation.toValue = [NSNumber numberWithFloat: M_PI * 2.0]; rotationAnimation.duration = 3.0f; rotationAnimation.cumulative = YES; rotationAnimation.repeatCount = MAXFLOAT; [self.albumContainer.layer addAnimation:rotationAnimation forKey:@"rotationAnimation"]; } ``` 加完效果是这样的 ![](/assets/images/20181108AwemeAlbumAnimation/album2.gif) #### 如何实现弧度动画 好 完成一半了 下面我们来说一下 弧度旋转. 现仔细观察一下动画的音符 ![](/assets/images/20181108AwemeAlbumAnimation/album3.gif) 这是一张音符动画 它的运动轨迹大概是这样的 ![](/assets/images/20181108AwemeAlbumAnimation/bezier1.webp) 我们其实用到的是贝塞尔曲线动画 (我画的不是很好 大家理解这个意思就好) 然后让音符的layer沿着 这个贝塞尔曲线做旋转... 其实是下面的一些列动作组合 这个需要一个动画组 包含如下动作 * 一个贝塞尔曲线运动的轨迹动画啊 * 旋转弧度 大概半圈 小一些 M_PI * 0.10 ~ M_PI * -0.10 之间旋转的动画 * 透明度 从0 到 1 在到 0 之间运动的透明度动画 * 缩放动画 从开始 1x 到 2x 之间变化 好我们来解决一下 关键的贝赛尔曲线 首先创建一个动画组 ``` objc CAAnimationGroup *animationGroup = [[CAAnimationGroup alloc]init]; animationGroup.duration = rate/4.0f; animationGroup.beginTime = CACurrentMediaTime() + delayTime; animationGroup.repeatCount = MAXFLOAT; animationGroup.removedOnCompletion = NO; animationGroup.fillMode = kCAFillModeForwards; animationGroup.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]; ``` > rate 外部传入 delayTime是 动画组开始动画的延迟的时间 我们设置 delayTime 为0就是不延时 下面解释为什么这么写 创建一个 贝赛尔曲线东动画 ``` objc //bezier路径帧动画 CAKeyframeAnimation * pathAnimation = [CAKeyframeAnimation animationWithKeyPath:@"position"]; ``` 然后把下面这坨代码加到 上面代码的底部 ``` objc CGFloat sideXLength = 40.0f; //X轴左右侧偏移量 CGFloat sideYLength = 100.0f; //Y轴上下偏移量 CGPoint beginPoint = CGPointMake(CGRectGetMidX(self.bounds) - 5, //贝赛尔曲线开始点CGRectGetMaxY(self.bounds)); CGPoint endPoint = CGPointMake(beginPoint.x - sideXLength, beginPoint.y - sideYLength); //贝塞尔曲线结束点 NSInteger controlLength = 60; //贝塞尔曲线控制点长度 CGPoint controlPoint = CGPointMake(beginPoint.x - sideXLength/2.0f - controlLength, beginPoint.y - sideYLength/2.0f + controlLength); //贝塞尔曲线控制点 UIBezierPath *customPath = [UIBezierPath bezierPath]; //创建贝塞尔轨迹 [customPath moveToPoint:beginPoint]; [customPath addQuadCurveToPoint:endPoint controlPoint:controlPoint]; //核心代码 二次曲线方程式 可以google查一下 pathAnimation.path = customPath.CGPath; //让动画沿着轨迹运动 ``` 我来解释一下 关键变量 > beginPoint 开始点: 当前视图X坐标中心 向 左偏移 5dp (X轴是左右) Y的坐标是当前视图高度 就是最下面 > endPoint 结束点: 开始点的X 减去 40左侧偏移(就是距离左侧更远) Y也是 减去偏移之后 到了 视图的外部 左上方. > controlPoint 控制点: 开始点 比如 X是 30 - 60/2.0 - 60 = -60,显然已经跑到最左边了 超出了视图范围, Y 后面是+ controlLength 说明是加大 Y坐标. 大家可以不用理解这些细节 看下面图就好了 ![](/assets/images/20181108AwemeAlbumAnimation/bezier2.webp) > customPath: 贝塞尔曲线对象 ``` objc [customPath moveToPoint:beginPoint]; //核心代码 二次曲线方程式 可以google查一下 [customPath addQuadCurveToPoint:endPoint controlPoint:controlPoint]; //让动画沿着轨迹运动 pathAnimation.path = customPath.CGPath; ``` 这就是 增加开始点 结束点 控制点之后的贝塞尔轨迹,然后 设置轨迹动画的path就完事了. 这一步搞完 然后 把`pathAnimation` 放到动画组中,然后创建一个 音符的layer添加动画组 ``` objc [animationGroup setAnimations:@[pathAnimation]]; CAShapeLayer *layer = [CAShapeLayer layer]; layer.contents = (__bridge id _Nullable)([UIImage imageNamed:imageName].CGImage); layer.frame = CGRectMake(beginPoint.x, beginPoint.y, 10, 10); [self.layer addSublayer:layer]; [self.noteLayers addObject:layer]; [layer addAnimation:animationGroup forKey:nil]; ``` > `[self.noteLayers addObject:layer];`这行代码是我们前面声明的全局变量 存layer,reset的时候删除相关layer和动画使用 我们来看下 简单一个音符 沿着贝塞尔曲线运动 ![](/assets/images/20181108AwemeAlbumAnimation/album4.gif) 好下面的工作就是 加旋转 透明 缩放动画 ``` objc //旋转帧动画 CAKeyframeAnimation * rotationAnimation = [CAKeyframeAnimation animationWithKeyPath:@"transform.rotation"]; //这里实际上是控制动画开始弧度和结束弧度 M_PI(180°) 就是半圆 * 0.10 或者 * -0.10j是为了关键点上下偏移的18°的间隙 [rotationAnimation setValues:@[ [NSNumber numberWithFloat:0], [NSNumber numberWithFloat:M_PI * 0.10], [NSNumber numberWithFloat:M_PI * -0.10]]]; //透明度帧动画 CAKeyframeAnimation * opacityAnimation = [CAKeyframeAnimation animationWithKeyPath:@"opacity"]; [opacityAnimation setValues:@[ [NSNumber numberWithFloat:0], [NSNumber numberWithFloat:0.2f], [NSNumber numberWithFloat:0.7f], [NSNumber numberWithFloat:0.2f], [NSNumber numberWithFloat:0]]]; //缩放帧动画 CABasicAnimation *scaleAnimation = [CABasicAnimation animation]; scaleAnimation.keyPath = @"transform.scale"; scaleAnimation.fromValue = @(1.0f); scaleAnimation.toValue = @(2.0f); ``` 最后添把所有的动画添加到动画组 ``` objc [animationGroup setAnimations:@[pathAnimation, scaleAnimation, rotationAnimation,opacityAnimation]]; ``` > 注意一下: 为了让音符的图片更生动我们需要把`layer.opacity = 0.0f;` 这个音符透明 从而用透明度帧动画控制透明. 然后封装好方法 把上边我们做的贝塞尔曲线 透明 渐变 缩放 动画组都放在这个方法里面 完整代码如下 ``` objc - (void)addNotoAnimation:(NSString *)imageName delayTime:(NSTimeInterval)delayTime rate:(CGFloat)rate{ CAAnimationGroup *animationGroup = [[CAAnimationGroup alloc]init]; animationGroup.duration = rate/4.0f; animationGroup.beginTime = CACurrentMediaTime() + delayTime; animationGroup.repeatCount = MAXFLOAT; animationGroup.removedOnCompletion = NO; animationGroup.fillMode = kCAFillModeForwards; animationGroup.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]; //bezier路径帧动画 CAKeyframeAnimation * pathAnimation = [CAKeyframeAnimation animationWithKeyPath:@"position"]; //X轴左右侧偏移量 CGFloat sideXLength = 40.0f; //Y轴上下偏移量 CGFloat sideYLength = 100.0f; //贝赛尔曲线开始点 CGPoint beginPoint = CGPointMake(CGRectGetMidX(self.bounds) - 5, CGRectGetMaxY(self.bounds)); //贝塞尔曲线结束点 CGPoint endPoint = CGPointMake(beginPoint.x - sideXLength, beginPoint.y - sideYLength); //贝塞尔曲线控制点长度 NSInteger controlLength = 60; //贝塞尔曲线控制点 CGPoint controlPoint = CGPointMake(beginPoint.x - sideXLength/2.0f - controlLength, beginPoint.y - sideYLength/2.0f + controlLength); //创建贝塞尔轨迹 UIBezierPath *customPath = [UIBezierPath bezierPath]; [customPath moveToPoint:beginPoint]; //核心代码 二次曲线方程式 可以google查一下 [customPath addQuadCurveToPoint:endPoint controlPoint:controlPoint]; //让动画沿着轨迹运动 pathAnimation.path = customPath.CGPath; //旋转帧动画 CAKeyframeAnimation * rotationAnimation = [CAKeyframeAnimation animationWithKeyPath:@"transform.rotation"]; //这里实际上是控制动画开始弧度和结束弧度 M_PI(180°) 就是半圆 * 0.10 或者 * -0.10j是为了关键点上下偏移的18°的间隙 [rotationAnimation setValues:@[ [NSNumber numberWithFloat:0], [NSNumber numberWithFloat:M_PI * 0.10], [NSNumber numberWithFloat:M_PI * -0.10]]]; //透明度帧动画 CAKeyframeAnimation * opacityAnimation = [CAKeyframeAnimation animationWithKeyPath:@"opacity"]; [opacityAnimation setValues:@[ [NSNumber numberWithFloat:0], [NSNumber numberWithFloat:0.2f], [NSNumber numberWithFloat:0.7f], [NSNumber numberWithFloat:0.2f], [NSNumber numberWithFloat:0]]]; //缩放帧动画 CABasicAnimation *scaleAnimation = [CABasicAnimation animation]; scaleAnimation.keyPath = @"transform.scale"; scaleAnimation.fromValue = @(1.0f); scaleAnimation.toValue = @(2.0f); [animationGroup setAnimations:@[pathAnimation, scaleAnimation, rotationAnimation,opacityAnimation]]; CAShapeLayer *layer = [CAShapeLayer layer]; layer.opacity = 0.0f; layer.contents = (__bridge id _Nullable)([UIImage imageNamed:imageName].CGImage); layer.frame = CGRectMake(beginPoint.x, beginPoint.y, 10, 10); [self.layer addSublayer:layer]; [self.noteLayers addObject:layer]; [layer addAnimation:animationGroup forKey:nil]; } ``` 在我们对外提供的startAnimation:方法中调用 ``` objc - (void)startAnimation:(CGFloat)rate { rate = fabs(rate); //check 防止 rate输入为负值 [self resetView]; //首先重置动画 //这里调用 [self addNotoAnimation:@"icon_home_musicnote1" delayTime:0.0f rate:rate]; //。。。封面的旋转动画 } ``` 写到这里大概就完成了一个音符的动画 如果像做多个音符动画 就多调用几次 然后控制好开始时间的延时 ``` objc [self addNotoAnimation:@"icon_home_musicnote1" delayTime:0.0f rate:rate]; [self addNotoAnimation:@"icon_home_musicnote2" delayTime:1.0f rate:rate]; [self addNotoAnimation:@"icon_home_musicnote1" delayTime:2.0f rate:rate]; ``` __写到这里可以看到我们实际上是 通过delayTime 延时(单位秒) 开控制 每个音符 距离上个音符的间隔时间,通过间隔时间来控制音符之间 交替 出现__. 所以上面的动画组里面有这样一行代码 ``` objc animationGroup.beginTime = CACurrentMediaTime() + delayTime; ``` 就是基于当前的时间延迟1秒或者2秒来控制 完成之后 就是这样了 ![](/assets/images/20181108AwemeAlbumAnimation/final.gif) # 总结 首先感谢开源的小伙伴 的代码,我认真研读了几遍也写了一些代码,有些东西真是 天下大事必做于细 天下难事必做于易的感受. 这里的代码实现主要分开 专辑图旋转和音符动画组的实现即可 希望和大家分享 技术技巧.写的比较凌乱 我会逐渐提高这方面的能力.希望大家多多指教 [最终的demo](https://github.com/sunyazhou13/MusicAlbumViewDemo) 参考 [iOS高仿抖音app](https://github.com/sunyazhou13/douyin-ios-objectc) URL: https://sunyazhou.com/2018/11/AwemeTopBottomScrollDemo/index.html.md Published At: 2018-11-06 17:55:09 +0000 # iOS抖音的上下滑实现 # 前言 一直一来都在 研究抖音App做的短视频 上下滑动 的技术实现, 今天写了个demo,方便学习技术技巧和记录知识, ![](/assets/images/20181106AwemeTopBottomScrollDemo/AwemeDemo1.gif) # 技术实现原理 * UITableView 其实就是一个UITableView改变上下显示范围. talk is cheap show me the code 我说话不绕弯子,代码如下 实现起来非常简单 ``` objc _tableView = [[UITableView alloc] initWithFrame:CGRectMake(0, -SCREEN_HEIGHT, SCREEN_WIDTH, SCREEN_HEIGHT * 5)]; _tableView.contentInset = UIEdgeInsetsMake(SCREEN_HEIGHT, 0, SCREEN_HEIGHT * 3, 0); ``` 1. 初始化的时候,TableView放在屏幕外边. 2. contentInset 显示内容的内边距, 以此是 `上`, `左`, `下`, `右`, 上边距 距离整好屏幕高度,底部 是 顶部边距(屏幕高度的 3倍) 方便滑动, 左右分别顶到两边 搞定. 我画个图演示一下. ![](/assets/images/20181106AwemeTopBottomScrollDemo/AwemeDemo2.webp) 看到这张图 大家也许 已经明白了,最核心的地方是控制 TableView的上下边距,上边距留够一个屏幕高度,下边距留够下滑3屏左右的缓冲. # 说一下用到的技巧 创建tableView很简单 如果理解不了 可以下载文章末尾demo 有个小技巧是 如何做到 上下滑动 能够完整的 滑动到对应位置 整好 占满屏幕类似 开启了UIScrollView的 `pagingEnabled`. ## 实现滑动的代理方法 首先需要声明一个当前滑动页码的成员变量 ``` objc @property (nonatomic, assign) NSInteger currentIndex; ``` 然后滑动代理停止的时候 判断一下 ``` objc #pragma mark - #pragma mark - ScrollView delegate - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{ dispatch_async(dispatch_get_main_queue(), ^{ CGPoint translatedPoint = [scrollView.panGestureRecognizer translationInView:scrollView]; //UITableView禁止响应其他滑动手势 scrollView.panGestureRecognizer.enabled = NO; if(translatedPoint.y < -50 && self.currentIndex < (kDataSourceCount - 1)) { self.currentIndex ++; //向下滑动索引递增 } if(translatedPoint.y > 50 && self.currentIndex > 0) { self.currentIndex --; //向上滑动索引递减 } [UIView animateWithDuration:0.15 delay:0.0 options:UIViewAnimationOptionCurveEaseOut animations:^{ //UITableView滑动到指定cell [self.tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:self.currentIndex inSection:0] atScrollPosition:UITableViewScrollPositionTop animated:NO]; } completion:^(BOOL finished) { //UITableView可以响应其他滑动手势 scrollView.panGestureRecognizer.enabled = YES; }]; }); } ``` > 这里的 `50` 实际上是你能允许滑动的最大触发区间.可以自己下载demo玩一下就知道了. 基于滑动区间 做 加减 当前页码控制.然后 做个简单的UIView动画. > 注意: 开始动画的时候最好不要相应pan手势,结束动画的时候再恢复回去,这样可以避免一些不必要的收拾滑动引起的问题. ### 为什么要滑动页码`self.currentIndex` 因为我们要用KVO 来实现 页面变动驱动滑动的动画 在 viewDidLoad:方法中 我们有个setupView:方法中 有下段代码 ``` objc [self addObserver:self forKeyPath:@"currentIndex" options:NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionNew context:nil]; ``` __是的我们要自己监听自己的成员变量去搞些事情__. ``` objc //观察currentIndex变化 -(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if ([keyPath isEqualToString:@"currentIndex"]) { //获取当前显示的cell AwemeListCell *cell = [self.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:_currentIndex inSection:0]]; __weak typeof (cell) wcell = cell; __weak typeof (self) wself = self; //用cell控制相关视频播放 } else { return [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; } } ``` > demo中有这段代码 其实是为了以后 cell上方palyerView的时候 控制相应暂停或者停止 或者其他操作的行为. 这里后期我们完善 ### 点击状态栏滑动到顶部 我们如何监听状态栏的事件? 我们当然可以设置TableView自动滑动到顶部.但是 我们怎么拦截下来这个事件去把我们 相关页码 __置`0`__ 为什么置0呢?看下 下面这张图 ![](/assets/images/20181106AwemeTopBottomScrollDemo/AwemeDemo3Error.gif) 虽然我们能实现 自动滑动TableView到顶部 但是 我们拦截不到顶部状态栏点击的事件,在这个事件调用的地方 把当前页码置`0`. #### 监听点击状态栏事件 这里使用的是在AppDelegate 中 复写 touchesBagan:方法 ``` objc - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { [super touchesBegan:touches withEvent:event]; //当触摸状态栏的时候发送触摸通知 这样控制器就收到了点击事件 CGPoint touchLocation = [[[event allTouches] anyObject] locationInView:self.window]; CGRect statusBarFrame = [UIApplication sharedApplication].statusBarFrame; if (CGRectContainsPoint(statusBarFrame, touchLocation)) { [[NSNotificationCenter defaultCenter] postNotificationName:StatusBarTouchBeginNotification object:nil]; } } ``` 在这里我们判断点击区域是否在状态栏范围内,是的话我们发送通知. 在我们用到TableView的VC里面注册这个通知,然后 置`0`. ``` objc #pragma mark - #pragma mark - event response 所有触发的事件响应 按钮、通知、分段控件等 - (void)statusBarTouchBegin { _currentIndex = 0; //KVO } ``` 这里我们置`0`处理. > 这里处理的方式简单粗暴,你有更好的实现方式可以底部评论,非常感谢. # 总结 以上是简单实现了抖音的上下滑,demo在下方, 下一期给大家演示更多细节,如果可能的话,最终搞出个视频放cell上 实现整个上下滑控制过程视频暂停 播放 停止等等,因为如果完整的实现抖音,需要很长的代码量,为了让大家一起学习,我把每个细节拆成一小节.单独写成文章讨论和学习. [抖音上下滑Demo](https://github.com/sunyazhou13/AwemeDemo) 参考开源 [抖音个人主页](https://github.com/sshiqiao/douyin-ios-objectc) URL: https://sunyazhou.com/2018/10/LabelDanceAnimation/index.html.md Published At: 2018-10-29 18:13:15 +0000 # iOS数字倍数动画 # 前言 写了一个简单的利用 透明度和 缩放 实现的 数字倍数动画 ![demo](/assets/images/20181029LabelDanceAnimation/danceLabel.gif) # 实现思路 上代码 看比较清晰 ``` objc // 数字跳动动画 - (void)labelDanceAnimation:(NSTimeInterval)duration { //透明度 CABasicAnimation *opacityAnimation = [CABasicAnimation animationWithKeyPath:@"opacity"]; opacityAnimation.duration = 0.4 * duration; opacityAnimation.fromValue = @0.f; opacityAnimation.toValue = @1.f; //缩放 CAKeyframeAnimation *scaleAnimation = [CAKeyframeAnimation animationWithKeyPath:@"transform.scale"]; scaleAnimation.duration = duration; scaleAnimation.values = @[@3.f, @1.f, @1.2f, @1.f]; scaleAnimation.keyTimes = @[@0.f, @0.16f, @0.28f, @0.4f]; scaleAnimation.removedOnCompletion = YES; scaleAnimation.fillMode = kCAFillModeForwards; CAAnimationGroup *animationGroup = [CAAnimationGroup animation]; animationGroup.animations = @[opacityAnimation, scaleAnimation]; animationGroup.duration = duration; animationGroup.removedOnCompletion = YES; animationGroup.fillMode = kCAFillModeForwards; [self.comboLabel.layer addAnimation:animationGroup forKey:@"kComboAnimationKey"]; } ``` 利用一个透明度从 0 ~ 1之间的alpha,然后缩放 之后加到动画组实现一下就好了 > 切记动画完成最好移除 否则可能引起动画内存问题 这里设置斜体字体 ``` objc self.comboLabel.font = [UIFont fontWithName:@"AvenirNext-BoldItalic" size:50]; ``` 看着比较明显 最后按钮点击的时候调用 ``` objc - (IBAction)clickAction:(UIButton *)sender { self.danceCount++; [self labelDanceAnimation:0.4]; self.comboLabel.text = [NSString stringWithFormat:@"+ %tu",self.danceCount]; } ``` 如果实现 dozen动画的话很简单, __danceCount % 10 == 0__ 求模就行了. # 总结 这个动画比较适合 有些直播场景的点击操作计数相关.感谢观看 [Demo在这里](https://github.com/sunyazhou13/LiveComboLabel) URL: https://sunyazhou.com/2018/09/BreathAnimation/index.html.md Published At: 2018-09-29 10:09:30 +0000 # iOS呼吸动画 # 前言 快放假了, 怕十一文章更新不及时,早点完成文章,保证每个月 2篇的产出量, 今天给大家带来的是 呼吸动画, 做的不是特别好. 上图 ![](/assets/images/20180929BreathAnimation/breathAnimation.gif) 大概是这个样子 # 需求和实现思路 具体要求 * 内部头像呼吸放大缩小 无限循环 * 每次放大同时需要背景还有一张图也放大 并且透明 * 点击缩放整个背景视图 ## 实现思路 首先 需要使用创建一个Layer 装第一个无限放大缩小的呼吸的图 背景也需要一个Layer 做 放大+透明度渐变的动画组并且也放置一张需要放大渐变的图片 最后点击触发. 添加一个一次性的缩放动画即可 ### 呼吸动画layer和动画 呼吸layer ``` objc CALayer *layer = [CALayer layer]; layer.position = CGPointMake(kHeartSizeWidth/2.0f, kHeartSizeHeight/2.0f); layer.bounds = CGRectMake(0, 0, kHeartSizeWidth/2.0f, kHeartSizeHeight/2.0f); layer.backgroundColor = [UIColor clearColor].CGColor; layer.contents = (__bridge id _Nullable)([UIImage imageNamed:@"breathImage"].CGImage); layer.contentsGravity = kCAGravityResizeAspect; [self.heartView.layer addSublayer:layer]; ``` > kHeartSizeHeight 和kHeartSizeWidth 是常量 demo中写好了100 加帧动画 ``` objc CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"transform.scale"]; animation.values = @[@1.f, @1.4f, @1.f]; animation.keyTimes = @[@0.f, @0.5f, @1.f]; animation.duration = 1; //1000ms animation.repeatCount = FLT_MAX; animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; [animation setValue:kBreathAnimationKey forKey:kBreathAnimationName]; [layer addAnimation:animation forKey:kBreathAnimationKey]; ``` > 差值器也可以自定义 例如: ``` objc [CAMediaTimingFunction functionWithControlPoints:0.33 :0 :0.67 :1] ``` 这里我做的持续时常1秒 ### 放大渐变动画group 创建新layer ``` objc CALayer *breathLayer = [CALayer layer]; breathLayer.position = layer.position; breathLayer.bounds = layer.bounds; breathLayer.backgroundColor = [UIColor clearColor].CGColor; breathLayer.contents = (__bridge id _Nullable)([UIImage imageNamed:@"breathImage"].CGImage); breathLayer.contentsGravity = kCAGravityResizeAspect; [self.heartView.layer insertSublayer:breathLayer below:layer]; //[self.heartView.layer addSublayer:breathLayer]; ``` > 这里用的是放在 呼吸layer后边 如果想放在呼吸layer前边 就把里面注释打开 然后注掉 inert那行代码 动画组 包含 放大 渐变 ``` objc //缩放 CAKeyframeAnimation *scaleAnimation = [CAKeyframeAnimation animationWithKeyPath:@"transform.scale"]; scaleAnimation.values = @[@1.f, @2.4f]; scaleAnimation.keyTimes = @[@0.f,@1.f]; scaleAnimation.duration = animation.duration; scaleAnimation.repeatCount = FLT_MAX; scaleAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn]; //透明度 CAKeyframeAnimation *opacityAnimation = [CAKeyframeAnimation animation]; opacityAnimation.keyPath = @"opacity"; opacityAnimation.values = @[@1.f, @0.f]; opacityAnimation.duration = 0.4f; opacityAnimation.keyTimes = @[@0.f, @1.f]; opacityAnimation.repeatCount = FLT_MAX; opacityAnimation.duration = animation.duration; opacityAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn]; //动画组 CAAnimationGroup *scaleOpacityGroup = [CAAnimationGroup animation]; scaleOpacityGroup.animations = @[scaleAnimation, opacityAnimation]; scaleOpacityGroup.removedOnCompletion = NO; scaleOpacityGroup.fillMode = kCAFillModeForwards; scaleOpacityGroup.duration = animation.duration; scaleOpacityGroup.repeatCount = FLT_MAX; [breathLayer addAnimation:scaleOpacityGroup forKey:kBreathScaleName]; ``` ### 点击缩放动画 跟第一个一样 只不过 执行次数默认一次 执行完就可以了 ``` objc - (void)shakeAnimation { CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"transform.scale"]; animation.values = @[@1.0f, @0.8f, @1.f]; animation.keyTimes = @[@0.f,@0.5f, @1.f]; animation.duration = 0.35f; animation.timingFunctions = @[[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut],[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]]; [self.heartView.layer addAnimation:animation forKey:@""]; } ``` 手势触发的时候 调用一下 ### 遇到的问题 在开发动画的时候遇到 都一个动画 要执行 呼吸 呼吸如果duration 到中间的话 比如1秒 那么0.5秒的时候 它就需要折回 那么第二个动画刚刚执行到一半,就会感觉很奇怪 ![](/assets/images/20180929BreathAnimation/aniamation.webp) 如果__渐变动画__执行0.5秒的话 它是重复的 那么他就重新开始 相当于 呼吸折回的时候它又重新开开始渐变 #### 怎么解决呢? 我们把0.5秒的动画加到 动画组里面,然后给动画组设置的时长保持和呼吸动画 一样,这样剩余的0.5的时候 渐变动画是不会重新开始的. # 总结 动画很久没玩了 基本都忘了一干二净了,以后要勤加练习,多出文章和demo,记录一些更多的知识技巧. 博客像车一样,要是不是的时候经常保养,才能走更远的路,记录更多的美好. 全文完 [Demo在这里下载](https://github.com/sunyazhou13/BreathAnimation) URL: https://sunyazhou.com/2018/09/IncreasingTapAreaOfButton/index.html.md Published At: 2018-09-20 09:40:06 +0000 # iOS扩大UIButton的点击的响应范围 # 前言 开发过程中经常遇到`UIButton`点击区域太小 又不想 改动按钮的大小. 今天的文章和大家分享一下解决这种问题的代码 # 实现思路 * 子类话UIButton 复写 它的`hitTest:`方法 * 子类话UIButton 复写 point:inside:withEvent: 方法 ## 第一种方式 ``` swift override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { let biggerButtonFrame = theButton.frame.insetBy(dx: -30, dy: -30) // 1 if biggerButtonFrame.contains(point) { // 2 return theButton // 3 } return super.hitTest(point, with: event) // 4 } ``` * 1. 让theButton的 x 扩大 30, y 扩大 30 (正数为缩小 负数为放大. 然后宽高 分别是2 * 30和 2 * 30) * 2. 判断点击的位置是否在放大完的frame内. * 3. 如果是 就返回button * 4. 不是的话让事件继续传递 > 注意:_这里没判断 theButton.alpha == 0 和 theButton.userInterface.. == YES 还有它是否可见之类的,请自行判断_ ## 第二种方式 复写UIView的point:方法 ``` swift override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { let biggerFrame = bounds.insetBy(dx: -30, dy: -30) return biggerFrame.contains(point) } ``` OC的版本是这样 ``` objc - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event { //这里写上 CGRectInset(<#CGRect rect#>, <#CGFloat dx#>, <#CGFloat dy#>) ... } ``` 但是 第二种方式 其实 是 hitTest:方法调用之前UIView的判断,它判断当前点击的point是否在这个UIView上. 不过 还是推荐第一种方式 ## 核心代码 其实 最核心的代码是 CGRectInset(<#CGRect rect#>, <#CGFloat dx#>, <#CGFloat dy#>) > CGRect CGRectOffset(CGRect rect, CGFloat dx, CGFloat dy)是以rect为中心,根据dx和dy来实现缩小。 如果dx 和 dy是负数 则放大 ,正数则缩小 但是大家可能很疑惑 那宽度和高度怎么 缩小放大 首先: 我们明确 这个API的含义 只要传入正数 它就缩放 那么 宽高也会适当前传入的dx和dy来决定 缩放比 因为是中心点缩放 所以宽高 __要 X 2__,因为有两侧嘛,左侧缩小30右侧也需要缩小30,上部和底部是一样的. 大家可自行查阅google看下 [参考Increasing the tap area of a UIButton](https://rolandleth.com/increasing-the-tap-area-of-a-uibutton) [参考 iOS触摸事件全家桶](https://mp.weixin.qq.com/s/9rvSRt4kfpy7e87EJoaJOQ) 全文完 URL: https://sunyazhou.com/2018/09/KeyboardAnimation/index.html.md Published At: 2018-09-18 09:49:58 +0000 # iOS键盘动画细节 ![](/assets/images/20180918KeyboardAnimation/keyboard1.webp) # 前言 很久没写技术文章里,本篇记录了一下一个键盘弹出的小细节动画,像微信一样流程 上图 ![](/assets/images/20180918KeyboardAnimation/keyboardAnimation.gif) # 动画细节代码 细节核心主要是通知中的一些key * 动画时长 * 动画的出现方式 ... 下面的通知是接收 键盘将要出现的通知`UIKeyboardWillShowNotification` ``` objc [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveKeyboardShowNotification:) name:UIKeyboardWillShowNotification object:nil]; ``` 然后是实现的核心代码 ``` objc - (void)didReceiveKeyboardShowNotification:(NSNotification *)noti { NSDictionary *userInfo = noti.userInfo; NSTimeInterval animationDuration; UIViewAnimationCurve animationCurve; CGRect keyboardFrame; [[userInfo objectForKey:UIKeyboardAnimationCurveUserInfoKey] getValue:&animationCurve]; [[userInfo objectForKey:UIKeyboardAnimationDurationUserInfoKey] getValue:&animationDuration]; [[userInfo objectForKey:UIKeyboardFrameEndUserInfoKey] getValue:&keyboardFrame]; UIViewAnimationOptions animationOptions = animationCurve << 16; self.bottomConstrains.offset = -CGRectGetHeight(keyboardFrame); [UIView animateWithDuration:animationDuration delay:0. options:animationOptions animations:^{ [self.view setNeedsUpdateConstraints]; [self.view layoutIfNeeded]; } completion:^(BOOL finished) { }]; } ``` > self.bottomConstrains.offset = -CGRectGetHeight(keyboardFrame); 是我写的约束 详细请参考demo 键盘消失也是一样的 `UIKeyboardWillHideNotification` 接收这个key ``` objc [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveKeyboardHideNotification:) name:UIKeyboardWillHideNotification object:nil]; ``` 消失的时候 把约束偏移量设置`0`即可 ``` objc - (void)didReceiveKeyboardHideNotification:(NSNotification *)noti { NSDictionary *userInfo = noti.userInfo; NSTimeInterval animationDuration; UIViewAnimationCurve animationCurve; CGRect keyboardFrame; [[userInfo objectForKey:UIKeyboardAnimationCurveUserInfoKey] getValue:&animationCurve]; [[userInfo objectForKey:UIKeyboardAnimationDurationUserInfoKey] getValue:&animationDuration]; [[userInfo objectForKey:UIKeyboardFrameEndUserInfoKey] getValue:&keyboardFrame]; UIViewAnimationOptions animationOptions = animationCurve << 16; self.bottomConstrains.offset = 0; [UIView animateWithDuration:animationDuration delay:0. options:animationOptions animations:^{ [self.view setNeedsUpdateConstraints]; [self.view layoutIfNeeded]; } completion:^(BOOL finished) { }]; } ``` > self.bottomConstrains.offset = 0; //设置偏移量会原来位置 利用Masonry做的动画 最后 别忘记移除通知 ``` objc - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; } ``` # 总结 键盘弹出这个微小的细节 很容易被大家忽视,写这篇文章是为了记录知识和技巧,希望各位多多指教 [Demo点击这里下载](https://github.com/sunyazhou13/KeyboardAnimation) 全文完 URL: https://sunyazhou.com/2018/09/Beacon/index.html.md Published At: 2018-09-02 12:12:51 +0000 # 船与灯塔 # 前言 ![](/assets/images/20180902Beacon/beacon.webp) 荷兰🇳🇱被称为"海上马车夫",今天讲述的故事与海上马车夫的有关的爱情故事,但它属于东方,属于中国。 # 船🚢 它是一搜小船,船上有两只船桨,这艘船从驶出造船厂的时候,就被定义为普通货物运输船只,载重不超过1吨,造这艘船的主人希望能靠这只船养家糊口,但又舍不得花更多的钱买好的木料来制造它。 # 灯塔 这是一座集成很多高新科技的灯塔,塔基可以在海上漂浮,自动平衡系统让他在海浪的动荡中稳如泰山,灯塔的父母距离这座新的灯塔不远,形成海上灯塔群,发出很强的激光束,指明航线方向,这坐新的灯塔还能发出加密的无线电信号,这座灯塔通过自身的太阳能外壳和风力发电机提供电力,还能通过坐落于港口海岸线的父母灯塔群无线传输电力给自己充电。只要她觉得电力不足,她就可以移动回父母灯塔群的无线充电有效范围补充能量,在众多灯塔群中,有两座地面灯塔为她提供稳定的电力输送。这座灯塔很幸福。但这座灯塔喜欢游弋在距离父母灯塔不远的浅海区,距离灯塔不超过几海里的范围内。 # 邂逅那只船 同在浅海区,有一天这只船在海上的一次风急浪高的夜晚迷失了方向,那只移动的灯塔发现了它,并回应了灯光交换信令,渐渐地,这艘船能识别这个灯塔发出的灯光束,船渐渐地喜欢上了这座移动的灯塔,这座灯塔也很钦佩这艘船,他能在如此大的风浪中屹立不沉,可是船自己深深地知道,自己是载货船,只有通过拉更多的货才能让主人把自己改造成更大的舰船,灯塔看出了船眼中的迷茫,说:"你可以留在浅海区,拉着普通的货物,我的灯塔父母那里是一座港口,他们也许有些小的货物可以给你",船心里也很想留在这座灯塔附近的港口做一些普通的货物运输,可是船出身寒微,怕被灯塔父母看不起,加之这座港口的贸易很不透明,只有有关系的船才能得到好的货物,船的自尊心受到了极大打击,决心一定要成为一艘旗舰,才有资格去这座港口拉重达几十吨的集装箱,去赢得灯塔父母和灯塔的尊重。就这样那天的风浪过后,船离开了这座灯塔,去了很多像这只小船组成的小船群队。 # 海上漂泊 在大海上运送完全超载的货物,过着漂流的生活,举目无亲,背井离乡,由于每次都拉超载的货物,船每次都打肿脸充胖子的运自己运不动的货物,每次触礁都撞得遍体鳞伤,渐渐地船学会了自我修复和疗伤,就这样船的主人赚了第一桶金,打算改造升级这艘木船,把他变成一艘铁甲舰船,去掉了双桨,还用发动机提供动力,这艘船的灯光系统不知为何,对那座灯塔发出的光有一种特殊的能力,识别率能达到200%.可是对其它灯塔的光束识别基本50%都达不到,船主人对这套灯光系统简直食之无用弃之可惜。可是船自己满足,知道灯光识别系统还在我就不会失去方向,我就能运送更重的货物,我就能看到那座灯塔,那座悬浮的灯塔。 刚货运不久,船给灯塔发出了无线电信号,请求灯塔能否和她一样来这座大的港口指名方向,船内心是没有底的,不知道新的港口是好是坏,只是这里的贸易很发达可以积累更多资本,灯塔发来反馈信号,表达了灯塔不想来到新港口的意愿,船很伤心,也很无助,伤心的是自己太弱小,无助的是就算灯塔来了船也给不了灯塔需要的一切电力和希望。不过船内心没有放弃,只要努力才能得到那些需要的一切。 第一次运送货物回到港口,船身已经从木板换成了轻型铁甲,可以运载不超过10吨的货物,当他第一次回到港口时,灯光系统识别那座灯塔发出的光束很精准,甚至光的色彩都可以模拟变换,用船的心里话说,我甚至可以识别那座灯塔光闪烁快慢,闪烁次数分别代表什么意思,以及最重要的是我能解密出她发出的加密信号。那座灯塔也很高兴,那艘很不一样的船回应她的速度很快很精准,也许这座灯塔的光线专为那艘船而发。 船停靠在港口一周时间,然后又要再一次的出海航行,船这一周想了很多,"我纵然能运重10吨的货物,可是马达还达不到指定的节数,速度不够快"(节 是指船只航行速度的单位,类似汽车的马力是多少匹),就这样道别了灯塔又开始扬帆起航。 在海上航行的时候,船一直思考一个问题,我为什么要放弃这些美好,是因为过上更好的生活吗?是为了把自身打造成旗舰去拉货吗?还是因为贫穷我需要自我救赎的脱贫致富? 在航行中,船会间隔一定时间给灯塔发送无线电波,从而证明他的存在。 在航行的几年中,灯塔很孤独,船也一样孤独,突然有一天船从其它船那里得知灯塔旁边出现了一座新的灯塔,船伤心了,自己没有机会去灯塔父母那里赢得他们的尊重了,于是船卸载了旧的灯光识别系统,卸载了无线电台收发器。直到一段时间以后船换上了新的无线电台和灯光系统,从光谱中删除了那座移动的灯塔的全部信道和交换秘钥,从此失去了一切和那座灯塔的联系。 每次运送货物的周期大概需要在海上航行一年,每年船都会回到浅海区的港口一次看望那座灯塔,远远的望着她,但那座灯塔心里了解,那艘船一年一定有一次会回到这里,只是它不发出光而已。 突然有一天船听说新建在悬浮灯塔旁边的新灯塔因为施工时间紧任务重导致质量不合格而坍塌,船为那座悬浮的灯塔深深地捏了一把汗,她没事吧?我是不是应该在这个时候跟她说句话,安慰一下她,可是我的无线电系统中已经没有了她的信道和交换秘钥。我比她都伤心,在她需要我的光束的时候我居然发不出来任何她需要的光束,让我唯一能有欣慰的是,她还有父母灯塔为她提供地面电力输送,保障她没有被坍塌的风险。 就这样时光过了4年2个月,在这四年中船在每个航行的夜晚时不时的做同一个梦,梦到那座移动的灯塔在发出寻找他的光束,他回应了这灯塔以同样的光束和无线电信号,互相拥抱着哭泣。每次梦里醒来,船的眼角里含着眼泪(当我写到这里时眼角里已经流出了眼泪),每次醒来船都会发出一次信号,记录下时间,就这样4年中梦到了灯塔15次,每次都是很伤心,船也许永远也想不明白自己到底做错了什么,怎么永远忘不掉那永不磨灭的电波,怎么永远也没有机会赢得属于自己的尊重,船的四年中不断完善自己的缺陷,把自己从轻型铁甲舰升级成了巡洋舰,但船的内心依然没有丝毫的高兴,即便自己能拉千吨货物,万吨排量,几千节速度,成为战列舰又能怎样,能换回那座悬浮的灯塔吗?不能,因为船很失败,一辈子也许都是载集装箱的命运,运更多的货物,船沉默了,船很想击沉自己永远沉没。 船试图启用新的灯光系统尝试发出那座灯塔需要的光束和新的无线电台接收系统,从其它友船那里找到这座灯塔信号频道和光谱。不久收到了悬浮灯塔的应答,船内心很矛盾,很高兴,很心酸,很无助,得知那座新灯塔倒塌后海洋总署联合地面港口以及父母灯塔,为悬浮灯塔建造了一座新的灯塔,并且用钢缆固定塔基,让她不在孤独,不在风浪的洗礼中摇摆不定。 灯塔也许不知道船的灯光系统中只换了灯芯和灯罩还有线路,灯光的芯片依然还是原来的芯片,只是现在仅仅能识别90%的那座灯塔光束。船的内心是替灯塔高兴的,她终于有了她想要的一切。但船也许很伤心,他的那座灯塔已经倒塌了,再也不能在海中为船指明方向了,船也不得不找下一座灯塔... # 结尾 这艘船叫"良工"号。 真人故事改编,版权所有翻版必究。 URL: https://sunyazhou.com/2018/08/CPUThreadDebug/index.html.md Published At: 2018-08-17 17:19:23 +0000 # iOS中CPU线程调试高级技巧 # 前言 最近在开发直播,发现CPU性能被打满后导致CPU降频,发热严重,然后卡顿... 为了定位这个问题我们花费了至少 3天的时间 一点一点跟踪CPU的线程代码,当遇到C++的thread的时候没有符号表,只能看见一坨对象地址,除此以外连个方法名都没有的时候真是手足无措.本篇介绍一个高级调试 方法,使用符号表和相关 指令寻踪 相关代码调用,写的不好 大佬们请轻喷.代码相关过程感谢同事 陈豪的大力支持. # Talk is cheap show me the code 我们的实现思路是找到动态库的首地址调用从此入手用相关指令恢复 ### 前期准备 * build setting中开启符号表 ![](/assets/images/20180817CPUThreadDebug/enableDysm.webp) ## 1.导入头文件 ``` objc #import ``` 这是mac os的可执行文件的动态链接库头文件 内部内建函数有几个我们需要用到 ## 2.复制下面代码到你的相关调用的地方 ``` objc //1 uint32_t count = _dyld_image_count(); DDLogInfo(@"Dyld image count %d", count); //2 for (int i = 0; i < count; i++) { char *image_name = (char *)_dyld_get_image_name(i); //3 const struct mach_header *mh = _dyld_get_image_header(i); intptr_t vmaddr_slide = _dyld_get_image_vmaddr_slide(i); //4 NSLog(@"Image name %s at address 0x%llx and ASLR slide 0x%lx.\n", image_name, (mach_vm_address_t)mh, vmaddr_slide); } ``` 我解释一下以上代码 * 1.拿出当前镜像数量 * 2.遍历镜像 * 3.获取镜像首地址 * 4.打印 然后运行你的程序 然后看下控制台 过滤一下 ASLR我们log中的键入内容 ![](/assets/images/20180817CPUThreadDebug/consoloDebug.webp) 然后 点击 工程中的Product ![](/assets/images/20180817CPUThreadDebug/products.webp) 右键 show in finder ![](/assets/images/20180817CPUThreadDebug/productDir.webp) 下一步骤 打开终端 cd 到这这个目录(可以打开终端 输入 cd 空格 拖拽那个文件夹) ![](/assets/images/20180817CPUThreadDebug/dirFinal.webp) 然后 `pwd`一下 看看 ## 3.控制台搜索相关我们打印log的代码 找到我们第一条首地址 ![](/assets/images/20180817CPUThreadDebug/importent.webp) > 注意:__这一步非常重要 如果不好使,请重试几次.__ #### 拿出main函数的首地址 ASLR中搜搜的 首地址然后复制 __回到终端中输入__ ``` sh atos -arch arm64 -o com_kwai_gif.app.dSYM/Contents/Resources/DWARF/com_kwai_gif -l 0x1006b8000 ``` > 注意:__这里是符号表路径__,如果不知道在哪里找到请google一下. 我们来测试一下 好不好使 首先在控制台顶部的面板点击 ![](/assets/images/20180817CPUThreadDebug/breakpoint1.webp) 然后 在 consolo中输入 `bt` ![](/assets/images/20180817CPUThreadDebug/main.webp) 如果看到 如下内容说明已经成功. ![](/assets/images/20180817CPUThreadDebug/mainResult.webp) ## 4.真机运行 找出未知线程 首先点击Xcode工程中的Profile运行`instruments`,我这里是运行工程之后 Xcode9.4可以无缝转换到`instruments` ![](/assets/images/20180817CPUThreadDebug/instruments0.webp) 我们找到相关线程 没有名称也不知道对象叫什么 就一个十六进制地址 ![](/assets/images/20180817CPUThreadDebug/instruments2.webp) 我们随便找个地址 在终端中输入 ![](/assets/images/20180817CPUThreadDebug/instruments3.webp) 好了 如果有问题 请删除product和符号表重新编译 # 总结 CPU调试的过程非常麻烦,而且中间过程的代码多数都是C++的调用,主要是线程消耗的开销,中有很多收获希望大家多多指教. 全文完 URL: https://sunyazhou.com/2018/08/TapticEngineFeedback/index.html.md Published At: 2018-08-13 14:28:04 +0000 # Taptic Engine振动反馈 ![](/assets/images/20180813TapticEngineFeedback/TapticEngine.webp) # 前言 Taptic Engine 是苹果产品上推出的全新震动模块,该元件最早出现在 Apple Watch 中。iPhone 6s 和 iPhone 6s Plus 中,也同样内置了Taptic Engine,在设计上有所升级。 Taptic Engine 振动模块为 Apple Watch 以及 iPhone 6s、iPhone 7 提供了 Force Touch 以及 3D Touch,不同的屏幕操作,可以感受到不同的振动触觉效果,带来更好的用户体验 # 触觉振动体验 ## 振动代码(旧方案) 调用这行代码虽然可以振动 但是它属于长振动 ``` objc AudioServicesPlaySystemSound(kSystemSoundID_Vibrate); ``` ## 振动代码(新方案) iOS10 引入了一种新的、产生触觉反馈的方式, 帮助用户认识到不同的震动反馈有不同的含义 。这个功能的核心就是由 UIFeedbackGenerator 提供。 `UIFeedbackGenerator` 可以帮助你实现 `haptic feedback`。它的要求是: * 支持 Taptic Engine 机型 (iPhone 7 以及 iPhone 7 Plus). * app 需要在前台运行 * 系统 Taptic setting 需要开启 > 下图开启 声音与触感 > 手机 -- 设置 -- 声音与触感 -- 系统触感反馈(打开) > ![](/assets/images/20180813TapticEngineFeedback/setting.webp) ### 调用相关振动代码实现振动功能 `UIFeedbackGenerator` 子类有: * UIImpactFeedbackGenerator * UISelectionFeedbackGenerator * UINotificationFeedbackGenerator #### UIImpactFeedbackGenerator振动 ``` objc UIImpactFeedbackGenerator *generator = [[UIImpactFeedbackGenerator alloc] initWithStyle: UIImpactFeedbackStyleLight]; [generator impactOccurred]; ``` 振动style有三种枚举 ``` objc typedef NS_ENUM(NSInteger, UIImpactFeedbackStyle) { UIImpactFeedbackStyleLight, UIImpactFeedbackStyleMedium, UIImpactFeedbackStyleHeavy }; ``` > 基本每次振动相当于创建一个实例调用一次方法就行了,如果觉得性能更好的设计可以搞成成员变量 反馈结果 | UIImpactFeedbackGenerator | UIImpactFeedbackStyleLight | UIImpactFeedbackStyleMedium | UIImpactFeedbackStyleHeavy | | ------| ------ | ------ | ------ | | iPhone 7(iOS 10)及以上机型 | 微弱短振 | 中等短振 | 明显短振 | | iPhone 6s Puls(iOS 9) | 长振 | 长振 | 长振 | | iPhone 6(iOS 10) | 无振动 | 无振动 | 无振动 | #### UISelectionFeedbackGenerator振动 这里我试图搞成成员变量模拟手势拖拽 振动 ``` objc @property (nonatomic, strong) UISelectionFeedbackGenerator *feedbackGesGenerator; ``` 事件相应 ``` objc - (IBAction)gestrueHandle:(UIGestureRecognizer *)sender { switch (sender.state) { case UIGestureRecognizerStateBegan: // Instantiate a new generator. self.feedbackGesGenerator = [[UISelectionFeedbackGenerator alloc] init]; // Prepare the generator when the gesture begins. [self.feedbackGesGenerator prepare]; break; case UIGestureRecognizerStateChanged: { // Check to see if the selection has changed... // Trigger selection feedback. [self.feedbackGesGenerator selectionChanged]; // Keep the generator in a prepared state. [self.feedbackGesGenerator prepare]; } break; case UIGestureRecognizerStateCancelled: case UIGestureRecognizerStateEnded: case UIGestureRecognizerStateFailed: // Release the current generator. self.feedbackGesGenerator = nil; break; default: // Do nothing. break; } } ``` > 注意: __这里调用了一下`[self.feedbackGesGenerator prepare]`方法让振动引擎准备就绪,方便下次快速启动__这个方法是父类的方法 #### UINotificationFeedbackGenerator振动 ``` objc UINotificationFeedbackGenerator *notifiFeedBack = [[UINotificationFeedbackGenerator alloc] init]; [notifiFeedBack notificationOccurred:UINotificationFeedbackTypeWarning]; ``` 同样`UINotificationFeedbackType`也是三种枚举 ``` objc typedef NS_ENUM(NSInteger, UINotificationFeedbackType) { UINotificationFeedbackTypeSuccess, UINotificationFeedbackTypeWarning, UINotificationFeedbackTypeError }; ``` # 总结 几种不同的振动API 可以视情况而使用, 比较常用的是 `UIImpactFeedbackGenerator`, 当然也可以随意使用注意操作系统判断检查。 例如: ``` objc if (@available(iOS 10.0, *)) { //写相关振动代码 } ``` 全文完 URL: https://sunyazhou.com/2018/07/ToolBarBlur/index.html.md Published At: 2018-07-23 18:22:05 +0000 # 利用UIToolBar做高斯模糊背景 ![](/assets/images/20180723ToolBarBlur/blur.gif) ``` objc - (UIView *)containerBackgroundView { if (!_containerBackgroundView) { UIToolbar *toolBar = [[UIToolbar alloc] initWithFrame:CGRectZero]; toolBar.barStyle = UIBarStyleBlack; toolBar.clipsToBounds = YES; _containerBackgroundView = toolBar; } return _containerBackgroundView; } ``` 也可以使用`UIBlurEffect` ``` objc UIBlurEffect *blurEffect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]; UIVisualEffectView *blurView = [[UIVisualEffectView alloc] initWithEffect:blurEffect]; blurView.frame = myView.bounds; [myView addSubview:blurView]; ``` UIBlurEffectStyle * UIBlurEffectStyleExtraLight,//额外亮度,(高亮风格) * UIBlurEffectStyleLight,//亮风格 * UIBlurEffectStyleDark//暗风格 > UIBlurEffect 不能调节模糊半径 如果要调整模糊半径 可以对图片进行高斯模糊 ``` objc -(UIImage *)convertToBlurImage:(UIImage *)image{ CIFilter *gaussianBlurFilter = [CIFilter filterWithName:@"CIGaussianBlur"]; [gaussianBlurFilter setDefaults]; CIImage *inputImage = [CIImage imageWithCGImage:[image CGImage]]; [gaussianBlurFilter setValue:inputImage forKey:kCIInputImageKey]; [gaussianBlurFilter setValue:@5 forKey:kCIInputRadiusKey]; CIImage *outputImage = [gaussianBlurFilter outputImage]; CIContext *context = [CIContext contextWithOptions:nil]; CGImageRef cgimg = [context createCGImage:outputImage fromRect:[inputImage extent]]; // note, use input image extent if you want it the same size, the output image extent is larger UIImage *convertedImage = [UIImage imageWithCGImage:cgimg]; return convertedImage; } ``` 核心代码是`[gaussianBlurFilter setValue:@5 forKey:kCIInputRadiusKey]`; 但是我测试100也没啥问题 没有测试出最大值 以上是几种高斯模糊的相关代码 全文完 URL: https://sunyazhou.com/2018/07/LinuxBash/index.html.md Published At: 2018-07-18 09:29:39 +0000 # Linux 终端 Bash 常用快捷键介绍及经验 ![](/assets/images/20180718LinuxBash/20130520LinuxLogoOnCentos5.webp) # bash及其特性 * [bash](http://cn.linux.vbird.org/linux_basic/0320bash.php)实质上是一个可执行程序,一个用户的工作环境。 * 在每一个shell下可以再打开一个shell,新打开的shell可以称为子shell,每一个shell之间是相互独立的。 * 可以使用pstree命令查看当前shell下的子shell个数。 ### 1. 最重要的自动补全 | 命令 | 解释 | | ----- | ----- | | Tab | 自动补全 | ### 2. 编辑跳转 | 命令 | 解释 | | ----- | ----- | | Ctrl + A | 跳转到当前行首 | |Ctrl + E | 跳转到当前行末 | |Alt + F | 将光标在当前行上向后移动一个单词 | |Alt + B | 将光标在当前行上向前移动一个单词 | |Ctrl + W | 删除当前光标前的一个单词 | |Ctrl + K | 删除当前光标后的内容 | |Ctrl + U | 清除整行 | |Ctrl + L | 清屏,类似于 clear 命令 | |Ctrl + H | 退格,类似于 backspace 键 | |Ctrl + T | 将当前光标前的两个字符互换位置 | |Esc + T | 将当前光标前的两个单词互换位置 | `Ctrl + W` 和 `Ctrl + U` 相当常用。拼写错是很常见的事。 `Ctrl + L` 也不用多说。 ### 3. 进程相关 | 命令 | 解释 | | ----- | ----- | | Ctrl + C | 终止当前进程 | | Ctrl + Z | 将当前进程在后台挂起 | Ctrl + D | 退出当前 Shell,类似于 exit 命令 | `Ctrl + C` 是向当前运行的进程发送 SIGINT 信号,终止进程。 > SIGINT - This signal is the same as pressing ctrl-c. On some systems, "delete" + "break" sends the same signal to the process. The process is interrupted and stopped. However, the process can ignore this signal. `Ctrl + Z` 并不结束进程,而是挂起在后台。之后仍然可以通过 `fg`命令恢复。对应的信号是 SIGTSTP。 ### 3. 搜索使用过的命令(特别推荐) | 命令 | 解释 | | ----- | ----- | | Ctrl + R | 用于搜索之前使用过的命令 | 我经常用 `history` 查看历史命令,其实已经有现成的快捷键可以用。 按下 `Ctrl + R` 之后,输入查询的关键字,如果不符合,可以继续按 `Ctrl + R` 进行遍历。 这个命令其实也是通过 `history` 记录来查询的。如果不喜欢这种方式,可以直接 `history | grep xxx` 也是不错的。 [参考 Linux公社](https://www.linuxidc.com/Linux/2017-11/148262.htm) # 总结 这些命令对工作效率提升很显著,需要反复学习牢记,文章最后推荐大家关注`Linux公社`这个具有历史人文精神的Linux社区, 它让我学到不少东西. URL: https://sunyazhou.com/2018/06/FilterString/index.html.md Published At: 2018-06-25 18:35:17 +0000 # Objective-C中使用正则去除非数字字母汉字 # 前言 今天碰到个需求,PM要求输入框中取出非字母数字汉字的输入. ![](/assets/images/20180625FilterString/RegularExpressDemo.gif) 带着这个疑问开始今天的文章 # 准备工作 创建个demo 代码如下 ``` objc @interface ViewController () @property (weak, nonatomic) IBOutlet UITextField *input; @property (weak, nonatomic) IBOutlet UILabel *label; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; self.input.delegate = self; [self.input addTarget:self action:@selector(textChange:) forControlEvents:UIControlEventEditingChanged]; } //当文本内容改变时调用 - (void)textChange:(UITextField *)textField { //这里调用相关方法过滤字符串显示出来 self.label.text = //...; } ``` 在网上找了一圈大多都是使用谓词去判断时候包含,没有几个给出相应的处理字符串. 我找到了3种 处理字符串的方式 * 方案1 使用谓词过滤 * 方案2 使用正则过滤增加寻找的字符串长度 * 方案3 使用正则精简过滤字符串 ``` objc 方案1 - (NSString *)filterString1:(NSString *)str { NSString *regex = @"^[a-zA-Z0-9\u4e00-\u9fa5]+"; NSPredicate *pred = [NSPredicate predicateWithFormat:@"SELF MATCHES %@", regex]; NSMutableString * retStr = [NSMutableString string]; for(NSInteger i=0; i< [str length];i++){ NSRange range = NSMakeRange(i, 1); NSString *character = [str substringWithRange:range]; if([pred evaluateWithObject:character]) { [retStr appendString:character]; } } return retStr; } ``` > 这种方式虽然能实现 但是代码略显冗长,不过能就解决问题 ``` objc //方案2 - (NSString *)filterString2:(NSString *)str { NSString *regex = @"[^a-zA-Z0-9\u4e00-\u9fa5]"; NSMutableString *mstr = [NSMutableString stringWithFormat:@"%@", str]; NSUInteger i = [mstr replaceOccurrencesOfString:regex withString:@"" options:NSRegularExpressionSearch range:NSMakeRange(0, mstr.length)]; return [NSString stringWithFormat:@"%@-长度:%zd",mstr,i]; } ``` > 同样的方法使用正则`replaceOccurrencesOfString:withString:options:range:`方法替换字符串 下面我们精简到2行代码 ``` objc //方案3 - (NSString *)filterString3:(NSString *)str { NSString *regex = @"[^a-zA-Z0-9\u4e00-\u9fa5]"; return [str stringByReplacingOccurrencesOfString:regex withString:@"" options:NSRegularExpressionSearch range:NSMakeRange(0, str.length)]; } ``` > 最终方案3 得到的预期结果还是不错,推荐使用 # 总结 有些问题都是在工作中遇到,希望记录下来一起分享和学习. 全完完 [Demo在这里](https://github.com/sunyazhou13/RegularExpressDemo) URL: https://sunyazhou.com/2018/06/NSAttributeString/index.html.md Published At: 2018-06-15 10:10:58 +0000 # 使用NSAttributeString实现不同颜色大小显示 ![](/assets/images/20180615NSAttributeString/richtext.webp) # 前言 最近开发需求遇到一个比较简单但又棘手的问题.先看需求 ![](/assets/images/20180615NSAttributeString/NSAttributeString1.webp) 一个`UILabel`显示不同大小颜色的字符串,当然我们首先的想到属性字符串,但是注意: 我们这里要处理国际化完成的字符串也就是说: 必须在国际化完成以后才能追加我们的逻辑,而不是一上来就加属性字符串 比如: `2分14秒` or `2min14secs` 也就是给我们的是一个 `"2分14秒"`字符串 我们需要匹配range来修改或者替换. 带着这个疑问开始今天的文章? ## 实现思路 孔圣贤有云:"举一隅不以三隅反,则不复也。" > 出自《论语·第七章·述而篇》 为了不愧对圣贤对我的期待我把 这个问题定位升级成 4个等级 * Level 1 最优解,时间复杂度最低,效率最高 * Level 2 非最优解,时间复杂度最低,效率高 * Level 3 都一般 * Level 4 简单粗暴 我想到了以下至少两种方法 1. 通过计算出来的时间 eg: `分` `秒` 字符串 range去国际化处理完的字符串去匹配修改 2. 用正则匹配数字 3. 用谓词匹配数字 4. level4太业余了不敢想向一个工作好几年的开发者还写出这么打脸的代码 ### 准备工作 在工程中拖拽了一个label ``` objc @interface ViewController () @property (weak, nonatomic) IBOutlet UILabel *label; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; //调用 NSAttributedString *resultTime = [self formattedCurrentTime:133]; self.label.attributedText = resultTime; } ``` ### 方案1: 字符串range匹配 ``` objc /** 返回当前时间格式 @return 返回组装好的字符串 */ - (NSAttributedString *)formattedCurrentTime:(NSTimeInterval)timeInterval { NSUInteger time = (NSUInteger)timeInterval; NSInteger minutes = (time / 60) % 60; NSInteger seconds = time % 60; NSString *minStr = [NSString stringWithFormat:@" %zd ",minutes]; NSString *secStr = [NSString stringWithFormat:@" %zd ",seconds]; //假设这就是我们国际化后的字符串 NSString *localizedFormatString = [NSString stringWithFormat:@"%@分%@秒",minStr,secStr]; NSMutableAttributedString *attributeStr = [[NSMutableAttributedString alloc] initWithString:localizedFormatString]; NSRange minRange, secRange; if (@available(iOS 9.0, *)) { minRange = [localizedFormatString localizedStandardRangeOfString:minStr]; secRange = [localizedFormatString localizedStandardRangeOfString:secStr]; } else { minRange = [localizedFormatString rangeOfString:minStr]; secRange = [localizedFormatString rangeOfString:secStr]; } NSDictionary *timeAttrs = @{ NSForegroundColorAttributeName : [UIColor redColor], NSFontAttributeName : [UIFont systemFontOfSize:40.0f]}; [attributeStr addAttributes:timeAttrs range:minRange]; [attributeStr addAttributes:timeAttrs range:secRange]; return [[NSAttributedString alloc] initWithAttributedString:attributeStr];; } ``` 看下显示结果 ![](/assets/images/20180615NSAttributeString/arrtributestring1.webp) > 是不是看上去很好 但我认为这并不完美,这种搞法虽然简单直接,但是过于依赖`minStr`和`secStr`的原始range,基于iOS9之后提供的API计算`range` ``` objc if (@available(iOS 9.0, *)) { minRange = [localizedFormatString localizedStandardRangeOfString:minStr]; secRange = [localizedFormatString localizedStandardRangeOfString:secStr]; } else { minRange = [localizedFormatString rangeOfString:minStr]; secRange = [localizedFormatString rangeOfString:secStr]; } ``` > 注意:*API平台区分* 但是这么实现有个Bug 当遇到同样字符串的时候就会匹配错位, 如图 ![](/assets/images/20180615NSAttributeString/NSAttributeStringBug1.webp) 错误的原因显然大家都了解 字符串 "0" 的range相同了,但就解决这个问题而言,简单判断一下range然后截取字符串向后跳跃length继续截取获取能实现,但这显然很啰嗦,万一有一天 你遇到的是 "`0小时0分12秒`"这种字符串那该如何写呢? 是不是要递归的遍历一遍然后挨个取`Range` 做属性修改? 这样的结果显然不但代码啰嗦 实现起来成本还是比较高的,对代码阅读性都有很大影响(写得好的代码除外哈). ##### 那怎么不啰嗦呢? 有一种搞法就是 用两个不同的字符占位.然后 国际化完成之后取Range,再然后替换文字,搞法虽然low点,但是时间复杂度降低了不少,还是可以考虑的.代码我就不写了 我怕小伙伴review代码的时候会虐我.继续往下看 **评级: Level 2** 那如何不依赖range解决这种问题呢? ### 方案2: 正则匹配 ``` objc /** 返回当前时间格式 @return 返回组装好的字符串 */ - (NSAttributedString *)formattedCurrentTime:(NSTimeInterval)timeInterval { NSUInteger time = (NSUInteger)timeInterval; NSInteger minutes = (time / 60) % 60; NSInteger seconds = time % 60; NSString *minStr = [NSString stringWithFormat:@" %zd ",minutes]; NSString *secStr = [NSString stringWithFormat:@" %zd ",seconds]; //假设这就是我们国际化后的字符串 NSString *localizedFormatString = [NSString stringWithFormat:@"%@分%@秒",minStr,secStr]; NSMutableAttributedString *attributeStr = [[NSMutableAttributedString alloc] initWithString:localizedFormatString]; NSDictionary *timeAttrs = @{ NSForegroundColorAttributeName : [UIColor redColor], NSFontAttributeName : [UIFont systemFontOfSize:40.0f]}; /** 方案2 **/ NSError *error = nil; NSRegularExpression *reg = [NSRegularExpression regularExpressionWithPattern:@"[0-9]+" options:NSRegularExpressionCaseInsensitive error:&error]; if (error == nil) { NSArray *matches = [reg matchesInString:localizedFormatString options:NSMatchingReportCompletion range:NSMakeRange(0, localizedFormatString.length)]; for (NSTextCheckingResult *match in matches) { for (NSUInteger i = 0; i < match.numberOfRanges; i++) { NSRange range = [match rangeAtIndex:i]; if (range.location != NSNotFound) { [attributeStr addAttributes:timeAttrs range:range]; } } } } return [[NSAttributedString alloc] initWithAttributedString:attributeStr];; } ``` 看下显示结果 ![](/assets/images/20180615NSAttributeString/attributestring2.webp) 完美实现 > 这种方案缺点就是,时间复杂度高了一些,需要每次正则遍历 > 有点是扩展性好一点,万一有一天PM又提了需求要做成 `A1` `B2` `C3 ` `XXX#话题`这种,那一定会出坑 但我第一次这么实现被小伙伴嘲笑很业余.确实很业余,但它能避免方案1中的bug.而且相当精确. **评级: Level 2** ### 方案3: 谓词匹配 这种搞法我没尝试,估计会比 方案1和方案2都快一些和简单直接一些,时间太紧张算了,期待评论轻喷吧! ### 方案4: 简单粗暴 就搞 4个label. 我都想象到了被实习生嘲讽+打脸的搞法发生在一个工作好几年开发者身上是多么惨痛的画面. 放弃这种low的搞法 # 总结 最终解决问题的方案还是方案2:正则匹配比较靠谱,而且一劳永逸 本篇主要蛋疼的问题是 国际化后的字符串返回结果后,对返回的结果进行加工处理. 没有做到Level 1级的做法很是遗憾,愧对圣贤. 希望小伙伴多提提建议. [Demo](https://github.com/sunyazhou13/NSAttributeStringDemo)在这里找到 ## 补充 格式化时间的代码 ``` objc /** 返回时间格式 HH:mm:ss @return 返回组装好的字符串 */ - (NSString *)formattedCurrentTime { NSUInteger time = (NSUInteger)self.recorder.currentTime; NSInteger hours = (time / 3600); NSInteger minutes = (time / 60) % 60; NSInteger seconds = time % 60; NSString *format = @"%02i:%02i:%02i"; return [NSString stringWithFormat:format, hours, minutes, seconds]; } ``` 全文完 URL: https://sunyazhou.com/2018/06/SwiftRandom/index.html.md Published At: 2018-06-08 09:18:03 +0000 # Swift4.2中的随机数 ![](/assets/images/20180608SwiftRandom/whatisnewinswift.webp) # 前言 在上一篇文章发布不久WWDC2018就拉开了序幕,让我觉得有一点比较蛋疼的(a bit of pain) 是swift4.2中增加了系统的随机数支持.所以不得不完善的好上一篇文章的缺漏和新技术的研究学习.特此新发一篇新的随机数文章以彰其咎. ## 开发环境 * Xcode10或者更高版本 * Swift4.2 * 使用Xcode中的Playground ## 生成随机数 在上一篇中我们大部分时间都在围绕[arcrandom()](https://man.openbsd.org/arc4random.3)函数来介绍随机数.当然也有它的一些变种.eg:arc4random_uniform(),rand(),random().但无论如何这些函数多数都是系统级函数。 在swift4.2中 所有的 数字类型(就是普通数据类型中代表数字的)都有一个静态方法`random(in:)`,这个方法将接收一个范围(Range)或者开闭范围,返回一个无序的随机数(a uniform distribution). 这些随机函数将会包含在swift的标准库中,如果跨平台的话标准库函数都是一致的,不像上面介绍的系统随机函数. ``` swift Int.random(in: 1...1000) //→ 580 Double.random(in: 0..<1) //→ 0.3211009027224093 UInt32.random(in: 0xD800...0xDFFF) //→ 56324 ``` ### 模偏差(Modulo bias) 以下代码演示了我们常用的取模 方式随机 ``` swift // Wrong! ❌ let between0And5 = UInt8.random() % 6 ``` 这种随机数 有可能不够均匀分布,这种非均匀分布的方式就叫[`模偏差`](https://www.quora.com/What-is-modulo-bias). 那如何解决这种莫偏差的问题呢? 在swift中就是用我上边介绍的方法. ``` swift // Correct ✅ let between0And5 = UInt8.random(in: 0..<6) // → 5 ``` 如果我们需要随机一个`数字数据类型`全范围的随机数的话可以使用 `.min ... .max`来进行范围随机. 如下代码: ``` swift let between0And255 = UInt8.random(in: .min ... .max) // → 190 ``` ### Bool值随机 虽然这种类型完全可以用 %2 ==0 或者 %2==0 来解决,但是swift还是很负责任的帮我们做到了这一点, 举个`抛硬币`场景的随机例子: ``` swift func coinToss(count tossCount: Int) -> (heads: Int, tails: Int) { var result = (heads: 0, tails: 0) for _ in 0.. heads → 人头面 > tails → 背面 ### 容器类型的元素随机(Random collection elements) 首先大家可以[`Collection`](https://developer.apple.com/documentation/swift/collection)理解成一个集成`NSObject`的类实现了容器协议的类型.eg: 数组,字典等等。。。。 这些`Collection` 类型都有一个`randomElement()`方法(可以看下上一篇文章末尾介绍的10个字符串的数).这个函数返回一个`Optional`可选类型.因为`Collection`可能为空. ``` swift let emptyRange = 10..<10 emptyRange.isEmpty // → true emptyRange.randomElement() // → nil ``` > 可以看到元素随机为nil 我们举个上一节的例子还测试一下 ``` swift var arr = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"] let randomElement = arr.randomElement()! // → "8" ``` 举个字符串emotion表情的demo ``` swift let emotions = "😀😂😊😍🤪😎😩😭😡" let randomEmotion = emotions.randomElement()! // → "😡" ``` ### Shuffling 集合随机排列(洗牌算法) 使用[shuffled()](https://developer.apple.com/documentation/swift/sequence/2996816-shuffled)方法去随机排列一个序列或容易. ``` swift (1...20).shuffled() // → numbers is now [16, 9, 2, 18, 5, 13, 8, 11, 17, 3, 6, 1, 14, 7, 10, 15, 20, 19, 12, 4] ``` 以上实现了一个类似洗牌算法的排序 1~20 之间的数 注意:左右都是闭区间(闭区间包含本身) 因为这里用的是`...`,不理解大家可以查找一下swift相关区间标识的知识. ## 随机数生成的协议(Random number generators) `Random number generators`简称`RNG`,以下简称`RNG`. ### 默认的RNG 以上介绍的使用方法都是被定义在swift的标准库中的方法. 叫[Random.default](https://forums.swift.org/t/se-0202-amendment-proposal-rename-random-to-defaultrandomnumbergenerator/12942) [SE-0202](https://github.com/apple/swift-evolution/blob/master/proposals/0202-random-unification.md) 讨论了这种默认随机的一些问题 我在这里简要一下 > The aspiration is that this RNG should be cryptographically secure, provide reasonable performance, and should be thread safe. If a vendor is unable to provide these goals, they should document it clearly. … if an RNG on a platform has the possibility of failing, then it must fail [i.e. trap] when it is unable to complete its operation. > 大概意思就是 高性能,高安全性,线程安全.... ### 自定义RNGs 对于大多数简单的用例,缺省的RNG应该是正确的选择。但是,如果您的代码对随机数生成器有特殊的要求,比如特定的算法或用可重复的种子初始化RNG的能力,那么您就可以通过采用随机数生成器协议来实现自己的RNG。协议只有一个要求:`next()`方法,该方法产生`8个新的字节随机数`: ``` swift public protocol RandomNumberGenerator { /// Returns a value from a uniform, independent /// distribution of binary data. public mutating func next() -> UInt64 } ``` > 注意:协议需要统一的分布。其思想是,需要具有非均匀分布的随机值的用户可以在第二步将期望的分布应用到均匀分布随机性序列里。 > 就是如果想按照自己的方法随机需要吧next()函数写上,写好泛型函数规则就行了. ### 使用自定义随机RNG 所有用于生成随机值的标准库api都提供了允许用户传入自定义随机数生成器的方法重载。例如,Int类型有以下两种方法: ``` swift extension Int { static func random(in range: Range) -> Int { ... } static func random(in range: Range, using generator: inout T) -> Int where T: RandomNumberGenerator { ... } // The overloads that take a ClosedRange are not shown } ``` 这个`generator`参数需要总是传入[`inout`](https://docs.swift.org/swift-book/ReferenceManual/Declarations.html#ID545),因为在产生新的随机性时,RNGs通常会改变它们的状态。 下面看下我们怎么调用自定义随机, 我们需要创建一个可变的并且满足inout的要求的方法. ``` swift var mersenneTwister = MersenneTwisterRNG(...) // assume this exists Int.random(in: 10..<20, using: &mersenneTwister) ``` ### 在自有类型中生成随机值 通过上面我们了解到 自定义随机协议需要满足两个标准库模式的步骤: * 提供静态随机方法`random() -> Self` 这个方法使用默认的RNG,如果我们规范随机范围的时候这个函数能补充额外参数.以便于我们规范随机的range. * 提供第二个方法`random(using generator: inout T) -> Self`这个是生成默认随机数的核心方法. 举个扑克游戏中的枚举例子, 这里面我们可以充分利用[`Swift4.2`](https://github.com/apple/swift-evolution/blob/master/proposals/0194-derived-collection-of-enum-cases.md)中的[allCase](https://developer.apple.com/documentation/swift/caseiterable)属性. ``` swift enum Suit: String, CaseIterable { case diamonds = "♦" case clubs = "♣" case hearts = "♥" case spades = "♠" static func random() -> Suit { return Suit.random(using: &Random.default) } static func random (using generator: inout T) -> Suit { // Force-unwrap can't fail as long as the // enum has at least one case. return allCases.randomElement(using: &generator)! } } let randomSuit = Suit.random() // → clubs randomSuit.rawValue // → "♠" ``` ## 总结 本篇补充了新版Swift4.2中对标准库中的随机函数支持,也介绍了洗牌函数默认随机均匀排列,希望小伙伴们看完有所收获,有问题还请多多指教 全文完 [参考](https://oleb.net/blog/2018/06/random-numbers-in-swift/) URL: https://sunyazhou.com/2018/06/Random/index.html.md Published At: 2018-06-01 17:30:56 +0000 # Swift中的随机数 ![](/assets/images/20180601Random/SwiftRandomNumbers.webp) # 前言 今天儿童节,写一篇`随机数`技术文章纪念`留守儿童(资深)`的童年. ## swift中的随机数使用 在我们开发的过程中,经常用到求取一些随机数,今天列举几种写篇文章 ## 整型随机数 首先是这个arc4random() > `arc4random()`使用了`arc4`密码加密的`key` `stream`生成器,产生一个`[0, 2^32)`区间的随机数(注意是左闭右开区间)。这个函数的返回类型是`UInt32` > 提示: _`[`和`]` 分别代表左右闭区间_, > _`(`和`)`代表左右开区间_ > 也就是`中括号` -> 代表 闭区间, 闭区间代表包含. > 小括号 -> 代表开区间, 开区间代表不包含. > 所以以下看到 ``` swift arc4random() //"4058056034" ``` 如果我们想生成一个**指定范围内**的整型随机数,则可以使用`arc4random()` `%` `upper_bound`的方式,其中`upper_bound`指定的是上边界,如下代码: 求一个10以内的随机数 ``` swift arc4random() % 10 // 0~9 注意没有10哈 ``` 不过使用这种方法,在`upper_bound`不是`2`的幂次方时,会产生一个所谓`Modulo bias`(模偏差)的问题。 可以使用`arc4random_uniform()`,它接受一个`UInt32`类型的参数,**指定随机数区间的上边界**`upper_bound`,该函数生成的随机数范围是[0, upper_bound),如下所示: ``` swift arc4random_uniform(10) // 5 ``` 那问题来了?我想指定区间随机 比如: `[10, 200)`. ``` swift let maxNum: UInt32 = 200 let minNum: UInt32 = 10 arc4random_uniform(maxNum - minNum) + minNum // 153 ``` 可以看到上述结果 是 `153`. swift也可以用C函数中的随机 eg: random() 或者 rand(),但是这些有下面缺点: * 这两个函数都需要初始种子,通常是以当前时间来确定,属于伪随机. * 这两个函数的上限在`RAND_MAX=0X7fffffff`(2147483647),是`arc4random`的一半. * `rand()`函数以有规律的低位循环方式实现,更容易预测 ``` c srand(UInt32(time(nil))) // 种子,random对应的是srandom rand() // 1,314,695,483 rand() % 10 // 8 ``` ## 64位整型随机数 我们发现这些函数主要都是针对`32`位整型数来操作的.如果需要生成一个`64`位的整型随机数呢? 可以使用如下代码: ``` swift func arc4random (type: T.Type) -> T { var r: T = 0 arc4random_buf(&r, MemoryLayout.size) return r } ``` 可以像下面这样调用 ``` swift arc4random(type: UInt64.self) //8021765689869396105 arc4random(type: UInt32.self) //1293034028 arc4random(type: UInt16.self) //29059 arc4random(type: UInt8.self) //183 ``` > swift 4 语法 这个函数中使用了`arc4random_buf()`来生成随机数。 这个函数使用ARC4加密的随机数来填充该函数第二个参数指定的长度的缓存区域。因此,如果我们传入的是sizeof(UInt64),该函数便会生成一个随机数来填充8个字节的区域,并返回给r。那么64位的随机数生成方法便可以如下实现: ``` swift extension UInt64 { static func random(lower: UInt64 = min, upper: UInt64 = max) -> UInt64 { var m: UInt64 let u = upper - lower var r = arc4random(type: UInt64.self) if u > UInt64(Int64.max) { m = 1 + ~u } else { m = ((max - (u * 2)) + 1) % u } while r < m { r = arc4random(type: UInt64.self) } return (r % u) + lower } } ``` 我们来试用一下: ``` swift UInt64.random() //9223372036854775807 ``` ## 浮点型随机数 如果需要一个浮点值的随机数,则可以使用drand48函数,这个函数产生一个[0.0, 1.0]区间中的浮点数。这个函数的返回值是Double类型。其使用如下所示: ``` swift srand48(Int(time(nil))) drand48() //0.4643666202473504 ``` > 注意:需要先调用srand48()生成种子 ## 示例实践 如何生成一个0~9 这几个数组做个随机排序,实现类似银行类动态键盘的功能 ``` swift var arr = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"] arr.sort { (s1, s2) -> Bool in arc4random() < arc4random() } print(arr) ``` 在闭包中,随机生成两个数,比较它们之间的大小,来确定数组的排序.注意不需要重新赋值了在swift4上. # 总结 随机数相关的知识容易忘记,特此记录一些技巧,争取每个月发布两篇文章. [参考](http://southpeak.github.io/2015/09/26/ios-techset-5/) URL: https://sunyazhou.com/2018/05/HowToCreateTopBottomRoundedCornersForViews/index.html.md Published At: 2018-05-15 09:58:00 +0000 # UIView不同方向的导角 ![](/assets/images/20180515HowToCreateTopBottomRoundedCornersForViews/TopBottomCornerDemo.webp) # 前言 开发中总因为一些比较蛋疼的导角问题而困扰着我们,尤其是我们要给一个UIView导角成 左上 、左下。。。 这种需求很值得用代码实现一下, 今天突然在[AppCode](https://www.appcoda.com/rounded-corners-uiview/?utm_source=feedburner&utm_medium=feed&utm_campaign=Feed%3A+appcoda+%28AppCoda%3A+Your+iOS+Programming+Community%29)找到了一篇好文章.于是有了下文 ## 通常导角 ``` swift self.view.cornerRadius = 20.0 self.view.clipToBounds = true ``` 这两行代码是全方向导角 如果像要搞成不同方向的话可以用iOS11 新的API和 iOS11以前的`CAShapeLayer`画贝赛尔曲线来解决 首先我们要创建一个UIView ``` swift class ViewController: UIViewController { var cardView: UIView! override func viewDidLoad() { super.viewDidLoad() cardView = UIView() view.addSubview(cardView) cardView.translatesAutoresizingMaskIntoConstraints = false //把View居中 cardView.widthAnchor.constraint(equalToConstant: 200).isActive = true cardView.heightAnchor.constraint(equalToConstant: 200).isActive = true cardView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true cardView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true cardView.backgroundColor = UIColor(red: 1.0, green: 0.784, blue: 0.2, alpha: 1) } } ``` iOS11 以后苹果提供了一个`UIView`的属性叫`maskedCorners`用于CALayer的动画相关. ``` swift public struct CACornerMask : OptionSet { public init(rawValue: UInt) public static var layerMinXMinYCorner: CACornerMask { get } public static var layerMaxXMinYCorner: CACornerMask { get } public static var layerMinXMaxYCorner: CACornerMask { get } public static var layerMaxXMaxYCorner: CACornerMask { get } } ``` 下面我说一下 * layerMinXMinYCorner 底部右侧 的圆角 -> 右下角 * layerMaxXMinYCorner 顶部右侧 的圆角 -> 右上角 * layerMinXMaxYCorner 底部左侧 的圆角 -> 左下角 * layerMinXMinYCorner 顶部左侧 的圆角 -> 左上角 一般我们都为UIView写个 extension ``` swift extension UIView { func roundCorners(cornerRadius: Double) { self.layer.cornerRadius = CGFloat(cornerRadius) self.clipsToBounds = true if #available(iOS 11.0, *) { self.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] } else { let path = UIBezierPath(roundedRect: self.bounds, byRoundingCorners: [.topLeft, .topRight], cornerRadii: CGSize(width: cornerRadius, height: cornerRadius)) let maskLayer = CAShapeLayer() maskLayer.frame = self.bounds maskLayer.path = path.cgPath self.layer.mask = maskLayer } } } ``` 这里区分了iOS11之前和之后的两种搞法. 之前的话我们都是用一个贝塞尔曲线画path.然后创建CAShapeLayer 给self.layer.mask做一种透明的遮罩来解决不同方向导角问题. ## 添加动画效果的导角 我们在原ViewDidLoad()方法里面加个手势. 并写好触发的事件, 完整的代码如下 ``` swift import UIKit class ViewController: UIViewController { var cardView: UIView! override func viewDidLoad() { super.viewDidLoad() cardView = UIView() view.addSubview(cardView) cardView.translatesAutoresizingMaskIntoConstraints = false cardView.widthAnchor.constraint(equalToConstant: 200).isActive = true cardView.heightAnchor.constraint(equalToConstant: 200).isActive = true cardView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true cardView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true cardView.backgroundColor = UIColor(red: 1.0, green: 0.784, blue: 0.2, alpha: 1) let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(animateCornerChange(recognizer:))) cardView.addGestureRecognizer(tapRecognizer) } @objc func animateCornerChange(recognizer: UITapGestureRecognizer) { let targetRadius: Double = (cardView.layer.cornerRadius == 0.0) ? 100.0:0.0 if #available(iOS 10.0, *) { UIViewPropertyAnimator(duration: 0.4, curve: .easeInOut) { self.cardView.roundCorners(cornerRadius: targetRadius) }.startAnimation() } else { UIView.animate(withDuration: 1.0, delay: 0.0, options: .curveEaseInOut, animations: { }, completion: nil) } } } extension UIView { func roundCorners(cornerRadius: Double) { self.layer.cornerRadius = CGFloat(cornerRadius) self.clipsToBounds = true if #available(iOS 11.0, *) { self.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] } else { let path = UIBezierPath(roundedRect: self.bounds, byRoundingCorners: [.topLeft, .topRight], cornerRadii: CGSize(width: cornerRadius, height: cornerRadius)) let maskLayer = CAShapeLayer() maskLayer.frame = self.bounds maskLayer.path = path.cgPath self.layer.mask = maskLayer } } } ``` ### 这里主要强调一下动画的新API iOS10之后U增加一个新的动画效果API ``` swift UIViewPropertyAnimator(duration: 0.4, curve: .easeInOut) { //这里写相关View的操作代码 。。。eg:下面代码 self.cardView.roundCorners(cornerRadius: targetRadius) }.startAnimation() ``` iOS之前可以通过古老的API来实现 ``` swift UIView.animate(withDuration: 1.0, delay: 0.0, options: .curveEaseInOut, animations: { //这里写相关View的操作代码 。。。eg:下面代码 self.cardView.roundCorners(cornerRadius: targetRadius) }, completion: nil) ``` 最终的效果 ![](/assets/images/20180515HowToCreateTopBottomRoundedCornersForViews/TopBottomCornerDemo.gif) # 总结 iOS一些简单的动画导角比较常用所以记录下来,希望大家多多指教 [本文Demo](https://github.com/sunyazhou13/TopBottomCornerDemo) URL: https://sunyazhou.com/2018/05/ManualControlUIViewControllerLifeCycle/index.html.md Published At: 2018-05-08 12:01:27 +0000 # 手动管理UIViewController的生命周期 # 前言 话说很久不用UIViewController的不常用 的API渐渐的都没有了印象,在 iOS 客户端中,多个 childViewController 的页面是个很常见的交互设计,最早的网易新闻,今日头条等.这篇文章回味一下古老的手动控制视图控制器的生命周期的API. # UIViewController 我们在使用`addChildViewController:`的时候会遇到个问题.如何手动控制被添加控制器的生命周期. 如下代码 ``` objc self.vc1 = [[VC1ViewController alloc] init]; //子控制器 self.vc2 = [[VC2ViewController alloc] init]; //子控制器 [self addChildViewController:self.vc1]; //添加到父控制器中 [self.view addSubview:self.vc1.view]; //把子控制器的 view 添加到父控制器的 view 上面 self.vc1.view.frame = CGRectMake(0, 0, 100, 100); //设置 frame [self.vc1 didMoveToParentViewController:self];//子控制器被通知有了一个父控制器 [self addChildViewController:self.vc2]; [self.view addSubview:self.vc2.view]; self.vc2.view.frame = CGRectMake(0, 0, 100, 100); [self.vc2 didMoveToParentViewController:self];//子控制器被通知有了一个父控制器 ``` 如果是移除的话使用如下代码 ``` objc //移除一个 childViewController [self.vc1 willMoveToParentViewController:nil];//子控制器被通知即将解除父子关系 [self.vc1.view removeFromSuperview];//把子控制器的 view 从到父控制器的 view 上面移除 [self.vc1 removeFromParentViewController];//真正的解除关系,会自己调用 [self.vc1 didMoveToParentViewController:nil] ``` 当我们添加child到父控制器的时候 它的 ``` objc - (void)viewWillAppear:(BOOL)animated{ [super viewWillAppear:animated]; } - (void)viewDidAppear:(BOOL)animated{ [super viewDidAppear:animated]; } - (void)viewWillDisappear:(BOOL)animated{ [super viewWillDisappear:animated]; } - (void)viewDidDisappear:(BOOL)animated{ [super viewDidDisappear:animated]; } ``` 这些放系统内部会自动帮我们调用 #### 手动管理child ViewController 的生命周期方法 需要在父ViewController里面复写如下方法 并返回`NO` ``` objc - (BOOL)shouldAutomaticallyForwardAppearanceMethods{ //手动管理子VC的生命周期 return NO; } ``` 不过我们需要注意的是,不能手动调用 viewWillAppear、viewDidAppear等等这些方法,而应该调用: ``` objc - (void)beginAppearanceTransition:(BOOL)isAppearing animated:(BOOL)animated; - (void)endAppearanceTransition; ``` > 注意:_**用这两个方法来间接触发子控制器的生命周期,并且它们需要成对使用**_ `isAppearing` 设置为 `YES` : 触发 `viewWillAppear:`; `isAppearing` 设置为 `NO` : 触发 `viewWillDisappear:`; `endAppearanceTransition`方法会基于我们传入的`isAppearing` 来调用`viewDidAppear:`以及`viewDidDisappear:`方法 为了测试我写一段代码 ``` objc - (IBAction)click:(UIButton *)sender { sender.selected = !sender.selected; if (sender.selected) { [self.vc1 beginAppearanceTransition:NO animated:YES]; //调用vc1的 viewWillDisappear: [self.vc2 beginAppearanceTransition:YES animated:YES]; //调用vc2的 viewWillAppear: [self.vc1 endAppearanceTransition]; //调用vc1的viewDidDisappear: [self.vc2 endAppearanceTransition]; //调用vc2的viewDidAppear: } else { [self.vc1 beginAppearanceTransition:YES animated:YES]; [self.vc2 beginAppearanceTransition:NO animated:YES]; [self.vc1 endAppearanceTransition]; [self.vc2 endAppearanceTransition]; } } ``` [Demo](https://github.com/sunyazhou13/VCLifeCycle) 全文完 URL: https://sunyazhou.com/2018/05/AudioUnit/index.html.md Published At: 2018-05-07 14:59:41 +0000 # AudioUnit ![](/assets/images/20180507AudioUnit/auHostApp.webp) # 前言 声音的渲染在iOS平台上回直接使用`AudioUnit`的API来完成.用来实现一些类似`大叔`,`KTV`,`耳返等效果`.... 今天带领大家深入了解和学习一下这些音效. ## 实现iOS变声的背景 声音变声一般都是发生在 一端采集录制另一端播放音频, 忽略中间的转码过程,在输入输出的中间过程中进行相应的音频参数就实现了变声. 下图是AVAudioSession的工作流 ![](/assets/images/20180507AudioUnit/ASPGIntro.webp) 大家常用的变声方案有很多: 1. FFMpeg提供内部效果器 eg:EQ均衡器 2. AVFoundation底层的`Audio Unit` eg: 混响reverb 3. SoundTouch 4. 其它方案... 这里我们选用iOS AVFoundation本身提供的音频处理单元`Audio Unit`. `Audio Unit`提供如下功能: * 低延迟的音频I/O eg:voip * 多路声音的合成并回放 eg:游戏中的音乐合成器 * Audio Unit 自身提供 eg:回声消除、Mix两轨音频、均衡器、压缩器、混响效果器等. * 需要图状的结构来处理音频. eg: 有点类似PC时代的主播经常用的一种叫KX 驱动. 下图是KX 驱动连线图 windows平台 ![](/assets/images/20180507AudioUnit/kx.webp) ## AudioUnit介绍 #### iOS层级架构图 ![](/assets/images/20180507AudioUnit/iPhone0sAudioArchitecture.webp) ![](/assets/images/20180507AudioUnit/AboutAudioUnitHosting.webp) > 声音的处理过程, 首先需要认识一下`AUGraph` ![](/assets/images/20180507AudioUnit/simpleAuChain.webp) > **audio processing graph**: A representation of a signal chain comprising an interconnection of audio units. Also called an AUGraph or graph. Core Audio represents such an interconnected network as a software object of typeAUGraph. Audio processing graphs must end in an output unit. See also audio unit. > 一种信号链的表示,包括音频单元的互连。也称为AUGraph或graph。Core Audio代表着这样一个相互连接的网络,它是一个`AUGraph`类型的对象。 #### audio unit 结构图(工作流) ![](/assets/images/20180507AudioUnit/auArchitecture.webp) #### Audio Unit 构成图 ![](/assets/images/20180507AudioUnit/AudioUnitScopes.webp) Unit 一般 分为 Element0 和 Element1 下面我们举Remote I/O Unit为例: RemoteIO 这个Unit是和硬件IO相关的Unit,它分为输入端和输出端, 输入端一般指麦克风,输出端一般指扬声器. > `Element0` 控制输出 > `Element1` 控制输入 > 图中Element 也叫 bus; > 音频流从输入域(input scope)输入, 从输出域(output scope)输出 > 整个Render过程就是一次RenderCycle ![](/assets/images/20180507AudioUnit/IOUnit.webp) __同时每个Element分为Input Scope 和 Output Scope.如果我们想使用扬声器的声音播放功能,必须需将这个Unit的`Element0`的`OutputScope`和Speak进行连接. 如果想使用麦克风录音功能,那么必须将这个Unit的`Element1`的`InputScope`和麦克风进行连接.__ ### 构建Audio Unit 首先需要启用音频会话 这些大家自己配置就好 了 ``` //配置会话相关伪代码 [[AVAudioSession sharedInstance].xxxxx xxxxx]; ``` 如何用代码构建一个Audio Unit? 这里我们以Remote I/O Unit 为例: 创建AudioUnit有两种方式 1. 直接使用AudioUnit裸创建 2. 使用AUGraph和AUNode来构建 * 第一种 裸创建 ``` objc #import "ViewController.h" #import @interface ViewController () { AudioUnit ioUnitInstance; //声明一变量 } @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; //首先构造出要用到创建Unit的结构体 AudioComponentDescription ioUnitDescription; ioUnitDescription.componentType = kAudioUnitType_Output; ioUnitDescription.componentSubType = kAudioUnitSubType_RemoteIO; ioUnitDescription.componentManufacturer = kAudioUnitManufacturer_Apple; ioUnitDescription.componentFlags = 0; ioUnitDescription.componentFlagsMask = 0; AudioComponent ioUnitRef = AudioComponentFindNext(NULL, &ioUnitDescription); //创建AudioUnit实例 AudioComponentInstanceNew(ioUnitRef, &ioUnitInstance); } ``` * 第二种使用AUGraph和AUNode ``` objc #import "ViewController.h" #import #import @interface ViewController () { AUGraph processingGraph; AUNode ioNode; AudioUnit ioUnit; } @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; //首先构造出要用到创建Unit的结构体 AudioComponentDescription ioUnitDescription; ioUnitDescription.componentType = kAudioUnitType_Output; ioUnitDescription.componentSubType = kAudioUnitSubType_RemoteIO; ioUnitDescription.componentManufacturer = kAudioUnitManufacturer_Apple; ioUnitDescription.componentFlags = 0; ioUnitDescription.componentFlagsMask = 0; //1 new NewAUGraph(&processingGraph); AUGraphAddNode(processingGraph, &ioUnitDescription, &ioNode); //2 open AUGraphOpen(processingGraph); //3 从相应的Node中获得AudioUnit AUGraphNodeInfo(processingGraph, ioNode, NULL, &ioUnit); } ``` > 推荐使用第二种因为这种创建扩展性更高一些 > 注意:__*AUNode必须和AudioUnit成对出现*__ 如下图 :Remote I/O Unit ![](/assets/images/20180507AudioUnit/IOUnit.webp) > 麦克风或者扬声器在Audio Unit中有相应的枚举. > 直播中的`耳返`就是用的这个把麦克风采集的数据直接扔给扬声器 这样就能做到 低延迟的实时听到麦克风的声音. > 直播中一般使用`Remote I/O` unit来进行采集工作 使用AudioUnit连接扬声器 ``` objc OSStatus status = noErr; UInt32 onFlag = 1; UInt32 busZero = 0; //Element0 就是bus0 status = AudioUnitSetProperty(remoteIOUnit, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Output, busZero, &onFlag, sizeof(onFlag)); CheckStatus(status, @"不能连接扬声器", YES); ``` > 注意: kAudioUnitScope_Output 就是连接扬声器的key 连接麦克风 ``` objc OSStatus status = noErr; UInt32 busOne = 1; //Element1 就是bus1 接麦克风输入 UInt32 oneFlag = 1; status = AudioUnitSetProperty(remoteIOUnit, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Input, busOne, &oneFlag, sizeof(oneFlag)); CheckStatus(status, @"不能连接麦克风", YES); ``` 可以使用如下代码检查每一步执行出错debug ``` objc static void CheckStatus(OSStatus status, NSString *message, BOOL fatal) { if (status != noErr) { char fourCC[16]; *(UInt32 *)fourCC = CFSwapInt32HostToBig(status); fourCC[4] = '\0'; if (isprint(fourCC[0]) && isprint(fourCC[1]) && isprint(fourCC[2]) && isprint(fourCC[4])) { NSLog(@"%@:%s",message, fourCC); } else { NSLog(@"%@:%d",message, (int)status); } if (fatal) { exit(-1); } } } ``` > 由于status每次报错都打印 相关数字大家可能不理解可以点击[OSStatus](https://www.osstatus.com/) 查询相关错误码 #### AVAudioMix 我们一般都在采集、录制或编辑音视频相应的类中使用AVAudioMixer. 举个例子:我们变声实现的流程大概是这个样子 __AVAudioPlayer -> AVPlayerItem -> AVAudioMixer-> AUGraph -> AUNode + AudioUnit__ ![](/assets/images/20180507AudioUnit/AVAudioMixClass.webp) #### AudioStreamBasicDescription 配置麦克风输入的参数 当我们控制Remote IO Unit的时候想告诉麦克风 各种input的参数 可以通过 一个叫ASBD 格式的结构体数据描述来设置给相应的Unit ##### Audio Stream Format 描述ASBD ``` objc UInt32 bytePerSample = sizeof(Float32); AudioStreamBasicDescription asbd; bzero(&asbd, sizeof(asbd)); asbd.mFormatID = kAudioFormatLinearPCM; asbd.mSampleRate = 44100; asbd.mChannelsPerFrame = channels; asbd.mFramesPerPacket = 1; asbd.mFormatFlags = kAudioFormatFlagsNativeFloatPacked | kAudioFormatFlagIsNonInterleaved; asbd.mBitsPerChannel = 8 * bytePerSample; asbd.mBytesPerFrame = bytePerSample; asbd.mBytesPerPacket = bytePerSample; ``` > 上边代码展示了如何填充ASBD结构体,这个描述音视频的具体格式. 下面具体介绍一下各个参数的意思 * mFormatID 可用来指定编码格式 eg:PCM * mSampleRate 采样率 * mChannelsPerFrame 每个Frame有几个channel * mFramesPerPacket 每个Packet有几Frame * mFormatFlags 这个是用来描述声音格式表示格式的参数,上面代码我们指定的是每个sample的表示格式为Float格式,有点类似SInt16,如果后边是NonInterleaved代表非交错的,对于这个音频来讲就是左右声道的是非交错存放的,实际的音频数据会存储在一个AudioBufferList结构中的变量mBuffers中,如果mFormatFlags指定的是NonInterleaved,那么左声道就在会在mBuffers[0]里面,右声道就在mBuffers[1]里面. * mBitsPerChannel 表示一个声道的音频数据用多少位来表示,上面我们用的是Float来表示, 所以这里使用的是 8 乘以 每个采样的字节数来赋值. * mBytesPerFrame 和 mBytesPerPacket 这两个的赋值需要根据mFormatFlags 的值来进行分配,如果是NonInterleaved非交错的情况下, 就赋值bytePerSample(因为左右声道是分开的).但如果是Interleaved的话,那就应该是 bytePerSample * channels (因为左右声道是交错存放),这样才能表示一个Frame里面到底有多少byte. 讲了这么多 那我们怎么把这个ASDB给 Unit? 如下代码 设置ASBD给相应的Audio Unit ``` objc AudioUnitSetProperty(remoteIOUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 1, &asbd, sizeof(asbd)); ``` 完整的代码如下 ``` objc //设置ASBD AudioStreamBasicDescription inputFormat; inputFormat.mSampleRate = 44100; inputFormat.mFormatID = kAudioFormatLinearPCM; inputFormat.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsNonInterleaved; inputFormat.mFramesPerPacket = 1; inputFormat.mChannelsPerFrame = 1; inputFormat.mBytesPerPacket = 2; inputFormat.mBytesPerFrame = 2; inputFormat.mBitsPerChannel = 16; //设置给输入端 配置麦克风输出的数据是什么格式 OSStatus status = noErr; status = AudioUnitSetProperty(audioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, InputBus, &inputFormat, sizeof(inputFormat)); CheckStatus(status, @"AudioUnitGetProperty bus1 output ASBD error", YES); ``` ### Audio Unit 分类 ``` objc CF_ENUM(UInt32) { kAudioUnitType_Output = 'auou', kAudioUnitType_MusicDevice = 'aumu', kAudioUnitType_MusicEffect = 'aumf', kAudioUnitType_FormatConverter = 'aufc', kAudioUnitType_Effect = 'aufx', kAudioUnitType_Mixer = 'aumx', kAudioUnitType_Panner = 'aupn', kAudioUnitType_Generator = 'augn', kAudioUnitType_OfflineEffect = 'auol', kAudioUnitType_MIDIProcessor = 'aumi' }; ``` | 分类 | 功能/作用 |类型 | | :------ | :------ | :------ | | Effect Unit | 提供声音特效处理| kAudioUnitType_Effect | | Mixer Units | 提供Mix多路声音的功能 | kAudioUnitType_Mixer| | I/O Units | I/O 采集音频与播放音频功能| kAudioUnitType_Output | | AUConverter Units | 格式转换 eg:采样格式Float转SInt16、交错或平铺、单双声道的转换| kAudioUnitType_FormatConverter | | Generator Units | 提供播放器功能 | kAudioUnitType_Generator | ``` objc CF_ENUM(UInt32) { kAudioUnitSubType_PeakLimiter = 'lmtr', kAudioUnitSubType_DynamicsProcessor = 'dcmp', kAudioUnitSubType_LowPassFilter = 'lpas', kAudioUnitSubType_HighPassFilter = 'hpas', kAudioUnitSubType_BandPassFilter = 'bpas', kAudioUnitSubType_HighShelfFilter = 'hshf', kAudioUnitSubType_LowShelfFilter = 'lshf', kAudioUnitSubType_ParametricEQ = 'pmeq', kAudioUnitSubType_Distortion = 'dist', kAudioUnitSubType_Delay = 'dely', kAudioUnitSubType_SampleDelay = 'sdly', kAudioUnitSubType_NBandEQ = 'nbeq' }; CF_ENUM(UInt32) { kAudioUnitSubType_Reverb2 = 'rvb2', kAudioUnitSubType_AUiPodEQ = 'ipeq' }; ``` #### Effect Unit 子类型及用途说明 | 子类型 | 用途说明 | 子枚举类型 | | :------ | :------ | :------ | |均衡效果器 | 为声音的某些[频带](https://baike.baidu.com/item/%E9%A2%91%E5%B8%A6)增强或衰减能量,效果器需要指定多个频带,然后为各频带设置增益最终改变声音在音域上的能量分布 | kAudioUnitSubType_NBandEQ| | 压缩效果器 | 当声音较小或较大通过设置阀值来提高或降低声音能量 eg:作用时间、释放时间、以及触发值从而最终控制声音在时域上的能量范围 | kAudioUnitSubType_DynamicsProcessor | | 混响效果器 | 通过声音反射的延迟控制声音效果 | kAudioUnitSubType_Reverb2 | > Effect Unit 下最常用的效果器就上边这三种, 像高通(High Pass)、低通(Low Pass)、带通(Band Pass)、延迟(Delay)、压限(Limiter) 等这些不是很常用,如果大家对这个很熟悉可以试试使用一下. #### Mixer Units 子类型及用途说明 | 子类型 | 用途说明 | 子枚举类型 | | :------ | :------ | :------ | | 3D Mixer | 仅支持 macOS | | | MultiChannelMixer | 多路声音混音效果器,可以接受多路音频输入,还可以分别调整每一路的音频增益和开关,并将多路音频合成一路 | kAudioUnitSubType_MultiChannelMixer | #### I/O Units 子类型及用途说明 | 子类型 | 用途说明 | 子枚举类型 | | :------ | :------ | :------ | | Remote I/O | 采集音频与播放音频,在Audio Unit中使用麦克风和扬声器的时候会用到这个Unit | kAudioUnitType_Output | | Generic Output | 进行离线处理,或者说AUGraph中不使用扬声器来驱动整个数据流,而希望使用一个输出(可以放入内存队列或者磁盘I/O操作)来驱动数据流时 | kAudioUnitSubType_GenericOutput | #### AUConverter Units 子类型及用途说明 | 子类型 | 用途说明 | 子枚举类型 | | :------ | :------ | :------ | | AUConverter | 格式转换,当某些效果器对输入的音频格式有明确要求时,或者我们将音频数据输入给一些其它的编码器进行编码。。。 |kAudioUnitSubType_AUConverter| |Time Pitch|变速变调效果器,调整声音音高. eg:会说话的Tom猫 |kAudioUnitSubType_NewTimePitch > 注意: AUConverter 如果由FFMpeg解码出来的PCM 是SInt16格式 如果要用格式转换效果器unit必须转成Float32格式表示的数据. #### Generator Units 子类型及用途说明 | 子类型 | 用途说明 | 子枚举类型 | | :------ | :------ | :------ | |AudioFilePlayer | 接收裸PCM 播放 一般大家可以用这个配合Remote I/O 做播放器 | kAudioUnitSubType_AudioFilePlayer | 相关shell命令 __将音频文件转成pcm__ ``` sh ffmpeg -i test.mp3 -acodec pcm_s16le -f s16le output.pcm ``` > brew install ffmpeg [Demo实现耳返功能](https://github.com/sunyazhou13/AduioUnitDemo) [Demo2实现耳返+伴奏播放](https://github.com/sunyazhou13/AudioUnitDemo2) #### 下面我分享一个变声中混响效果代码 ``` objc //声明部分 .h @interface KSYAudioReverbFilter : NSObject -(instancetype)init; - (void)setupWithAUGraph:(AUGraph)auGraph asbd:(const AudioStreamBasicDescription *)asbd maxFrame:(CMItemCount)max; // Global, CrossFade, 0->100, 100 @property (nonatomic) double dryWetMix; // Global, Decibels, -20->20, 0dB. @property (nonatomic) double gain; // Global, Secs, 0.0001->1.0, 0.008 @property (nonatomic) double minDelayTime; // Global, Secs, 0.0001->1.0, 0.050 @property (nonatomic) double maxDelayTime; // Global, Secs, 0.001->20.0, 1.0 @property (nonatomic) double decayTimeAt0Hz; // Global, Secs, 0.001->20.0, 0.5 @property (nonatomic) double decayTimeAtNyquist; // Global, Integer, 1->1000, 1 @property (nonatomic) double randomizeReflections; @end //实现部分 //通用的宏 #define RC_CHECK(rc, str) if (rc != noErr) \ { \ NSLog(@"Err :%@ %@ %@", @(rc), str, @(__func__)); \ } @implementation KSYAudioReverbFilter -(instancetype)init{ self = [super init]; if (self){ self.acDes = (AudioComponentDescription){kAudioUnitType_Effect, kAudioUnitSubType_Reverb2, kAudioUnitManufacturer_Apple, 0, 0}; } return self; } - (void)setupWithAUGraph:(AUGraph)auGraph asbd:(const AudioStreamBasicDescription *)asbd maxFrame:(CMItemCount)maxFrame { // OSStatus status = noErr; NSAssert(auGraph != nil, @"auGraph is null"); audioGraph = auGraph; NSLog(@"setup :%@", NSStringFromCode(_acDes.componentSubType)); status = AUGraphAddNode(auGraph, &_acDes, &_node); if (noErr != status){ NSString *error = [NSString stringWithFormat:@"add node with type %u failed", _acDes.componentType]; NSLog(@"%@", error); return ; } status = AUGraphNodeInfo(auGraph, _node, NULL, &_audioUnit); if (noErr != status){ NSLog(@"create audiouinit failed err:%@", @(status)); return ; } RC_CHECK(AudioUnitSetProperty(_audioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, asbd, sizeof(AudioStreamBasicDescription)), @"kAudioUnitProperty_StreamFormat kAudioUnitScope_Input err"); RC_CHECK(AudioUnitSetProperty(_audioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 0, asbd, sizeof(AudioStreamBasicDescription)), @"kAudioUnitProperty_StreamFormat kAudioUnitScope_Output err"); // Set audio unit maximum frames per slice to max frames. RC_CHECK(AudioUnitSetProperty(_audioUnit, kAudioUnitProperty_MaximumFramesPerSlice, kAudioUnitScope_Global, 0, &maxFrame, (UInt32)sizeof(UInt32)), @"set kAudioUnitProperty_MaximumFramesPerSlice err"); } #pragma mark - Setters - (void)setDryWetMix:(double)dryWetMix { [self setGlobalParam:kReverb2Param_DryWetMix value:dryWetMix]; } - (void)setGain:(double)gain { [self setGlobalParam:kReverb2Param_Gain value:gain]; } - (void)setMinDelayTime:(double)minDelayTime { [self setGlobalParam:kReverb2Param_MinDelayTime value:minDelayTime]; } - (void)setMaxDelayTime:(double)maxDelayTime { [self setGlobalParam:kReverb2Param_MaxDelayTime value:maxDelayTime]; } - (void)setDecayTimeAt0Hz:(double)decayTimeAt0Hz { [self setGlobalParam:kReverb2Param_DecayTimeAt0Hz value:decayTimeAt0Hz]; } - (void)setDecayTimeAtNyquist:(double)decayTimeAtNyquist { [self setGlobalParam:kReverb2Param_DecayTimeAtNyquist value:decayTimeAtNyquist]; } - (void)setRandomizeReflections:(double)randomizeReflections { [self setGlobalParam:kReverb2Param_RandomizeReflections value:randomizeReflections]; } //通用方法 - (void)setGlobalParam:(AudioUnitParameterID)paramId value:(AudioUnitParameterValue)value { RC_CHECK(AudioUnitSetParameter(_audioUnit, paramId, kAudioUnitScope_Global, 0, value, 0), ([NSString stringWithFormat:@"set %u value %f err", paramId, value])); } @end ``` 外部调用的话就是这样的 ``` objc AURenderCallbackStruct renderCallbackStruct; renderCallbackStruct.inputProc = ksyme_RenderCallback; renderCallbackStruct.inputProcRefCon = (void *)self.apt; if (!_reverbFilter){ _reverbFilter = [[KSYAudioReverbFilter alloc] init]; [_reverbFilter setupWithAUGraph:auGraph asbd:format maxFrame:max]; _reverbFilter.renderCallBack = renderCallbackStruct; } ``` ### 连接node ``` objc AUGraphClearConnections(auGraph); NSMutableArray *array = [[NSMutableArray alloc] init]; [array addObject:@(_mixFilter.node)]; [array addObjectsFromArray:@[@(_reverbFilter.node),@(_delayFilter.node),@(_pitchFilter.node)]]; for (int i = 0; i < array.count -1; i++) { AUGraphConnectNodeInput(auGraph,[array[i] intValue], 0,[array[i+1] intValue], 0); } ``` 核心代码就是如何连接Node ``` objc AUGraphConnectNodeInput(auGraph,reverbNode, 0, remoteIONode, 0) ``` > 0代表 bus0 系统定义的API是这样的 ``` objc extern OSStatus AUGraphConnectNodeInput( AUGraph inGraph, AUNode inSourceNode, UInt32 inSourceOutputNumber, AUNode inDestNode, UInt32 inDestInputNumber) __OSX_AVAILABLE_STARTING(__MAC_10_0,__IPHONE_2_0); ``` ## 总结 Audio Unit的相关技术学习点比较多大家灵活掌握运用,不懂没关系从简单的Unit开始学起. 全文完 参考列表: [iOS Audio相关术语(Glossary)](https://developer.apple.com/library/content/documentation/MusicAudio/Reference/CoreAudioGlossary/Glossary/core_audio_glossary.html#//apple_ref/doc/uid/TP40004453-CH210-SW1) [参考](https://developer.apple.com/library/content/documentation/MusicAudio/Conceptual/AudioUnitHostingGuide_iOS/Introduction/Introduction.html) [如何自己制作一个Audio Unit](https://developer.apple.com/library/content/documentation/MusicAudio/Conceptual/AudioUnitProgrammingGuide/Tutorial-BuildingASimpleEffectUnitWithAGenericView/Tutorial-BuildingASimpleEffectUnitWithAGenericView.html#//apple_ref/doc/uid/TP40003278-CH5-SW4) [金山云直播音效实现](https://www.jianshu.com/p/05cae433faea) [Audio Unit官方文档](https://developer.apple.com/library/content/documentation/MusicAudio/Conceptual/AudioUnitHostingGuide_iOS/AudioUnitHostingFundamentals/AudioUnitHostingFundamentals.html#//apple_ref/doc/uid/TP40009492-CH3-SW12) URL: https://sunyazhou.com/2018/04/RunLoop/index.html.md Published At: 2018-04-02 14:16:34 +0000 # 深入理解RunLoop ![](/assets/images/20180402RunLoop/RunLoop6.webp) # 前言 `RunLoop` 是 `iOS` 和 `OSX` 开发中非常基础的一个概念,这篇文章将从`CFRunLoop`的源码入手,介绍 `RunLoop` 的概念以及底层实现原理。之后会介绍一下在 `iOS` 中,苹果是如何利用`RunLoop`实现自动释放池、延迟回调、触摸事件、屏幕刷新等功能的。 ## 本文内容 * RunLoop 的概念 * RunLoop 与线程的关系 * RunLoop 对外的接口 * RunLoop 的 Mode * RunLoop 的内部逻辑 * RunLoop 的底层实现 * 苹果用 RunLoop 实现的功能 1. AutoreleasePool 2. 事件响应 3. 手势识别 4. 界面更新 5. 定时器 6. PerformSelecter 7. 关于GCD 8. 关于网络请求 * RunLoop 的实际应用举例 1. AFNetworking 2. AsyncDisplayKit ### RunLoop 的概念 一般来讲,一个线程一次只能执行一个任务,执行完成后线程就会退出。如果我们需要一个机制,让线程能随时处理事件但并不退出,通常的代码逻辑是这样的: ``` swift function loop() { initialize(); do { var message = get_next_message(); process_message(message); } while (message != quit); } ``` 这种模型通常被称作 [Event Loop](https://en.wikipedia.org/wiki/Event_loop)。 `Event Loop` 在很多系统和框架里都有实现,比如 `Node.js`的事件处理,比如 `Windows` 程序的消息循环,再比如 `OSX/iOS` 里的 `RunLoop`。实现这种模型的关键点在于:如何`管理事件/消息`,如何让线程在没有处理消息时休眠以避免资源占用、在有消息到来时立刻被唤醒。 所以,`RunLoop` 实际上就是一个对象,这个对象管理了其需要处理的事件和消息,并提供了一个入口函数来执行上面 `Event Loop` 的逻辑。线程执行了这个函数后,就会一直处于这个函数内部 “`接受消息`->`等待->`处理`” 的循环中,直到这个循环结束(比如传入 `quit` 的消息),函数返回。 `OSX/iOS` 系统中,提供了两个这样的对象:`NSRunLoop` 和 `CFRunLoopRef`。 `CFRunLoopRef` 是在 `CoreFoundation` 框架内的,它提供了纯 `C` 函数的 `API`,所有这些 `API` 都是线程安全的。 `NSRunLoop` 是基于 `CFRunLoopRef` 的封装,提供了面向对象的 `API`,但是这些 `API` 不是线程安全的。 CFRunLoopRef 的代码是[开源](http://opensource.apple.com/source/CF/CF-855.17/CFRunLoop.c)的,你可以在这里 [http://opensource.apple.com/tarballs/CF/](https://opensource.apple.com/tarballs/CF/) 下载到整个 `CoreFoundation` 的源码来查看。 (Update: Swift 开源后,苹果又维护了一个跨平台的 CoreFoundation 版本:[https://github.com/apple/swift-corelibs-foundation/](https://github.com/apple/swift-corelibs-foundation/),这个版本的源码可能和现有`iOS`系统中的实现略不一样,但更容易编译,而且已经适配了`Linux/Windows`。) ### RunLoop 与线程的关系 首先,`iOS` 开发中能遇到两个线程对象: `pthread_t` 和 `NSThread`。过去苹果有份文档标明了 `NSThread` 只是 `pthread_t` 的封装,但那份文档已经失效了,现在它们也有可能都是直接包装自最底层的 `mach thread`。苹果并没有提供这两个对象相互转换的接口,但不管怎么样,可以肯定的是 `pthread_t` 和 `NSThread` 是一一对应的。比如,你可以通过 `pthread_main_thread_np()` 或 `[NSThread mainThread]` 来获取主线程;也可以通过 `pthread_self()` 或 `[NSThread currentThread]` 来获取当前线程。`CFRunLoop` 是基于 `pthread` 来管理的。 苹果不允许直接创建 `RunLoop`,它只提供了两个自动获取的函数:`CFRunLoopGetMain()` 和 `CFRunLoopGetCurrent()`。 这两个函数内部的逻辑大概是下面这样: ``` c /// 全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef static CFMutableDictionaryRef loopsDic; /// 访问 loopsDic 时的锁 static CFSpinLock_t loopsLock; /// 获取一个 pthread 对应的 RunLoop。 CFRunLoopRef _CFRunLoopGet(pthread_t thread) { OSSpinLockLock(&loopsLock); if (!loopsDic) { // 第一次进入时,初始化全局Dic,并先为主线程创建一个 RunLoop。 loopsDic = CFDictionaryCreateMutable(); CFRunLoopRef mainLoop = _CFRunLoopCreate(); CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop); } /// 直接从 Dictionary 里获取。 CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread)); if (!loop) { /// 取不到时,创建一个 loop = _CFRunLoopCreate(); CFDictionarySetValue(loopsDic, thread, loop); /// 注册一个回调,当线程销毁时,顺便也销毁其对应的 RunLoop。 _CFSetTSD(..., thread, loop, __CFFinalizeRunLoop); } OSSpinLockUnLock(&loopsLock); return loop; } CFRunLoopRef CFRunLoopGetMain() { return _CFRunLoopGet(pthread_main_thread_np()); } CFRunLoopRef CFRunLoopGetCurrent() { return _CFRunLoopGet(pthread_self()); } ``` 从上面的代码可以看出,线程和`RunLoop`之间是一一对应的,其关系是保存在一个全局的 `Dictionary` 里。线程刚创建时并没有 `RunLoop`,如果你不主动获取,那它一直都不会有。`RunLoop` 的创建是发生在第一次获取时,`RunLoop` 的销毁是发生在线程结束时。你只能在一个线程的内部获取其 `RunLoop`(主线程除外). ### RunLoop 对外的接口 在 `CoreFoundation` 里面关于 `RunLoop` 有5个类: * `CFRunLoopRef` * `CFRunLoopModeRef` * `CFRunLoopSourceRef` * `CFRunLoopTimerRef` * `CFRunLoopObserverRef` 其中 `CFRunLoopModeRef` 类并没有对外暴露,只是通过 `CFRunLoopRef` 的接口进行了封装。他们的关系如下: ![](/assets/images/20180402RunLoop/RunLoop0.webp) 一个 `RunLoop` 包含若干个 `Mode`,每个 `Mode `又包含若干个 `Source`/`Timer`/`Observer`。每次调用 `RunLoop` 的主函数时,只能指定其中一个 `Mode`,这个`Mode`被称作 `CurrentMode`。如果需要切换 `Mode`,只能退出 `Loop`,再重新指定一个 `Mode` 进入。这样做主要是为了分隔开不同组的 `Source`/`Timer`/`Observer`,让其互不影响。 **CFRunLoopSourceRef** 是事件产生的地方。`Source`有两个版本:`Source0` 和 `Source1`。 * `Source0` 只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 `CFRunLoopSourceSignal(source)`,将这个 `Source` 标记为待处理,然后手动调用 `CFRunLoopWakeUp(runloop)` 来唤醒 `RunLoop`,让其处理这个事件。 * `Source1` 包含了一个 `mach_port` 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。这种 `Source` 能主动唤醒 `RunLoop` 的线程,其原理在下面会讲到。 **CFRunLoopTimerRef** 是基于时间的触发器,它和 `NSTimer` 是`toll-free bridged `的,可以混用。其包含一个时间长度和一个回调(函数指针)。当其加入到 `RunLoop` 时,`RunLoop`会注册对应的时间点,当时间点到时,`RunLoop`会被唤醒以执行那个回调。 **CFRunLoopObserverRef** 是观察者,每个 `Observer` 都包含了一个回调(函数指针),当 `RunLoop` 的状态发生变化时,观察者就能通过回调接受到这个变化。可以观测的时间点有以下几个: ``` c 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 }; ``` 上面的 `Source`/`Timer`/`Observer` 被统称为 `__mode item__`,一个 `item` 可以被同时加入多个`mode`。但一个 `item` 被重复加入同一个 `mode` 时是不会有效果的。如果一个 `mode` 中一个 `item` 都没有,则 `RunLoop` 会直接退出,不进入循环。 ### RunLoop 的 Mode `CFRunLoopMode` 和 `CFRunLoop` 的结构大致如下: ``` objc struct __CFRunLoopMode { CFStringRef _name; // Mode Name, 例如 @"kCFRunLoopDefaultMode" CFMutableSetRef _sources0; // Set CFMutableSetRef _sources1; // Set CFMutableArrayRef _observers; // Array CFMutableArrayRef _timers; // Array ... }; struct __CFRunLoop { CFMutableSetRef _commonModes; // Set CFMutableSetRef _commonModeItems; // Set CFRunLoopModeRef _currentMode; // Current Runloop Mode CFMutableSetRef _modes; // Set ... }; ``` 这里有个概念叫 “`CommonModes`”:一个 `Mode` 可以将自己标记为”`Common`”属性(通过将其 `ModeName` 添加到 `RunLoop` 的 “`commonModes`” 中)。每当 `RunLoop` 的内容发生变化时,`RunLoop` 都会自动将 `_commonModeItems` 里的 `Source`/`Observer`/`Timer`同步到具有 “`Common`” 标记的所有`Mode`里。 应用场景举例:主线程的 `RunLoop` 里有两个预置的 `Mode`:`kCFRunLoopDefaultMode` 和 `UITrackingRunLoopMode`。这两个 `Mode` 都已经被标记为”`Common`”属性。`DefaultMode` 是 `App` 平时所处的状态,`TrackingRunLoopMode` 是追踪 `ScrollView`滑动时的状态。当你创建一个`Timer`并加到 `DefaultMode` 时,`Timer` 会得到重复回调,但此时滑动一个`TableView`时,`RunLoop` 会将 `mode` 切换为 `TrackingRunLoopMode`,这时 `Timer` 就不会被回调,并且也不会影响到滑动操作。 有时你需要一个`Timer`,在两个 `Mode` 中都能得到回调,一种办法就是将这个 `Timer` 分别加入这两个 `Mode`。还有一种方式,就是将 `Timer` 加入到顶层的 `RunLoop` 的 “`commonModeItems`” 中。”`commonModeItems`” 被 `RunLoop` 自动更新到所有具有”`Common`”属性的 `Mode` 里去。 `CFRunLoop`对外暴露的管理 `Mode` 接口只有下面2个: ``` c CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName); CFRunLoopRunInMode(CFStringRef modeName, ...); ``` `Mode` 暴露的管理 `mode item` 的接口有下面几个: ``` c CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName); CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName); CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode); CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName); CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName); CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode); ``` 你只能通过 `mode name` 来操作内部的 `mode`,当你传入一个新的 `mode name` 但 `RunLoop` 内部没有对应 `mode` 时,`RunLoop`会自动帮你创建对应的 `CFRunLoopModeRef`。对于一个 `RunLoop` 来说,其内部的 `mode` 只能增加不能删除。 苹果公开提供的 `Mode` 有两个:`kCFRunLoopDefaultMode` (`NSDefaultRunLoopMode`) 和 `UITrackingRunLoopMode`,你可以用这两个 `Mode Name` 来操作其对应的`Mode`。 同时苹果还提供了一个操作 `Common` 标记的字符串:`kCFRunLoopCommonModes` (`NSRunLoopCommonModes`),你可以用这个字符串来操作 `Common Items`,或标记一个 `Mode` 为 “`Common`”。使用时注意区分这个字符串和其他 `mode name`。 ### RunLoop 的内部逻辑 根据苹果在[文档](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html#//apple_ref/doc/uid/10000057i-CH16-SW23)里的说明,`RunLoop` 内部的逻辑大致如下: ![](/assets/images/20180402RunLoop/RunLoop1.webp) 其内部代码整理如下 (太长了不想看可以直接跳过去,后面会有说明) ``` c /// 用DefaultMode启动 void CFRunLoopRun(void) { CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false); } /// 用指定的Mode启动,允许设置RunLoop超时时间 int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) { return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled); } /// RunLoop的实现 int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) { /// 首先根据modeName找到对应mode CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false); /// 如果mode里没有source/timer/observer, 直接返回。 if (__CFRunLoopModeIsEmpty(currentMode)) return; /// 1. 通知 Observers: RunLoop 即将进入 loop。 __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry); /// 内部函数,进入loop __CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) { Boolean sourceHandledThisLoop = NO; int retVal = 0; do { /// 2. 通知 Observers: RunLoop 即将触发 Timer 回调。 __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers); /// 3. 通知 Observers: RunLoop 即将触发 Source0 (非port) 回调。 __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources); /// 执行被加入的block __CFRunLoopDoBlocks(runloop, currentMode); /// 4. RunLoop 触发 Source0 (非port) 回调。 sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle); /// 执行被加入的block __CFRunLoopDoBlocks(runloop, currentMode); /// 5. 如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息。 if (__Source0DidDispatchPortLastTime) { Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg) if (hasMsg) goto handle_msg; } /// 通知 Observers: RunLoop 的线程即将进入休眠(sleep)。 if (!sourceHandledThisLoop) { __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting); } /// 7. 调用 mach_msg 等待接受 mach_port 的消息。线程将进入休眠, 直到被下面某一个事件唤醒。 /// • 一个基于 port 的Source 的事件。 /// • 一个 Timer 到时间了 /// • RunLoop 自身的超时时间到了 /// • 被其他什么调用者手动唤醒 __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) { mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg } /// 8. 通知 Observers: RunLoop 的线程刚刚被唤醒了。 __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting); /// 收到消息,处理消息。 handle_msg: /// 9.1 如果一个 Timer 到时间了,触发这个Timer的回调。 if (msg_is_timer) { __CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time()) } /// 9.2 如果有dispatch到main_queue的block,执行block。 else if (msg_is_dispatch) { __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg); } /// 9.3 如果一个 Source1 (基于port) 发出事件了,处理这个事件 else { CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort); sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg); if (sourceHandledThisLoop) { mach_msg(reply, MACH_SEND_MSG, reply); } } /// 执行加入到Loop的block __CFRunLoopDoBlocks(runloop, currentMode); if (sourceHandledThisLoop && stopAfterHandle) { /// 进入loop时参数说处理完事件就返回。 retVal = kCFRunLoopRunHandledSource; } else if (timeout) { /// 超出传入参数标记的超时时间了 retVal = kCFRunLoopRunTimedOut; } else if (__CFRunLoopIsStopped(runloop)) { /// 被外部调用者强制停止了 retVal = kCFRunLoopRunStopped; } else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) { /// source/timer/observer一个都没有了 retVal = kCFRunLoopRunFinished; } /// 如果没超时,mode里没空,loop也没被停止,那继续loop。 } while (retVal == 0); } /// 10. 通知 Observers: RunLoop 即将退出。 __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit); } ``` ### RunLoop 的底层实现 从上面代码可以看到,`RunLoop` 的核心是基于 `mach port` 的,其进入休眠时调用的函数是 `mach_msg()`。为了解释这个逻辑,下面稍微介绍一下 `OSX/iOS` 的系统架构。 ![](/assets/images/20180402RunLoop/RunLoop3.webp) 苹果官方将整个系统大致划分为上述4个层次: 应用层包括用户能接触到的图形应用,例如 `Spotlight`、`Aqua`、`SpringBoard` 等。 应用框架层即开发人员接触到的 `Cocoa `等框架。 核心框架层包括各种核心框架、`OpenGL` 等内容。 `Darwin` 即操作系统的核心,包括系统内核、驱动、`Shell` 等内容,这一层是开源的,其所有源码都可以在 [opensource.apple.com](https://opensource.apple.com/) 里找到。 __我们在深入看一下 Darwin 这个核心的架构:__ ![](/assets/images/20180402RunLoop/RunLoop4.webp) 其中,在硬件层上面的三个组成部分:`Mach`、`BSD`、`IOKit` (还包括一些上面没标注的内容),共同组成了 `XNU` 内核。 `XNU` 内核的内环被称作 `Mach`,其作为一个微内核,仅提供了诸如处理器调度、`IPC` (进程间通信)等非常少量的基础服务。 `BSD` 层可以看作围绕 `Mach` 层的一个外环,其提供了诸如进程管理、文件系统和网络等功能。 `IOKit` 层是为设备驱动提供了一个面向对象(`C++`)的一个框架。 `Mach` 本身提供的 `API` 非常有限,而且苹果也不鼓励使用 `Mach` 的 `API`,但是这些`API`非常基础,如果没有这些`API`的话,其他任何工作都无法实施。在 `Mach` 中,所有的东西都是通过自己的对象实现的,进程、线程和虚拟内存都被称为”对象”。和其他架构不同, `Mach` 的对象间不能直接调用,只能通过消息传递的方式实现对象间的通信。”`消息`”是 `Mach` 中最基础的概念,消息在两个端口 (`port`) 之间传递,这就是 `Mach` 的 `IPC` (进程间通信) 的核心。 `Mach` 的消息定义是在 `` 头文件的,很简单: ``` c typedef struct { mach_msg_header_t header; mach_msg_body_t body; } mach_msg_base_t; typedef struct { mach_msg_bits_t msgh_bits; mach_msg_size_t msgh_size; mach_port_t msgh_remote_port; mach_port_t msgh_local_port; mach_port_name_t msgh_voucher_port; mach_msg_id_t msgh_id; } mach_msg_header_t; ``` 一条 `Mach` 消息实际上就是一个二进制数据包 (`BLOB`),其头部定义了当前端口 `local_port` 和目标端口 `remote_port`,发送和接受消息是通过同一个 `API` 进行的,其 `option` 标记了消息传递的方向: ``` c mach_msg_return_t mach_msg( mach_msg_header_t *msg, mach_msg_option_t option, mach_msg_size_t send_size, mach_msg_size_t rcv_size, mach_port_name_t rcv_name, mach_msg_timeout_t timeout, mach_port_name_t notify); ``` 为了实现消息的发送和接收,`mach_msg()` 函数实际上是调用了一个 `Mach` 陷阱 `(trap)`,即函数`mach_msg_trap()`,陷阱这个概念在 `Mach` 中等同于系统调用。当你在用户态调用`mach_msg_trap()` 时会触发陷阱机制,切换到内核态;内核态中内核实现的 `mach_msg()` 函数会完成实际的工作,如下图: ![](/assets/images/20180402RunLoop/RunLoop5.webp) 这些概念可以参考维基百科: [System_call](http://en.wikipedia.org/wiki/System_call)、[Trap_(computing)](http://en.wikipedia.org/wiki/Trap_(computing))。 `RunLoop` 的核心就是一个 `mach_msg()` (见上面代码的第7步),`RunLoop` 调用这个函数去接收消息,如果没有别人发送 `port` 消息过来,内核会将线程置于等待状态。例如你在模拟器里跑起一个 `iOS` 的 `App`,然后在 `App` 静止时点击暂停,你会看到主线程调用栈是停留在 `mach_msg_trap()` 这个地方。 关于具体的如何利用 `mach port` 发送信息,可以看看 [NSHipster 这一篇文章](http://nshipster.com/inter-process-communication/),或者[这里](http://segmentfault.com/a/1190000002400329)的中文翻译. 关于`Mach`的历史可以看看这篇很有趣的文章:[Mac OS X 背后的故事(三)Mach 之父 Avie Tevanian](http://www.programmer.com.cn/8121/)。 ### 苹果用 RunLoop 实现的功能 首先我们可以看一下`App`启动后`RunLoop`的状态: ``` c CFRunLoop { current mode = kCFRunLoopDefaultMode common modes = { UITrackingRunLoopMode kCFRunLoopDefaultMode } common mode items = { // source0 (manual) CFRunLoopSource {order =-1, { callout = _UIApplicationHandleEventQueue}} CFRunLoopSource {order =-1, { callout = PurpleEventSignalCallback }} CFRunLoopSource {order = 0, { callout = FBSSerialQueueRunLoopSourceHandler}} // source1 (mach port) CFRunLoopSource {order = 0, {port = 17923}} CFRunLoopSource {order = 0, {port = 12039}} CFRunLoopSource {order = 0, {port = 16647}} CFRunLoopSource {order =-1, { callout = PurpleEventCallback}} CFRunLoopSource {order = 0, {port = 2407, callout = _ZL20notify_port_callbackP12__CFMachPortPvlS1_}} CFRunLoopSource {order = 0, {port = 1c03, callout = __IOHIDEventSystemClientAvailabilityCallback}} CFRunLoopSource {order = 0, {port = 1b03, callout = __IOHIDEventSystemClientQueueCallback}} CFRunLoopSource {order = 1, {port = 1903, callout = __IOMIGMachPortPortCallback}} // Ovserver CFRunLoopObserver {order = -2147483647, activities = 0x1, // Entry callout = _wrapRunLoopWithAutoreleasePoolHandler} CFRunLoopObserver {order = 0, activities = 0x20, // BeforeWaiting callout = _UIGestureRecognizerUpdateObserver} CFRunLoopObserver {order = 1999000, activities = 0xa0, // BeforeWaiting | Exit callout = _afterCACommitHandler} CFRunLoopObserver {order = 2000000, activities = 0xa0, // BeforeWaiting | Exit callout = _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv} CFRunLoopObserver {order = 2147483647, activities = 0xa0, // BeforeWaiting | Exit callout = _wrapRunLoopWithAutoreleasePoolHandler} // Timer CFRunLoopTimer {firing = No, interval = 3.1536e+09, tolerance = 0, next fire date = 453098071 (-4421.76019 @ 96223387169499), callout = _ZN2CAL14timer_callbackEP16__CFRunLoopTimerPv (QuartzCore.framework)} }, modes = { CFRunLoopMode { sources0 = { /* same as 'common mode items' */ }, sources1 = { /* same as 'common mode items' */ }, observers = { /* same as 'common mode items' */ }, timers = { /* same as 'common mode items' */ }, }, CFRunLoopMode { sources0 = { /* same as 'common mode items' */ }, sources1 = { /* same as 'common mode items' */ }, observers = { /* same as 'common mode items' */ }, timers = { /* same as 'common mode items' */ }, }, CFRunLoopMode { sources0 = { CFRunLoopSource {order = 0, { callout = FBSSerialQueueRunLoopSourceHandler}} }, sources1 = (null), observers = { CFRunLoopObserver >{activities = 0xa0, order = 2000000, callout = _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv} )}, timers = (null), }, CFRunLoopMode { sources0 = { CFRunLoopSource {order = -1, { callout = PurpleEventSignalCallback}} }, sources1 = { CFRunLoopSource {order = -1, { callout = PurpleEventCallback}} }, observers = (null), timers = (null), }, CFRunLoopMode { sources0 = (null), sources1 = (null), observers = (null), timers = (null), } } } ``` 可以看到,系统默认注册了5个`Mode: 1. `kCFRunLoopDefaultMode`: `App`的默认 `Mode`,通常主线程是在这个 `Mode` 下运行的。 2. `UITrackingRunLoopMode`: 界面跟踪 `Mode`,用于 `ScrollView` 追踪触摸滑动,保证界面滑动时不受其他 `Mode` 影响。 3. `UIInitializationRunLoopMode`: 在刚启动 `App` 时第进入的第一个 `Mode`,启动完成后就不再使用。 4. `GSEventReceiveRunLoopMode`: 接受系统事件的内部 `Mode`,通常用不到。 5. `kCFRunLoopCommonModes`: 这是一个占位的 `Mode`,没有实际作用。 你可以在[这里](http://iphonedevwiki.net/index.php/CFRunLoop)看到更多的苹果内部的 `Mode`,但那些 `Mode` 在开发中就很难遇到了。 当 `RunLoop` 进行回调时,一般都是通过一个很长的函数调用出去 (`call out`), 当你在你的代码中下断点调试时,通常能在调用栈上看到这些函数。下面是这几个函数的整理版本,如果你在调用栈中看到这些长函数名,在这里查找一下就能定位到具体的调用地点了: ``` c { /// 1. 通知Observers,即将进入RunLoop /// 此处有Observer会创建AutoreleasePool: _objc_autoreleasePoolPush(); __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry); do { /// 2. 通知 Observers: 即将触发 Timer 回调。 __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers); /// 3. 通知 Observers: 即将触发 Source (非基于port的,Source0) 回调。 __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources); __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block); /// 4. 触发 Source0 (非基于port的) 回调。 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0); __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block); /// 6. 通知Observers,即将进入休眠 /// 此处有Observer释放并新建AutoreleasePool: _objc_autoreleasePoolPop(); _objc_autoreleasePoolPush(); __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeWaiting); /// 7. sleep to wait msg. mach_msg() -> mach_msg_trap(); /// 8. 通知Observers,线程被唤醒 __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting); /// 9. 如果是被Timer唤醒的,回调Timer __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer); /// 9. 如果是被dispatch唤醒的,执行所有调用 dispatch_async 等方法放入main queue 的 block __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block); /// 9. 如果如果Runloop是被 Source1 (基于port的) 的事件唤醒了,处理这个事件 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1); } while (...); /// 10. 通知Observers,即将退出RunLoop /// 此处有Observer释放AutoreleasePool: _objc_autoreleasePoolPop(); __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopExit); } ``` #### AutoreleasePool App启动后,苹果在主线程 `RunLoop` 里注册了两个 `Observer`,其回调都是 `_wrapRunLoopWithAutoreleasePoolHandler()`。 第一个 `Observer` 监视的事件是 `Entry`(即将进入`Loop`),其回调内会调用 `_objc_autoreleasePoolPush()` 创建自动释放池。其 `order` 是`-2147483647`,优先级最高,保证创建释放池发生在其他所有回调之前。 第二个 `Observer` 监视了两个事件: `BeforeWaiting`(准备进入休眠) 时调用`_objc_autoreleasePoolPop()` 和 `_objc_autoreleasePoolPush()` 释放旧的池并创建新池;`Exit`(即将退出`Loop`) 时调用 `_objc_autoreleasePoolPop()` 来释放自动释放池。这个 `Observer` 的 `order` 是 `2147483647`,优先级最低,保证其释放池子发生在其他所有回调之后。 在主线程执行的代码,通常是写在诸如事件回调、`Timer`回调内的。这些回调会被 `RunLoop` 创建好的 `AutoreleasePool` 环绕着,所以不会出现内存泄漏,开发者也不必显示创建` Pool` 了。 #### 事件响应 苹果注册了一个 `Source1` (基于 `mach port` 的) 用来接收系统事件,其回调函数为 `__IOHIDEventSystemClientQueueCallback()`。 当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个 `IOHIDEvent` 事件并由 `SpringBoard` 接收。这个过程的详细情况可以参考[这里](http://iphonedevwiki.net/index.php/IOHIDFamily)。`SpringBoard` 只接收按键(锁屏/静音等),触摸,加速,接近传感器等几种 `Event`,随后用 `mach port` 转发给需要的`App`进程。随后苹果注册的那个 `Source1` 就会触发回调,并调用 `_UIApplicationHandleEventQueue()` 进行应用内部的分发。 `_UIApplicationHandleEventQueue()` 会把 `IOHIDEvent` 处理并包装成 `UIEvent` 进行处理或分发,其中包括识别 `UIGesture`/`处理屏幕旋转`/发送给 `UIWindow` 等。通常事件比如 `UIButton` 点击、`touchesBegin`/`Move`/`End`/`Cancel` 事件都是在这个回调中完成的。 #### 手势识别 当上面的 `_UIApplicationHandleEventQueue()` 识别了一个手势时,其首先会调用 `Cancel` 将当前的 `touchesBegin`/`Move`/`End` 系列回调打断。随后系统将对应的 `UIGestureRecognizer` 标记为待处理。 苹果注册了一个 `Observer` 监测 `BeforeWaiting` (`Loop`即将进入休眠) 事件,这个`Observer`的回调函数是 `_UIGestureRecognizerUpdateObserver()`,其内部会获取所有刚被标记为待处理的 `GestureRecognizer`,并执行`GestureRecognizer`的回调。 当有 `UIGestureRecognizer` 的变化(`创建`/`销毁`/`状态改变`)时,这个回调都会进行相应处理。 #### 界面更新 当在操作 `UI` 时,比如改变了 `Frame`、更新了 `UIView`/`CALayer` 的层次时,或者手动调用了 `UIView`/`CALayer` 的 `setNeedsLayout`/`setNeedsDisplay`方法后,这个 `UIView`/`CALayer` 就被标记为待处理,并被提交到一个全局的容器去。 苹果注册了一个 `Observer` 监听 `BeforeWaiting`(即将进入休眠) 和 `Exit` (即将退出`Loop`) 事件,回调去执行一个很长的函数: `_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()`。这个函数里会遍历所有待处理的 `UIView`/`CAlayer` 以执行实际的绘制和调整,并更新 `UI` 界面。 这个函数内部的调用栈大概是这样的: ``` c++ _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv() QuartzCore:CA::Transaction::observer_callback: CA::Transaction::commit(); CA::Context::commit_transaction(); CA::Layer::layout_and_display_if_needed(); CA::Layer::layout_if_needed(); [CALayer layoutSublayers]; [UIView layoutSubviews]; CA::Layer::display_if_needed(); [CALayer display]; [UIView drawRect]; ``` #### 定时器 `NSTimer` 其实就是 `CFRunLoopTimerRef`,他们之间是 `toll-free bridged` 的。一个 `NSTimer` 注册到 `RunLoop` 后,`RunLoop` 会为其重复的时间点注册好事件。例如 `10:00`, `10:10`, `10:20` 这几个时间点。`RunLoop`为了节省资源,并不会在非常准确的时间点回调这个`Timer`。`Timer` 有个属性叫做 `Tolerance` (宽容度),标示了当时间点到后,容许有多少最大误差。 如果某个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行。就比如等公交,如果 `10:10` 时我忙着玩手机错过了那个点的公交,那我只能等 `10:20` 这一趟了。 `CADisplayLink` 是一个和屏幕刷新率一致的定时器(但实际实现原理更复杂,和 `NSTimer` 并不一样,其内部实际是操作了一个 `Source`)。如果在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被跳过去(和 `NSTimer` 相似),造成界面卡顿的感觉。在快速滑动`TableView`时,即使一帧的卡顿也会让用户有所察觉。`Facebook` 开源的`AsyncDisplayLink` 就是为了解决界面卡顿的问题,其内部也用到了 `RunLoop`,这个稍后我会再单独写一页博客来分析。 #### PerformSelecter 当调用 `NSObject` 的 `performSelecter:afterDelay:` 后,实际上其内部会创建一个 `Timer` 并添加到当前线程的 `RunLoop` 中。所以如果当前线程没有 `RunLoop`,则这个方法会失效。 当调用 `performSelector:onThread:` 时,实际上其会创建一个 `Timer` 加到对应的线程去,同样的,如果对应线程没有 `RunLoop` 该方法也会失效。 #### 关于GCD 实际上 `RunLoop` 底层也会用到 GCD 的东西, 例如 `dispatch_async()`。 > NSTimer 是用了 XNU 内核的 `mk_timer`来驱动的,而非 GCD 驱动的. 当调用 `dispatch_async(dispatch_get_main_queue(), block)` 时,`libDispatch` 会向主线程的 `RunLoop` 发送消息,RunLoop会被唤醒,并从消息中取得这个 `block`,并在回调 `__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__()` 里执行这个 `block`。但这个逻辑仅限于 `dispatch` 到主线程,`dispatch` 到其他线程仍然是由 `libDispatch` 处理的。 #### 关于网络请求 iOS 中,关于网络请求的接口自下至上有如下几层: ``` c CFSocket CFNetwork ->ASIHttpRequest NSURLConnection ->AFNetworking NSURLSession ->AFNetworking2, Alamofire ``` * `CFSocket` 是最底层的接口,只负责 `socket` 通信。 * `CFNetwork` 是基于 `CFSocket` 等接口的上层封装,`ASIHttpRequest` 工作于这一层。 * `NSURLConnection` 是基于 `CFNetwork` 的更高层的封装,提供面向对象的接口,`AFNetworking`工作于这一层。 * `NSURLSession` 是 `iOS7` 中新增的接口,表面上是和 `NSURLConnection`并列的,但底层仍然用到了 `NSURLConnection` 的部分功能 (比如 `com.apple.NSURLConnectionLoader` 线程),`AFNetworking2` 和 `Alamofire` 工作于这一层。 ##### 下面主要介绍下 NSURLConnection 的工作过程。 通常使用 `NSURLConnection`时,你会传入一个 `Delegate`,当调用了 `[connection start]` 后,这个 `Delegate` 就会不停收到事件回调。实际上,`start` 这个函数的内部会会获取 `CurrentRunLoop`,然后在其中的 `DefaultMode` 添加了4个 `Source0 `(即需要手动触发的`Source`)。`CFMultiplexerSource` 是负责各种 `Delegate` 回调的,`CFHTTPCookieStorage` 是处理各种 `Cookie` 的。 当开始网络传输时,我们可以看到 `NSURLConnection` 创建了两个新线程:`com.apple.NSURLConnectionLoader` 和 `com.apple.CFSocket.private`。其中 `CFSocket` 线程是处理底层 `socket` 连接的。`NSURLConnectionLoader` 这个线程内部会使用 `RunLoop` 来接收底层 `socket` 的事件,并通过之前添加的 `Source0` 通知到上层的 `Delegate`。 ![](/assets/images/20180402RunLoop/RunLoopNetwork.webp) `NSURLConnectionLoader` 中的 `RunLoop` 通过一些基于 `mach port` 的`Source` 接收来自底层 `CFSocket` 的通知。当收到通知后,其会在合适的时机向 `CFMultiplexerSource` 等 `Source0` 发送通知,同时唤醒 `Delegate` 线程的 `RunLoop` 来让其处理这些通知。`CFMultiplexerSource` 会在 `Delegate` 线程的 `RunLoop` 对 `Delegate` 执行实际的回调。 ### RunLoop 的实际应用举例 #### AFNetworking [AFURLConnectionOperation](https://github.com/AFNetworking/AFNetworking/blob/master/AFNetworking%2FAFURLConnectionOperation.m) 这个类是基于 `NSURLConnection` 构建的,其希望能在后台线程接收 `Delegate` 回调。为此 `AFNetworking `单独创建了一个线程,并在这个线程中启动了一个 `RunLoop` ``` objc + (void)networkRequestThreadEntryPoint:(id)__unused object { @autoreleasepool { [[NSThread currentThread] setName:@"AFNetworking"]; NSRunLoop *runLoop = [NSRunLoop currentRunLoop]; [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode]; [runLoop run]; } } + (NSThread *)networkRequestThread { static NSThread *_networkRequestThread = nil; static dispatch_once_t oncePredicate; dispatch_once(&oncePredicate, ^{ _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil]; [_networkRequestThread start]; }); return _networkRequestThread; } ``` `RunLoop` 启动前内部必须要有至少一个 `Timer`/`Observer`/`Source`,所以 `AFNetworking` 在 `[runLoop run]` 之前先创建了一个新的 `NSMachPort` 添加进去了。通常情况下,调用者需要持有这个 `NSMachPort` (`mach_port`) 并在外部线程通过这个 `port` 发送消息到 `loop` 内;但此处添加 `port` 只是为了让 `RunLoop` 不至于退出,并没有用于实际的发送消息。 ``` objc - (void)start { [self.lock lock]; if ([self isCancelled]) { [self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]]; } else if ([self isReady]) { self.state = AFOperationExecutingState; [self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]]; } [self.lock unlock]; } ``` 当需要这个后台线程执行任务时,`AFNetworking` 通过调用 `[NSObject performSelector:onThread:..]` 将这个任务扔到了后台线程的 `RunLoop` 中。 #### AsyncDisplayKit [AsyncDisplayKit](https://github.com/facebook/AsyncDisplayKit) 是 Facebook 推出的用于保持界面流畅性的框架,其原理大致如下: `UI` 线程中一旦出现繁重的任务就会导致界面卡顿,这类任务通常分为3类:`排版`,`绘制`,`UI对象操作`。 排版通常包括计算视图大小、计算文本高度、重新计算子式图的排版等操作。 绘制一般有文本绘制 (例如 `CoreText`)、图片绘制 (例如预先解压)、元素绘制 (`Quartz`)等操作。 `UI`对象操作通常包括 `UIView`/`CALayer` 等 UI 对象的创建、设置属性和销毁。 其中前两类操作可以通过各种方法扔到后台线程执行,而最后一类操作只能在主线程完成,并且有时后面的操作需要依赖前面操作的结果 (例如`TextView`创建时可能需要提前计算出文本的大小)。`ASDK` 所做的,就是尽量将能放入后台的任务放入后台,不能的则尽量推迟 (例如视图的创建、属性的调整)。 为此,`ASDK` 创建了一个名为 `ASDisplayNode` 的对象,并在内部封装了 `UIView`/`CALayer`,它具有和 `UIView`/`CALayer` 相似的属性,例如 `frame`、`backgroundColor`等。所有这些属性都可以在后台线程更改,开发者可以只通过 `Node` 来操作其内部的 `UIView`/`CALayer`,这样就可以将排版和绘制放入了后台线程。但是无论怎么操作,这些属性总需要在某个时刻同步到主线程的 `UIView`/`CALayer` 去。 `ASDK` 仿照 `QuartzCore`/`UIKit` 框架的模式,实现了一套类似的界面更新的机制:即在主线程的 `RunLoop` 中添加一个 `Observer`,监听了 `kCFRunLoopBeforeWaiting` 和 `kCFRunLoopExit` 事件,在收到回调时,遍历所有之前放入队列的待处理的任务,然后一一执行。 具体的代码可以看这里:[_ASAsyncTransactionGroup](https://github.com/facebook/AsyncDisplayKit/blob/master/AsyncDisplayKit%2FDetails%2FTransactions%2F_ASAsyncTransactionGroup.m)。 ### 最后 `RunLoop` 需要深入理解 全文完 [参考](https://blog.ibireme.com/2015/05/18/runloop/#base) URL: https://sunyazhou.com/2018/03/Swft4ArraySkills/index.html.md Published At: 2018-03-14 10:17:56 +0000 # Swift 4 中的数组技巧 # 前言 年前买了本[Swift 进阶](https://objccn.io/products/advanced-swift/)(swift4.0),过完年回来正在一点点学习,不得不说喵神写的东西还是不错的,¥69元对广大程序员来说已经不算啥了.如果感兴趣可以买一本,真心不错 当我从头来学习数组的时候发现好多函数真的太有用了 ## Swift 4.0 中的可变数组技巧 我们可用 Xcode 创建playground 来进行练习 __首先创建个数组__ ``` swift let array = NSMutableArray(array: [1, 2, 3, 4 , 5, 6]) ``` __for in 循环遍历__ ``` swift for x in array { print(x) } ``` 打印 ``` sh 1 2 3 4 5 6 ``` __想要扣除第一个元素剩余的元素进行迭代遍历呢?__ ``` swift for x in array.dropFirst(){ print(x) } ``` 打印 ``` sh 2 3 4 5 6 ``` > dropFirst() 函数参数是可以添加数值的 for x in array.dropFirst(3) 打印:4 5 6. 有 `first` 的地方基本就有`last` __想要扣除最后 3 个元素以外的元素进行遍历?__ ``` swift for x in array.dropLast(3){ print(x) } ``` 打印 ``` sh 1 2 3 ``` __带下标和数组元素遍历__ ``` swift for (num, element) in array.enumerated() { print(num, element) } ``` 打印 左边下标 右边元素 ``` sh 0 1 1 2 2 3 3 4 4 5 5 6 ``` > 左边下标 右边元素 全文完 URL: https://sunyazhou.com/2018/03/WhatIsThedSYM/index.html.md Published At: 2018-03-08 11:14:12 +0000 # 什么是符号表? ![](/assets/images/20180308WhatIsThedSYM/homePageLog.webp) # 前言 iOS 开发中经常回定位 bug 通过崩溃堆栈,此时我们需要借助符号表来恢复内存地址对应代码调用信息,为了解开这个大家耳熟能详却总有人问的问题的面纱,我在 bugle 平台和一些文章中收集了相关知识整理出来,以便后续方便记忆. ## 本周主要内容如下 * 什么是符号表? * 为什么要配置符号表? * dSYM文件? ### 什么是符号表? 符号表是内存地址与函数名、文件名、行号的映射表。符号表元素如下所示: `<起始地址>` `<结束地址>` `<函数>` [`<文件名>`:`<行号>`] ### 为什么要配置符号表? 为了能快速并准确地定位用户APP发生`Crash`的代码`位置`,我们可以使用符号表对APP发生`Crash`的程序`堆栈`进行`解析`和`还原`。 举一个例子: ![](/assets/images/20180308WhatIsThedSYM/stackSymbol.webp) 上图是我们通过符号表来解析出来崩溃堆栈的调用 ### 什么是dSYM文件? iOS平台中,`dSYM`文件是指具有调试信息的目标文件,文件名通常为: `com.公司名.dSYM`。如下图所示: ![](/assets/images/20180308WhatIsThedSYM/testdSYM.webp) 一般都是和Xcode 工程名的 aget一样的名字 > 为了方便找回Crash对应的dSYM文件和还原堆栈,建议每次构建或者发布APP版本的时候,备份好dSYM文件 #### 如何定位dSYM文件? 一般情况下,项目编译完`dSYM`文件跟`app`文件在同一个目录下,下面以`XCode`作为IDE详细说明定位`dSYM`文件 ![](/assets/images/20180308WhatIsThedSYM/dSYM1.webp) ![](/assets/images/20180308WhatIsThedSYM/dSYM2.webp) > 这里用 release 模式做的测试 我们看到 和工程 `target`一样的名称的 `.dSYM`. #### XCode编译后没有生成dSYM文件? XCode在 `Release`编译环境下默认会生成`dSYM`文件,而`Debug`编译环境下默认不会生成 如果要在`Debug`对应的`Xcode`配置如下: `XCode -> Build Settings -> Code Generation -> Generate Debug Symbols -> Yes` `XCode -> Build Settings -> Build Option -> Debug Information Format -> DWARF with dSYM File` ![](/assets/images/20180308WhatIsThedSYM/dSYM3.webp) ![](/assets/images/20180308WhatIsThedSYM/dSYM4.webp) #### 开启Bitcode之后需要注意哪些问题? * 在点`Upload to App Store`上传到`App Store`服务器的时候需要声明符号文件(`dSYM`文件)的生成: ![](/assets/images/20180308WhatIsThedSYM/dSYM5.webp) * 在配置符号表文件之前,需要从App Store中把该版本对应的dSYM文件下载回本地,然后用符号表工具生成和上传符号表文件。 这里找回`ipa`版本对应的dSYM文件有两种方式 1. 通过Xcode的归档文件找回dSYM,打开`Xcode` 顶部菜单栏 -> `Window` -> `Organizer` 窗口,如下图: ![](/assets/images/20180308WhatIsThedSYM/BitcodedSYM2.webp) 打开 `Xcode` 顶部菜单栏,选择`Archive` 标签: ![](/assets/images/20180308WhatIsThedSYM/BitcodedSYM3.webp) 找到发布的归档包,右键点击对应归档包,选择`Show in Finder`操作: ![](/assets/images/20180308WhatIsThedSYM/BitcodedSYM4.webp) 右键选择定位到的归档文件,选择显示包内容操作: ![](/assets/images/20180308WhatIsThedSYM/BitcodedSYM5.webp) 选择`dSYMs`目录,目录内即为下载到的 `dSYM` 文件: ![](/assets/images/20180308WhatIsThedSYM/BitcodedSYM6.webp) 2. 通过[iTunes Connect](https://itunesconnect.apple.com/)找回 ![](/assets/images/20180308WhatIsThedSYM/itunesConnect.webp) 在“所有构件版本(All Builds)”中选择某一个版本,点“下载`dSYM`(Download dSYM)”下载dSYM文件. > 注意:_一个`Archiver`与`dSYM`文件一一对应,搞错了容易翻译不出来来源码的调用_ ## 还原符号命令 2024年04月03日更新 ``` sh atos -o KWPlayer.app.dSYM/Contents/Resources/DWARF/KWPlayer -arch arm64 -l 0x102100000 0x10720df70 0x10720a5ac 0x10720e13c 0x107211aa0 0x107215574 0x107211aa0 0x10720770c 0x10720772c 0x10720f6ec 0x10720f9e8 0x107208df0 0x1072039b8 ``` 输出如下: ``` objc -[LOTLayerContainer display] (in KWPlayer) (LOTLayerContainer.m:385) -[LOTCompositionContainer displayWithFloatFrame:forceUpdate:] (in KWPlayer) (LOTCompositionContainer.m:107) -[LOTLayerContainer displayWithFloatFrame:forceUpdate:] (in KWPlayer) (LOTLayerContainer.m:411) -[LOTRenderGroup updateWithFrame:withModifierBlock:forceLocalUpdate:] (in KWPlayer) (LOTRenderGroup.m:142) -[LOTTrimPathNode updateWithFrame:withModifierBlock:forceLocalUpdate:] (in KWPlayer) (LOTTrimPathNode.m:62) -[LOTRenderGroup updateWithFrame:withModifierBlock:forceLocalUpdate:] (in KWPlayer) (LOTRenderGroup.m:142) -[LOTAnimatorNode updateWithFrame:withModifierBlock:forceLocalUpdate:] (in KWPlayer) (LOTAnimatorNode.m:51) -[LOTAnimatorNode updateWithFrame:withModifierBlock:forceLocalUpdate:] (in KWPlayer) (LOTAnimatorNode.m:54) -[LOTPathAnimator performLocalUpdate] (in KWPlayer) (LOTPathAnimator.m:36) -[LOTPathInterpolator pathForFrame:cacheLengths:] (in KWPlayer) (LOTPathInterpolator.m:0) -[LOTBezierPath LOT_addCurveToPoint:controlPoint1:controlPoint2:] (in KWPlayer) (LOTBezierPath.m:167) LOT_PointInCubicCurve (in KWPlayer) (CGGeometry+LOTAdditions.m:366) ``` `-l` 命令后可以接多个地址 可以用 `,`逗号和 空格隔开 原始文件如下 ``` sh Heaviest stack for the target process: 5 ??? (dyld + 24012) [0x1be4d6dcc] 5 ??? (KWPlayer + 108397220) [0x1088602a4] 5 ??? (UIKitCore + 2276456) [0x19dbc0c68] 5 ??? (UIKitCore + 2278956) [0x19dbc162c] 5 ??? (GraphicsServices + 13560) [0x1ded1e4f8] 5 ??? (CoreFoundation + 210040) [0x19b79d478] 3 ??? (CoreFoundation + 211096) [0x19b79d898] 3 ??? (CoreFoundation + 215900) [0x19b79eb5c] 3 ??? (CoreFoundation + 222120) [0x19b7a03a8] 3 ??? (CoreFoundation + 225580) [0x19b7a112c] 3 ??? (UIKitCore + 696208) [0x19da3ef90] 3 ??? (UIKitCore + 696020) [0x19da3eed4] 3 ??? (UIKitCore + 698340) [0x19da3f7e4] 3 ??? (UIKitCore + 699596) [0x19da3fccc] 3 ??? (QuartzCore + 416484) [0x19cddbae4] 3 ??? (QuartzCore + 417340) [0x19cddbe3c] 2 ??? (QuartzCore + 445280) [0x19cde2b60] 2 ??? (QuartzCore + 419644) [0x19cddc73c] 1 ??? (KWPlayer + 84991856) [0x10720df70] 1 ??? (KWPlayer + 84977068) [0x10720a5ac] 1 ??? (KWPlayer + 84992316) [0x10720e13c] 1 ??? (KWPlayer + 85007008) [0x107211aa0] 1 ??? (KWPlayer + 85022068) [0x107215574] 1 ??? (KWPlayer + 85007008) [0x107211aa0] 1 ??? (KWPlayer + 84965132) [0x10720770c] 1 ??? (KWPlayer + 84965164) [0x10720772c] 1 ??? (KWPlayer + 84997868) [0x10720f6ec] 1 ??? (KWPlayer + 84998632) [0x10720f9e8] 1 ??? (KWPlayer + 84970992) [0x107208df0] 1 ??? (KWPlayer + 84949432) [0x1072039b8] ``` 参考如下: [Bugly iOS 符号表配置](https://bugly.qq.com/docs/user-guide/symbol-configuration-ios/?v=1520478187041#dsym_1) [App 启动时间:过去,现在和未来](https://techblog.toutiao.com/2017/07/05/session413/) 全文完 URL: https://sunyazhou.com/2018/03/ComputerGraphicsRenderingProcess/index.html.md Published At: 2018-03-05 12:11:41 +0000 # 计算机图形渲染的流程 ![](/assets/images/20180305ComputerGraphicsRenderingProcess/IvanSutherland.webp) # 前言 今天在网上找到了一篇有价值的文章,来说明计算机中的图像渲染流程以及像素点计算和坐标点相关的知识. ## 计算机图形渲染的流程 计算机的绘图过程可以简单用流水线来说明,而产品(数据)就是经过流水线作业(渲染)到屏幕的图像。这条流水线可以简化为(本文的概念):绘图位置座标指定;着色指定;输出指定;下图简单解释了这一个流水线过程。计算机绘图需要一个输入绘图数据,这个数据可以是用户指定的,也可以是操作系统决定的,也可以是混合的。这些数据是分组的。 * 座标生成:当绘图数据送入座标生成系统后,流水线就会对其进行座标分派,图1右上的线框图抽象描述了这个过程。 * 着色指定:当座标系统生成出带座标的绘图数据后就需要送入着色器,着色器指定了这些线框的填充颜色或纹理。 * 渲染:着色器将绘图数据加上着色数据后就被送入渲染器,渲染器根据绘图数据描述,将像素填充到描述的线框组里并送入帧缓存,然后然后送入显示器,显示器获取到帧缓存的数据后再根据数据的描述来绘图到屏幕上。 ![](/assets/images/20180305ComputerGraphicsRenderingProcess/render1.webp) ## 像素与点(point)与点(dot) 像素与点(point),点(dot)这三个单位很容易令人混淆,原因在于它们在很多场合上是可以互换的。但是本文需要区分这两者的概念。 像素指的是一种数据结构,这个数据结构包含了RGB三个数据,分别对应的是红色,绿色,蓝色。我们说一张计算机生成的位图时,我们会说这图是多少像素x多少像素,例如800x600像素。值得注意的是,像素没有一个固定的尺寸单位,它只是一个抽象概念。 点(dot)指的是显示器屏幕的点或打印的点,是具体指代的事物。我们想说的DPI即dot per inch,每英寸多少个点。一般来说1个点对应一个像素,常见的打印尺寸是72DPI,即每英寸72点,也就是包含72个像素的数据。当像素被计算机输出成点投射都屏幕或纸面上时,它才具备了尺寸的概念,即点(dot)。 点(point)指的是座标点,是一个数据结构,包含了两个数据(或三个)X和Y(和Z)座标。绘图数据里是包含了这个座标数据的。对于没有使用HiDPI的操作系统来说,一个座标点对应一个像素。 ## 点(point)不一定等于像素 一般来说,点(dot)与像素是可以互换指代的,而且我们在Retina的概念被提出前一直这样使用它们。但是,现在这两个概念必须要区分出来。像素只是一个描述RGB的数据结构,它没有任何尺寸单位,它更不是一个矩形。当像素被输出到屏幕或纸张上时,我们应该用点来指代这种含有颜色,有尺寸的具体事物。 普通的显示屏幕或打印机,我们会说屏幕上的一个点(dot)是由一个像素(RGB数据)组成的,打印后的点是由一个像素经过色彩转换(CMYK数据)组成的。 对于打印机来说,一般的DPI是72。也就是指我们在显示器屏幕上看到720x720像素的位图,在打印出来后的面积是10x10英寸,但是屏幕上的位图面积并不会跟打印出来的面积一致。因为屏幕上的一个点与打印的点的尺寸不一样。 PPI指的是每英寸多少像素,与DPI有一定概念上的区别。PPI一般指的是屏幕的点密度,DPI指的是打印点的密度。PPI不是固定的,不同屏幕尺寸结合不同的分辨率会有不同的PPI,但是DPI则是相对固定在72。 HiDPI是苹果的一项绘图技术,结合这种技术,计算机座标系统上的一个点(point)不再对应一个像素,一般来说会是一个座标点对应四个像素,而一个像素对应屏幕的一个物理点(dot)。 由于像素是一组色彩数据,所以绘图数据在经过着色器后才包含了它。举个例子,绘图数据在送入着色器前是描述一个100x100的矩形,经过着色器指定色彩属性后会被送入一个HiDPI系统,这个系统将200x200个像素的数据添加到绘图数据里。在经过渲染器后,相当于将200x200个像素填充进100x100这个矩形线框。 ![](/assets/images/20180305ComputerGraphicsRenderingProcess/render2.webp) ## 帧缓存与显示器屏幕 帧缓存是储存计算机渲染后的图形数据的,这些数据包括座标,像素,分辨率等等。。简单来说就是描述图象的数据,当这些描述数据送入显示器后,显示器就知道怎么绘图了。 一般来说的屏幕分辨率指的是渲染器生产出来的像素数据排列,例如1280x800像素。值得注意的是这个屏幕分辨率与显示器屏幕的物理点排列没关系的。屏幕分辨率是可设置的,显示器的物理点排列是固定的。例如帧缓存里的分辨率是1280x800像素,但是显示器屏幕是1920x1200点排列的,那么显示器会怎么将帧缓存里的数据呈现到屏幕上呢?答案是通过自适应放缩,是经过显示器内部芯片来转换的。 13寸的RMBP在分辨率设置里是这样描述的,看起来像1280x800像素,看起来像1440x900像素。我们需要这样理解,1280x800像素是相对于旧款不带Retina的机器,也就是绘图数据送入着色器前的座标系统与渲染后的座标是1:1对应的参考值。实际上在经过渲染后,它的实际像素是2560x1600,也就是帧缓存里是数据是2560x1600像素。同样地看起来1440x900像素实际渲染后的像素是2880x1800。由于13寸的屏幕实际点排列是2560x1600,所以帧缓存2880x1800像素在输出到屏幕后会被自适应缩放掉。 ## DPI与Retina 操作系统标准的桌面打印DPI是72,但是随着HiDPI技术和高PPI屏幕出现后,这个标准也许会有一定的变化。我们在Retina的OS X下用Photoshop新建一个文件时默认的DPI指定在144上了,这是标准转变的一个信号。 在没有使用类似HiDPI技术的操作系统上,屏幕分辨率对应的打印DPI是72。使用HiDPI的Retina机器的打印DPI是144,用以保证在统一尺下具有更多的点密度。这点对于印前工作非常重要。 全文完 URL: https://sunyazhou.com/2018/03/LearningAVFoundationPlayingVideo/index.html.md Published At: 2018-03-04 16:56:06 +0000 # Learning AV Foundation(五)播放视频 ![](/assets/images/20180304LearningAVFoundationPlayingVideo/5kAirplay.webp) # 前言 很久没有写Learning AV Foundation相关的文章了,言归正传 本篇介绍一下简单的视频播放 了解视频播放之前我们来看戏`AVPlayer`需要的一些组件模型 ![AVPlayer组件模型](/assets/images/20180304LearningAVFoundationPlayingVideo/AVPlayer.webp) ## AVPlayer `AVPlayer`是一个用来播放基于基于时间的视听媒体的控制对象,支持播放: * 本地 媒体文件 * 异步下载 媒体文件 * HTTP Live Streaming协议的流媒体 文件 `AVPlayer` 是个 逻辑层组件 (应用可以分为如下几层) > UI层 > 业务逻辑层 > 持久层+网络层 如果播放`MP3`或`AAC`等音频文件, 是没有啥UI可视化的页面的。要是播放一个` QuickTime`的电影或一个`MPEG-4`视频, 就会搞得很不适应. 如果要播放视频等功能设计到UI的话,可以使用`AVPlayerLayer`类。 > 注意: _`AVPlayer`只管理一个单独资源的播放, 如果播放多个可以使用`AVPlayer`的子类`AVQueuePlayer`, 用它来管理一个资源队列, 当需要在一个序列中播放多个条目或者 为音频、视频资源设置播放循环时刻使用这个类_. ## AVPlayerLayer `AVPlayerLayer`构建于 `Core Animation`之上, 是`AV Foundation`中能找到的位数不多的UI组件. `Core Animation` 是`Mac`和`iOS`平台上负责图形渲染与动画的基础框架,主要用于这些平台的美化和动画流畅度的提升. `Core Animation`本身具有基于时间的属性,并且由于它基于`OpenGL`,所以具有很好的性能. `AVPlayerLayer`扩展了`Core Animation` 的`CALayer`类, 并通过框架显示视频内容到屏幕上. 我们知道Layer是不响应事件的. 创建`AVPlayerLayer`需要实例化一个`AVPlayer`的对象,`AVPlayerLayer`有一个`videoGravity`属性可以设置三种类似填充模式的东西,用来拉扯和缩放的视频. 下面列举了16:9的视频置于4:3矩形范围来说明不同的`gravity`. 如下图: __AVLayerVideoGravityResizeAspect__保持缩放比例 ![](/assets/images/20180304LearningAVFoundationPlayingVideo/AVLayerVideoGravityResizeAspect.webp) __AVLayerVideoGravityResizeAspectFill__填充 ![](/assets/images/20180304LearningAVFoundationPlayingVideo/AVLayerVideoGravityResizeAspectFill.webp) __AVLayerVideoGravityResize__拉伸 ![](/assets/images/20180304LearningAVFoundationPlayingVideo/AVLayerVideoGravityResize.webp) ## AVPlayerItem 我们需要使用`AVPlayer`播放`AVAsset`,前面我知道`AVAsset`元数据里面有`创建时间`、`元数据`和`时长`等信息.但是并没有媒体中特定位置的方法. __这是因为`AVAsset`模型只包含媒体资源的静态信息.这些不变的属性用来描述对象的静态信息.这就意味着仅使用`AVAsset`对象是不能实现播放功能的.如果播放我们需要使用`AVPlayerItem`__ __`AVPlayerItem`可以理解成是一个动态的`AVAsset`模型,__ `AVPlayerItem`有`seekToTime:`方法和`presentationSize:`,`AVPlayerItem`由一个或多个媒体曲目组成. `AVPlayerItem`里面有``AVPlayerItemTrack`轨道属性. ## 播放示例 ``` objc - (void)viewDidLoad { self.localURL = [[NSBundle mainBundle] URLForResource:@"hubblecast" withExtension:@"m4v"]; AVAsset *asset = [AVAsset assetWithURL:self.localURL]; AVPlayerItem *item = [AVPlayerItem playerItemWithAsset:asset]; AVPlayer *player = [AVPlayer playerWithPlayerItem:item]; AVPlayerLayer *layer = [AVPlayerLayer playerLayerWithPlayer:player]; [self.view.layer addSublayer:layer]; } ``` 这个`AVPlayerItem`并没有任何代理告知我们是否已经开始播放,所以一般的搞法都是使用`KVO`去监听它的一个属性,`AVPlayerItemStatus` ``` objc typedef NS_ENUM(NSInteger, AVPlayerItemStatus) { AVPlayerItemStatusUnknown, AVPlayerItemStatusReadyToPlay, AVPlayerItemStatusFailed }; ``` 当它的`status`变成`AVPlayerItemStatusReadyToPlay`就说明已载入完成准备播放. ## CMTime 使用`CMTime`来处理各种音视频相关的时间操作,他是`CoreMedia`framework中的结构体.专门用于处理精确的时间,我们以前用的`NSTimeInterval`是存在计算不精确的问题(苹果官方说的). ``` objc typedef struct { CMTimeValue value; //分子 CMTimeScale timescale; //分母 CMTimeFlags flags; //标记是否失效 eg. kCMTimeFlags_Valid, kCMTimeFlags_PositiveInfinity CMTimeEpoch epoch; } CMTime; ``` 这个结构体最关键的即使`value`(64位整形)和`timescale`(32位整形). 它表达时间的方式以分数表示比如: `0.5`秒 ``` objc CMTime halfSecond = CMTimeMake(1, 2); //0.5秒 CMTime fiveSecond = CMTimeMake(5, 1); //5秒 CMTime oneSample = CMTimeMake(1, 44100); //一个抽样的样本 CMTime zeroTime = kCMTimeZero; ``` ## 创建自己的播放器 首先需要封装一个`player`, ``` objc #import #import "TransportProtocol.h" @class AVPlayer; @interface PlayerView : UIView @property (nonatomic, readonly) id transport; - (id)initWithPlayer:(AVPlayer *)player; @end ``` .m文件实现 ``` objc #import "PlayerView.h" #import #import "THOverlayView.h" @interface PlayerView () @property (nonatomic, strong) THOverlayView *overlayView; @end @implementation PlayerView + (Class)layerClass{ return [AVPlayerLayer class]; } - (id)initWithPlayer:(AVPlayer *)player{ self = [super initWithFrame:CGRectZero]; if (self) { self.backgroundColor = [UIColor blackColor]; self.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; [(AVPlayerLayer *)[self layer] setPlayer:player]; [[NSBundle mainBundle] loadNibNamed:@"THOverlayView" owner:self options:nil]; [self addSubview:self.overlayView]; } return self; } - (void)layoutSubviews{ [super layoutSubviews]; self.overlayView.frame = self.bounds; } - (id )transport{ return self.overlayView; } @end ``` transport 是播放器的视图点击视图代理等集成了 在一起 ``` objc @protocol TransportDelegate - (void)play; - (void)pause; - (void)stop; - (void)scrubbingDidStart; - (void)scrubbedToTime:(NSTimeInterval)time; - (void)scrubbingDidEnd; - (void)jumpedToTime:(NSTimeInterval)time; @optional - (void)subtitleSelected:(NSString *)subtitle; @end @protocol TransportProtocol @property (weak, nonatomic) id delegate; - (void)setTitle:(NSString *)title; - (void)setCurrentTime:(NSTimeInterval)time duration:(NSTimeInterval)duration; - (void)setScrubbingTime:(NSTimeInterval)time; - (void)playbackComplete; - (void)setSubtitles:(NSArray *)subtitles; @end ``` THOverlayView文件是顶层视图点击播放等等控件. ``` objc #import #import @interface PlayerController : NSObject @property (nonatomic, strong, readonly) UIView *view; - (id)initWithURL:(NSURL *)assetURL; @end ``` 播放器的实现文件如下 ``` objc #import "PlayerController.h" #import #import "TransportProtocol.h" #import "PlayerView.h" #import "AVAsset+Additions.h" #import "UIAlertView+Additions.h" #import "THThumbnail.h" // AVPlayerItem's status property #define STATUS_KEYPATH @"status" // Refresh interval for timed observations of AVPlayer #define REFRESH_INTERVAL 0.5f // Define this constant for the key-value observation context. static const NSString *PlayerItemStatusContext; @interface PlayerController () @property (nonatomic, strong) AVAsset *asset; @property (nonatomic, strong) AVPlayerItem *playerItem; @property (nonatomic, strong) AVPlayer *player; @property (nonatomic, strong) PlayerView *playerView; @property (nonatomic, weak) id transport; @property (nonatomic, strong) id timeObserver; @property (nonatomic, strong) id itemEndObserver; @property (nonatomic, assign) float lastPlaybackRate; @property (strong, nonatomic) AVAssetImageGenerator *imageGenerator; @end @implementation PlayerController #pragma mark - Setup - (id)initWithURL:(NSURL *)assetURL { self = [super init]; if (self) { _asset = [AVAsset assetWithURL:assetURL]; // 1 [self prepareToPlay]; } return self; } - (void)prepareToPlay { NSArray *keys = @[ @"tracks", @"duration", @"commonMetadata", @"availableMediaCharacteristicsWithMediaSelectionOptions" ]; self.playerItem = [AVPlayerItem playerItemWithAsset:self.asset // 2 automaticallyLoadedAssetKeys:keys]; [self.playerItem addObserver:self // 3 forKeyPath:STATUS_KEYPATH options:0 context:&PlayerItemStatusContext]; self.player = [AVPlayer playerWithPlayerItem:self.playerItem]; // 4 self.playerView = [[PlayerView alloc] initWithPlayer:self.player]; // 5 self.transport = self.playerView.transport; self.transport.delegate = self; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if (context == &PlayerItemStatusContext) { dispatch_async(dispatch_get_main_queue(), ^{ // 1 [self.playerItem removeObserver:self forKeyPath:STATUS_KEYPATH]; if (self.playerItem.status == AVPlayerItemStatusReadyToPlay) { // Set up time observers. // 2 [self addPlayerItemTimeObserver]; [self addItemEndObserverForPlayerItem]; CMTime duration = self.playerItem.duration; // Synchronize the time display // 3 [self.transport setCurrentTime:CMTimeGetSeconds(kCMTimeZero) duration:CMTimeGetSeconds(duration)]; // Set the video title. [self.transport setTitle:self.asset.title]; // 4 [self.player play]; // 5 [self loadMediaOptions]; [self generateThumbnails]; } else { [UIAlertView showAlertWithTitle:@"Error" message:@"Failed to load video"]; } }); } } - (void)loadMediaOptions { NSString *mc = AVMediaCharacteristicLegible; // 1 AVMediaSelectionGroup *group = [self.asset mediaSelectionGroupForMediaCharacteristic:mc]; // 2 if (group) { NSMutableArray *subtitles = [NSMutableArray array]; // 3 for (AVMediaSelectionOption *option in group.options) { [subtitles addObject:option.displayName]; } [self.transport setSubtitles:subtitles]; // 4 } else { [self.transport setSubtitles:nil]; } } - (void)subtitleSelected:(NSString *)subtitle { NSString *mc = AVMediaCharacteristicLegible; AVMediaSelectionGroup *group = [self.asset mediaSelectionGroupForMediaCharacteristic:mc]; // 1 BOOL selected = NO; for (AVMediaSelectionOption *option in group.options) { if ([option.displayName isEqualToString:subtitle]) { [self.playerItem selectMediaOption:option // 2 inMediaSelectionGroup:group]; selected = YES; } } if (!selected) { [self.playerItem selectMediaOption:nil // 3 inMediaSelectionGroup:group]; } } #pragma mark - Time Observers - (void)addPlayerItemTimeObserver { // Create 0.5 second refresh interval - REFRESH_INTERVAL == 0.5 CMTime interval = CMTimeMakeWithSeconds(REFRESH_INTERVAL, NSEC_PER_SEC); // 1 // Main dispatch queue dispatch_queue_t queue = dispatch_get_main_queue(); // 2 // Create callback block for time observer __weak PlayerController *weakSelf = self; // 3 void (^callback)(CMTime time) = ^(CMTime time) { NSTimeInterval currentTime = CMTimeGetSeconds(time); NSTimeInterval duration = CMTimeGetSeconds(weakSelf.playerItem.duration); [weakSelf.transport setCurrentTime:currentTime duration:duration]; // 4 }; // Add observer and store pointer for future use self.timeObserver = // 5 [self.player addPeriodicTimeObserverForInterval:interval queue:queue usingBlock:callback]; } - (void)addItemEndObserverForPlayerItem { NSString *name = AVPlayerItemDidPlayToEndTimeNotification; NSOperationQueue *queue = [NSOperationQueue mainQueue]; __weak PlayerController *weakSelf = self; // 1 void (^callback)(NSNotification *note) = ^(NSNotification *notification) { [weakSelf.player seekToTime:kCMTimeZero // 2 completionHandler:^(BOOL finished) { [weakSelf.transport playbackComplete]; // 3 }]; }; self.itemEndObserver = // 4 [[NSNotificationCenter defaultCenter] addObserverForName:name object:self.playerItem queue:queue usingBlock:callback]; } #pragma mark - THTransportDelegate Methods - (void)play { [self.player play]; } - (void)pause { self.lastPlaybackRate = self.player.rate; [self.player pause]; } - (void)stop { [self.player setRate:0.0f]; [self.transport playbackComplete]; } - (void)jumpedToTime:(NSTimeInterval)time { [self.player seekToTime:CMTimeMakeWithSeconds(time, NSEC_PER_SEC)]; } - (void)scrubbingDidStart { // 1 self.lastPlaybackRate = self.player.rate; [self.player pause]; [self.player removeTimeObserver:self.timeObserver]; self.timeObserver = nil; } - (void)scrubbedToTime:(NSTimeInterval)time { // 2 [self.playerItem cancelPendingSeeks]; [self.player seekToTime:CMTimeMakeWithSeconds(time, NSEC_PER_SEC) toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero]; } - (void)scrubbingDidEnd { // 3 [self addPlayerItemTimeObserver]; if (self.lastPlaybackRate > 0.0f) { [self.player play]; } } #pragma mark - Thumbnail Generation - (void)generateThumbnails { self.imageGenerator = // 1 [AVAssetImageGenerator assetImageGeneratorWithAsset:self.asset]; // Generate the @2x equivalent self.imageGenerator.maximumSize = CGSizeMake(200.0f, 0.0f); // 2 CMTime duration = self.asset.duration; NSMutableArray *times = [NSMutableArray array]; // 3 CMTimeValue increment = duration.value / 20; CMTimeValue currentValue = 2.0 * duration.timescale; while (currentValue <= duration.value) { CMTime time = CMTimeMake(currentValue, duration.timescale); [times addObject:[NSValue valueWithCMTime:time]]; currentValue += increment; } __block NSUInteger imageCount = times.count; // 4 __block NSMutableArray *images = [NSMutableArray array]; AVAssetImageGeneratorCompletionHandler handler; // 5 handler = ^(CMTime requestedTime, CGImageRef imageRef, CMTime actualTime, AVAssetImageGeneratorResult result, NSError *error) { if (result == AVAssetImageGeneratorSucceeded) { // 6 UIImage *image = [UIImage imageWithCGImage:imageRef]; id thumbnail = [THThumbnail thumbnailWithImage:image time:actualTime]; [images addObject:thumbnail]; } else { NSLog(@"Error: %@", [error localizedDescription]); } // If the decremented image count is at 0, we're all done. if (--imageCount == 0) { // 7 dispatch_async(dispatch_get_main_queue(), ^{ NSString *name = THThumbnailsGeneratedNotification; NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; [nc postNotificationName:name object:images]; }); } }; [self.imageGenerator generateCGImagesAsynchronouslyForTimes:times // 8 completionHandler:handler]; } #pragma mark - Housekeeping - (UIView *)view { return self.playerView; } - (void)dealloc { if (self.itemEndObserver) { // 5 NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; [nc removeObserver:self.itemEndObserver name:AVPlayerItemDidPlayToEndTimeNotification object:self.player.currentItem]; self.itemEndObserver = nil; } } @end ``` 这里说一下如何监听时间从而得知播放时间回调 ### 监听时间 当播放器播放的时候我们无法得知播放到播放器的哪个位置,为了解决这个问题`AVPlayerItem`添加了两个监听播放的方法以及具体的用法`API`. #### 定期监听 ``` objc - (id)addPeriodicTimeObserverForInterval:(CMTime)interval queue:(nullable dispatch_queue_t)queue usingBlock:(void (^)(CMTime time))block; ``` 这里主要是为了随着时间的变化移动播放器seek位置更新时间显示,通过`AVPlayer`的`addPeriodicTimeObserverForInterval:queue:usingBlock:` 来监听播放时间的变化 * `interv`_监听周期的间隔`CMTime`_ * `queue` _通知发送的顺序调度队列,一般我们都放在主线程回掉.(注意这里不能放在并行队列中)_ * `block` _指定周期的时间回调._ 下面是示例代码 ``` objc - (void)addPlayerItemTimeObserver { // Create 0.5 second refresh interval - REFRESH_INTERVAL == 0.5 CMTime interval = CMTimeMakeWithSeconds(REFRESH_INTERVAL, NSEC_PER_SEC); // 1 // Main dispatch queue dispatch_queue_t queue = dispatch_get_main_queue(); // 2 // Create callback block for time observer __weak PlayerController *weakSelf = self; // 3 void (^callback)(CMTime time) = ^(CMTime time) { NSTimeInterval currentTime = CMTimeGetSeconds(time); NSTimeInterval duration = CMTimeGetSeconds(weakSelf.playerItem.duration); [weakSelf.transport setCurrentTime:currentTime duration:duration]; // 4 }; // Add observer and store pointer for future use self.timeObserver = // 5 [self.player addPeriodicTimeObserverForInterval:interval queue:queue usingBlock:callback]; } ``` #### 边界监听 什么叫边界监听呢?就是播放器播放到某个时间的触发的 时间位置. ``` objc - (id)addBoundaryTimeObserverForTimes:(NSArray *)times queue:(nullable dispatch_queue_t)queue usingBlock:(void (^)(void))block; ``` * `times` _CMTime值组成一个`NSArray`,这里面定义的一个时间点的数组.eg: 25% 50% 75%等时间点._ * `queue` _通知发送的顺序调度队列,一般我们都放在主线程回掉.(注意这里不能放在并行队列中)_ * `block` _指定周期的时间回调._ ### 显示字幕 `AVPlayerLayer`里有两个类来处理字幕 * AVMediaSelectionGroup * AVMediaSelectionOption `AVMediaSelectionOption` 用于表示`AVAsset`备用媒体显示.在前几篇中我讲过一个媒体元数据中有`音频轨`、`视频轨`、`字幕轨`,`备用相机角度`等. 我们如果想找出字幕的话需要用到`AVAsset`的`availableMediaCharacteristicsWithMediaSelectionOptions`属性. ``` objc @property (nonatomic, readonly) NSArray *availableMediaCharacteristicsWithMediaSelectionOptions NS_AVAILABLE(10_8, 5_0); ``` 这个属性会返回一个数组的`字符串`,这些`字符串`用于表示保存在资源中可用选项的媒体特征,其实数组中包含的字符串的值为如下: * AVMediaCharacteristicVisual 视频 * AVMediaCharacteristicAudible 音频 * AVMediaCharacteristicLegible 字幕或隐藏式字幕 ``` objc - (nullable AVMediaSelectionGroup *)mediaSelectionGroupForMediaCharacteristic:(AVMediaCharacteristic)mediaCharacteristic NS_AVAILABLE(10_8, 5_0); ``` 请求可用媒体特性数据后,调用`AVAsset`的`mediaSelectionGroupForMediaCharacteristic:`方法.为其传递要检索的选项的特定媒体特征.这个方法返回一个`AVMediaSelectionGroup`,它作为一个或多个互斥的`AVMediaSelectionGroup`实例的容器. ``` objc - (void)loadMediaOptions { NSString *mc = AVMediaCharacteristicLegible; // 1 AVMediaSelectionGroup *group = [self.asset mediaSelectionGroupForMediaCharacteristic:mc]; // 2 if (group) { NSMutableArray *subtitles = [NSMutableArray array]; // 3 for (AVMediaSelectionOption *option in group.options) { [subtitles addObject:option.displayName]; } [self.transport setSubtitles:subtitles]; // 4 } else { [self.transport setSubtitles:nil]; } } ``` ## AirPlay AirPlay相信大部分iOS开发者都耳熟能详,这个东西是用于无线方式将流媒体音频/视频内容在`Apple TV`上播放.或者将纯音频内容在多种第三方音频系统中播放(如汽车中内置的CarPlay).如果大家有`Apple TV`或其它音频系统中的一个,就会觉得这个功能实在太实用了.其实把这个功能整合到我们的APP中十分容易. `AVPlayer`有一个属性是`allowsExternalPlayback`,允许启用或者禁用`AirPlay`播放功能.该属性默认是`YES`,即在不做任何额外编码的情况下,播放器应用程序也会自动支持`AirPlay`功能. ``` objc @property (nonatomic) BOOL allowsExternalPlayback NS_AVAILABLE(10_11, 6_0); ``` 不过从iOS11之后才有专门针对AirPlay的framework功能API,在以前我们使用`Media Player`中的`MPVolumeView`来实现. 示例代码: ``` MPVolumeView *volumeView = [[MPVolumeView alloc] init]; volumeView.showsVolumeSlider = NO; [volumeView sizeToFit]; [transportView addSubview:volumeView]; ``` 当AirPlay可用时,而且WIFI 网络启用时才会显示线路选择按钮.这两个条件只有一个不满足, MPVolumeView 就会自动隐藏按钮. ## 总结 本章讲述了 如何使用AVPlayer以及AVPlayerItem 的一些属性 监听播放进度回调,取 字幕等等. [详细demo请参考](https://github.com/sunyazhou13/Learning-AV-Foundation-Demos) URL: https://sunyazhou.com/2018/02/WordEmbeding/index.html.md Published At: 2018-02-04 12:24:30 +0000 # NLP分词WordEmbeding ![](/assets/images/20180204WordEmbeding/wordembeding.webp) # 前言 学习过程中记录一下python代码 ``` python #!/usr/bin/env python # coding:utf8 import sys reload(sys) sys.setdefaultencoding('utf8') # 加载包 from gensim.models import Word2Vec from gensim.models.word2vec import LineSentence # 训练模型 # sentences = LineSentence('wiki.zh.word.text') # size:词向量的维度 # window:上下文环境的窗口大小 # min_count:忽略出现次数低于min_count的词 # model = Word2Vec(sentences, size=128, window=5, min_count=5, workers=4) # 保存模型 # model.save('word_embedding_128') # 如果已经保存过模型,则直接加载即可 # 前面训练并保存的代码都可以省略 model = Word2Vec.load("word_embedding_128") # 使用模型 # 返回和一个词语最相关的多个词语以及对应的相关度 items = model.most_similar(u'中国') for item in items: # 词的内容,词的相关度 print item[0], item[1] # 返回两个词语之间的相关度 model.similarity(u'男人', u'女人') ``` 参考分词如下: [哈工大分词](https://www.ltp-cloud.com/demo/) [jieba分词](https://github.com/fxsjy/jieba) [stanford分词](https://nlp.stanford.edu/software/segmenter.shtml) URL: https://sunyazhou.com/2018/01/PythonMySQL/index.html.md Published At: 2018-01-13 22:27:18 +0000 # 使用Python操作MySQL数据库 ![](/assets/images/20180113PythonMySQL/MysqlPython.webp) # 前言 为了实现`不斷學習 與時俱進`周末把大部分时间放在了学习`Python`上. 在最近的学习中有一些有价值的部分都摘录整理出来放到博客上,以免后续用到的时候忘记时回来翻翻博客. 我是在`study.163.com`的这个[《全栈数据工程师养成攻略》](http://study.163.com/course/courseMain.htm?courseId=1003520028)课程中学习的,推荐大家学习一下. ## 本篇主要内容 主要分为三个大部分 1. 搭建`Web`环境 2. 数据库MySQL的使用方法 3. 使用Python操作MySQL ### 搭建`Web`环境 * Web环境: Apache、Nginx... * Web服务启动中相关配置. #### Web环境: Apache、Nginx... 两个平台的相关的下载 [MAMP](https://www.mamp.info/en/): Mac, Apache, MySQL, PHP > Mac, Apache, MySQL, PHP 缩写`MAMP` [WAMP](https://www.mamp.info/en/): Windows, Apache, MySQL, PHP > Windows, Apache, MySQL, PHP 缩写`WAMP` 当然还有linux版本这里就不做多介绍了 总之需要安装这个软件进行环境配置的搭建. 我这里用`MAMP`举例说明一下 ![](/assets/images/20180113PythonMySQL/mamp1.webp) 打开之后 ![](/assets/images/20180113PythonMySQL/mamp2.webp) #### Web服务启动中相关配置 开启`Apatch Server`和`MySQL Server`服务(右上角). 然后点击`Perferences`,进行本地端口配置. ![](/assets/images/20180113PythonMySQL/mamp3.webp) 这里有两种默认配置(红色框选部分) 如果把服务开启的话那么打开浏览器输入:`localhost:8888`就可以看到相关的效果 > localhost == 127.0.0.1 `8888`是服务的端口 下面这张图可以选择文件根目录 ![](/assets/images/20180113PythonMySQL/mamp4.webp) 什么意思呢? 就是你把网页的相关文件放到 这个文件夹的话 就会在浏览器上直接浏览. ![](/assets/images/20180113PythonMySQL/mamp2.webp) 这张图中间的`Open Start Page`. ![](/assets/images/20180113PythonMySQL/sql1.webp) 进入到数据库配置相关 配置数据库名称 ![](/assets/images/20180113PythonMySQL/sql2.webp) 输入表名 ![](/assets/images/20180113PythonMySQL/sql3.webp) 配置数据库表 ![](/assets/images/20180113PythonMySQL/sql4.webp) 配置完右侧完成 ### 数据库MySQL的使用方法 * 基本概念 * 终端配置Python MySQL * Navicat 数据的导出导入 * 个人的习惯搞法 #### 基本概念 `CURD`操作: * `C` Create * `R` Read * `U` Update * `D` Delete 这就是数据库相关知识中 `增``删``改``查` #### 终端配置Python MySQL 在终端中使用如下指令安装MySQL环境 ``` sh pip install MySQL-python ``` 我安装的时候出错了 ![](/assets/images/20180113PythonMySQL/PipInstallMysqlPython.webp) 最后执行 ``` sh brew install mysql-python ``` 然后再去执行`pip install MySQL-python` 如何测试是否成功 在shell中输入`python` ![](/assets/images/20180113PythonMySQL/pythonshell1.webp) 执行 ``` python import MySQLdb ``` 如果没有错误就是OK的. #### Navicat 数据的导出导入 这个数据库可视化操作软件大家自行下载吧 ![](/assets/images/20180113PythonMySQL/navicat1.webp) 打开之后点击左上角点击新建connect 选择MySQL ![](/assets/images/20180113PythonMySQL/navicat2.webp) 接着配置数据库的信息 ![](/assets/images/20180113PythonMySQL/navicat3.webp) 这里的名称就是__数据库名称__ `host`地方本地,如果是远程的话,填写`ip`或者`url` `port`前面我们设置了`8889` 账号和密码输入`root`(前面图里面已经看到了账号密码都是一样的) 下面就是连接数据库 ![](/assets/images/20180113PythonMySQL/navicat4.webp) 下面这张图就是 ![](/assets/images/20180113PythonMySQL/navicat5.webp) __数据库导出和导入,当然也可以导出导入数据表.__ #### 个人的习惯搞法 * 使用`phpmyadmin`新建数据库和数据表 * 使用`python`插入、读取、更新、修改数据 * 使用`Navicat`导出数据库 * 使用`phpmyadmin`导入数据库 最后deloy(部署)到线上,这样就可以避免各种错误操作数据库的问题 ### 使用Python操作MySQL 这个没啥就是coding部分,使用之前把点击[这里下载](/assets/images/20180113PythonMySQL/DoubanMovieClean.txt)这个文本文件 我们用`sublime text`新建一个`text.py`文件 ``` python #!/usr/bin/env python # coding:utf8 import sys reload(sys) sys.setdefaultencoding("utf8") import MySQLdb import MySQLdb.cursors ``` ![](/assets/images/20180113PythonMySQL/Pythoncode1.webp) > 注意:_test.py最好和douban_movie_clean.txt保持在同一个目录这样就不用写路径了_ 接着创建数据库连接 ``` python db = MySQLdb.connect(host='127.0.0.1', user='root', passwd='root', db='douban', port=8889, charset='utf8', cursorclass=MySQLdb.cursors.DictCursor) //1 db.autocommit(True) //2 cursor = db.cursor() //3 fr = open('douban_movie_clean.txt','r') //4 fr.close() //4 cursor.close() //3 db.close() //1 ``` > 注意 `db`记得用完关闭,`cursor`也要记得关闭, `fr`是文件的读写 和数据库没啥关系也需要记得用完关闭 下面解释一下什么意思 1. `db`创建输入库实例,输入参数 `host`(这里用的是127.0.0.1也可以换成localhost)、`passwd`、`db`、`port`、`charset`、`cursorclass`. 2. 自动改完提交完成更新数据库 3. 通过`db`实例拿到一个连接`cursor` 每次都通过`cursor.execute()`执行增删改查操作sql语句 4. 读取本地的文本文件 大概就是这个意思 #### 读取数据 ``` python # Create # 读取数据 fr = open('douban_movie_clean.txt', 'r') count = 0 for line in fr: count += 1 # count表示当前处理到第几行了 print count # 跳过表头 if count == 1: continue # strip()函数可以去掉字符串两端的空白符 # split()函数按照给定的分割符将字符串分割为列表 line = line.strip().split('^') # 插入数据,注意对齐字段 # execute()函数第一个参数为要执行的SQL命令 # 这里用字符串格式化的方法生成一个模板 # %s表示一个占位符 # 第二个参数为需要格式化的参数,传入到模板中 cursor.execute("insert into movie(title, url, rate, length, description) values(%s, %s, %s, %s, %s)", [line[1], line[2], line[4], line[-3], line[-1]]) # 关闭读文件 fr.close() ``` 通过我们拿到的`cursor`连接实例来执行`cursor.execute()`函数进行`sql`的插入操作. ![](/assets/images/20180113PythonMySQL/Pythoncode2.webp) 来看下结果 ![](/assets/images/20180113PythonMySQL/sqlresult.webp) #### 更新数据 更新数据 比如我想把id=1的记录更新一下`title`字段和`length`长度 ``` python # Update cursor.execute("update movie set title=%s, length=%s where id=1", ['孙亚洲', 999]) ``` #### 读取数据 ``` python # Read cursor.execute("select title, length from movie where id=1") movies = cursor.fetchone() ``` #### 删除数据 ``` python # Delete cursor.execute("delete from movie where id=%s",[2]) ``` --- 下面看下完成的代码 ``` python #!/usr/bin/env python # coding:utf8 import sys reload(sys) sys.setdefaultencoding("utf8") import MySQLdb import MySQLdb.cursors db = MySQLdb.connect(host='127.0.0.1', user='root', passwd='root', db='douban', port=8889, charset='utf8', cursorclass=MySQLdb.cursors.DictCursor) db.autocommit(True) cursor = db.cursor() fr = open('douban_movie_clean.txt','r') # Create count = 0 for line in fr: count += 1 print count if count == 1: continue line = line.strip().split('^') cursor.execute("insert into movie(title, url, rate, length, description) values(%s, %s, %s, %s, %s)", [line[1], line[2], line[4], line[-3], line[-1]]) fr.close() # Update cursor.execute("update movie set title=%s, length=%s where id=1", ['孙亚洲', 999]) # Read cursor.execute("select title, length from movie where id=1") movies = cursor.fetchone() print len(movies) # print movies[0] # Delete cursor.execute("delete from movie where id=%s",[2]) cursor.close() db.close() ``` ## 总结 通过学习`python`操作数据库很有收获,想起了大学李月辉老师教我怎么用java连接数据库. 在工作中我们可能会遇到一大堆数据如何插入到数据等问题,通过学习了本章内容可以很容易的处理批量数据. 关于更多的SQL语句 参考[SQL 教程](http://www.runoob.com/sql/sql-tutorial.html) 全文完 URL: https://sunyazhou.com/2018/01/AVAudioSessionCategory/index.html.md Published At: 2018-01-12 10:32:18 +0000 # AVAudioSession-Category各种姿势 ![AVAudioSession](/assets/images/20180112AVAudioSessionCategory/ASPGIntro.webp) # 前言 2018新年第一篇, 梳理`AVAudioSession`的`Category`,解决音频开发中的各种播放被打断或者首次启动时无声音的问题 ## 开篇 由于`iOS`系统的特殊性,所有`App`共用一个`AVAudioSession`所以这个会话是个单例对象.(`macOS`是支持同时播放多路音频文件) 当遇到`插拔耳机`,`接电话`,`调起 siri`,等等,就出现音频会话被系统时间打断等行为表现: * 是进行录音还是播放? * 当系统静音键按下时该如何表现? * 是从扬声器还是从听筒里面播放声音? * 插拔耳机后如何表现? * 来电话/闹钟响了后如何表现? * 其他音频App启动后如何表现? ### Session默认行为 * 可以进行播放,但是不能进行录制。 * 当用户将手机上的静音拨片拨到“静音”状态时,此时如果正在播放音频,那么播放内容会被静音。 * 当用户按了手机的锁屏键或者手机自动锁屏了,此时如果正在播放音频,那么播放会静音并被暂停。 * 如果你的App在开始播放的时候,此时QQ音乐等其他App正在播放,那么其他播放器会被静音并暂停。 `AVAudioSession`默认的行为相当于设置了`Category`为`AVAudioSessionCategorySoloAmbient` 示例代码: ``` objc - (void)configSession{ [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategorySoloAmbient error:&error]; if (error) { NSLog(@"%@",error); } } ``` ### AVAudioSession 上边说了 这个类是个单例 ``` objc + (AVAudioSession *)sharedInstance; ``` 通过上边方法获得单例 虽然系统会在App启动的时候,激活这个唯一的`AVAudioSession`,但是最好还是在自己用的时候再次进行激活: ``` - (BOOL)setActive:(BOOL)active error:(NSError * _Nullable *)outError; ``` 通过设置`active`为`YES`激活`Session`,设置为`NO`解除`Session`的激活状态。`BOOL`返回值表示是否成功,如果失败的话可以通过`NSError`的`error.localizedDescription`查看出错原因。 > 因为`AVAudioSession`会影响其他`App`的表现,当自己`App`的`Session`被激活,其他`App`的就会被解除激活. __如何要让自己的`Session`解除激活后恢复其他`App Session`的激活状态呢?__ 此时可以使用: ``` - (BOOL)setActive:(BOOL)active withOptions:(AVAudioSessionSetActiveOptions)options error:(NSError * _Nullable *)outError; ``` __这里的`options`传入`AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation`即可.__ 当然,也可以通过`otherAudioPlaying`变量来提前判断当前是否有其他App在播放音频。 ``` objc NSLog(@"Current Category:%@", [AVAudioSession sharedInstance].category); //返回当前 category ``` ``` sh Current Category:AVAudioSessionCategorySoloAmbien ``` ### 七大Category 下面介绍一下`AVAudioSession`非常重要的七种`Category`. ``` objc #pragma mark -- Values for the category property -- AVF_EXPORT NSString *const AVAudioSessionCategoryAmbient; AVF_EXPORT NSString *const AVAudioSessionCategorySoloAmbient; AVF_EXPORT NSString *const AVAudioSessionCategoryPlayback; AVF_EXPORT NSString *const AVAudioSessionCategoryRecord; AVF_EXPORT NSString *const AVAudioSessionCategoryPlayAndRecord; AVF_EXPORT NSString *const AVAudioSessionCategoryAudioProcessing NS_DEPRECATED_IOS(3_0, 10_0) __TVOS_PROHIBITED __WATCHOS_PROHIBITED; AVF_EXPORT NSString *const AVAudioSessionCategoryMultiRoute NS_AVAILABLE_IOS(6_0); ``` `AVAudioSession`将使用音频的场景分成七大类,通过设置`Session`为不同的类别,可以控制: * 当App激活Session的时候,是否会打断其他不支持混音的App声音 * 当用户触发手机上的“静音”键时或者锁屏时,是否相应静音 * 当前状态是否支持录音 * 当前状态是否支持播放 * 每个App启动时都会设置成上面说的默认状态,即其他App会被中断同时相应“静音”键的播放模式。通过下表可以细分每个类别的支持情况: | 类别 | 当按“静音”或者锁屏是是否静音 | 是否引起不支持混音的App中断 | 是否支持录音和播放 | | :------: | :------: | :------: | :------: | | AVAudioSessionCategoryAmbient | 是 | 否 | 只支持播放 | | AVAudioSessionCategoryAudioProcessing | N/A | 都不支持 | N/A | | AVAudioSessionCategoryMultiRoute | 否 | 是 | 既可以录音也可以播放 | | AVAudioSessionCategoryPlayAndRecord | 否 | 默认不引起 | 既可以录音也可以播放 | | AVAudioSessionCategoryPlayback | 否 | 默认引起 | 只用于播放 | | AVAudioSessionCategoryRecord | 否 | 是 | 只用于录音 | | AVAudioSessionCategorySoloAmbient | 是 | 是 | 只用于播放 | 可以看到,其实默认的就是`AVAudioSessionCategorySoloAmbient`类别. 从表中我们可以总结如下: * _`AVAudioSessionCategoryAmbient`:只用于播放音乐时,并且可以和QQ音乐同时播放,比如玩游戏的时候还想听QQ音乐的歌,那么把游戏播放背景音就设置成这种类别。同时,当用户锁屏或者静音时也会随着静音,这种类别基本使用所有App的背景场景。_ * _`AVAudioSessionCategoryAudioProcessing`:主要用于音频格式处理,一般可以配合AudioUnit进行使用._ * _`AVAudioSessionCategoryMultiRoute`:想象一个DJ用的App,手机连着HDMI到扬声器播放当前的音乐,然后耳机里面播放下一曲,这种常人不理解的场景,这个类别可以支持多个设备输入输出._ * _`AVAudioSessionCategoryPlayAndRecord`: 如果既想播放又想录制该用什么模式呢?比如VoIP,打电话这种场景,PlayAndRecord就是专门为这样的场景设计的._ * _`AVAudioSessionCategoryPlayback`:如果锁屏了还想听声音怎么办?用这个类别,比如App本身就是播放器,同时当App播放时,其他类似QQ音乐就不能播放了。所以这种类别一般用于播放器类App._ * _`AVAudioSessionCategoryRecord`:有了播放器,肯定要录音机,比如微信语音的录制,就要用到这个类别,既然要安静的录音,肯定不希望有QQ音乐了,所以其他播放声音会中断。想想微信语音的场景,就知道什么时候用他了._ * _`AVAudioSessionCategorySoloAmbient`:也是只用于播放,但是和`AVAudioSessionCategoryAmbient`不同的是,用了它就别想听QQ音乐了,比如不希望QQ音乐干扰的App,类似节奏大师。同样当用户锁屏或者静音时也会随着静音,锁屏了就玩不了节奏大师了._ 了解了这七大类别,我们就可以根据自己的需要进行对应类别的设置了: ``` objc - (BOOL)setCategory:(NSString *)category error:(NSError **)outError; ``` 传入对应的列表枚举即可。如果返回`NO`可以通过`NSError`的`error.localizedDescription`查看原因. 可以通过: ``` objc @property(readonly) NSArray *availableCategories; ``` 属性,查看当前设备支持哪些类别,然后再进行设置,从而保证传入参数的合法,减少错误的可能. 比如使用如下代码: ``` objc NSLog(@"Current Category:%@", [AVAudioSession sharedInstance].category); NSError *error = nil; [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:&error]; if (nil != error) { NSLog(@"set Option error %@", error.localizedDescription); } NSLog(@"Current Category:%@", [AVAudioSession sharedInstance].category); ``` 此时在播放音乐的时候,再去按下静音键,会发现,音乐还在继续播放,不会被静音。 ### 类别的选项(Category Options) 上面介绍的这个七大类别,可以认为是设定了七种主场景,而这七类肯定是不能满足开发者所有的需求的。`CoreAudio`提供的方法是,__首先定下七种的一种基调,然后在进行微调.`CoreAudio`为每种`Category`都提供了些许选项来进行微调.__ 在设置完类别后,可以通过: ``` objc @property(readonly) AVAudioSessionCategoryOptions categoryOptions; ``` 属性,查看当前类别设置了哪些选项,注意这里的返回值是`AVAudioSessionCategoryOptions`,__实际是多个`options`的`|`运算__. 默认情况下是`0`. ``` objc typedef NS_OPTIONS(NSUInteger, AVAudioSessionCategoryOptions) { AVAudioSessionCategoryOptionMixWithOthers = 0x1, AVAudioSessionCategoryOptionDuckOthers = 0x2, AVAudioSessionCategoryOptionAllowBluetooth __TVOS_PROHIBITED __WATCHOS_PROHIBITED = 0x4, AVAudioSessionCategoryOptionDefaultToSpeaker __TVOS_PROHIBITED __WATCHOS_PROHIBITED = 0x8, AVAudioSessionCategoryOptionInterruptSpokenAudioAndMixWithOthers NS_AVAILABLE_IOS(9_0) = 0x11, AVAudioSessionCategoryOptionAllowBluetoothA2DP API_AVAILABLE(ios(10.0), watchos(3.0), tvos(10.0)) = 0x20, AVAudioSessionCategoryOptionAllowAirPlay API_AVAILABLE(ios(10.0), tvos(10.0)) __WATCHOS_PROHIBITED = 0x40, } NS_AVAILABLE_IOS(6_0); ``` | 选项 | 适用类别 | 作用 | | :------ | :------ | :------: | | AVAudioSessionCategoryOptionMixWithOthers | AVAudioSessionCategoryPlayAndRecord, AVAudioSessionCategoryPlayback, and AVAudioSessionCategoryMultiRoute | 是否可以和其他后台App进行混音 | | AVAudioSessionCategoryOptionDuckOthers | AVAudioSessionCategoryAmbient, AVAudioSessionCategoryPlayAndRecord, AVAudioSessionCategoryPlayback, and AVAudioSessionCategoryMultiRoute | 是否压低其他App声音 | | AVAudioSessionCategoryOptionAllowBluetooth | AVAudioSessionCategoryRecord and AVAudioSessionCategoryPlayAndRecord | 是否支持蓝牙耳机 | | AVAudioSessionCategoryOptionDefaultToSpeaker | AVAudioSessionCategoryPlayAndRecord | 是否默认用免提声音 | > 目前主要的选项有这几种,都有对应的使用场景,除此之外,还有iOS9之后新增加的一些 | 选项 | 适用类别 | 作用 | 最低适用系统 | | :------ | :------ | :------: | :------| | AVAudioSessionCategoryOptionInterruptSpokenAudioAndMixWithOthers | -- | -- | iOS 9| | AVAudioSessionCategoryOptionAllowBluetoothA2DP | -- | -- | iOS 10| | AVAudioSessionCategoryOptionAllowAirPlay | -- | 支持蓝牙A2DP耳机和AirPlay | iOS 10| 下面介绍一下每个子场景选项的作用: * _`AVAudioSessionCategoryOptionMixWithOthers`:如果确实用的`AVAudioSessionCategoryPlayback`实现的一个背景音,可是,又想和QQ音乐并存,那么可以在`AVAudioSessionCategoryPlayback`类别下在设置这个选项,就可以实现共存了._ * _`AVAudioSessionCategoryOptionDuckOthers`:在实时通话的场景,比如QQ音乐,当进行视频通话的时候,会发现QQ音乐自动声音降低了,此时就是通过设置这个选项来对其他音乐App进行了压制._ * _`AVAudioSessionCategoryOptionAllowBluetooth`:如果要支持蓝牙耳机电话,则需要设置这个选项._ * _`AVAudioSessionCategoryOptionDefaultToSpeaker`: 如果在VoIP模式下,希望默认打开免提功能,需要设置这个选项._ 通过接口: ``` objc - (BOOL)setCategory:(NSString *)category withOptions:(AVAudioSessionCategoryOptions)options error:(NSError **)outError; ``` 来对当前的类别进行选项(options)的设置. 实例代码: ``` objc - (void)xxxMethod{ [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback withOptions:AVAudioSessionCategoryOptionMixWithOthers error:&error]; if (nil != error) { NSLog(@"set Option error %@", error.localizedDescription); } options = [[AVAudioSession sharedInstance] categoryOptions]; NSLog(@"Category[%@] has %lu options", [AVAudioSession sharedInstance].category, options); } ``` 此时,如果打开QQ音乐播放器,然后再开始进行播放,会发现,QQ和我们的实例都在播放,并且进行了自动混音。 ### 七大模式 通过上面的`七大类别`: ``` objc #pragma mark -- Values for the mode property -- AVF_EXPORT NSString *const AVAudioSessionModeDefault NS_AVAILABLE_IOS(5_0); AVF_EXPORT NSString *const AVAudioSessionModeVoiceChat NS_AVAILABLE_IOS(5_0); AVF_EXPORT NSString *const AVAudioSessionModeGameChat NS_AVAILABLE_IOS(5_0); AVF_EXPORT NSString *const AVAudioSessionModeVideoRecording NS_AVAILABLE_IOS(5_0); AVF_EXPORT NSString *const AVAudioSessionModeMeasurement NS_AVAILABLE_IOS(5_0); AVF_EXPORT NSString *const AVAudioSessionModeMoviePlayback NS_AVAILABLE_IOS(6_0); AVF_EXPORT NSString *const AVAudioSessionModeVideoChat NS_AVAILABLE_IOS(7_0); AVF_EXPORT NSString *const AVAudioSessionModeSpokenAudio NS_AVAILABLE_IOS(9_0); ``` 我们基本覆盖了常用的__主场景__,在每个主场景中可以通过`Option`进行__微调__。为此`CoreAudio`提供了七大比较常见微调后的子场景。叫做`各个类别的模式`. | 模式Mode | 适用的类别 | 场景 | | :------ | :------ | :------: | | AVAudioSessionModeDefault | 所有类别 | 默认的模式 | | AVAudioSessionModeVoiceChat | AVAudioSessionCategoryPlayAndRecord | VoIP | | AVAudioSessionModeGameChat | AVAudioSessionCategoryPlayAndRecord | 游戏录制,由GKVoiceChat自动设置,无需手动调用 | | AVAudioSessionModeVideoRecording | AVAudioSessionCategoryPlayAndRecord AVAudioSessionCategoryRecord | 录制视频时| | AVAudioSessionModeMoviePlayback | AVAudioSessionCategoryPlayback | 视频播放| | AVAudioSessionModeMeasurement | AVAudioSessionCategoryPlayAndRecord AVAudioSessionCategoryRecord AVAudioSessionCategoryPlayback | 最小系统 | | AVAudioSessionModeVideoChat | AVAudioSessionCategoryPlayAndRecord | 视频通话 | 每个模式有其适用的类别,所以,并不是有“七七 四十九”种组合。如果当前处于的类别下没有这个模式,那么是设置不成功的。 设置完Category后可以通过如下代码: ``` objc @property(readonly) NSArray *availableModes; ``` 这个属性,查看其支持哪些属性,做合法性校验。 下面说一下具体应用场景: * __`AVAudioSessionModeDefault`: 每种类别默认的就是这个模式,所有要想还原的话,就设置成这个模式。__ * __`AVAudioSessionModeVoiceChat`:主要用于VoIP场景,此时系统会选择最佳的输入设备,比如插上耳机就使用耳机上的麦克风进行采集。此时有个副作用,他会设置类别的选项为`AVAudioSessionCategoryOptionAllowBluetooth`从而支持蓝牙耳机。__ * __`AVAudioSessionModeVideoChat` : 主要用于视频通话,比如QQ视频、FaceTime。时系统也会选择最佳的输入设备,比如插上耳机就使用耳机上的麦克风进行采集并且会设置类别的选项为`AVAudioSessionCategoryOptionAllowBluetooth`和`AVAudioSessionCategoryOptionDefaultToSpeaker`。__ * __`AVAudioSessionModeGameChat` : 适用于游戏App的采集和播放,比如“GKVoiceChat”对象,一般不需要手动设置.__ > 另外几种和音频APP关系不大,一般我们只需要关注VoIP或者视频通话即可。 通过调用: ``` objc - (BOOL)setMode:(NSString *)mode error:(NSError **)outError; ``` 可以在设置`Category`之后再设置模式。 当然,这些模式只是`CoreAduio`总结的,不一定完全满足要求,对于具体的模式,在`iOS10`中还是可以微调的。 通过接口: ``` objc - (BOOL)setCategory:(NSString *)category mode:(NSString *)mode options:(AVAudioSessionCategoryOptions)options error:(NSError **)outError; ``` 但是在`iOS9`及以下就只能在`Category`上调了,其实本质是一样的,可以认为是个API语法糖,接口封装. ### 系统中断响应 上面说的这些`Categorg`、`Option`以及`Mode`都是对自己作为播放主体时的表现,但是假设,现在正在播放着,突然来电话了、闹钟响了或者你在后台放歌但是用户启动其他App用上面的方法影响的时候,我们的App该如何表现呢?最常用的场景当然是先暂停,待恢复的时候再继续。那我们的App要如何感知到这个终端以及何时恢复呢? `AVAudioSession`提供了多种`Notifications`来进行此类状况的通知。其中将来电话、闹铃响等都归结为一般性的中断 用`AVAudioSessionInterruptionNotification`来通知。其回调回来的`userInfo`主要包含两个键: * _`AVAudioSessionInterruptionTypeKey`: 取值为`AVAudioSessionInterruptionTypeBegan`表示中断开始,我们应该暂停播放和采集,取值为`AVAudioSessionInterruptionTypeEnded`表示中断结束,我们可以继续播放和采集。_ * _`AVAudioSessionInterruptionOptionKey`: 当前只有一种值`AVAudioSessionInterruptionOptionShouldResume`表示此时也应该恢复继续播放和采集。_ __而将其他`App`占据`AudioSession`的时候用`AVAudioSessionSilenceSecondaryAudioHintNotification`来进行通知。其回调回来的__`userInfo`键为: ``` objc AVAudioSessionSilenceSecondaryAudioHintTypeKey ``` 可能包含的值: * `AVAudioSessionSilenceSecondaryAudioHintTypeBegin`: 表示其他`App`开始占据`Session`. * `AVAudioSessionSilenceSecondaryAudioHintTypeEnd`: 表示其他`App`开始释放`Session`. ### 外设改变 除了其他`App`和系统服务,会对我们的`App`产生影响以外,用户的手也会对我们产生影响。默认情况下,`AudioSession`会在`App`启动时选择一个最优的输出方案,比如插入耳机的时候,就用耳机。但是这个过程中,用户可能拔出耳机,我们App要如何感知这样的情况呢? 同样`AVAudioSession`也是通过`Notifications`来进行此类状况的通知。 假设有这样的App ![](/assets/images/20180112AVAudioSessionCategory/RouteChange.webp) 最开始在录音时,用户插入和拔出耳机我们都停止录音,这里通过`Notification`来通知有新设备了,或者设备被退出了,然后我们控制停止录音。或者在播放时,当耳机被拔出出时,`Notification`给了通知,我们先暂停音乐播放,待耳机插回时,在继续播放。 在`NSNotificationCenter`中对`AVAudioSessionRouteChangeNotification`进行注册。在其`userInfo`中有键: * `AVAudioSessionRouteChangeReasonKey` : 表示改变的原因 * `AVAudioSessionSilenceSecondaryAudioHintTypeKey`: 和上面的中断意义意义一样。 | 枚举值 | 意义 | | :------ | :------: | | AVAudioSessionRouteChangeReasonUnknown | 未知原因 | | AVAudioSessionRouteChangeReasonNewDeviceAvailable | 有新设备可用 | | AVAudioSessionRouteChangeReasonOldDeviceUnavailable | 老设备不可用 | | AVAudioSessionRouteChangeReasonCategoryChange | 类别改变了 | | AVAudioSessionRouteChangeReasonOverride | App重置了输出设置 | | AVAudioSessionRouteChangeReasonWakeFromSleep | 从睡眠状态呼醒 | | AVAudioSessionRouteChangeReasonNoSuitableRouteForCategory | 当前Category下没有合适的设备 | | AVAudioSessionRouteChangeReasonRouteConfigurationChange | Rotuer的配置改变了 | # 总结 `AVAudioSession`构建了一个音频使用生命周期的上下文。当前状态是否可以录音、对其他App有怎样的影响、是否响应系统的静音键、如何感知来电话了等都可以通过它来实现。尤为重要的是`AVAudioSession`不仅可以和`AVFoundation`中的`AVAudioPlyaer`/`AVAudioRecorder`配合,其他录音/播放工具比如`AudioUnit`、`AudioQueueService`也都需要他进行录音、静音等上下文配合。 [参考](https://www.jianshu.com/p/3e0a399380df) [参考2](http://cinvoke.me/?p=37) 全文完 URL: https://sunyazhou.com/2017/12/FinalSummary/index.html.md Published At: 2017-12-31 10:46:20 +0000 # 2017年终总结 ![](/assets/images/20171230FinalSummary/2017FinalSummart1.webp) # 前言 **时间像车轮一样在飞转** **历史像江河一样在流淌** **整整走过了365个风雨历程** **2017年过的如此不寻常** **一度春夏秋冬 一度雨雪风霜** **一度翻山越岭 一度起伏延宕** 又一年过去了,我也得墨守成规的按时交出我的年终总结了,为了不标新立异,我必须尽量不废话.(**talk is cheap, show me the code!**) ## 2017回顾 这一年的重点事件主要如下: * 技术 * 买房 * 工作 * 书法 * 读书 * 运动 * 驾照 ### 技术 这一年让我唯一有些许成就感的事情莫过于我搭建了自己的技术博客,能在这里记录我的技术成长和生活中的点点滴滴. 从年初我搭建博客开始至今已发表整整45篇. ![blog](/assets/images/20171230FinalSummary/2017FinalSummaryBlog1.webp) 正如我的**关于**页面说的,我并没有对它的品质太过于苛求,尽量记录一下容易忘记的技术和生活经验. 虽然大家看到这些并没有太过留意,但我自己确觉得如数家珍,毕竟这是一笔财富.当我几年后会看这篇年终总结的时候,希望自己**没有因为虚度年华而悔恨,也没有因为碌碌无为而羞耻**. ![](/assets/images/20171230FinalSummary/2017FinalSummaryBlog2.webp) 我是一个不喜欢玩各种游戏的90后,每天看着统计后台才找回一点干活的心情. 今年的技术多数集中在多媒体上,每天和音视频斗智斗勇.今年的前半部分在开发TCP的聊天应用叫`百度Hi for mac`. 当然厂长[Robin(李彦宏)](https://baike.baidu.com/item/%E6%9D%8E%E5%BD%A6%E5%AE%8F/125160?fr=aladdin))也在用. *对于这个满脑子人工智能一门心思研究无人驾驶每次产品都落地的不咋地整整核心技术人员总留不住的厂长,我递过去了我的辞职申请*.今年下半年在全力研究多媒体短视频相关的开发,`Open GL`,`GPUImage`,`AV Foundation`...... 吭哧吭哧(kengchi 一声). 直到写博客现在为止,才刚刚研究完`AV Foundation`最后一页. 搞了很久技术深感在某些领域需要深耕,不能盲目的跟风,还得脚踏实地. 28岁,90后.这一年感觉技术上没有啥成就感,到有如下感慨: > 也许大神都是命中注定的,而我很不幸,没有被注定. --- ### 买房 这是我2017年经历过最始料未及的一个大事件. #### 看房 这事得从2016的十一长假说起, 我回哈尔滨看房 从哈尔滨万达广场沿着 哈尔滨大街 步行, 到群力 又步行回来.跟我一个比较要好的高中室友. 大概的路线是这样的: ![看房](/assets/images/20171230FinalSummary/2017FinalSummaryFindHouse.webp) 据我后来统计 每天至少步行18公里以上. 于是我得出看房的经验列表: | 区域 | 房价 | 房屋类型 | 装修类型 | 发展潜力 | 推荐看房装备 | | :------: | :------: | :------: | :------: | :------: | :------: | | 哈西 | 均价8k~1w | 期房多/现房少 | 毛坯多/精/简装少 | 高铁/学区/商圈/地铁 | 电动车+跑鞋 | | 群力 | 均价6k~9k | 期房少/现房少 | 毛坯 | 周边啥也没有(关东古巷/松花江) | 电动车+跑鞋 | | 其它 | N/A | N/A | 二手居多 | 周边配套没调研 | 电动车+跑鞋 | > 注意:*为啥我推荐的是电动车+跑鞋,因为我看房时发现有的区域跨度太大,需要车最好,有的区域跨度小有车没地方停,尤其是一些还没建成的地块,你去了漫天漂灰,泥泞不堪,你开车不是刷车就是麻烦耽误时间,车速太快你看不清楚哪是哪,走走停停车我认为不方便,摩托车太大一般,只有电动车最合适,一个骑车一个看,哪里不清楚看哪.但需要一个续航里程高点的电动车.还有跑鞋,看房必须得走,上楼下楼,这个单元那个小区的.没有一双舒服的鞋走一上午就知道啥滋味了.* 可惜我没车,只有一双腿和一双`new balance`跑鞋,每天走上18公里,可能是鞋垫太舒服了,脚都磨红了都没起泡,我还暗自慨叹我自己多高明得回没穿别的鞋,可是没当我高兴一会儿就发现,鞋垫快磨碎了. 所以建议大家看房要 `管住嘴`,`带够水`,`迈开腿`,别吃太饱了. | 我看过的房地产 | 推荐意见和结论 | | :------: | :------: | | 有个叫什么红星城 | 纯扯淡浪费时间 不推荐 | | 东方新天地 | 缺钱的可以考虑,据说是房本下来很费劲 | | 恒大珺庭 | 房屋朝向让我觉得难受,简装,怕费事的可以考虑,拎包入住大开发比较靠谱| | 金爵万象 | 貌似在新天地东边,墙皮子直掉不敢买,不推荐 | | 辰能溪树庭院 | 精装,推荐,就是贵,估计都没有了 | | 北纬45度 | 就剩公寓了,没住宅了,买不了了 | | 君贵东方瑞景 | 售楼员挺二,清华大街南北还是城中村,一般,价格死贵还没啥亮点 | | 漫步巴黎 | 没房了,剩下的都不咋好,房屋太大,内室很不适合我们东北人的习惯,一般 | | 百年俪景 | 没房, 价格一般,房屋格局还行,喜欢安静的可以考虑,价格稍贵| | 观江国际 | 是我见过群力最贵的了当时1.5w 不过没啥好房子了,不差钱推荐| | 其它 | 基本没有好的,板楼基本都是期房还没有,高层不是时间长就各种不靠谱| 上述这些只代表我个人看法. 时间到达2017年1月份,过年回家 我趁这个时间点又去了几家,发现 房价 从`8`千涨到`1w`,恒大珺庭最为明显.十一的时候还有些好的户型,十一以后基本绝迹.剩下一堆破户型,要不挡光要么房屋布局狭窄. #### 哈尔滨户口和北京公积金 我是个北漂的码农大学毕业直接去了北京. 户口还不属于哈尔滨市. 据我跑完大部分的售楼处后,打听的公积金使用方式, 1. 北京的公积金如果想在哈尔滨市用必须是哈尔滨市户口才能用. 2. 想办理哈尔滨市户口必须得买房,或者单位接收等集体户口. 3. 想在`哈尔滨`买房用`北京公积金`必须得是**哈尔滨市户口**,别的地方用不了. 写到这里我真的想骂娘了,但为了保持儒士的风范我还是忍了,我不得不说,这是个__鸡生蛋__,__蛋生鸡__的问题. 如果说农村户口想成为哈尔滨市市民是因为我们农村人每个人分块地的话,那我请政府收回我的土地,把我户口改成哈尔滨市户口.那块破土地如果能发家致富我又何必背井离乡去北漂. #### 过年筹划买房 ![](/assets/images/20171230FinalSummary/HaerbinFuture.webp) [原图清晰](/assets/images/20171230FinalSummary/HaerbinFutureBig.webp) 回家过年,这年过得我心情都不在年上,心情很矛盾,过了初七就上班回北京了.我的家在绥化市海伦市. 回北京的两个月时间里,根本都没有什么干活的劲头,于是我就给父母打电话 我说了至今为止最具掷地有声的话. 我在电话中对父母说:"我在北京,距离哈尔滨1200多公里,你们说我是千里奔波去看房,还是你们去呢,我周末就两天假,去了回来最多也就能看上半天,你们在家没啥事距离哈尔滨那么近为啥不去帮我去看看房呢."(父母每年都去外面打工) > 父母确实听了这一番话,也都感同身受 于是俩人行动了起来, 一周跑了两趟哈尔滨.由于哈尔滨也没啥亲戚只能去了 回海伦,去了回海伦. 我的一番话起了决定性作用, 这俩人在周五打电话告诉我,他们看中了爱达88.回来吧! #### __天时__ __地利__ __人和__ 买房绝对是一件`天时`,`地利`,`人和`的事情. 我父母帮我看房这一周,我基本也没心情干活,我的老大(经理)看我的心情我都能猜到了,最后他给了我一周假,回家买房. 因为这一周我在办理各种银行的 收入证明,银行流水的打印贷款哈尔滨各种银行的模板,公积金等 北京的公积金如果想拿出来当首付,必须退出北京市公积金.相当于离开北京. 说白了就是得离职. 这段经历让我至今难忘,当时的真是进退维谷. #### 坐飞机回家买房 2017年的3月某日凌晨5点,周六 天气:__轻度雾霾__,地点:去往北京首都国际机场T2航站楼的路上 前天晚上预约的滴滴专车早已在楼下等候,我和`我家那位`在最后一遍确认 户口本、身份证、公司人力资源部签发的各种银行的收入证明、各种银行卡、银行流水单...... 确认完毕 出发. 路上司机是个话唠 司机说:"这是去哪!" 我说:"回家看看房" ... 司机说:"我们北京政府给我们分的房子 一平 才3000多,距离地铁不算太远" 我说:"北京人真好...国家都如此照顾" ... 司机说:"唉 都是这些外来人口把这房价搞起来的,要不我们还能再便宜点!" 我说:"...要啥自行车知足吧!" 司机说:"&%&*#$#&#$#^#$^!" ... 我真都懒的和他吐口水. > 牛逼哄哄的北京人,你要是去哈尔滨租房,北京户口,哥的房子就双倍价格租你,不住就回北京,冰城人不太欢迎你这种得了便宜还不知足的北京土著 终于到机场,雾霾貌似散去了不少. 东方航空的飞机 早已在停机坪等着值机(值机就是你选好你的座位)检票了. 打印取票... 等了好长时间发现去太早了,不去太早也不行,就北京这交通还是宁愿早去三个小时也不能相信taxi.地铁绕道好远. 我看飞机票上边写着 9:58分到哈尔滨 ... 检票... 登机... 7:59 起飞 (起飞起了半个小时我服了,别告诉你要delay了,坐火车总晚点,坐飞机还晚点我也是醉了) 当飞机起飞开到北京的上空,早已没了雾霾,出现在眼前的是`祖国的万里长城`(这个时候如果能放上一首《我的中国心》你想起那首歌词, 长江...长城..),如果说去过长城觉得很没意思,那么当你在飞机上俯视它的时候,我觉得你会看到另一种心情,那感觉绝对前所未有.有机会大家可以试试,但要在天气非常好的时候,太阳刚从东方升起. 后来我才发现如果你是个海归的人才当你看到祖国的万里长城心理是什么滋味(你的飞机要降落在祖国的首都国际 机场,看着梦寐已久的万里长城). > 当时我的直觉告诉我,今天是个好兆头.买房应该十拿九稳. 飞机穿越辽宁的上空,看到一架 `歼-15`貌似是战斗机 飞机腹部带着一颗导弹 直线向南飞去... 估计是每天执行任务,紧接着 就是白雪皑皑的 平原 尽览无余. 我刻意观察过,这雪的厚度 从略裹地表... 飞过吉林逐渐增加雪的厚度...到哈尔滨 太厚了. 飞行也就一个小时... 如果哈尔滨机场是我见过最破的机场那应该是我说错了,如果我说你看到一片`苞米地`还有稍许`电线杆子`飞机跑道的水泥地上都冻成冰了 青一块紫一块的 地表 看清后这是飞机起降的跑道你也许不信,但是这飞机就能在这种恶劣环境正常起降. 降落半个小时,飞行员这个屌丝 不知道干啥 飞行速度降落的时候慢的可怜,哈尔滨周边的村庄屯子 看的一清二楚,哎呀,当时那心情,我只能说我第一次坐飞机回哈尔滨,不知道太平机场周边太荒凉. 降落之后我总结 原来 行程两个小时其实 中途飞行也就一个小时,起飞半个小时,降落半个小时.我能说啥呢,完美没晚点.这飞机开的我给你打101分. 坐大巴到哈西... 下来大巴路过哈西站东广场,直奔爱达售楼处. **奇迹的一幕发生了** 我吐了一口唾沫, 不偏不正, 沿着 广场上有地漏缝隙的大理石`直直`吐到了下水道,吐进去了,连边都没沾,就是这么准, 我当时惊呆了,可惜当时着急没照相. > 这是真事,我当时的直觉告诉我,怎么这么正道, unblievable看来今天买房能买成. 到了爱达售楼处我父母早已在那等着我了, 我父母说要不要在周边看看,我说不用了砍瓜切菜挑户型,买吧! 于是就是干净利落的交完定金,回家办理结婚手续,张罗钱. > 看到这里提醒一下大家,事后我发现我选的户型没有阳台,是飘窗那种看样子. ![](/assets/images/20171230FinalSummary/2017FinalSummaryHouseLayout.webp) 爱达88-1单元-12楼-三室两厅一卫-`124.7`平,当时的价格 10800/平 来张封顶的照片 感谢__红姐__提供: ![](/assets/images/20171230FinalSummary/Aida88Contructing.webp) 在过去的几年我回家的路线是这样的: ![](/assets/images/20171230FinalSummary/HailunPath.webp) 从`北京坐地铁`到`北京站`-->`哈尔滨站`-->`海伦站`-->`大客(大巴)`-->`第二良种场` 全程`1455`公里左右,说的凄惨一些坐大巴回农村地图都没有导航.压根都没有这个路线,到海伦`40公里左右` 如果说交房以后我的路线应该是这样的: ![](/assets/images/20171230FinalSummary/HaerbinPath.webp) 从`北京坐地铁`到`北京站`-->`哈尔滨西站` 完事 走5分钟我到家了. > 为了这`265`公里(哈尔滨减去到海伦农村的公里)我已为之奋斗了至少`4`年,未来还会继续奋斗到把房贷还完. #### 贷款 回家办理完最坑的事情发生了 我首付交了61w多 贷了60万,哈尔滨银行是我见过最SB的银行,我的银行流水已经高出两个月的还贷价格,但它还要我女朋友的流水也要达到和我同等标准.我当时真是膈应的哈尔滨银行咬牙切齿. 后来通过招商银行办理的商业贷款买的房.至此我想说如果你有能力达到这个银行的标准你应该离中产阶级不远了,已经是小康中的小康了. 在这里我说几个比较坑的地方 1. 银行的贷款银行流水需要现场打印,作假的流水就不要去贷款了否则会得不偿失. 2. 还贷能力不够的需要找一个中间人.他的要求是和你一样准备相关材料.这种基本就需要靠各种亲戚了,可惜的是我家有这样的亲戚却有贷款. 3. 贷款的时候 贷款业务员总推荐你买各种理财产品,不要买trust me. 4. 贷款的银行里面必须保证有至少还贷能力三个月的存款. 我被第`4`条坑了,我交完首付的时候要说身无分文那有点扯淡,但是也就最多`2000多元`,两个人回北京的路费去除掉 吃饭住宿都勉强不能维持了.还要我存进去`3万`这不扯淡嘛?后来又和比较好的同学,姐姐借点钱存进去了.唉 太TM扯淡了 我已经回到解放前了,还要我怎样. 至此我觉得一个国家连最基本的老百姓住房都成问题这已经不是一个好的国家了. > __注意:银行贷款放款之后一定要去售楼处索要发票,只有首付款发票+尾款发票=房产证,两张发票有一张没有的话都容易拿不到房产证,这事比较坑的是售楼处的售楼员不告诉我们这些业主. (2018/02/28日更新)__ #### 办理完所有买房手续 有一种感觉叫如释重负,就是形容买完房办理贷款.等待审批完成最后成为一个不折不扣的房奴,那这段话说的应该是我,心理的一块石头算是落了地. 4月份收到招商银行的审批合同,银行要求必须本人去取.也可以通过代理人拿着本人身份证 代理人身份证去取,于是我求助了我的一个 小学、初中、高中同学.帮我去爱建支行取出我的贷款合同帮我邮寄到了北京. 我买房父母没拿多少钱,全是这几年大学毕业辛苦北漂忍受雾霾拿生命积攒下来的. 所以我不羡慕那些拿钱给儿女买房的父母. 至此买房 算是告一段落. > 买房如果写两个人名的话,是签发的6份合同, 房地产,银行,本人各一份 > 产权所有是谁签 51% 和谁签 49%的比例 #### 提取北京公积金 北京公积金提取需要提供如下材料: * 购房合同原件和购房合同复印件 * 贷款合同原件和贷款合同复印件 * 北京纳税连续一年以上打印 * 购房发票原件以及税务查询截图打印 * 购房的付款小票 * 公司的提取公积金章程表格 * 部门经理签字认可单 * 本人亲自签字认可对提供材料的真实性认可单 这些东西办完之后 公积金可以约定支取.由于百度破公司的信誉不好,只能每个季度(3个月)提取一次. **办理完之后北京住房公积金会返回给我们一张`公积金约定提取记录单`用于其它人提取的凭证,下次提取必须凭借这张提取记录单,比如我女朋友她想提取必须拿这个记录单,记住这很重要.** 第一笔钱入账的时候都已经是 6月末,第一件事就是把买房时候借点钱都还清. > 我就想不明白为啥公积金的钱是我的我还提取出来这么费劲,即便是十九大各种开会减少各种手续我发现外地人依然还是老样子,只有北京人提取各种方便.我满腔激愤的膈应起来那个出租车司机. 经过了如此复杂的手续 我才得以买套房,想想真是心酸无助,确又黯然神伤, 在中国买房就是这么费事, 后来买房的看客们, 做好一个打持久战的准备,因为我们在和中国的体制斗智斗勇. 如果对本人买房经历有任何问题 请底部留言,我一一解答. 到此买房结束, 我相信跟我做邻居的人绝不是一般的普通人,他也必定经历我的这些至少一部分,收入、地位、等等等等,因为能在爱达这个地块买房的人,非富即贵,希望我的经历和买房经验能给你提供借鉴,少走弯路. --- ### 工作 #### 离开中国互联网之巅-百度 ![](/assets/images/20171230FinalSummary/2017FinalSummaryWork1.webp) 如果你是一个年轻需要锻炼的程序员我建议你去百度,那里有你成长所需要的土壤 ![](/assets/images/20171230FinalSummary/2017FinalSummaryWork2.webp) 上边这张是办公环境,可以说确实很好. **有一种离职叫真的干够了** 离开百度与其说是一种**损失**不如说是**选择比努力更重要**. 认识了很多精英小伙伴,他们不是__干啥啥行__的人就是在某个领域__世界排行榜都有成就__的人. 我一开始在百度云设备,后来产品不济转岗到**百度网盘**.是的你今天用的`iOS`百度网盘或者__macOS百度网盘__就有我开发的一段故事. 后来觉得团队气氛不是很好转战到百度hi做聊天,如果我说QQ微信你知道的话,那么百度有没有类似QQ、微信这样的聊天工具呢,有的,这玩意就叫百度Hi. 在Hi团队 接触到了一位来自`清华大学`毕业的老乡__涛兄__. 如果说这听起来感觉很有面子的话,那我来说一个2017年我听过最真实的笑话. 据涛兄说: __他当年高考的时候,数学打了`149`分(150满分),让涛兄很不能释怀的是他有时候吃饭的时候都再思考为啥不是`150`分,他卷面也已经很干净了,没有错误的题目啊.终于有一天他找到了答案,他说:"有可能是卷面太干净了扣了一分"__. > 这是一件2017年我听过最搞笑且真实的笑话,回想我的高考..还是算了,这就是差距. 离开的百度Hi 来到的新的公司 金山云 进公司的时候唉 一眼望去 好多张熟悉的面孔,原来相当一部分同事都是百度的前同事. 工作就这样把,不多介绍了 --- ### 书法 这一年买了两只毛笔花了大概200多 ![](/assets/images/20171230FinalSummary/Brush.webp) 不过用起来确实很好,好马配好鞍,好笔配毛毡. ![](/assets/images/20171230FinalSummary/calligraphy.webp) 有幸在百度碰到书法高手-老乡`潘旭`,这个绥棱人真是干啥啥行,写代码飞驰电掣.还有一手好书法.让我这个海伦人很佩服. ![](/assets/images/20171230FinalSummary/Linzexu.webp) 这是林则徐当年写给皇帝的虎门销烟奏折.是我学习书法的最终目标. --- ### 读书 在文学上 2017读过+听过的书我都一一记录 也推荐大家看看. * 《卑鄙的圣人曹操》1~10部全听完 ![](/assets/images/20171230FinalSummary/Novel1.webp) * 《知行合一 王阳明 》 ![](/assets/images/20171230FinalSummary/Novel2.webp) * 《大清相国》 陈廷敬 ![](/assets/images/20171230FinalSummary/Novel3.webp) * 《晚清的最后十八年》1~3部 ![](/assets/images/20171230FinalSummary/Novel4.webp) * 《Learn AV Foundation》 ![](/assets/images/20171230FinalSummary/LearnAvFoundation.webp) 简单就这么多 这里比较推荐的是__《大清相国》__和__《晚清最后的十八年》__ #### 读《晚清最后的十八年》中有一个故事值得学习 ![](/assets/images/20171230FinalSummary/FuDaoAnZheng.webp) > 福岛安正(1852一1919)日本信州人.日本殖民机构关东都督府都督。陆军大将。有“日本情报战之父”之称。他是一个具有超前眼光的阴谋家、战略家. > 1887年,福岛安正被任命为日本驻德国武官。在德国的五年中,他详细的考察了欧洲各国的情况,其中俄国的动向引起了他的极大关注。迫于英国的牵制,俄国传统的南进政策被迫放缓,福岛安正正确预测俄国必然会转向东进。果然,1891年1月,俄国公布了建造西伯利亚大铁路的计划,并立即动工。这条连接俄国首都圣彼得堡和符拉迪沃斯托克(海参崴),横穿欧亚大陆的铁路,可以说是俄国侵略亚洲的最大武器。西伯利亚铁路横穿中国东北部,从哈尔滨向南有一分支,直达旅顺、大连,可以使俄国轻而易举的进入亚洲。福岛预测这条铁路大约要耗时十年,他认为这十年是关系日本生死存亡的十年,日本对此绝不能袖手旁观。 ![](/assets/images/20171230FinalSummary/Russia.webp) > __为了掌握俄国东进政策的实际情况,福岛决定亲自沿着西伯利亚铁路进行实地侦察。这个大胆的计划立即得到了参谋次长川上操六的大力支持。由于福岛的军人身份会暴露穿越计划的目的,于是福岛对外宣布要进行一次单骑穿越严寒时期西伯利亚的探险旅行。即使是土生土长的俄国人,也不敢轻言在严冬时期穿越西伯利亚,更何况是一个没有严寒地区生活经历的外国人。福岛此言一出,世界哗然,西方探险家对他的“穿越”计划嗤之以鼻,大家都等着看这个日本人的笑话。但福岛安正极为坚定,他深知这个计划对日本意味着什么__。 > __1892年2月11日,福岛和爱骑“凯旋”从德国踏上穿越之旅,在零下20摄氏度的严寒中北上。3月下旬,到达圣彼得堡后,福岛向日本参谋本部发去了第一份关于俄国陆军的调查报告。此时,俄方也隐约觉察到福岛的此次“旅行”是项庄舞剑,意在沛公。4月9日,福岛离开圣彼得堡,于当月下旬抵达莫斯科。在那里受到了沙皇和皇后的接见和赐宴。经过细致考察,福岛向日本参谋本部提交了关于西伯利亚铁路建设方面的报告。9月下旬,福岛到达中俄两国的界山―海拔3000多米的阿尔泰山。从阿尔泰山极目远望,满目皑皑白雪。至此,福岛已经走了7000公里,完成一半路程。 福岛安正在旅途中迎来了1893年。1月下旬到2月是西伯利亚最寒冷的时期,气温达到零下50度。俄国人几乎不在这样严寒的冬天外出,但是福岛却以惊人的意志在荒无人烟的冰天雪地里顽强前行。最终,凭借顽强的的毅力,福岛走出了西伯利亚。沿着逐渐转暖的黑龙江一路南下进入中国,福岛安正又用了两个多月的时间,在瑷珲、齐齐哈尔、吉林等地刺探军情。经过一年多的艰苦跋涉,福岛已经身心俱疲。他终于到了终点站--符拉迪沃斯托克。在这次的西伯利亚穿越中,福岛安正至少换了八匹马,历时488天,行程14000公里,创下了情报侦察史上的奇迹__。 居住在符拉迪沃斯托克的日本人欣喜若狂地迎接福岛的到来。全世界都在大肆报到他单骑穿越西伯利亚的消息。福岛安正成了世界的名人。明治天皇特授予他三等旭日重光勋章并亲自设宴款待。福岛安正在穿越中获得的第一手资料,成为日俄战争中日本获胜的重要信息保障。 一个国家的情报工作,单靠几个人的力量是远远不够的,经过长期的经营才能形成一个庞大而有效的情报网络。在这方面,福岛也是一个行家里手 这个人日本人横穿俄国欧亚远东大铁路,也就是今天的莫斯科->>满洲里->>齐齐哈尔->>大庆->>哈尔滨->>牡丹江->>海参崴 对俄贸易跨国专列铁路线. 就为了搜集战略情报,铁路途经山川地形地貌河流都绘制成地图(那时候还没有google卫星定位地图),为了战争做好充足的准备. 这个人的意志深深的鼓舞了我,如果一个国家想侵略另一个国家,出一两个这样的人才就差不多够了. 下图是晚清的东北军 ![](/assets/images/20171230FinalSummary/ManqingDongbeijun.webp) 这些书多数都是听的少数用kindle看的.2018年会再接再厉. --- ### 运动 这一年一直坚持打羽毛球 ![](/assets/images/20171230FinalSummary/BadmintonTeam2.webp) 参加了金山的羽毛球比赛拿了一个季军 老实说纯打酱油哈见笑见笑 ![](/assets/images/20171230FinalSummary/Awards1.webp) ![](/assets/images/20171230FinalSummary/Awards2.webp) 金山的小伙伴们还是不错的团队 ![](/assets/images/20171230FinalSummary/BadmintonTeam1.gif) --- ### 驾照 这是2017年我觉得最值得去做的一件事,终于把驾驶证考了.上学的时候家里条件实在太差了,没钱考驾照,上班以后没有了时间.总之结果还是好的 来张海淀驾校的照片. ![](/assets/images/20171230FinalSummary/DrivingLicence.webp) 说一下考驾照的感触 科目一、二、三、四 一气呵成.2个月顺利拿本,总计跑了11趟驾校. 不过不得不说首都的教育还是很好的,教学设置一应俱全.果然还是得看首都的发展. 如果是自尊心强的人我建议可以报名.如果挨骂几句都接受不了的话还是别报了. ## 2018目标 说完了2017年的各种大事件 下面我列一下今年的目标 * swift4 进阶看完 * iOS Core Animation 看完 * Learn AV Foundation 要写几篇博客从上次段的位置续上 * 学会Python和数据挖掘 为机器学习做铺垫 * 人工智能领域要有跨足 * 多媒体相关技术深耕 * 英语水平再提高一个level 好了目标就这些吧 # 总结 这一年过很多事件都没来得及想就发生了,有些想的基本很少完成,博客质量有量的积累却没有质的飞跃. 如果用几个字来结束2017的年终总结那我只能发出这句话吧! **雄关漫道真如铁,而今迈步从头越.** URL: https://sunyazhou.com/2017/12/markdownAudio/index.html.md Published At: 2017-12-27 12:04:07 +0000 # Markdown中插入音频文件 # 前言 喜欢在博客文章打开的时候 播放一首背景音乐, 但Markdown本身是不支持插入音频视频,带着这个疑问开始这篇文章. ## markdown插入音乐 `markdown`其实就是 一种`html`的转换语法,其实内部也同时支持直接写`html`标签, 如果不了解各种标签请点击[w3cschool](https://www.w3schools.com/tags/tag_iframe.asp)查看各种 API 的用法,此时要用到的标签为`iframe`,代码如下所示,其中 * `div`用于控制格式,若无则默认为居左 * `frameborder`用于规定是否显示框架周围的边框,1为是,0为否 * `marginwidth`及`marginheight`表示距离边缘的像素大小 * `width`及`height`表示播放条的长度和宽度 * `src`为播放链接,可以在如网易云音乐的`生成外链播放器`获取该链接,同时也获得以下代码,并可以自行更改;也可将音频链接改为视频链接,从而播放视频 > 值得注意的是,音频和视频在默认情况下是会自动循环播放的,可以修改链接的值进行修改 在`src`域中,`auto`值表示是否自动播放,当值为`1`时为自动播放,`0`则不是 在`src`域中,有些链接会附有`height`或`width`值,其表示表示播放框的基本宽高,可以更换其值以获得想要的播放框大小,此时可以不用填写外部的`width`及`height`. ``` html
```
## 接口说明 这里面可以看到 用了 ``` https://music.163.com/outchain/player?type=2&id=34341360&auto=0&height=66 ``` 这个接口的`id=34341360`是从这里获取的 ![](/assets/images/20171227MarkdownAudio/markdownAudio1.webp) 找到`复制链接`,然后用浏览器打开. ![](/assets/images/20171227MarkdownAudio/markdownAudio2.webp) 后边的 `id=34341360`就是我们要的 `id` 然后接口替换就可以了 更多技巧可参考以前写的一篇文章 [markdown折叠](https://www.sunyazhou.com/2017/10/25/20171025markdownSkill/) [markdown 表格](https://www.sunyazhou.com/2017/09/29/20170929MarkdownTable/) 全文完 URL: https://sunyazhou.com/2017/12/AudioPan/index.html.md Published At: 2017-12-19 11:40:13 +0000 # 音频声像Pan值电平左右声道平衡 ![](/assets/images/20171219AudioPan/AudioPan.webp) # 前言 最近在开发多媒体音视频相关业务,期间遇到的问题这里全做记录下来,下面是同事提供的一个例子我整理出来,以备后续开发遇到此类问题有个备案. ## 开篇 最近开发音频涉及到左右声道调节,基于左右声道的音量实现 声音环绕效果. 下面是 UI 演示. ![](/assets/images/20171219AudioPan/AudioPanDemo.gif) 这里其实修改的类似 `AVAudioPlayer`里面的`pan`值修改 ![](/assets/images/20171219AudioPan/PanAudioApi.webp) 我在以前的文章也有一篇提到过这个[pan 值](https://www.sunyazhou.com/2017/03/17/Learning-AV-Foundation-AVAudioPlayer/) 可能大家不理解为啥 这个 API 起名叫`pan` 在声学领域这个东西有专门的名字叫 `声像`. [这篇文章](http://underwaysoft.com/writing/books/dsp-develop.html#%E7%BA%BF%E6%80%A7%E5%A3%B0%E5%83%8F%EF%BC%88Pan%EF%BC%89)介绍了一些我们对声学知识的简单介绍,虽然不知道作者是谁,但是作者应该是非常专业的声学开发者. 其实按照我们平常的理解应该是这样去实现这个 pan 值的修改 左声道音量给右声道声音的补偿 或者右侧声道给左侧声道的补偿,通过滑块的 value 来决定两边谁加多少减多少,但是大家的思路是对的,但是做法是不正确的,因为 两边的音量放在中间必须是1.0,也就是说 range 在 `-1 ~ 1`之间. 如果按照这个滑动方式回导致滑动过大. 带着这个问题我的同事找到了一个公式 来计算 这个值 ![](/assets/images/20171219AudioPan/PanAlgorithm.webp) * `pan`就是我们的滑块的`value` * `Vl` 代表左侧音量 * `Vr` 代表右侧音量 根据这个公式我们有如下 代码 ``` objc #import typedef NS_ENUM(NSUInteger, KSYMCChannelType) { KSYMCChannelTypeLeft = 0, KSYMCChannelTypeRight = 1 }; @interface KSYMultiCanvasHelper : NSObject + (CGFloat)calculateVolume:(KSYMCChannelType)type panValue:(CGFloat)pan volume:(CGFloat)volume; @end @implementation KSYMultiCanvasHelper + (CGFloat)calculateVolume:(KSYMCChannelType)type panValue:(CGFloat)pan volume:(CGFloat)volume{ if (type == KSYMCChannelTypeLeft) { CGFloat leftVolumn = sqrt(2) * cos((1 + pan)*M_PI_4) *volume; return leftVolumn; } else if (type == KSYMCChannelTypeRight) { CGFloat rightVolumn = sqrt(2) * sin((1 + pan)*M_PI_4) *volume; return rightVolumn; } return 0; } @end ``` 这里的计算还是比较准确的. 经过测试 左侧 音量 为 0 时 右侧音量应该是 1.41左右 ## 总结 经过上述测试音频的 pan 值修改 如果自行开发还是比较好搞得,只是鄙人对音频的知识积累的太少了.这篇文章看起来虽然没什么技术含量,全当知识的点滴积累吧. 至于为啥 是 `M_PI_4`还请专研一下文章的扩展链接,因为要把一个线性的操作转换成一个圆型方便数学的计算,以及 **声像**和**声向**的区别. 全文完 URL: https://sunyazhou.com/2017/12/CellAddKVO/index.html.md Published At: 2017-12-15 17:05:10 +0000 # UICollectionViewCell添加KVO ![](/assets/images/20171215CellAddKVO/UICollectionViewCell.webp) # 前言 都一个多月没更新博客了,这一段时间太忙了. 这篇带来的分享内容是**如何正确的给一个`UICollectionViewCell`添加`KVO`监听**. ## 开篇 由于目前在开发[短视频](https://github.com/ksvc/KSYMediaEditorKit_iOS)相关的SDK,面向的多数都是小白开发者,为了能让小白以最低的成本看懂 SDK 的代码以及用法,这就要求我们以小白最容易理解的方式开发代码,比如最低级的`MVC`模式,最直白的`Objective-C`(老实说我都烦透了 OC 这种超级长看着都难受的编程语言,早想用 swift 来玩一把了),所以在开发的技术选型和代码编写过程中都是达到小白最低的理解能力的开发模式,但有时候不得不面对在*小白能理解*和*功能的高级实现*之间做妥协.最近开发遇到个问题,如下: > PM 有个需求 要实现在一个屏幕内多个 cell 上随意切换 录制视图并且能随意点击取消,再加上录制完成的视频如果不在选中状态就显示封面,如果在选中状态就继续预览,如果没有录制完的视频并且不在预览的 CELL要显示添加功能. 听完这个需求是不是都晕了,我们来看张我实现完成的图. ![](/assets/images/20171215CellAddKVO/RecordDemo.gif) 1. 录制完的视频取出封面 2. 正在预览的随时准备录制 3. 随意能切换 cell 不影响录制视图 4. 未录制的并且没有已录制完视频文件的 cell 显示 添加按钮 第一眼看着没啥技术含量都 UI 是吧 好我们来玩点有技术含量的 #### 问题1 如果使用传统MVC 模式的话`Cell`上边显示数据,那`model`里面是不是要放一个`record`的实例对象 告诉它 啥时候开始啥时候结束,当然你有更好的方式我就不说了我其实也知道. #### 问题2 取出封面很简单让 cell里面存储一下录制完的 URL 就可以了,然后每次调用 UICollectonView 的 `reload:`方法 #### 问题3 我们实现录制视图的方式是放在 cell 的一个 subview 上, 正在录制的视图如果 reload 的话 应该会瞬间没了.就算吭哧吭哧实现完开始录制、暂停录制、恢复录制、结束录制... 这活我觉得问题和隐患应该非常多.别想了 不能这么玩 #### 问题4 cell的选中和非选中问题,你有没有发现 如果正在录制的 cell 上的 view 是选中的有个红色的框代表当前属于 焦点状态. 那录制完成呢.是不是需要重新 reload cell 告诉它当前谁 选中 谁取消,如果点击的是同一个 cell 还要取反操作.如果正在预览是不是再次选中说明要停止预览显示加号或者封面图,想着想着你发现这玩意是个状态机.必须要想好 model 构造,要让model 的参数足够多去控制当前 cell 的选中状态、非选中状态、预览状态、非预览状态、录制状态、非录制状态、录完状态、停止录制状态... 想着想着 太麻烦了 于是我整理出一个状态机的表格 如下: | Cell Status | 当前cell显示内容 |其它 cell 显示内容| 点击当前 | 点击其它选中 | | :------: | :------: | :------: | :------: | :------: | | 无预览状态 | 显示加号/封面图 | 显示加号/或者封面 | 开始预览 | 切换预览视图 | | 正在预览状态 | 预览视频 | 显示加号/或者封面 | 显示加号/或者封面 | 切换预览视图 | | 正在录制状态 | 预览视频/播放视频 | (显示加号或播放视频)/(显示加号或预览视频) | 无操作(上锁) | 无操作(上锁) | > _这些不重要,有这个印象就行了不用仔细看_ 并不是我把问题复杂化,是 PM 的需求太复杂.不得不完整列出所有状态,精简,再精简,让小白开发者也能看懂的 SDK 才是好 SDK. 其实 其它的问题还有好多 我就不列出来了,好 现在我们来依次解决问题 **其实,综合上述信息来看,归根结底的原因是,实现这个录制随意切换功能等等的交互并不适用于`MVC`这种传统的玩法. 更像是一个`MVVM`的搞法**,于是我想到了 MVVM 里面的精髓所在.要用**数据驱动视图**. 上面的4个主要问题不就是因为 model 的状态修改了要通知 cell 变化嘛.那我们使用 model 的状态来控制 > 注意:_如果使用 MVVM 的玩法就不要再去调用 collectionview 的 reload:方法了_ 目前开发实现`MVVM`的方式主流两种 * RAC * KVO 显然`RAC`太大并不适用于我们 demo,用 KVO 搞一把.(代码有删减) #### 第一步定义 model ``` objc typedef void (^CompletionHandler)(UIImage * image); //取出 Image 给 Cell 显示的回调 typedef NS_ENUM(NSUInteger,KSYMultiCanvasModelStatus){ KSYMultiCanvasModelStatusNOPreview = 0,//无预览状态 KSYMultiCanvasModelStatusINPreview = 1,//正在预览状态 KSYMultiCanvasModelStatusRecording = 2 //正在录制状态 }; @interface KSYCanvasModel : NSObject @property (nonatomic, strong) NSURL *videoURL; //存放录制完视频 URL @property (nonatomic, assign) BOOL isSelected;//是否是选中 @property (nonatomic, assign) KSYMultiCanvasModelStatus modelStatus; //重要!!!:模型状态用它控制 cell 显示 - (void)gengrateImageBySize:(CGSize)size completionHandler:(CompletionHandler)handler; @end @interface KSYCanvasModel () @property(nonatomic, strong)AVAssetImageGenerator *imageGenerator; @end @implementation KSYCanvasModel - (void)gengrateImageBySize:(CGSize)size completionHandler:(CompletionHandler)handler{ if (self.videoURL == nil) { handler(nil); } AVURLAsset *asset = [AVURLAsset assetWithURL:self.videoURL]; self.imageGenerator = nil; self.imageGenerator = [AVAssetImageGenerator assetImageGeneratorWithAsset:asset]; self.imageGenerator.maximumSize = size; NSError *error=nil; CMTime time= kCMTimeZero;//CMTime是表示电影时间信息的结构体,第一个参数表示是视频第几秒,第二个参数表示每秒帧数.(如果要活的某一秒的第几帧可以使用CMTimeMake方法) CMTime actualTime; CGImageRef cgImage= [self.imageGenerator copyCGImageAtTime:time actualTime:&actualTime error:&error]; if(error){ NSLog(@"截取视频缩略图时发生错误,错误信息:%@",error.localizedDescription); handler(nil); return; } CMTimeShow(actualTime); UIImage *image = [UIImage imageWithCGImage:cgImage];//转化为UIImage CGImageRelease(cgImage); handler(image); } @end ``` ok model 大概是这样 .m 文件主要是从视频中取封面图 #### 第二步定义 cell ``` objc #import #import "KSYCanvasModel.h" static const NSString *KSYModelKVOStatusContext; static NSString *KSYKeyPathForModelStatus = @"modelStatus"; static NSString *KSYKeyPathForIsSelected = @"isSelected"; @interface KSYCanvasCell : UICollectionViewCell @property (weak, nonatomic) IBOutlet UIView *canvasImageView; @property (weak, nonatomic) IBOutlet UIImageView *addImageView; @property (weak, nonatomic) IBOutlet UIImageView *boundsView; @property (nonatomic, strong) KSYCanvasModel *model; //注册和移除观察接口 - (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context; - (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(void *)context; @end @interface KSYCanvasCell() // 使用 ObservableKeys 保存 keyPath 观察状态,避免重复注册和重复移除(重复移除会导致 crash) @property (nonatomic, strong) NSMutableSet *observableKeySets; @end @implementation KSYCanvasCell - (void)awakeFromNib { [super awakeFromNib]; //千万别把 KOV 监听写在这里 } //.,,此处省略了不太相关的代码 - (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context{ if ([self.observableKeySets containsObject:keyPath]) { return; } if (self.observableKeySets == nil) { self.observableKeySets = [NSMutableSet set]; } [self.observableKeySets addObject:keyPath]; [self.model addObserver:observer forKeyPath:keyPath options:options context:context]; } - (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(void *)context{ if (![self.observableKeySets containsObject:keyPath]) { return; } [self.model removeObserver:observer forKeyPath:keyPath context:context]; [self.observableKeySets removeObject:keyPath]; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{ if ([KSYKeyPathForModelStatus isEqualToString:keyPath]) { KSYMultiCanvasModelStatus modelStatus = [[change objectForKey:NSKeyValueChangeNewKey] integerValue]; NSLog(@"当前状态:%zd",modelStatus); //拿到模型状态然后做适当的处理 } else if([KSYKeyPathForIsSelected isEqualToString:keyPath]){ //处理是否显示边框 } } @end ``` > 这里要在.h 里面复写 下面这俩个方法 因为要再 ViewController 里面拿到 cell 调用这个方法 * `addObserver:forKeyPath:options:context:` 这个方法是系统方法需要复写并对外暴露接口 * `removeObserver:forKeyPath:context:` 这个方法是系统方法需要复写并对外暴露接口 这里定义了一个上下文对象用于找到识别这个在 cell的监听还有两个要监听的属性(KSYCanvasCell.h 顶部) ``` objc static const NSString *KSYModelKVOStatusContext; static NSString *KSYKeyPathForModelStatus = @"modelStatus"; static NSString *KSYKeyPathForIsSelected = @"isSelected"; ``` > 注意:**为了防止 cell 重复注册导致复用的时候崩溃,这里用`NSMutableSet`让 model 的观察者只注册一次** ``` objc @interface KSYCanvasCell() // 使用 ObservableKeys 保存 keyPath 观察状态,避免重复注册和重复移除(重复移除会导致 crash) @property (nonatomic, strong) NSMutableSet *observableKeySets; @end ``` 添加的时候做一次check ``` objc - (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context{ if ([self.observableKeySets containsObject:keyPath]) { return; } if (self.observableKeySets == nil) { self.observableKeySets = [NSMutableSet set]; } [self.observableKeySets addObject:keyPath]; ... } ``` 移除的时候要做一次 check ``` objc - (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(void *)context{ if (![self.observableKeySets containsObject:keyPath]) { return; } [self.model removeObserver:observer forKeyPath:keyPath context:context]; [self.observableKeySets removeObject:keyPath]; } ``` ok cell 大概是这个意思 #### 第三步在 ViewController里面 适当的位置 注册/移除监听 并在ViewController控制器的生命周期内也做好相关监听的移除和添加 这里我们需要实现`UICollectionViewDelegate`的代理协议来调用 cell 的添加 cell 和移除 cell ``` objc - (void)collectionView:(UICollectionView *)collectionView willDisplayCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath{ KSYCanvasCell *canvasCell = (KSYCanvasCell *)cell; [canvasCell addObserver:canvasCell forKeyPath:KSYKeyPathForModelStatus options:NSKeyValueObservingOptionNew context:&KSYModelKVOStatusContext]; [canvasCell addObserver:canvasCell forKeyPath:KSYKeyPathForIsSelected options:NSKeyValueObservingOptionNew context:&KSYModelKVOStatusContext]; } - (void)collectionView:(UICollectionView *)collectionView didEndDisplayingCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath{ KSYCanvasCell *canvasCell = (KSYCanvasCell *)cell; //状态变化 [canvasCell removeObserver:canvasCell forKeyPath:KSYKeyPathForModelStatus context:&KSYModelKVOStatusContext]; //选中变化 [canvasCell removeObserver:canvasCell forKeyPath:KSYKeyPathForIsSelected context:&KSYModelKVOStatusContext]; } ``` 你是不是会问为啥写这 我来告诉我我遇到的一个坑 如果你在 下面的方法里写注册 后果不堪设想,因为 cell 是复用的,每次复写 KVO 都是在创建新的对象 ``` objc - (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{ KSYCanvasCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:[KSYCanvasCell className] forIndexPath:indexPath]; cell.model = [self.models objectAtIndex:indexPath.row]; //如果写在这里 return cell; } ``` KVO的实现原理很简单,就是把这个对象的监听属性在底层复写一下,监听两个值之间的变化.KVO 原理相关的就不多废话了,这都是家常便饭了 我一开始写在了 cell 的 awakeFromNib: 因为都是 cell 拖拽的控件,但是麻烦真是接踵而至,各种崩溃 ``` objc - (void)awakeFromNib { [super awakeFromNib]; //别写在这里 } ``` 如果你不信邪可以试试. 最后我们在控制器的适当位置修改 model 的状态这样就做到了实时更新 cell ``` objc - (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath{ //------------处理点击----------- KSYCanvasModel *lastModel = [self.models objectAtIndex:self.lastSelectedIndexPath.row]; KSYCanvasModel *selectedModel = [self.models objectAtIndex:indexPath.row]; BOOL clickSameCell = (self.lastSelectedIndexPath == indexPath); if (clickSameCell) { //选择同一个cell selectedModel.isSelected = !selectedModel.isSelected; } else { lastModel.isSelected = NO; selectedModel.isSelected = YES; } selectedModel.modelStatus = KSYMultiCanvasModelStatusRecording; //这就会出发 cell的 KVO 了 } ``` 最后别忘了在ViewController的生命周期添加和移出观察者 ``` objc - (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; [self.canvasCollectionView.visibleCells enumerateObjectsUsingBlock:^(KSYCanvasCell *cell, NSUInteger idx, BOOL * _Nonnull stop) { [cell removeObserver:cell forKeyPath:KSYKeyPathForModelStatus context:&KSYModelKVOStatusContext]; }]; } ``` 这样的实现过程就 解决了 上边提到的 问题 1、2、3、4 这也是最精简的实现方式,以小白的开发视角 来看也需要熟悉一点 MVVM 了.这都是成了 iOS 最标配了. ## 总结 这种偏向MVVM模式开发的方式 我个人觉得还是不错的,虽然现在各种MVVM格式早已经烂大街了,但只要想起来,用起来,能用简单直白的方式解决问题,它就是好的开发设计模式.当然本章也主要讲了一些技巧而已,不足之处还请各位指正. demo我就不写了 可以参考[我们的短视频 demo](https://github.com/ksvc/KSYMediaEditorKit_iOS) multicanvas target 全文完 URL: https://sunyazhou.com/2017/10/ElegantPresentTransition/index.html.md Published At: 2017-10-31 11:32:17 +0000 # 论一个优雅的模态转场的自我修养 # 前言 在开发过程中虽然 UI 这个活很没技术含量,但有时候还是需要做些的特别的转场效果.本教程参考[UIPresentationController Tutorial: Getting Started](https://www.raywenderlich.com/139277/uipresentationcontroller-tutorial-getting-started) 这篇博文进行 OC 版翻译 也加入了一些小改动 > swift 点这里下载[demo](https://koenig-media.raywenderlich.com/uploads/2016/08/Medal_Count_Completed.zip) > Objective-C 点击这里直达[demo](https://github.com/sunyazhou13/SlideInPresentation) ![](/assets/images/20171031ElegantPresentTransition/ElegantPresentTransition.gif) ### 需求背景 转场对目前的 iOS 来讲已经不能再熟悉了 但想找个靠谱点的带遮盖的转场 没找到几个靠谱的 不是这个问题不行就那个问题不能满足 ![](/assets/images/20171031ElegantPresentTransition/demo1.webp) 根据`Raywenderrich`的教程 我翻译成了 OC 版本 并加了一些小改动 ### 如何使用 * 导入头文件 ``` objc #import "SlideInPresentationManager.h" ``` * 声明属性 ``` objc @property (nonatomic, strong) SlideInPresentationManager *slideInTransitioningDelegate; ``` * 弹出模态控制器的时候如下代码 ``` objc - (IBAction)presentAction:(UIButton *)sender { PresentationDirection direction; if (sender.tag == 100) { NSLog(@"左侧弹出模态转场"); direction = PresentationDirectionLeft; } else if (sender.tag == 101) { NSLog(@"上弹出模态转场"); direction = PresentationDirectionTop; } else if (sender.tag == 102) { NSLog(@"右弹出模态转场"); direction = PresentationDirectionRight; } else { NSLog(@"下弹出模态转场"); direction = PresentationDirectionBottom; } self.slideInTransitioningDelegate = nil; //控制现实遮盖的视图转场(core 代码) self.slideInTransitioningDelegate = [[SlideInPresentationManager alloc] init]; self.slideInTransitioningDelegate.direction = direction; self.slideInTransitioningDelegate.disableCompactHeight = NO; self.slideInTransitioningDelegate.sliderRate = 1.0/3.0; //创建控制器实例 PresentController *presentVC = [[PresentController alloc] initWithNibName:@"PresentController" bundle:[NSBundle mainBundle]]; presentVC.transitioningDelegate = self.slideInTransitioningDelegate; presentVC.modalPresentationStyle = UIModalPresentationCustom; [self presentViewController:presentVC animated:YES completion:nil]; } ``` 剩下的就可以愉快的玩耍了 URL: https://sunyazhou.com/2017/10/MarkdownSkill/index.html.md Published At: 2017-10-25 16:10:35 +0000 # markdown嵌入折叠标签 # 前言 > 这几天一直在开十九大,导致我的梯子翻墙不好使了,就在此时[喵神发表了一篇博文 关于 Swift Error 的分类](https://onevcat.com/2017/10/swift-error-category/) 每次看喵神的文章就像诸葛亮跟周瑜聊天一样如饮美酒,我不能自比诸葛孔明和周公瑾. 当我仔细看喵神博客的时候发现 原来`markdown`支持很多`html`标签的小技巧 比如: ![喵神文章中的](/assets/images/20171025MarkdownSkill/MarkdownSkill.gif) 第一眼我震撼了 原来 markdown 里面还能嵌入这么多好玩的 就这个问题 问了一下喵神 ![与喵神对话](/assets/images/20171025MarkdownSkill/MarkdownQuestion.webp) [简单的 summary tag 而已..](https://www.w3schools.com/tags/tag_summary.asp) 于是我测试了一下代码 ``` html
点击时的区域标题

- 测试 测试测试

测试二 测试三 。。。。。 .

``` 下面我们来玩一下试试
这是孙先生的博客 点击查看更多内容.

666666 昨天程序员节 是不是被 PM 虐了 QA 提个很多 bug 不想 fix.

昨天一不小心驾照考下来了 耗时2个月 快不快。。。。.

还可以嵌入图片
书法

``` html
书法

``` OK 上边就是我们用到的几行代码 很简单直接嵌入 markdown 编辑器里面就马上出效果 感谢[喵神的指导](https://onevcat.com/) 更多标签相关 可参考[w3schools](https://www.w3schools.com/tags/tag_summary.asp) markdown如何改字体颜色, 如下代码: 黑体带颜色 ``` html 黑体带颜色 ``` 20201012更新 全文完 URL: https://sunyazhou.com/2017/10/UIViewRendering/index.html.md Published At: 2017-10-16 13:00:30 +0000 # 理解UIView的绘制 ![UIView渲染](/assets/images/20171016UIViewRendering/UIViewRendering.webp) # 前言 最近研究OpenGL ES相关和 GPU 相关 发现这篇文章很具有参考的入门价值. ### 理解 UIView 的绘制, UIView 是如何显示到 Screen 上的? 首先要从`Runloop`开始说,iOS 的`MainRunloop` 是一个60fps 的回调,也就是说16.7ms(毫秒)会绘制一次屏幕,这个时间段内要完成: * `view`的缓冲区创建 * `view`内容的绘制(如果重写了 drawRect) 这些 `CPU`的工作. 然后将这个缓冲区交给`GPU`渲染, 这个过程又包含: * 多个`view`的拼接(compositing) * 纹理的渲染(Texture)等. 最终现实在屏幕上.因此,如果在16.7ms 内完不成这些操作, eg: CPU做了太多的工作, 或者`view`层次过于多,图片过于大,导致`GPU`压力太大,就会导致"卡"的现象,也就是 **丢帧**,**掉帧**. 苹果官方给出的最佳帧率是:__60fps__(60Hz),也就是一帧不丢, 当然这是理想中的绝佳体验. ### 这个`60fps`该怎么理解呢? 一般来说如果帧率达到 `60+fps`(fps >= 60帧以上,如果帧率fps > 50,人眼就基本感觉不到卡顿了,因此,如果你能让你的 iOS 程序__稳定__保持在`60fps`已经很不错了, 注释,是"稳定"在60fps,而不是, `10fps`,`40fps`,`20fps`这样的跳动,如果帧频不稳就会有卡的感觉,`60fps`真的很难达到, 尤其是在 iPhone 4/4s等 32bit 位机上,不过现在苹果已经全面放弃32位,支持最低64位会好很多. > fps 代表的是刷新频率,单位赫兹Hz,因为电子工程中考虑到能耗和视觉以及其它方面,60Hz是一个比较理想的刷新频率,所以家用电器也经常会出现60Hz的字样. > 视频中帧率FPS >= 25 才不会人眼察觉有卡顿,因为视频中视频模糊视频中的i p b帧能够给予前后帧一些需要的像素信息方便GPU的离屏渲染,GPU的索引可以节省很多性能. 总的来说, UIView从绘制到Render的过程有如下几步: * 每一个`UIView`都有一个`layer` * 每一个`layer`都有个`content`,这个`content`指向的是一块缓存,叫做__`backing store`__. `UIView`的绘制和渲染是两个过程: * 当`UIView`被绘制时,CPU执行`drawRect`,通过`context`将数据写入__`backing store`__ * 当__`backing store`__写完后,通过render server交给GPU去渲染,将backing store中的bitmap数据显示在屏幕上. 上面提到的从`CPU`到`GPU`的过程可用下图表示: ![](/assets/images/20171016UIViewRendering/CPUToGPU.webp) 下面具体来讨论下这个过程 * CPU bound: 假设我们创建一个 UILabel ``` objc UILabel* label = [[UILabel alloc]initWithFrame:CGRectMake(10, 50, 300, 14)]; label.backgroundColor = [UIColor whiteColor]; label.font = [UIFont systemFontOfSize:14.0f]; label.text = @"test"; [self.view addSubview:label]; ``` 这个时候不会发生任何操作, 由于 UILabel 重写了`drawRect`方法,因此,这个 `View`会被 `marked as "dirty"`: 类似这个样子: ![](/assets/images/20171016UIViewRendering/DrawRect.webp) 然后一个新的`Runloop`到来,上面说道在这个`Runloop`中需要将界面渲染上去,对于`UIKit`的渲染,Apple用的是它的`Core Animation`。 做法是在Runloop开始的时候调用: ``` objc [CATransaction begin] ``` 在`Runloop`结束的时候调用 ``` objc [CATransaction commit] ``` 在`begin`和`commit`之间做的事情是将`view`增加到`view hierarchy`中,这个时候也不会发生任何绘制的操作。 当`[CATransaction commit]`执行完后,`CPU`开始绘制这个`view`: ![CPU绘制图](/assets/images/20171016UIViewRendering/CATransactionCommit.webp) 首先`CPU`会为`layer`分配一块内存用来绘制`bitmap`,叫做__`backing store`__ 创建指向这块`bitmap`缓冲区的指针,叫做`CGContextRef` 通过`Core Graphic`的`api`,也叫`Quartz2D`,绘制`bitmap` 将`layer`的`content`指向生成的`bitmap` 清空`dirty flag`标记 这样`CPU`的绘制基本上就完成了. 通过`time profiler`可以完整的看到个过程: ``` sh Running Time Self Symbol Name 2.0ms 1.2% 0.0 +[CATransaction flush] 2.0ms 1.2% 0.0 CA::Transaction::commit() 2.0ms 1.2% 0.0 CA::Context::commit_transaction(CA::Transaction*) 1.0ms 0.6% 0.0 CA::Layer::layout_and_display_if_needed(CA::Transaction*) 1.0ms 0.6% 0.0 CA::Layer::display_if_needed(CA::Transaction*) 1.0ms 0.6% 0.0 -[CALayer display] 1.0ms 0.6% 0.0 CA::Layer::display() 1.0ms 0.6% 0.0 -[CALayer _display] 1.0ms 0.6% 0.0 CA::Layer::display_() 1.0ms 0.6% 0.0 CABackingStoreUpdate_ 1.0ms 0.6% 0.0 backing_callback(CGContext*, void*) 1.0ms 0.6% 0.0 -[CALayer drawInContext:] 1.0ms 0.6% 0.0 -[UIView(CALayerDelegate) drawLayer:inContext:] 1.0ms 0.6% 0.0 -[UILabel drawRect:] 1.0ms 0.6% 0.0 -[UILabel drawTextInRect:] ``` 假如某个时刻修改了`label`的`text`: ``` objc label.text = @"hello world"; ``` 由于内容变了,`layer`的`content`的`bitmap`的尺寸也要变化,因此这个时候当新的`Runloop`到来时,`CPU`要为`layer`重新创建一个`backing store`,重新绘制`bitmap`. `CPU`这一块最耗时的地方往往在`Core Graphic`的绘制上,关于`Core Graphic`的性能优化是另一个话题了,又会牵扯到很多东西,就不在这里讨论了. GPU bound: `CPU`完成了它的任务:将`view`变成了`bitmap`,然后就是`GPU`的工作了,`GPU`处理的单位是`Texture`. 基本上我们控制`GPU`都是通过`OpenGL`来完成的,但是从`bitmap`到`Texture`之间需要一座桥梁,`Core Animation`正好充当了这个角色: `Core Animation`对`OpenGL`的`api`有一层封装,当我们要渲染的`layer`已经有了`bitmap content`的时候,这个`content`一般来说是一个`CGImageRef`,`CoreAnimation`会创建一个`OpenGL`的`Texture`并将`CGImageRef(bitmap)`和这个`Texture`绑定,通过`TextureID`来标识。 这个对应关系建立起来之后,剩下的任务就是`GPU`如何将`Texture`渲染到屏幕上了。 `GPU`大致的工作模式如下: ![](/assets/images/20171016UIViewRendering/GPUWorkflow.webp) 整个过程也就是一件事: `CPU`将准备好的`bitmap`放到`RAM`里,`GPU`去搬这快内存到`VRAM`中处理。 而这个过程`GPU`所能承受的极限大概在16.7ms完成一帧的处理,所以最开始提到的60fps其实就是GPU能处理的最高频率. 因此,`GPU`的挑战有两个: * 将数据从`RAM`搬到`VRAM`中 * 将`Texture`渲染到屏幕上 这两个中瓶颈基本在第二点上。渲染`Texture`基本要处理这么几个问题: * Compositing: `Compositing`是指将多个纹理拼到一起的过程,对应`UIKit`,是指处理多个`view`合到一起的情况,如: ``` objc [self.view addsubview : subview]。 ``` 如果`view`之间没有叠加,那么`GPU`只需要做普通渲染即可. 如果多个`view`之间有叠加部分,`GPU`需要做`blending`. 加入两个`view`大小相同,一个叠加在另一个上面,那么计算公式如下: `R` = `S`+`D`*(`1`-`Sa`) > `R`: 为最终的像素值 > `S`: 代表 上面的Texture(Top Texture) > `D`: 代表下面的Texture(lower Texture) 其中`S`,`D`都已经`pre-multiplied`各自的`alpha`值。 `Sa`代表`Texture`的`alpha`值。 假如`Top Texture`(上层`view`)的`alpha`值为`1`,即不透明。那么它会遮住下层的`Texture`. 即,`R` = `S`。是合理的。 假如`Top Texture`(上层`view`)的`alpha`值为`0.5`, `S`为`(1,0,0)`,乘以`alpha`后为`(0.5,0,0)`。 `D`为`(0,0,1)`。 得到的`R`为`(0.5,0,0.5)`。 基本上每个像素点都需要这么计算一次。 因此,`view`的层级很复杂,或者`view`都是半透明的(`alpha`值不为`1`)都会带来`GPU`额外的计算工作。 * Size 这个问题,主要是处理`image`带来的,假如内存里有一张`400x400`的图片,要放到`100x100`的`imageview`里,如果不做任何处理,直接丢进去,问题就大了,这意味着,`GPU`需要对大图进行缩放到小的区域显示,需要做像素点的`sampling`,这种`smapling`的代价很高,又需要兼顾`pixel alignment`。 计算量会飙升。 * Offscreen Rendering And Mask 如果我们对`layer`做这样的操作: ``` objc label.layer.cornerRadius = 5.0f; label.layer.masksToBounds = YES; ``` 会产生`offscreen rendering`,它带来的最大的问题是,当渲染这样的`layer`的时候,需要额外开辟内存,绘制好`radius,mask`,然后再将绘制好的`bitmap`重新赋值给`layer`。 因此继续性能的考虑,`Quartz`提供了优化的`api`: ``` objc label.layer.cornerRadius = 5.0f; label.layer.masksToBounds = YES; label.layer.shouldRasterize = YES; label.layer.rasterizationScale = label.layer.contentsScale; ``` 简单的说,这是一种`cache`机制。 同样`GPU`的性能也可以通过`instrument`去衡量: ![](/assets/images/20171016UIViewRendering/RenderingResult.webp) 红色代表`GPU`需要做额外的工作来渲染`View`,绿色代表`GPU`无需做额外的工作来处理`bitmap`。 全文完 URL: https://sunyazhou.com/2017/09/DeviceCheck/index.html.md Published At: 2017-09-30 09:45:25 +0000 # DeviceCheck ![](/assets/images/20170930DeviceCheck/DeviceCheck.webp) # 前言 iOS11 苹果改动了一个比较引开发者关注的亮点 **UDID之类的写到系统 keychain 的唯一标识会随着 app 删除而删除** 这个问题在微博上已经争论好几天 ### iOS11新的设备唯一标识 DCDevice #### 介绍 API 我们首先看看`DCDevice`类都有啥 ``` objc #import NS_ASSUME_NONNULL_BEGIN API_AVAILABLE(ios(11.0), tvos(11.0)) API_UNAVAILABLE(watchos, macos) @interface DCDevice : NSObject //当前设备 @property (class, readonly) DCDevice *currentDevice; //是否支持 @property (getter=isSupported, readonly) BOOL supported; //生成唯一标识的 token 注意:每call一次就会生成一个新的 token(和前边不同) - (void)generateTokenWithCompletionHandler:(void(^)(NSData * _Nullable token, NSError * _Nullable error))completion; @end NS_ASSUME_NONNULL_END ``` 接口简直不能再简单了 **创建实例调方法** #### 使用 API 下面我们来看下如何使用`DCDevice` 导入头文件 ``` objc #import ``` check 是否支持 如果支持 的话会在回调以后返回 `token`(NSData) ``` objc - (void)viewDidLoad { [super viewDidLoad]; //下面是调用代码 if([DCDevice currentDevice].supported){ [[DCDevice currentDevice] generateTokenWithCompletionHandler:^(NSData * _Nullable token, NSError * _Nullable error) { NSLog(@"%@",token); }]; } } ``` token 是个 2188字节(2k 多点)的二进制流,很小 ![](/assets/images/20170930DeviceCheck/DCDeviceCode.webp) 我尝试各种字符串编码最终也不知道里面是啥 没能成功打印出来 ![](/assets/images/20170930DeviceCheck/DCDeviceBinary.webp) 谁要是打印出来烦请 share 一下 #### 删除/重装App 如何处理 > DeviceCheck 允许你通过你的服务器与 Apple 服务器通讯,并为单个设备设置2k左右 的数据。 在设备上用 DeviceCheck API 生成一个 2字节的 token (00, 01,10,11),然后将这个 token 发给自己的服务器,再由自己的服务器与 Apple 的 API 进行通讯,来更新或者查询该设备的值。这两字节 的数据用来追踪用户。比如。借助两个自己的数据,你可以得知用户究竟使用了该 App 多久。 该 API 可以成为:反欺诈领域: > 试用7天 Uber、滴滴司机被封号后,防止重新注册账号接单 该用户是否已经领取过首次注册红包 APP防多开 因为传输的是 flag 级别的数据,并不会定位到该设备的使用者,所以相对安全。 > 但是对于购买了二手手机的使用场景,可能会出现一些边界情况,这个在业务中也需要考虑进去。 引自[iOS11开发新特性之实用小tips](https://github.com/ChenYilong/iOS11AdaptationTips/issues/22) 首先要明白我们 的 token 需要发给谁 1. token 需要发送给我们自己公司的`server`做记录 2. 我们公司自己的`server`去`Apple`的`server`查询`token`是否有效,从而来更新或者查询该设备值. 3. 这`2k 左右的 token`不会因为设备删除 app 而删除 会一直存在苹果的 server(其实我觉得就是苹果自己去获取的设备唯一标识). 那么 怎么查询和更新呢 ##### 查询接口 **https://api.development.devicecheck.apple.com/v1/query_two_bits** 可以用终端自己模拟一下 就当作你自己是自己的服务器访问Apple 的服务器 ``` sh curl -i --verbose -H "Authorization: Bearer " \ -X POST --data-binary @ValidQueryRequest.json \ https://api.development.devicecheck.apple.com/v1/query_two_bits ``` json 的定义如下: | 字段 key | 类型 | 说明 | 必须 | |:------:|:------:| :------:| :------:| | device_token | String | 设备唯一标识 token| 是 | | transaction_id | String | 服务器产生的一个ID| 是 | | timestamp | Long | 服务器生成的UTC时间戳| 是 | 它会 返回 如下格式 ``` json { "device_token" : "wlkCDA2Hy/CfrMqVAShs1BAR/0sAiuRIUm5jQg0a..." "transaction_id" : "5b737ca6-a4c7-488e-b928-8452960c4be9", "timestamp" : 1487716472000 } ``` ##### 更新接口 **https://api.development.devicecheck.apple.com/v1/update_two_bits** ``` sh curl -i --verbose -H "Authorization: Bearer " \ -X POST --data-binary @ValidUpdateRequest.json \ https://api.development.devicecheck.apple.com/v1/update_two_bits ``` json 的定义如下: | 字段 key | 类型 | 说明 | 必须 | |:------:|:------:| :------:| :------:| | device_token | String | 设备唯一标识 token| 是 | | transaction_id | String | 服务器产生的一个ID| 是 | | timestamp | Long | 服务器生成的UTC时间戳| 是 | | bit0 | Boolean | 新的布尔值1| 否 | | bit1 | Boolean | 新的布尔值2| 否 | json 的示例: ``` json { "device_token" : "wlkCDA2Hy/CfrMqVAShs1BAR/0sAiuRIUm5jQg0a..." "transaction_id" : "5b737ca6-a4c7-488e-b928-8452960c4be9", "timestamp" : 1487716472000, "bit0" : true, "bit1" : false } ``` ### 最终的方案 1. iOS11以前版本暂且才用 UUID 等 keychian 方式 2. iOS11尽量才用新的 api 来适配解决 对于 server 来讲可以 把 token 搞成新的附属字段 比如一个账号下登录多少个设备 那么 一个 UID 下面 就要附属 iOS 版本+ token 相信过不了多久 很成熟的 token方案会脱颖而出 *如果本文有误之处还请各路大神指教* 全文完 URL: https://sunyazhou.com/2017/09/MarkdownTable/index.html.md Published At: 2017-09-29 18:01:08 +0000 # Markdown插入表格语法 # 前言 ![](/assets/images/20170929MarkdownTable/table.webp) 以前总用 markdown 插入表格不成功 这篇分享一下 markdown 如何插入表格 demo: * 普通样式 ``` | 一个普通标题 | 一个普通标题 | 一个普通标题 | | ------| ------ | ------ | | 短文本 | 中等文本 | 稍微长一点的文本 | | 稍微长一点的文本 | 短文本 | 中等文本 | ``` 现实效果是这样的 | 一个普通标题 | 一个普通标题 | 一个普通标题 | | ------| ------ | ------ | | 短文本 | 中等文本 | 稍微长一点的文本 | | 稍微长一点的文本 | 短文本 | 中等文本 | 对齐 ``` | 左对齐标题 | 右对齐标题 | 居中对齐标题 | | :------| ------: | :------: | | 短文本 | 中等文本 | 稍微长一点的文本 | | 稍微长一点的文本 | 短文本 | 中等文本 | ``` 效果是这样的 | 左对齐标题 | 右对齐标题 | 居中对齐标题 | | :------| ------: | :------: | | 短文本 | 中等文本 | 稍微长一点的文本 | | 稍微长一点的文本 | 短文本 | 中等文本 | 语法说明: * `|`、`-`、`:`、 之间多余的空格会被忽略,不影响布局. * 默认标题栏**居中对齐**, 内容居左对齐. * `-:`表示内容和标题栏居右对齐. * `:-`表示内容和标题栏居左对齐. * `:-:`表示内容和标题栏居中对齐. * **内容**和`|`之间的多余空格会被忽略. * 每行第一个`|`和最后一个`|`可以省略. * `-`的数量至少有一个. 每次总忘记 markdown 表格怎么写 如果你需要 加一列 可以复制粘贴前边`|`和 下边的`|:-----:|` 我用的是开源免费的 [MacDown](https://github.com/MacDownApp/macdown/releases) 大家可以自行下载,平常发表文章都用这个. 全文完 URL: https://sunyazhou.com/2017/09/CharlesCaptureHttps/index.html.md Published At: 2017-09-01 23:17:09 +0000 # 如何使用Charles截获https请求 # 前言 ![](/assets/images/20170901CharlesCaptureHttps/CharlesAlbum.webp) 如何使用`charles`在iOS设备上截获`https`的请求 ## 1.安装Charles [官网下载](https://www.charlesproxy.com/download/)就行了 至于破解之类的 自行google吧 我这里使用的是Charles 4.1.3版本 目前应该是最新的 ## 2.HTTP抓包配置 #### (1) 查看电脑IP ![](/assets/images/20170901CharlesCaptureHttps/WiFiIpmac.webp) #### (2) 设置手机HTTP代理 手机连上电脑,点击“设置->无线局域网->连接的WiFi”,设置HTTP代理: 服务器为电脑IP地址:如192.168.1.108 端口:8888 ![](/assets/images/20170901CharlesCaptureHttps/WiFiIpPortiPhone.webp) 注意:*这里用我自己电脑的IP举例 红色区域 记得替换成你自己的电脑的IP* 设置代理后,需在在电脑上打开Charles,这个时候 如果手机有请求就会弹出如下图: ![](/assets/images/20170901CharlesCaptureHttps/CharlesAllow.webp) 点击**Allow** 就可以了 ### 3. HTTPS抓包 左上角菜单中 选择`SSL Proxying Settings` ![](/assets/images/20170901CharlesCaptureHttps/CharlesStep1.webp) 然后 勾选`Enable SSL Proxying` 紧接着点击 `Add` ![](/assets/images/20170901CharlesCaptureHttps/CharlesStep2.webp) 再然后在 `Host`: 输入 `*` 代表通配所有 如果你要截获 比如 *.baidu.com那就写上 `Port`: 443 默认端口 填完 点击OK ![](/assets/images/20170901CharlesCaptureHttps/CharlesStep3.webp) 紧接着 点击`Help` -> `SSL Proxying` -> 安装根证书 ![](/assets/images/20170901CharlesCaptureHttps/CharlesStep4.webp) 安装到钥匙串后 点击charles的root证书 选择 `使用信任` ![](/assets/images/20170901CharlesCaptureHttps/CharlesCerRootMac.webp) 下一步 是安装手机的root证书 ![](/assets/images/20170901CharlesCaptureHttps/CharlesStep6.webp) 这时 需要在 设置 代理ip的手机上 (iPhone上)用 Safari 直接打开网址: [chls.pro/ssl](chls.pro/ssl) 此时手机一会儿就弹出这样的 提示 点击**允许** ![](/assets/images/20170901CharlesCaptureHttps/iPhone1.webp) 然后 安装证书 ![](/assets/images/20170901CharlesCaptureHttps/iPhone2.webp) 安装完 最后一步**非常重要** __必须到 通用->关于本机->证书信任设置__去信任 证书 ![](/assets/images/20170901CharlesCaptureHttps/iPhone3.webp) 如果不信任 就会抓取的时候出现下图这样的问题 ![](/assets/images/20170901CharlesCaptureHttps/CharlesRootCerError.webp) > 注意:*iOS10.3以上版本 貌似才需要* 最后放上一张截获成功的图 (支付宝的接口) ![](/assets/images/20170901CharlesCaptureHttps/Result.webp) 全文完 URL: https://sunyazhou.com/2017/08/ARKit/index.html.md Published At: 2017-08-30 14:50:43 +0000 # ARKit # 前言 ![](/assets/images/20170830ARKit/ARKitPreview.webp) 本篇会从广泛介绍到详细介绍,也就是从粗粒度向细粒度逐渐过度讲解. 期间有任何问题请大家集思广益,多多指教. # 主要内容 * __AR技术介绍__ * __ARKit工作原理及流程介绍__ * __ARKit简单代码实现__ * __ARKit框架所有API介绍__ * __ARScnView介绍__ * __ARSession介绍__ * __ARCamera介绍__ * __ARKit捕捉平地__ * __AR代码demo实现__ ## AR技术介绍 #### AR技术简介 * 增强现实技术(Augmented Reality,简称 AR),是一种实时的计算摄影机影像的位置及角度并加上相应图像、视频、3D模型的技术,这种技术的目标是在屏幕上把虚拟世界套在现实世界并进行互动 * AR场景实现技术要素 1. 多媒体捕捉现实图像:如摄像头 2. 三维建模:3D立体模型 3. 传感器追踪:主要追踪现实世界动态物体的六轴变化,这六轴分别是X、Y、Z轴位移及旋转。其中位移三轴决定物体的方位和大小,旋转三轴决定物体显示的区域 4. 坐标识别及转换:3D模型显示在现实图像中不是单纯的frame坐标点,而是一个三维的矩阵坐标 5. AR还可以与虚拟物体进行一些交互(optional) ![](/assets/images/20170830ARKit/ARDirection.webp) #### ARKit概述及特点 * `ARKit` 框架(framework)提供 两种 AR 技术 * __基于3D 场景(`SceneKit`) 实现的增强现实__ * __基于2D 场景(`SpriktKit`) 实现的增强现实__ > 一般主流都是基于3D 实现,`ARkit`兼容 `SceneKit`和 `SpriktKit` (苹果推出游戏引擎)framework * 显示AR 效果为什么依赖 __3D引擎`SceneKit`和 2D 引擎`SpriktKit` 苹果的游戏引擎框架? 原因是游戏引擎才可以加载物体模型。__ > 虽然`ARKit`中的视图**(`ARSCNView`)继承自`SCNView`,`SCNView`继承自`UIView`**, > 但是目前`ARKit `框架本身只包含相机追踪, 不能直接加载物体模型, 所以只能依赖游戏引擎加载 ARKit (我觉得苹果是在充分利用和整合现有资源并推广自己的 framework 一石二鸟) 因为就目前我没有看到 任何`ARKit`支持 `Unity3D`或者 `Cocoas2D` #### iOS11 如何支持`ARKit` iOS11虽然推出了 `ARKit` 但不是所有 iOS11系统都支持 * 必须 CPU A9以上(iPhone 6s 以上 还有 iPhone SE) 开发环境 * Xcode版本:Xcode9及以上 * 系统: iOS11及以上 * iOS设备:处理器A9及 以上(6S机型及以上) * macOS系统: 10.12.4及以上 #### Xcode 自带创建模板(多数都不用) ![](/assets/images/20170830ARKit/ARKitModel.webp) demo 演示 ``` objc @interface ViewController () //AR视图:展示3D界面 @property (nonatomic, strong) IBOutlet ARSCNView *sceneView; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; //场景代理 self.sceneView.delegate = self; // 显示帧率 self.sceneView.showsStatistics = YES; // 创建一个场景 SCNScene *scene = [SCNScene sceneNamed:@"art.scnassets/ship.scn"]; // Set the scene to the view self.sceneView.scene = scene; } - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; // 世界坐标系配置 ARWorldTrackingSessionConfiguration *configuration = [ARWorldTrackingSessionConfiguration new]; // 运行3D 场景的 会话 [self.sceneView.session runWithConfiguration:configuration]; } - (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; // 暂停 [self.sceneView.session pause]; } - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; // Release any cached data, images, etc that aren't in use. } #pragma mark - ARSCNViewDelegate //给当前锚点创建一个节点 (node 可以理解成 UIView) /* // Override to create and configure nodes for anchors added to the view's session. - (SCNNode *)renderer:(id)renderer nodeForAnchor:(ARAnchor *)anchor { SCNNode *node = [SCNNode new]; // Add geometry to the node... return node; } */ //会话有错误 - (void)session:(ARSession *)session didFailWithError:(NSError *)error { // Present an error message to the user } //被打断 类似音频播放的时候被打断 eg:前后台切换 来电话 siri - (void)sessionWasInterrupted:(ARSession *)session { // Inform the user that the session has been interrupted, for example, by presenting an overlay } //会话结束 - (void)sessionInterruptionEnded:(ARSession *)session { // Reset tracking and/or remove existing anchors if consistent tracking is required } ``` ## ARKit工作原理及流程介绍 ### 主要内容 ![](/assets/images/20170830ARKit/RenderingARKit.webp) `ARKit` 并不是一个独立运行的框架 必须要配合`SceneKit`,没有`SceneKit` `ARKit`和普通相机没有任何区别 > _难点:3D坐标的矩阵转换_ (3D X/Y/Z, 4x4坐标) 下面是一张图 ARKit 的所有.h 头文件 (没有列出派生的子类) ![](/assets/images/20170830ARKit/ARDefines.webp) * ARKit框架中的核心类 * __`ARScnView`__ * __`ARSession`__ * __`ARCamera`__ * `ARKit`demo 示例 实现 * 捕捉平面 添加物体 * AR虚拟增强现实就是指在相机捕捉到的现实世界图像中显示一个虚拟的3D模型,这个过程可以分为两个步骤 1. 相机捕捉现实世界图像 (由ARKit 实现) 2. 在图像中现实虚拟3D 模型(由SceneKit 实现) * `ARKit`与`SceneKit`框架关系图 ![](/assets/images/20170830ARKit/ARKitUML.webp) 1. `ARSCNView` --> `SCNView`(SceneKit.framework)-->`UIView`(UIKit.framework) 2. ARSCNView视图容器,它管理一个 `ARSession` 3. 在一个完整的虚拟增强现实体验中,`ARKit`只负责把真实世界画面转变为一个3D 场景, 这个过程主要分为两个环节: * ARCamera 负责捕捉摄像头画面 * ARSession 负责搭建3D 场景 4. 在一个完整的虚拟增强现实体验中,将虚拟物体展现在3D场景中是由`SceneKit`框架来完成的 > 每一个虚拟的物体都是一个节点SCNNode,每一个节点构成了一个场景SCNScene,无数个场景构成了3D世界 > 可以理解UIViewController 的[view addSubview:xxxView]; ### ARKit工作原理 #### ARSCNView与 ARSession关系 说之前先了解一下这个`Session` 和`Context`命名以及含义 `Session` 直译: 会话 `Context` 直译: 上下文 _在iOS框架中,凡是带session或者context后缀的,这种类一般自己不干活,作用一般都是两个_: * __管理其他类,帮助他们搭建沟通桥梁,好处就是解耦__ * __负责帮助我们管理复杂环境下的内存__ _`Session` 和`Context`不同点_ * session 有硬件参与的(一般与硬件打交道), eg: 摄像头捕捉 `ARSession`、音频 `AVAudioSession`, 网卡相关的`NSURLSession` .... 等等. * context 一般没有硬件参与的, eg: `CGGraphicContext`、`EAGLContext` 绘图上下文, 以及自定义转场里面的那个 Context 就不详细列举了. 回到正题, 如上所说 我们要想实现一个 `ARSCNView` 与`ARCamera` 之间相互协同配合 (`ARSCNView` 与`ARCamera` 两者之间没有直接关系) * __ARSCNView -----> ARCamera 或__ * __ARSCNView <----- ARCamera__ > __ARSCNView里有个 ARFrame(属性 成员变量), ARFrame 里面包含 ARCamera(属性 成员变量)__ 就需要一个沟通的桥梁 进行调度配合协作完成 图像捕捉到视觉渲染的过程__这个桥梁 就是 `ARSession`__ ![](/assets/images/20170830ARKit/ARKitSession.webp) 如果想运行一个 `ARSession`会话,必须指定一个叫`会话追踪配置`的对象`ARConfiguration`,`ARConfiguration`主要目的负责追踪相机在3D 世界中的位置以及一些特征场景的捕捉,__比如捕捉平面__,这个类 作用很大 * `ARConfiguration`是个父类,为了更好实现增强现实的效果,苹果建议我们使用派生自它的子类`ARWorldTrackingConfiguration `(这个类仅支持`A9`芯片之后的机型). 注意: _原来是这个`ARWorldTrackingSessionConfiguration`现在被弃用了._ ![](/assets/images/20170830ARKit/ARWorldTrackingConfiguration.webp) #### ARFrame 与 ARWorldTrackingConfiguration `ARSession` 搭建沟通的桥梁的参与者有两个 1. ARFrame 2. ARWorldTrackingConfiguration `ARWorldTrackingConfiguration` (3D世界追踪配置) 的作用是跟踪设备的__方向、位置、检测摄像头捕捉到的内容__. 它的内部实现了一系列庞大的算法和调用iPhone 上必要的传感器来检测手机的 __移动、旋转、平移__(六轴位置方向变化) 当`ARWorldTrackingConfiguration`计算出相机在3D 世界中的位置时,会把这个**位置数据** 交个`ARSession`去管理, 而相机的**位置数据**对应的类就是`ARFrame` > `ARSession`类一个属性叫`currentFrame` 就是 ARFrame 的实例变量 `ARCamera`只负责捕捉图像,不参与数据的处理. 它属于3D 场景中的一个环节, 每个3D Scene 都会有一个 Camera, 这个 Camera 决定了我们看到物体的视野. 这三者关系如下: ![](/assets/images/20170830ARKit/RelationshipARFrame.webp) > `ARFrame`里面有我们需要的`CVPixelBufferRef`(capturedImage) 和 `ARCamera` 也就是我们需要的图像的原始数据 __ARCamera在3D世界的位置__ ![](/assets/images/20170830ARKit/Coordinate.webp) ### ARKit 工作流程 ![](/assets/images/20170830ARKit/ARWorkflow.webp) 图片来自:[坤小](http://www.jianshu.com/p/0492c7122d2f) 1. `ARSCNView`加载场景`SCNScene` 2. `SCNScene` 启动相机`ARCamera`开始捕捉场景 3. `ARCamera` 捕捉场景后 `ARSCNView`开始将场景数据给`ARSession` 4. `ARSession` 通过`ARConfiguration`(或者它的派生子类) 实现场景的追踪并返回一个`ARFrame`. 5. 给`ARSCNView`的 scene 添加一个子节点(3D物体模型) > `ARConfiguration` 捕捉相机的位置的目的是__能够在添加的3D 物体模型的时候计算出3D 物体模型相对于相机的真实矩阵位置__ 注意:__在3D坐标系统中__ * 世界坐标系 相当于 UIView 的 `Frame` * 本地坐标系 相当于 UIView 的 `bounds` > 坐标系的转换在 ARKit 中是比较难部分 ## ARKit简单代码实现 [代码ARKitDemo1](https://github.com/sunyazhou13/ARDemos/blob/master/ARDemo1.zip) ``` objc - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{ //1. 使用场景加载 scn 文件 SCNScene *scene = [SCNScene sceneNamed:@"Models.scnassets/vase/vase.scn"]; SCNNode *plantNode = scene.rootNode.childNodes[0]; //2. 调整位置 将节点添加到当前屏幕中 plantNode.position = SCNVector3Make(0, -1, -1); //3. 将飞机节点添加到当前屏幕 [self.arSceneView.scene.rootNode addChildNode:plantNode]; } ``` 这里需要补充一点 > 我们先抛开`ARSCNView`和 `UIViewController` 以及`UIView`的关系 __我们只说一下`ARSCNView`,`scene`,`node`啥关系__ `ARSCNView` 是个集成自 UIView 的视图 `ARSCNView` 有个属性(成员变量)叫`scene` 这就相当于 VC `scene`有 `rootNode` 相当于 VC 有`self.view` __可以把`scene`理解为 VC__ __把`rootNode`理解为 `self.view`__ 我们知道 self.view 可以添加 子 view 通过 ``` objc [self.view addSubview:xxx]; ``` 那么 node 也就有了相应的方法 ``` objc [scene.rootNode addChildNode:xxxNode]; ``` > 注意:*__所有的场景有且只有一个根节点,其他所有节点都是根节点的子节点__* ## ARKit框架所有API介绍 ![](/assets/images/20170830ARKit/ARDefines.webp) 还是拿上边这张图说一下 这里我们诉求剖析所有 API 从而达到大家都了解 ARKit的内容 #### ARAnchor `ARAnchor`表示一个物体在3D 空间的位置和方向(ARAnchor 俗称 3D 锚点, 类似 UIKit 框架中 CALayer的 Anchor) ``` objc @interface ARAnchor : NSObject //锚点唯一标识 @property (nonatomic, readonly) NSUUID *identifier; //锚点的旋转变换矩阵,定义了锚点的旋转、位置、缩放。是一个4x4的矩阵 @property (nonatomic, readonly) matrix_float4x4 transform; //构造方法,一般我们无需构造。因为添加一个3D物体时ARKit会有代理告知我们物体的锚点 - (instancetype)initWithTransform:(matrix_float4x4)transform; ``` > `ARFrame`表示的也是物体的位置和方向,但是`ARFrame`通常标识的是 AR 相机的**位置**和**方向**以及追踪相机的时间戳,还有捕捉到相机的图片帧(CVPixelBufferRef) #### ARError `ARError` 错误描述类, eg: 设备不支持, 常驻后台的会话中断...等 ``` objc FOUNDATION_EXTERN NSString *const ARErrorDomain; typedef NS_ERROR_ENUM(ARErrorDomain, ARErrorCode) { /** Unsupported session configuration. */ ARErrorCodeUnsupportedConfiguration = 100, /** A sensor required to run the session is not available. */ ARErrorCodeSensorUnavailable = 101, /** A sensor failed to provide the required input. */ ARErrorCodeSensorFailed = 102, /** App does not have permission to use the camera. The user may change this in settings. */ ARErrorCodeCameraUnauthorized = 103, /** World tracking has encountered a fatal error. */ ARErrorCodeWorldTrackingFailed = 200, }; ``` #### ARFrame `ARFrame` 主要是追踪当前的状态 eg: 图片帧, 时间戳,位置方向等参数. ``` objc @interface ARFrame : NSObject //时间戳 @property (nonatomic, readonly) NSTimeInterval timestamp; //图片帧 @property (nonatomic, readonly) CVPixelBufferRef capturedImage; //相机(表示这个ARFrame是哪一个相机的,iPhone7plus有两个摄像机) @property (nonatomic, copy, readonly) ARCamera *camera; //返回当前相机捕捉到的锚点数据(当一个3D虚拟模型加入到ARKit中时,锚点值得就是这个模型在AR中的位置) @property (nonatomic, copy, readonly) NSArray *anchors; //灯光 指的是灯光强度 一般是0-2000,系统默认1000 @property (nonatomic, strong, nullable, readonly) ARLightEstimate *lightEstimate; //特征点(应该是捕捉平地或者人脸的,比较苹果有自带的人脸识别功能) 仅限 world 的追踪配置可用 @property (nonatomic, strong, nullable, readonly) ARPointCloud *rawFeaturePoints; //根据2D坐标点搜索3D模型,这个方法通常用于,当我们在手机屏幕点击某一个点的时候,可以捕捉到这一个点所在的3D模型的位置,至于为什么是一个数组非常好理解。手机屏幕一个是长方形,这是一个二维空间。而相机捕捉到的是一个由这个二维空间射出去的长方体,我们点击屏幕一个点可以理解为在这个长方体的边缘射出一条线,这一条线上可能会有多个3D物体模型 point:2D坐标点(手机屏幕某一点) ARHitTestResultType:捕捉类型 点还是面 (NSArray *):追踪结果数组 - (NSArray *)hitTest:(CGPoint)point types:(ARHitTestResultType)types; //相机窗口的的坐标变换(可用于相机横竖屏的旋转适配) -(CGAffineTransform)displayTransformWithViewportSize:(CGSize)viewportSize orientation:(UIInterfaceOrientation)orientation; @end ``` 这里说一下一个技巧 如果在一个类里面我们不想提供`init:`方法的话 该如何写? 我们都知道`OC`里面所有的`class`都是继承 自`NSObject`,`NSObject`里面有`init:`、`dealloc:`等方法. ``` objc @interface ARAnchor (Unavailable) - (instancetype)init NS_UNAVAILABLE; + (instancetype)new NS_UNAVAILABLE; @end ``` > 这里用到的技巧就是 写个 Category 并复写该方法后边标注为`NS_UNAVAILABLE` #### ARHitTestResult `ARHitTestResult` 点击回调结果,这个类主要用于AR 技术中 __现实世界与3D 场景中虚拟物体的交互.__ eg:在相机中移动、拖拽3D 虚拟物体,都可以通过这个类来获取 ARKit 所捕捉的结果. ``` objc typedef NS_OPTIONS(NSUInteger, ARHitTestResultType) { //点 ARHitTestResultTypeFeaturePoint = (1 << 0), //水平面 y为0. ARHitTestResultTypeEstimatedHorizontalPlane = (1 << 1), //已结存在的平面 ARHitTestResultTypeExistingPlane = (1 << 3), //已结存在的锚点和平面 ARHitTestResultTypeExistingPlaneUsingExtent = (1 << 4), } NS_SWIFT_NAME(ARHitTestResult.ResultType); @interface ARHitTestResult : NSObject //捕捉类型 @property (nonatomic, readonly) ARHitTestResultType type; //3D虚拟物体与相机的距离(单位:米) @property (nonatomic, readonly) CGFloat distance; //本地坐标矩阵(世界坐标指的是相机为场景原点的坐标,而每一个3D物体自身有一个场景,本地坐标就是相对于这个场景的坐标)类似于frame和bounds的区别 @property (nonatomic, readonly) matrix_float4x4 localTransform; //世界坐标矩阵 @property (nonatomic, readonly) matrix_float4x4 worldTransform; //锚点(3D虚拟物体,在虚拟世界有一个位置,这个位置参数是SceneKit中的SCNVector3:三维矢量),而锚点anchor是这个物体在AR现实场景中的位置,是一个4x4的矩阵 @property (nonatomic, strong, nullable, readonly) ARAnchor *anchor; @end ``` 这里需要了解一下: * **matrix_float4x4** __worldTransform__; 世界坐标系 __以相机为场景的原点开始(0,0,0,0)__ 也就是以相机为圆心向外,参照这个原点 3D 物体的位置信息. 这就相当于2D 的 UIView 的 frame (绝对坐标) * **matrix_float4x4** __localTransform__; 本地坐标系 以一个 node 为父节点为参照,相当于距离这个父场景的坐标. * **matrix_float4x4** [4x4 矩阵请参考这里](http://www.opengl-tutorial.org/cn/beginners-tutorials/tutorial-3-matrices/) #### ARLightEstimate `ARLightEstimate` ``` objc @interface ARLightEstimate : NSObject //环境灯光强度 范围0-2000 默认1000 @property (nonatomic, readonly) CGFloat ambientIntensity; //环境光温度 @property (nonatomic, readonly) CGFloat ambientColorTemperature; @end ``` #### ARPlaneAnchor `ARPlaneAnchor`派生自`ARAnchor`的子类, 平面锚点.`ARKit`能自动识别平地,并且添加一个锚点到场景中,当然要想看到真实世界中的平地效果需要我们自己使用 `SCNNode`渲染一个锚点. > 锚点只是一个位置 ``` objc @interface ARPlaneAnchor : ARAnchor //平地类型,目前只有一个,就是水平面 @property (nonatomic, readonly) ARPlaneAnchorAlignment alignment; //3轴矢量结构体,表示平地的中心点 x/y/z @property (nonatomic, readonly) vector_float3 center; //3轴矢量结构体,表示平地的大小(宽度和高度) x/y/z @property (nonatomic, readonly) vector_float3 extent; @end ``` #### ARPointCloud `ARPointCloud` 点状渲染 ``` objc @interface ARPointCloud : NSObject //点数 @property (nonatomic, readonly) NSUInteger count; //每一个点的位置的集合(结构体带*表示的是结构体数组) @property (nonatomic, readonly) const vector_float3 *points; @end ``` #### ARConfiguration `ARConfiguration` 会话追踪配置 ``` objc //追踪对其方式 typedef NS_ENUM(NSInteger, ARWorldAlignment) { /* 相机位置 vector (0, -1, 0) / ARWorldAlignmentGravity, /* 相机位置及方向. vector (0, -1, 0) heading :(0, 0, -1) */ ARWorldAlignmentGravityAndHeading, /* 相机方向. */ ARWorldAlignmentCamera } ; typedef NS_OPTIONS(NSUInteger, ARPlaneDetection) { ARPlaneDetectionNone = 0, ARPlaneDetectionHorizontal = (1 << 0), //探测平面是水平横向 } ; @interface ARConfiguration : NSObject //当前设备是否支持,一般A9芯片以下设备不支持 @property(class, nonatomic, readonly) BOOL isSupported; //世界坐标的对齐方式 @property (nonatomic, readwrite) ARWorldAlignment worldAlignment; //是否需要自适应灯光效果,默认是YES @property (nonatomic, readwrite, getter=isLightEstimationEnabled) BOOL lightEstimationEnabled; @end //世界会话追踪配置,苹果建议我们使用这个类,这个子类只有一个属性,也就是可以帮助我们追踪相机捕捉到的平地 @interface ARWorldTrackingSessionConfiguration : ARConfiguration //探测的类型 @property (nonatomic, readwrite) ARPlaneDetection planeDetection; @end ``` > 会话配置的恩子类:ARWorldTrackingSessionConfiguration,它们在同一个API文件中 #### ARSKView `ARSKView`是2D 的 AR 视图 这个基本就不需要讲了 和 `ARSCNView`一样,就不重复介绍了 #### ARScnView 重点介绍 ``` objc @interface ARSCNView : SCNView //代理 @property (nonatomic, weak, nullable) id delegate; //AR 会话 @property (nonatomic, strong) ARSession *session; //场景 @property(nonatomic, strong) SCNScene *scene; //是否自动适应灯光 @property(nonatomic) BOOL automaticallyUpdatesLighting; //返回对应节点的锚点,节点是一个3D虚拟物体,它的坐标是虚拟场景中的坐标,而锚点ARAnchor是ARKit中现实世界的坐标 - (nullable ARAnchor *)anchorForNode:(SCNNode *)node; //返回对应锚点的物体 - (nullable SCNNode *)nodeForAnchor:(ARAnchor *)anchor; /** */ - (NSArray *)hitTest:(CGPoint)point types:(ARHitTestResultType)types; @end ``` > 根据2D坐标点搜索3D模型,这个方法通常用于,当我们在手机屏幕点击某一个点的时候,可以捕捉到这一个点所在的3D模型的位置,至于为什么是一个数组非常好理解。手机屏幕一个是长方形,这是一个二维空间。而相机捕捉到的是一个由这个二维空间射出去的长方体,我们点击屏幕一个点可以理解为在这个长方体的边缘射出一条线,这一条线上可能会有多个3D物体模型,point:2D坐标点(手机屏幕某一点) ARHitTestResultType:捕捉类型 点还是面 (NSArray *):追踪结果数组 详情见本章节ARHitTestResult类介绍 数组的结果排序是由近到远 代理方法 ``` objc //代理的内部实现了SCNSceneRendererDelegate:scenekit代理 和ARSessionObserver:ARSession监听(KVO机制) #pragma mark - ARSCNViewDelegate @protocol ARSCNViewDelegate @optional //自定义节点的锚点 - (nullable SCNNode *)renderer:(id )renderer nodeForAnchor:(ARAnchor *)anchor; //当添加节点是会调用,我们可以通过这个代理方法得知我们添加一个虚拟物体到AR场景下的锚点(AR现实世界中的坐标) - (void)renderer:(id )renderer didAddNode:(SCNNode *)node forAnchor:(ARAnchor *)anchor; //将要刷新节点 - (void)renderer:(id )renderer willUpdateNode:(SCNNode *)node forAnchor:(ARAnchor *)anchor; //已经刷新节点 - (void)renderer:(id )renderer didUpdateNode:(SCNNode *)node forAnchor:(ARAnchor *)anchor; //移除节点 - (void)renderer:(id )renderer didRemoveNode:(SCNNode *)node forAnchor:(ARAnchor *)anchor; @end ``` #### ARSesson 重点介绍 ![](/assets/images/20170830ARKit/SessionBridge.webp) `ARSesson` 是一个连接底层与 AR 视图之间的桥梁, `ARSCNView`里的所有方法都是又`ARSession`提供的 __`ARSesson`获取相机位置数据主要由两种方式__ * push 通过实现 Session 的代理`session:didUpdateFrame:`告知用户 * pull 用户想要可主动去取 `ARSession`的`currentFrame`属性 ``` objc @interface ARSession : NSObject //代理 @property (nonatomic, weak) id delegate; //指定代理执行的线程(主线程不会有延迟,子线程会有延迟),不指定的话默认主线程 @property (nonatomic, strong, nullable) dispatch_queue_t delegateQueue; //相机当前的位置(是由会话追踪配置计算出来的) @property (nonatomic, copy, nullable, readonly) ARFrame *currentFrame; //会话配置 @property (nonatomic, copy, nullable, readonly) ARConfiguration *configuration; //运行会话(这行代码就是开启AR的关键所在) - (void)runWithConfiguration:(ARConfiguration *)configuration NS_SWIFT_UNAVAILABLE("Use run(_:options:) instead"); //运行会话,只是多了一个参数ARSessionRunOptions:作用就是会话断开重连时的行为。 - (void)runWithConfiguration:(ARConfiguration *)configuration options:(ARSessionRunOptions)options NS_SWIFT_NAME(run(_:options:)); //暂停会话 - (void)pause; //添加锚点 - (void)addAnchor:(ARAnchor *)anchor NS_SWIFT_NAME(add(anchor:)); //移除锚点 - (void)removeAnchor:(ARAnchor *)anchor NS_SWIFT_NAME(remove(anchor:)); @end ``` > 运行会话__runWithConfiguration:options:__ options 是个`ARSessionRunOptions`枚举 > 这个方法的作用就是会话断开重连时的行为 ``` objc typedef NS_OPTIONS(NSUInteger, ARSessionRunOptions) { //表示重置追踪 ARSessionRunOptionResetTracking = (1 << 0), //移除现有锚点 ARSessionRunOptionRemoveExistingAnchors = (1 << 1) }; ``` 下面来看下`ARSession`的代理 `ARSession` 分 两种 * KVO 观察者 ARSessionObserver * delegate 委托代理 ARSessionDelegate > 看到这我也很奇怪这个玩法 ARSessionObserver如下 ``` objc @protocol ARSessionObserver @optional //session 失败 - (void)session:(ARSession *)session didFailWithError:(NSError *)error; //相机追踪状态发生改变 - (void)session:(ARSession *)session cameraDidChangeTrackingState:(ARCamera *)camera; //session 意外断开(如果开启ARSession之后,APP退到后台就有可能导致会话断开) - (void)sessionWasInterrupted:(ARSession *)session; //session 会话断开后 恢复(短时间退到后台再进入APP会自动恢复) - (void)sessionInterruptionEnded:(ARSession *)session; //session 已经输出了一个 音频数据 `CMSampleBufferRef` - (void)session:(ARSession *)session didOutputAudioSampleBuffer:(CMSampleBufferRef)audioSampleBuffer; @end ``` ARSessionDelegate 如下 ``` objc 相机位置发生改变 就是相机的位置有变动 - (void)session:(ARSession *)session didUpdateFrame:(ARFrame *)frame; // 已添加锚点 - (void)session:(ARSession *)session didAddAnchors:(NSArray*)anchors; //刷新锚点 - (void)session:(ARSession *)session didUpdateAnchors:(NSArray*)anchors; //移除锚点 - (void)session:(ARSession *)session didRemoveAnchors:(NSArray*)anchors; @end ``` 以上是 `ARSession` #### ARCamera 重点介绍 `ARCamera`是个相机, 它是链接虚拟场景和现实场景的之间的枢纽. 在`ARKit`中,它是捕捉现实图像的相机,在 SceneKit 中它是3D 虚拟世界中的相机 > 游戏里一般第一人称3D 游戏, 英雄就是一个3D 相机, 我们电脑屏幕看到的画面就是这个相机捕捉到的画面 一般情况下我们不需要自己去创建一个`ARCamera`实例, 因为每次初始化一个 `ARSCNView`的时候,它会默认为我们创建一个`ARCamera`实例,而且这个相机就是摄像头的位置,同时也是3D 世界中的原点所在 __(0,0,0)__ 至于`ARCamera` 的 api 一般我们也不用 care,`ARKit`默认会配置好. ``` objc @interface ARCamera : NSObject //4x4矩阵表示相机位置 跟锚点类似 @property (nonatomic, readonly) matrix_float4x4 transform; //相机方向(旋转)的矢量欧拉角 分别是x/y/z @property (nonatomic, readonly) vector_float3 eulerAngles; //相机追踪状态(在下方会有枚举值介绍) @property (nonatomic, readonly) ARTrackingState trackingState NS_REFINED_FOR_SWIFT; //追踪运动类型 @property (nonatomic, readonly) ARTrackingStateReason trackingStateReason NS_REFINED_FOR_SWIFT; //相机曲率 3x3矩阵 @property (nonatomic, readonly) matrix_float3x3 intrinsics; //摄像头分辨率 @property (nonatomic, readonly) CGSize imageResolution; //投影矩阵 @property (nonatomic, readonly) matrix_float4x4 projectionMatrix; //创建相机 使用x,y,z 位置 - (CGPoint)projectPoint:(vector_float3)point orientation:(UIInterfaceOrientation)orientation viewportSize:(CGSize)viewportSize; //创建相机投影矩阵 近面距离 远面距离 - (matrix_float4x4)projectionMatrixForOrientation:(UIInterfaceOrientation)orientation viewportSize:(CGSize)viewportSize zNear:(CGFloat)zNear zFar:(CGFloat)zFar; //创建相机投影矩阵 - (matrix_float4x4)viewMatrixForOrientation:(UIInterfaceOrientation)orientation; @end ``` > 上边提到的远面和近面距离 ![](/assets/images/20170830ARKit/distance.webp) 可以参考这张图 摘自[投影变换](http://www.jianshu.com/p/bc151ff65cef) > ![](/assets/images/20170830ARKit/distance2.webp) > 这属于 OpenGL的学习范畴.有兴趣可以学习一下 ``` objc //相机追踪状态枚举 typedef NS_ENUM(NSInteger, ARTrackingState) { /* 不被允许 */ ARTrackingStateNotAvailable, /* 最小 */ ARTrackingStateLimited, /* 正常. */ ARTrackingStateNormal, }; //追踪运动类型 typedef NS_ENUM(NSInteger, ARTrackingStateReason) { /* 无. */ ARTrackingStateReasonNone, //初始化 ARTrackingStateReasonInitializing, /* 运动. */ ARTrackingStateReasonExcessiveMotion, /** 脸部捕捉. */ ARTrackingStateReasonInsufficientFeatures, } ``` > 这里面涉及到的 一个叫做`eulerAngles`[欧拉角](https://zh.wikipedia.org/wiki/%E6%AC%A7%E6%8B%89%E8%A7%92) > ![](/assets/images/20170830ARKit/EulerAngles.webp) > 这个欧拉角是解决3D物体的 旋转矩阵 等取向问题, 就有有一个平面 是静止不动的 一个平面是动的 根据圆心距离两个平面相交的 角度或者 sin cos 来解决一些夹角标记、旋转矩阵等问题 具体可以参考维基百科的解释 (我研究了一阵 还是云里雾里 见笑见笑) #### ARKit捕捉平地 ![](/assets/images/20170830ARKit/PlaneDetective.gif) [探测平地demo](https://github.com/sunyazhou13/ARDemos/blob/master/ARDemoPlaneDetective.zip) 贴一下核心代码 添加节点时候调用(当开启平地捕捉模式之后,如果捕捉到平地,ARKit会自动添加一个平地节点) ``` objc #pragma mark - #pragma mark - ARSCNViewDelegate 代理 - (void)renderer:(id )renderer didAddNode:(SCNNode *)node forAnchor:(ARAnchor *)anchor{ if ([anchor isMemberOfClass:[ARPlaneAnchor class]]) { NSLog(@"捕捉到平地"); //添加一个3D 平面模型,ARKit 只有捕捉能力,锚点是一个空间位置,要想更加清楚的看到这个空间,我们需要给空间添加一个平地的3D模型来渲染他 //1. 获取扑捉到的平地锚点 ARPlaneAnchor *planeAnchor = (ARPlaneAnchor *)anchor; //2. 创建一个3D物体模型 (系统捕捉到的平地是一个不规则大小的长方形,这里我们将其变成一个长方形,并且对平地做一次缩放) //创建长方形 参数:长,宽,高,圆角 SCNBox *plane = [SCNBox boxWithWidth:planeAnchor.extent.x * 0.3 height:0 length:planeAnchor.extent.x * 0.3 chamferRadius:0]; //3. 使用Material渲染3D模型 默认模型是白色的 plane.firstMaterial.diffuse.contents = [UIColor cyanColor]; //4. 创建一个基于3D 物体模型的节点 SCNNode *planeNode = [SCNNode nodeWithGeometry:plane]; //5. 设置节点的位置为捕捉到的平地的锚点和中心位置 SceneKit框架中节点的位置position是一个基于3D坐标系的矢量坐标SCNVector3Make planeNode.position = SCNVector3Make(planeAnchor.center.x, 0, planeAnchor.center.z); [node addChildNode:planeNode]; //6. 当捕捉到平地时,2s之后开始在平地上添加一个3D模型 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ //1.创建一个花瓶场景 SCNScene *scene = [SCNScene sceneNamed:@"Models.scnassets/vase/vase.scn"]; //2.获取花瓶节点(一个场景会有多个节点,此处我们只写,花瓶节点则默认是场景子节点的第一个) //所有的场景有且只有一个根节点,其他所有节点都是根节点的子节点 SCNNode *vaseNode = scene.rootNode.childNodes[0]; //4.设置花瓶节点的位置为捕捉到的平地的位置,如果不设置,则默认为原点位置,也就是相机位置 vaseNode.position = SCNVector3Make(planeAnchor.center.x, 0, planeAnchor.center.z); //5.将花瓶节点添加到当前屏幕中 //!!!此处一定要注意:花瓶节点是添加到代理捕捉到的节点中,而不是AR试图的根节点。因为捕捉到的平地锚点是一个本地坐标系,而不是世界坐标系 [node addChildNode:vaseNode]; }); } } ``` #### AR代码demo实现 [所有相关的 demos 仓库](https://github.com/sunyazhou13/ARDemos) 参考文献 [Using ARKit with Metal](http://metalkit.org/2017/07/29/using-arkit-with-metal.html) [坤小1~10篇](http://www.jianshu.com/p/c97b230fa391) URL: https://sunyazhou.com/2017/08/LearningAVFoundationAVAssetSenior/index.html.md Published At: 2017-08-07 20:36:46 +0000 # Learning AV Foundation(四)AVAsset元数据(高级篇) ![](/assets/images/20170807LearningAVFoundationAVAssetSenior/AudioArtwork.webp) # 前言 先上图 ![](/assets/images/20170807LearningAVFoundationAVAssetSenior/metadata.gif) 这一篇 **我们将学习解决如何一套代码解析大部分 多媒体格式的文件然后形成通用的 model - 元数据键值空间标准化** ## 内容介绍 结构图 ![](/assets/images/20170807LearningAVFoundationAVAssetSenior/MetaDataModel.webp) --- class 代码 * __MediaItem (一个直接对外的接口)__ * __MetaData (元数据model)__ * __Genre (风格)__ * __AVMetadataItem+Additions__ * __MetadataDefines__ * __MetadataKit__ * __Converters (文件夹包含如下:)__ * __MetadataConverter (Protocol 存取 `AVMetadataItem`)__ * __MetadataConverterFactory__ * __DefaultMetadataConverter__ * __ArtworkMetadataConverter__ * __CommentMetadataConverter__ * __TrackMetadataConverter__ * __DiscMetadataConverter__ * __GenreMetadataConverter__ --- ### MediaItem 这个类主要对外直接暴露接口 如下代码即可调用使用 ``` objc __weak typeof(self) weakSelf = self; MediaItem *item = [[MediaItem alloc] initWithURL:self.url]; [item prepareWithCompletionHandler:^(BOOL complete) { __strong typeof(weakSelf) strongSelf = weakSelf; [strongSelf refreshDataByItem:item]; NSLog(@"%@",[item modelDescription]); }]; ``` 代码实现部分 ``` objc #import #import #import "MetaData.h" typedef void(^CompletionHandler)(BOOL complete); @interface MediaItem : NSObject @property (strong, readonly) NSString *filename; @property (strong, readonly) NSString *filetype; @property (strong, readonly) MetaData *metadata; @property (readonly, getter = isEditable) BOOL editable; - (id)initWithURL:(NSURL *)url; /** 此方法完成之后如果成功即可取metadata @param handler 回调 block */ - (void)prepareWithCompletionHandler:(CompletionHandler)handler; - (void)saveWithCompletionHandler:(CompletionHandler)handler; @end @end ``` `.m`可参考源码 比较多就不赘述了 当 block 完成时使用 目前支持获取元数据信息的媒体格式如下: * m4a * mov * mp4 * mp3 > 注意:_**mp3文件是不可编辑的文件故而不能进行编辑 比如改变歌手名称之类 如果要编辑可使用其它专业软件尝试**_ 我尝试了 mac 版本的 demo 编辑 文件 是 OK 的 但是在 iOS 上 我更改其它格式也没能保存成功 如果你看到有解决办法 可以留言给我或者发邮件给我 非常感谢. ### MetaData ``` objc #import #import @class Genre; //风格 eg: 蓝调、 古典 .... @interface MetaData : NSObject @property (copy) NSString *name; @property (copy) NSString *artist; @property (copy) NSString *albumArtist; @property (copy) NSString *album; @property (copy) NSString *grouping; @property (copy) NSString *composer; @property (copy) NSString *comments; @property (strong) UIImage *artwork; @property (strong) Genre *genre; @property NSString *year; @property id bpm; @property NSNumber *trackNumber; @property NSNumber *trackCount; @property NSNumber *discNumber; @property NSNumber *discCount; - (void)addMetadataItem:(AVMetadataItem *)item withKey:(id)key; - (NSArray *)metadataItems; @end ``` 看到上边的代码估计你也猜到了 这就是我们需要的 比如 mp3文件解析出来的真正 model 这里东西比较多 有些值有可能没有 请自行做好 check ### MetadataConverter 这个协议是为了支持所有多媒体文件统一解析使用,比如:mp3文件和mp4文件两个是不一样的文件格式,虽然里面有很多相同的key,但是肯定数据结构是不一样的,这样就要求,搞一个统一的协议,比如输入的是一个URL返回一个 model那么为了解决key value参差不齐问题 就搞了这个协议. ``` objc @protocol zh @optional /** AVMetadataItem to Model 转换 用于UI显示的model @param item AVMetadataItem @return model */ - (id)displayValueFromMetadataItem:(AVMetadataItem *)item; /** AVMetadataItem映射通用字段 @param value 通过媒体元数据取出的某个key的value @param item AVMetadataItem @return AVMetadataItem */ - (AVMetadataItem *)metadataItemFromDisplayValue:(id)value withMetadataItem:(AVMetadataItem *)item; @end ``` ### MetadataConverterFactory 这个类用于统一输出遵守`MetadataConverter`协议的model并且找到适当的转换器去转换响应的格式 ``` objc @interface MetadataConverterFactory : DefaultMetadataConverter - (id )converterForKey:(NSString *)key; @end @implementation MetadataConverterFactory - (id )converterForKey:(NSString *)key{ id converter = nil; if ([key isEqualToString:MetadataKeyArtwork]) { converter = [[ArtworkMetadataConverter alloc] init]; } else if ([key isEqualToString:MetadataKeyTrackNumber]) { converter = [[TrackMetadataConverter alloc] init]; } else if ([key isEqualToString:MetadataKeyDiscNumber]) { converter = [[DiscMetadataConverter alloc] init]; } else if ([key isEqualToString:MetadataKeyComments]) { converter = [[CommentMetadataConverter alloc] init]; } else if ([key isEqualToString:MetadataKeyGenre]) { converter = [[GenreMetadataConverter alloc] init]; } else { converter = [[DefaultMetadataConverter alloc] init]; } return converter; } @end ``` ### DefaultMetadataConverter 简单实现`MetadataConverter`协议 ``` objc @interface DefaultMetadataConverter : NSObject @end @implementation DefaultMetadataConverter - (id)displayValueFromMetadataItem:(AVMetadataItem *)item { return item.value; } - (AVMetadataItem *)metadataItemFromDisplayValue:(id)value withMetadataItem:(AVMetadataItem *)item { AVMutableMetadataItem *metadataItem = [item mutableCopy]; metadataItem.value = value; return metadataItem; } ``` ### ArtworkMetadataConverter 实现`MetadataConverter`协议 取出专辑封面 此处省略 .h 文件只贴出.m ( .h里面啥也没有 大家可参考 demo) ``` objc @implementation ArtworkMetadataConverter - (id)displayValueFromMetadataItem:(AVMetadataItem *)item { UIImage *image = nil; //下面是核心代码取出图片 if ([item.value isKindOfClass:[NSData class]]) { // 1 image = [[UIImage alloc] initWithData:item.dataValue]; } else if ([item.value isKindOfClass:[NSDictionary class]]) { // 2 NSDictionary *dict = (NSDictionary *)item.value; image = [[UIImage alloc] initWithData:dict[@"data"]]; } return image; } - (AVMetadataItem *)metadataItemFromDisplayValue:(id)value withMetadataItem:(AVMetadataItem *)item { AVMutableMetadataItem *metadataItem = [item mutableCopy]; UIImage *image = (UIImage *)value; metadataItem.value = UIImagePNGRepresentation(image); // 3 return metadataItem; } @end ``` 这里 mp3 (id3v2格式)取图片的方式可能有不一样的地方 1出判断 属于哪种格式 3处把 UIImage 转 NSData 再放回去 需要注意一个地方是 返回`AVMetadataItem`的类型 由于`AV Foundation`无法写入 ID3元数据 所以这里使用了 `AVMutableMetadataItem`来存储封面图 `AVMutableMetadataItem` 是 `AVMetadataItem`的子类 ### CommentMetadataConverter 注释转换 ``` objc @implementation CommentMetadataConverter - (id)displayValueFromMetadataItem:(AVMetadataItem *)item { NSString *value = nil; if ([item.value isKindOfClass:[NSString class]]) { // 1 value = item.stringValue; } else if ([item.value isKindOfClass:[NSDictionary class]]) { // 2 NSDictionary *dict = (NSDictionary *) item.value; if ([dict[@"identifier"] isEqualToString:@""]) { value = dict[@"text"]; } } return value; } - (AVMetadataItem *)metadataItemFromDisplayValue:(id)value withMetadataItem:(AVMetadataItem *)item { AVMutableMetadataItem *metadataItem = [item mutableCopy]; // 3 metadataItem.value = value; return metadataItem; } @end ``` 1. `MPEG-4`和`QuickTime`媒体的 value 为 `NSString` 2. `MP3`的注释保存在一个定义`ID3 COMM帧`的`NSDictionary`中(如果处理的是`ID3V2.2`,则为`COM`),所有类型的值都保存在这个帧中. eg: iTune 在这个帧中保存音频标准化和无缝播放设置等,意味着当请求 `ID3`元数据时需要多接收多个`COMM帧`.包含实际注释内容的特定`COMM帧`被存储在一个带有空字符串标识的帧中.找到需要的条目后 通过请求`text` key 来检索出注释内容 ### TrackMetadataConverter 音轨数据转换 音轨: 通常包含一首歌曲在整个唱片中的编号位置信息(eg: 12首歌中的第4首 4/12)等信息. ``` objc @implementation TrackMetadataConverter - (id)displayValueFromMetadataItem:(AVMetadataItem *)item { NSNumber *number = nil; NSNumber *count = nil; if ([item.value isKindOfClass:[NSString class]]) { // 1 NSArray *components = [item.stringValue componentsSeparatedByString:@"/"]; if (components.count > 0) { number = @([components[0] integerValue]); } if (components.count > 1) { count = @([components[1] integerValue]); } } else if ([item.value isKindOfClass:[NSData class]]) { // 2 NSData *data = item.dataValue; if (data.length == 8) { uint16_t *values = (uint16_t *) [data bytes]; if (values[1] > 0) { number = @(CFSwapInt16BigToHost(values[1])); // 3 } if (values[2] > 0) { count = @(CFSwapInt16BigToHost(values[2])); // 4 } } } NSMutableDictionary *dict = [NSMutableDictionary dictionary]; // 5 [dict setObject:number ?: [NSNull null] forKey:MetadataKeyTrackNumber]; [dict setObject:count ?: [NSNull null] forKey:MetadataKeyTrackCount]; return dict; } - (AVMetadataItem *)metadataItemFromDisplayValue:(id)value withMetadataItem:(AVMetadataItem *)item { AVMutableMetadataItem *metadataItem = [item mutableCopy]; NSDictionary *trackData = (NSDictionary *)value; NSNumber *trackNumber = trackData[MetadataKeyTrackNumber]; NSNumber *trackCount = trackData[MetadataKeyTrackCount]; uint16_t values[4] = {0}; // 6 if (trackNumber && ![trackNumber isKindOfClass:[NSNull class]]) { values[1] = CFSwapInt16HostToBig([trackNumber unsignedIntValue]); // 7 } if (trackCount && ![trackCount isKindOfClass:[NSNull class]]) { values[2] = CFSwapInt16HostToBig([trackCount unsignedIntValue]); // 8 } size_t length = sizeof(values); metadataItem.value = [NSData dataWithBytes:values length:length]; // 9 return metadataItem; } @end ``` 1. 刚才所说 `mp3`格式已 `xx/xx` 格式的字符串标识一个歌曲 在整个唱片中的第几首 所以我们用`/`分割 2. iTunes `M4A`文件的唱片信息保存在一个 `NSData` 中,`NSData`包含3个16位的`big encoding`数字,如果直接在控制台打印 NSData 会输出**`<00000008 000a0000>`这是4个16位的`big endian`数字数组的十六进制表现形式**. 数组中第2个和第3个元素分别保存唱片编号和唱片计数值 3. 如果唱片编号 != 0, 则获取该值并使用[`CFSwapInt16BigToHost()`](https://developer.apple.com/documentation/corefoundation/1425282-cfswapint16bigtohost?language=objc)函数执行`endian`转换,转换成一个`little endian` 并打包成`NSNumber` 4. 同样如果音轨计数值不为0, 则获取该值并在字节上执行`endian`转换并打包成`NSNumber` 5. 6. 步骤反过来换成3个`uint16_t` 保存音轨编号和计数值. 7. 如果音轨编号有效, 将字节转换为`big endian`格式并保存到数组第2个位置 8. 如果音轨计数值有效, 将字节转换为`big endian`格式并保存到数组第3个位置 9. 打成 NSData 保存将其设置为元数据项的 value ### DiscMetadataConverter 唱片数据转换 唱片计数信息用于表示一首歌曲所在的CD是所有唱片中的第几张 通常都是 1/1 (通常都是一个 cd 一首) 上下的和音轨 非常类似了 如果是4/10就是 10首里面的第4首 由于唱片这玩意都过时了 你现在应该很少看到 屌丝 带着 walkman 在大街上压马路了都看不到了 但是逻辑还是在的 这里逻辑看代码吧 和 音轨 基本一模一样 ### GenreMetadataConverter 风格转换 数字音频使用的标准风格最初来自 MP3. ID3 规范定义了80个默认的风格类型及 另外46个 WinAmp 扩展,共计 126个风格. 不过这些都不属于正式格式. 由于 mp3风格的主导地位比较明显, iTunes 没有另造轮子,而是基本遵循 ID3 的风格分类,不过做了点小变化。**iTunes 音乐风格的标号比响应的 ID3标识符大 `1` .** ![](/assets/images/20170807LearningAVFoundationAVAssetSenior/gener.webp) 虽然 iTunes 使用了 ID3集合中的预定义音乐风格, 不过 iTunes 对电视、电影和有声读物等定义了自己的风格集. [Apple's Genre IDs Appendix](https://affiliate.itunes.apple.com/resources/documentation/genre-mapping/) 示例代码已经包含了这些类型 虽不在赘述 请参考 demo ### 保存元数据 `AVAsset`是一个不可变类型 我们不能直接修改 `AVAsset` 而是使用`AVAssetExportSession`类来导出新的资源副本以及元数据的改动. #### 使用`AVAssetExportSession` ``` objc - (void)saveWithCompletionHandler:(CompletionHandler)handler { NSString *presetName = AVAssetExportPresetPassthrough; // 1 AVAssetExportSession *session = [[AVAssetExportSession alloc] initWithAsset:self.asset presetName:presetName]; NSURL *outputURL = [self tempURL]; // 2 session.outputURL = outputURL; session.outputFileType = self.filetype; session.metadata = [self.metadata metadataItems]; // 3 [session exportAsynchronouslyWithCompletionHandler:^{ AVAssetExportSessionStatus status = session.status; BOOL success = (status == AVAssetExportSessionStatusCompleted); if (success) { // 4 NSURL *sourceURL = self.url; NSFileManager *manager = [NSFileManager defaultManager]; [manager removeItemAtURL:sourceURL error:nil]; [manager moveItemAtURL:outputURL toURL:sourceURL error:nil]; [self reset]; // 5 } if (handler) { dispatch_async(dispatch_get_main_queue(), ^{ handler(success); }); } NSLog(@"sessionError:%@",session.error); }]; } - (NSURL *)tempURL { // 获取Caches目录路径 NSString *cachesDir = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject]; NSString *tempDir = cachesDir; NSString *ext = [[self.url lastPathComponent] pathExtension]; NSString *tempName = [NSString stringWithFormat:@"temp.%@", ext]; NSString *tempPath = [tempDir stringByAppendingPathComponent:tempName]; return [NSURL fileURLWithPath:tempPath]; } ``` > 注意: __**`AVAssetExportPresetPassthrough` 这个预设值 确实允许修改`MPEG-4`和`QuickTime`容器中的存在的元数据信息, 不过它不可以添加新的元数据,添加元数据的唯一方法是使用转码预设值, 此外不能修改 `ID3`(mp3)标签。 框架不支持写入 MP3数据.**__ ## 总结 经过了代码实现和解析多媒体元数据 `AVAsset`,我们也熟悉了多媒体文件的构造, ID3(MP3)格式的文件解析 arkwork 功能. 从而在后续开发过程中 提升开发效率. 最后放出 代码的 demo 请大家多多指教 **[示例 demo](https://github.com/sunyazhou13/MetaDemo)** URL: https://sunyazhou.com/2017/07/ios11NewSkills/index.html.md Published At: 2017-07-13 10:55:15 +0000 # iOS 11 新技能 ![](/assets/images/20170713ios11NewSkills/whatisnewsinios11.webp) ## 可用性检查API 在swift代码中经常可以看到 某个API 适用于 iOS10.0 如下代码 ``` swift if (@available(iOS 11, *)) { //iOS 11可用 } else { //老版本API } ``` 在Xcode9 中, 编译器增加了 Objective-C 版本的 API 可用性检查 ##### 通过`API_AVAILABLE`宏来标注方法的可用性 ``` objc @interface ViewController : UIViewController - (void)xxxMethodA API_AVAILABLE(ios(11.0)); - (void)xxxMethodB API_AVAILABLE(ios(8.0), macos(10.10), watchos(2.0), tvos(9.0)); @end ``` > 切记 `macos`、`ios`、`watchos`、`tvos`都是小写 通过这种写法进行可用性判断, 编辑器就不会产生警告了, 并且在`运行时`就根据iOS系统版本执行相应代码. ##### 通过`API_AVAILABLE`宏来标注整个`class`的可用性 ``` objc API_AVAILABLE(ios(11.0)) @interface A : NSObject - (void)xxxMothod; @end ``` 看了大家会发现 这个 都是用于OC的代码 那C/C++ 有吗? 必须有 ##### C/C++ 代码 可以使用 `` 判断是否可用 ``` c++ if (__builtin_available(iOS 11, macOS 10.13, *)) { xxxxFunc(); } ``` ``` c++ //导入头文件 #include //可用性判断用于 声明函数 void myFunctionForiOS11OrNewer(int i) API_AVAILABLE(ios(11.0), macos(10.13)); //可用性判断 用于类 XXXClassA class API_AVAILABLE(ios(11.0), macos(10.13)) XXXClassA; ``` 默认 `API_AVAILABLE()` 只能用于 `iOS 11` / `tvOS 11` / `macOS 10.13` / `watchOS 4` 以上的 API 生效 如果就工程想使用这种llvm新版特性的话 需要修改 `buid setting`里面的 `Unguarded availability` 如下图: ![](/assets/images/20170713ios11NewSkills/availability.webp) ## 静态分析 前面的文章我又讲过[静态分析](http://www.sunyazhou.com/2017/06/20/enable-static-analyer/) 这里说一下变化 ### NSNumber/CFNumberRed 静态分析 延时 当我们错误的判断 NSNumber时 静态分析 则给出了提示 ![](/assets/images/20170713ios11NewSkills/error.webp) 在Xcode9 中可以直接把这种倍忽视 的问题改成 当错误处理 ![](/assets/images/20170713ios11NewSkills/static.webp) ## 开启 LTO 并设置为 Incremental 模式 链接时优化(以下简称 `LTO`)是 LLVM 的一项优化特性,其主要原理是: *利用对象文件经过一些优化得到的中间格式在链接阶段再进行深度优化,包含代码逻辑层面的分析,去除实际未用到的函数、变量、甚至局部代码片段,继而减小安装包大小,同时提高了运行时的效率。* 对于 LTO,Xcode 9 做出的改进主要是在进一步优化了编译速度。 苹果演示的例子是以某个大型 C++ 工程为参考,对于一次完整链接,Xcode 9 比 Xcode 8 提升了 35%;对于一次增量链接,Xcode 9 比 Xcode 8 提升了近 60%。 ![](/assets/images/20170713ios11NewSkills/lto1.webp) ![](/assets/images/20170713ios11NewSkills/lto2.webp) 开启LTO ![](/assets/images/20170713ios11NewSkills/LTO.webp) 据说对包大小和运行时速度有 10% 左右的优化 ## GCD 统一队列标识 统一队列标识是指我们在工程中散落在各处的创建队列,如果队列标识是一样的,他们在内核中会被 bind 在一起,其效率可以提高 30%。Apple 没有告诉我们其内核是怎么做到的,它提供了这样的建议,如果一类操作重要性程度或其他属性接近,亦或开发者希望散落在工程各处的代码可以放在同一个队列里去控制,那么我们在创建队列的时候就可以指定一个共同的标识符。 然后系统在内核中会把这些标识相同的队列 bind 到一起来管理 如下代码 如果app里面都使用同一个字符串 的话 效率可以提高30% ``` objc dispatch_queue_t queue = dispatch_queue_create("com.sunyazhou.demo.queue", DISPATCH_QUEUE_CONCURRENT); dispatch_async(queue, ^{ //异步执行代码写在这里 }); ``` > 老实说 工程里面 避免不了文件上传下载 或者耗时任务处理 如果 整体搞成一个queue显然 不太符合业务需求 如果尽量保持 一个标识的Queue的话 也只能根据 业务分类来做到 可以有机会尝试一下 全文完 [参考](https://techblog.toutiao.com/2017/07/05/session0-2/) URL: https://sunyazhou.com/2017/07/Arc4RandomColor/index.html.md Published At: 2017-07-04 17:45:28 +0000 # iOS生成随机UIColor颜色代码 ``` objc - (UIColor *)randomColor { CGFloat hue = ( arc4random() % 256 / 256.0 ); // 0.0 to 1.0 CGFloat saturation = ( arc4random() % 128 / 256.0 ) + 0.5; // 0.5 to 1.0, away from white CGFloat brightness = ( arc4random() % 128 / 256.0 ) + 0.5; // 0.5 to 1.0, away from black UIColor *color = [UIColor colorWithHue:hue saturation:saturation brightness:brightness alpha:1]; return color; } ``` 我们通常接触到的颜色空间是RGB,其实常用的还有HSV又叫HSB。 HSV即Hue, Saturation, Value. 什么意思呢? 先看一张图 ![](/assets/images/20170704Arc4RandomColor/hsv.webp) HSV这个color space可以用上图的圆柱体来表示。 Hue代表从0°到360°的不同颜色. Saturation指的是色彩的饱和度,它用0%至100%的值描述了相同色相、明度下色彩纯度的变化。数值越大,颜色中的灰色越少,颜色越鲜艳,呈现一种从理性(灰度)到感性(纯色)的变化 Value指的是色彩的明度,作用是控制色彩的明暗变化。它同样使用了0%至100%的取值范围。数值越小,色彩越暗,越接近于黑色;数值越大,色彩越亮,越接近于白色。 [色彩引用](https://zhuanlan.zhihu.com/p/31202175) URL: https://sunyazhou.com/2017/06/GetSandboxPathios/index.html.md Published At: 2017-06-26 16:44:22 +0000 # iOS获取各种文件目录的路径 ``` objc // 获取沙盒主目录路径 NSString *homeDir = NSHomeDirectory(); // 获取Documents目录路径 NSString *docDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject]; // 获取Library的目录路径 NSString *libDir = [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) lastObject]; // 获取Caches目录路径 NSString *cachesDir = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject]; // 获取tmp目录路径 NSString *tmpDir = NSTemporaryDirectory(); ``` URL: https://sunyazhou.com/2017/06/RsaUniversalCrossPlatformiOSAndroidPhp/index.html.md Published At: 2017-06-26 10:42:47 +0000 # iOS Android Php RSA加密解密通配方案 ![](/assets/images/20170626RsaUniversalCrossPlatformiOSAndroidPhp/RSALogo.webp) # 前言 先膜拜一下 RSA的作者 ![](/assets/images/20170626RsaUniversalCrossPlatformiOSAndroidPhp/RSATeam.webp) RSA非对称加密 原理 各种。。。 请自行百度 ## 弯路 最近开发涉及到如何使用RSA进行鉴权 等技术。。。老实说 我找了一圈根本就找到一个真正能在 iOS、Android、web跑通的代码. 浪费了好几天开发时间 就没有一个靠谱能好使的 所以我必须发一篇博客 把真正 好使的代码拿出来 share一下 (当时我真的 想骂娘了 我擦 百度搜出来的 一堆垃圾) # 代码实现 ## 第一步 生成公私钥对 ### 命令生成原始 RSA私钥文件 rsa_private_key.pem ``` sh openssl genrsa -out rsa_private_key.pem 1024 ``` ### 命令将原始 RSA私钥转换为 pkcs8格式 ``` sh openssl pkcs8 -topk8 -inform PEM -in rsa_private_key.pem -outform PEM -nocrypt -out private_key.pem ``` ### 生成RSA公钥 rsa_public_key.pem ``` sh openssl rsa -in rsa_private_key.pem -pubout -out rsa_public_key.pem ``` > 从上面看出通过私钥能生成对应的公钥,因此我们将私钥`private_key.pem`用在*服务器端*,*公钥*发放给`android`跟`ios`等前端 > ## 第二步 php代码实现 ``` php ``` 打开`private_key.pem`,将上面的$PRIVATE_KEY,替换成private_key.pem的内容即可,服务器端我们只需要使用私钥来加密解密。 ## 第三步 android端 代码实现 使用java的Cipher类来实现加密解密类,代码如下: ``` java import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.security.KeyFactory; import java.security.NoSuchAlgorithmException; import java.security.PublicKey; import java.security.spec.X509EncodedKeySpec; import javax.crypto.Cipher; import android.util.Base64; /** * @author alun (http://alunblog.duapp.com) * @version 1.0 * @created 2013-5-17 */ public class Rsa { private static final String RSA_PUBLICE = "xxxxxxxxxxxxxxxxC" + "\r" + "Qf/xxxxxxxhVuwdNH6aRFE0ms3bkpp/WL4cfVDgnCO" + "\r" + "+W9J6vRVpuTuD/xxxxxxxxbJeO74fYnYqo/mmyJSeLE5iZg4I" + "\r" + "Zm5LPWBZWUp3ULCAZQIDAQAB"; private static final String ALGORITHM = "RSA"; /** * 得到公钥 * @param algorithm * @param bysKey * @return */ private static PublicKey getPublicKeyFromX509(String algorithm, String bysKey) throws NoSuchAlgorithmException, Exception { byte[] decodedKey = Base64.decode(bysKey,Base64.DEFAULT); X509EncodedKeySpec x509 = new X509EncodedKeySpec(decodedKey); KeyFactory keyFactory = KeyFactory.getInstance(algorithm); return keyFactory.generatePublic(x509); } /** * 使用公钥加密 * @param content * @param key * @return */ public static String encryptByPublic(String content) { try { PublicKey pubkey = getPublicKeyFromX509(ALGORITHM, RSA_PUBLICE); Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); cipher.init(Cipher.ENCRYPT_MODE, pubkey); byte plaintext[] = content.getBytes("UTF-8"); byte[] output = cipher.doFinal(plaintext); String s = new String(Base64.encode(output,Base64.DEFAULT)); return s; } catch (Exception e) { return null; } } /** * 使用公钥解密 * @param content 密文 * @param key 商户私钥 * @return 解密后的字符串 */ public static String decryptByPublic(String content) { try { PublicKey pubkey = getPublicKeyFromX509(ALGORITHM, RSA_PUBLICE); Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); cipher.init(Cipher.DECRYPT_MODE, pubkey); InputStream ins = new ByteArrayInputStream(Base64.decode(content,Base64.DEFAULT)); ByteArrayOutputStream writer = new ByteArrayOutputStream(); byte[] buf = new byte[128]; int bufl; while ((bufl = ins.read(buf)) != -1) { byte[] block = null; if (buf.length == bufl) { block = buf; } else { block = new byte[bufl]; for (int i = 0; i < bufl; i++) { block[i] = buf[i]; } } writer.write(cipher.doFinal(block)); } return new String(writer.toByteArray(), "utf-8"); } catch (Exception e) { return null; } } } ``` __*注意:*__在初始化`Cipher`对象时,一定要指明使用`"RSA/ECB/PKCS1Padding"`格式如`Cipher.getInstance("RSA/ECB/PKCS1Padding");` 打开`rsa_public_key.pem`文件,将上面代码的`RSA_PUBLICE`替换成其中内容即可. ## 第四步 iOS端代码实现 iOS上没有直接处理RSA加密的API,网上说的大多数也是处理X.509的证书的方法来实现,不过X.509证书是带签名的,在php端`openssl_pkey_get_private`方法获取密钥时,第二个参数需要传签名,而android端实现X.509证书加密解密较为不易,在这里我们利用ios兼容c程序的特点,利用openssl的api实现rsa的加密解密,代码如下: CRSA.h代码 ``` objc // // CRSA.h // RSA_C_demo // // Created by sunyazhou on 2017/6/25. // Copyright © 2017年 Kingsoft, Inc. All rights reserved. // #import #import #import #import typedef enum { KeyTypePublic, KeyTypePrivate }KeyType; typedef enum { RSA_PADDING_TYPE_NONE = RSA_NO_PADDING, RSA_PADDING_TYPE_PKCS1 = RSA_PKCS1_PADDING, RSA_PADDING_TYPE_SSLV23 = RSA_SSLV23_PADDING }RSA_PADDING_TYPE; @interface CRSA : NSObject{ RSA *_rsa; } @property(nonatomic, copy)NSString *rsaKeyPath; //证书路径 + (id)shareInstance; - (BOOL)importRSAKeyFromeStringWithType:(KeyType)type andKey:(NSString *)keyPath; - (BOOL)importRSAKeyWithType:(KeyType)type; - (int)getBlockSizeWithRSA_PADDING_TYPE:(RSA_PADDING_TYPE)padding_type; - (NSString *)encryptByRsa:(NSString*)content withKeyType:(KeyType)keyType; - (NSString *)decryptByRsa:(NSString*)content withKeyType:(KeyType)keyType; @end ``` CRSA.m ``` obj // CRSA.m // RSA_C_demo // // Created by sunyazhou on 2017/6/25. // Copyright © 2017年 Kingsoft, Inc. All rights reserved. // #import "CRSA.h" #define BUFFSIZE 1024 //#import "NSString+Base64.h" //#import "NSData+Base64.h" #define PADDING RSA_PADDING_TYPE_PKCS1 @implementation CRSA + (id)shareInstance { static KSYCRSA *_crsa = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ _crsa = [[self alloc] init]; }); return _crsa; } - (BOOL)importRSAKeyWithType:(KeyType)type { FILE *file; NSString *keyName = type == KeyTypePublic ? @"public_key" : @"private_key"; NSString *keyPath = [[NSBundle mainBundle] pathForResource:keyName ofType:@"pem"]; file = fopen([keyPath UTF8String], "rb"); if (NULL != file) { if (type == KeyTypePublic) { _rsa = PEM_read_RSA_PUBKEY(file, NULL, NULL, NULL); assert(_rsa != NULL); } else { _rsa = PEM_read_RSAPrivateKey(file, NULL, NULL, NULL); assert(_rsa != NULL); } fclose(file); return (_rsa != NULL) ? YES : NO; } return NO; } - (BOOL)importRSAKeyWithPath:(KeyType)type { FILE *file; NSString *keyName = type == KeyTypePublic ? @"public_key.pem" : @"private_key.pem"; NSString *keyPath = [self.rsaKeyPath stringByAppendingPathComponent:keyName]; file = fopen([keyPath UTF8String], "rb"); if (NULL != file) { if (type == KeyTypePublic) { _rsa = PEM_read_RSA_PUBKEY(file, NULL, NULL, NULL); assert(_rsa != NULL); } else { _rsa = PEM_read_RSAPrivateKey(file, NULL, NULL, NULL); assert(_rsa != NULL); } fclose(file); return (_rsa != NULL) ? YES : NO; } return NO; } - (BOOL)importRSAKeyFromeStringWithType:(KeyType)type andKey:(NSString *)key{ if (key.length == 0) { return NO; } BIO *keybio ; keybio = BIO_new_mem_buf((__bridge void *)(key), -1); if (keybio==NULL) { printf( "Failed to create key BIO"); return 0; } if(type == KeyTypePublic) { _rsa = PEM_read_bio_RSA_PUBKEY(keybio, &_rsa,NULL, NULL); } else { _rsa = PEM_read_bio_RSAPrivateKey(keybio, &_rsa,NULL, NULL); } BIO_free(keybio); return (_rsa != NULL) ? YES : NO; } - (NSString *) encryptByRsa:(NSString*)content withKeyType:(KeyType)keyType { if (![self importRSAKeyWithPath:keyType]) return nil; // if (![self importRSAKeyWithType:keyType]) // return nil; int status; NSUInteger length = [content length]; unsigned char input[length + 1]; bzero(input, length + 1); int i = 0; for (; i < length; i++) { input[i] = [content characterAtIndex:i]; } NSInteger flen = [self getBlockSizeWithRSA_PADDING_TYPE:PADDING]; char *encData = (char*)malloc(flen); bzero(encData, flen); switch (keyType) { case KeyTypePublic: status = RSA_public_encrypt(length, (unsigned char*)input, (unsigned char*)encData, _rsa, PADDING); break; default: status = RSA_private_encrypt(length, (unsigned char*)input, (unsigned char*)encData, _rsa, PADDING); break; } if (status) { NSData *returnData = [NSData dataWithBytes:encData length:status]; free(encData); encData = NULL; NSString *ret = [self base64EncodedStringForData:returnData ]; return ret; } free(encData); encData = NULL; return nil; } - (NSString *) decryptByRsa:(NSString*)content withKeyType:(KeyType)keyType { if (![self importRSAKeyWithPath:keyType]) return nil; // if (![self importRSAKeyWithType:keyType]) // return nil; int status; NSData *data = [self base64DecodedDataForString:content]; NSUInteger length = [data length]; NSInteger flen = [self getBlockSizeWithRSA_PADDING_TYPE:PADDING]; char *decData = (char*)malloc(flen); bzero(decData, flen); switch (keyType) { case KeyTypePublic: status = RSA_public_decrypt(length, (unsigned char*)[data bytes], (unsigned char*)decData, _rsa, PADDING); break; default: status = RSA_private_decrypt(length, (unsigned char*)[data bytes], (unsigned char*)decData, _rsa, PADDING); break; } if (status) { NSMutableString *decryptString = [[NSMutableString alloc] initWithBytes:decData length:strlen(decData) encoding:NSASCIIStringEncoding]; free(decData); decData = NULL; return decryptString; } free(decData); decData = NULL; return nil; } - (int)getBlockSizeWithRSA_PADDING_TYPE:(RSA_PADDING_TYPE)padding_type { int len = RSA_size(_rsa); if (padding_type == RSA_PADDING_TYPE_PKCS1 || padding_type == RSA_PADDING_TYPE_SSLV23) { len -= 11; } return len; } //---------------加密工具方法 - (NSString *)base64EncodedStringForData:(NSData *)data { return [self base64EncodedStringWithWrapWidth:0 data:data]; } - (NSString *)base64EncodedStringWithWrapWidth:(NSUInteger)wrapWidth data:(NSData *)data { //ensure wrapWidth is a multiple of 4 wrapWidth = (wrapWidth / 4) * 4; const char lookup[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; long long inputLength = [data length]; const unsigned char *inputBytes = [data bytes]; long long maxOutputLength = (inputLength / 3 + 1) * 4; maxOutputLength += wrapWidth? (maxOutputLength / wrapWidth) * 2: 0; unsigned char *outputBytes = (unsigned char *)malloc(maxOutputLength); long long i; long long outputLength = 0; for (i = 0; i < inputLength - 2; i += 3) { outputBytes[outputLength++] = lookup[(inputBytes[i] & 0xFC) >> 2]; outputBytes[outputLength++] = lookup[((inputBytes[i] & 0x03) << 4) | ((inputBytes[i + 1] & 0xF0) >> 4)]; outputBytes[outputLength++] = lookup[((inputBytes[i + 1] & 0x0F) << 2) | ((inputBytes[i + 2] & 0xC0) >> 6)]; outputBytes[outputLength++] = lookup[inputBytes[i + 2] & 0x3F]; //add line break if (wrapWidth && (outputLength + 2) % (wrapWidth + 2) == 0) { outputBytes[outputLength++] = '\r'; outputBytes[outputLength++] = '\n'; } } //handle left-over data if (i == inputLength - 2) { // = terminator outputBytes[outputLength++] = lookup[(inputBytes[i] & 0xFC) >> 2]; outputBytes[outputLength++] = lookup[((inputBytes[i] & 0x03) << 4) | ((inputBytes[i + 1] & 0xF0) >> 4)]; outputBytes[outputLength++] = lookup[(inputBytes[i + 1] & 0x0F) << 2]; outputBytes[outputLength++] = '='; } else if (i == inputLength - 1) { // == terminator outputBytes[outputLength++] = lookup[(inputBytes[i] & 0xFC) >> 2]; outputBytes[outputLength++] = lookup[(inputBytes[i] & 0x03) << 4]; outputBytes[outputLength++] = '='; outputBytes[outputLength++] = '='; } //truncate data to match actual output length outputBytes = realloc(outputBytes, outputLength); NSString *result = [[NSString alloc] initWithBytesNoCopy:outputBytes length:outputLength encoding:NSASCIIStringEncoding freeWhenDone:YES]; #if !__has_feature(objc_arc) [result autorelease]; #endif return (outputLength >= 4)? result: nil; } - (NSData *)base64DecodedDataForString:(NSString *)string { return [self dataWithBase64EncodedString:string]; } - (NSData *)dataWithBase64EncodedString:(NSString *)string { const char lookup[] = { 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 62, 99, 99, 99, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 99, 99, 99, 99, 99, 99, 99, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 99, 99, 99, 99, 99, 99, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 99, 99, 99, 99, 99 }; NSData *inputData = [string dataUsingEncoding:NSASCIIStringEncoding allowLossyConversion:YES]; long long inputLength = [inputData length]; const unsigned char *inputBytes = [inputData bytes]; long long maxOutputLength = (inputLength / 4 + 1) * 3; NSMutableData *outputData = [NSMutableData dataWithLength:maxOutputLength]; unsigned char *outputBytes = (unsigned char *)[outputData mutableBytes]; int accumulator = 0; long long outputLength = 0; unsigned char accumulated[] = {0, 0, 0, 0}; for (long long i = 0; i < inputLength; i++) { unsigned char decoded = lookup[inputBytes[i] & 0x7F]; if (decoded != 99) { accumulated[accumulator] = decoded; if (accumulator == 3) { outputBytes[outputLength++] = (accumulated[0] << 2) | (accumulated[1] >> 4); outputBytes[outputLength++] = (accumulated[1] << 4) | (accumulated[2] >> 2); outputBytes[outputLength++] = (accumulated[2] << 6) | accumulated[3]; } accumulator = (accumulator + 1) % 4; } } //handle left-over data if (accumulator > 0) outputBytes[outputLength] = (accumulated[0] << 2) | (accumulated[1] >> 4); if (accumulator > 1) outputBytes[++outputLength] = (accumulated[1] << 4) | (accumulated[2] >> 2); if (accumulator > 2) outputLength++; //truncate data to match actual output length outputData.length = outputLength; return outputLength? outputData: nil; } ``` > 这里面我增加了 密钥直接从字符串读取的方法 原来方法是 从`NSBundle`读取private_key.pem和 public_key.pem 但是考虑到被篡改 我增加了 密钥直接搞成字符串(把字符串写到本地沙盒然后加载文件的方式) 这样代码 安全就提高了一些 如果能破译.m的话 拿到的也只能是 publicKey(公钥) 只要不能篡改 就是安全的 ### 外部调用 ``` objc NSString *publicKey = @"-----BEGIN PUBLIC KEY-----\n此处替换生成的公钥 记得换行 按照一定规则加'\n' \n-----END PUBLIC KEY-----"; NSString *privateKey = @"-----BEGIN PRIVATE KEY-----\n 此处替换生成的私钥 \n-----END PRIVATE KEY-----"; NSFileManager *fm = [NSFileManager defaultManager]; // 获取Documents目录路径 NSString *docDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject]; NSString *bundleIdentifier = [[NSBundle mainBundle] bundleIdentifier]; NSString *path = [docDir stringByAppendingFormat:@"/%@",bundleIdentifier]; NSString *publicKeyPath = [path stringByAppendingPathComponent:@"public_key.pem"]; NSString *privateKeyPath = [path stringByAppendingPathComponent:@"private_key.pem"]; BOOL isDir; BOOL exists = [fm fileExistsAtPath:path isDirectory:&isDir]; if (exists) { /* file exists */ if (isDir) { NSError *error = nil; BOOL pubResult = [publicKey writeToFile:publicKeyPath atomically:YES encoding:NSUTF8StringEncoding error:&error]; if (error) { NSLog(@"%@",[error localizedDescription]); } BOOL privateResult = [privateKey writeToFile:privateKeyPath atomically:YES encoding:NSUTF8StringEncoding error:&error]; if (error) { NSLog(@"%@",[error localizedDescription]); } } }else { [fm createDirectoryAtPath:path withIntermediateDirectories:YES attributes:nil error:nil]; NSError *error = nil; BOOL pubResult = [publicKey writeToFile:publicKeyPath atomically:YES encoding:NSUTF8StringEncoding error:&error]; if (error) { NSLog(@"%@",[error localizedDescription]); } BOOL privateResult = [privateKey writeToFile:privateKeyPath atomically:YES encoding:NSUTF8StringEncoding error:&error]; if (error) { NSLog(@"%@",[error localizedDescription]); } } rsa.rsaKeyPath = path; [rsa importRSAKeyFromeStringWithType:KeyTypePublic andKey:publicKeyPath]; [rsa importRSAKeyFromeStringWithType:KeyTypePrivate andKey:privateKeyPath]; NSString *pubDesc = [rsa encryptByRsa:@"需要加密的字符串" withKeyType:KeyTypePrivate]; NSLog(@"加密内容:%@\n--------\n",encryptString); NSLog(@"摘要:\n---------\n%@\n--------\n",pubDesc); //剩下的大家自己探索一下 没什么难度 ``` 其中openssl api包,我们可以在第一步RSA密钥生成工具openssl的include文件夹中得到 下面我说一下如何集成openssl到 iOS工程 ### 1.下载 openssl library [openssl ios下载](https://github.com/st3fan/ios-openssl) ### 2.导入到工程中 拖拽 openssl的库 _(包含 `include` & `lib` 的文件夹 )_到工程中 然后 去 project targets -> `Build Settings` * 找到 **Header Search Paths**, 添加 `"${SRCROOT}/Libraries/openssl/include"` 为你的工程 * 找到 **Library Search Paths**, 添加 `"${SRCROOT}/Libraries/openssl/lib"` 然后就可以了如果中间出问题 请检查一下目录是否正确理论上应该是OK的 -- ## 最后说一下我遇到RSA加密的坑 在iOS端加密 生成摘要到android的时候 android解析不出来(有时候解析出结果前面 一堆乱码) 这是base64有问题 建议 android使用原生的恩 ``` java import android.util.Base64; ``` 如果是iOS 请使用 如下 Base64 [base64来源](https://github.com/nicklockwood/Base64) 上边的ios 的base64和android一一对应 不要理解错了 随便搞个Base64就行了 不信我 你可以试试 base64的代码我已经把代码实现写到`CRSA.m`了 如果像剥离很简单 好 demo我就不写了 已经把所有实现都搞上去了 希望大家找到 好使的RSA方法实现 如果有问题 随时留言 最后我说一句 很简单的一个RSA跨平台方案 那些抄袭CSDN的文章小伙伴 少坑点人 连搜索引擎都不会放过你 全文完 [参考](https://www.lvtao.net/dev/android_ios_php_openssl.html) URL: https://sunyazhou.com/2017/06/EnableStaticAnalyer/index.html.md Published At: 2017-06-20 15:07:33 +0000 # Xcode开启静态分析器 ![](/assets/images/20170620EnableStaticAnalyer/static.webp) ## Clang 静态分析器 Clang 编译器(也就是 XCode 使用的编译器)有一个 静态分析器(static analyer) ,用来执行代码控制流和数据流的分析,可以发现许多编译器检查不出的问题。 你可以在 Xcode 的 Product → Analyze 里手动运行分析器。 分析器可以运行“`shallow`”和“`deep`”两种模式。后者要慢得多,但是有跨方法的控制流分析以及数据流分析,因此能发现更多问题。 ## 建议: 开启分析器的 全部 检查(方法是在 `build setting` 的“`Static Analyzer`”部分开启所有选项) 在 `build setting` 里,对 `release` 的 `build` 配置开启 “`Analyze during` `‘Build’`” 。(真的,一定要这样做——你不会记得手动跑分析器的。) 把 `build setting` 里的 “Mode of Analysis for `‘Analyze’`” 设为 `Deep` 把 `build setting` 里的 “Mode of Analysis for `‘Build’`” 设为 `Shallow` (faster) ![](/assets/images/20170620EnableStaticAnalyer/EnableSStaticAnalyer.webp) 全文完 [参考](http://mp.weixin.qq.com/s/x6XSQ_rrYCOXi2EVeiMfCg) URL: https://sunyazhou.com/2017/06/HowToDeprecatedAMothodInObjC/index.html.md Published At: 2017-06-16 16:40:26 +0000 # 如何在Objective-C中废弃一个方法 # 前言 ![](/assets/images/20170616HowToDeprecatedAMothodInObjC/deprecated.webp) 最新在从事SDK方向的开发 有的时候 不能轻易的把某个API去掉 因为有些人还在使用 于是为了保留 相关方法 并标识为弃用 的方式 我采用如下代码 ``` objc __attribute__((deprecated("此方法已弃用,请使用xxxxx:方法"))); ``` ### 场景1 我想标识一个方法使用其它方式传入某个参数 例如:控制器中我想标识设置URL的方法直接使用setter方法就可以了 ``` objc @interface VideoEditorViewController : UIViewController @property(nonatomic, strong)NSURL *videoPath; -(instancetype)initWithUrl:(NSURL *)path __attribute__((deprecated("使用setVideoPath:方法传入"))); @end ``` 这样调用的时候就直接显示警告了 告诉当前方法传入URL被弃用 ![](/assets/images/20170616HowToDeprecatedAMothodInObjC/code.webp) 相关`__attribute__`更多用法 请参考苹果官方文档和其它博客 后续会持续更新更多用法 全文完 [参考](http://www.jianshu.com/p/0237c34158f0) URL: https://sunyazhou.com/2017/06/XcodeDebugViaWireless/index.html.md Published At: 2017-06-16 11:07:46 +0000 # Xcode9新功能 无线真机调试 ### 效果 今天小伙伴(王可成)发现Xcode有个很方便的功能 可以真机无线调试运行 看一张图 ![](/assets/images/20170616XcodeDebugViaWireless/debug.gif) ### 配置 第一步 选择设备选项 ![](/assets/images/20170616XcodeDebugViaWireless/setting1.webp) 第二步 点勾选 via network ![](/assets/images/20170616XcodeDebugViaWireless/setting2.webp) 剩下的工作就是拔掉那个我们非常讨厌而且有不得不用还死贵且总折头处的白色数据线了 *注意:第一次运行的时候比较慢 可以通过数据线插上先运行一次* *运行环境: Xcode9 beta* *运行设备操作系统: iOS11 beta* *Macbook+iPhone 最好在同一个局域网* 我估计苹果发布正式版本的时候这个应该比较好用了 好了 现在可以愉快的玩耍 了 URL: https://sunyazhou.com/2017/06/LearningAVFoundationAVAssetBasic/index.html.md Published At: 2017-06-16 10:11:19 +0000 # Learning AV Foundation(四)AVAsset元数据(基础篇) ![](/assets/images/20170616LearningAVFoundationAVAssetBasic/AlbumDetail.webp) # 前言 本篇讲述的`AVAsset`元数据(可以简单理解成 比如一个mp3音频格式的model信息. title:xxxx, art:刘德华, album:专辑 爱你一万年.... 等这些数据的来源). 当然这种意义上的字段信息 属于`AVAsset`的一个属性。`AV Foundation`通过`AVAsset`封装来处理各种音频的元数据, __比如从mp3文件中解析出来封面图(artwork)__等。 本章的具体内容如下: ### __理解资源含义__ ### __创建资源__ * iOS Asset库 * iOS iPod库 * macOS iTunes库 ### __异步载入__ ### __媒体元数据__ * 元数据格式 1. QuickTime 2. MPEG-4音频和视频(mp4) 3. MP3 ### __使用元数据__ * 查询元数据 * 使用`AVMetadataItem` ### __创建MetaManager Demo__ * MediaItem(相当于Model) * MediaItem实现 * 数据转换器(model to AVMetadataItem || AVMetadataItem to mode) * DefaultMetadata默认转换 * 转换Artwork(唱片的封面或者专辑图那种) * 转换注释 * 转换音轨数据(track) * 转换唱片数据 * 转换风格数据(genre, eg: blue蓝调, classic古典,pop流行等126种..) * 完成最终demo #### __保存元数据__ --- ### __理解`AVAsset`资源含义__ `AVAsset`是一个不可变的抽象类,定义媒体资源混合呈现方式.里面包含音视频的**曲目**、**格式**、**时长**, 以及**元数据NSData**(二进制的bytes). `AVAsset`不用考虑媒体资源具有的两个范畴: * 提供对基本媒体格式的抽象层 * 不用考虑处理因为不同格式获取内容方式不一样 这意味着无论是处理`Quick Time`影片、`MPEG-4`视频还是`MP3`音频,框架提供统一的接口,我们只需要理解只有资源这个概念。这样做的目的是为了__开发者在面对不同格式的内容时有一个统一的处理方法。不需要care多种编码器和容器格式因为细节不同而带来的困扰__. 当然获取这些其余信息可以通过其它方式实现. `AVAsset`还隐藏了资源位置(GPS定位)信息,当处理一个媒体对象时,通过URL来初始化init. URL可以是Bundle里面 也可以是沙盒的本地文件系统URL.也可以从iPod库中取到的URL。还可以是远程服务器的音频流或视频流的URL。 `AVAsset`属于低耦合组件方式的封装 让框架来处理那些繁重的工作, 我们就可以很方便的不用考虑文件位置的前提下获取或者载入媒体。由于不用care文件合适和文件位置等复杂的问题。`AVAsset`为开发者处理`timed media(时基媒体)`提供了一种简单统一的方式. `AVAsset`本身不是媒体资源. 可以把它理解成承载`timed media(时基媒体)`的容器类。它有很多描述自身元数据的媒体组成. `AVAssetTrack`才是我们真正存储媒体资源的统一媒体类型。并对每个资源建立相应的model. `AVAssetTrack`最常见的形式就是 音频流和视频流, 但是他还可以用于表示诸如__文本__、__副标题__、__隐藏字幕__等媒体类型. 如下示意图理解`AVAsset` 和 `AVAssetTrack` ![](/assets/images/20170616LearningAVFoundationAVAssetBasic/AVAssetTrack.webp) _**`AVAsset.tracks`**_ 如下 ``` objc @property (nonatomic, readonly) NSArray *tracks; ``` 资源曲目可通过tracks属性访问到. 该属性返回一个NSArray的数组,这个数组中的元素就是专辑包含的所有曲目. 此外,`AVAsset`还可以通过标识符、媒体类型或媒体特征等信息找到相应的曲目.这使得在未来更高级的处理中我们可以很容易获取一组需要的曲目 #### __创建资源__ 当为一个现有的媒体资源创建`AVAsset`对象时, 可以通过URL对它的进行的初始化来实现. 一般来说是一个本地文件URL, 也可以是远程的资源URL ``` objc NSURL *assetURL = //.... AVAsset *asset = [AVAsset assetWithURL: assetURL]; ``` > .... `AVAsset`是个抽象类, 它不能直接被实例化. 当使用`assetWithURL:`方法创建实例时,实际上是创建了`AVAsset`的子类`AVURLAsset` 有时候会直接使用这个类, 因为它允许通过传递选项字典来精细调整资源的创建方式, 举个例子,比如创建 用在音频或视频编辑场景中的资源, 可能希望传递一个选项(option)的字典来告诉程序提供更精确的时长和计时等信息 例如: ``` objc NSURL *assetURL = //.... NSDictionary *options = @{AVURLAssetPreferPreciseDurationAndTimingKey:@YES}; AVAsset *asset = [AVAsset assetWithURL: assetURL]; ``` > ... 这里传递的是希望得到稍长一点的加载事件,来获取更精确的时长及时间信息.很多常见的位置是开发时大家想创建资源对象的地方. 在iOS设备上我们希望在用户的照片库中访问视频文件, 或者在iPod库中访问歌曲. 在Mac上 我们希望从用户的iTunes库中找到媒体项. 借助iOS和macOS中的这些辅助framework我们可以使用上边的媒体资源。下面介绍一下这些要用到的framework的例子 ##### iOS Assets库 在iOS上拍照或者通过前置和后置相机捕捉到的音视频,它们保存在用户的照片库中.iOS提供的Assets库框架可以实现从照片库中读写的功能, 下例从用户资源库中的视频创建一个AVAsset: ``` objc ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init]; [library enumerateGroupsWithTypes:ALAssetsGroupSavedPhotos usingBlock:^(ALAssetsGroup *group, BOOL *stop) { //Filter down to only videos [group setAssetsFilter:[ALAssetsFilter allVideos]]; //Grab the first video returned [group enumerateAssetsAtIndexes:[NSIndexSet indexSetWithIndex:0] options:0 usingBlock:^(ALAsset *result, NSUInteger index, BOOL *stop) { if (result) { id representation = [result defaultRepresentation]; NSURL *url = [representation url]; AVAsset *asset = [AVAsset assetWithURL:url]; //创建 调用一些其它API } }]; } failureBlock:^(NSError *error) { NSLog(@"%@", [error localizedDescription]); }]; ``` 上面是如何获取保存在 相册中的视频资源(iOS10.10以后就废弃了上述方式) 获取出筛选结果的第一个视频, 库中的条目全部被建模为`ALAsset`对象, 为默认的呈现方式选用`ALAsset`类型返回一个`ALAssetRepresentation`对象,它提供了一个适用于创建`AVAset`的URL. ##### iOS iPod库 我们获取媒体的一个常见位置就是用户的iPod库. `MediaPlayer` framework 框架提供了API, 用于在iPod库中查询和获取条目. 当找到想获取的item时, 可以得到一个存储的URL并使用这个得到的URL初始化一个资源, 如下例所示: ``` objc //艺术家 MPMediaPropertyPredicate *artistPredicate = [MPMediaPropertyPredicate predicateWithValue:@"刘德华" forProperty:MPMediaItemPropertyArtist]; //专辑 MPMediaPropertyPredicate *albumPredicate = [MPMediaPropertyPredicate predicateWithValue:@"真永远" forProperty:MPMediaItemPropertyAlbumTitle]; //歌曲名称 MPMediaPropertyPredicate *songPredicate = [MPMediaPropertyPredicate predicateWithValue:@"爱你一万年" forProperty:MPMediaItemPropertyTitle]; //查询 MPMediaQuery *query = [[MPMediaQuery alloc] init]; [query addFilterPredicate:artistPredicate]; [query addFilterPredicate:albumPredicate]; [query addFilterPredicate:songPredicate]; NSArray *result = [query items]; if (result.count > 0) { MPMediaItem *item = result[0]; NSURL *assetURL = [item valueForProperty:MPMediaItemPropertyAssetURL]; AVAsset *asset = [AVAsset assetWithURL:assetURL]; // Asset 信息 } ``` `MediaPlayer`framework提供了一个为`MPMediaPropertyPredicate`的类,用户帮助用户在iPod库中查找到具体内容所用的查询语句 上边举例一个例子: 在`刘德华`的`真永远`(真永远专辑) 唱片中查找`爱你一万年`这首歌, 执行完查询 会返回这个媒体 条目的资源URL属性(`MPMediaItemPropertyAssetURL`). 并使用这个属性创建`AVAsset` ##### macOS iTunes库 在 macOS(以前叫 OS X)上, iTunes是用户的媒体资源中心. 要识别这个库中的资源, 我们通常要对iTunes音乐目录中的iTunes Music Library.xml 文件进行解析, 从而得到相关数据. 不过在 Mac OS X 10.8山狮之后 有了比较简单的方法--`iTunesLibrary`framework. ``` objc ITLibrary *library = [ITLibrary libraryWithAPIVersion:@"1.0" error:nil]; NSArray *items = library.allMediaItems; NSString *query = @"artist.name == '刘德华'" "album.title == '真永远'" "title == '爱你一万年'"; NSPredicate *predicate = [NSPredicate predicateWithFormat:query]; NSArray *songs = [items filteredArrayUsingPredicate:predicate]; if (songs.count > 0) { ITLibMediaItem *item = songs[0]; AVAsset *asset = [AVAsset assetWithURL:item.location]; // asset info } ``` `iTunesLibrary`框架并没有像MediaPlayer框架那样给出具体的查询API. 不过开发者可使用标准的Cocoa NSPredicate(谓词)类来构建一个复杂的查询,当筛出需要的media item集合后,可使用`ITLibMediaItem`的`location`属性得到一个URL并创建`AVAsset`. #### 异步载入 `AVAsset`具有多种有用的方法和属性, 可以提供有关资源的信息, 比如时长、创建日期、元数据等. `AVAsset`还包含一些用于获取和使用曲目集合的方法. 不过有一点很重要, 就是当创建时资源就是对基础文件的处理, `AVAsset` 采用一种lazy load的加载方式, 提升了快速创建资源和立即载入的速度. __*注意`AVAsset`的属性访问是同步的,如果正在请求的属性没有预先载入,程序就会阻塞,直到它做出响应为止*__这个搞法不是很好,eg: avasset.duration 可能是个比较耗时的操作,如果使用MP3文件时没有在头文件中设置`TLEN`标签,这个标签用于定义duration值,则整个音频曲目都需要进行解析来准确的知道它的duration, 如果在主线程做这样的访问操作就会阻塞主线程,直到相关操作完成为止, APP可能会出现卡顿,导致系统监视器介入,并终止APP运行,如果解决这种问题,我们应该使用异步的 方式来查询资源属性. ``` objc - (AVKeyValueStatus)statusOfValueForKey:(NSString *)key error:(NSError * _Nullable *)outError; - (void)loadValuesAsynchronouslyForKeys:(NSArray *)keys completionHandler:(void (^)(void))handler; ``` 可以使用statusOfValueForKey:error:方法查询一个给定的属性状态,返回一个`AVKeyValueStatus`的枚举值 ``` objc typedef enum AVKeyValueStatus : NSInteger { AVKeyValueStatusUnknown, AVKeyValueStatusLoading, AVKeyValueStatusLoaded, AVKeyValueStatusFailed, AVKeyValueStatusCancelled } AVKeyValueStatus; ``` 用于表示当前所请求的属性的状态, 如果状态不是`AVKeyValueStatusLoaded`说明此时这个属性可能导致程序卡顿,要异步载入一个给定的属性loadValuesAsynchronouslyForKeys:completionHandler:方法,参数keys 是一个或多个`资源属性名`的数组和一个callback,当资源处于回应状态时,就会调用这个`completionHandler` ``` objc NSURL *assetURL = [[NSBundle mainBundle] URLForResource:@"384551_1438267683" withExtension:@"mov"]; AVAsset *asset = [AVAsset assetWithURL:assetURL]; //异步加在 tracks property NSArray *keys = @[@"tracks"]; [asset loadValuesAsynchronouslyForKeys:keys completionHandler:^{ //查询tracks的属性状态 NSError *error = nil; AVKeyValueStatus status = [asset statusOfValueForKey:@"tracks" error:&error]; switch (status) { case AVKeyValueStatusLoaded: //继续处理后续逻辑 break; case AVKeyValueStatusFailed: //有error break; case AVKeyValueStatusCancelled: // 处理意外取消等情况的逻辑 break; default: break; } }]; ``` 这里用bundle中的`QuickTime`电影创建一个AVAsset, 并异步载入该对象的`tracks`属性.在`completionHandler` block中,我们希望通过调用资源的statusOfValueForKey:error:方法来拿出请求属性的状态,NSError用于判断资源包含错误信息. _*注意:`completionHandler:`block可能在任意一个队列中调用,在对UI界面做出相应更新之前,必须先回到主线程队列,否则必被坑!!!*_ > _*注意:上面的demo载入了一个tracks属性,其实可以在一个吊用中请求多个属性,如果请求多个属性的时候需要注意以下两点:*_ > (1) 每次调用 `loadValuesAsynchronouslyForKeys:completionHandler:`方法时只会调用一次`completionHandler`block, 调用这个callback的次数不是根据传递给这个方法的key个数决定的. > (2) 需要为每个请求的属性调用`statusOfValueForKey:error:`,不能假设所有属性都返回相同的状态值. ### __媒体元数据__ 当创建一个媒体应用程序时,了解该媒体的组织格式非常重要, 简单的展示一堆文件名也许在文件不多的时候还能接受, 如果大规模批量的文件需要展示就比较蛋疼了, 我们真正需要的是 _找到一种方法对媒体进行描述,当用户可以方便的找到、识别和组织这些媒体._ 我们所使用的`AV Foundation`中的主要媒体格式(*.mp4、*.mp3、*.mov、*.mkv.....)都可以嵌入描述其内容的元数据.因为各种媒体格式的描述不尽相同,要搞一套通用的策略去解析各种媒体的格式文件,这要求我们对底层技术有一些了解.不过`AV Foundation`让这些变得简单,因为它使开发者不需要考虑大多数特定格式的细节; 在处理媒体元数据方面, AV Foundation`提供了一套统一的方法. #### 元数据格式 虽然存在多种格式的媒体资源,但是我们在Apple环境下遇到的媒体类型主要有4种, 分别是:`QuickTime(mov)`、`MPEG-4 video(mp4和m4v)`、`MPEG-4 audio(m4a)`和`MPEG-Layer Ⅲ audio(mp3)`. 虽然`AV Foundation`处理这些文件中嵌入的元数据时都使用一个接口, 但是理解这些不同类型资源的元数据如何存储及存储位置仍然很有价值. 这里只做概述, 但是如果深入研究这些都是必学的基础. 1. __QuickTime__ `QuickTime`是苹果自己开发的一种跨平台媒体架构, 其中一部分是Quick File Format规范, 定义了 .mov文件的内部结构.`QuickTime`文件由一种称为`atom`的数据结构组成. 一般规则是这样的: 一个`atom`包含了描述媒体资源的某一方面的数据, 或者嵌套包含其它`atom`,但不能两者都包含.有时候苹果自己的方法实现可能会违背这一规则.`atom`以一种复杂的树状结构组合在一起, 详细的对布局、音频样本格式、视频帧信息乃至需要呈现的元数据信息(作者,版权等)做了描述. ![](/assets/images/20170616LearningAVFoundationAVAssetBasic/atom.webp) *为了能记住`atom`我把它戏称为`阿童木`哈哈-跟阿童木压根没啥关系*. 了解`QuickTime`的一个好办法是用十六进制编辑器中打开一个.mov格式的文件.(常见的十六进制编辑器有Hex Fiend或Synalyze It! Pro).典型的十六进制工具会将一个真实的`QuickTime`文件的数据显示出来,但其中的结构和`atom`间的关系不是很直观能理解,推荐苹果有一个`Atom Inspector`工具.这个工具将atom结构以`NSOutlineView`(树形UI控件类似UITableView)方式显示.所以`atom`的树形瓜西会很清晰的看到,这个工具还提供一个小型的十六进制查看器,可以从中查看到__实际字节布局__. ![](/assets/images/20170616LearningAVFoundationAVAssetBasic/AtomInspector.webp) 下载地址:[Atom Inspector 猛击这里](http://adcdownload.apple.com/QuickTime/atom_inspector/atom_inspector.dmg) 貌似需要登录开发者帐号 下载中心:[苹果官方软件下载中心](https://developer.apple.com/download/more/) 貌似需要登录开发者帐号 下图就是atom格式 ![](/assets/images/20170616LearningAVFoundationAVAssetBasic/QuickTimeAtomStructureNew.webp) *atom格式* __`QuickTime`文件最少包含三个高级的`atom`__ * __用于描述文件类型和兼容类型的`fypy`__ * __包含实际音频和视频媒体的`mdat`__ * __moov atom(moo-vee) 媒体资源的所有细节做了完整描述包括原始的二进制数据__ 下图是我实际测试一个mov文件的atom ![](/assets/images/20170616LearningAVFoundationAVAssetBasic/QuickTimeAtomStructureReal.webp) *实测* 当处理QuickTime电影时会遇到两种类型的元数据. 标准的`QuickTime`元数据由`Final Cut Pro X`这样的工具编写, 位于/moov/meta/plist中, 它的key几乎都具有com.apple.quicktime前缀. 其它类型的数据被认为是`QuickTime`用户数据, 保存在/moov/udta/中.`QuickTime`用户数据可以包含播放器需要查找的标准数据,eg: 歌曲的演唱者或版权信息, 除此之外还可以包含任何对应用程序有帮助的信息. 上述两种元数据类型在 `AV Foundation`中都是可以读写的. 如果想了解更多`QuickTime`细节可以查看[Quick Time Format Specification](https://developer.apple.com/library/content/documentation/QuickTime/QTFF/QTFFPreface/qtffPreface.html)官方文档(400多页). 掌握moov atom的核心知识很重要,有助于我们更好的了解`AV Foundation`是如何使用这些数据的. 2. __MPEG-4 (MP4)音频和视频__ MPEG-4 Part 14是定义MP4文件格式的规范. `MP4`直接派生自`QuickTime`文件格式,所以`MP4`文件格式与`QuickTime`文件结构很类似. 其实有时候能解析一种文件类型的工具也适用于其它文件类型.`MP4`文件也由成为`atom`的数据结构组成.技术上讲,`MPEG-4`规范将这些称为`boxes`,因为其大部分来自于`QuickTime`所以大家都还是把它成为`atom`. ![](/assets/images/20170616LearningAVFoundationAVAssetBasic/mp4AtomBook.webp) *MPEG-4 atom结构* ![](/assets/images/20170616LearningAVFoundationAVAssetBasic/mp4Atom.webp) *MPEG-4 atom结构 实测结果* `MPEG-4`文件的元数据保存在`/moov/udat/meta/ilst`中. 对于`atom`中使用key没有标准, 大家都墨守成规的遵循苹果尚未发布的iTunes元数据规范中对key的定义. 虽然没有正式的发布,但iTunes元数据格式的相关文档已经在网上广为人知了(我就很纳闷 这算是发了版本还是没发,发了怎么还是尚未公布,没发怎么又广为人知...). 可以参考[mp4v2库](https://code.google.com/archive/p/mp4v2/wikis/iTunesMetadata.wiki)文档了解更多mp4内容. `mp4`是对MPEG-4媒体的标准扩展.eg: `.m4v`、`.m4a`、`.m4p`、`.m4b`.这些变体使用的都是`MPEG-4`容器格式,也有些包含了附加的扩展功能. 大家只需要记住几点: * __`M4V`__ 文件是带有苹果公司针对__`FairPlay`__加密及__`AC3-audio`__扩展的`MPEG-4`视频格式 * __`MP4`__ 如果不涉及`FairPlay`加密及`AC3-audio`扩展,`M4V`就仅仅是扩展名不一样而已 * __`M4A`__ 专门针对音频,使用这种扩展名的目的是让大家知道这种格式的文件只带有音频资源 * __`M4P`__ 苹果很古老的iTunes格式,使用其`FairPlay`扩展 * __`M4B`__ 用于有声读物,同窗包含章节标签以及提供书签功能,让读者可以返回到指定位置开始阅读(类似有声小说) 3. __MP3__ `MP3`文件和`MPEG-4 (.mp4)`、`QuickTime(.mov)`有显著区别,`MP3`不使用容器格式,而使用__编码音频数据__,文件开头通常包含可选元数据的结构块.`mp3`文件使用一种称为ID3v2的格式来保存关于音频内容的描述信息,包含: artist(艺术家)、演唱者、album(所属专辑)、音乐风格等. `ID3`数据很easy,`mp3`前10个字节带有嵌入的元数据, 这10个字节定义了`ID3`块的头部.10个字节中的前三个字节始终为'49 44 33'(ID3,用于表示一个`ID3v2标签`,后面两个字姐用于定义主版本信息,既`2、3、4`和版本号.剩余字节用于定义标志集合及ID3快的大小. ![](/assets/images/20170616LearningAVFoundationAVAssetBasic/ID3Header.webp) *ID3 header* `ID3`块中剩下的数据都是用于描述不同元数据的的key-value键值对的帧.每一帧都有一个__实际标签名称的10字节的头__,之后的4字节表示尺寸,再之后的两个字节用来定义选项标志. id3 | version(2字节) | revision (剩余字节)| flag(2字节)| size(4字节) | 帧剩下的字节包含了实际的元数据值.如果值是文本类型tag中的第一个字节包含了实际的元数据值.如果值是文本类型,tag中的第一个字节用来定义编码类型. eg:Ox00, 代表:`ISO-8859=1`,也支持其它类型的编码。如下图ID3结构示意图. __`AV Foundation`支持读取`ID3v2`标签的的所有版本, 但不支持写入.MP3格式收到专利限制.所以`AV Foundation`无法支持对MP3或ID3数据进行编码.__ 不过最近听说德国的MP3专利研究所说专利打算撤销因为`AAC`格式将有更好的效果相对于MP3而言.看看后续苹果API变动会不会增加修改MP3的数据吧. ![](/assets/images/20170616LearningAVFoundationAVAssetBasic/ID3Structure.webp) *ID3v2 结构图* > `AV Foundation`支持所有`ID3v2`标签格式的读取操作,但是`ID3v2`是要加星号的.`ID3v2.2`的布局和`ID3v2.3`及之后版本的布局不同. 需要注意:有些标签由3个字符组成,而不是4个字符, 比如一首歌曲的标注信息, 当标签为`ID3v2.2`时,是被保存到COM帧中,但当同一首歌使用`ID3v2.3`标签或更新版本的标签时,歌曲的标注信息会被保存在COMM帧中,框架定义的字符常量只适用于`ID3v2.3`及以后版本,后续demo中我们通过代码演示如何向前兼容`ID3v2.2`. ### __使用元数据__ `AVAsset`和`AVAssetTrack`可以实现查询元数据功能 * `AVAsset` 大部分情况下会使用 * `AVAssetTrack` 获取曲目一级元数据 读取具体资源元数据的接口可以使用`AVMetadataItem`类提供的方法访问`QuickTime`、`MPEG-4 atom`和`ID3`帧中的元数据进行访问. `AVAsset`和`AVAssetTrack`提供了两种方法可以获取相关元数据.但是有各自的适用范围.了解适用范围之前首先要知道 __键空间__(key space)的含义. `AV Foundation`使用__键空间(key space)__作为将相关键组合在一起的方法, 可以实现对`AVMetadataItem`实例集合的筛选.每个资源至少包含两个键空间,以便从中获取元数据. ![](/assets/images/20170616LearningAVFoundationAVAssetBasic/keyspace.webp) `Common`键空间用来定义所有支持媒体类型的键, 包括: 曲名、歌手、插图信息等常见元素. 这提供了一种对所有支持的媒体格式进行一定级别的元数据标准化过程.我们可以从`Common`键空间查询 资源或者曲目的`commonMetadata`属性来获取元数据 这个属性会返回一个包含所有可用元数据的数组 访问指定格式的元数据需要在资源或曲目上调用`metadataForFormat:`方法.这个方法返回一个包含所有相关元数据信息的数组.`AVMetadataFormat.h`为不同的元数据格式提供对应的字符串常量. 由于不同格式的元数据导致key value对应类型不一致,我们可以利用 `availableMetadataFormats`(AVAsset的属性)获取到信息.如下代码: ``` objc NSURL *url = [NSURL fileURLWithPath:@"xxx.mp4"];//给个路径地址 //创建元数据 AVAset *asset = [AVAsset assetWithURL:url]; NSArray *keys = @[@"availableMetadataFormats"]; [asset loadValuesAsynchronouslyForKeys:keys completionHandler:^{ NSMutableArray *metadata = [NSMutableArray array]; for (NSString *format in asset.availableMetadataFormats){ [metadata addObjectsFromArray:[asset metadataForFormat:format]]; } //处理metadata (AVMetadataItems) }]; ``` ### __查找元数据__ 当我们拿到一个包含元数据项的数组(上文中的metadata (AVMetadataItems))时,我们通常通过遍历取出里面的数据值. 元数据(AVAsset)提供一个AVMetadataItem的遍历方法, 例如我们相得到一个M4A的音频文件演奏者和唱片的元数据. 如下: ``` objc NSArray *metaData = //AVMetadataItems数组 NSString *keySpace = AVMetadataKeySpaceiTunes; NSString *artistKey= AVMetadataiTunesMetadataKeyArtist; NSString *albumKey = AVMetadataiTunesMetadataKeyAlbum; NSArray *artistMetadata = [AVMetadataItem metadataItemsFromArray:metaData withKey:artistKey keySpace:keySpace]; NSArray *albumMetadata = [AVMetadataItem metadataItemsFromArray:metaData withKey:albumKey keySpace:keySpace]; AVMetadataItem *artistItem, *albumItem; if (artistMetadata.count > 0) { artistItem = artistMetadata[0]; } if (albumMetadata.count > 0) { albumItem = albumMetadata[0]; } ``` 这里通过下面的方法来拿出匹配key 和 keySpace的标准对象 通常情况下这个数组就只有一个实例对象 ``` objc + (NSArray *)metadataItemsFromArray:(NSArray *)metadataItems withKey:(id)key keySpace:(AVMetadataKeySpace)keySpace; ``` ### 使用AVMetadataItem AVMetadataItem 可以理解成它是一个 专用于元数据的 字典(key: value) 类型, 唯一的区别是 它的key 有可能是 数字(NSNumber), 它提供了 转 字符串(stringValue) 和numberValue以及 dataValue 的转换 举个例子: 如果输出的key是 145238391 .... 我觉得大家肯定不知道这代表啥意思 为了解决这个问题 需要对`AVMetadataItem`进行category扩展 把这种语义不清楚的整形key换成字符串的key就好理解了 代码如下: ``` objc #import "AVMetadataItem+Additions.h" @implementation AVMetadataItem (Additions) - (NSString *)keyString { if ([self.key isKindOfClass:[NSString class]]) { // 1 return (NSString *)self.key; } else if ([self.key isKindOfClass:[NSNumber class]]) { UInt32 keyValue = [(NSNumber *) self.key unsignedIntValue]; // 2 // Most, but not all, keys are 4 characters ID3v2.2 keys are // only be 3 characters long. Adjust the length if necessary. size_t length = sizeof(UInt32); // 3 if ((keyValue >> 24) == 0) --length; if ((keyValue >> 16) == 0) --length; if ((keyValue >> 8) == 0) --length; if ((keyValue >> 0) == 0) --length; long address = (unsigned long)&keyValue; address += (sizeof(UInt32) - length); // keys are stored in big-endian format, swap keyValue = CFSwapInt32BigToHost(keyValue); // 4 char cstring[length]; // 5 strncpy(cstring, (char *) address, length); cstring[length] = '\0'; // Replace '©' with '@' to match constants in AVMetadataFormat.h if (cstring[0] == '\xA9') { // 6 cstring[0] = '@'; } return [NSString stringWithCString:(char *) cstring // 7 encoding:NSUTF8StringEncoding]; } else { return @"<>"; } } @end ``` 基础篇部分讲解到这里 下一篇 会写个demo演示一下元数据的各种不同格式如何统一解析 全文完 URL: https://sunyazhou.com/2017/05/OpenGLglslLanguage/index.html.md Published At: 2017-05-30 20:32:33 +0000 # OpenGL编程语言-glsl基础 # 前言 ![](/assets/images/20170530OpenGLglslLanguage/OpenglVboShaderGlslVaoGPU.webp) 最近在研究OpenGL 被各种陌生的名词虐成狗,所以记录下来一些学习知识点供学习和参考. ## GLSL是什么? GLSL(OpenGL Shading Language) 是OpenGL的着色器语言,纯粹的和GPU打交道的计算机语言.可以理解为C的变种专门针对OpenGL编程,不支持指针等等一些C的特性等. (名词解释:着色器(Shader)) **GPU是多线程并行处理器**,GLSL直接面向[单指令流多数据流(SIMD)](https://zh.wikipedia.org/wiki/%E5%8D%95%E6%8C%87%E4%BB%A4%E6%B5%81%E5%A4%9A%E6%95%B0%E6%8D%AE%E6%B5%81)模型的多线程计算。 GLSL编写的着色器函数是对每个数据同时执行的。 每个顶点都会由顶点着色器中的算法处理,每个像素也都会由 **片段着色器(也有叫片元着色器)**中的算法处理。 初学者在编写自己的着色器时,需要考虑到SIMD的并发特性,并用并行计算的思路来思考问题 这就是GLSL. 我们最常见的用法是在 **顶点着色器**里生成所需要的值,然后传给 **片断着色器**用. ## GLSL能做什么 * 日以逼真的材质 -- 金属,岩石,木头,油漆等 * 日益逼真的光照效果 -- 区域光和软阴影 * 非现实材质 -- 美术效果,钢笔画,水墨画和对插画技术的模拟 * 针对纹理内存的新用途 * 更少的纹理访问 * 图形处理 -- 选择,边缘钝化遮蔽和复杂混合 * 动画效果 -- 关键帧插值,粒子系统 * 用户可编程的反走样方法 ## GLSL注意 * **GLSL支持函数重载**(就是父类定义方法,子类复写该方法叫重载) * **GLSL不存在数据类型的自动提升(就是不支持类型自动向上转换 eg:float 转 double),类型必须严格保持一致.** * **GLSL不支持指针,字符串,字符,它基本上是一种处理数字数据的语言** * **GLSL不支持联合(union)、枚举类型(enum)、结构体(stuct)位字段(>> or << 左右移)及按位运算符(| or &这种按位与)**(就是干掉麻烦的C操作 让这个更单纯的处理图形数据使用) ## GLSL的数据类型 GLSL有三种基本数据类型: * float * int * double * 由float、int、double组成的array[]或者结构体 ``` glsl 42 // 十进制 042 // 八进制 0x2A // 十六进制 ``` __**注意:GLSL不支持指针,GLSL把向量和矩阵作为基本数据类型**__ [向量(vector)](http://baike.baidu.com/link?url=XKZL51jLByIFnqrj3vaZ-4cnL-AedjBKiVBcD7pEGQG26Jmb9RYl7QOrX4Mwck-mT0nNlzD8UtzXi4ueVYNGkdO1b2uARr59UAih7ulWRvO):有起始位置有方向的线段,也称作 **矢量**(不要被这些名词吓到,我记得这个向量是我高二的时候数学学的东西). ## 矢量 矢量可以和标量甚至矩阵做加减乘除(必须遵守一定规则才可以 否则报错) ``` vec2, vec3, vec4 //包含2/3/4个浮点数的矢量(浮点型) ivec2, ivec3, ivec4 //包含2/3/4个整数的矢量(整形数 前边带i 代表integer) bvec2, bvec3, bvec4 //包含2/3/4个布尔值的矢量(bool不用解释) ``` 上边这些是一种GLSL的数据类型, 可以简单理解为 `vec+数字` 就代表 是一个数组里面放几个元素(应该都是 vec2~vec4之间,没见过 vec5以上和vec2以下,好像这就代表几维坐标系),默认元素是float浮点类型,前边带`i`代表`integer`整形,`b`代表`bool`. ### vec如何声明使用? ``` vec3 v; //声明三维浮点型向量v v[1] = 3.0; //给向量v的第二个元素赋值(数组从0开始,下标为1就是第二个元素) //下面两种等价 vec3 v = vec3(0.6); //数组是连续的存储空间 相当于其它元素默认被这个0.6值填充 vec3 v = vec3(0.6,0.6,0.6); ``` > _注意: 除了用索引方式外,还能用选择运算符的方式来使用向量.择运算符是对于向量的各个元素(最多为4个)约定俗成的名称,用一个小写拉丁字母来表示。根据向量表示对象的意义不同,可以使用以下选择运算符:_ * 表示顶点可以用 (x、y、z、w) (坐标系) * 表示颜色可以用 (r、g、b、a) (颜色值带透明) * 表示纹理坐标用 (s、t、p、q) 三种任选一种都一样,作用都是等效的. 也就是说,如果`v`是一个向量,那么: * `v[0]` * `v.x` * `v.r` * `v.s` 都指的是向量v的第一个元素。 例如: ``` glsl //用构造函数的方式声明并初始化四维浮点型 vec4 v1 = vec4(1.0, 2.0, 3.0, 4.0); vec4 v2; v2.xy=v1.yz; //将v1的第二个和第三个元素复制到v2的第一个和第二个元素 v2.z=2.0; //给v2的第三个元素赋值 v2.xy=v1.yx; //将v1的头两个元素互换,再复制到v2的头两个元素中 ``` ## 矩阵(matrix) 矩阵(matrix)以下类型都以mat开头 * `mat2` 代表2x2的矩阵 * `mat3` 代表3x3的矩阵 * `mat4` 代表4x4的矩阵 _**注意:矩阵是按列顺序组织的,先列后行**_ 如下代码: ``` glsl mat4 m; //声明四维浮点型方阵m m[2][3]=2.0; //给方阵的第三列、第四行元素赋值 // 下面两种等价,初始化矩阵对角 mat2 m = mat2(1.0) mat2 m = mat2(1.0, 0.0, 0.0, 1.0); ``` ## 取样器(Sampler) 纹理查找需要制定哪个纹理或者纹理单元将制定查找. ``` glsl sampler1D // 访问一个一维纹理 sampler2D // 访问一个二维纹理 sampler3D // 访问一个三维纹理 samplerCube // 访问一个立方贴图纹理 sampler1DShadow // 访问一个带对比的一维深度纹理 sampler2DShadow // 访问一个带对比的二维深度纹理 ``` ``` glsl uniform sampler2D grass; vcc2 coord = vec2(100, 100); vec4 color = texture2D(grass, coord); ``` 如果一个着色器在程序里结合多个文理, 可以使用取样器数组. ``` glsl const int tex_nums = 4; uniform sampler2D textures[tex_nums]; for(int i = 0; i < tex_nums; ++i) { sampler2D tex = textures[i]; // todo ... } ``` ## 结构体 这是唯一的用户能用的自定义类型 ``` glsl struct light { vec3 position; vec3 color; }; light ceiling_light; ``` ## 数组 数组索引是从0开始的,而且没有指针概念 ``` glsl // 创建一个10个元素的数组 vec4 points[10]; // 创建一个不指定大小的数组 vec4 points[]; points[2] = vec4(1.0); // points现在大小为3 points[7] = vec4(2.0); // points现在大小为8 ``` ## void 只能用于声明函数返回值 ## 类型转换 必须明确地进行类型转换,不会自动类型提升 ``` glsl float f = 2.3; bool b = bool(f); // b is true ``` ## 限定符 **GLSL中有4个限定符(variable qualifiers)可供使用,它们限定了被标记的变量不能被更改的"范围".** * `const` * `attribute` * `uniform` * `varying` `const`: 和C++里差不多,定义不可变常量 表示限定的变量在编译时不可被修改. `attribute`:是应用程序传给顶点着色器用的 不允许声明时初始化 `attribute`限定符标记的是一种全局变量,该变量在顶点着色器中是只读(read-only)的,该变量被用作从OpenGL应用程序向顶点着色器中传递参数,因此该限定符仅能用于顶点着色器. ``` attribute变量是只能在vertex shader中使用的变量 它不能在fragment shader中声明attribute变量, 也不能被fragment shader中使用) 在application中,一般用函数glBindAttribLocation()来绑定每个attribute变量的位置,然后用函数 glVertexAttribPointer()为每个attribute变量赋值。 以下是例子: uniform mat4 u_matViewProjection; attribute vec4 a_position; attribute vec2 a_texCoord0; varying vec2 v_texCoord; void main(void) { gl_Position = u_matViewProjection * a_position; v_texCoord = a_texCoord0; } ``` `uniform`:一般是应用程序用于设定顶点着色器和片断着色器相关初始化值.不允许声明时初始化.`uniform`限定符标记的是一种全局变量,该变量对于一个图元(`primitive`)来说是不可更改的 它可以从`OpenGL`应用程序中接收传递来的参数 ``` uniform变量 外部程序传递给shader的变量. 函数glUniform**()函数赋值的. shader 中是只读变量,不能被 shader 修改. uniform变量一般用来表示:变换矩阵,材质,光照参数和颜色等信息。 uniform mat4 viewProjMatrix; //投影+视图矩阵 uniform mat4 viewMatrix; //视图矩阵 uniform vec3 lightPosition; //光源位置 ``` `varying`:用于传递顶点着色器的值给片断着色器.它提供了从顶点着色器向片段着色器传递数据的方法,varying限定符可以在顶点着色器中定义变量,然后再传递给光栅化器,光栅化器对数据插值后,再将每个片段的值交给片段着色器. ``` varying变量是vertex和fragment shader之间做数据传递用的。 一般vertex shader修改varying变量的值, 然后fragment shader使用该varying变量的值。 因此varying变量在vertex和fragment shader二者之间的声 明必须是一致的。 application不能使用此变量。 以下是例子: // Vertex shaderuniform mat4 u_matViewProjection; attribute vec4 a_position; attribute vec2 a_texCoord0; varying vec2 v_texCoord; // Varying in vertex shader void main(void) { gl_Position = u_matViewProjection * a_position; v_texCoord = a_texCoord0; } // Fragment shaderprecision mediump float; varying vec2 v_texCoord; // Varying in fragment shader uniform sampler2D s_baseMap; uniform sampler2D s_lightMap; void main() { vec4 baseColor; vec4 lightColor; baseColor = texture2D(s_baseMap, v_texCoord); lightColor = texture2D(s_lightMap, v_texCoord); gl_FragColor = baseColor * (lightColor + 0.25); } ``` _**注意:以上这几种限定符很重要**_ ## 限制性 * 不能在if-else中声明变量 * 用于判断的条件必须是bool类型(if,while,for...) * (?:)操作符后两个参数必须类型相同 * 不支持switch语句 ``` glsl vec4 toonify(in float intensify) { vec4 color; color = vec4(0.8,0.8,0.8,0.8) return color; } ``` ## discard `discard`关键字可以避免片段更新帧缓冲区,当流控制遇到这个关键字时,正在处理的片段就会被标记为丢. 如果不理解什么叫标记为丢 可以参考一下[UIView的绘制过程](理解UIView的绘制) ## 函数 * 函数名可以通过参数类型重载,但是和返回值类型无关 * 所有参数必须完全匹配,参数不会自动 * 函数不能被递归调用 * 函数返回值不能是数组 函数参数标识符 * `in`: 进复制到函数中,但不返回的参数(默认) * `out`: 不将参数复制到函数中,但返回参数 * `inout`: 复制到函数中并返回 ## 混合操作 通过在选择器(.)后列出各分量名,就可以选择这些分量 ``` glsl vec4 v4; v4.rgba; // 得到vec4 v4.rgb; // 得到vec3 v4.b; // 得到float v4.xy; // 得到vec2 v4.xgba; // 错误!分量名不是同一类 v4.wxyz; // 打乱原有分量顺序 v4.xxyy; // 重复分量 ``` 最后推荐一个GLSL编辑调试工具[OpenGL Shader Builder(Graphics Tools.dmg)](http://adcdownload.apple.com/Developer_Tools/Graphics_Tools_for_Xcode_7.2/Graphics_Tools_for_Xcode_7.2.dmg) # 总结: 由于本人记性不好使 找东西有时候总找不到 把一些 名词知识点收录出来并加以解释 方便后来的学习者学习. 参考: [GLSL基础](http://www.cnblogs.com/luweimy/p/4208570.html?utm_source=tuicool&utm_medium=referral) [iOS开发-OpenGL ES入门教程2](http://www.jianshu.com/p/ee597b2bd399) 全文完 URL: https://sunyazhou.com/2017/05/NSTableRowViewMouseTrackColor/index.html.md Published At: 2017-05-10 15:24:20 +0000 # 自定义NSTableRowView实现鼠标跟踪动态显示选中/非选中颜色 ``` objc #import "BDRowView.h" #define k_NORMAL_COLOR [NSColor colorFromInt:0xfcfdfe] #define k_SELECTED_COLOR [NSColor colorFromInt:0xeff1f3] @interface BDRowView () @property(strong) NSTrackingArea *trackingArea; @property(assign) BOOL isHovering; @end @implementation BDRowView - (void)drawRect:(NSRect)dirtyRect { [super drawRect:dirtyRect]; } - (void)drawSelectionInRect:(NSRect)dirtyRect { if (self.selectionHighlightStyle != NSTableViewSelectionHighlightStyleNone) { NSRect selectionRect = NSInsetRect(self.bounds, 0, 0); [k_SELECTED_COLOR setStroke]; [k_SELECTED_COLOR setFill]; NSBezierPath *selectionPath = [NSBezierPath bezierPathWithRoundedRect:selectionRect xRadius:0 yRadius:0]; [selectionPath fill]; [selectionPath stroke]; } } -(void)updateTrackingAreas { if(self.trackingArea != nil) { [self removeTrackingArea:self.trackingArea]; } int opts = (NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | NSTrackingActiveAlways); self.trackingArea = [ [NSTrackingArea alloc] initWithRect:[self bounds] options:opts owner:self userInfo:nil]; [self addTrackingArea:self.trackingArea]; } - (void)mouseEntered:(NSEvent *)theEvent { self.isHovering = YES; [self setBackgroundColor:[self getBackgroundColor:YES]]; } - (void)mouseExited:(NSEvent *)theEvent { self.isHovering = NO; [self setBackgroundColor:[self getBackgroundColor:NO]]; } -(NSColor*)getBackgroundColor:(BOOL)isSelected { if(isSelected) { return k_SELECTED_COLOR; }else{ return k_NORMAL_COLOR; } } @end ``` URL: https://sunyazhou.com/2017/05/HowToCreateSymbolicLinkOnMacosInCode/index.html.md Published At: 2017-05-09 17:41:17 +0000 # 如何在macOS/MAC OS X上创建替身文件 ![](/assets/images/20170509HowToCreateSymbolicLinkOnMacosInCode/symboliclink.webp) ### 前言 熟悉WIN 开发的同学一定很熟悉快捷方式,在macOS上叫做替身 最近开发插件相关逻辑 发现需要把插件复制到指定目录所以有了此文 ### 软连接 如果你深刻的理解了内存管理的原理,软连接就如同内存管理中的“指向指针的指针”,软连接本质就是指向硬连接的一个地址,自然它也只会对这一个硬连接有效,一旦软连接所指向的硬连接被删除,软连接也就失效了。当然这与”指针的指针”也有一个很微妙的差别,那就是你对软链接的操作都是通过跳转到硬连接再映射到了对节点的操作 创建软链接可以使用`NSFileManager`中的两个方法: ``` objc - (BOOL)createSymbolicLinkAtPath:(NSString *)path withDestinationPath:(NSString *)destPath error:(NSError **)error ; - (BOOL)createSymbolicLinkAtURL:(NSURL *)url withDestinationURL:(NSURL *)destURL error:(NSError **)error; ``` ### 使用场景 最近在开发插件 需要把插件从工程目录 copy到 系统的插件目录`~/Library/Internet Plug-Ins/` ([这里用了老谭的插件举例](http://www.tanhao.me/pieces/1084.html/)) 如下图: ![](/assets/images/20170509HowToCreateSymbolicLinkOnMacosInCode/step1.webp) 本想把它直接copy过去, 但可能存在以后升级问题,后续判断各种版本 删除旧的版本逻辑处理比较麻烦,于是想到用替身的方式实现 ![](/assets/images/20170509HowToCreateSymbolicLinkOnMacosInCode/step2.webp) 使用这种方式创建替身: ``` objc //工程目录文件 NSString *homePath = [[NSBundle mainBundle] pathForResource:@"NPAPI_Download_Plugin" ofType:@"plugin"]; //插件在系统的目录位置 NSString *strHome = [NSString stringWithUTF8String:getenv("HOME")]; NSString *desc = [NSString stringWithFormat:@"%@/Library/Internet Plug-Ins/NPAPI_Download_Plugin.plugin",strHome]; NSFileManager *fm = [NSFileManager defaultManager]; //创建替身代码 [fm createSymbolicLinkAtPath:desc withDestinationPath:homePath error:nil]; ``` *注意:`createSymbolicLinkAtPath:withDestinationPath:error:`方法 第一个参数`LinkAtPath`是`desc`,它是放替身文件的位置. 第二个参数`DestinationPath`是`homePath`代表本地文件的原始路径,这里用工程目录的文件是为了方便,切记不要和 copyItem方法搞混* ![](/assets/images/20170509HowToCreateSymbolicLinkOnMacosInCode/step3.webp) ### 总结 主要涉及的一些macOS开发技巧, 希望不足之处大家多多指教. 参考:[详解OSX(Unix)中的Hard Link与Symbolic Link(硬连接与软连接)](http://www.tanhao.me/pieces/597.html/) 全文完 URL: https://sunyazhou.com/2017/04/CmputerScienceAndTechnologyClassic/index.html.md Published At: 2017-04-29 09:15:21 +0000 # 向往计算机科学相关专业的必学经典 ![](/assets/images/20170429CmputerScienceAndTechnologyClassic/album.webp) # 前言 > 无论是何时毕业,何时参加工作,我的这篇文章将是大家迈进大学校门之前或者过程中荐举之章. __学习好一定要坚持下去,学习不好并不代表以后没有机会__, 如果你大学想学的是计算机相关专业,我可以把我之所学和必备的一些学习装备合盘托出毫无保留的告诉你, 并切能节省大部分学习开支. # 计算机专业学习路线 [计算机专业学习路线](https://hackway.org/docs/cs/intro) ![](/assets/images/20170429CmputerScienceAndTechnologyClassic/learnCSLine.webp) 据说站长是北大学生,参考美国名校的计算机培养方案,整理的计算机专业学习路线。 上边的才是最近更新学习计算机课程的正解,2023/06/30更新↑↑↑. ## 主要分3个方面 * 书籍 * 技法 * 装备 ### 书籍 首先来说 书籍 大学计算机必须的基本经典书籍 * [C语言程序设计 谭浩强](http://baike.baidu.com/link?url=rVNBy5FqKGq6YBb22T6Sj0IrRCeFj_SKf9QLjV7avP1cXIelBdhMza-y9Xu4fBHk0ynNI-RiFxbGySk68agqb1zrrQ2xPBZO9-WiqQe3AJy8IHEtHGBWGnxcwJlf77xiVaN3VdvdSn9_OaHodVRmuSl8MbPLBh5e0JzEhE0ikuNIGivWRklNzsL2WTD6WOJT) __必学__ ![](/assets/images/20170429CmputerScienceAndTechnologyClassic/CLanguage.webp) * [清华大学计算机系列教材:数据结构(C语言版)](https://item.jd.com/11076338.html) __必学__ ![](/assets/images/20170429CmputerScienceAndTechnologyClassic/DataStructure.webp) * [鸟哥的Linux私房菜 (基础学习篇 第三版)](https://item.jd.com/10064429.html) __必学__ ![](/assets/images/20170429CmputerScienceAndTechnologyClassic/linux.webp) *[Linux Torvalds博客](https://github.com/torvalds)* * [C++](https://item.jd.com/11017238.html) ![](/assets/images/20170429CmputerScienceAndTechnologyClassic/Cplusplus.webp) * Git ![](/assets/images/20170429CmputerScienceAndTechnologyClassic/git.webp) * 数据库 ![](/assets/images/20170429CmputerScienceAndTechnologyClassic/sqlite.webp) > 这个很重要可以简单理解成excel表格只不过是以文件存储数据的文件. eg:sqlite sql server 以上的书可以买纸质的,至于 python、java、js、nodejs、机器学习相关的大家可以不必买书.但考虑大家还是很潦倒的 可以从网上下载电子版学习是一样的. 比如ibook(苹果电脑上的epub格式) kindle(mobo格式) 还有pdf常用的格式. __但我还是建议不用买,耐心往下看__ ### 技法 * [免费的编程中文书籍索引](https://github.com/sunyazhou13/free-programming-books-zh_CN) 这就是不需要大家买书的原因,所有计算机相关的技术书籍和视频教程博客等全部资料都在这,是的你确实没看错就是一个链接搞定. * 学习一下如何搭建自己博客这样就可以把所学的知识点记录下来还能提高影响力 详情可以参考我的文章[如何搭建HEXO博客](http://localhost:4000/2017/02/10/build-hexo-blog-Tutorial/) * 每天尽量早起一个小时看书,预习今天要讲的内容. * 看大神的博客 * 订阅各种博客发布文章(推荐一个软件叫[Reeder](http://www.0daydown.com/12/436535.html)) ![](/assets/images/20170429CmputerScienceAndTechnologyClassic/Reeder.webp) (估计初学者还不知道谁是在某个领域的大神,这个可以慢慢摸索) 我是个学习iOS方向的所以我给出的一个学习资料的github链接和大神列表 * [中文 iOS/Mac 开发博客列表](https://github.com/tangqiaoboy/iOSBlogCN)是的你又没看错就是一个链接 这里有如何把博客的RSS搞到Reeder软件里 * [唐巧博客](http://blog.devtang.com/2014/07/27/ios-levelup-tips/) 下图是我的reeder ![](/assets/images/20170429CmputerScienceAndTechnologyClassic/ReederDetail.webp) ### 装备 这个是大家最关心的也是父母最心烦的事了. 大学装备真的很重要没错 那我先从电脑说起把 * Mac电脑(苹果电脑 macbook) ![](/assets/images/20170429CmputerScienceAndTechnologyClassic/macbook.webp) 因为它完美结合了 Unix 系的 Shell 和优秀的图形界面(跟window PC说再见吧 这台电脑一直能用到你参加工作) #### __方案1(高贵轻奢型)__ 如果要求比较高的可以买[MacBook Pro MF839CH](https://detail.tmall.com/item.htm?spm=a220m.1000858.1000725.9.7HDqEB&id=44131265268&skuId=79231665633&areaId=110100&standard=1&user_id=1669409267&cat_id=2&is_b=1&rn=892765ac3efe1f5cd3df8f0d2eb48f87) `¥7988` 840型号 貌似贵些 当然这些针对于家庭条件比较好的同学 ![](/assets/images/20170429CmputerScienceAndTechnologyClassic/macbookPRO.webp) #### __方案2(轻巧实用型)__ 如果要求一般也可以省钱买个13.3吋 的 [MacBook Air13.3](https://detail.tmall.com/item.htm?spm=a220m.1000858.1000725.1.7HDqEB&id=530945296812&skuId=3163301283248&areaId=110100&standard=1&user_id=2616970884&cat_id=2&is_b=1&rn=892765ac3efe1f5cd3df8f0d2eb48f87) 足够用了 我同事就有一个用了5年多 `¥6488` ![](/assets/images/20170429CmputerScienceAndTechnologyClassic/macbookair.webp) 大家是不是会觉得这个很贵、我既然说让你省钱就能让你省钱 往下看 #### __方案3(穷困潦倒型)__ 有一个苹果电脑它很便宜叫[mac mini](https://www.apple.com/cn/mac-mini/) 港版淘宝`¥3300`左右 ![](/assets/images/20170429CmputerScienceAndTechnologyClassic/macmini.webp) 注意: __*没有显示器和鼠标键盘*__ 需要自己买鼠标键盘 可以买个二手的显示器可以但是我记得一般大学都可以提供租借给同学使用 显示器+鼠标键盘 1500新的也够了 这个学习和使用完全能满足 只是大家很不习惯 苹果键盘和pc键盘按键有点稍稍区别 不过没关系 淘宝 50块钱能买个 蓝牙的苹果能用的键盘 这个是我认为最好的方案了 即经济又实惠 好说完电脑 我们继续往下说 * 树莓派 首先介绍一下这个是我所见过迄今为止 最便宜的最小的电脑 并且能运行linux各种操作系统 只不过是arm级别的 但是对于学生来说 非常适合学习 因为简单一个TF卡就可以当硬盘 键盘鼠标 wifi 蓝牙 网卡 全带 而且只需要 `¥275`左右 是的就是这么便宜 还是进口英国产的 如果国产的 只需要`¥230` ![](/assets/images/20170429CmputerScienceAndTechnologyClassic/raspberry.webp) 为啥树莓派是必备的是因为学习linux过程中 你总不能来回折腾电脑安装各种操作系统吧 其实这个也不是必要的 但是为了学习 shell建议 还是 用这个 比较方便 这个适合计算机相关专业的同学爱折腾 爱学习技术 玩技术 的必备 当我以后老去 我一定把这个放在我的博物馆里 *注意:这个需要显示器和鼠标键盘 如果上边选择方案3 这个非常适合* * 智能手机 这个我只能推荐大家买iPhone 或者便宜点的Android iPhone 5s/SE `¥3288`左右 ![](/assets/images/20170429CmputerScienceAndTechnologyClassic/iPhone.webp) 手机如果搞iOS开发当然要买个iPhone 现在不是钱的问题是移动互联网发展火爆的时代,得有个像样的装备以备不时之需. * Kindle 这个东西是个看书的好东西 上边说的那些图书 都可以通过 关注一些微信的公众号推送到这台设备免费获得, 一个很小的设备能装下很多书 很适合大家轻松学习不必带一堆没用沉重的书籍. `¥558` ![](/assets/images/20170429CmputerScienceAndTechnologyClassic/kindle.webp) ### 总结 可以这样说 看完上述文章 你至少知道如何学习并且方便的找到学习资料 下面我们开始算一笔入学装备的帐 `电脑 + 树莓派 + 智能手机 + Kindle = 大学学习保障` `方案1 + 树莓派 + 智能手机 + Kindle ` = ? `¥7988` + `¥275` + `¥3288` + `¥558` = `¥12109` 这个估计家长很难接受 适合 高贵轻奢型 `方案2 + 树莓派 + 智能手机 + Kindle ` = ? `¥6488` + `¥275` + `¥3288` + `¥558` = `¥10609` 这个也得1w多 主要费钱的是手机 如果手机 能省一些 估计 不到1w够了 `方案3 + 树莓派 + 智能手机 + Kindle ` = ? `¥3300` + `¥275` + `¥3288` + `¥558` = `¥7421` 这个是包含主设备 考虑到还要 租借显示器 和键盘 我们 暂且 加 ¥1000 `¥3300` + `¥275` + `¥3288` + `¥558` + `¥1000` = `¥8421` 最终还是 需要8K多 不过这已经 比较省了 如果手机买个Android那么 下面是这样的 `¥3300` + `¥275` + `¥699` + `¥558` + `¥1000` = `¥4832` 是的 5k足够了 其实这才是最经济的方案 如果 手机不用买 鼠标键盘也不用买的花 也就 4k多一点 无论是毕业生还是大学生 切记要让钱花在刀刃上 东西要物尽其用 尤其是大学期间 根本不需要多好 能用就行了 参加工作 自己自足之后 想买什么就不是问题了 **让自己的大学生活过得有章法一点.** 全文完 URL: https://sunyazhou.com/2017/04/UniqueFilenameInSystem/index.html.md Published At: 2017-04-20 16:35:42 +0000 # 如何在iOS/macOS系统中创建文件时创建唯一的文件名 ![](/assets/images/20170420UniqueFilenameInSystem/StockPhoto.webp) ## 前言 当我无数次看到大家写代码的时候总是以一个`时间戳+arc4random()`创建某文件的时候 深感心碎,难道操作系统就没有提供相关的函数么 于是 我找到了如下代码 解决大家因为创建文件重名问题. ``` objc /* Create a recording file */ NSString *filePath = [@"~/Movies/AVScreenShackRecording_XXXXXX" stringByStandardizingPath]; char *screenRecordingFileName = strdup([filePath fileSystemRepresentation]); if (screenRecordingFileName) { int fileDescriptor = mkstemp(screenRecordingFileName); if (fileDescriptor != -1) { NSString *filenameStr = [[NSFileManager defaultManager] stringWithFileSystemRepresentation:screenRecordingFileName length:strlen(screenRecordingFileName)]; NSLog(@"唯一的文件名:%@",filenameStr); } remove(screenRecordingFileName); free(screenRecordingFileName); } ``` 使用前 ![](/assets/images/20170420UniqueFilenameInSystem/before.webp) 过程中 ![](/assets/images/20170420UniqueFilenameInSystem/after.webp) 完成之后 ![](/assets/images/20170420UniqueFilenameInSystem/done.webp) *__切记文件后缀需要 加上 `XXXXXX`__* 几个`X`就代表几位`数字+字母`混合 *注意*:最好是6个X或者6个以上 [参考Linux](http://man7.org/linux/man-pages/man3/mkstemp.3.html) 主要的是要明白下面这两个函数 [strdup()用于c语言中常用的一种字符串拷贝](http://baike.baidu.com/item/strdup/5522525) [mkstemp()函数在系统中以唯一的文件名创建一个文件并打开](http://baike.baidu.com/link?url=wFhfkOVXafm15-4vGfxEQiQynIG7BG2yYAurwzS4uHKmby2C2lfhiO2T6WAqbdc3nOP9mEOVTMaBqxOc2eZps7_JIAsIWI0p11pEIl7Vku_) OK 希望大家有收获 全文完 URL: https://sunyazhou.com/2017/04/XcodeShortcuts/index.html.md Published At: 2017-04-06 10:36:30 +0000 # Xcode快捷键 ![shortcuts](/assets/images/20170406XcodeShortcuts/cover.webp) 前言 在iOS和macOS开发者中积累了一些快捷操作记录下来 隐藏左侧面板 -- ![](/assets/images/20170406XcodeShortcuts/Command0.webp) > **command+0**=显示/隐藏左侧面板 隐藏右侧面板 -- ![](/assets/images/20170406XcodeShortcuts/CommondOption0.webp) > **command+option+0**=显示/隐藏右侧面板 隐藏debug区域 -- ![](/assets/images/20170406XcodeShortcuts/CommandShiftY.webp) > **command+shift+y**=显示/隐藏底部控制等调试区域 跳转到代码多少行 -- ![](/assets/images/20170406XcodeShortcuts/CommandLine.webp) > **command + l (L)**=跳转到代码多少行 注意看清是l 不是i 隐藏/显示函数体 -- ![](/assets/images/20170406XcodeShortcuts/CommondOptionLeft.webp) > **command+option+⬅︎**=隐藏函数 `⬅︎`代表左箭头 iOS, macOS, Objective-C > **command+option+➡︎**=显示函数 `➡︎`代表右箭头 上下移动代码行 -- ![](/assets/images/20170406XcodeShortcuts/CommondOptionMove.webp) > **command + option + {**=向上移动代码行 > **command + option + }**=向下移动代码行 前后移动代码行 -- ![](/assets/images/20170406XcodeShortcuts/CommondMove.webp) > **command + {**=向前移动代码行 > **command + }**=向后移动代码行 全工程查找文件 -- ![](/assets/images/20170406XcodeShortcuts/CommandShiftO.webp) > **command + shift + o**=向前移动代码行 o大写字母 添加代码行注释 -- ![](/assets/images/20170406XcodeShortcuts/CommondOptionQ.webp) > **command + option + ?**=自动添加函数描述声明 撤销/反撤销 修改 -- > **command + z**=撤销 > **command + shift + z**=反撤销 后续有时间会持续更新 全文完 URL: https://sunyazhou.com/2017/03/AccessPrivacySensitive/index.html.md Published At: 2017-03-29 10:54:40 +0000 # 隐私及敏感数据访问权限(Access privacy-sensitive data) 在你访问照相机、通讯录、等等隐私以及敏感数据之前,你必须请求授权。否则你的app会在你尝试访问这些隐私时崩溃。Xcode会log这些: > This app has crashed because it attempted to access privacy-sensitive data without a usage description. The app's Info.plist must contain an NSContactsUsageDescription key with a string value explaining to the user how the app uses this data. 打开你工程中名叫 `info.plist` 的文件,右键点击选择 `opening as Source Code`,把下面的代码粘贴进去。或者你可以使用默认的 `Property List` 打开 `info.plist`,点击add按钮,当你输入 `Privacy` - Xcode会给你自动补全的建议,用上下键去选择吧。 私有数据的框架列表可是个不小的东西: > 通讯录 日历 提醒 照片 蓝牙共享 耳机 相机 定位 健康 homeKit 多媒体库 运动 callKit 语音识别 SiriKit TV Provider 参考 [这个大神](https://github.com/ChenYilong/iOS10AdaptationTips) ``` sh NSCameraUsageDescription 用于拍照捕捉视频内容,及拍摄短视频时访问相机 NSMicrophoneUsageDescription 用于拍摄短视频时访问麦克风收录视频声音 NSPhotoLibraryAddUsageDescription 用于保存拍摄完成的视频内容到相册,及选择相册内视频上传 NSPhotoLibraryUsageDescription 用于保存拍摄完成的视频内容到相册,及选择相册内视频上传 ``` 以上是常用的并且通过 App Store 审核的文本 下面是方便调试使用的隐私文案提醒 ``` objc NSPhotoLibraryUsageDescription $(PRODUCT_NAME) photo use NSPhotoLibraryAddUsageDescription $(PRODUCT_NAME) photo album use NSCameraUsageDescription $(PRODUCT_NAME) camera use NSMicrophoneUsageDescription $(PRODUCT_NAME) microphone use NSLocationUsageDescription $(PRODUCT_NAME) location use NSLocationWhenInUseUsageDescription $(PRODUCT_NAME) location use NSLocationAlwaysUsageDescription $(PRODUCT_NAME) always uses location NSCalendarsUsageDescription $(PRODUCT_NAME) calendar events NSRemindersUsageDescription $(PRODUCT_NAME) reminder use NSContactsUsageDescription $(PRODUCT_NAME) contact use NSMotionUsageDescription $(PRODUCT_NAME) motion use NSHealthUpdateUsageDescription $(PRODUCT_NAME) heath update use NSHealthShareUsageDescription $(PRODUCT_NAME) heath share use NSBluetoothPeripheralUsageDescription $(PRODUCT_NAME) Bluetooth Peripheral use NSAppleMusicUsageDescription $(PRODUCT_NAME) media library use NSSiriUsageDescription $(PRODUCT_NAME) siri use NSHomeKitUsageDescription $(PRODUCT_NAME) home kit use NSSpeechRecognitionUsageDescription $(PRODUCT_NAME) speech use NSVideoSubscriberAccountUsageDescription $(PRODUCT_NAME) tvProvider use NFCReaderUsageDescription $(PRODUCT_NAME) use the device’s NFC reader ``` > 注意:_上线前务必换成友好的提醒文案._ URL: https://sunyazhou.com/2017/03/LearningAVFoundationAVAudioRecorder/index.html.md Published At: 2017-03-28 09:40:18 +0000 # Learning AV Foundation(三)AVAudioRecorder ![](/assets/images/20170328LearningAVFoundationAVAudioRecorder/cover.webp) 前言 -- 在`AV Foundation`中使用`AVAudioRecorder`类添加音频录制功能和使用`AVAudioPlayer`一样简单, 都是在`Audio Queue Server`上层构建的.同时支持`macOS`和`iOS`平台.可以从内置麦克风录制音频,也可以支持数字音频接口或USB外接麦克风录制. 主要内容如下: -- 如何创建AVAudioRecorder 1. 音频格式 2. 采样率 3. 通道数 创建Demo 1. 配置音频会话 2. 实现录音功能 3. 使用Audio Metering实现声波视觉显示 创建`AVAudioRecorder`之前先了解一下它的方法和成员变量 ``` objc @property (readonly, getter=isRecording) BOOL recording;//是否正在录音 @property (readonly) NSDictionary *settings;//录音配置:采样率、音频格式、通道数... @property (readonly) NSURL *url;//录音文件存放URL @property (readonly) NSTimeInterval currentTime;//录音时长 @property (getter=isMeteringEnabled) BOOL meteringEnabled;//是否监控声波 ``` `AVAudioRecorder`的实例方法: ``` objc - (BOOL)prepareToRecord;//为录音准备缓冲区 - (BOOL)record;//录音开始,暂停后调用会恢复录音 - (BOOL)recordAtTime:(NSTimeInterval)time;//在指定时间后开始录音 - (BOOL)recordForDuration:(NSTimeInterval) duration;//按指定时长录音 - (BOOL)recordAtTime:(NSTimeInterval)time forDuration:(NSTimeInterval)duration;//上面2个的合体 - (void)pause; //暂停录音 - (void)stop; //停止录音 - (BOOL)deleteRecording;//删除录音,必须先停止录音再删除 ``` `AVAudioRecorder`的代理方法: ``` objc //录音完成后调用 - (void)audioRecorderDidFinishRecording:(AVAudioRecorder *)recorder successfully:(BOOL)flag; //录音编码发生错误时调用 - (void)audioRecorderEncodeErrorDidOccur:(AVAudioRecorder *)recorder error:(NSError *)error; ``` 如何创建`AVAudioRecorder` -- 创建`AVAudioRecorder`对象所需要的参数如下: * 音频流录制时写入到本地的路径URL * `settings`录音配置:采样率、音频格式、通道数...等键值参数字典 * 发生错误的`NSError`指针 如下代码: ``` objc /** 创建录音器 */ - (void)createRecorder { NSString *directory = NSTemporaryDirectory(); NSString *filePath = [directory stringByAppendingPathComponent:@"voice1.m4a"]; NSURL *url = [NSURL fileURLWithPath:filePath]; NSDictionary *setting = @{AVFormatIDKey : @(kAudioFormatMPEG4AAC), AVSampleRateKey: @22050.0f, AVNumberOfChannelsKey: @1}; NSError *error; self.recorder = [[AVAudioRecorder alloc] initWithURL:url settings:setting error:&error]; if (self.recorder) { [self.recorder prepareToRecord]; } else { NSLog(@"Recorder Create Error: %@", [error localizedDescription]); } } ``` 这里的建议调用`[self.recorder prepareToRecord]`方法对录音实例进行预设就像[上一章](http://sunyazhou.com/2017/03/17/Learning-AV-Foundation-AVAudioPlayer/)创建`AVAudioPlayer`类似.都是为了执行底层`Audio Queue`初始化的必要过程.这个`prepareToRecord`方法还在给定的URL参数指定的位置创建一个文件,这样就减少了录制启动时的延时 音频格式 -- `AVFormatIDKey`key指定录制格式,这里的除了`kAudioFormatMPEG4AAC`格式还有下面这些: ``` objc CF_ENUM(AudioFormatID) { kAudioFormatLinearPCM = 'lpcm', kAudioFormatAC3 = 'ac-3', kAudioFormat60958AC3 = 'cac3', kAudioFormatAppleIMA4 = 'ima4', kAudioFormatMPEG4AAC = 'aac ', kAudioFormatMPEG4CELP = 'celp', kAudioFormatMPEG4HVXC = 'hvxc', kAudioFormatMPEG4TwinVQ = 'twvq', kAudioFormatMACE3 = 'MAC3', kAudioFormatMACE6 = 'MAC6', kAudioFormatULaw = 'ulaw', kAudioFormatALaw = 'alaw', kAudioFormatQDesign = 'QDMC', kAudioFormatQDesign2 = 'QDM2', kAudioFormatQUALCOMM = 'Qclp', kAudioFormatMPEGLayer1 = '.mp1', kAudioFormatMPEGLayer2 = '.mp2', kAudioFormatMPEGLayer3 = '.mp3', kAudioFormatTimeCode = 'time', kAudioFormatMIDIStream = 'midi', kAudioFormatParameterValueStream = 'apvs', kAudioFormatAppleLossless = 'alac', kAudioFormatMPEG4AAC_HE = 'aach', kAudioFormatMPEG4AAC_LD = 'aacl', kAudioFormatMPEG4AAC_ELD = 'aace', kAudioFormatMPEG4AAC_ELD_SBR = 'aacf', kAudioFormatMPEG4AAC_ELD_V2 = 'aacg', kAudioFormatMPEG4AAC_HE_V2 = 'aacp', kAudioFormatMPEG4AAC_Spatial = 'aacs', kAudioFormatAMR = 'samr', kAudioFormatAMR_WB = 'sawb', kAudioFormatAudible = 'AUDB', kAudioFormatiLBC = 'ilbc', kAudioFormatDVIIntelIMA = 0x6D730011, kAudioFormatMicrosoftGSM = 0x6D730031, kAudioFormatAES3 = 'aes3', kAudioFormatEnhancedAC3 = 'ec-3' }; ``` 这里的`kAudioFormatLinearPCM`会将为压缩的音频流写入到文件中,这就是原始数据,保真度最高,当然文件也最大, 选择ACC`kAudioFormatMPEG4AAC`或者AppleIMA4`kAudioFormatAppleLossless`等格式会显著缩小文件,还能保证音频质量. > *注意:* > *指定的音频格式一定要和文件写入的URL文件类型保持一致。如果录制xxx.wav文件格式 是 Waveform Audio File Format(WAVE)的格式要求,即 低字节序、 LinePCM。 如果`AVFormatIDKey`指定的值不是`kAudioFormatLinearPCM`则会发生错误。NSError 会返回如下错误* > *The operation couldn't be completed. (OSState error 1718449215.)* 采样率 -- 上边的代码里`AVSampleRateKey`用于定义录音器的采样率. **采样率定义了对输入的模拟音频信号每一秒内的采样数**. 如果使用**低采样率** 比如8kHz,会导致粗粒度、AM广播类型的录制效果, 不过文件会比较小; 使用**44.1kHz的采样率(CD质量的采样率)**会得到非常高质量的内容, 不过文件比较大. 至于使用什么样的采样率没有明确的定义. 不过开发者应该尽量使用**标准的采样率,比如: 8000Hz、16 000Hz(16kHz)、22050Hz(22.05kHz)或 44100Hz(44.1kHz)、当然还有48000Hz和96000Hz** ,(kHz代表千赫),超过48000或96000的采样对人耳已经没有意义.最终是我们的耳朵在进行判断.([上一章](http://sunyazhou.com/2017/03/17/Learning-AV-Foundation-AVAudioPlayer/)说了 **人耳所能听到的声音,最低的频率是从20Hz起一直到最高频率20kHz**,录音最好采用 x 2 倍的频率) 通道数 -- `AVNumberOfChannelsKey`用于定义记录音频内容的通道数。**指定默认值1 意味着使用单声道录制**、**设置2意味着使用立体声录制**。除非使用外部硬件进行录制,否则同窗应该创建单声道录音。 这里的通道数是指 录制设备的输入数量 可以理解为 麦克风 内置 或者外接麦克风录制比如 插入Apple耳机 里面的麦克风。 > 以上是全面`AVAudioRecorder`的部分概念,`AVAudioRecorder`支持**无限时长录制**,还可以设置从**未来某一时间点开始录制**或**指定时长录制** 网络流媒体处理 -- `AVAudioPlayer`音频播放器只能播放本地文件,并且是一次性加载所有的音频数据,但我们有时候需要边下载边听怎么办? `AVAudioPlayer`是不支持这种网络流媒体形式的音频播放,要播放这种网络流媒体,我们需要使用`AudioToolbox`框架的音频队列服务`Audio Queue Services`。 __音频队列服务分为3个部分:__ > * 3个缓冲器 > * 1个缓冲队列 > * 1个回调 **1. 下面是录音的音频队列服务的工作原理:** ![](/assets/images/20170328LearningAVFoundationAVAudioRecorder/QueueServiceRecord.webp) **2. 下面是播放音频的音频队列服务的工作原理;** ![](/assets/images/20170328LearningAVFoundationAVAudioRecorder/QueueServicePlay.webp) 当然处理这些不需要我们自己去写C语言函数实现 有个开源库[FreeStreamer](https://github.com/sunyazhou13/FreeStreamer) FreeStreamer使用 ``` objc #import - (void)viewDidLoad { [super viewDidLoad]; [self initAudioStream]; //播放网络流媒体音频 [self.audioStream play]; } /* 初始化网络流媒体对象 */ - (void)initAudioStream{ NSString *urlStr = @"http://sc1.111ttt.com/2016/1/02/24/195242042236.mp3"; NSURL *url = [NSURL URLWithString:urlStr]; //创建FSAudioStream对象 self.audioStream = [[FSAudioStream alloc] initWithUrl:url]; //设置播放错误回调Block self.audioStream.onFailure = ^(FSAudioStreamError error, NSString *description){ NSLog(@"播放过程中发生错误,错误信息:%@",description); }; //设置播放完成回调Block self.audioStream.onCompletion = ^(){ NSLog(@"播放完成!"); }; [self.audioStream setVolume:0.5];//设置声音大小 } ``` 有点跑远了 回到正题 本章将不会把这个写到demo中 请谅解 下面我们来写个`AVAudioRecorder`的Demo 完成上述功能 == 配置会话 -- 首先创建以一个AVAudioRecorderDemo工程iOS平台这些相信大家非常熟练了. 在`AppDelegate`里面导入`#import ` 写上设置如下代码 ``` objc - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { AVAudioSession *session = [AVAudioSession sharedInstance]; NSError *error; if (![session setCategory:AVAudioSessionCategoryPlayAndRecord error:&error]) { NSLog(@"Category Error: %@",[error localizedDescription]); } //激活会话 if (![session setActive:YES error:&error]) { NSLog(@"Activation Error: %@",[error localizedDescription]); } return YES; } ``` 这`AVAudioSessionCategoryPlayAndRecord`是[上一章](http://sunyazhou.com/2017/03/17/Learning-AV-Foundation-AVAudioPlayer/)说的那几种Category,我们需要__录音+播放__功能 下一步 配置 plist文件访问权限信息 可以参考[Access privacy-sensitive data](http://localhost:4000/2017/03/20/Access-privacy-sensitive-data-private-access-permission/)这篇文章把访问权限需要的 信息填充上. ![plist1](/assets/images/20170328LearningAVFoundationAVAudioRecorder/FillInfo.webp) 然后选择SourceCode ![plist2](/assets/images/20170328LearningAVFoundationAVAudioRecorder/SourceCode.webp) 填写上 ``` xml NSMicrophoneUsageDescription $(PRODUCT_NAME) microphone use ``` 上边这些是为了访问本地授权, 记得授权如果第一次被拒就必须让用户手动 到通用-设置里面去配置否则将永远不好使哈。如果不写这种本地授权 程序应该会 crash 录音代码实现 -- 首先我们来封装一个类起名叫`BDRecoder`吧. 这里类我们让它负责所有 音频录制、暂停录制、保存录制文件等功能 并有回调函数等block. `BDRecoder.h`看起来像下面这样, 这里后续完善的话可以加个代理 表示录制过程中意外中断或者线路切换等逻辑. ``` objc // // BDRecorder.h // AVAudioRecorderDemo // // Created by sunyazhou on 2017/3/29. // Copyright © 2017年 Baidu, Inc. All rights reserved. // #import #import @class MemoModel; //录音停止的回调 typedef void (^BDRecordingStopCompletionHanlder)(BOOL); //保存录音文件完成的回调 typedef void (^BDRecordingSaveCompletionHanlder)(BOOL, id); @interface BDRecorder : NSObject /** * 外部获取当前录制的时间 * 小时:分钟:秒 当然后续可以加微秒和毫秒哈就是格式字符串 00:03:02 这样 */ @property (nonatomic, readonly) NSString *formattedCurrentTime; - (BOOL)record; //开始录音 - (void)pause; //暂停录音 - (void)stopWithCompletionHandler:(BDRecordingStopCompletionHanlder)handler; - (void)saveRecordingWithName:(NSString *)name completionHandler:(BDRecordingSaveCompletionHanlder)handler; /** 回放录制的文件 @param memo 备忘录文件model 放着当前播放的model @return 是否播放成功 */ - (BOOL)playbackURL:(MemoModel *)memo; @end ``` `BDRecoder.m` ``` objc // // BDRecorder.m // AVAudioRecorderDemo // // Created by sunyazhou on 2017/3/29. // Copyright © 2017年 Baidu, Inc. All rights reserved. // #import "BDRecorder.h" #import "MemoModel.h" @interface BDRecorder () @property (nonatomic, strong) AVAudioPlayer *player; @property (nonatomic, strong) AVAudioRecorder *recorder; @property (nonatomic, strong) BDRecordingStopCompletionHanlder completionHandler; @end @implementation BDRecorder - (instancetype)init { self = [super init]; if (self) { NSString *temDir = NSTemporaryDirectory(); NSString *filePath = [temDir stringByAppendingPathComponent:@"test1.caf"]; NSURL *fileURL = [NSURL fileURLWithPath:filePath]; NSDictionary *setting = @{AVFormatIDKey: @(kAudioFormatAppleIMA4), AVSampleRateKey: @44100.0f, AVNumberOfChannelsKey: @1, AVEncoderBitDepthHintKey: @16, AVEncoderAudioQualityKey: @(AVAudioQualityMedium) }; NSError *error; self.recorder = [[AVAudioRecorder alloc] initWithURL:fileURL settings:setting error:&error]; if (self.recorder) { self.recorder.delegate = self; [self.recorder prepareToRecord]; } else { NSLog(@"Create Recorder Error: %@",[error localizedDescription]); } } return self; } - (BOOL)record { return [self.recorder record]; } - (void)pause { [self.recorder pause]; } - (void)stopWithCompletionHandler:(BDRecordingStopCompletionHanlder)handler { self.completionHandler = handler; [self.recorder stop]; } - (void)saveRecordingWithName:(NSString *)name completionHandler:(BDRecordingSaveCompletionHanlder)handler { NSTimeInterval timestamp = [NSDate timeIntervalSinceReferenceDate]; NSString *filename = [NSString stringWithFormat:@"%@-%f.caf", name, timestamp]; NSString *docDir = [self documentsDirectory]; NSString *destPath = [docDir stringByAppendingPathComponent:filename]; NSURL *srcURL = self.recorder.url; NSURL *destURL = [NSURL fileURLWithPath:destPath]; NSError *error; BOOL success = [[NSFileManager defaultManager] copyItemAtURL:srcURL toURL:destURL error:&error]; if (success) { MemoModel *model = [MemoModel memoWithTitle:name url:destURL]; handler(YES, model); } } - (NSString *)documentsDirectory { NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); return [paths objectAtIndex:0]; } - (void)audioRecorderDidFinishRecording:(AVAudioRecorder *)recorder successfully:(BOOL)flag { if (self.completionHandler) { self.completionHandler(flag); } } @end ``` 这里的`self.completionHandler`当外部调用`stopWithCompletionHandler`的时候暂存一下block是为了录音完成时告诉外部通知一下以便于可以弹出一个UIAlertView去显示保存等操作 当停止录音, 进入语音备忘阶段命名阶段时 让外部调用`saveRecordingWithName:completionHandler `传入文件的命名,然后我们通过`self.recorder.url`获取到URL并且copy到tmp里面是目录并命名 下一步要实现`playbackURL:` 这里面有个`MemoModel`参数的对象, 这个`MemoModel`是一个对象model放着 文件name、url... ``` objc #import @interface MemoModel : NSObject @property (copy, nonatomic, readonly) NSString *title; @property (strong, nonatomic, readonly) NSURL *url; @property (copy, nonatomic, readonly) NSString *dateString; @property (copy, nonatomic, readonly) NSString *timeString; + (instancetype)memoWithTitle:(NSString *)title url:(NSURL *)url; - (BOOL)deleteMemo; @end //具体实现请参考我的最终demo ``` 实现播放部分需要创建播放器 这里就简单创建一下`AVAudioPlayer` ``` objc /** 回放录制的文件 @param memo 备忘录文件model 放着当前播放的model @return 是否播放成功 */ - (BOOL)playbackURL:(MemoModel *)memo { [self.player stop]; self.player = [[AVAudioPlayer alloc] initWithContentsOfURL:memo.url error:nil]; if (self.player) { [self.player prepareToPlay]; return YES; } return NO; } ``` 这里通过memo.url 给当前播放器播放, 这里就简单实现一下 如果需要复杂实现可以参考我上一章讲解的`AVAudioPlayer` 最后把显示事件部分的代码加上 ``` objc /** * 外部获取当前录制的时间 * 小时:分钟:秒 当然后续可以加微秒和毫秒哈就是格式字符串 00:03:02 这样 */ @property (nonatomic, readonly) NSString *formattedCurrentTime; ``` 这里我们需要复写`formattedCurrentTime`get方法获取时间格式例如: 00:00:00 ``` objc /** 返回当前录制的时间格式 HH:mm:ss @return 返回组装好的字符串 */ - (NSString *)formattedCurrentTime { NSUInteger time = (NSUInteger)self.recorder.currentTime; NSInteger hours = (time / 3600); NSInteger minutes = (time / 60) % 60; NSInteger seconds = time % 60; NSString *format = @"%02i:%02i:%02i"; return [NSString stringWithFormat:format, hours, minutes, seconds]; } ``` 上边大致是封装`BDRecorder`的过程 下面是对`ViewController`UI的设置, 设置好时间格式 我们需要在`ViewController`里 自己搞个定时器去更新录制的时间在UI上的显示, 因为`self.recorder.currentTime`是只读熟悉 没提供set方法 所以我们也无法用KVO监听recorder的属性变化. 代码如下: ``` objc // // ViewController.m // AVAudioRecorderDemo // // Created by sunyazhou on 2017/3/28. // Copyright © 2017年 Baidu, Inc. All rights reserved. // #import "ViewController.h" #import #import #import "BDRecorder.h" #import "LevelMeterView.h" #import "MemoModel.h" #import "MemoCell.h" #import "LevelPair.h" #define MEMOS_ARCHIVE @"memos.archive" @interface ViewController () @property (nonatomic, strong) NSMutableArray *memos; @property (nonatomic, strong) BDRecorder *recorder; @property (nonatomic, strong) NSTimer *timer; @property (nonatomic, strong) CADisplayLink *levelTimer; @property (weak, nonatomic) IBOutlet UIView *containerView; @property (weak, nonatomic) IBOutlet UIButton *recordButton; @property (weak, nonatomic) IBOutlet UIButton *stopButton; @property (weak, nonatomic) IBOutlet UILabel *timeLabel; @property (weak, nonatomic) IBOutlet LevelMeterView *levelMeterView; @property (weak, nonatomic) IBOutlet UITableView *tableview; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; self.recorder = [[BDRecorder alloc] init]; self.memos = [NSMutableArray array]; self.stopButton.enabled = NO; UIImage *recordImage = [[UIImage imageNamed:@"record"] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal]; UIImage *pauseImage = [[UIImage imageNamed:@"pause"] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal]; UIImage *stopImage = [[UIImage imageNamed:@"stop"] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal]; [self.recordButton setImage:recordImage forState:UIControlStateNormal]; [self.recordButton setImage:pauseImage forState:UIControlStateSelected]; [self.stopButton setImage:stopImage forState:UIControlStateNormal]; NSData *data = [NSData dataWithContentsOfURL:[self archiveURL]]; if (!data) { _memos = [NSMutableArray array]; } else { _memos = [NSKeyedUnarchiver unarchiveObjectWithData:data]; } [self.tableview registerNib:[UINib nibWithNibName:@"MemoCell" bundle:[NSBundle mainBundle]] forCellReuseIdentifier:@"MemoCell"]; [self layoutSubveiws]; } - (void)layoutSubveiws{ [self.containerView mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(self.view.mas_top).offset(30); make.left.equalTo(self.view.mas_left).offset(20); make.right.equalTo(self.view.mas_right).offset(-20); make.centerX.equalTo(self.view.mas_centerX); make.bottom.equalTo(self.tableview.mas_top).offset(-50); }]; [self.tableview mas_makeConstraints:^(MASConstraintMaker *make) { make.left.right.bottom.equalTo(self.view); make.top.equalTo(self.view.mas_top).offset(200); }]; [self.timeLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.top.left.right.equalTo(self.containerView); make.centerX.equalTo(self.containerView.mas_centerX); make.height.equalTo(@25); }]; [self.recordButton mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.containerView.mas_left); make.bottom.equalTo(self.containerView.mas_bottom); make.width.height.equalTo(@71); }]; [self.stopButton mas_makeConstraints:^(MASConstraintMaker *make) { make.right.equalTo(self.containerView.mas_right); make.bottom.equalTo(self.containerView.mas_bottom); make.width.height.equalTo(@71); }]; [self.levelMeterView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.right.equalTo(self.view); make.height.equalTo(@30); make.bottom.equalTo(self.tableview.mas_top); }]; } - (void)startTimer { [self.timer invalidate]; self.timer = [NSTimer timerWithTimeInterval:0.5 target:self selector:@selector(updateTimeDisplay) userInfo:nil repeats:YES]; [[NSRunLoop mainRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes]; } - (void)stopTimer { [self.timer invalidate]; self.timer = nil; } - (void)updateTimeDisplay { self.timeLabel.text = self.recorder.formattedCurrentTime; } - (void)startMeterTimer { [self.levelTimer invalidate]; self.levelTimer = [CADisplayLink displayLinkWithTarget:self selector:@selector(updateMeter)]; // if ([self.levelTimer respondsToSelector:@selector(setPreferredFramesPerSecond:)]) { // self.levelTimer.preferredFramesPerSecond = 5; // } else { self.levelTimer.frameInterval = 5; // } [self.levelTimer addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; } - (void)stopMeterTimer { [self.levelTimer invalidate]; self.levelTimer = nil; [self.levelMeterView resetLevelMeter]; } - (void)updateMeter { LevelPair *levels = [self.recorder levels]; self.levelMeterView.level = levels.level; self.levelMeterView.peakLevel = levels.peakLevel; [self.levelMeterView setNeedsDisplay]; } #pragma mark - #pragma mark - UITableViewDelegate - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return self.memos.count; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { MemoCell *cell = [tableView dequeueReusableCellWithIdentifier:@"MemoCell"]; cell.model = self.memos[indexPath.row]; return cell; } - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{ MemoModel *model = self.memos[indexPath.row]; [self.recorder playbackURL:model]; } - (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath { return YES; } - (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath { if (editingStyle == UITableViewCellEditingStyleDelete) { MemoModel *memo = self.memos[indexPath.row]; [memo deleteMemo]; [self.memos removeObjectAtIndex:indexPath.row]; [self saveMemos]; [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; } } - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { return 80; } #pragma mark - event response 所有触发的事件响应 按钮、通知、分段控件等 - (IBAction)record:(UIButton *)sender { self.stopButton.enabled = YES; if ([sender isSelected]) { [self stopMeterTimer]; [self stopTimer]; [self.recorder pause]; } else { [self startMeterTimer]; [self startTimer]; [self.recorder record]; } [sender setSelected:![sender isSelected]]; } - (IBAction)stopRecording:(UIButton *)sender { [self stopMeterTimer]; self.recordButton.selected = NO; self.stopButton.enabled = NO; [self.recorder stopWithCompletionHandler:^(BOOL result) { double delayInSeconds = 0.01; dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t) (delayInSeconds * NSEC_PER_SEC)); dispatch_after(popTime, dispatch_get_main_queue(), ^{ [self showSaveDialog]; }); }]; } - (void)showSaveDialog { UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"保存录音" message:@"输入名称" preferredStyle:UIAlertControllerStyleAlert]; [alertController addTextFieldWithConfigurationHandler:^(UITextField * _Nonnull textField) { textField.placeholder = @"我的录音"; }]; UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:nil]; UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { NSString *filename = [alertController.textFields.firstObject text]; [self.recorder saveRecordingWithName:filename completionHandler:^(BOOL success, id object) { if (success) { [self.memos insertObject:object atIndex:0]; [self saveMemos]; [self.tableview reloadData]; } else { NSLog(@"Error saving file: %@", [object localizedDescription]); } }]; }]; [alertController addAction:cancelAction]; [alertController addAction:okAction]; [self presentViewController:alertController animated:YES completion:nil]; } #pragma mark - Memo Archiving //保存备忘录model 这里简单用归档的方式存储一下 - (void)saveMemos { NSData *fileData = [NSKeyedArchiver archivedDataWithRootObject:self.memos]; [fileData writeToURL:[self archiveURL] atomically:YES]; } //存储归档的路径 - (NSURL *)archiveURL { NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); NSString *docsDir = [paths objectAtIndex:0]; NSString *archivePath = [docsDir stringByAppendingPathComponent:MEMOS_ARCHIVE]; return [NSURL fileURLWithPath:archivePath]; } - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; // Dispose of any resources that can be recreated. } @end ``` 代码稍稍有点长 我简单说一下 大家可以参照最终的demo ``` objc @property (nonatomic, strong) NSMutableArray *memos; @property (nonatomic, strong) BDRecorder *recorder; ``` 声明一个数组 存放需要播放的model对象信息 名称 文件url、日期等 ``` objc @property (nonatomic, strong) NSTimer *timer; @property (nonatomic, strong) CADisplayLink *levelTimer; ``` 一个timer用于 刷新录制时间 `levelTimer`用于刷新录制的视波图也叫`Audio Metering`对音频进行计量 在`BDRecorder`中增加了 ``` objc - (LevelPair *)levels { [self.recorder updateMeters]; float avgPower = [self.recorder averagePowerForChannel:0]; float peakPower = [self.recorder peakPowerForChannel:0]; float linearLevel = [self.meterTable valueForPower:avgPower]; float linearPeak = [self.meterTable valueForPower:peakPower]; return [LevelPair levelsWithLevel:linearLevel peakLevel:linearPeak]; } ``` 这两个方法 1. averagePowerForChannel取出波谷平均值 2. peakPowerForChannel取出波峰 两个方法都会返回一个用于表示声音分贝(dB)等级的浮点值. 这个值的表示范围`0dB(fullscale) ~ -160dB` 0dB最大 -160dB最小 **开启音频计量 (需要在`BDRecorder`中开启, 如下代码) 会带来很多额外的开销,但我觉得还是很划算的 毕竟要显示视觉效果才是王道. 如果`meteringEnabled`开启则音频录音器就会对捕捉到的音频样本进行分贝计算。** **开启音频计量(Audio Metering)方法:** ``` objc self.recorder.meteringEnabled = YES; ``` 更新前调用了如下代码 ``` objc - (LevelPair *)levels { [self.recorder updateMeters]; ... } ``` 每当读取值之前需要调用`[self.recorder updateMeters]`方法才能获取到最新值,否则可能获取的不够精确 然后 使用`MeterTable`类 声明的函数`valueForPower:` 把上边两个阀值 转成线性运算 **就是分贝值从对数形式的`-160 ~ 0`范围转换为线性0到1的形式.** ``` objc // // MeterTable.m // AVAudioRecorderDemo // // Created by sunyazhou on 2017/4/5. // Copyright © 2017年 Baidu, Inc. All rights reserved. // #import "MeterTable.h" #define MIN_DB -60.0f #define TABLE_SIZE 300 @implementation MeterTable { float _scaleFactor; NSMutableArray *_meterTable; } - (id)init { self = [super init]; if (self) { float dbResolution = MIN_DB / (TABLE_SIZE - 1); _meterTable = [NSMutableArray arrayWithCapacity:TABLE_SIZE]; _scaleFactor = 1.0f / dbResolution; float minAmp = dbToAmp(MIN_DB); float ampRange = 1.0 - minAmp; float invAmpRange = 1.0 / ampRange; for (int i = 0; i < TABLE_SIZE; i++) { float decibels = i * dbResolution; float amp = dbToAmp(decibels); float adjAmp = (amp - minAmp) * invAmpRange; _meterTable[i] = @(adjAmp); } } return self; } float dbToAmp(float dB) { return powf(10.0f, 0.05f * dB); } - (float)valueForPower:(float)power { if (power < MIN_DB) { return 0.0f; } else if (power >= 0.0f) { return 1.0f; } else { int index = (int) (power * _scaleFactor); return [_meterTable[index] floatValue]; } } @end ``` > **这个类创建了一个数组`_meterTable`保存从计算前的分贝数到使用一定级别分贝解析之后的转换结果, 这里使用的解析率`-0.2dB`, 解析等级可以通过`MIN_DB` `TABLE_SIZE`这两个宏的值来修改,每个分贝值都调用`dbToAmp:`函数转换为线性范围内的值,使其处于`0(-60dB) ~ 1()`范围内, 之后由这些范围内的值构成平行曲线,开平方计算并保存到内部查找表格中. 然后如果外部需要可以调用`valueForPower:`来获取.** 然后保存到`LevelPair`的实例对象返回 这个实例很简单存放两个值一个`level`一个`peakLevel` ``` objc @interface LevelPair : NSObject @property (nonatomic, readonly) float level; @property (nonatomic, readonly) float peakLevel; + (instancetype)levelsWithLevel:(float)level peakLevel:(float)peakLevel; - (instancetype)initWithLevel:(float)level peakLevel:(float)peakLevel; @end ``` 在`ViewController`中显示相关的UI ``` objc - (void)startTimer { [self.timer invalidate]; self.timer = [NSTimer timerWithTimeInterval:0.5 target:self selector:@selector(updateTimeDisplay) userInfo:nil repeats:YES]; [[NSRunLoop mainRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes]; } - (void)stopTimer { [self.timer invalidate]; self.timer = nil; } - (void)updateTimeDisplay { self.timeLabel.text = self.recorder.formattedCurrentTime; } - (void)startMeterTimer { [self.levelTimer invalidate]; self.levelTimer = [CADisplayLink displayLinkWithTarget:self selector:@selector(updateMeter)]; // if ([self.levelTimer respondsToSelector:@selector(setPreferredFramesPerSecond:)]) { // self.levelTimer.preferredFramesPerSecond = 5; // } else { self.levelTimer.frameInterval = 5; // } [self.levelTimer addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; } - (void)stopMeterTimer { [self.levelTimer invalidate]; self.levelTimer = nil; [self.levelMeterView resetLevelMeter]; } - (void)updateMeter { LevelPair *levels = [self.recorder levels]; self.levelMeterView.level = levels.level; self.levelMeterView.peakLevel = levels.peakLevel; [self.levelMeterView setNeedsDisplay]; } ``` 用于定时器的处理 事件的相关响应 ``` objc #pragma mark - event response 所有触发的事件响应 按钮、通知、分段控件等 - (IBAction)record:(UIButton *)sender { self.stopButton.enabled = YES; if ([sender isSelected]) { [self stopMeterTimer]; [self stopTimer]; [self.recorder pause]; } else { [self startMeterTimer]; [self startTimer]; [self.recorder record]; } [sender setSelected:![sender isSelected]]; } - (IBAction)stopRecording:(UIButton *)sender { [self stopMeterTimer]; self.recordButton.selected = NO; self.stopButton.enabled = NO; [self.recorder stopWithCompletionHandler:^(BOOL result) { double delayInSeconds = 0.01; dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t) (delayInSeconds * NSEC_PER_SEC)); dispatch_after(popTime, dispatch_get_main_queue(), ^{ [self showSaveDialog]; }); }]; } ``` 这里保存数据使用的是归档方式 `BDRecorder`没有 处理意外中断等情况 比如外接麦克风 和音频意外来电等,如果需要处理 就可以在`BDRecorder`中声明几个代理监听音频回话的那几个通知就可以了 这里出于学习为目的就简单写到这里吧,如果大家需求强烈我可以回头补上并开源。 很多人纠结如何根据波形绘制更好的图 我这里是借助本书作者的demo完成相关波形处理的视图。 ``` objc #import "LevelMeterView.h" #import "LevelMeterColorThreshold.h" @interface LevelMeterView () @property (nonatomic) NSUInteger ledCount; @property (strong, nonatomic) UIColor *ledBackgroundColor; @property (strong, nonatomic) UIColor *ledBorderColor; @property (nonatomic, strong) NSArray *colorThresholds; @end @implementation LevelMeterView - (id)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { [self setupView]; } return self; } - (id)initWithCoder:(NSCoder *)coder { self = [super initWithCoder:coder]; if (self) { [self setupView]; } return self; } - (void)setupView { self.backgroundColor = [UIColor clearColor]; _ledCount = 20; _ledBackgroundColor = [UIColor colorWithWhite:0.0f alpha:0.35f]; _ledBorderColor = [UIColor blackColor]; UIColor *greenColor = [UIColor colorWithRed:0.458 green:1.000 blue:0.396 alpha:1.000]; UIColor *yellowColor = [UIColor colorWithRed:1.000 green:0.930 blue:0.315 alpha:1.000]; UIColor *redColor = [UIColor colorWithRed:1.000 green:0.325 blue:0.329 alpha:1.000]; _colorThresholds = @[[LevelMeterColorThreshold colorThresholdWithMaxValue:0.5 color:greenColor name:@"green"], [LevelMeterColorThreshold colorThresholdWithMaxValue:0.8 color:yellowColor name:@"yellow"], [LevelMeterColorThreshold colorThresholdWithMaxValue:1.0 color:redColor name:@"red"]]; } - (void)drawRect:(CGRect)rect { CGContextRef context = UIGraphicsGetCurrentContext(); CGContextTranslateCTM(context, 0, CGRectGetHeight(self.bounds)); CGContextRotateCTM(context, (CGFloat) -M_PI_2); CGRect bounds = CGRectMake(0., 0., [self bounds].size.height, [self bounds].size.width); CGFloat lightMinValue = 0.0f; NSInteger peakLED = -1; if (self.peakLevel > 0.0f) { peakLED = self.peakLevel * self.ledCount; if (peakLED >= self.ledCount) { peakLED = self.ledCount - 1; } } for (int ledIndex = 0; ledIndex < self.ledCount; ledIndex++) { UIColor *ledColor = [self.colorThresholds[0] color]; CGFloat ledMaxValue = (CGFloat) (ledIndex + 1) / self.ledCount; for (int colorIndex = 0; colorIndex < self.colorThresholds.count - 1; colorIndex++) { LevelMeterColorThreshold *currThreshold = self.colorThresholds[colorIndex]; LevelMeterColorThreshold *nextThreshold = self.colorThresholds[colorIndex + 1]; if (currThreshold.maxValue <= ledMaxValue) { ledColor = nextThreshold.color; } } CGFloat height = CGRectGetHeight(bounds); CGFloat width = CGRectGetWidth(bounds); CGRect ledRect = CGRectMake(0.0f, height * ((CGFloat) ledIndex / self.ledCount), width, height * (1.0f / self.ledCount)); // Fill background color CGContextSetFillColorWithColor(context, self.ledBackgroundColor.CGColor); CGContextFillRect(context, ledRect); // Draw Light CGFloat lightIntensity; if (ledIndex == peakLED) { lightIntensity = 1.0f; } else { lightIntensity = clamp((self.level - lightMinValue) / (ledMaxValue - lightMinValue)); } UIColor *fillColor = nil; if (lightIntensity == 1.0f) { fillColor = ledColor; } else if (lightIntensity > 0.0f) { CGColorRef color = CGColorCreateCopyWithAlpha([ledColor CGColor], lightIntensity); fillColor = [UIColor colorWithCGColor:color]; CGColorRelease(color); } CGContextSetFillColorWithColor(context, fillColor.CGColor); UIBezierPath *fillPath = [UIBezierPath bezierPathWithRoundedRect:ledRect cornerRadius:2.0f]; CGContextAddPath(context, fillPath.CGPath); // Stroke border CGContextSetStrokeColorWithColor(context, self.ledBorderColor.CGColor); UIBezierPath *strokePath = [UIBezierPath bezierPathWithRoundedRect:CGRectInset(ledRect, 0.5, 0.5) cornerRadius:2.0f]; CGContextAddPath(context, strokePath.CGPath); CGContextDrawPath(context, kCGPathFillStroke); lightMinValue = ledMaxValue; } } CGFloat clamp(CGFloat intensity) { if (intensity < 0.0f) { return 0.0f; } else if (intensity >= 1.0) { return 1.0f; } else { return intensity; } } - (void)resetLevelMeter { self.level = 0.0f; self.peakLevel = 0.0f; [self setNeedsDisplay]; } @end ``` 这里给出了level和peak的阀值 有很多第三方开源的view大家可以自行研究一下 很简单 就是把相关阀值量化的过程。 总结 -- `AVAudioRecorder` 的学习还算完整的搞完了,随时记录一下学习内容和技术知识。 ![](/assets/images/20170328LearningAVFoundationAVAudioRecorder/FinalDemo.webp) __最终[Demo](https://github.com/sunyazhou13/AVAudioRecorderDemo)__ 欢迎大家指正错误 全文完 URL: https://sunyazhou.com/2017/03/CheckNSWindowisFullScreen/index.html.md Published At: 2017-03-23 14:03:32 +0000 # 判断NSWindow是否全屏 ``` objc @interface NSWindow (FullScreen) - (BOOL)mn_isFullScreen; @end @implementation NSWindow (FullScreen) - (BOOL)mn_isFullScreen { return (([self styleMask] & NSFullScreenWindowMask) == NSFullScreenWindowMask); } @end ``` refs:[How to know if a NSWindow is fullscreen in Mac OS X Lion?](http://stackoverflow.com/questions/6815917/how-to-know-if-a-nswindow-is-fullscreen-in-mac-os-x-lion) URL: https://sunyazhou.com/2017/03/LearningAVFoundationAVAudioPlayer/index.html.md Published At: 2017-03-17 10:26:06 +0000 # Learning AV Foundation(二)AVAudioPlayer ![AVAudioPlayer](/assets/images/20170317LearningAVFoundationAVAudioPlayer/cover.webp) 开篇 -- 最近在学习`AV Foundation` 试图把学习内容记录下来 并参考一些博客文章 本期的内容是`AVAudioPlayer` 音频知识基础 -- > 音频文件的生成过程是将声音信息__采样__、__量化__和__编码__产生的数字信号的过程,__人耳所能听到的声音,最低的频率是从20Hz起一直到最高频率20KHZ__,因此音频文件格式的最大带宽是20KHZ。根据[奈奎斯特](https://zh.wikipedia.org/wiki/%E5%A5%88%E5%A5%8E%E6%96%AF%E7%89%B9%E9%A2%91%E7%8E%87)的理论,只有采样频率高于声音信号最高频率的两倍时,才能把数字信号表示的声音还原成为原来的声音,所以音频文件的采样率一般在__40~50KHZ__,比如最常见的CD音质采样率__44.1KHZ__。 (所以一般大家都觉得CD音质是最好的.) 对声音进行采样、量化过程被称为[脉冲编码调制](https://zh.wikipedia.org/wiki/%E8%84%88%E8%A1%9D%E7%B7%A8%E8%99%9F%E8%AA%BF%E8%AE%8A)(Pulse Code Modulation),简称PCM。PCM数据是最原始的音频数据完全无损,所以PCM数据虽然音质优秀但体积庞大,为了解决这个问题先后诞生了一系列的音频格式,这些音频格式运用不同的方法对音频数据进行压缩,其中有无损压缩(ALAC、APE、FLAC)和有损压缩(MP3、AAC、OGG、WMA)两种 来源:[iOS音频播放 (一):概述](http://msching.github.io/blog/2014/07/07/audio-in-ios/) by [码农人生](http://msching.github.io/) -- 我觉得程寅大牛的处理音频说的很明白 大神列出一个经典的音频播放流程(以MP3为例) 1. 读取MP3文件 2. 解析采样率、码率、时长等信息,分离MP3中的音频帧 3. 对分离出来的音频帧解码得到PCM数据 4. 对PCM数据进行音效处理(均衡器、混响器等,非必须) 5. 把PCM数据解码成音频信号 6. 把音频信号交给硬件播放 7. 重复1-6步直到播放完成 在iOS系统中apple对上述的流程进行了封装并提供了不同层次的接口 ![](https://developer.apple.com/library/content/documentation/MusicAudio/Conceptual/CoreAudioOverview/Art/core_audio_layers_2x.webp) > 这是CoreAudio的接口层次 下面对其中的中高层接口进行功能说明: * Audio File Services:读写音频数据,可以完成播放流程中的第2步; * Audio File Stream Services:对音频进行解码,可以完成播放流程中的第2步; * Audio Converter services:音频数据转换,可以完成播放流程中的第3步; * Audio Processing Graph Services:音效处理模块,可以完成播放流程中的第4步; * Audio Unit Services:播放音频数据:可以完成播放流程中的第5步、第6步; * Extended Audio File Services:Audio File Services和Audio * Converter services的结合体; * AVAudioPlayer/AVPlayer(AVFoundation):高级接口,可以完成整个音频播放的过程(包括本地文件和网络流播放,第4步除外); * Audio Queue Services:高级接口,可以进行录音和播放,可以完成播放流程中的第3、5、6步; * OpenAL:用于游戏音频播放,暂不讨论 可以看到apple提供的接口类型非常丰富,可以满足各种类别类需求: * 如果你只是想实现音频的播放,没有其他需求AVFoundation会很好的满足你的需求。它的接口使用简单、不用关心其中的细节; * 如果你的app需要对音频进行流播放并且同时存储,那么AudioFileStreamer加AudioQueue能够帮到你,你可以先把音频数据下载到本地,一边下载一边用NSFileHandler等接口读取本地音频文件并交给AudioFileStreamer或者AudioFile解析分离音频帧,分离出来的音频帧可以送给AudioQueue进行解码和播放。如果是本地文件直接读取文件解析即可。(这两个都是比较直接的做法,这类需求也可以用AVFoundation+本地server的方式实现,AVAudioPlayer会把请求发送给本地server,由本地server转发出去,获取数据后在本地server中存储并转送给AVAudioPlayer。另一个比较trick的做法是先把音频下载到文件中,在下载到一定量的数据后把文件路径给AVAudioPlayer播放,当然这种做法在音频seek后就回有问题了。) * 如果你正在开发一个专业的音乐播放软件,需要对音频施加音效(均衡器、混响器),那么除了数据的读取和解析以外还需要用到AudioConverter来把音频数据转换成PCM数据,再由AudioUnit+AUGraph来进行音效处理和播放(但目前多数带音效的app都是自己开发音效模块来坐PCM数据的处理,这部分功能自行开发在自定义性和扩展性上会比较强一些。PCM数据通过音效器处理完成后就可以使用AudioUnit播放了,当然AudioQueue也支持直接使对PCM数据进行播放。)。下图描述的就是使用AudioFile + AudioConverter + AudioUnit进行音频播放的流程 ![](http://msching.github.io/images/iOS-audio/audioUnitPlay.webp) 以上内容均转自[码农人生](http://msching.github.io/blog/2014/07/07/audio-in-ios/) 希望大神不要介意 如果有问题 我可立即清除 使用`AVAudioPlayer `之前对AudioSession简介 -- > `AVAudioSession`负责管理音频会话 它是个单例 在应用程序和操作系统之间负责中间人的角色 [AudioSession参考](http://msching.github.io/blog/2014/07/08/audio-in-ios-2/) `AVAudioSession`主要功能包括以下几点: * app是如何使用的音频服务 播放 还是录制 之类的 * 控制协调app输入输出设备(比如 麦克风,耳机、手机外放比如蓝牙连接一个外置音响 或airplay) * 协调你的app的音频播放和系统以及其他app行为(例如有电话时需要打断,电话结束时需要恢复,按下静音按钮时是否歌曲也要静音等) ![](https://developer.apple.com/library/content/documentation/Audio/Conceptual/AudioSessionProgrammingGuide/Art/aspg_intro_2x.webp) *注:AVAudioSession iOS6以后使用 以前叫AudioSession* 如何使用`AVAudioPlayer` -- 在我的博客里面我尽量使用code胜过千言万语 使用`AVAudioPlayer`之前需要在`AppDelegate`里面导入`#import ` 并且启动音频会话 ``` objc - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { AVAudioSession *session = [AVAudioSession sharedInstance]; NSError *error; if (![session setCategory:AVAudioSessionCategoryPlayback error:&error]) { NSLog(@"Category Error: %@", [error localizedDescription]); } if (![session setActive:YES error:&error]) { NSLog(@"Activation Error: %@", [error localizedDescription]); } return YES; } ``` 上边已经介绍了`AVAudioSession` 这里面说一下`[session setCategory:AVAudioSessionCategoryPlayback error:&error]` 里面的`AVAudioSessionCategoryPlayback` ![音频会话分类](/assets/images/20170317LearningAVFoundationAVAudioPlayer/AVAudioPlayerCategory.webp) 这是这几种分类的列表大家可以看下 记得开启后台播放 ![](/assets/images/20170317LearningAVFoundationAVAudioPlayer/BackgounrdPlay.webp) 或者在plist里面修改 ![](/assets/images/20170317LearningAVFoundationAVAudioPlayer/PlistModify.webp) 下面就是创建音频播放器代码 ``` objc #import "ViewController.h" #import #import "THControlKnob.h" #import "THPlayButton.h" #import @interface ViewController () //三个控制推子 @property (weak, nonatomic) IBOutlet THOrangeControlKnob *panKnob; @property (weak, nonatomic) IBOutlet THOrangeControlKnob *volumnKnob; @property (weak, nonatomic) IBOutlet THGreenControlKnob *rateKnob; @property (weak, nonatomic) IBOutlet THPlayButton *playButton; //音乐播放器 @property (nonatomic, strong) AVAudioPlayer *musicPlayer; @property (nonatomic, getter = isPlaying) BOOL playing; //播放状态 //无关代码 @property (weak, nonatomic) IBOutlet UILabel *LeftRightRoundDec; @property (weak, nonatomic) IBOutlet UILabel *voiceDec; @property (weak, nonatomic) IBOutlet UILabel *rateDec; @property (weak, nonatomic) IBOutlet UILabel *trackDescrption; @end ``` > 导入几个第三方控件的类用于音乐播放 ![](/assets/images/20170317LearningAVFoundationAVAudioPlayer/Buttons.webp) 这上边的三个旋钮就是导入的开源库 下面创建播放器`AVAudioPlayer` 创建时需要一个`NSURL`代表要播放的文件路径 这里简单从bundle中拖了一首歌进去了 ``` objc #pragma mark - #pragma mark - 创建AVAudioPlayer与播放状态控制 /** 创建音乐播放器 @param fileName 文件名 @param fileExtension 文件扩展名 @return 播放器实例 */ - (AVAudioPlayer *)createPlayForFile:(NSString *)fileName withExtension:(NSString *)fileExtension{ NSURL *url = [[NSBundle mainBundle] URLForResource:fileName withExtension:fileExtension]; NSError *error = nil; AVAudioPlayer *audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:url error:&error]; if (audioPlayer) { audioPlayer.numberOfLoops = -1; //-1无限循环 audioPlayer.enableRate = YES; //启动倍速控制 [audioPlayer prepareToPlay]; } else { NSLog(@"Error creating player: %@",[error localizedDescription]); } return audioPlayer; } ``` `numberOfLoops` = -1; 代表本首歌 无限循环 其它常数代表循环次数 `enableRate` 代表是否启用倍速调节 0.5x 1.0x 2.0x 等倍速 1.0代表正常速度 这里说一下`[audioPlayer prepareToPlay]` __调用这个函数是为了取得需要的音频硬件并预加载`Audio Queue`的缓冲区.__ 当然也可以不调用这个方法直接调用 `[audioPlayer play]`,但当 __调用`play`方法时也会隐性激活__,调用`prepareToPlay`是为了减少 创建播放器时预设加载和听到声音输出之间的延时 ``` objc @implementation ViewController - (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; if (self) { if (self.musicPlayer == nil) { self.musicPlayer = [self createPlayForFile:@"384551_1438267683" withExtension:@"mp3"]; } [self setupNotifications]; } return self; } - (void)awakeFromNib{ [super awakeFromNib]; if (self.musicPlayer == nil) { self.musicPlayer = [self createPlayForFile:@"384551_1438267683" withExtension:@"mp3"]; } [self setupNotifications]; } ``` > 在`initWithNibName`或`awakeFromNib`时候调用一下创建播放器的代码 这个`[self setupNotifications];`后面说 先添加一些常见的方法封装 比如 __播放、暂停、停止__ ``` objc - (void)play { if (self.musicPlayer == nil) { return; } if (!self.playing) { NSTimeInterval delayTime = [self.musicPlayer deviceCurrentTime] + 0.01; [self.musicPlayer playAtTime:delayTime]; self.playing = YES; } self.trackDescrption.text = [self.musicPlayer.url absoluteString]; [self configNowPlayingInfoCenter]; //配置后台播放的页面信息 } - (void)stop { if (self.musicPlayer == nil) { return; } if (self.playing) { [self.musicPlayer stop]; self.musicPlayer.currentTime = 0.0f; self.playing = NO; } } - (void)pause { if (self.musicPlayer == nil) { return; } if (self.playing) { [self.musicPlayer pause]; self.playing = NO; } } ``` 这里看到`[self.musicPlayer deviceCurrentTime] + 0.01` 加了 -0.01的延时, 是为了以后大家做播放器的时候 有可能暂停或者歌曲切换时 有可能 向前向后做片段衔接, 也是为了使用 `playAtTime`去播放 指定位置的音乐用于 意外暂停或者播放上次播放的配置信息使用 这里看到我写了一个 `[self configNowPlayingInfoCenter];`配置后台播放的页面信息 这个主要用于播放音乐在后台时 锁屏显示的屏幕信息 请看下面代码 ``` objc //设置锁屏状态,显示的歌曲信息 -(void)configNowPlayingInfoCenter{ if (NSClassFromString(@"MPNowPlayingInfoCenter")) { NSMutableDictionary *dict = [[NSMutableDictionary alloc] init]; //歌曲名称 [dict setObject:@"歌曲名称" forKey:MPMediaItemPropertyTitle]; //演唱者 [dict setObject:@"演唱者" forKey:MPMediaItemPropertyArtist]; //专辑名 [dict setObject:@"专辑名" forKey:MPMediaItemPropertyAlbumTitle]; //专辑缩略图 UIImage *image = [UIImage imageNamed:@"sunyazhou"]; MPMediaItemArtwork *artwork = [[MPMediaItemArtwork alloc] initWithImage:image]; [dict setObject:artwork forKey:MPMediaItemPropertyArtwork]; //音乐剩余时长 [dict setObject:@20 forKey:MPMediaItemPropertyPlaybackDuration]; //音乐当前播放时间 在计时器中修改 // [dict setObject:[NSNumber numberWithDouble:100.0] forKey:MPNowPlayingInfoPropertyElapsedPlaybackTime]; //设置锁屏状态下屏幕显示播放音乐信息 [[MPNowPlayingInfoCenter defaultCenter] setNowPlayingInfo:dict]; } } ``` 如果需要在计时器中不断刷新锁屏状态下的播放进度条请写如下代码 ``` objc //计时器修改进度 - (void)changeProgress:(NSTimer *)sender{ if(self.player){ //当前播放时间 NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithDictionary:[[MPNowPlayingInfoCenter defaultCenter] nowPlayingInfo]]; [dict setObject:[NSNumber numberWithDouble:self.player.currentTime] forKey:MPNowPlayingInfoPropertyElapsedPlaybackTime]; //音乐当前已经过时间 [[MPNowPlayingInfoCenter defaultCenter] setNowPlayingInfo:dict]; } } ``` > 参考[IOS后台运行 之 后台播放音乐](http://www.iliunian.com/2831.html) 下面我们来介绍一下 `[self setupNotifications];`注册监听 音频意外中断和耳机拔出时要暂停音乐播放 实现代码如下 ``` objc /** 播放的通知处理 */ - (void)setupNotifications { NSNotificationCenter *nsnc = [NSNotificationCenter defaultCenter]; //添加意外中断音频播放的通知 [nsnc addObserver:self selector:@selector(handleInterruption:) name:AVAudioSessionInterruptionNotification object:[AVAudioSession sharedInstance]]; //添加线路变化通知 [nsnc addObserver:self selector:@selector(hanldeRouteChange:) name:AVAudioSessionRouteChangeNotification object:[AVAudioSession sharedInstance]]; } ``` *注:记得在delloc里面`[[NSNotificationCenter defaultCenter] removeObserver:self]`* 意外中断音频发生的场景 例如 听歌过程中来电话或者 按住home键使用siri 下面是具体方法实现 ``` objc /** 音频意外打断处理 @param notification 通知信息 */ - (void)handleInterruption:(NSNotification *)notification { NSDictionary *info = notification.userInfo; AVAudioSessionInterruptionType type = [info[AVAudioSessionInterruptionTypeKey] unsignedIntegerValue]; if (type == AVAudioSessionInterruptionTypeBegan) { //Handle AVAudioSessionInterruptionTypeBegan [self pause]; } else { //Handle AVAudioSessionInterruptionTypeEnded AVAudioSessionInterruptionOptions options = [info[AVAudioSessionInterruptionTypeKey] unsignedIntegerValue]; NSError *error = nil; //激活音频会话 允许外接音响 [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback withOptions:AVAudioSessionCategoryOptionAllowBluetooth error:nil]; [[AVAudioSession sharedInstance] setActive:YES withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:&error]; if (options == AVAudioSessionInterruptionOptionShouldResume) { [self play]; } else { [self play]; } self.playButton.selected = YES; if (error) { NSLog(@"AVAudioSessionInterruptionOptionShouldResume失败:%@",[error localizedDescription]); } } } ``` 先说`handleInterruption`意外情况下中断比如我按住home键使用siri 我会收到意外打断的通知当 type == `AVAudioSessionInterruptionTypeBegan`时 我们停止音乐播放或者暂停. 当type != `AVAudioSessionInterruptionTypeBegan`的时候一定是`AVAudioSessionInterruptionTypeEnded`这个时候`notification.userInfo`里面包含一个`AVAudioSessionInterruptionOptions`值来表明音频会话是否已经重新激活以及是否可以再次播放 __*注:这个地方遇到个坑*__ 当意外中断时候有时音频会话会很不灵敏 后来发现这种情况下需要重新激活会话 如下代码: ``` objc [[AVAudioSession sharedInstance] setActive:YES withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:&error]; ``` 这里`AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation`是为了通知其它应用会话被我激活了 很多播放器开发者很不讲究 每次从来不用这个方法导致每次别人播放完音频 自己都收不到音频重新播放的信息 建议大家以和为贵, 写良心代码. 因为我外接的小米蓝牙音响发现还是不好使 最后又补上了`AVAudioSessionCategoryOptionAllowBluetooth`这个 __激活音频会话 允许外接音响__ ``` objc [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback withOptions:AVAudioSessionCategoryOptionAllowBluetooth error:nil]; ``` 就好使了 下面说一下耳机插拔或者USB麦克风断开 Apple有个什么`Human Interface Guidelines(HIG)`相关定义 意思是说当硬件耳机拔出时建议 暂停播放音乐或者麦克风断开时。就是处于静音状态。是为了保密播放内容不被外界听到,不管苹果啥规定 我们都得照办 否则就得被拒。 ``` objc - (void)hanldeRouteChange:(NSNotification *)notification { NSDictionary *info = notification.userInfo; AVAudioSessionRouteChangeReason reason = [info[AVAudioSessionRouteChangeReasonKey] unsignedIntegerValue]; //老设备不可用 if (reason == AVAudioSessionRouteChangeReasonOldDeviceUnavailable) { AVAudioSessionRouteDescription *previousRoute = info[AVAudioSessionRouteChangePreviousRouteKey]; AVAudioSessionPortDescription *previousOutput = previousRoute.outputs[0]; NSString *portType = previousOutput.portType; if ([portType isEqualToString:AVAudioSessionPortHeadphones]) { [self stop]; self.playButton.selected = NO; } } } ``` 这需要用`AVAudioSessionRouteChangeReasonKey`取出线路切换的原因`AVAudioSessionRouteChangeReason` 原因有这么多 ``` objc typedef NS_ENUM(NSUInteger, AVAudioSessionRouteChangeReason) { AVAudioSessionRouteChangeReasonUnknown = 0, AVAudioSessionRouteChangeReasonNewDeviceAvailable = 1, AVAudioSessionRouteChangeReasonOldDeviceUnavailable = 2, AVAudioSessionRouteChangeReasonCategoryChange = 3, AVAudioSessionRouteChangeReasonOverride = 4, AVAudioSessionRouteChangeReasonWakeFromSleep = 6, AVAudioSessionRouteChangeReasonNoSuitableRouteForCategory = 7, AVAudioSessionRouteChangeReasonRouteConfigurationChange NS_ENUM_AVAILABLE_IOS(7_0) = 8 } NS_AVAILABLE_IOS(6_0); ``` 我们需要这个`AVAudioSessionRouteChangeReasonOldDeviceUnavailable` 判断是否是旧设备 通过`AVAudioSessionRouteChangePreviousRouteKey`拿出 `AVAudioSessionRouteDescription`描述信息 `previousRoute` 在通过 `previousRoute.outputs[0]`拿出`AVAudioSessionPortDescription` 拿出`NSString *portType = previousOutput.portType` 如果`[portType isEqualToString:AVAudioSessionPortHeadphones]` 如果是耳机`AVAudioSessionPortHeadphones`则暂停播放 以上就是中断和线路切换的一些代码逻辑 下面我介绍一些好玩的 ![](/assets/images/20170317LearningAVFoundationAVAudioPlayer/demo.webp) 前面说的一些后台设置信息显示的内容就是上图所示 在锁屏的时候显示 但是大家一定很奇怪的是怎么实现接收 __锁屏状态下 点击 上一曲 暂停/播放 下一曲等操作__ 需要在AppDelegate里面写上 ``` objc - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { AVAudioSession *session = [AVAudioSession sharedInstance]; NSError *error; if (![session setCategory:AVAudioSessionCategoryPlayback error:&error]) { NSLog(@"Category Error: %@", [error localizedDescription]); } if (![session setActive:YES error:&error]) { NSLog(@"Activation Error: %@", [error localizedDescription]); } [[UIApplication sharedApplication] beginReceivingRemoteControlEvents]; [self becomeFirstResponder]; return YES; } ``` 这`[[UIApplication sharedApplication] beginReceivingRemoteControlEvents];` 行代码 以及调用自己为 `[self becomeFirstResponder];`第一响应者 这样写是为了应用响应音频播放 后台切换或者中断的时候更灵敏. ``` objc - (BOOL)canBecomeFirstResponder { return YES; } ``` 然后 写上如下代码 处理__锁屏状态下 点击 上一曲 暂停/播放 下一曲等操作__ ``` objc - (void)remoteControlReceivedWithEvent:(UIEvent *)event { if (event.type == UIEventTypeRemoteControl) { switch (event.subtype) { case UIEventSubtypeRemoteControlPlay: NSLog(@"暂停播放"); break; case UIEventSubtypeRemoteControlPause: NSLog(@"继续播放"); break; case UIEventSubtypeRemoteControlNextTrack: NSLog(@"下一曲"); break; case UIEventSubtypeRemoteControlPreviousTrack: NSLog(@"上一曲"); break; default: break; } } } ``` 剩余逻辑大家自己填充吧我就不介绍了. 好了AVAudioPlayer就到这吧!有啥疑问大家可以评论留言都能看到或者指正我的错误。我会及时改正. 全文完 __文章的最终[demo](https://github.com/sunyazhou13/AVAudioPlayerDemo)__ URL: https://sunyazhou.com/2017/03/LearningAVFoundationAVSpeechSynthesizer/index.html.md Published At: 2017-03-11 12:38:53 +0000 # Learning AV Foundation(一)汉字语音朗读 ![AVSpeechSynthesizer](/assets/images/20170311LearningAVFoundationAVSpeechSynthesizer/Cover.webp) 前言 > 最近在研究`AV Foundation` 框架 发现有一本书叫做 [AV Foundation开发秘籍:实践掌握iOS & OS X 应用的视听处理技术](http://item.jd.com/11742630.html) 然后google查了一下英文版叫 [Learning AV Foundation: A Hands-on Guide to Mastering the AV Foundation Framework](http://www.informit.com/store/learning-av-foundation-a-hands-on-guide-to-mastering-9780321961808) 看着国人的翻译不仅慨叹的想说一句话: 为啥不自己写一本书 何必这么费劲翻译它搞得原来很有技术含量 这么直译就没技术含量了。看着开发秘籍这名字不禁想起大学时那些书 从开发到入门... 21天学会xxx... 开发指南... 开发秘籍... 我大学读的都是`假书` 今天给大家分享的是 iOS上如何 把汉字转换成语音朗读, 当然这个没什么技术含量(大神可以飞过). AVFoundation整体架构 -- 研究这个功能之前先介绍一下`AV Foundation`整体架构 ![iOS](/assets/images/20170311LearningAVFoundationAVSpeechSynthesizer/frameworksBlockDiagram.webp) 这是iOS上的架构设计 (上图) ![iOS](/assets/images/20170311LearningAVFoundationAVSpeechSynthesizer/frameworksBlockDiagramOSX.webp) 这是macOS上的架构设计(上图) 看完之后我们就来用代码实现这个demo 首先导入`` 这我需要使用的是iOS上的`AVSpeechSynthesizer`,macOS上叫`NSSpeechSynthesizer ` ``` objc @property (strong, nonatomic) AVSpeechSynthesizer *synthesizer; ``` `AVSpeechSynthesizer` 它的功能 * __将文字添加到语音, 就是用语音播放一段文字__ 初始化 ``` objc - (void)awakeFromNib { [super awakeFromNib]; //创建语音合成器 self.synthesizer = [[AVSpeechSynthesizer alloc] init]; self.synthesizer.delegate = self; //播放的国家的语言 self.voices = @[[AVSpeechSynthesisVoice voiceWithLanguage:@"zh-CN"],[AVSpeechSynthesisVoice voiceWithLanguage:@"en-US"] ]; self.speechStrings = [[NSMutableArray alloc] init]; } ``` 这里的`[AVSpeechSynthesisVoice voiceWithLanguage:@"zh-CN"]` 设置的是简体中文语音 文章末尾会列出所有语音播放信息不用担心写错. `AVSpeechSynthesizer`的delegate方法如下 主要是对语音播放状态的监听 ``` objc @protocol AVSpeechSynthesizerDelegate // 代理方法 @optional // 开始播放 语音单元 - (void)speechSynthesizer:(AVSpeechSynthesizer *)synthesizer didStartSpeechUtterance:(AVSpeechUtterance *)utterance; // 完成播放 语音单元 - (void)speechSynthesizer:(AVSpeechSynthesizer *)synthesizer didFinishSpeechUtterance:(AVSpeechUtterance *)utterance; // 暂停播放 语音单元 - (void)speechSynthesizer:(AVSpeechSynthesizer *)synthesizer didPauseSpeechUtterance:(AVSpeechUtterance *)utterance; // 继续播放 语音单元 - (void)speechSynthesizer:(AVSpeechSynthesizer *)synthesizer didContinueSpeechUtterance:(AVSpeechUtterance *)utterance; // 取消播放 语音单元 - (void)speechSynthesizer:(AVSpeechSynthesizer *)synthesizer didCancelSpeechUtterance:(AVSpeechUtterance *)utterance; // 这里 指的是 又来监听 播放 字符范围 - (void)speechSynthesizer:(AVSpeechSynthesizer *)synthesizer willSpeakRangeOfSpeechString:(NSRange)characterRange utterance:(AVSpeechUtterance *)utterance; @end ``` __这里的`AVSpeechSynthesizer`主要的方法有__ ``` objc /* 添加 播放话语 到 播放语音 队列, 可以设置utterance的属性来控制播放 */ - (void)speakUtterance:(AVSpeechUtterance *)utterance; // 对于 stopSpeakingAtBoundary: 语音单元的操作, 如果中断, 会清空队列 // 中断 - (BOOL)stopSpeakingAtBoundary:(AVSpeechBoundary)boundary; // 暂停 - (BOOL)pauseSpeakingAtBoundary:(AVSpeechBoundary)boundary; // 继续 - (BOOL)continueSpeaking; ``` > 这里我们用的`speakUtterance`方法来播放文字 speakUtterance:(AVSpeechUtterance *)utterance 1. `AVSpeechUtterance`是对文字朗读的封装 2. 被播放的语音文字, 可以理解为一段需要播放的文字 这里我们设置`AVSpeechUtterance`朗读播放的信息 ``` objc //播放语音 NSArray *speechStringsArray = [self buildSpeechStrings]; //buildSpeechStrings播放字符串的数组 for (NSUInteger i = 0; i < speechStringsArray.count; i++) { //创建AVSpeechUtterance 对象 用于播放的语音文字 AVSpeechUtterance *utterance = [[AVSpeechUtterance alloc] initWithString:speechStringsArray[i]]; //设置使用哪一个国家的语言播放 utterance.voice = self.voices[0]; //本段文字播放时的 语速, 应介于AVSpeechUtteranceMinimumSpeechRate 和 AVSpeechUtteranceMaximumSpeechRate 之间 utterance.rate = 0.5; //在播放特定语句时改变声音的声调, 一般取值介于0.5(底音调)~2.0(高音调)之间 utterance.pitchMultiplier = 0.8f; //声音大小, 0.0 ~ 1.0 之间 utterance.volume = 1.0f; //播放后的延迟, 就是本次文字播放完之后的停顿时间, 默认是0 utterance.preUtteranceDelay = 0; //播放前的延迟, 就是本次文字播放前停顿的时间, 然后播放本段文字, 默认是0 utterance.postUtteranceDelay = 0.1f; [self.synthesizer speakUtterance:utterance]; } ``` `AVSpeechUtterance`的属性如下 ``` objc // 设置使用哪一个国家的语言播放 @property(nonatomic, retain, nullable) AVSpeechSynthesisVoice *voice; // 获取当前需要播放的文字, 只读属性 @property(nonatomic, readonly) NSString *speechString; // 获取当前需要播放的文字 - 富文本, 只读属性, iOS10以后可用 @property(nonatomic, readonly) NSAttributedString *attributedSpeechString; // 本段文字播放时的 语速, 应介于AVSpeechUtteranceMinimumSpeechRate 和 AVSpeechUtteranceMaximumSpeechRate 之间 @property(nonatomic) float rate; // 在播放特定语句时改变声音的声调, 一般取值介于0.5(底音调)~2.0(高音调)之间 @property(nonatomic) float pitchMultiplier; // 声音大小, 0.0 ~ 1.0 之间 @property(nonatomic) float volume; // 播放后的延迟, 就是本次文字播放完之后的停顿时间, 默认是0 @property(nonatomic) NSTimeInterval preUtteranceDelay; // 播放前的延迟, 就是本次文字播放前停顿的时间, 然后播放本段文字, 默认是0 @property(nonatomic) NSTimeInterval postUtteranceDelay; ``` `AVSpeechUtterance`的方法如下 以下全部都是初始化方法, 分为 类方法 和 对象方法, 富文本的初始化方法要在iOS10以后才可以用 ``` objc + (instancetype)speechUtteranceWithString:(NSString *)string; + (instancetype)speechUtteranceWithAttributedString:(NSAttributedString *)string NS_AVAILABLE_IOS(10_0); - (instancetype)initWithString:(NSString *)string; - (instancetype)initWithAttributedString:(NSAttributedString *)string ``` 可以使用__`[AVSpeechSynthesisVoice speechVoices]`__代码打印出支持朗读语言的国家 ``` ar-SA 沙特阿拉伯(阿拉伯文) en-ZA, 南非(英文) nl-BE, 比利时(荷兰文) en-AU, 澳大利亚(英文) th-TH, 泰国(泰文) de-DE, 德国(德文) en-US, 美国(英文) pt-BR, 巴西(葡萄牙文) pl-PL, 波兰(波兰文) en-IE, 爱尔兰(英文) el-GR, 希腊(希腊文) id-ID, 印度尼西亚(印度尼西亚文) sv-SE, 瑞典(瑞典文) tr-TR, 土耳其(土耳其文) pt-PT, 葡萄牙(葡萄牙文) ja-JP, 日本(日文) ko-KR, 南朝鲜(朝鲜文) hu-HU, 匈牙利(匈牙利文) cs-CZ, 捷克共和国(捷克文) da-DK, 丹麦(丹麦文) es-MX, 墨西哥(西班牙文) fr-CA, 加拿大(法文) nl-NL, 荷兰(荷兰文) fi-FI, 芬兰(芬兰文) es-ES, 西班牙(西班牙文) it-IT, 意大利(意大利文) he-IL, 以色列(希伯莱文,阿拉伯文) no-NO, 挪威(挪威文) ro-RO, 罗马尼亚(罗马尼亚文) zh-HK, 香港(中文) zh-TW, 台湾(中文) sk-SK, 斯洛伐克(斯洛伐克文) zh-CN, 中国(中文) ru-RU, 俄罗斯(俄文) en-GB, 英国(英文) fr-FR, 法国(法文) hi-IN 印度(印度文) ``` > 总结 为了学习__`AVFoundation`__我先从一个简单的知识点入手,唯一觉得遗憾的是我不太确定是否这个合成器支持自定义语音朗读,这个后续研究一下,把相关学习内容填补上. __最终的demo 支持iOS和macOS:[Learning-AV-Foundation(一)汉字语音朗读](https://github.com/sunyazhou13/AVSpeechSynthesizerDemo)__ 参考: [AV Foundation Apple 官方文档](https://developer.apple.com/library/content/documentation/AudioVideo/Conceptual/AVFoundationPG/Articles/00_Introduction.html#//apple_ref/doc/uid/TP40010188) [AVSpeechSynthesizer 和 AVSpeechUtterance](http://www.jianshu.com/p/acd57725ba4d) [AVSpeechSynthesizer详解](http://www.jianshu.com/p/a41cb018f0b5) [AVFoundation](http://www.jianshu.com/p/cc79c45b4ccf) URL: https://sunyazhou.com/2017/02/macOSsimulateKeyboradNSEvent/index.html.md Published At: 2017-02-22 16:17:00 +0000 # macOS上模拟发送键盘事件 ![](/assets/images/20170222macOSsimulateKeyboradNSEvent/Cover.webp) 最近在开发macOS远程协助功能, 需要模拟从windows传过来的键盘事件映射成macOS `NSEvent`, macOS上模拟事件都是底层的`CoreGraphic`的`class`,下面说下实现的片断代码. 1. 导入`#import ` 2. 创建`CGEventSourceRef` 事件源对象**(注意它不是OC对象,声明的时候对象前边没有`*`,而且需要用`CFRelease()`释放内存)**. 3. 创建`CGEventRef`使用`CGEventCreateKeyboardEvent`, 第三个参数`true`代表`keydown`就是键盘按键的按下状态,如果是`false`则代表`keyup`. 这里用了一个键盘`kVK_ANSI_A ` A键作为例子 4. `CGEventTapLocation` 这个是下一个函数需要的参数 应该是键盘硬件按下的键位信息(如果搞错了欢迎指正,马上修改) 5. `CGEventPost()`发送`NSEvent`事件 6. 释放内存 > *talk is cheap, show me the code --LINUS TORVALDS* 下面是演示代码 ``` objc #import int main(int argc, const char * argv[]) { @autoreleasepool { CGEventSourceRef source = CGEventSourceCreate(kCGEventSourceStatePrivate); CGEventRef A = CGEventCreateKeyboardEvent(source, kVK_ANSI_A, true); CGEventTapLocation location = kCGHIDEventTap; //发送事件 CGEventPost(location, A); CFRelease(A); CFRelease(source); } return 0; } ``` `CGEventSourceCreate()`里定义了三个**枚举** ``` objc typedef CF_ENUM(int32_t, CGEventSourceStateID) { kCGEventSourceStatePrivate = -1, kCGEventSourceStateCombinedSessionState = 0, kCGEventSourceStateHIDSystemState = 1 }; ``` > `kCGEventSourceStatePrivate` 代表 专门的应用,如远程控制程序可以生成和跟踪事件源状态独立于其他进程。这些程序应该使用kCGEventSourceStatePrivate值在创建他们的事件源。 > `kCGEventSourceStateCombinedSessionState` 该状态表反映了所有事件源的组合状态发布到当前用户的登录会话。如果您的程序发布的事件在一个登录会话,您应该使用这个源状态当你创建一个事件源。 > `kCGEventSourceStateHIDSystemState` 该状态表反映了 组合硬件输入源从HID系统硬件层面发送的事件源。生成的事件。 就是外接键盘或者macbook本机键盘以及一些系统定义的按键点击事件 这里我使用的是第一个恰巧它有说明`remote control` 上面就是今天要分享的模拟发送系统键盘事件全部逻辑, 如有错误欢迎指正, 鄙人定当咨诹善道察纳雅言. **附* macOS ANSI码 **枚举** ``` objc /* * Summary: * Virtual keycodes * * Discussion: * These constants are the virtual keycodes defined originally in * Inside Mac Volume V, pg. V-191. They identify physical keys on a * keyboard. Those constants with "ANSI" in the name are labeled * according to the key position on an ANSI-standard US keyboard. * For example, kVK_ANSI_A indicates the virtual keycode for the key * with the letter 'A' in the US keyboard layout. Other keyboard * layouts may have the 'A' key label on a different physical key; * in this case, pressing 'A' will generate a different virtual * keycode. */ enum { kVK_ANSI_A = 0x00, kVK_ANSI_S = 0x01, kVK_ANSI_D = 0x02, kVK_ANSI_F = 0x03, kVK_ANSI_H = 0x04, kVK_ANSI_G = 0x05, kVK_ANSI_Z = 0x06, kVK_ANSI_X = 0x07, kVK_ANSI_C = 0x08, kVK_ANSI_V = 0x09, kVK_ANSI_B = 0x0B, kVK_ANSI_Q = 0x0C, kVK_ANSI_W = 0x0D, kVK_ANSI_E = 0x0E, kVK_ANSI_R = 0x0F, kVK_ANSI_Y = 0x10, kVK_ANSI_T = 0x11, kVK_ANSI_1 = 0x12, kVK_ANSI_2 = 0x13, kVK_ANSI_3 = 0x14, kVK_ANSI_4 = 0x15, kVK_ANSI_6 = 0x16, kVK_ANSI_5 = 0x17, kVK_ANSI_Equal = 0x18, kVK_ANSI_9 = 0x19, kVK_ANSI_7 = 0x1A, kVK_ANSI_Minus = 0x1B, kVK_ANSI_8 = 0x1C, kVK_ANSI_0 = 0x1D, kVK_ANSI_RightBracket = 0x1E, kVK_ANSI_O = 0x1F, kVK_ANSI_U = 0x20, kVK_ANSI_LeftBracket = 0x21, kVK_ANSI_I = 0x22, kVK_ANSI_P = 0x23, kVK_ANSI_L = 0x25, kVK_ANSI_J = 0x26, kVK_ANSI_Quote = 0x27, kVK_ANSI_K = 0x28, kVK_ANSI_Semicolon = 0x29, kVK_ANSI_Backslash = 0x2A, kVK_ANSI_Comma = 0x2B, kVK_ANSI_Slash = 0x2C, kVK_ANSI_N = 0x2D, kVK_ANSI_M = 0x2E, kVK_ANSI_Period = 0x2F, kVK_ANSI_Grave = 0x32, kVK_ANSI_KeypadDecimal = 0x41, kVK_ANSI_KeypadMultiply = 0x43, kVK_ANSI_KeypadPlus = 0x45, kVK_ANSI_KeypadClear = 0x47, kVK_ANSI_KeypadDivide = 0x4B, kVK_ANSI_KeypadEnter = 0x4C, kVK_ANSI_KeypadMinus = 0x4E, kVK_ANSI_KeypadEquals = 0x51, kVK_ANSI_Keypad0 = 0x52, kVK_ANSI_Keypad1 = 0x53, kVK_ANSI_Keypad2 = 0x54, kVK_ANSI_Keypad3 = 0x55, kVK_ANSI_Keypad4 = 0x56, kVK_ANSI_Keypad5 = 0x57, kVK_ANSI_Keypad6 = 0x58, kVK_ANSI_Keypad7 = 0x59, kVK_ANSI_Keypad8 = 0x5B, kVK_ANSI_Keypad9 = 0x5C }; /* keycodes for keys that are independent of keyboard layout*/ enum { kVK_Return = 0x24, kVK_Tab = 0x30, kVK_Space = 0x31, kVK_Delete = 0x33, kVK_Escape = 0x35, kVK_Command = 0x37, kVK_Shift = 0x38, kVK_CapsLock = 0x39, kVK_Option = 0x3A, kVK_Control = 0x3B, kVK_RightCommand = 0x36, kVK_RightShift = 0x3C, kVK_RightOption = 0x3D, kVK_RightControl = 0x3E, kVK_Function = 0x3F, kVK_F17 = 0x40, kVK_VolumeUp = 0x48, kVK_VolumeDown = 0x49, kVK_Mute = 0x4A, kVK_F18 = 0x4F, kVK_F19 = 0x50, kVK_F20 = 0x5A, kVK_F5 = 0x60, kVK_F6 = 0x61, kVK_F7 = 0x62, kVK_F3 = 0x63, kVK_F8 = 0x64, kVK_F9 = 0x65, kVK_F11 = 0x67, kVK_F13 = 0x69, kVK_F16 = 0x6A, kVK_F14 = 0x6B, kVK_F10 = 0x6D, kVK_F12 = 0x6F, kVK_F15 = 0x71, kVK_Help = 0x72, kVK_Home = 0x73, kVK_PageUp = 0x74, kVK_ForwardDelete = 0x75, kVK_F4 = 0x76, kVK_End = 0x77, kVK_F2 = 0x78, kVK_PageDown = 0x79, kVK_F1 = 0x7A, kVK_LeftArrow = 0x7B, kVK_RightArrow = 0x7C, kVK_DownArrow = 0x7D, kVK_UpArrow = 0x7E }; ``` 全文完 URL: https://sunyazhou.com/2017/02/ClearNSUserDefaultCcontent/index.html.md Published At: 2017-02-20 19:05:01 +0000 # 使用终端删除NSUserDefault的内容 前言 -- > 大家对`NSUserDefaults`非常熟悉 今天给大家讲一下如何用终端清理`NSUserDefaults`的信息 `NSUserDefaults`和`win`开发的注册表一样 用于存储一些标记位 最近开发用到的比较多是如何不运行代码的情况下清理`NSUserDefaults `信息 **$ defaults delete + 包名** eg: com.baidu.demo 下面这样会删除所有以`com.baidu.demo`为包名的文件 ``` shell $ defaults delete com.baidu.demo ``` > 实际的的路径(把 my app和前后剪头 换成自己的应用的包名) macOS应用非沙盒权限(如下图) `~/Library/Preferences/.plist ` eg:QQ ![非沙盒路径](/assets/images/20170220ClearNSUserDefaultCcontent/NonSandBoxPermission.webp) macOS应用沙盒权限(如下图) `~/Library/Containers//Data/Library/Preferences/.plist` eg:qq ![](/assets/images/20170220ClearNSUserDefaultCcontent/SandBoxPermission.webp) 总结 -- > defaults 还有其它指令还可以为某个`key`设置`value` 大家可自行google 谢谢大家 全文完 URL: https://sunyazhou.com/2017/02/ScanBoundsTracking/index.html.md Published At: 2017-02-18 19:08:56 +0000 # iOS如何让二维码/条形码扫描框跟随二维码移动 前言 -- > 开发过程中经常会遇到二维码条形码,但总会有一个比较蛋疼的问题 如何让二维码的扫描框跟随扫码到的二维码移动跟踪呢(就是智能探测扫描的layer.bounds)? ![](http://www.appcoda.com/wp-content/uploads/2016/11/qrcode-reader-5-1024x637.webp) 这里有一篇文字讲述了开发过程我这里就不赘述了, 如果有小伙伴觉得需要我翻译的话请在底部留言 我会及时更新代码 > [Building a Barcode and QR Code Reader in Swift 3 and Xcode 8](http://www.appcoda.com/barcode-reader-swift/) *这里比较核心的代码如下* `AVCaptureMetadataOutputObjectsDelegate`代理 ```swift func captureOutput(_ captureOutput: AVCaptureOutput!, didOutputMetadataObjects metadataObjects: [Any]!, from connection: AVCaptureConnection!) { // Check if the metadataObjects array is not nil and it contains at least one object. if metadataObjects == nil || metadataObjects.count == 0 { qrCodeFrameView?.frame = CGRect.zero messageLabel.text = "No QR code is detected" return } // Get the metadata object. let metadataObj = metadataObjects[0] as! AVMetadataMachineReadableCodeObject if metadataObj.type == AVMetadataObjectTypeQRCode { // If the found metadata is equal to the QR code metadata then update the status label's text and set the bounds let barCodeObject = videoPreviewLayer?.transformedMetadataObject(for: metadataObj) //核心代码在这 qrCodeFrameView?.frame = barCodeObject!.bounds if metadataObj.stringValue != nil { messageLabel.text = metadataObj.stringValue } } }} ``` `qrCodeFrameView?.frame = barCodeObject!.bounds` 这行代码最核心 就是拿到barCodeObject.bounds 给我们自己创建透明的那个view就行了 **[最终项目](https://github.com/sunyazhou13/QRCodeReader)** ![QRCode 跟踪](/assets/images/20170218ScanBoundsTracking/ScanBoundsTracking.gif) 全文完 URL: https://sunyazhou.com/2017/02/iOSInternationalizationLanguageSkills/index.html.md Published At: 2017-02-17 10:01:19 +0000 # iOS语言本地化/国际化一些技巧 代码获取国际化语言数组 -- 获取当前app使用的语言 ``` objc NSArray *langArr1 = [[NSUserDefaults standardUserDefaults] valueForKey:@"AppleLanguages"]; NSString *language1 = langArr1.firstObject; NSLog(@"模拟器语言:%@",language1); ``` 切换语言 `en`代表 英语, `zh-Hans` 简体中文, `zh-Hant` 繁体中文. ``` objc NSArray *lans = @[@"en"]; [[NSUserDefaults standardUserDefaults] setObject:lans forKey:@"AppleLanguages"]; ``` 修改scheme切换启动语言 -- ![图1](/assets/images/20170217iOSInternationalizationLanguageSkills/AppleLanguages1.webp) ![图2](/assets/images/20170217iOSInternationalizationLanguageSkills/AppleLanguages2.webp) > `-AppleLanguages (zh-Hans)` 代表简体中文 > `-AppleLanguages (zh-Hant)` 代表繁体中文 > `-AppleLanguages (en)` 代表英文 > 其它小伙伴们自己总结一下也可以 注意 **空格** 国际化取不同图片代码 -- ``` objc #import "ViewController.h" @interface ViewController () @property (weak, nonatomic) IBOutlet UIImageView *imageView; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; //xxx 是国际化 图片的名字 例如xxx.png //如果是 xxx.jpg 必须写把xxx 替换成xxx.jpg NSString *imageName = NSLocalizedString(@"xxx", nil); self.imageView.image = [UIImage imageNamed:imageName]; } @end ``` 下面是我写的一个[demo](https://github.com/sunyazhou13/LocalizedDemo/tree/master) 主要完成 如下内容 1. 工程名称配置plist 国际化 2. 字符串国际化 3. 自定义字符串国际化 4. 图片国际化 参考 [VV木公子](http://www.jianshu.com/p/88c1b65e3ddb) 全文完 URL: https://sunyazhou.com/2017/02/BuildHexoBlogTutorial/index.html.md Published At: 2017-02-10 10:24:25 +0000 # 搭建hexo博客教程 > [Jekyll迁移到Hexo搭建个人博客](http://www.ezlippi.com/blog/2016/02/jekyll-to-hexo.html) > [HEXO + Github 搭建自己的博客系统](http://www.czhzero.com/2016/06/25/hexoblog/) > [利用Hexo和Github Pages搭建个人博客](http://skx926.com/2016/01/26/build-hexo/) ### 1 初级功能 github pages 配置 大家自行google吧 真的很简单 上边是我参考的文章 主要是环境比较费劲 1. nodejs 2. npm包管理工具 3. hexo 4. nvm管理node 5. 本地配置重启shell又被reverse 6. ...其它 ### 2 高级功能部分 打赏 自行 搜索简书吧 很多 主题不太一样 套路都是一样的恩 ### 3 SSL(自定义域名支持HTTPS) [支持https](https://gbin.me/2017/08/03/Hexo-deployed-in-github-and-coding/) URL: https://sunyazhou.com/2017/02/DFSAlgorithm/index.html.md Published At: 2017-02-10 10:07:55 +0000 # DFS算法扫描上传文件/文件夹 DFS需求背景 -- > 在开发过程中难免会遇到类似 上传文件夹的功能,但是上传文件夹会遇到一种情况 1. 如果文件里面包含子文件夹的N层嵌套 2. 如何过滤非空文件夹 3. 如何处理根层文件夹没有文件那么文件目录也需要创建 举例例子 ![](/assets/images/20170210DFSAlgorithm/DFS1.webp) 这种文件夹如何 `此文件夹为空且是叶子结点` 走上传逻辑(就是发个 http请求 create一下 dir就行了) 我们要的结果是 过滤出这个路径走上传逻辑 创建一下这个最深处目录 那么下次再遇到它的父目录 `/1/` 的话应该就不用创建了. 还有一种情况 > eg: ~/Downloads/A/B/C/ 里面有个 1.txt > 路径是: ~/Downloads/A/B/C/1.txt 一般如果广度优先做上传的话 Downloads、A、B、C分别要发4个http请求 如果深度优先发一个上传这个文件~/Downloads/A/B/C//A/B/C/1.txt就可以了,因为一般server都会做 容错处理发现父目录有没有没有就创建之类的逻辑。 算法 -- > 不要害怕, 很简单 一般我们处理这种问题都是采用自己写的递归算法, 估计是鄙人算法不咋好没搞出来什么好的递归,最后找到了 苹果自带的递归方法 ```objc //搞个点击事件 在这里我拿macOS上的 文件选择面板做一下测试 - (IBAction)dfsAction:(NSButton *)sender { NSOpenPanel *panelPath = [NSOpenPanel openPanel]; [panelPath setCanChooseFiles:YES]; [panelPath setCanChooseDirectories:YES]; [panelPath setTitle:@"上传文件选择"]; [panelPath setCanCreateDirectories:YES]; [panelPath setPrompt:@"上传"]; [panelPath setMessage:@"这就是message"]; panelPath.allowsMultipleSelection = YES; [panelPath beginSheetModalForWindow:self.window completionHandler:^(NSInteger result) { if (result == NSFileHandlingPanelOKButton) { [self dfsUrls:panelPath.URLs]; } }]; } ``` ![](/assets/images/20170210DFSAlgorithm/DFS2.webp) 然后 ```objc /** 选择文件夹的目录 @param urls 所有选中的目录/文件URL */ - (void)dfsUrls:(NSArray *)urls { //开一个线程在异步处理这些耗时任务 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ NSLog(@"所有URLs%@",urls); if (urls.count == 0) { return; } NSTimeInterval currentTime = [[NSDate date] timeIntervalSince1970]; //深度遍历 NSFileManager *fileManager = [NSFileManager defaultManager]; NSMutableArray *urlDirFiles = [[NSMutableArray alloc] initWithCapacity:0]; NSArray *keys = [NSArray arrayWithObjects:NSURLIsDirectoryKey,NSURLParentDirectoryURLKey, nil]; NSUInteger *total = 0; for (NSURL *localUrl in urls) { NSDirectoryEnumerator *enumerator = [self enumeratorPathByFileManager:fileManager atURL:localUrl propertiesForKeys:keys options:0]; //这里包含的元素是 有子文件的忽略父路径结点 //eg: /A/1/2/ (这个就需要移除) /A/1/2/sun.txt(保留这个文件即可) for (NSURL *url in enumerator) { total++; NSError *error; NSNumber *isDirectory = nil; if (![url getResourceValue:&isDirectory forKey:NSURLIsDirectoryKey error:&error]) { // handle error } //是否为文件夹 if ([isDirectory boolValue]) { //方案1 // NSDirectoryEnumerator *dirEnumerator = [self enumeratorPathByFileManager:fileManager atURL:url propertiesForKeys:@[NSURLIsDirectoryKey] options:NSDirectoryEnumerationSkipsSubdirectoryDescendants]; // if (dirEnumerator.allObjects.count > 0) { // NSLog(@"文件夹内有文件,忽略此条路径 %@",[url path]); // } else { // [urlDirFiles addObject:[url path]]; // } //方案2 NSError *error = nil; NSArray *listOfFiles = [fileManager contentsOfDirectoryAtPath:[url path] error:nil]; if (listOfFiles != nil && listOfFiles.count == 0) { [urlDirFiles addObject:[url path]]; } else if (error == nil){ NSLog(@"文件夹内有文件,忽略此条路径 %@",[url path]); } else { NSLog(@"文件遍历该层出错:%@",error); } } else { [urlDirFiles addObject:[url path]]; } } NSLog(@"所有可上传文件列表:\n%@",urlDirFiles); } NSTimeInterval nowTime = [[NSDate date] timeIntervalSince1970]; NSLog(@"\n文件数量:%zd 遍历总数:%zd\n耗时:%.2f 秒",urlDirFiles.count,total,(nowTime - currentTime)); total = 0; dispatch_async(dispatch_get_main_queue(), ^{ NSLog(@"scan end"); }); }); } ``` 接下来就是最核心的代码块 ```objc - (NSDirectoryEnumerator *)enumeratorPathByFileManager:(NSFileManager *)fileManager atURL:(NSURL *)url propertiesForKeys:(nullable NSArray *)keys options:(NSDirectoryEnumerationOptions)mask { NSDirectoryEnumerator *enumerator = [fileManager enumeratorAtURL:url includingPropertiesForKeys:keys options:mask errorHandler:^(NSURL *url, NSError *error) { // Handle the error. // Return YES if the enumeration should continue after the error. NSLog(@"深度遍历出错%@",error); return YES; }]; return enumerator; } ``` `NSDirectoryEnumerator` 是一个路径枚举迭代器 > talk is cheap, show me the result. 下面是我扫描本地`下载`目录的结果 ![](/assets/images/20170210DFSAlgorithm/DFS3.webp) ![](/assets/images/20170210DFSAlgorithm/Result.webp) 结果还是比较快的 单从数据上来讲 比广度优先节省至少7万次Http请求 我怀疑是macOS对系统目录有索引或者缓存 第二次扫码速度比较快 总结 -- 总体来看,效果还可以,如果你有更好的算法来解决这种问题 欢迎@我 或者发邮件我也学习一下. > [最终DFSdemo](https://github.com/sunyazhou13/DFSDemo) 也可学一下:[Swift Depth First Search](https://www.raywenderlich.com/157949/swift-algorithm-club-depth-first-search) URL: https://sunyazhou.com/2017/02/HowToUseGitManageCode/index.html.md Published At: 2017-02-09 19:35:45 +0000 # 如何使用git管理代码 ![](/assets/images/20170209HowToUseGitManageCode/guide.webp) # 全局配置git -- ``` sh $ git config --global user.name "username" $ git config --global user.email "email@you.com" ``` > `username` 一般代表提交的本机用户名 > `email@you.com` 一般是邮箱地址 创建本地仓库进行初始化 -- ``` sh $ git init ``` > 执行完成之后在本地创建一个 .git 的隐藏文件夹包含git的信息在里面 克隆远程版本库 -- ``` sh $ git clone git@github.com:sunyazhou13/sunyazhou13.github.io.git ``` 查看当前代码库状况 -- ``` sh $ git status ``` > `git status` 命令会显示当前代码库的状况,包括添加,修改(modified),删除(deleted) 版本管理 -- 指向git当前最新版本为`HEAD`,`HEAD^`表示上一版本,`HEAD^^`上上一个版本,`HEAD~100`表示往上100个版本 添加本地修改代码 -- ``` sh //添加当前目录的所有修改 $ git add . ``` > //如果需要添加指定文件可以这样 > `$ git add A B C ` // 中间用空格隔开 > //如果有些文件标红 代表未纳入git 管理 可以 `rm -rf xxx`删除该文件 > //如果有些文件标黄 代表有修改 > //如果有些文件标绿 代表有文件已经纳入 `git` 管理 提交 -- ``` sh $ git commit -am "[产品名称][迭代名称] 1.修改点 2.修改点xxx" ``` push到`git`代码仓库 -- ``` sh $ git push origin HEAD:refs/for/master ``` > 如果是第一次提交 使用 `git push -u origin master` push 的时候进行代码追踪 -- ``` sh $ git push --set-upstream origin + 分支名 ``` 如果提交被废弃 -- ``` sh $ git fetch origin master $ git reset --soft origin/master $ git add . $ git commit -m "some comments" $ git push origin HEAD:refs/for/master ``` > 回到本地代码库库中,执行 分支管理 -- 创建分支并切换过去 ``` sh $ git branch -b 分支名 ``` 切换分支 ``` sh $ git checkout 分支名 ``` > 查看远程分支 `git branch -r` r 代表remote 合并分支 -- `$ git merge br-name`将`br-name`分支合并到当前分支下 加入`--no-ff`则表示禁用Fast forward模式。即新建commit而不是切换HEAD指针来实现 `$ git merge --no-ff -m "merge with no-ff" dev` 合并分支前可以通过`git diff `来查看两者不同 合并冲突 -- merge分支时,如果两分支对同一地方做了不同的修改,则为冲突,冲突的文件git会生成如下内容 ``` <<<<<<< HEAD Creating a new branch is quick & simple. ======= Creating a new branch is quick AND simple. >>>>>>> feature1 ``` 解决完冲突 合并之后 记得执行 ``` sh $ git rebase --continue ``` 强制更新 tag 到指定的 commit ``` sh git tag --force v1.0.0 bc63359 git push --tags -f ``` > git ll 可以看到短版本号,如果不好使 请执行如下脚本然后重试 下面是常用的 git 别名 ``` sh git config --global alias.ll "log --graph --all --pretty=format:'%Cred%h %Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit --date=relative" git config --global alias.co checkout git config --global alias.br branch git config --global alias.ci commit git config --global alias.st status git config --global alias.last 'log -1 HEAD' git config --global alias.df diff git config --global alias.co checkout ``` [详细资料参考](https://www.zybuluo.com/ValenW/note/364756) 批量删除 本地分支 ``` git branch | grep 'branchName' |xargs git branch -D ``` 这个是通过 shell 管道命令来实现的批量删除分支的功能。 **grep**是对**git branch**的输出结果进行匹配,匹配值为**branchName**。 **xargs**的作用是将参数列表转换成小块分段传递给其他命令。 所以这个命令的意思就是 从分支列表中匹配到指定的分支,然后一个一个(分成小块)传递给删除分支的命令,最后进行删除。 从而达到批量删除分支的目的 例如:我想删除本地 以 5.8.开头的分支 我可以这样写 ``` sh git branch | grep '5.8.*' |xargs git branch -D ``` 通过通配符即可 > 2018年9月30日更新 持续更新 git clean 清理掉不在git版本控制之内的文件 -- ``` sh git clean -dfx ``` > 2020.1.7 update 删除工程中总是放到ignore中跟踪不上小的xcode userdata 最近工程中总是出现下面这个文件 无论如何放gitignore总是不生效 ``` sh Crown.xcworkspace/xcuserdata/sunyazhou.xcuserdatad/UserInterfaceState.xcuserstate ``` 下面看下正确的操作 ` [project] `替换成你的工程 ` [username] `替换成你的用户名 ``` sh git rm --cached [project].xcodeproj/project.xcworkspace/xcuserdata/[username].xcuserdatad/UserInterfaceState.xcuserstate git commit -m "Removed file that shouldn't be tracked" ``` > 2022.12.2 更新自[Can't ignore UserInterfaceState.xcuserstate](https://stackoverflow.com/questions/6564257/cant-ignore-userinterfacestate-xcuserstate) 如果需要各种语言的ignore可以参考[A collection of useful .gitignore templates](https://github.com/github/gitignore) # 合并多次提交 在 Git 中,`rebase` 命令用于将一系列提交从一个分支上移动到另一个分支上,同时保持提交的顺序和内容不变。`git rebase -i` 是一个交互式的变体,允许你对提交进行编辑。以下是如何使用 `git rebase -i HEAD~N` 来合并多次提交为一次提交的步骤: 1. **确定需要合并的提交数量**: - `HEAD~N` 表示从当前提交向前数 N 个提交。你需要确定 N 的值,即你想要合并的提交的数量。 2. **启动交互式变基**: - 打开终端或 Git Bash,并切换到你的 Git 仓库目录。 - 输入 `git rebase -i HEAD~N` 并替换 N 为你确定的提交数量。 ``` bash git rebase -i HEAD~3 ``` > 合并最近3次提交 3. **编辑提交列表**: - 这将打开一个文本编辑器,列出了从 `HEAD~N` 到当前 HEAD 的所有提交。 - 你将看到每个提交前面都有一个 `pick` 命令。你可以通过编辑这些命令来决定如何处理每个提交。 4. **合并提交**: - 要合并提交,你需要将除了第一个提交之外的所有提交的 `pick` 改为 `squash` 或 `s`(`squash` 的简写)。这样,除了第一个提交外,其他所有提交都会被合并到第一个提交中。 - 例如,如果你有以下提交列表: ``` bash pick 3f3f3f3 第一个提交信息 pick 4b4b4b4 第二个提交信息 pick 5c5c5c5 第三个提交信息 ``` 你应该将它们修改为: ``` bash pick 3f3f3f3 第一个提交信息 squash 4b4b4b4 第二个提交信息 squash 5c5c5c5 第三个提交信息 ``` > pick作为最开始的基点 5. **编辑提交信息**: - 保存并关闭编辑器后,Git 会合并你选择的提交,并打开另一个编辑器让你编辑新的提交信息。 - 你可以选择保留第一个提交的信息,或者编辑一个新的提交信息来总结所有合并的提交。 6. **完成变基**: - 保存并关闭提交信息编辑器,Git 将完成变基操作,并将你的提交历史更新为一个新的线性历史。 请注意,变基是一个破坏性操作,它会改变历史提交的哈希值。因此,只有在你确定不会影响其他人的工作时才应该使用它,特别是在公共分支上。如果你在团队中工作,最好在进行这样的操作之前与团队成员沟通。 # 大文件处理 ``` sh brew install git-lfs # install via homebrew git lfs install # initialize lfs for yor repo git lfs track ios-app/Frameworks/*.framework/**/* # track all frameworks in your project. *.xcframework git add --all # stage git commit -m "Added files to git lfs" # commit git lfs ls-files git push ``` URL: https://sunyazhou.com/2017/02/HowtToDisableWebviewNSScrollViewScroll/index.html.md Published At: 2017-02-09 13:37:28 +0000 # Webview的NSScrollView禁用滑动功能 在`webview`的`WebFrameLoadDelegate`代理里面实现如下代码 ``` objc #pragma mark - #pragma mark - WebViewDelegate - (void)webView:(WebView *)sender didFinishLoadForFrame:(WebFrame *)frame { [sender stringByEvaluatingJavaScriptFromString:@"document.documentElement.style.overflow='hidden'"]; } ``` 以上代码属于`macOS`上的开发内容. URL: https://sunyazhou.com/2017/01/SpringFestival/index.html.md Published At: 2017-01-28 13:42:34 +0000 # 2017回家过年 ![](http://p1.bqimg.com/584350/7df258f2ecddcf81.webp) > 练习了一年的小楷, 感谢老乡**潘旭**的帮助,我的书法有很多提升. 出发 -- 当我拿着行李箱等待过年回家地铁那一刻 又是一年过去了 ![](http://p1.bqimg.com/584350/035a303723a4b623.webp) 这么多年总觉得自己和农民工一样,无非就是穿着体面一些,但内心还是农民工的内心. 高铁 -- 睁开眼睛看到这篇黑土地盖着一层雪, 我应该快到家了。 ![Markdown](http://p1.bpimg.com/584350/5addde1fe4d65611.webp) 我就是从这片黑土地走出去的年轻人, 我23年没有离开过这片黑土地,如今在京码砖好几年。买房没资格,买车没牌照,只能每天骑车我心爱的小牛M1上班。(其实我内心是崩溃的),只有雾霾陪着哥,有事没事来北京看看我和北京的土著。 24岁入职百度如今已经快3年了, 想想已经很久没换工作了.工资少的可怜,想起那个每天都一门心思研究人工智能的厂长,我真想拒绝再给他打工了. 中转站 -- 擦 车到站了 (我打算以后在这买个楼) ![Markdown](http://i1.piimg.com/584350/698eb97cae2b2dea.webp) 刚下火车 已经被这种熟悉的冷瞬间打透.这种冷是多么的熟悉, 我早穿上了 [林甸鞋](https://item.taobao.com/item.htm?spm=a230r.1.14.9.pmOPas&id=36777946060&ns=1&abbucket=15#detail),只有熟悉东北的小伙伴才知道啥叫林甸鞋 > 大庆市林甸县制造 的林甸鞋 俗称**老头鞋** 我就不上照片了,我觉得这是东北人值得骄傲的文化遗产,都可以申请世界文化遗产了,在冰城没有这鞋我估计你很难度过漫长严寒的冬天。一双鞋售价仅仅 33 元 送一双羊毛鞋垫, 说句实话这绝对是东北人的业界良心,这鞋一年都不断帮断底,一个冬天穿这鞋很暖和,不信你可以问问你身边的东北人。 > 当然不熟悉套路的估计不知道买啥牌子好, 我印象中只有 林嘉牌 双丽牌比较好 但是工厂估计早破产了,33元每双上哪挣钱去啊。想买的小伙伴或者想去东北旅游的小伙伴 建议随便买一双的就行了 都一样 反正不贵。 ![Markdown](http://p1.bqimg.com/584350/fb737f626882c66b.webp) 小吃 -- 古云人:"上车饺子下车面" 这句话说的很有哲理我特意在百度搜了一下 不过我还是想去吃哈尔滨的喜家德水饺 ![Markdown](http://p1.bqimg.com/584350/042feb2337732f5e.webp) > trust me如果到哈尔滨必须要尝尝**喜家德水饺**和**东方饺子王** > 不去尝尝永远不知道啥叫地道的饺子, 做饺子能做到淋漓尽致也没谁了,饺子皮、大小、厚度、弧度、都有专门的计量工具, 如果把**做饺子**当做一个app的话,我觉得喜家德应该是 app 中的支付宝, 但是比较蛋疼的是这些传统的民族企业都不发展互联网, 不支持任何网络支付、支付宝、微信、百度钱包都用不了、我真觉的喜家德CEO应该聘请哥给他当互联网总裁、用互联网发展一下传统企业、这种封闭的经营模式我持批评的态度.这是新时代的中国、想发展必须要与时俱进、跟上时代的发展不能闭关锁国、我怀疑喜家德的 老大不太相信互联网给他带来的任何改变、这一点我必须批评一下他、要拿出李鸿章大人以一人抵一国的能力. 不断学习,与时俱进 冰雪大世界 -- 说来惭愧在哈尔滨好几年都没去过**冰雪大世界**(上学时比较穷)门票在糯米上至少需要360左右/人,这绝对有点贵. 参加工作以后挣钱了必须想圆了学生时代的梦想,于是 让我大姐请我去了哈哈 ![Markdown](http://p1.bqimg.com/584350/d449b5d2f5debbed.webp) ![Markdown](http://i1.piimg.com/584350/f894381b5bd3aaee.webp) ![Markdown](http://i1.piimg.com/584350/d1115c4e6d7981ab.webp) > 这个世界玩的都是人的想象力,我从来没想过冰和雪能玩出什么花样,去了才知道只要有想象力和创造力 一件普通东西能这么有意思。记得一点要有车去大世界最好, 我建议租个车或者亲戚有车,不然必须坐黑车回来,这个很蛋疼,什么滴滴打车 用司机的话 **"啥滴都不好使"** 这个确实很无奈,大家玩完回不去很是蛋疼.政府得加大力度管制这些出租车司机. 美丽冰城 -- ![Markdown](http://i1.piimg.com/584350/a9abec8d1c2c56e0.webp) ![Markdown](http://i1.piimg.com/584350/7bf35db34b720d2a.webp) ![Markdown](http://i1.piimg.com/584350/dc97959821ac8fa2.webp) ![Markdown](http://i1.piimg.com/584350/a260c895027dd64c.webp) > 在哈尔滨俄罗斯的建筑随处可见, 哈站前边(前两张照片)始于1907年有个什么斯基开个茶馆, 建的楼群依然还矗立在果戈里大街附近, 没去的小伙伴可以去看看, 最后一张断壁残垣在抢救性修复中。 红肠 -- 哈尔滨有个比较出名的东西貌似叫红肠,但是 不是本地人很难知道哪里买的才算地道 我今天可以告诉大家啥叫地道的哈尔滨红肠 ![Markdown](http://i1.piimg.com/584350/1736e20ba1e625f5.webp) ![Markdown](http://p1.bpimg.com/584350/40a9d97bdeee2b05.webp) > 大家一定听过秋林红肠,都以为红肠应该是秋林牌子,其实好像叫秋林里道斯系列的食品 > 如果想买正宗的红肠我推荐2个品牌个两个地点 * **商委红肠** 这个商委在 道外红旗大街514号(黑龙江工程学院对面)前店后厂,之所以这么出名是因为商委红肠全程纯手工制作,深得龙江人喜爱.很多人喜欢吃它那个专有的味道.也有很多人不喜欢它的味道嫌它大蒜放的多哈哈.反正我吃都一样没啥特别的.如果我回哈尔滨发展我一定学学红肠怎么制作的自己做的才放心哈哈. 这个红肠火爆的程度得早晨5点去排队 每天几百斤,卖没了就售罄了,尤其过节期间 * **秋林里道斯红肠** 这个就在上边那绿色图片的地方地下一层,看哪里排队人多就在那里买就是秋林红肠最正宗的卖点,一定不要相信任何人说秋林红肠换地方了,好几十年了都没换地方怎么可能说换就换.都是商业竞争搞得鬼把戏,岂能欺骗我这个程序猿. (东大直街319号(近秋林国际购物中心) 15245082279,(0451)58938888) 就下图这地方 ![Markdown](http://p1.bqimg.com/584350/b4c23a2cdd0e057b.webp) 以后不要问谁哪里买红肠最好了,上边的就是最正宗的买红肠的地方了. 回家 -- ![Markdown](http://p1.bpimg.com/584350/8f60e12618c08949.webp) 看着家乡的夕阳,我真的快到家了-海伦市 URL: https://sunyazhou.com/2017/01/iOSDevelopmentKnowledgePointAccumulate/index.html.md Published At: 2017-01-18 13:44:57 +0000 # iOS开发知识点积累 > 搞了很久iOS开发, 以前都是用脑子记某种技术文章和技术实现的代码,但是当一个人的大脑超过一定存储极限的时候就会出现栈溢出(其实我比较笨),后来开始逐渐记某博客的是谁写的,或者技巧实现的代码。。。后来发现不但栈溢出,堆也快存不住海量的iOS技术文章了。。。唉于是我的chrome上保留了所有经典的文章标签和浏览器网页地址,现在我想把它整理出来放在博客里,方便查找某技术实现的代码(其实我的原百度云小伙伴实习生都觉得我能对某种技术存储如此详细赶到惊叹).好了 我们开始iOS知识点技术导航 iOS技术分类如下 * 音频 * 相机与照片 * 图形图像 * 动画 * UI转场 * ASDK(AsyncDisplayKit) * swift相关 * 数学图形 * 架构 * Masonry * Cocoapods * 文件相关 音频 -- __[iOS音频播放 (一):概述](http://msching.github.io/blog/2014/07/07/audio-in-ios/)__ __[iOS音频播放 (二):AudioSession](http://msching.github.io/blog/2014/07/08/audio-in-ios-2/)__ __[iOS音频播放 (三):AudioFileStream](http://msching.github.io/blog/2014/07/09/audio-in-ios-3/)__ __[iOS音频播放 (四):AudioFile](http://msching.github.io/blog/2014/07/19/audio-in-ios-4/)__ __[iOS音频播放 (五):AudioQueue](http://msching.github.io/blog/2014/08/02/audio-in-ios-5/)__ __[iOS音频播放 (六):简单的音频播放器实现](http://msching.github.io/blog/2014/08/09/audio-in-ios-6/)__ __[iOS音频播放 (七):播放iPod Library中的歌曲](http://msching.github.io/blog/2014/09/07/audio-in-ios-7/)__ __[iOS音频播放 (八):NowPlayingCenter和RemoteControl](http://msching.github.io/blog/2014/11/06/audio-in-ios-8/)__ __[iOS音频播放 (九):边播边缓存](http://msching.github.io/blog/2016/05/24/audio-in-ios-9/)__ > 以上内容来自[码农人生](http://msching.github.io/) 这个哥们我有过交流,感觉底层音频技术比较透彻,适合初学者以及中级开发者研究学习和使用。 图形处理 -- __[基础知识](https://objccn.io/issue-21-1/)__一些列教程可以连续看完 __[GPUImage](https://github.com/BradLarson/GPUImage)__库 __[iOS GPUImage源码解读(一)](http://mp.weixin.qq.com/s/pg2vPYftkfghoQswxJFIvw)__ __[开源一个上架 App Store 的相机 App](http://hawk0620.github.io/blog/2017/02/17/zpcamera-opensource-share/)__ 图形图像 -- __[基于 OpenCV 的人脸识别](https://www.objccn.io/issue-21-9/)__ __[图片编辑](https://github.com/3tinkers/TKImageView)__ 动画 -- __[QQ中未读气泡拖拽消失的实现分析](http://kittenyang.com/drawablebubble/)__ __[iOS 自定义下拉线条动画](http://kittenyang.com/curvelineanimation/)__ __[一个库涵盖了所有iOS动画效果](https://github.com/sunyazhou13/Animations)__ __[pop](https://github.com/facebook/pop)__ > 学动画先从[骑滔(Kitten)](http://kittenyang.com/)的动画搞起最靠谱 > 以上是普通动画内容2篇来自Kitten > 持续更新中 转场动画 -- __[WWDC 2013 Session笔记 - iOS7中的ViewController切换]()__ 喵神的这篇必看 __[UIPresentationController Tutorial: Getting Started](https://www.raywenderlich.com/139277/uipresentationcontroller-tutorial-getting-started)__ 需要翻墙 *(话说我解释一下这个词"翻墙",翻墙名词叫科学上网,黑话叫自备梯子,因为大家一开始都用[云梯VPN](https://www.yuntipub.com/)访问国外网站,因为我国搞了个垃圾防火墙的大型局域网,虽然阻碍了世界文明和技术科技的发展但也防范了一些不健康内容,比如万一有一天你搞个车床,制造个微冲出来怎么办哈哈,所以要翻越那个防火墙就俗称翻墙)* __[自定义控制器转场动画及下拉菜单的小Demo | AppCoda翻译系列](http://wxgbridgeq.github.io/blog/2015/08/10/custom-transition-animation/)__ >还有可以github搜索__[Transition](https://github.com/search?l=Objective-C&o=desc&q=Transition&s=stars&type=Repositories&utf8=%E2%9C%93)__ >很多这种转场动画不一一介绍 ASDK(AsyncDisplayKit) -- __[官方文档](http://asyncdisplaykit.org/)__ (需要翻墙) __[中文翻译](http://reactnative.cn/docs/0.46/getting-started.html)__ __[AsyncDisplayKit 2.0 Tutorial: Getting Started](https://www.raywenderlich.com/124311/asyncdisplaykit-2-0-tutorial-getting-started)__ __[AsyncDisplayKit 2.0 Tutorial: Automatic Layout](https://www.raywenderlich.com/124696/asyncdisplaykit-2-0-tutorial-automatic-layout)__ __[AsyncDisplayKit官方文档翻译](http://awhisper.github.io/2016/05/04/AsyncDisplayKit%E5%AE%98%E6%96%B9%E6%96%87%E6%A1%A3%E7%BF%BB%E8%AF%91/)__ __[AsyncDisplayKit源码分析(一)轮廓梳理](http://awhisper.github.io/2016/05/06/AsyncDisplayKit%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/)__ __[AsyncDisplayKit源码分析(二) 异步渲染](http://awhisper.github.io/2016/12/16/AysncDisplayKit%E5%88%86%E6%9E%90-%E4%BA%8C/)__ __[使用ASDK性能调优-提升iOS界面的渲染性能](http://draveness.me/asdk-rendering/)__ > 以上几篇分别来自 __[raywenderlich](https://www.raywenderlich.com/)__ > 源码分析来自于 __[折腾范儿の味精 > ](http://awhisper.github.io/)__ 一个百度阅读团队同事的博客 > 在这里我说一下我对ASDK的看法,我视图读过源码和官方文档,我发现这个不是你想用想用就能马上用的东西,简直可以让一个初学者学习一遍 **UIKit** 一样集成起来倒是很简单,但是就那是那个布局就足够一个开发人员研究一阵子,用不了masonry,但是功能单一的页面需要调优可以考虑一下。 swift相关 -- __[喵神的网站](http://swifter.tips/)__ 目前好像停止了更新 iOS开发领域喵神 真是神一般的存在 __[swift随机数](http://southpeak.github.io/2015/09/26/ios-techset-5/)__ 来自__[南峰子 老驴](http://southpeak.github.io/)__ 一个百度前同事现在在京东金融貌似, 有过技术交流很NB的一个人. __[Swift 3必看:从使用场景了解GCD新API](http://www.jianshu.com/p/fc78dab5736f)__ 这个哥们我没有了解过 不过很多文章写的很好希望以后有机会交流一下 > 持续更新中更新... 数学图形 -- __[图形数学](https://jackschaedler.github.io/)__ eg:傅里叶变换 架构 -- __[Casa博客](http://casatwy.com/)__ > 我必须评价一下这个Casa哥们,iOS架构师我唯一佩服的人,用我的话就是,这才是真正程序员心中的架构师,而不是哪些所谓的听起来很NB的架构师,我在百度个人云(你们看到的是百度网盘)工作时有个T8架构师就坐在我对面,那个架构师每天闲的我真想撅他,改iOS程序 xib的引用没去掉都不知道 最好导致线上崩溃,我其实非常想送他一句话,不管技术多NB 每天都要保持写代码,记得孔子的话:『吾尝终日而思矣,不如须臾之所学也』。 > 这个Casa的哥们让我看到了什么叫 架构工程师 和业务工程师,这是一个能真正去写架构代码然后扔给业务工程师说:按照这个搞法 Masonry -- __[](http://tutuge.me/2015/05/23/autolayout-example-with-masonry/)__ __[有趣的Autolayout示例-Masonry实现](http://tutuge.me/2015/05/23/autolayout-example-with-masonry/)__ __[有趣的Autolayout示例2-Masonry实现](http://tutuge.me/2015/08/08/autolayout-example-with-masonry2/)__ __[有趣的Autolayout示例3-Masonry实现](http://tutuge.me/2015/12/14/autolayout-example-with-masonry3/)__ __[有趣的Autolayout示例4-Masonry实现](http://tutuge.me/2016/08/06/autolayout-example-with-masonry4/)__ __[有趣的Autolayout示例5-Masonry实现](http://tutuge.me/2017/03/12/autolayout-example-with-masonry5/)__ __[iOS自动布局框架-Masonry详解](http://www.jianshu.com/p/ea74b230c70d)__ __[Masonry — 使用纯代码进行iOS应用的autolayout自适应布局](http://www.ios122.com/2015/09/masonry/)__ 中文翻译 > 话说 我个人认为学习masonry只需要看看 中文翻译之后 再去看看土土哥的教程就会了。土土哥的masonry教程简直就是中文文档。写的非常好 Cocoapods -- __[用CocoaPods做iOS程序的依赖管理](http://blog.devtang.com/2014/05/25/use-cocoapod-to-manage-ios-lib-dependency/)__ 巧神的文章必看 文件相关 -- __[文件列表](https://github.com/sunyazhou13/FileExplorer)__ __[HYFileManager](https://github.com/sunyazhou13/HYFileManager)__ 博客列表 -- | 博客地址 | RSS地址 | | ---------------------------------------- | :--------------------------------------- | | [OneV's Den](http://onevcat.com) | | | [破船之家](http://beyondvincent.com) | | | [NSHipster](http://nshipster.cn) | | | [Limboy 无网不剩](http://blog.leezhong.com/) | | | [唐巧的技术博客](http://blog.devtang.com) | | | [Lex iOS notes](http://ios.lextang.com) | | | [念茜的博客](http://nianxi.net) | | | [Xcode Dev](http://blog.xcodev.com) | | | [Ted's Homepage](http://wufawei.com/) | | | [txx's blog](http://blog.t-xx.me) | | | [KEVIN BLOG](http://imkevin.me) | | | [阿毛的蛋疼地](http://www.xiangwangfeng.com) | | | [亚庆的 Blog](http://billwang1990.github.io) | | | [Nonomori](http://nonomori.farbox.com) | | | [言无不尽](http://tang3w.com) | | | [Wonderffee's Blog](http://wonderffee.github.io) | | | [I'm TualatriX](http://imtx.me) | | | [vclwei](http://vclwei.com) | | | [Cocoabit](http://blog.cocoabit.com) | | | [nixzhu on scriptogr.am](http://nixzhu.me) | | | [不会开机的男孩](http://studentdeng.github.io) | | | [Nico](http://www.taofengping.com) | | | [阿峰的技术窝窝](http://hufeng825.github.io) | | | [answer_huang](http://answerhuang.duapp.com) | | | [webfrogs](http://webfrogs.me) | | | [代码手工艺人](http://joeyio.com) | | | [Lancy's Blog](http://gracelancy.com) | | | [I'm Allen](http://www.imallen.com) | | | [Travis' Blog](http://imi.im/) | | | [王中周的技术博客](http://wangzz.github.io/) | | | [会写代码的猪](http://jiajun.org/) | | | [克伟的博客](http://wangkewei.cnblogs.com/) | | | [摇滚诗人](http://cnblogs.com/biosli) | | | [Luke's Homepage](http://geeklu.com/) | | | [萧宸宇](http://iiiyu.com/) | | | [Yuan博客](http://www.heyuan110.com/) | | | [Shining IO](http://shiningio.com/) | | | [YIFEIYANG--易飞扬的博客](http://www.yifeiyang.net/) | | | [KooFrank's Blog](http://koofrank.com/) | | | [hello it works](http://helloitworks.com) | | | [码农人生](http://msching.github.io/) | | | [玉令天下的Blog](http://yulingtianxia.com) | | | [不掏蜂窝的熊](http://www.hotobear.com/) | | | [猫·仁波切](https://andelf.github.io/) | | | [煲仔饭](http://ivoryxiong.org/) | | | [里脊串的开发随笔](http://adad184.com) | | | [Chun Tips](http://chun.tips/) | | | [Why's blog - 汪海的实验室](http://blog.callmewhy.com/) | | URL: https://sunyazhou.com/2017/01/LearningmacOSdevelopmentShortcut/index.html.md Published At: 2017-01-13 11:38:30 +0000 # 学习macOS开发的路线 ## 开发总结 ![macOS icon](/assets/images/20170113LearningmacOSdevelopmentShortcut/CocoaProgrammingForOSX.webp) 1. 学习完所有__[raywenderlich](https://www.raywenderlich.com/category/macos)__的__macOS__开发教程 2. __[Cocoa Programming for OS X (5th Edition)](https://pan.baidu.com/s/1nuMnePj)__必看(英文版没有中文) 3. __[[Advanced.Mac.OS.X.Programming(3rd.2011)].Mark.Dalrymple.文字版](https://pan.baidu.com/s/1i493Zpz)__必看 以上这些必须看一遍 包括那两本epub的书籍 虽然都是英文的(我本人英语应该不算太好但也不差)看这个很easy __博客__ __[老谭笔记](http://www.tanhao.me/)__ 这个大牛是原新浪网盘mac端的技术开发他的每一篇文章我都看过并实践过,大家如果学习macOS开发我建议先认真扫一遍他的博客 __[喵神](https://onevcat.com/)__ __[巧神](http://blog.devtang.com/)__ 这些博客都是每个iOS&macOS开发必看的 剩下的就是一堆博客了太多 我就不列了 大家github搜一下 欢迎大家提出问题 一起交流和学习,本人开发年头也不算太短了,以前搞不明白博客咋玩的,2017年开始决定先订个小目标 把博客搞好欢迎各位批评指教,鄙人定当咨诹善道察纳雅言,多谢! URL: https://sunyazhou.com/2017/01/UIViewControllerCodeStandard/index.html.md Published At: 2017-01-13 11:18:07 +0000 # UIViewController代码规范 # pragma 标准写法 ## Objective-C ```objc #pragma mark - #pragma mark - private methods 私有方法 #pragma mark - #pragma mark - public methods 公有方法 #pragma mark - #pragma mark - override methods 复写方法 #pragma mark - #pragma mark - getters and setters 设置器和访问器 #pragma mark - #pragma mark - UITableViewDelegate #pragma mark - #pragma mark - CustomDelegate 自定义的代理 #pragma mark - #pragma mark - event response 所有触发的事件响应 按钮、通知、分段控件等 #pragma mark - #pragma mark - life cycle 视图的生命周期 #pragma mark - #pragma mark - StatisticsLog 各种页面统计Log ``` ## Swift ```swift // MARK: - // MARK: - override methods 复写方法 // MARK: - // MARK: - getters and setters 设置器和访问器 // MARK: - // MARK: - UITableViewDelegate 代理 // MARK: - // MARK: - CustomDelegate 自定义的代理 // MARK: - // MARK: - event response 所有触发的事件响应 按钮、通知、分段控件等 // MARK: - // MARK: - private methods 私有方法 // MARK: - // MARK: - public methods 公有方法 // MARK: - // MARK: - life cycle 视图的生命周期 // MARK: - // MARK: - StatisticsLog 各种页面统计Log ``` --- # UIViewController生命周期方法 在 Objective-C 中,`UIViewController` 的生命周期方法涵盖了视图控制器从创建到销毁的整个过程。以下是这些方法的全面列表,包括它们被调用的时机和一些细节: 1. **初始化和加载视图** - `initWithNibName:bundle:`:使用 nib 文件初始化视图控制器。 - `initWithCoder:`:使用 storyboard 初始化视图控制器。 - `loadView`:加载视图控制器的视图,如果未设置 `view` 属性,此方法会被自动调用。 - `viewDidLoad`:视图加载完成后调用,常用于初始化代码。 2. **视图将要出现** - `viewWillAppear:`:视图将要显示在屏幕上之前调用,可以在此方法中进行界面更新。 - `viewWillLayoutSubviews`:在视图的布局将要发生之前调用,可以在此方法中进行子视图的布局。 - `viewDidLayoutSubviews`:在视图的布局已经完成后调用。 3. **视图已经出现** - `viewDidAppear:`:视图已经显示在屏幕上后调用,可以在此方法中进行界面更新。 - `viewDidDisappear:`:视图从屏幕上消失后调用。 4. **内存警告** - `didReceiveMemoryWarning`:当系统内存不足时调用,视图控制器可以释放一些资源。 5. **旋转和大小变化** - `willRotateToInterfaceOrientation:duration:`:设备将要旋转到指定方向之前调用(iOS 6 及之前版本)。 - `willAnimateRotationToInterfaceOrientation:duration:`:设备将要旋转到指定方向时调用,可以进行动画(iOS 6 及之前版本)。 - `didRotateFromInterfaceOrientation:`:设备从指定方向旋转完成后调用(iOS 6 及之前版本)。 - `viewWillTransitionToSize:withTransitionCoordinator:`:设备将要旋转或视图控制器的尺寸将要变化时调用(iOS 8 及以后版本)。 - `viewDidTransitionFromSize:withTransitioningCoordinator:`:设备旋转完成或视图控制器尺寸变化完成后调用(iOS 8 及以后版本)。 6. **交互** - `shouldAutorotate`:询问视图控制器是否支持自动旋转(iOS 6 及之前版本)。 - `supportedInterfaceOrientations`:返回视图控制器支持的界面方向。 - `preferredInterfaceOrientationForPresentation`:返回视图控制器的首选显示方向。 7. **交互消失** - `viewWillDisappear:`:视图将要消失时调用。 - `viewWillUnload`:视图将要被销毁时调用,iOS 6 以后版本中不再推荐使用。 8. **终止** - `dealloc`:视图控制器被销毁时调用。 请注意,从 iOS 6 开始,苹果推荐使用自动旋转支持方法(`shouldAutorotate`、`supportedInterfaceOrientations` 和 `preferredInterfaceOrientationForPresentation`)来处理设备方向变化,而不是使用 `willRotateToInterfaceOrientation:duration:` 等方法。此外,`viewWillUnload` 方法在 iOS 6 及以后的版本中不再被调用,苹果推荐使用 `viewDidDisappear:` 来替代。 这些方法为视图控制器的生命周期提供了丰富的控制点,允许开发者在适当的时机进行资源管理、界面更新和状态保存。 ### UIViewController的示例代码如下: 以下是一些示例代码,展示了在 Objective-C 中如何实现 `UIViewController` 生命周期方法: ```objc // ViewController.h #import @interface ViewController : UIViewController @end // ViewController.m #import "ViewController.h" @implementation ViewController // 初始化和加载视图 - (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; if (self) { // Custom initialization } return self; } - (instancetype)initWithCoder:(NSCoder *)aDecoder { self = [super initWithCoder:aDecoder]; if (self) { // Custom initialization } return self; } - (void)loadView { [super loadView]; // Create your custom view here if not using a nib or storyboard } - (void)viewDidLoad { [super viewDidLoad]; // Perform additional setup after loading the view, typically from a nib. } // 视图将要出现 - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; // Prepare your view for display } - (void)viewWillLayoutSubviews { [super viewWillLayoutSubviews]; // Layout your subviews here } - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; // Perform any additional layout here } // 视图已经出现 - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; // Update your UI here } - (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; // Prepare your view for disappearance } - (void)viewDidDisappear:(BOOL)animated { [super viewDidDisappear:animated]; // Clean up after your view disappears } // 内存警告 - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; // Release any cached data, images, etc. that aren't in use } // 旋转和大小变化 - (BOOL)shouldAutorotate { return YES; } - (NSUInteger)supportedInterfaceOrientations { return UIInterfaceOrientationMaskAll; } - (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation { return UIInterfaceOrientationPortrait; } - (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id)coordinator { [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator]; // Handle the transition to a different size } - (void)viewDidTransitionFromSize:(CGSize)fromSize withTransitioningCoordinator:(id)coordinator { [super viewDidTransitionFromSize:fromSize withTransitioningCoordinator:coordinator]; // Handle the transition completion } @end ``` 这段代码展示了 `UIViewController` 生命周期方法的基本实现。在实际应用中,你需要根据具体需求在这些方法中添加相应的逻辑。例如,在 `viewDidLoad` 中初始化视图,在 `viewWillAppear:` 中更新 UI,在 `viewWillDisappear:` 中保存状态等。