Swift高阶函数

高阶函数是指可以接收函数作为参数或者返回函数作为结果的函数。Swift 标准库提供了多个强大的高阶函数,主要用于集合类型的操作。以下是 Swift 中常用的高阶函数及其使用场景:

1. map(_:)

功能:对集合中的每个元素进行转换,返回一个新的数组。

使用场景:当你需要对数组中的每个元素进行相同操作并得到新数组时。

let numbers = [1, 2, 3, 4]
let squared = numbers.map { $0 * $0 }  // [1, 4, 9, 16]

2. compactMap(_:)

功能:类似于 map,但会自动过滤掉 nil 值。

使用场景:转换可能产生 nil 的情况,并希望自动去除 nil 值。

let strings = ["1", "2", "three", "4"]
let numbers = strings.compactMap { Int($0) }  // [1, 2, 4]

3. flatMap(_:)

功能:将二维数组"压平"为一维数组,或处理嵌套可选值。

使用场景

  • 处理嵌套数组
  • 处理嵌套可选值(Swift 4.1+ 中对于可选值推荐使用 compactMap)
let nestedArray = [[1, 2, 3], [4, 5, 6]]
let flattened = nestedArray.flatMap { $0 }  // [1, 2, 3, 4, 5, 6]

4. filter(_:)

功能:根据条件过滤集合中的元素。

使用场景:需要从集合中筛选符合条件的元素时。

let numbers = [1, 2, 3, 4, 5, 6]
let evens = numbers.filter { $0 % 2 == 0 }  // [2, 4, 6]

5. reduce(_:_:)

功能:将集合中的所有元素组合成一个值。

使用场景:需要计算总和、拼接字符串或任何需要将集合"缩减"为单个值的操作。

let numbers = [1, 2, 3, 4]
let sum = numbers.reduce(0) { $0 + $1 }  // 10
let product = numbers.reduce(1, *)  // 24

6. sorted(by:)

功能:根据条件对集合进行排序。

使用场景:需要自定义排序规则时。

let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
let sortedNames = names.sorted(by: { $0 < $1 })  // 升序排列

7. forEach(_:)

功能:对集合中的每个元素执行操作。

使用场景:需要对每个元素执行操作但不需要返回值时(与 map 的区别)。

["a", "b", "c"].forEach { print($0) }

8. contains(where:)

功能:检查集合中是否包含满足条件的元素。

使用场景:需要检查集合中是否存在符合条件的元素时。

let numbers = [1, 2, 3, 4, 5]
let hasEven = numbers.contains(where: { $0 % 2 == 0 })  // true

9. first(where:)/last(where:)

功能:返回集合中第一个/最后一个满足条件的元素。

使用场景:需要查找集合中第一个或最后一个符合条件的元素时。

let numbers = [10, 20, 30, 40]
let firstMultipleOf15 = numbers.first(where: { $0 % 15 == 0 })  // 30

10. prefix(while:)/drop(while:)

功能

  • prefix(while:):从开头开始获取满足条件的元素,直到条件不满足
  • drop(while:):跳过开头满足条件的元素,返回剩余部分

使用场景:需要基于条件获取或跳过集合开头部分时。

let scores = [85, 90, 78, 92, 88, 76]
let goodScores = scores.prefix(while: { $0 >= 85 })  // [85, 90]
let afterFirstBad = scores.drop(while: { $0 >= 85 })  // [78, 92, 88, 76]

11. zip(_:_:)

功能:将两个序列组合成一个元组序列。

使用场景:需要同时遍历两个集合时。

let names = ["Alice", "Bob", "Charlie"]
let scores = [85, 92, 78]
for (name, score) in zip(names, scores) {
    print("\(name): \(score)")
}

使用建议

  1. 链式调用:可以组合多个高阶函数

    let result = (1...10)
        .filter { $0 % 2 == 0 }
        .map { $0 * $0 }
        .reduce(0, +)
  2. 性能考虑:每个高阶函数都会创建一个新集合,对于大数据集可能需要考虑性能
  3. 可读性:当逻辑复杂时,有时传统的 for 循环可能更易读

这些高阶函数使 Swift 代码更加简洁、表达力更强,是函数式编程风格的重要组成部分。

补充:flatMap 与 compactMap 的区别

compactMap 不会自动将二维数组降为一维数组,这是它与 flatMap 的一个重要区别。

