iOS下的闭包下篇-Closure

2126次阅读  |  发布于2年以前

iOS下的闭包下篇-Closure

题记:用最通俗的语言,描述最难懂的技术

❝最近在学习和迁移Swift方面的代码,正好看到了闭包这部分,看完之后整个人都被着魔了一样,于是便有了这篇文章,如果有哪些结论模糊或者不准确,请联系weiniu@sohu-inc.com

目录表

Closure是什么

ClosureSwift语言下的闭包的实现,就像The Swift Programming Language 5.5 Edition(链接附文后)中提到的一样,「Closure是独立的功能块,可以在你的代码中传递和使用」。可以这么理解Closure(swift) ≈ Block(c,c++,objective-c)≈ lambdas (other languages)

Block一样,Closure可以捕获和存储代码上下文中声明的常量和变量。同样Swift会处理所有捕获的值的内存。

Closure三种表现形式

Swift对闭包的优化

Closure有什么用

综上所述,它的作用已经很清楚了:可选的传递某些参数从而实现某些回调功能

语法

局部变量

// 通用格式
{ (parameters) -> return type in
 statements
}

var variableName: (parametersType) -> return type
eg:
var successClosure: ([String : Int]) -> (Void)

尾随闭包(作为方法的最后一个参数,优化过多的参数和返回值)

func someMethod(closureName: (parameters) -> return type) {...}
eg:
func urlRequest(successBlock: ([String : Int]) -> (Void)) { ... }

逃逸闭包(在方法完成之后进行调用)

func someFuncEscapingClousre(closure: @escaping (parametersType) -> return Type) { ... }

eg:
/// Use @escaping keyword to define
func loadImageCompletion(closure: @escaping () -> Void) { ... }

自动闭包(闭包不带参数,作为函数的参数,返回一个封装的数据作为结果)

func someFuncAutoclosure(closure: () -> return Type) { ... }
eg:
func haveBreakfast(for food: () -> String) { ... }

自动+逃逸

///  @autoclosure @escaping must define
func someFuncAutoEscapeclosure(closure:  @autoclosure @escaping () -> return Type) { ... }
eg:
func haveBreakfast(closure: @autoclosure @escaping () -> String) { ...  }

使用场景

❝注释:在Swift中,枚举和结构体的初始化之后应该称为实例,而类初始化之后称之为对象,根据对象的特性,继承特性是区分的关键

原理

生成SIL文件

如果你对一个问题没有任何思路,那就从相似的问题中找一些突破口,比如Objective-C下是使用clang命令把OC代码转成相对底层的C++源码,所以我们可以推断,预测有一个xxx的指令也可以把swift语言转成相对底层的语言,然后你就搜索相关关键词swift底层源码等去找答案,现在我帮你找好了,这个命令就是swiftc,目标文件就是SIL(Swift Intermediate Language),这个SIL就等价于OC中的IR具体的SIL相关知识不做讲解,请自行查阅,接下来还是准备工作

创建项目Xcode->File->New->Project

选择macOS->Command Line Tool->Next

填入Product Name和选择Language修改为Swift

执行命令,swiftc -emit-sil main.swift | xcrun swift-demangle > ./main.sil,查看更多使用swiftc -h

❝注释:xcrun swift-demangle,是把变量或者方法名混淆还原成可读的代码

特别说明下,这个文件可比那个11w的舒服太多了,我们接下来开始分析

闭包捕获列表

main.swift中文件写下测试代码,并使用swiftc命令生成main.sil中间文件

let bdNum = 3
let printNum = {
    [bdNum] in
    let _ = bdNum
}
printNum()

查看main.sil文件

可以清楚的看到闭包在main函数中被转化为@closure #1,继续定位该方法的实现

在这个定位过程中,我们发现闭包的类型由() -> () 变为 (int) -> (),猜测应该是把外部的全局变量传入进来了,为了验证猜测我们可以增加几个参数

从这个文件中我们就可以看出,编译器把(Float) -> () 转化为(Float,Int,String) -> ()类型,保存捕获列表里的值,类似函数传参一样,进行了值拷贝

let bdNum = 3
let name = "Augus"
let printNum = {
  [bdNum,name] (height: Float) in
 _ = bdNum
  _ = name
}
printNum(1.74)

闭包捕获总结

闭包捕获上下文

这个小节以The Swift Programming Language 5.5 Edition中的例子为例进行分析

func makeIncrementer() -> () -> Int {
    var runningTotal = 12
    func incrementer() -> Int {
        runningTotal += 1
        return runningTotal
    }
    return incrementer
}

main.sil文件注释

