跳转至

方法

没有用到的变量名,不需要定义变量名,使用_代替

// MARK: 同名方法
func addTwoInts(_ a: Int, _ b: Int) -> Int {
  return a + b
}

func addTwoInts(_ a: Double, _ b: Double) -> Double {//同名函数,参数和返回值不同
  return a + b
}

let a: (Double, Double) -> Double = addTwoInts//a变量名后面需要加函数类型
print(a(10, 20))
let b = a
print(b(20 ,30))

异变方法 mutating

class 和 struct 都能定义方法。区别是:默认情况下值类型属性不能被自身的实例方法修改

通过 SIL 来对比一下,不添加 mutating 访问和添加 mutating 两者有什么本质的区别

struct Point {
    var x = 0.0, y = 0.0

        func test(){
            let tmp = self.x
    }

      //需要在func前面加mutating
    func moveBy(x deltaX: Double, y deltaY:Double) { 
        //会报错,修改x、y相当于修改self,self是结构体,如果是class就不需要加mutating
        x += deltaX
        y += deltaY
    }
}

//test
func testMutating() {
    var p = Point()
      //相当于p.moveBy(x: 20.0, y: 30.0, &p),把p地址传进去,修改x、y对外部p有影响
    p.moveBy(x: 20.0, y: 30.0)
}
//没加mutating Point接收的是结构体实例 self值
sil hidden [ossa] @$s4main5PointV4testyyF : $@convention(method) (Point) -> 

debug_value %0 : $Point, let, name "self", argno 1 // id: %1 

//加了mutating的方法 多了一个inout 接收的是一个地址
sil hidden [ossa] @$s4main5PointV6moveBy1x1yySd_SdtF : $@convention(method) (Double, Double, self : @inout Point)

@inout Point 

debug_value_addr %2 : $*Point, var, name "self", argno 3 // id: %5 

//方法是否加mutating,两者区别:
1. 添加inout
2. 取地址和取值
let self = Point //常量 不可修改 取的是值
var self = &Point //可变 &取的是地址

SIL 文档的解释

An @inout parameter is indirect. The address must be of an initialized object.

@inout参数是间接的,地址必须是初始化对象的地址。

异变方法的本质

对于变异方法, 传入的 self 被标记为 inout 参数。取地址赋值,无论在mutating 方法内部发生什么,都会影响外部依赖类型的一切。 相当于self地址传入函数内部。

输入输出参数:如果想函数能够修改一个形式参数的值,而且希望这些改变在函数结束之后依然生效,那么就需要将形式参数定义为输入输出形式参数 。在形式参数定义开始的时候在前边添加一个inout关键字可以定义一个输入输出形式参数

var age = 10

//函数的形式参数都是let类型
func modifyage(_ age: Int) {
    age += 1//报错
}

要想修改形式参数

var age = 10

func modifyage(_ age: inout Int) {
    age += 1
}

modifyage(&age) //传地址
print(age)

例:修改参数字典

dic 参数前添加了 inout 关键字,这表示这个字典是可以在函数内部被修改的,并且这些修改会在函数调用结束后反映到原始字典上。

func exchangeValue(forIndexPathA indexPathA: IndexPath, andIndexPathB indexPathB: IndexPath, withIdentifier identifier: String, dictionary dic: inout [String: Double]) {
    // 根据 identifier 和 indexPathA 生成 keyA
    let keyA = makeKey(withIdentifier: identifier, indexPath: indexPathA)
    // 根据 identifier 和 indexPathB 生成 keyB
    let keyB = makeKey(withIdentifier: identifier, indexPath: indexPathB)
    // 临时保存 keyA 对应的值
    let temp = dic[keyA]
    // 将 keyB 对应的值赋给 keyA
    dic[keyA] = dic[keyB]
    // 将临时保存的 keyA 原始值赋给 keyB
    dic[keyB] = temp
}

调用这个函数时,需要使用 & 符号来传递一个可变字典的引用,例如:

