13.OC和Swift混编

OC 和 Swift 运行时简介

Objective-C 运行时
  • 动态类型(dynamic typing)
  • 动态绑定(dynamic binding)
  • 动态装载(dynamic loading)


    01
02

派发方式

  • 直接派发 (Direct Dispatch)
  • 函数表派发 (Table Dispatch )
  • 消息机制派发 (Message Dispatch )
直接派发
  • 直接派发是最快的, 不止是因为需要调用的指令集会更少, 并且编译器还能够有很大的优化空间, 例如函数内联等, 直接派发也有人称为静态调用。
  • 然而, 对于编程来说直接调用也是最大的局限, 而且因为缺乏动态性所以没办法支持继承和多 态。
函数表派发
  • 函数表派发是编译型语言实现动态行为最常见的实现方式. 函数表使用了一个数组来存储类声明的 每一个函数的指针. 大部分语言把这个称为 “virtual table”(虚函数表), Swift 里称为 “witness table”. 每一个类都会维护一个函数表, 里面记录着类所有的函数, 如果父类函数被 override 的 话, 表里面只会保存被 override 之后的函数. 一个子类新添加的函数, 都会被插入到这个数组的最 后. 运行时会根据这一个表去决定实际要被调用的函数.


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

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

消息机制派发

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


    04

Swift 运行时

  • 纯 Swift 类的函数调用已经不再是 Objective-c 的运行时发消息,而是类似 C++ 的 vtable,在编译时就确定了 调用哪个函数,所以没法通过 runtime 获取方法、属性。

  • 而 Swift 为了兼容 Objective-C,凡是继承自 NSObjec t的类都会保留其动态性,所以我们能通过 runtime 拿 到他的方法。这里有一点说明:老版本的 Swift(如2.2)是编译期隐式的自动帮你加上了@objc,而4.0以后版 本的 Swift 编译期去掉了隐式特性,必须使用显式添加。

  • 不管是纯 Swift 类还是继承自 NSObject 的类只要在属性和方法前面添加 @objc 关键字就可以使用 runtime。


    05
  • 值类型总是会使用直接派发, 简单易懂

  • 而协议和类的 extension 都会使用直接派发

  • NSObject 的 extension 会使用消息机制进行派发

  • NSObject 声明作用域里的函数都会使用函数表进行派发.

  • 协议里声明的, 并且带有默认实现的函数会使用函数表进行派发

06

Swift 运行时-final @objc

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

桥接

09
10
11
12

相互调用

Swift 调用 OC
  • 新建1个桥接头文件,文件名格式默认为:{targetName}-Bridging-Header.h
    在{targetName}-Bridging-Header.h 文件中#import OC需要暴露给Swift的内容
  • 如果C语言暴露给Swift的函数名跟Swift中的其他函数名冲突了
    可以在Swift中使用 @_silgen_name 修改C函数名
 // C语言
int sum(int a, int b) {
return a + b; 
}
 // Swift
@_silgen_name("sum") func swift_sum(_ v1: Int32, _ v2: Int32) -> Int32 
print(swift_sum(10, 20)) // 30
print(sum(10, 20)) // 30
13
OC 调用 Swift
  • Xcode已经默认生成一个用于OC调用Swift的头文件,文件名格式是: {targetName}-Swift.h
  • Swift暴露给OC的类最终继承自NSObject
//使用@objcMembers修饰类
//代表默认所有成员都会暴露给OC(包括扩展中定义的成员)
//最终是否成功暴露,还需要考虑成员自身的访问级别
@objcMembers class Car: NSObject {
    var price: Double
    var band: String
    init(price: Double, band: String) {
        self.price = price
        self.band = band
    }
    func run() {
        print(price, band, "run")
    }
    static func run() { print("Car run") }
}
extension Car {
    func test() { print(price, band, "test") }
}
  • 可以通过@objc 重命名Swift暴露给OC的符号名(类名、属性名、函数名等)
  @objc(MJCar)
@objcMembers class Car: NSObject {
var price: Double
@objc(name)
var band: String
init(price: Double, band: String) {
self.price = price
self.band = band }
@objc(drive)
func run() { print(price, band, "run") } static func run() { print("Car run") }
}
extension Car {
    @objc(exec:v2:)
func test() { print(price, band, "test") } }
MJCar *c = [[MJCar alloc] initWithPrice:10.5 band:@"BMW"]; c.name = @"Bently";
c.price = 108.5;
[c drive]; // 108.5 Bently run
[c exec:10 v2:20]; // 108.5 Bently test [MJCar run]; // Car run
14
15

NS_SWIFT_NAME

  • 在 Objective-C 中,重新命名在swift中的名称。
NS_SWIFT_UNAVAILABLE
  • 在 Swift 中不可见,不能使用。

采坑指南