// makeIncrementer()
sil hidden @main.makeIncrementer() -> () -> Swift.Int : $@convention(thin) () -> @owned @callee_guaranteed () -> Int {
bb0:
  // 在堆上开辟一个空间,并取名为"runningTotal"
  %0 = alloc_box ${ var Int }, var, name "runningTotal" // users: %8, %7, %6, %1
  // 把该值和类型包装成project_box的类型
  %1 = project_box %0 : ${ var Int }, 0           // user: %4
  // 初始化该值为12
  %2 = integer_literal $Builtin.Int64, 12         // user: %3
  %3 = struct $Int (%2 : $Builtin.Int64)          // user: %4
  store %3 to %1 : $*Int                          // id: %4
  // function_ref incrementer #1 () in makeIncrementer()
  %5 = function_ref @incrementer #1 () -> Swift.Int in main.makeIncrementer() -> () -> Swift.Int : $@convention(thin) (@guaranteed { var Int }) -> Int // user: %7
  strong_retain %0 : ${ var Int }                 // id: %6
  // 把包装后的"runningTotal"传递给闭包
  %7 = partial_apply [callee_guaranteed] %5(%0) : $@convention(thin) (@guaranteed { var Int }) -> Int // user: %9
  strong_release %0 : ${ var Int }                // id: %8
  return %7 : $@callee_guaranteed () -> Int       // id: %9
} // end sil function 'main.makeIncrementer() -> () -> Swift.Int'

// incrementer #1 () in makeIncrementer()
sil private @incrementer #1 () -> Swift.Int in main.makeIncrementer() -> () -> Swift.Int : $@convention(thin) (@guaranteed { var Int }) -> Int {
// %0 "runningTotal"                              // user: %1
bb0(%0 : ${ var Int }):
  // 把传递过来包装后的"runningTotal"值赋值给%1
  %1 = project_box %0 : ${ var Int }, 0           // users: %16, %4, %2
  debug_value_addr %1 : $*Int, var, name "runningTotal", argno 1 // id: %2
  // 要累加的Int类型的值 1
  %3 = integer_literal $Builtin.Int64, 1          // user: %8
  %4 = begin_access [modify] [dynamic] %1 : $*Int // users: %13, %5, %15
  %5 = struct_element_addr %4 : $*Int, #Int._value // user: %6
  // 取出"runningTotal"目前的值
  %6 = load %5 : $*Builtin.Int64                  // user: %8
  %7 = integer_literal $Builtin.Int1, -1          // user: %8
  // 调用加法
  %8 = builtin "sadd_with_overflow_Int64"(%6 : $Builtin.Int64, %3 : $Builtin.Int64, %7 : $Builtin.Int1) : $(Builtin.Int64, Builtin.Int1) // users: %10, %9
  %9 = tuple_extract %8 : $(Builtin.Int64, Builtin.Int1), 0 // user: %12
  %10 = tuple_extract %8 : $(Builtin.Int64, Builtin.Int1), 1 // user: %11
  // 判断是否堆栈溢出
  cond_fail %10 : $Builtin.Int1, "arithmetic overflow" // id: %11
  // 将计算结果赋值给包装后的"runningTotal"
  %12 = struct $Int (%9 : $Builtin.Int64)         // user: %13
  store %12 to %4 : $*Int                         // id: %13
  %14 = tuple ()
  end_access %4 : $*Int                           // id: %15
  // 打开包装,进行最新值的读取
  %16 = begin_access [read] [dynamic] %1 : $*Int  // users: %17, %18
  %17 = load %16 : $*Int                          // user: %19
  end_access %16 : $*Int                          // id: %18
  // 返回最新值
  return %17 : $Int                               // id: %19
} // end sil function 'incrementer #1 () -> Swift.Int in main.makeIncrementer() -> () -> Swift.Int'

捕获的总结

如何存储捕获值

如果想了解存储的原理,SIL文件是不够的,这个时候就需要更底层的编译器的指令,还是main.swift文件,添加以下代码

// 声明一个结构体,添加一个名字为biBao,类型为closure的变量属性
struct BDTest {
    var biBao: (() -> ())
}

执行编译器的相关指令swiftc -emit-ir main.swift | xcrun swift-demangle > ./main.ll

%swift.vwtable = type { i8*, i8*, i8*, i8*, i8*, i8*, i8*, i8*, i64, i64, i32, i32 }
%swift.type_metadata_record = type { i32 }
// 1.%swift.type就是 UInt64的封装,所以%swift.type*就是一个指向UInt64整型的指针
%swift.type = type { i64 }
// 2.%swift.refcounted的构成部分,%swift.type*是%swift.type类型的指针,i64为UInt64
%swift.refcounted = type { %swift.type*, i64 }
// 3.BDTest结构体的声明,在llvm下该结构体为<{ %swift.function }>,属性biBao类型为%swift.function
%T4main6BDTestV = type <{ %swift.function }>
// 4.%swift.function的构成部分,i代表Int,后面的数字代表位数,i8=UInt8,i64=UInt64等,%swift.refcounted* 是swift.refcounted类型的指针
%swift.function = type { i8*, %swift.refcounted* }
%"main.BDTest.biBao.modify : () -> () with unmangled suffix ".Frame"" = type {}
%swift.opaque = type opaque
%swift.metadata_response = type { %swift.type*, i64 }