var myDictionary: [String: Double] = ...
exchangeValue(forIndexPathA: indexPathA, andIndexPathB: indexPathB, withIdentifier: identifier, dictionary: &myDictionary)

这样,myDictionary 中的值就会根据函数内部的逻辑被交换。

方法调度(派发方式)

1、直接派发 (Direct Dispatch)

直接派发是最快的。因为需要调用的指令集更少,并且编译器还能够有很大的优化空间(例如函数内联等)。直接派发也称静态调用。

对于编程来说直接调用局限也是最大的, 而且因为缺乏动态性所以没办法支持继承和多态。

2、函数表派发 (Table Dispatch )

函数表派发是编译型语言实现动态行为最常见的实现方式。

函数表使用了一个数组来存储 类 声明的每一个函数的指针,大部分语言把这个称为 “virtual table”(虚函数表),Swift 里称为 “witness table”,每一个类都会维护一个函数表,里面记录着类所有的函数。

如果父类函数被override的话,表里面只会保存被override之后的函数。

子类新添加的函数都会被插入到这个数组的最后,运行时会根据这一个表去决定实际要被调用的函数.

优缺点:

查表是一种简单、 易实现、 而且性能可预知的方式。然而,这种派发方式比起直接派发还是慢一点。 从字节码角度来看,多了两次读和一次跳转,由此带来了性能的损耗。另一个慢的原因在于编译器可能会由于函数内执行的任务导致无法优化 (如果函数带有副作用的话)。

这种基于数组的实现,缺陷在于函数表无法拓展。子类会在虚数函数表的最后插入新的函数,没有位置可以让extension 安全地插入函数。

3、消息机制派发 (Message Dispatch )

消息机制是调用函数最动态的方式. 也是 Cocoa 的基石, 这样的机制催生了 KVO, UIAppearence 和 CoreData等功能。这种运作方式的关键在于开发者可以在运行时改变函数的行为. 不止可以通过 swizzling 来改变, 甚至可以用 isa-swizzling 修改对象的继承关系, 可以在面向对象的基础上实现自定义派发。

OC:objc_msgsend

Swift 中的方法调度

class LGTeacher{
    func teach(){
        print("teach")
    }
}

let t = LGTeacher()
t.teach()

通过汇编查看,一般bl blr是函数调用

teach函数的调用过程:

  1. 找到 Metadata
  2. 确定函数地址(metadata + 偏移量)
  3. 执行函数

函数基于函数表V-Table的调度。

方法调度方式总结:

  • 结构体(值类型)没有继承关系,总是静态派发。编译之后静态指针,编译链接之后函数内存地址已经确定,不需要添加到表中记录。
  • 协议里声明的, 并且带有默认实现的函数会使用函数表进行派发
  • 协议和类的 extension 都会使用直接派发
  • NSObject 的 extension 会使用消息机制进行派发
  • NSObject 声明作用域里的函数都会使用函数表进行派发
class Teacher{

    //final 子类不能重写,函数是地址调用,不在vtable中。
    final func teach(){
        print("teach")
    }

    //dynamic 赋予函数动态性,但调用方式还是vtable
    dynamic func teach1(){
        print("teach1")
    }
        //一般@objc和dynamic配合使用,变成了消息调度机制objc_msgsend,可以method_swizzling 使用runtime的API
    //    @objc dynamic func teach1(){
    //        print("teach1")
    //    }

    func teach2(){
        print("teach2")
    }
}

//teach3不在函数表vtable中
extension Teacher{
    @_dynamicReplacement(for: teach1)
    //teach3来替代teach1函数。编译器把teach1指向的imp改为了teach3
    func teach3(){
        print("teach3")
    }
}

//在子类PartTeacher的vtable中
class PartTeacher: Teacher {
    override func teach2() {
        print("Part Teach2")
    }
    func teach4(){
        print("teach4")
    }
}   