主要区别

  1. compactMap:

    • 主要用途:在映射过程中过滤掉 nil
    • 不会自动"压平"二维数组
    • 返回的数组维度与输入相同
  2. flatMap (在 Swift 4.1 及以后版本):

    • 主要用途:将二维数组降为一维数组
    • 不处理 nil 值过滤(对于可选值过滤,改用 compactMap

示例对比

1. compactMap 行为

let nestedArray = [[1, nil, 3], [nil, 5, 6]]
let result = nestedArray.compactMap { $0 }
print(result) 
// 输出: [[Optional(1), nil, Optional(3)], [nil, Optional(5), Optional(6)]]
// 仍然是二维数组,只是对最外层数组进行了 nil 过滤(这里没有 nil 所以没变化)

2. 正确使用 compactMap 过滤 nil

let arrayWithOptionals: [Int?] = [1, nil, 3, nil, 5]
let nonNilValues = arrayWithOptionals.compactMap { $0 }
print(nonNilValues) // 输出: [1, 3, 5]

3. flatMap 降维示例

let nestedArray = [[1, 2, 3], [4, 5, 6]]
let flattened = nestedArray.flatMap { $0 }
print(flattened) // 输出: [1, 2, 3, 4, 5, 6]

历史变化

  • Swift 4.1 之前flatMap 有三种功能:

    1. 二维数组降维
    2. 过滤 nil (类似现在的 compactMap)
    3. 组合操作
  • Swift 4.1 及以后

    • flatMap 只保留降维功能
    • 过滤 nil 的功能迁移到 compactMap

如何实现带 nil 过滤的降维

如果需要同时处理二维数组降维和过滤 nil,可以组合使用:

let nestedWithOptionals = [[1, nil, 3], [nil, 5, 6]]
let result = nestedWithOptionals.flatMap { $0 }.compactMap { $0 }
print(result) // 输出: [1, 3, 5, 6]

Dart Mixin 是什么?

Mixin 是 Dart 中一种代码复用的机制,允许将多个类的功能组合到一个类中,而无需使用继承。Mixin 通过 with 关键字实现,通常用于在不创建复杂继承层次的情况下共享代码。

如何使用 Mixin?

  1. 定义 Mixin
    使用 mixin 关键字定义 Mixin,Mixin 可以包含方法和属性,但不能有构造函数。

    mixin LoggingMixin {
      void log(String message) {
        print('Log: $message');
      }
    }
  2. 使用 Mixin
    在类中使用 with 关键字将 Mixin 的功能添加到类中。

    class User with LoggingMixin {
      String name;
    
      User(this.name);
    
      void greet() {
        log('Hello, $name');
      }
    }
  3. 调用 Mixin 方法
    通过类的实例调用 Mixin 中的方法。

    void main() {
      var user = User('Alice');
      user.greet();  // 输出: Log: Hello, Alice
    }

Mixin 用于解决什么问题?

  1. 代码复用
    Mixin 允许在不使用继承的情况下复用代码,避免多重继承的复杂性。
  2. 避免继承的局限性
    Dart 不支持多重继承,Mixin 提供了一种灵活的方式来组合多个类的功能。
  3. 模块化设计
    Mixin 可以将功能模块化,便于在不同类中灵活组合。

其他特性

  1. 限制 Mixin 的使用范围
    使用 on 关键字限制 Mixin 只能用于特定类或其子类。

    mixin LoggingMixin on User {
      void log(String message) {
        print('User Log: $message');
      }
    }
  2. 多个 Mixin
    一个类可以使用多个 Mixin,按顺序应用。

    class User with LoggingMixin, SerializableMixin {
      // 类实现
    }

总结

Mixin 是 Dart 中一种强大的代码复用工具,通过 with 关键字将多个 Mixin 组合到一个类中,解决了代码复用和多重继承的问题,同时支持模块化设计。

尽管 Dart 中的 Mixin 是一种强大的代码复用工具,但在使用过程中可能会遇到一些问题或挑战。以下是一些常见的问题及其注意事项:


1. 命名冲突

  • 问题:如果多个 Mixin 或类中存在相同名称的方法或属性,可能会导致冲突。
  • 示例

    mixin A {
      void log() => print('A');
    }
    
    mixin B {
      void log() => print('B');
    }
    
    class C with A, B {} // 使用 A 和 B 的 log 方法

    在这种情况下,C 会使用最后一个 Mixin(B)中的 log 方法,A 中的 log 会被覆盖。

  • 解决方法

    • 避免命名冲突,确保 Mixin 中的方法或属性名称唯一。
    • 使用 super 关键字显式调用特定 Mixin 的方法。

2. Mixin 的顺序影响行为

  • 问题:Mixin 的顺序会影响方法的调用顺序,因为后面的 Mixin 会覆盖前面的同名方法。
  • 示例

    mixin A {
      void log() => print('A');
    }
    
    mixin B {
      void log() => print('B');
    }
    
    class C with A, B {} // B 的 log 会覆盖 A 的 log

    如果调换顺序(with B, A),Alog 会覆盖 Blog

  • 解决方法

    • 明确 Mixin 的顺序,确保逻辑正确。
    • 使用 super 关键字在 Mixin 中调用父类或其他 Mixin 的方法。

3. Mixin 的依赖性问题

  • 问题:Mixin 可能依赖于某些特定类的方法或属性,如果使用不当,会导致运行时错误。
  • 示例

    mixin LoggingMixin {
      void log() {
        print(name); // 假设 name 属性存在
      }
    }
    
    class User with LoggingMixin {
      String name = 'Alice';
    }
    
    class Admin with LoggingMixin {} // 错误:Admin 没有 name 属性

    这里,LoggingMixin 依赖于 name 属性,但 Admin 类没有定义 name,会导致运行时错误。

  • 解决方法

    • 使用 on 关键字限制 Mixin 的使用范围,确保 Mixin 只能用于特定类或其子类。
    • 在 Mixin 中明确依赖的接口或属性。

4. Mixin 的滥用导致代码复杂度增加

  • 问题:过度使用 Mixin 可能导致代码难以理解和维护,尤其是当多个 Mixin 组合在一起时。
  • 示例

    class User with LoggingMixin, SerializableMixin, CachingMixin, ValidationMixin {
      // 类实现
    }

    如果 Mixin 过多,类的行为会变得不清晰,难以追踪方法的来源。

  • 解决方法

    • 限制 Mixin 的数量,避免过度组合。
    • 将功能相似的 Mixin 合并为一个。

5. Mixin 不能有构造函数

  • 问题:Mixin 不能定义构造函数,因此无法直接初始化状态。
  • 示例

    mixin CounterMixin {
      int count = 0; // 需要初始化
    }

    如果需要在 Mixin 中初始化状态,只能通过方法或属性来实现。

  • 解决方法

    • 在 Mixin 中提供初始化方法。
    • 将状态初始化交给使用 Mixin 的类。

6. Mixin 的调试难度

  • 问题:由于 Mixin 的行为是动态组合的,调试时可能难以追踪方法的调用路径。
  • 解决方法

    • 使用清晰的命名和文档,明确每个 Mixin 的职责。
    • 避免过于复杂的 Mixin 组合。

7. Mixin 与继承的混淆

  • 问题:Mixin 和继承在某些情况下可能被混淆,尤其是当 Mixin 依赖于特定类时。
  • 示例

    mixin LoggingMixin on User {
      void log() => print(name);
    }
    
    class Admin extends User with LoggingMixin {} // 正确
    class Guest with LoggingMixin {} // 错误:Guest 不是 User 的子类

    这里,LoggingMixin 只能用于 User 或其子类。

  • 解决方法

    • 明确区分 Mixin 和继承的使用场景。
    • 使用 on 关键字时,确保 Mixin 的依赖关系清晰。

总结

使用 Mixin 时需要注意以下问题:

  1. 命名冲突和顺序问题。
  2. Mixin 的依赖性和适用范围。
  3. 避免滥用 Mixin,导致代码复杂度增加。
  4. Mixin 不能有构造函数,状态初始化需要额外处理。

通过合理设计 Mixin 并遵循最佳实践,可以充分发挥其优势,同时避免潜在问题。

在 Flutter 中,HitTestBehavior 是一个枚举类型,用于控制 手势检测 时 Widget 的命中测试行为。它通常与 IgnorePointer 或 AbsorbPointer 等 Widget 一起使用,来决定如何处理用户的触摸事件。

HitTestBehavior 有三个枚举值,分别是:

HitTestBehavior.deferToChild

HitTestBehavior.opaque
HitTestBehavior.translucent

以下是对每个枚举值的详细解释:

  1. HitTestBehavior.deferToChild

含义:将命中测试的决定权交给子 Widget。
行为:
如果子 Widget 通过了命中测试(即子 Widget 在触摸区域内),则父 Widget 也会通过命中测试。
如果子 Widget 没有通过命中测试,则父 Widget 也不会通过命中测试。
使用场景:
当希望父 Widget 的命中测试结果完全依赖于子 Widget 时使用。
例如,IgnorePointer 的默认行为就是 deferToChild。
示例

IgnorePointer(
child: Container(

width: 100,
height: 100,
color: Colors.blue,
child: Center(child: Text('Click Me')),

),
)
在这个例子中,如果用户点击了 Container,IgnorePointer 会根据子 Widget 的命中测试结果来决定是否忽略事件。

  1. HitTestBehavior.opaque

含义:父 Widget 总是通过命中测试,无论子 Widget 是否通过。
行为:
即使子 Widget 不在触摸区域内,父 Widget 也会通过命中测试。
父 Widget 会拦截所有触摸事件,子 Widget 无法接收到事件。
使用场景:
当希望父 Widget 完全拦截触摸事件时使用。
例如,AbsorbPointer 的默认行为就是 opaque。
示例

AbsorbPointer(
child: Container(

width: 100,
height: 100,
color: Colors.red,
child: Center(child: Text('Click Me')),

),
)
在这个例子中,无论用户点击哪里,AbsorbPointer 都会拦截事件,子 Widget 不会接收到任何触摸事件。

  1. HitTestBehavior.translucent

含义:父 Widget 总是通过命中测试,但子 Widget 也可以接收到事件。
行为:
父 Widget 和子 Widget 都会通过命中测试。
父 Widget 不会拦截事件,事件会同时传递给父 Widget 和子 Widget。
使用场景:
当希望父 Widget 和子 Widget 都能接收到触摸事件时使用。
例如,某些自定义手势检测场景。
示例

Listener(
behavior: HitTestBehavior.translucent,
onPointerDown: (event) {

print('Parent Widget received event');

},
child: Container(

width: 100,
height: 100,
color: Colors.green,
child: Center(
  child: Listener(
    onPointerDown: (event) {
      print('Child Widget received event');
    },
    child: Text('Click Me'),
  ),
),

),
)
在这个例子中,当用户点击 Container 时,父 Widget 和子 Widget 都会接收到事件,并分别打印日志。

总结对比

枚举值 含义 行为 使用场景
HitTestBehavior.deferToChild 将命中测试的决定权交给子 Widget。 子 Widget 通过命中测试,父 Widget 才通过;否则父 Widget 不通过。 父 Widget 的命中测试结果依赖于子 Widget。
HitTestBehavior.opaque 父 Widget 总是通过命中测试,拦截所有事件。 父 Widget 拦截所有事件,子 Widget 无法接收到事件。 父 Widget 需要完全拦截触摸事件。
HitTestBehavior.translucent 父 Widget 总是通过命中测试,但子 Widget 也可以接收到事件。 父 Widget 和子 Widget 都会接收到事件。 父 Widget 和子 Widget 都需要处理触摸事件。
实际应用场景

HitTestBehavior.deferToChild:

用于 IgnorePointer,当希望根据子 Widget 的命中测试结果来决定是否忽略事件时。
HitTestBehavior.opaque:

用于 AbsorbPointer,当希望完全拦截触摸事件时。
HitTestBehavior.translucent:

用于自定义手势检测,当希望父 Widget 和子 Widget 都能接收到事件时。
通过理解 HitTestBehavior 的枚举值及其行为,可以更好地控制 Flutter 中 Widget 的触摸事件处理逻辑。

LRU算法

LRULeast Recently Used 的缩写,即最近最少使用,是系统中常用的一种页面置换算法,其算法思想就是:如果一个页面最近最少使用,那么在将来它被使用的概率就很低。在 App 中,也时常将 LRU 引用为缓存的一种淘汰策略,接下来我们将使用 Swift 来实现一个基于 LRU 的缓存机制。

leetcode题目

运用所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制 。
实现 LRUCache 类:

  • LRUCache(int capacity) 以正整数作为容量 capacity 初始化 LRU 缓存
  • int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。
  • void put(int key, int value) 如果关键字已经存在,则变更其数据值;如果关键字不存在,则插入该组「关键字-值」。当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间。
  • 要求:get 和 put 操作都要在 O(1)的时间复杂度内完成。

来源:力扣(LeetCode)

分析需求

  1. 要满足 O(1) 时间复杂度,能满足的数据结构就是哈希表。
  2. 最久最未使用,那么数据就应该是有顺序的,数据结构中能满足的是数组和链表。
  3. 数组在分配内存时是连续的,在 Swift 中,扩展一个已分配空间的数组,需要重新分配更大的空间,然后将内容复制到新空间中,这种开销比较大。
  4. 链表的存储是分散的,单向链表要在某个位置上插入或删除一个节点的时间复杂度为 O(n),因为需要先遍历到该位置,而双向链表则不需要遍历,比较灵活。

综上,这个缓存应该是哈希表 + 双向链表的数据结构。

代码实现

缓存的主体是哈希表,双向链表是作为一个顺序的维护。
那么我们可以把链表的节点存储到哈希表中,节点中保存 key和 value ,再通过节点的前趋和后继指针维持双向链表结构。

链表的节点

由于 Swift 的 struct 不允许自身的嵌套,即以下的写法是错误的:

struct Node {
    var prev:Node
}

因此选用 class 来定义链表的节点:

//链表节点
class Node {
    var key,value :Int
    //前趋节点、后继节点
    var prev, next : Node?

    init(_ key:Int,_ value:Int) {
        self.key = key
        self.value = value
    }
    deinit {
        print("Node deinit :",key,"-",value)
    }
}

双向链表的实现

根据单一职责原则,我们将链表的实现和操作单独封装在 DuLinkList 类中,为方便调试,增加 printList() 函数来打印观察。

//双向链表实现
class DuLinkList {
    //首节点、尾节点
    private var header,tail : Node
    
    init() {
        header = Node.init(0, 0)
        tail = Node.init(0, 0)
        header.next = tail
        tail.prev = header
    }
    ///添加到链表首节点
    func addToFirst(_ node:Node){
        node.next = header.next
        header.next?.prev = node
        
        node.prev = header
        header.next = node
    }
    ///删除指定节点
    func removeNode(_ node:Node){
        //首尾节点不删除
        if node.prev == nil || node.next == nil {
            return
        }
        node.prev?.next = node.next
        node.next?.prev = node.prev
        node.prev = nil
        node.next = nil
    }
    ///删除最后一个节点
    func removeLast() -> Node {
        let note = tail.prev!
        removeNode(note)
        return note
    }
    ///打印链表
    func printList(){
        var next = header.next
        while let node = next, node.next != nil {
            print("list note : ",node.key,"-",node.value)
            next = node.next
        }
    }
    ///销毁链表
    private func destroyList(){
        var node:Node? = header
        repeat{
            let next = node?.next
            node?.prev = nil
            node?.next = nil
            node = next
        }while( node != nil)
    }
    
    deinit {
        print("DuLinkList deinit")
        destroyList()
    }
}

值的注意的是:双向链表的节点之间是相互引用的,构成了循环引用,会导致链表无法释放,因此增加 destroyList() 函数来销毁链表,并在 DuLinkList 类中的 deinit函数中调用。

LRUCache 的实现

/// 通过构建哈希表和双向链表来实现LRU算法
 /// 其中,哈希表的 value 是 链表的的节点
class LRUCache {
    //存储容量
    var capacity = 5
    
    //哈希表
    private var cacheTable = Dictionary<Int,Node>()
    //双向链表
    private let linkList = DuLinkList()
    
    init(_ capacity:Int) {
        self.capacity = capacity
    }
    
    /// 将 value 存入哈希表中
    /// 如果表中未存在,则生成节点,并存入表中,并将节点移到链表的首节点
    /// 如果表中已存在,则将更新 value ,并将节点移到链表的首节点
    /// 如果超出存储容量,则删除链表末尾的节点,再存入,并将节点移到链表的首节点
    func put(_ key:Int,_ value:Int){
        if let node = cacheTable[key] {
            node.value = value
            cacheTable[key] = node
            //删除node节点,重新添加到链表首节点
            linkList.removeNode(node)
            linkList.addToFirst(node)
        }else{
            //创建节点
            let note = Node.init(key, value)
            
            if cacheTable.count >= capacity {
                //超出容量,则删除最后一个节点、清除哈希表的 value
                let temp = linkList.removeLast()
                cacheTable[temp.key] = nil
            }
            cacheTable[key] = note
            linkList.addToFirst(note)
        }
    }
    
    /// 从哈希表中取出 value
    /// 如果哈希表中存在value,则将对应的节点放到链表首节点,反之则返回 -1
    func get(_ key:Int) -> Int? {
        guard let node = cacheTable[key] else {
            return -1
        }
        //删除node节点,重新添加到链表首节点
        linkList.removeNode(node)
        linkList.addToFirst(node)

        return node.value
    }
    
    func printTable() {
        print("cache : ",cacheTable)
        linkList.printList()
        print("---------------------")
    }
    
    deinit {
        print("LRUCache deinit")
    }
}

测试

let cache = LRUCache.init(3)
cache.put(1, 1)
cache.printTable()
cache.put(2, 2)
cache.printTable()
cache.put(3, 3)
cache.printTable()
cache.put(4, 4)
cache.printTable()
cache.put(5, 5)
cache.printTable()
print("cache get :",cache.get(3))
cache.printTable()
cache.put(4, 9)
cache.printTable()

测试结果:

cache :  [1: SwiftLang.Node]
list note :  1 - 1
---------------------
cache :  [2: SwiftLang.Node, 1: SwiftLang.Node]
list note :  2 - 2
list note :  1 - 1
---------------------
cache :  [2: SwiftLang.Node, 1: SwiftLang.Node, 3: SwiftLang.Node]
list note :  3 - 3
list note :  2 - 2
list note :  1 - 1
---------------------
Node deinit : 1 - 1
cache :  [2: SwiftLang.Node, 4: SwiftLang.Node, 3: SwiftLang.Node]
list note :  4 - 4
list note :  3 - 3
list note :  2 - 2
---------------------
Node deinit : 2 - 2
cache :  [4: SwiftLang.Node, 5: SwiftLang.Node, 3: SwiftLang.Node]
list note :  5 - 5
list note :  4 - 4
list note :  3 - 3
---------------------
cache get : Optional(3)
cache :  [4: SwiftLang.Node, 5: SwiftLang.Node, 3: SwiftLang.Node]
list note :  3 - 3
list note :  5 - 5
list note :  4 - 4
---------------------
cache :  [4: SwiftLang.Node, 5: SwiftLang.Node, 3: SwiftLang.Node]
list note :  4 - 9
list note :  3 - 3
list note :  5 - 5
---------------------
LRUCache deinit
DuLinkList deinit
Node deinit : 4 - 9
Node deinit : 3 - 3
Node deinit : 5 - 5
Node deinit : 0 - 0
Node deinit : 0 - 0

本文主要讲述以下几个问题:

  1. 什么是block
  2. block有多少种类型
  3. block如何捕获外部变量
  4. 循环引用如何解决
  5. block copy 的不同结果
  6. __block修饰的变量在底层是什么样的

一、 block是什么

查看c++代码

在 xcode 中生成两个文件:TestBlock.h 、TestBlock.m 。其中 TestBlock.m 添加如下代码:

-(void)blockFunc {
    int n = 10;
    int (^myBlock)(int m) = ^(int m){
        return n + m ;
    };
    NSLog(@"%d",myBlock(1));
}

然后通过 clang -rewrite-objc TestBlock.m 命令将 TestBlock.m 转换出 TestBlock.cpp 文件,该文件中是底层如何实现 block 的c++代码。
我们看到,blockFunc 方法在底层代码中变成了 _I_TestBlock_blockFunc 函数:

static void _I_TestBlock_blockFunc(TestBlock * self, SEL _cmd) {
    int n = 10;
    //定义 myBlock
    int (*myBlock)(int m) = ((int (*)(int))&__TestBlock__blockFunc_block_impl_0((void *)__TestBlock__blockFunc_block_func_0, &__TestBlock__blockFunc_block_desc_0_DATA, n));
    //调用 myBlock
    ((int (*)(__block_impl *, int))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock, 1);
}

再看 __TestBlock__blockFunc_block_impl_0 的定义:

struct __TestBlock__blockFunc_block_impl_0 {
  struct __block_impl impl;  //block 主体
  struct __TestBlock__blockFunc_block_desc_0* Desc;  //block描述
  int n;  //block 捕获的外部变量 n
  //构造函数
  __TestBlock__blockFunc_block_impl_0(void *fp, struct __TestBlock__blockFunc_block_desc_0 *desc, int _n, int flags=0) : n(_n) {
    impl.isa = &_NSConcreteStackBlock;   //初始化为栈 block
    impl.Flags = flags;
    impl.FuncPtr = fp;  //block实现的函数地址
    Desc = desc;
  }
};

block的实现部分被封装在 __TestBlock__blockFunc_block_func_0 函数中,通过 __TestBlock__blockFunc_block_impl_0 构造函数传递到 __block_impl 结构体中:

static int __TestBlock__blockFunc_block_func_0(struct __TestBlock__blockFunc_block_impl_0 *__cself, int m) {
    int n = __cself->n; // bound by copy

    return n + m ;
}
struct __block_impl {
  void *isa;  //isa指针表示block的类型: _NSConcreteStackBlock、_NSConcreteGlobalBlock、_NSConcreteMallocBlock
  int Flags;
  int Reserved;
  void *FuncPtr; //函数指针,__TestBlock__blockFunc_block_func_0 函数
};

在上述代码中,我们发现,myBlock 实质上是 __TestBlock__blockFunc_block_impl_0 结构体的对象。

总结

block 是在底层是一个结构体对象,它封装了 block 的实现代码和捕获的外部变量。

二、block的三种类型

全局block

NSGlobalBlock 类型,位于全局区。
当在 block 内部不使用外部变量,或只是用静态变量和全局变量时,则是全局 block。
示例代码:

static int b = 100;
void (^Block)(void) = ^{
   NSLog(@"== %d",b);
};
NSLog(@"%@",Block);
输出结果为:<__NSGlobalBlock__: 0x10eaca050>

栈block

NSStackBlock 类型,位于栈区。
当使用 __weak 修饰 block 时,被弱引用的 block 没有被 copy 到堆上,则为栈区block。
示例代码:

NSString *str = @"haha";
void (^__weak Block)(void) = ^{
   NSLog(@"-- %@",str);
};
NSLog(@"%@",Block);
输出结果为:<__NSStackBlock__: 0x7ffee1136168>

堆block

NSMallocBlock 类型,位于堆区。
当 block 内部引用了外部变量,且block被强引用时,则为堆区block
示例代码:

NSString *str = @"haha";
void (^Block)(void) = ^{
   NSLog(@"-- %@",str);
};
NSLog(@"%@",Block);
输出结果为:<__NSMallocBlock__: 0x60000227ef10>

理解不同类型的 block

示例代码:

-(void)testBlock{
    NSString *str = @"hello";  //对象类型的 str 局部变量
    int num = 10;  //值类型的 num 局部变量
    void (^__weak weakBlock)(void) ;  //声明 weakBlock
    //代码块
    {
        void (^__weak myBlock)(void) = ^{
            NSLog(@"num : %d",num);
            NSLog(@"str : %@",str);
        };
        NSLog(@"myBlock : %@",myBlock);
        weakBlock = myBlock;
    }
    NSLog(@"weakBlock : %@",weakBlock);
    weakBlock();
}

输出结果:

myBlock : <__NSStackBlock__: 0x7ffeee7b2170>
weakBlock : <__NSStackBlock__: 0x7ffeee7b2170>
num : 10
str : (null)

通过代码与结果分析,我们发现 myBlock 和 weakBlock 都是栈block,并且都指向同一块内存空间 0x7ffeee7b2170 ,而这块内存的生命周期是在 testBlock 函数的作用域之内,即使 myBlock 出了代码块范围,也依然存在于该函数的栈空间中,因此调用 weakBlock() 依然有效。

示例代码:

-(void)testBlock{
    int num = 10;  //值类型的 num 局部变量
    void (^__weak weakBlock)(void) ;  //声明 weakBlock
    //代码块
    {
        void (^myBlock)(void) = ^{
            NSLog(@"num : %d",num);
        };
        NSLog(@"myBlock : %@",myBlock);
        weakBlock = myBlock;
    }
    NSLog(@"weakBlock : %@",weakBlock);
    weakBlock();
}

输出结果:

myBlock : <__NSMallocBlock__: 0x6000034e55f0>
weakBlock : (null)

程序崩溃:EXC_BAD_ACCESS (code=1, address=0x10)
myBlock 去掉__weak修饰后,变成堆 block,而它的生命周期只在代码块中,出了代码块就被释放掉了,因此再调用weakBlock() 程序崩溃。

示例代码:

-(void)testBlock{
    void (^__weak weakBlock)(void) ;  //声明 weakBlock
    //代码块
    {
        void (^myBlock)(void) = ^{
            NSLog(@"hello~");
        };
        NSLog(@"myBlock : %@",myBlock);
        weakBlock = myBlock;
    }
    NSLog(@"weakBlock : %@",weakBlock);
    weakBlock();
}

输出结果:

myBlock : <__NSGlobalBlock__: 0x10e4790d0>
weakBlock : <__NSGlobalBlock__: 0x10e4790d0>
hello~

不使用外部变量后,myBlock 变成全局 block,在内存中类似于单例,一直存在直到程序终止,因此调用 weakBlock()有效。

三、捕获外部变量

在实践中发现,block 对不同类型和不同作用域的外部变量捕获方式是不尽相同的,总结起来如下列表格:

变量类型是否捕获捕获内容
局部变量值类型:捕获值 ; 对象类型: 捕获变量地址和修饰符
静态局部变量捕获变量地址
全局变量/静态全局变量不捕获,直接访问

四、解决 block 循环引用的几个方法

循环引用的问题在 iOS 经常遇到:A对象引用了B对象,B对象中又引用了A对象,导致两个对象都无法释放。
循环引用经常发生在 block 中,下面我们来看个例子:

@interface TestObject()

@property(nonatomic,strong) void(^myBlock)(void);

@end

@implementation TestObject

-(void)blockFunc{
    self.myBlock = ^{
        NSLog(@"self : %@",self);
    };
    self.myBlock();
}

在上述代码中,self 持有了 myBlock ,而在 myBlock 中又捕获了 self,因而构成循环引用,下面我们再来看看解决办法。

1、__weak__strong

常用手法,不再赘述。

2、__block

使用 __block 将 block 中的 self 置为nil,从而使得 block 不再持有 self 。

-(void)blockFunc{
    __block TestObject *temp = self;
    self.myBlock = ^{
        NSLog(@"self:%@",temp);
        temp = nil;
    };
    self.myBlock();
}

3、将强引用对象写成 block 的入参

block 的入参不需要被捕获,因此通过参数传入可以使得 block 不对 self 进行持有。

//修改 myBlock 定义,使其可以传入参数
@property(nonatomic,strong) void(^myBlock)(TestObject *obj);
-(void)blockFunc{
    self.myBlock = ^(TestObject *obj){
        NSLog(@"self:%@",obj);
    };
    self.myBlock(self);
}

五、block copy

copy操作对于不同类型 block 的结果是不一样的,总结起来看表格:

block类型copy结果
NSGlobalBlockcopy无效,结果还是同一个block,不会增加引用计数
NSStackBlockcopy 之后变成 NSMallocBlock,得到新内存,类似深拷贝
NSMallocBlockcopy之后还是 NSMallocBlock,但是同一块内存,会增加引用计数,类似浅拷贝

在 MRC 模式下,以下这段代码将生成一个 NSStackBlock,然后通过 copy 变成 NSMallocBlock

int n = 0;
void (^block)(void) = ^{
    NSLog(@"n = %d",n);
};
NSLog(@"b : %@",block);
block = [block copy];
NSLog(@"bb : %@",block);  

在 MRC 模式下输出为:

b : <__NSStackBlock__: 0x7ffee701c158>
bb : <__NSMallocBlock__: 0x60000178b000>

在 ARC模式下输出为:

b : <__NSMallocBlock__: 0x600003854420>
bb : <__NSMallocBlock__: 0x600003854420>

通过上面的比较,我们发现,block 创建时是 NSStackBlock ,而在 ARC 模式下,block创建时就执行了copy操作变成了 NSMallocBlock,并且 NSMallocBlock copy 之后还是同一块内存。

六、__block修饰的变量

__block修饰的变量在底层是一个结构体,我们来看一下代码:

__block int number = 100;
    void (^myBlock)(void) = ^{
        number = 101;
    };
    number = 2;

通过 clang rewrite 命令转换为 c++代码:

// __block number 在底层是一个名为 __Block_byref_number_0 的结构体
__attribute__((__blocks__(byref))) __Block_byref_number_0 number = {(void*)0,(__Block_byref_number_0 *)&number, 0, sizeof(__Block_byref_number_0), 100};
// myBlock 定义
    void (*myBlock)(void) = ((void (*)())&__TestBlock__blockFunc_block_impl_0((void *)__TestBlock__blockFunc_block_func_0, &__TestBlock__blockFunc_block_desc_0_DATA, (__Block_byref_number_0 *)&number, 570425344));

//block外部修改number的值为 2
    (number.__forwarding->number) = 2;  

__Block_byref_number_0 结构体:

struct __Block_byref_number_0 {
  void *__isa;
__Block_byref_number_0 *__forwarding;
 int __flags;
 int __size;
 int number;
};

从上述代码中,我们可以看到,在block内部和外部,对 number 的赋值都是通过对 number.__forwarding->number 赋值完成的,那么我们来了解一下其缘由。

__forwarding 指针

还是使用上面 __block int number = 100; 的例子,__forwarding指针是 __Block_byref_number_0 结构体的成员,在栈block中,它指向了__Block_byref_number_0 结构体自身(如图1),当栈block 被 copy 变成堆block时,栈区block中的 __forwarding指针指向了堆区block 的__Block_byref_number_0 结构体,而堆区block 的__forwarding指针还是指向结构体自身(如图2)。
图1:
图1
图2:
图2

通过上述代码,我们看到对 number 复制或访问都是通过__forwarding 指针完成的,这样做的好处是无论是栈block还是堆block,通过__forwarding 指针,都能够正确的访问到 __block 修饰的变量。

总结

1、当没有应用外部变量时是全局Block。
2、当 __weak 修饰Block 时是栈区Block
3、其他情况属于堆区Block
4、Block的本质是一个封装了实现和应用外部变量的结构体
5、__block 修饰的变量会被封装成一个结构体,并且Block结构体中有一个指针始终指向它,所以可以修改变量。