现在最大的疑问就是%swift.refcounted,所以我们去Swift开源代码(链接附文后)中寻找答案,其余的%swift.function%swift.type均在这个源码文件中找到答案

  RefCountedStructTy = llvm::StructType::create(getLLVMContext(), "swift.refcounted");
  RefCountedPtrTy = RefCountedStructTy->getPointerTo(/*addrspace*/ 0);
  RefCountedNull = llvm::ConstantPointerNull::get(RefCountedPtrTy);

  // A type metadata record is the structure pointed to by the canonical
  // address point of a type metadata.  This is at least one word, and
  // potentially more than that, past the start of the actual global
  // structure.
  TypeMetadataStructTy = createStructType(*this, "swift.type", {
    MetadataKindTy          // MetadataKind Kind;
  });

   FunctionPairTy = createStructType(*this, "swift.function", {
    FunctionPtrTy,
    RefCountedPtrTy,
  });

对以上的源码进行分析

我们如果分析RefCountedPtrTy会比较模糊,但是我们可以根据它的向下一层的结构swift.type的进行猜测,因为TypeMetadataStructTy其实就是MetadataKindTy的封装,而MetadataKindTy的底层就是HeapObject,一个基于Objc的结构,所以目前的闭包用底层结构表达就会类似这样

struct HeapObject {
 var Kind: UInt64
 var refcount: UInt64
}

struct FunctionPairTy {
 // UnsafeMutableRawPointer swift下表示指针的结构体,以后会单独开一篇文章介绍它,在这里你需要知道他是swift下操作内存地址的结构即可
 // 闭包代码的实现的内存地址
 var FunctionPairTy: UnsafeMutableRawPointer
 // 捕获上下文变量的指针,在堆空间,如果没有捕获,为null
 var RefCountedStructTy: UnsafeMutablePointer<HeapObject>
}

闭包捕获变量的流程,用llvm文件进行编译

/// 仍然是官方的例子
func makeIncrementer() -> (() -> Int) {
    var runningTotal = 12
    func incrementer() -> Int {
        runningTotal += 1
        return runningTotal
    }
    return incrementer
}

代码解释

define hidden swiftcc { i8*, %swift.refcounted* } @"main.makeIncrementer() -> () -> Swift.Int"() #0 {
entry:
  %runningTotal.debug = alloca %TSi*, align 8
  %0 = bitcast %TSi** %runningTotal.debug to i8*
  call void @llvm.memset.p0i8.i64(i8* align 8 %0, i8 0, i64 8, i1 false)
  // 1. %1调用了swift_allocObject向堆申请了空间,类型是%swift.refcounted* 的指针类型
  %1 = call noalias %swift.refcounted* @swift_allocObject(%swift.type* getelementptr inbounds (%swift.full_boxmetadata, %swift.full_boxmetadata* @metadata, i32 0, i32 2), i64 24, i64 7) #1
  // 2. 把%1类型强转为%2,也就是%2的类型是<{ %swift.refcounted, [8 x i8] }>*的指针类型
  %2 = bitcast %swift.refcounted* %1 to <{ %swift.refcounted, [8 x i8] }>*
  // 3. 重点是%3的结构,%3取的是结构体{ %swift.refcounted, [8 x i8] }类型 %2的第二个元素,也就是 %3是结构体{ %swift.refcounted, [8 x i8] }中 [8 x i8]的指针,分析开始的12放到了该位置
  %3 = getelementptr inbounds <{ %swift.refcounted, [8 x i8] }>, <{ %swift.refcounted, [8 x i8] }>* %2, i32 0, i32 1
  // 4. %3 类型强转为 %4
  %4 = bitcast [8 x i8]* %3 to %TSi*
  store %TSi* %4, %TSi** %runningTotal.debug, align 8
  // 5.  %._value是取的是%4结构体第一元素的指针
  %._value = getelementptr inbounds %TSi, %TSi* %4, i32 0, i32 0
  // 6. 存储UInt64类型的变量12到 %._value,
  store i64 12, i64* %._value, align 8
  // 7. 引用计数的+1操作
  %5 = call %swift.refcounted* @swift_retain(%swift.refcounted* returned %1) #1
  // 8. 引用计数的-1操作
  call void @swift_release(%swift.refcounted* %1) #1
  // 9. 包装box返回结果
  // i8*被插入了 { i8* bitcast (i64 (%swift.refcounted*)* @"partial apply forwarder for incrementer #1 () -> Swift.Int in main.makeIncrementer() -> () -> Swift.Int" to i8*), %swift.refcounted* undef }, 这么长的一段其实就是闭包的实现的内存地址
  // %swift.refcounted*则被插入了%1的地址,也就是存放12值的{ %swift.refcounted, [8 x i8] }类型的指针
  %6 = insertvalue { i8*, %swift.refcounted* } { i8* bitcast (i64 (%swift.refcounted*)* @"partial apply forwarder for incrementer #1 () -> Swift.Int in main.makeIncrementer() -> () -> Swift.Int" to i8*), %swift.refcounted* undef }, %swift.refcounted* %1, 1
  // 10. 返回包装box结果
  ret { i8*, %swift.refcounted* } %6
}