//test
func testClassFunc() {
    let cs = TeacherCS()
    cs.teach1()//teach3
    cs.teach3()//teach3

    let pt = PartTeacher()
    pt.teach1()//Part Teach1
}
类型原始定义extension扩展
值类型struct 静态派发(直接派发) 静态派发(没有合并到函数表)
协议 函数表派发 静态派发(直接派发)
类class 函数表派发 静态派发(直接派发)
继承自NSObject的类 函数表派发 消息机制派发

影响函数派发方式

1、final 直接派发

添加了 final 关键字的函数自己独有的,子类不能重写,使用静态派发,不会在 vtable 中出现,且对 objc 运行时不可见。

实际开发过程中属性,方法,类不需要被重载的时候,使用final关键字。

final @objc

可以在标记为 final 的同时, 也使用 @objc 来让函数可以使用消息机制派发. 这么做的结果就是, 调用函数的时候会使用直接派发, 但也会在 Objective-C 的运行时里注册相应的 selector. 函数可以响应 perform(selector:)以及别的 Objective-C 特性, 但在直接调用时又可以有直接派发的性能.

2、dynamic 消息机制派发

函数均可添加 dynamic 关键字,为非objc类和值类型的函数赋予动态性,函数表派发

3、@objc

该关键字可以将Swift函数暴露给Objc运行时,函数表派发

4、@objc + dynamic 消息机制派发

消息派发的方式。变成了消息调度的机制

5、@inline

告诉编译器可以直接派发

还原方法

// MARK: 还原方法
struct TargetFunctionTypeMetadata{
    var kind: Int
    var flags: Int
    var arguments: ArgumentsBuffer<Any.Type>//连续的内存数组空间

    func numberArguments() -> Int{//函数有多少个参数
        return self.flags & 0x0000FFFF
    }
}

struct ArgumentsBuffer<Element>{
    var element: Element

    mutating func buffer(n: Int) -> UnsafeBufferPointer<Element> {
        return withUnsafePointer(to: &self) {
            let ptr = $0.withMemoryRebound(to: Element.self, capacity: 1) { start in
                return start
            }
            return UnsafeBufferPointer(start: ptr, count: n)
        }
    }

    mutating func index(of i: Int) -> UnsafeMutablePointer<Element> {
        return withUnsafePointer(to: &self) {
            return UnsafeMutablePointer(mutating: UnsafeRawPointer($0).assumingMemoryBound(to: Element.self).advanced(by: i))
        }
    }
}

func test_huanyuanFunc() {
    func addTwoInts(_ a: Double, _ b: Double) -> Double {//同名函数,参数和返回值不同
        return a + b
    }

    let value = type(of: addTwoInts)//获取类型
    //unsafeBitCast按位转换
    let functionType = unsafeBitCast(value as Any.Type, to: UnsafeMutablePointer<TargetFunctionTypeMetadata>.self)
    print(functionType.pointee.numberArguments())
}

函数内联

函数内联是一种编译器优化技术,它通过使用方法的内容替换直接调用该方法,从而优化性能。

内联函数在swift中是一种默认行为。

  • 将确保有时内联函数。这是默认行为,我们无需执行任何操作. Swift 编译器可能会自动内联函数作为优化。
  • @inline(__always):将确保始终内联函数。通过在函数前添加 @inline(__always) 来实现此行为。
  • @inline(never):将确保永远不会内联函数。这可以通过在函数前添加 @inline(never) 来实现。
  • 如果函数很长并且想避免增加代码段大小,请使用@inline(never)。
//始终内联
@inline(__always) 
func test_always(){
    print(#function)
}
//不内联
@inline(never) func test_never(){
    print(#function)
}

如果对象只在声明的文件中可见,可以用privatefileprivate进行修饰。编译器会对privatefileprivate对象进行检查,确保方法没有其他继承关系的情形下,自动打上final标记,进而使得对象获得静态派发的特性

class LGPerson{
    private var sex: Bool
    private func unpdateSex(){
        self.sex = !self.sex
    }
    init(sex innerSex: Bool) {
        self.sex = innerSex
    }
}