扣丁书屋

iOS下的闭包下篇-Closure

iOS下的闭包下篇-Closure

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

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

目录表

  • Closure是什么

  • Closure有什么用

  • 使用场景

  • 原理

  • 生成SIL文件

  • 闭包捕获列表

  • 闭包捕获上下文

  • 如何存储捕获值

  • 注意事项

  • 参考文献

  • 结束语

Closure是什么

ClosureSwift语言下的闭包的实现,就像The Swift Programming Language 5.5 Edition(链接附文后)中提到的一样,「Closure是独立的功能块,可以在你的代码中传递和使用」??梢哉饷蠢斫?code>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'

捕获的总结

  • 捕获的上下文存储在堆空间,也就是引用类型的基础
  • 闭包的类型由() -> Int 转化为(@guaranteed { var Int }) -> Int,引用类型的runningTotal被当作参数传递进来,从而实现了闭包捕获上下文中变量的过程

如何存储捕获值

如果想了解存储的原理,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())

关于选关键词的说明

  • 使用weak那么,在以后的代码中使用self的时候需要加上self?书写,一方面可读性,另一方面美观都会降低
  • 效率问题,[weak self]会添加self的弱引用计数,而弱引用计数需要开辟一个新的空间存SideTable,SideTable中会存放弱引用计数及其它引用计数,而开辟空间操作相对于常规操作来说,性能消耗相比unowned是多的
  • Apple官方的选择说明,如果修饰的实例声明周期短那就选择weak,反之选择unowned,其实也就是效率的高级体现

关于为什么是self?

  • 如果执行环境是多线程,那么不确定哪个线程会进行调用,调用几次,代码块内完成之后self就会被释放,这个时候需要进行安全判断,所以在Objc下是强化或者非法提前退出进行处理,而在Swift下则是可选值的使用

参考文献

  • The Swift Programming Language 5.5 Edition:https://docs.swift.org/swift-book/
  • SIL Doc:https://github.com/apple/swift/blob/main/docs/SIL.rst#abstract
  • Swift开源代码:https://github.com/apple/swift/blob/d5e5253cee0599a5362363d6ba0fe640493ea0e6/lib/IRGen/IRGenModule.cpp#L255

结束语

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


https://mp.weixin.qq.com/s/97Ij2N545ydx6WBNAwncOA

最多阅读

iOS 性能检测新方式?——AnimationHitches 1年以前  |  22337次阅读
快速配置 Sign In with Apple 3年以前  |  6005次阅读
APP适配iOS11 3年以前  |  4860次阅读
App Store 审核指南[2017年最新版本] 3年以前  |  4686次阅读
所有iPhone设备尺寸汇总 3年以前  |  4591次阅读
使用 GPUImage 实现一个简单相机 3年以前  |  4283次阅读
开篇 关于iOS越狱开发 3年以前  |  4078次阅读
在越狱的iPhone设置上使用lldb调试 3年以前  |  3981次阅读
给数组NSMutableArray排序 3年以前  |  3904次阅读
使用ssh访问越狱iPhone的两种方式 3年以前  |  3591次阅读
UITableViewCell高亮效果实现 3年以前  |  3588次阅读
关于Xcode不能打印崩溃日志 3年以前  |  3517次阅读
iOS虚拟定位原理与预防 11月以前  |  3402次阅读
使用ssh 访问越狱iPhone的两种方式 3年以前  |  3340次阅读

手机扫码阅读
18禁止午夜福利体验区,人与动人物xxxx毛片人与狍,色男人窝网站聚色窝
<蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <文本链> <文本链> <文本链> <文本链> <文本链> <文本链>