Subclass
  • 对于自定义的类而言,Objective-C 的类,不能继承自 Swift 的类,即要混编的 OC 类不能是 Swift 类的子类。反过来,需要混编的 Swift 类可以继承自 OC 的类。
  • 定义一个常量值,后面可以方便使用;如 #define TOOLBAR_HEIGHT 44;
  • 定义一个不变化的常用值,或者一个较长的对象属性;如#define SCREEN_WIDTH ([[UIScreen mainScreen] bounds].size.width);
  • 定义一个会变化的常用变量值,或者一个较长的对象属性;如:#define STATUS_BAR_HEIGHT ([UIApplication sharedApplication].statusBarFrame.size.height);
  • 定义一个带参数的宏,类似于一个函数;如#define RGB_COLOR(r,g,b) [UIColor colorWithRed:r/255.f green:g/255.f blue:b/255.f alpha:1.0]
  • 第一种的话就比较简单,可以直接使用let TOOLBAR_HEIGTH:CGFloat = 44来替换就可以了;
  • 第二种因为后面的值永远不会改变,也可以使用let来替换;可以用let SCREEN_WIDTH = UIScreen.mainScreen().bounds.size.width;
  • 第三种情况,也就是后面的值会发生改变,如状态栏高度,就不能够使用let来替换了,因为let是定义的常量,如果使用let,将 会导致不能够获取正确的值;这里可以使用函数来获取:func STATUSBAR_HEIGHT() -> CGFloat { return UIApplication.sharedApplication().statusBarFrame.size.height };使用时通过函数STATUSBAR_HEIGTH()获取状态栏高度;
  • 第四种,因为有输入参数,所以也只能使用函数来替换;如:func RGB_COLOR(r:CGFloat, g:CGFloat, b:CGFloat) -> UIColor {return UIColor(red: r, green: g, blue: b, alpha: 1.0)};

Swift 独有特性

  • Swift 中有许多 OC 没有的特性,比如,Swift 有元组、为一等公民的函数、还有特有的枚举类 型。所以,要使用的混编文件要注意 Swift 独有属性问题。

NS_REFINED_FOR_SWIFT

  • Objective-C 的 API 和 Swift 的风格相差比较大,Swift 调用 Objective-C 的API时可能由于数据类型等不 一致导致无法达到预期(比如,Objective-C 里的方法采用了C语言风格的多参数类型;或者 Objective-C 方法返回 NSNotFound,在 Swift 中期望返回 nil)。这时候就要 NS_REFINED_FOR_SWIFT了。


    16
知识点

// MARK: 类似于OC中的 #pragma mark
// MARK: - 类似于OC中的 #pragma mark -
// TODO: 用于标记未完成的任务
// FIXME: 用于标记待修复的问题

条件编译
func log<T>(_ msg: T,
            file: NSString = #file,
            line: Int = #line,
            fn: String = #function) {
    #if DEBUG
    let prefix = "\(file.lastPathComponent)_\(line)_\(fn):"
    print(prefix, msg)
    #endif
}

API可用性

@available(iOS 10, macOS 10.15, ) class Person {}
struct Student {
@available(
, unavailable, renamed: "study")
func study_() {}
func study() {}

@available(iOS, deprecated: 11)
@available(macOS, deprecated: 10.12)
func run() {}
}

参考: https://docs.swift.org/swift-book/ReferenceManual/Attributes.html

  • 可以通过@objc 重命名Swift暴露给OC的符号名(类名、属性名、函数名等)
@objc(MJCar)
@objcMembers class Car: NSObject {
      var price: Double
      @objc(name)
       var band: String
       init(price: Double, band: String) {
              self.price = price
              self.band = band
       }
        @objc(drive)
         func run() { print(price, band, "run") } 
        static func run() { print("Car run") }
}
extension Car {
        @objc(exec:v2:)
        func test() { print(price, band, "test") } }

  • Swift中依然可以使用选择器,使用#selector(name)定义一个选择器
    必须是被@objcMembers或@objc修饰的方法才可以定义选择器
 @objcMembers class Person: NSObject {
          func test1(v1: Int) { print("test1") }
          func test2(v1: Int, v2: Int) { print("test2(v1:v2:)") }
          func test2(_ v1: Double, _ v2: Double) { print("test2(_:_:)") } 
          func run() {
                    perform(#selector(test1)) perform(#selector(test1(v1:)))                   
                    perform(#selector(test2(v1:v2:)))       
                    perform(#selector(test2(_:_:))) 
                    perform(#selector(test2 as (Double, Double) -> Void))
          } 
}
关联对象(Associated Object)
  • 在Swift中,class依然可以使用关联对象
  • 默认情况,extension不可以增加存储属性
    借助关联对象,可以实现类似extension为class增加存储属性的效果

class Person {}
extension Person {
private static var AGE_KEY: Void?
var age: Int {
get {
(objc_getAssociatedObject(self, &Self.AGE_KEY) as? Int) ?? 0
}
set {
objc_setAssociatedObject(self, &Self.AGE_KEY,newValue, .OBJC_ASSOCIATION_ASSIGN)
}
}
}

var p = Person()
print(p.age) // 0
p.age = 10
print(p.age) // 10

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350