根据以上的代码注释swiftclosure的底层原理结构可以更具体一些


 struct HeapObject {
  var Kind: UInt64
  var refcount: UInt64
 }

 // 负责包装的结构体,也就是用来包装捕获需要更新的值
 struct Box {
     var refCounted: HeapObject
     // 这个捕获的值的类型根据捕获的值进行分配,此处规范操作是写泛型
     // var value: Int
      var value: <T>
 }

 struct FunctionPairTy {
  var FunctionPairTy: UnsafeMutableRawPointer
  var RefCountedStructTy: UnsafeMutablePointer<Box>
 }

验证猜测

struct FunctionPairTy {
    var FunctionPtrTy: UnsafeMutableRawPointer
    var RefCountedPtrTy: UnsafeMutablePointer<Box>
}

struct HeapObject {
    var Kind: UInt64
    var refcount: UInt64
}

struct Box {
    var refCounted: HeapObject
    var value: Int
}

func makeIncrementer() -> () -> Int {
    var runningTotal = 12
    func incrementer() -> Int {
        runningTotal += 1
        return runningTotal
    }
    return incrementer
}

// 这里需要用结构体把闭包包一层,否则会被底层的逻辑所包装
struct FuncShell {
    var fun: () -> Int
}

var fun = FuncShell(fun: makeIncrementer())

var closure = withUnsafeMutablePointer(to: &fun) {
    return UnsafeMutableRawPointer($0).assumingMemoryBound(to: FunctionPairTy.self).pointee
}

print(closure)

print("end")

print("end")处进行断点,然后进行截图中的一些验证

x/8g 内存地址:查看内存里的值

dis -s 内存地址:查看汇编

捕获值扩展,在main.swift文件中输入以下代码

func makeIncrementer() -> () -> Int {
    var runningTotal = 12
    var bd1 = 1
    let bd2 = 2
    var bd3 = "a"
    let bd4 = "b"
    func incrementer() -> Int {
        runningTotal += 1
        bd1 += bd2
        bd3 += bd4
        return runningTotal
    }
    return incrementer
}

直接运行swiftc -emit-ir main.swift | xcrun swift-demangle > ./main.ll命令,直接查看Box结构体存放的值类型

我们等价替换一下截图中的类型,%swift.refcounted可以看作是一个包装类型Box,那么从左到右依次是Box *,Box*,Int,Box*,String

编译器验证,把这段代码替换刚才的那个程序中的同名函数,然后依然是进行断点

通过上述论述不难发现,这和我们的推断是一样的,但是此处还有一个问题,就是所有的值都会被包装Box么?我们依然是通过源码进行定位,把以上的程序生成SIL文件

可以清楚的看出,凡是变量在闭包内进行更新的就会被包装,反之则不会

注意事项

引用循环,不管是Block还是Closure我们都可以把它当作对象来对待,然后它捕获的变量自然是强引用,如果外部有对该闭包也有一个强引用,那么就会造成引用循环。这也考验我们在实际开发中需要及时对引用关系进行准确的梳理,然后对一方的引用进行弱引用修饰,打破循环即可,原理都是一样的,表现方式不同

Closure下的引用循环,原理和Block下的解决思路一致,让我们看看实现方式

class Cat {

    let name: String?
    lazy var nickName: () -> String = {
        // [weak self] in
        [unowned self] in
        if let name = self.name {
            return "nick of \(name)"
        } else {
            return "none of nick"
        }

    }

    init(name: String?) {
        self.name = name
    }

    deinit {
        print("cat is deinitialized")
    }
}
// aCat strong to instance of Cat
// instance of Cat strong to () -> String
// () -> String strong to self
var aCat: Cat? = Cat(name: "Tom")
print(aCat!.nickName())

关于选关键词的说明

关于为什么是self?

参考文献

结束语

好了,关于iOS下的闭包说了很多,也有一些底层的东西需要去理解和动手,困难肯定是有的,希望一起乘风破浪,所向披靡

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8