Files
knowledge-kit/Chapter1 - iOS/1.124.md
2024-04-27 13:01:58 +08:00

210 lines
7.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 从 OC 到 Swift
## OC 与 Swift 混编模式下,方法调用原理探究
OC 与 Swift 混编
`Person.h`
```objective-c
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface Person : NSObject
- (instancetype)initWithCat:(id)cat;
- (void)showPower;
@end
NS_ASSUME_NONNULL_END
#import "Person.h"
#import "TestiOSWithSwift-Swift.h"
@interface Person()
@property (nonatomic, strong) Cat *cat;
@end
@implementation Person
- (instancetype)initWithCat:(id)cat {
if (self = [super init]) {
_cat = cat;
}
return self;
}
- (void)showPower {
NSLog(@"I have a cat");
[self.cat sayHi];
[self.cat run];
}
@end
```
`Cat.Swift`
```swift
import Foundation
@objcMembers class Cat: NSObject {
var name: String
init(_ name: String = "Tom") {
self.name = name
}
func sayHi () {
print("My name is \(name)")
}
func test1(v1: Int) {
print("test1")
}
func test2(v1: Int, v2: Int) {
print("test2")
}
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(_:_:)))
}
}
```
点击屏幕触发事件,在 `ViewController.swift`
```swift
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
var cat: Cat = Cat("屁屁")
var person: Person = Person(cat: cat)
person.showPower()
}
```
问题:
1. 为什么 Swift 暴露给 OC 的类最终要继承自 NSObject
因为在 OC 中,方法消息走的是消息传递,也就是 Runtime 的机制Runtime 的实现依赖于 isa 指针,所以类必须继承自 NSObject。
2. Swift 代码中调用 OC 对象的方法 `person.showPower() ` 底层是怎么调用的?
底层实现还是需要用汇编来验证。断点加在 `person.showPower() ` 处
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/CallOCMethodInSwiftDemo1.png" style="zoom:25%">
可以看到即使在 Swift 代码中,调用 OC 对象方法,本质上还是走 Objc Runtime 的一套流程。50行代码将 showPower 的地址赋值给 `rsi` 寄存器,然后调用 `objc_msgSend` 方法。
LLDB 下 输入 `si` 窥探下实现。
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/CallOCMethodInSwiftDemo2.png" style="zoom:25%">
可以看到一个很大的地址 `0x00007ff80002d7c0` 就是动态库的符号方法地址。同时 Xcode 很智能,右侧给出了函数名称。
3. OC 调用 Swift 底层又是如何调用的?在 OC 类 Person 中,底层调用 Swift Cat 类的 sayHi 方法。
断点加在 `[self.cat sayHi]` 处,可以看到本质上还是 Runtime objc_msgSend 那一套。
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/CallOCMethodInSwiftDemo3.png" style="zoom:25%">
4. `cat.run()` 底层是怎么调用的?
如果一个 Swift 类,不继承自 NSObject那么方法调用的本质就是走虚表那套逻辑找到指针的前8个字节根据前8个字节找到类信息然后在类信息中前面一些内存地址存储类型信息后续根据偏移在方法列表中找到需要调用的函数地址。类似下面的图。
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftClassPointerDemo2.png" style="zoom:25%">
那 Swift 类继承自 NSObject 后,依然在 Swfit 中调用方法,背后的原理是什么?
在 ViewController.swift 中 `cat.sayHi()` 下断点
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/CallOCMethodInSwiftDemo4.png" style="zoom:25%">
## Swift 方法如何走 Runtime 消息机制
可以看到,即使一个 Swift 类继承自 NSObject但依旧在 Swift 中调用对象方法,本质上还是走虚表那套方法调用流程,不会走 Runtime 消息机制。
如果想让 Swift 方法调用走 Runtime 消息机制,可以在方法前加 `@objc dynamic`
```swift
dynamic func sayHi () {
print("My name is \(name)")
}
```
断点查看,发现在 Swift 代码中调用同样的 Swift 对象方法,此时走了 Runtime 消息机制。
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/CallOCMethodInSwiftDemo5.png" style="zoom:25%">
## Swift OC 混编,内存布局会改变吗
如果一个 Swift 类继承自 NSObject内存布局会改变
```swift
class Person {
var age: Int = 28
var height: Int = 175
}
let p: Person = Person()
print(Mems.memStr(ofRef: p))
// console
0x0000000100010540 0x0000000000000003 0x000000000000001c 0x00000000000000af
```
可以看到一个 Swift 类前8个字节用来存放类信息的指针其次8个字节用来存放引用计数信息后16个字节用来存放28和175就是存储属性信息
调整下:
```swift
import Foundation
class Person: NSObject {
var age: Int = 28
var height: Int = 175
}
let p: Person = Person()
print(Mems.memStr(ofRef: p))
// console
0x011d8001000104e9 0x000000000000001c 0x00000000000000af 0x0000000000000000
```
可以看到当 Swift 类继承自 NSObject 后前8个字节存放的是 isa 指针其次的16个字节存放存储属性信息最后的8个字节用来内存对齐。
## 混编
### Swift 类如何在 OC 中使用
OC 项目,使用 Swift 开发默认会创建 `项目名-Swift.swift` 文件。Swift 类都可以在该文件中找到
默认情况下生成的 Swift class 是不可以直接在 OC 中使用的,如果需要访问需要在 class 前加 `@objc`,编译器生成的代码如下:
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftClassCannotUseInOC1.png" style="zoom:25%">
class 不仅需要创建对象,还需要访问属性和方法,可以在属性或者方法前加 `@objc`,效果如下
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftClassCannotUseInOC2.png" style="zoom:25%">
但这样很麻烦,需要给每个属性、方法添加 `@objc`。有简便方法,可以直接在 class 前加 `@objcMembers`,这样该 class 所有的属性、方法都可以在 OC 中访问
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/SwiftClassCannotUseInOC3.png" style="zoom:25%">
### OC 类、对象方法如何在 Swift 中访问
要在 Swift 中访问 OC 类需要创建桥接文件OC 工程首次创建 Swift 文件时Xcode 默认创建桥接文件 `项目名-Bridging-Header.h`。如果是手动创建的,则需要配置(在项目的 Build Settings 中,找到 Objective-C Bridging Header 设置项,并指定桥接头文件的路径。确保桥接头文件的路径正确无误,并且文件名和扩展名都正确)。
在桥接文件中(`项目名-Bridging-Header.h` 写好需要在 Swift 中使用的 objective-C 类。
Swift 中不允许访问 objective-c 的方法或者需要换个方法名去调用,该怎么实现?
<img src="https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/ObjcMethodCannotUseInSwift.png" style="zoom:25%">
`- (void)showPower NS_SWIFT_NAME(diaplayPower());` oc 对象方法名,在 Swift 中使用时,想换个名字,可以用 `NS_SWIFT_NAME(新的方法名())`
`- (void)displayPower NS_SWIFT_UNAVAILABLE("请使用 showPower");` oc 对象方法名,不想在 Swift 使用时,可以加 `NS_SWIFT_UNAVAILABLE(原因)`