帕帕's Blog

记录点点滴滴

Cell 组件

在 iOS 开发中,当我们需要实现列表需求时,通常会写出 Cell 组件来实现每个列表项的布局和样式。但是这种方式的缺点是,当列表的实现方式从 UITableView 切换到 UICollectionView 时,我们的 Cell 组件就需要做出一些修改,这不够灵活。甚至当我们需要在其他地方使用 Cell 组件时,还需要重新实现一遍,这对于复用和维护来说都不是最佳的解决方案。

当使用 Cell 的方式实现下面的课程组件:

列表组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 定义课程的 Cell 组件
final class CourseCell: UITableCell {
// ...

let coverView = CourseCoverView()
let infoView = CourseInfoView()
}

// 定义课程的封面组件
final class CourseCoverView: UIView {
// ...
}

// 定义课程的详细信息组件
final class CourseInfoView: UIView {
// ...
}

如果现在又来一个新的需求,需要在列表的最上面放一排可以横向滚动的封面图,下面依旧还是课程列表。这里使用 UICollectionView 来实现我们的这个需求,那么需要修改的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 定义课程的 Cell 组件
final class CourseCell: UICollectionViewCell {
// ...

let coverView = CourseCoverView()
let infoView = CourseInfoView()
}

// 定义课程封面的 Cell 组件
final class CourseCoverCell: UICollectionViewCell {
// ...

let coverView = CourseCoverView()
}

// 定义课程的封面组件
final class CourseCoverView: UIView {
// ...
}

// 定义课程的详细信息组件
final class CourseInfoView: UIView {
// ...
}

View 组件

因此,使用 View 组件实现列表项会更加灵活。在这种情况下,Cell 的作用只是一个容器,只是把 View 放到 Cell 上去,并且负责做一些注册和重用的机制。但这种方式的问题是,每当我们把这个 View 用在 UITableView 或 UICollectionView 上时,我们就必须编写对应的 Cell 容器代码,这部分代码的工作基本相同(添加 View,添加约束)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// MARK: - Cell 组件
// 定义课程的 UICollectionViewCell 组件
final class CourseCollectionCell: UICollectionViewCell {
// ...

let courseView = CourseView()
}

// 定义课程的 UITableViewCell 组件
final class CourseTableCell: UITableViewCell {
// ...

let courseView = CourseView()
}

// MARK: - View 组件
// 定义课程组件
final class CourseView: UIView {
// ...

let coverView = CourseCoverView()
let infoView = CourseInfoView()
}

// 定义课程的封面组件
final class CourseCoverView: UIView {
// ...
}

// 定义课程的详细信息组件
final class CourseInfoView: UIView {
// ...
}

我们可以从上面的示例代码中看出,这种方式在实际的工程中依旧会出现大量的模板代码,所这就需要我们思考有没有什么方式可以来避免编写大量、重复的模板代码了?

CheapCell

CheapCell 是之前的项目重构成 Swift 之后实现的一套能够让你不用写 Cell 容器的库,这个库当时帮助我们减少了上千行的 Cell 容器代码。它的核心代码很简单,最重要的是利用了 Swift 的面相协议编程的方式来实现的,能够极大地减少代码量和提高代码的复用性和可维护性。具体的实现代码可以看我的 Github 仓库 https://github.com/HParis/CheapCell。

下面是集成了 CheapCell 之后的使用方式:

第一步:让我们的 View 组件遵循我们定义的 CheapCell 协议

1
2
3
// MARK: - CheapCell 协议
extension CourseView: CheapCell {}
extension CourseCoverView: CheapCell {}

第二步:在 UICollectionView 中的使用方式(UITableView 的使用方式也是类似的)

1
2
3
4
5
6
7
// Register cheap cell
collectionView.registerCheapCell(CourseView.self)
collectionView.registerCheapCell(CourseCoverView.self)

// Reuse cheep cell
let cell = collectionView.dequeueReusableCheapCell(for: indexPath) as CollectionCell<CourseView>
let cell = collectionView.dequeueReusableCheapCell(for: indexPath) as CollectionCell<CourseCoverView>

配合第三方库使用

如果你使用了一些第三方的 Cell 的话,比如 SwipeCellKit,那么你可以按照下面的方式来实现你的 CheapCell:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import SwipeCellKit

public extension UICollectionView {
func registerCheapSwipeCell<T: CheapCell>(_ : T.Type) {
register(CollectionSwipeCell<T>.self, forCellWithReuseIdentifier: CollectionSwipeCell<T>.identifier)
}

func dequeueReusableCheapSwaipeCell<T: CheapCell>(for indexPath: IndexPath) -> CollectionSwipeCell<T> {
dequeueReusableCell(withReuseIdentifier: CollectionSwipeCell<T>.identifier, for: indexPath) as! CollectionSwipeCell<T>
}
}


public final class CollectionSwipeCell<T: CheapCell>: SwipeCollectionViewCell {
public static var identifier: String {
return T.identifier
}

public let itemView = T()
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}

required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupView()
}

private func setupView() {
contentView.addSubview(itemView)

itemView.translatesAutoresizingMaskIntoConstraints = false
itemView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor).isActive = true
itemView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor).isActive = true
itemView.topAnchor.constraint(equalTo: contentView.topAnchor).isActive = true
itemView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor).isActive = true
}
}

现在你就可以直接通过 CheapCell 的方式来使用了:

1
2
3
4
5
6
// Register cheap swipe cell
collectionView.registerCheapSwipeCell(CourseView.self)

// Reuse cheep swipe cell
let cell = collectionView.dequeueReusableCheapCell(for: indexPath) as CollectionSwipeCell<CourseView>
cell.delegate = self

由于业务线的调整,之前负责的产品不打算上架 AppStore 了,需要调整为只提供给公司内部的员工使用。当时考虑的解决方案有两种:

  1. 使用新的 BundleID 和企业证书打包,采用此种方案需要重新申请部分第三方 SDK 的权限
  2. 直接在现在 AppStore 的包的基础之上使用企业证书重签名,这种方式会导致推送功能无法使用,还有一些依赖于证书的特性(Capabilities)无法使用

后来经过团队的考虑决定采用第二种方案,但是在采用第二种方案的之后出现了一个崩溃问题,而这个问题就是可以被我们利用来防止 App 被重签名的关键了。

写过 Swift 的同学都知道,在 Swift 中有一种 optional 类型的数据,它可以通过 ! 进行强制解包,而当它强制解包不成功的时候就会导致程序出现崩溃问题。

(说到这里可能会有同学认为在代码中就不应该使用强制解包的语法,那请问 Swift 为什么要推出这种语法呢?其实当我们能够百分比确认某一个变量或业务的状态,那我们就应该使用强制解包,这个时候强制解包的好处就是能够帮助我们在开发阶段提前把问题暴露出来,并且在代码的使用上也更方便。)

我们的应用由于需要在 Extension 和 Host App 之间进行数据共享,所以我们开启了 App Groups 这个特性。而在 Swift 代码中我们是这样去使用:

1
2
3
let store = UserDefaults(suiteName: "group.com.yourcompany.product")!
// or
let url = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.com.yourcompany.product")!

由于使用重签名的企业证书并没有包含这个的 App Group 的特性,所以最后会导致当我们手机安装重签名的包之后并且在启动后运行到这段代码的时候因为强制解包失败导致应用直接崩溃。
所以一般的应用即使在业务上没有相关的需求,我依旧建议大家也应该增加这样的特性功能,并且利用这个特性来让其他人没有那么容易就能对你的应用进行重签名之后拿去使用。

最近在面试的过程中让我突然线程优先级反转和自旋锁的关系有了一个新的认识。

优先级反转

线程优先级反转问题(Priority Inversion)即当一个高优先级任务通过信号量机制访问共享资源时,该信号量已被一低优先级任务占有,而这个低优先级任务在访问共享资源时可能又被其它一些中等优先级任务抢先,因此造成高优先级任务被许多具有较低优先级任务阻塞,实时性难以得到保证。

关于线程优先级反转可以参考【优先级反转那点事儿】。

优先级反转的问题一般可以通过调整线程的优先级得到解决:

  • 优先级天花板(Priority Ceiling)
  • 优先级继承(Priority Inheritance)

自旋锁

自旋锁是一种线程同步机制,在等待锁的过程中,线程会不断地轮询锁的状态(忙等待),直到获得锁为止。

自旋锁的优点是可以避免线程的阻塞和唤醒,从而减少了线程上下文切换的开销。但是自旋锁不能很好地应对长时间占用锁的情况,可能会导致其他线程无法及时获得 CPU 时间片,影响系统的并发性,并且在高竞争的情况下可能会导致 CPU 占用率过高,影响系统的性能。

自旋锁一般都是针对比较轻量的任务使用的,在 iOS 中一般就是对属性的 atomic 修饰,weak 的实现等这些轻量的任务使用自旋锁。

自旋锁不安全的原因是由于 iOS 的线程调度器和 QOS 的原因可能会导致低优先级的线程最终不会被执行。线程调度器总是优先考虑给 QOS 中较高等级中可运行的线程,而不是低等级的线程。由于在自旋锁上旋转的线程总是可运行的,这意味着如果有足够多的高 QOS 线程在等待一个由低 QOS 线程持有的锁,拥有该锁的线程将永远不会执行。

优先级反转和自旋锁的关系

其实我们可以发现优先级反转其实跟自旋锁没有关系,但是为什么在 iOS 面试中总是会把自旋锁和优先级反转放在一起来说呢?这是因为一旦出现优先级反转问题,自旋锁会让优先级反转问题不容易解决,甚至造成更严重的线程等待问题

参考资料:

  1. https://zhuanlan.zhihu.com/p/146132061
  2. https://forums.swift.org/t/thread-safety-of-weak-properties/422/12

被控制的 UIScrollView

在 UIViewController 中有个属性:automaticallyAdjustsScrollViewInsets,这个属性是用来控制 UIScrollView 的偏移行为的。

The default value of this property is true, which lets container view controllers know that they should adjust the scroll view insets of this view controller’s view to account for screen areas consumed by a status bar, search bar, navigation bar, toolbar, or tab bar. Set this property to false if your view controller implementation manages its own scroll view inset adjustments.

官方文档的意思当在 UIViewController 上添加 UIScrollView 的时候,会根据当前页面的 status bar、 search bar、navigation bar、toolbar 或 tab bar 来修改 UIScrollView 的内容区域。但是这个阶段的 UIViewController 比较蠢,不管任何情况下都会修改 UIScrollView 的偏移量。

比如我们现在有个 UINavigationController,然后添加一个 UIScrollView,然后在 UIScrollView 上面添加一个红色的方块,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let scrollView = UIScrollView()
scrollView.backgroundColor = .blue
scrollView.translatesAutoresizingMaskIntoConstraints = false
// 这里强制设置 contentSize 只是为了让 scrollView 能滚动起来
scrollView.contentSize = CGSize.init(width: view.frame.size.width, height: 1000)
view.addSubview(scrollView)
// ⚠️ 这里是直接跟 view 的 topAnchor 产生约束
scrollView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
scrollView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
scrollView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true

let redView = UIView()
redView.backgroundColor = .red
redView.translatesAutoresizingMaskIntoConstraints = false
scrollView.addSubview(redView)
redView.centerXAnchor.constraint(equalTo: scrollView.centerXAnchor).isActive = true
redView.topAnchor.constraint(equalTo: scrollView.topAnchor).isActive = true
redView.widthAnchor.constraint(equalToConstant: 100).isActive = true
redView.heightAnchor.constraint(equalToConstant: 100).isActive = true
图一:iOS 10 模拟器效果

当我们把 UIScrollView 的 topAnchor 修改为跟 UIViewController 的 topLayoutGuide 发生约束:

1
scrollView.topAnchor.constraint(equalTo: topLayoutGuide.bottomAnchor).isActive = true
图二:iOS 10 模拟器效果

我们发现最终的效果是 UIScrollView 也发生了偏移,而且这个偏移是根据你顶部的 status bar 和 navigation bar 的高度来决定的。所以在 iOS 10 及以下的版本的时候,添加到 UIViewController 的 UIScrollView 总是会发生偏移。但是你可以通过把刚才说的那个属性 automaticallyAdjustsScrollViewInsets设置成 false,UIViewController 就不会让你的 UIScrollView 发生偏移。但是这个属性会影响到所有添加到 UIViewController 上的 UIScrollView,如果有些想要发生偏移,有些不想发生偏移的时候就需要把 automaticallyAdjustsScrollViewInsets设置成 false,然后通过代码单独去为每个 UIScrollView 设置不同的 contentInset。

这种被控制的生活很不是滋味,于是随着 iOS 系统来到 11 之后,UIScrollView 终于夺回了自己的偏移控制权。UIViewController 的automaticallyAdjustsScrollViewInsets终于被废弃了,取而代之的是 UIScrollView 自己的contentInsetAdjustmentBehavior

自由的 UIScrollView

This property specifies how the safe area insets are used to modify the content area of the scroll view. The default value of this property is UIScrollView.ContentInsetAdjustmentBehavior.automatic.

UIScrollView 的contentInsetAdjustmentBehavior的默认行为是automatic,这和 iOS 10 默认行为的最大区别就是它会判断 UIScrollView 是被添加到哪个位置,然后根据这个位置来判断是否需要修改 UIScrollView 的偏移量。

还是拿上面图二的情况来讲,在 iOS 11 及 iOS 11 之后,我们还是照样只修改 UIScrollView 的 topAnchor:

1
scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
图三:iOS 12 模拟器效果

此时我们发现 UIScrollView 并没有发生偏移,这也是因为 iOS 11 之后引入来 safeArea 的概念之后带来的 UI 方面的优化。

contentInsetAdjustmentBehavior还有两个值,其中always对应了automaticallyAdjustsScrollViewInsetstrue,never对应了automaticallyAdjustsScrollViewInsetsfalse

至于scrollableAxes,它其实就是根据 UIScrollView 的滚动方向来决定在哪个轴上使用 sa feArea。

通过contentInsetAdjustmentBehavior我们就可以为 UIViewController 上的每一个 UIScrollView 定制它们的偏移行为。

重复添加相同观察者

我们先来看看日常开发中我们对 NSNotification 的正常用法,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 定义通知
let TestNotification = NSNotification.Name.init("com.papa.test")

// 测试类
class Test {

init() {
NotificationCenter.default.addObserver(self, selector: #selector(Test.test(notification:)), name: TestNotification, object: nil)
}

// 注意
deinit {
NotificationCenter.default.removeObserver(self)
}

@objc func test(notification: Notification) {
print("Test")
}
}

但是如果我们在刚才代码中的 init 方法里面对同一个通知多次添加同一个观察者的话,会发生什么?

1
2
3
4
5
6
7
init() {
NotificationCenter.default.addObserver(self, selector: #selector(Test.test(notification:)), name: TestNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(Test.test(notification:)), name: TestNotification, object: nil)
}

// 发送 TestNotification 通知
NotificationCenter.default.post(name: TestNotification, object: nil)

答案是会输出:

1
2
Test
Test

所以我们要尽量避免重复添加观察者,因为这有可能会造成一些未知现象的发生。

通知中的线程问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 定义通知
let ThreadNotification = NSNotification.Name.init("com.papa.thread")

// 测试类
class Test {

init() {
print("Add Observer: \(Thread.current)")
NotificationCenter.default.addObserver(self, selector: #selector(Test.test(notification:)), name: ThreadNotification, object: nil)
}

// 注意
deinit {
NotificationCenter.default.removeObserver(self)
}

@objc func test(notification: Notification) {
print("Receive: \(Thread.current)")
}
}

DispatchQueue.init(label: "com.ps.test.queue").async {
print("Post: \(Thread.current)")
NotificationCenter.default.post(name: ThreadNotification, object: nil)
}

我们来看看观察者是在什么线程上接受到通知的:

1
2
3
Add Observer: <NSThread: 0x60000147d1c0>{number = 1, name = main}
Post: <NSThread: 0x600001462640>{number = 3, name = (null)}
Receive: <NSThread: 0x600001462640>{number = 3, name = (null)}

虽然我们是在主线程中去添加观察者,但是因为我们是在其他线程中去发送通知的,所以最后我们也是在其他线程中接收到通知的。

通知中的阻塞问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 定义通知
let SleepNotification = NSNotification.Name.init("com.papa.sleep")

// 测试类
class Test {

init() {
NotificationCenter.default.addObserver(self, selector: #selector(Test.test(notification:)), name: SleepNotification, object: nil)
}

// 注意
deinit {
NotificationCenter.default.removeObserver(self)
}

@objc func test(notification: Notification) {
sleep(3)
}
}

let start = Date()
NotificationCenter.default.post(name: SleepNotification, object: nil)
let end = Date()
print("相差:\(end.timeIntervalSince(start))")

我们可以看到最后相差时间大概是 3s ,通过上面的代码我们就知道单 NotificationCenter 去 post 一个通知的时候,它会等待观察者处理完改通知之后才会继续往后执行。所以平常使用过程中我们要注意 post 有可能会阻塞当前线程,特别是在主线程中。

@objc

@objc 的作用是为了让 Objective-C 能够调用 Swift 的代码。其中的关键是 @objc 会生成一段 thunk 代码,Objective-C 通过这段 thunk 代码来间接调用 Swift 代码。如果是 Swift 来调用被 @objc 修饰的方法的时候,此时是不需要经过 thunk 代码就能直接调用的。

所以我们可以想象,如果方法变得复杂或者被 @objc 修饰的方法数量变得越来越多会发生什么事?答案就是 thunk 代码变得越来越多,最后会导致我们的包大小也变得越来越大。并且动态链接器(dynamic linker)还需要整理这些 thunk 代码,最后导致加载时间也会变得越来越长。

在 Swift3 的时候,编译器会推断出你的方法不是 Swift 专用的(比如有元组、结构体),就会默认给你的方法增加 @objc 的修饰。这种方式就导致了在 Swift3 的时候,会生成大量的 thunk 代码,并且这其中的大部分代码都不会被使用。所以 Swift4 默认是不做 @objc 的推断,只有我们手动添加了 @objc 之后,Objective-C 才能调用我们的 Swift 代码。

dynamic

Swift 的方法是通过 vtable 来调用的,使用 vtable 要比 Objective-C 的 runtime 更高效。

而使用 dynamic 来修饰的方法,代表这个方法是可以被动态调用的。而由于目前 Swfit 还没有实现自己的 runtime 机制,所以动态调用只能够在 Objective-C 去调用。在 Swift4 使用 dynamic 修饰一个方法的时候,编译器会要求你还需要使用 @objc 去修饰。这是为了明确的告诉编译器这个方法是由 Objective-C 的 runtime 来调用的,同时也是为了兼容以后可能会出现的 Swift runtime 机制。

由于目前使用 @objc dynamic 修饰的方法并不在 Swift 实例对象的 vtable 里面,所以 Swift 来调用该方法的时候依旧需要通过 thunk 代码来调用。

总结

此图出自 https://swiftunboxed.com/interop/objc-dynamic/

通过上图我们知道:

除非明确的知道会在 Objective-C 中调用这段代码,否则别使用 @objc;
除非明确的知道该方法需要被 Objective-C 的 runtime 动态调用,否则别使用 @objc dynamic。


参考文献

  1. https://swiftunboxed.com/interop/objc-dynamic/

  2. https://github.com/apple/swift-evolution/blob/master/proposals/0160-objc-inference.md

相信大家在 Objective-C 中都会通过 __waek 的修饰符来保证 block 和 self 不会互相引用,代码如下:

1
2
3
4
5
__weak typeof(self) weakSelf = self;
self.block = ^{
__strong typeof(self) strongSelf = self;
...
}

但是你思考过 self 在这一段旅程中的引用计数变化么,接下来我会通过三个例子来展示这一段旅程是怎样的?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 🌰1
NSLog(@"Before block:%ld", CFGetRetainCount((__bridge CFTypeRef)(self)));
self.block = ^{
self;
NSLog(@"Within block:%ld", CFGetRetainCount((__bridge CFTypeRef)(self)));
};
self.block();
NSLog(@"After block:%ld", CFGetRetainCount((__bridge CFTypeRef)(self)));


// 🌰2
__weak typeof(self) weakSelf = self;
NSLog(@"Before block:%ld", CFGetRetainCount((__bridge CFTypeRef)(self)));
self.block = ^{
weakSelf;
NSLog(@"Within block:%ld", CFGetRetainCount((__bridge CFTypeRef)(weakSelf)));
};
self.block();
NSLog(@"After block:%ld", CFGetRetainCount((__bridge CFTypeRef)(self)));


// 🌰3
__weak typeof(self) weakSelf = self;
NSLog(@"Before block:%ld", CFGetRetainCount((__bridge CFTypeRef)(self)));
self.block = ^{
__strong typeof(self) strongSelf = weakSelf;
NSLog(@"Within block:%ld", CFGetRetainCount((__bridge CFTypeRef)(weakSelf)));
};
self.block();
NSLog(@"After block:%ld", CFGetRetainCount((__bridge CFTypeRef)(self)));

我们可以通过 Clang 对上面的三个例子做一下编译,通过编译后的 C 代码(接下来所展示代码都是经过简化),我们可以推导出 self 的引用计数变化。


🌰1 的 C 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// Block 结构体。这个大家可以通过其他的资料去看看,我们今天主要是来探寻一下 self 的旅程,这里就不对 Block 本身做更详细的介绍
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};

// ^{} 的实现
struct __BlockTest__test_block_impl_0 {
struct __block_impl impl;
struct __BlockTest__test_block_desc_0* Desc;
BlockTest *const __strong self;
__BlockTest__test_block_impl_0(void *fp, struct __BlockTest__test_block_desc_0 *desc, BlockTest *const __strong _self, int flags=0) : self(_self) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

// Block 方法
static void __BlockTest__test_block_func_0(struct __BlockTest__test_block_impl_0 *__cself) {
BlockTest *const __strong self = __cself->self; // bound by copy
self;
}

// Block 的 copy 操作
static void __BlockTest__test_block_copy_0(struct __BlockTest__test_block_impl_0*dst, struct __BlockTest__test_block_impl_0*src) {_Block_object_assign((void*)&dst->self, (void*)src->self, 3/*BLOCK_FIELD_IS_OBJECT*/);}

// Block 的 dispose 操作
static void __BlockTest__test_block_dispose_0(struct __BlockTest__test_block_impl_0*src) {_Block_object_dispose((void*)src->self, 3/*BLOCK_FIELD_IS_OBJECT*/);}

// 描述 Block 的 copy 和 dispose
static struct __BlockTest__test_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __BlockTest__test_block_impl_0*, struct __BlockTest__test_block_impl_0*);
void (*dispose)(struct __BlockTest__test_block_impl_0*);
} __BlockTest__test_block_desc_0_DATA = { 0, sizeof(struct __BlockTest__test_block_impl_0), __BlockTest__test_block_copy_0, __BlockTest__test_block_dispose_0};

// 方法主体
static void _I_BlockTest_test(BlockTest * self, SEL _cmd) {
((void (*)(id, SEL, void (*)()))(void *)objc_msgSend)((id)self, sel_registerName("setBlock:"), ((void (*)())&__BlockTest__test_block_impl_0((void *)__BlockTest__test_block_func_0, &__BlockTest__test_block_desc_0_DATA, self, 570425344)));
((void (*(*)(id, SEL))())(void *)objc_msgSend)((id)self, sel_registerName("block"))();

}
  1. 在方法主体里面首先会构造一个 __BlockTest__test_block_impl_0 的结构体,该结构体捕获了 self;
  2. __BlockTest__test_block_impl_0 的构造函数中使用了 __strong 来捕获 self,所以我们知道在构造的时候默认是使用 __strong 来捕获外部的对象变量,此时 self 的引用计数应该要 +1;
  3. Block 被构造出来之后需要被赋值给 self,我们知道在 ARC 模式下此时的 Block 会执行 Copy 操作,从 _NSConcreteStackBlock 变成 _NSMallocBlock
  4. Block 通过 __BlockTest__test_block_desc_0_DATA 找到 Copy 方法的具体实现 __BlockTest__test_block_copy_0,从上面的代码中我们知道该方法的实现是通过 _Block_object_assign 来实现的(对于这个方法的实现细节暂时还没有找到更相信的资料,有知道的可以麻烦告诉一下),通过名字我们可以猜测出该方法只是把捕获的变量地址直接拷贝一份到堆内存中,但是不会引起引用计数的变化;
  5. 当 Block 被真正执行的时候会通过 __block_implFuncPtr 找到真正的实现代码 __BlockTest__test_block_func_0,我们观察到在这个方法里面有这样一句代码 BlockTest *const __strong self = __cself->self,很明显此时 self 的引用计数会 +1,当该 __BlockTest__test_block_func_0 执行完毕之后还是会释放 self 的,此时引用计数会 -1;

从上面的分析过程中,我们知道由于 Block 在构造的时候默认就对捕获的 self 进行了强引用,导致 self 的引用计数 +1;而又由于 self 持有了 Block,所以这里就造成了循环引用的问题。

我们来看 🌰2 能不能解决这个问题?


🌰2 的 C 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// ^{} 结构体
struct __BlockTest__test_block_impl_0 {
struct __block_impl impl;
struct __BlockTest__test_block_desc_0* Desc;
BlockTest *const __weak weakSelf;
__BlockTest__test_block_impl_0(void *fp, struct __BlockTest__test_block_desc_0 *desc, BlockTest *const __weak _weakSelf, int flags=0) : weakSelf(_weakSelf) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

// Block 方法
static void __BlockTest__test_block_func_0(struct __BlockTest__test_block_impl_0 *__cself) {
BlockTest *const __weak weakSelf = __cself->weakSelf; // bound by copy
weakSelf;
}

// Block 的 copy 操作
static void __BlockTest__test_block_copy_0(struct __BlockTest__test_block_impl_0*dst, struct __BlockTest__test_block_impl_0*src) {_Block_object_assign((void*)&dst->weakSelf, (void*)src->weakSelf, 3/*BLOCK_FIELD_IS_OBJECT*/);}

// Block 的 dispose 操作
static void __BlockTest__test_block_dispose_0(struct __BlockTest__test_block_impl_0*src) {_Block_object_dispose((void*)src->weakSelf, 3/*BLOCK_FIELD_IS_OBJECT*/);}

// 描述 Block 的 copy 和 dispose
static struct __BlockTest__test_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __BlockTest__test_block_impl_0*, struct __BlockTest__test_block_impl_0*);
void (*dispose)(struct __BlockTest__test_block_impl_0*);
} __BlockTest__test_block_desc_0_DATA = { 0, sizeof(struct __BlockTest__test_block_impl_0), __BlockTest__test_block_copy_0, __BlockTest__test_block_dispose_0};

// 方法主体
static void _I_BlockTest_test(BlockTest * self, SEL _cmd) {
__attribute__((objc_ownership(weak))) typeof(self) weakSelf = self;
((void (*)(id, SEL, void (*)()))(void *)objc_msgSend)((id)self, sel_registerName("setBlock:"), ((void (*)())&__BlockTest__test_block_impl_0((void *)__BlockTest__test_block_func_0, &__BlockTest__test_block_desc_0_DATA, weakSelf, 570425344)));
((void (*(*)(id, SEL))())(void *)objc_msgSend)((id)self, sel_registerName("block"))();
}
  1. 方法主体会先用 __weak 初始化一个 weakSelf,此时 self 的引用计数是不会发生变化的;之后会构造一个__BlockTest__test_block_impl_0 的结构体,该结构体捕获了 weakSelf;
  2. __BlockTest__test_block_impl_0 的构造函数中使用了 __weak 来捕获 weakSelf,所以我们知道此时 self 的引用计数应该要也是不会发生变化的;
  3. 然后把该结构体赋值给 self.block,block 结构体被从栈复制到堆的时候使用了 _Block_object_assign,所以此时 self 的引用计数不会发生变化
  4. 然后 block 在被执行的时候做了一下 __weak 的操作 BlockTest *const __weak weakSelf = __cself->weakSelf,这时候 self 的引用计数也不会发生变化
  5. 由于 block 对 weakSelf 没有强引用,所以在 block 执行完成之后也不需要做释放 weakSelf 的工作

所以,在该例子中 block 无法强引用 weakSelf,weakSelf 的引用计数没有发生任何变化。由于 self 没有被 block 强应用,所以当 self 要被释放的时候,block 也会被释放,这就解决了我们 🌰1 中的循环引用的问题。但是在 block 方法执行的过程中,self 对象有可能已经被释放了,此时如果你还去使用 weakSelf 就有可能造成奔溃的情况。


🌰3 的 C 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
struct __BlockTest__test_block_impl_0 {
struct __block_impl impl;
struct __BlockTest__test_block_desc_0* Desc;
BlockTest *const __weak weakSelf;
__BlockTest__test_block_impl_0(void *fp, struct __BlockTest__test_block_desc_0 *desc, BlockTest *const __weak _weakSelf, int flags=0) : weakSelf(_weakSelf) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

static void __BlockTest__test_block_func_0(struct __BlockTest__test_block_impl_0 *__cself) {
BlockTest *const __weak weakSelf = __cself->weakSelf; // bound by copy
__attribute__((objc_ownership(strong))) typeof(self) strongSelf = weakSelf;
}

static void __BlockTest__test_block_copy_0(struct __BlockTest__test_block_impl_0*dst, struct __BlockTest__test_block_impl_0*src) {_Block_object_assign((void*)&dst->weakSelf, (void*)src->weakSelf, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static void __BlockTest__test_block_dispose_0(struct __BlockTest__test_block_impl_0*src) {_Block_object_dispose((void*)src->weakSelf, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static struct __BlockTest__test_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __BlockTest__test_block_impl_0*, struct __BlockTest__test_block_impl_0*);
void (*dispose)(struct __BlockTest__test_block_impl_0*);
} __BlockTest__test_block_desc_0_DATA = { 0, sizeof(struct __BlockTest__test_block_impl_0), __BlockTest__test_block_copy_0, __BlockTest__test_block_dispose_0};

static void _I_BlockTest_test(BlockTest * self, SEL _cmd) {
__attribute__((objc_ownership(weak))) typeof(self) weakSelf = self;
((void (*)(id, SEL, void (*)()))(void *)objc_msgSend)((id)self, sel_registerName("setBlock:"), ((void (*)())&__BlockTest__test_block_impl_0((void *)__BlockTest__test_block_func_0, &__BlockTest__test_block_desc_0_DATA, weakSelf, 570425344)));
((void (*(*)(id, SEL))())(void *)objc_msgSend)((id)self, sel_registerName("block"))();
}

前面的步骤都跟 🌰2 中的一样,关键是在 Block 的方法实现里面有点不一样。我们来看看 __BlockTest__test_block_func_0,它首先调用了 BlockTest *const __weak weakSelf = __cself->weakSelf, 所以它此时的引用计数不会发生变化;但是接下来又用 objc_ownership(strong) 来强引用 weakSelf,所以此时 self 的引用计数 +1。这就保证了在函数执行的过程中,Block 会一直持有 self,知道 Block 执行完毕之后会释放 weakSelf。

所以 🌰3 完美的解决了循环应用和直接使用 __weak 可能导致奔溃的问题。


最后,说一下关于 _Block_object_assign 的猜想:

1
_Block_object_assign((void*)&dst->weakSelf, (void*)src->weakSelf, 3/*BLOCK_FIELD_IS_OBJECT*/);

通过上面的例子,我们知道 Block 在构造的时候就会对捕获的变量进行内存管理(强引用和弱引用),所以当 Block 在做 Copy 操作的时候其实没有必要对它捕获的变量再做一遍内存管理了。这也应该是 Block 的 Copy 操作使用了 _Block_object_assign 这种不会导致引用计数发生变化的方式来实现的原因。

当系统存在两个线程及以上的时候,双方都在等待对方停止执行,以获得系统资源,但是没有一方提前退出的时候就叫做死锁。

那在 Objective-C 里面如何实现死锁呢:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
self.lock1 = [NSLock new];
self.lock2 = [NSLock new];

dispatch_async(dispatch_queue_create("com.papa.task1", DISPATCH_QUEUE_SERIAL), ^{
[self.lock1 lock];
NSLog(@"task1 获得 lock1");
sleep(2);
[self.lock2 lock];
NSLog(@"task1 获得 lock2");
[self.lock2 unlock];
[self.lock1 unlock];
});

dispatch_async(dispatch_queue_create("com.papa.task2", DISPATCH_QUEUE_SERIAL), ^{
[self.lock2 lock];
NSLog(@"task2 获得 lock2");
sleep(2);
[self.lock1 lock];
NSLog(@"task2 获得 lock1");
[self.lock1 unlock];
[self.lock2 unlock];
});

我们可以看到最后控制台输出的结果是:

1
2
task2 获得 lock2
task1 获得 lock1

1
2
task2 获得 lock2
task1 获得 lock1

为什么是两种结果呢,可以参考我的「初步了解 GCD」

但是不管如何,这两种结果都没有输出 task1 获得 lock2task2 获得 lock1。我们来分析一下:

  • task1 获得 lock1,然后沉睡 2s
  • task2 获得 lock2,然后沉睡 2s
  • task1 经过 2s 的沉睡之后想要去获取 lock2,此时发现 lock2 已经被使用,那就继续等待
  • task2 经过 2s 的沉睡之后想要去获取 lock1,此时发现 lock1 已经被使用,那就继续等待
  • task1 又被唤醒想要去获取 lock2,此时 lock2 依旧没有被 task2 释放,只能继续等待
  • task2 又被唤醒想要去获取 lock1,此时 lock1 依旧没有被 task1 释放,只能继续等待

于是 task1 和 task2 都在等待对方释放资源,但是自己也不退出也不释放资源,最终导致死锁的产生。

接下来,我们来讨论另外一个例子是不是死锁:

1
2
3
4
5
6
// 某个按钮的点击事件
- (void)onClick:(UIEvent *)event {
dispatch_sync(dispatch_get_main_queue(), ^{
...
});
}

相信大家都知道上面这个例子会导致主线程发生阻塞的现象,但是这是因为死锁造成的么?

Submits a block to a dispatch queue for synchronous execution. Unlike dispatch_async, this function does not return until the block has finished. Calling this function and targeting the current queue results in deadlock.

在官方文档里面明确的说了,这就是死锁。我们可以把上面的例子“翻译”一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 首先进入 onClick 的时候,我们可以认为此时是需要加锁的
// 某个按钮的点击事件
- (void)onClick:(UIEvent *)event {
[self.lock1 lock];

// 此时我们可以认为 dispatch_sync 是在获取 block 里面的 lock2
{
[self.lock2 lock];

// 放在主线程执行,那么它也需要获得 lock1
[self.lock1 lock];
[self.lock1 unlock];

[self.lock2 unlock];
}

[self.lock1 unlock];
}

我们可以看到其实上面的情况就是两个任务都在同时竞争主线程的资源,并且谁都没有退出最终导致死锁的产生。但是这两个任务并不是普通的两个线程在竞争资源,而是都在主线程上,一个嵌套另外一个。而且这种特殊的情况,在运行的时候会直接导致奔溃,而不像我们一开始的例子一样只是在互相等待。但是既然苹果把这种情况也称为死锁,那我们就当做死锁来看待,毕竟他们都是在竞争系统资源。

浅拷贝(Shallow copies)和深拷贝(Deep copies)

我们都知道 Objective-C 中把 Copy 操作分成两种:浅拷贝(Shallow copies)深拷贝(Deep copies)。学过 C 语言的同学应该知道区分这两种操作的区别其实很简单:

浅拷贝(Shallow copies): 指针拷贝,指向的还是同一块内容的地址
深拷贝(Deep copies): 内容拷贝

但是在 Objective-C 里面对于 Copy 的实现还是跟 C 语言的有点差别。我们先来看看 Apple 的官方文档给出的一张图:

Collections Programming Topics

通过上图可以看出浅拷贝过后,Array 1 和 Array 2 的元素都是相同的指针地址,指向相同的内容;深拷贝过后,内容被拷贝一份新的出来,Array 2 的元素的指针地址都和 Array 1 不一样,因为 Array2 的元素的指针地址都指向新的内容。

immutable 和 mutable 对象的拷贝

在 Objective-C 中一般会用 copy 或 mutableCopy 进行拷贝操作,我们可以通过观察指针变化来确定这两种拷贝操作是浅复制还是深复制

  • immutable 对象的复制操作

    1
    2
    3
    4
    5
    6
    7
    8
    NSString * aName = @"帕帕";
    NSString * bName = [aName copy];
    NSMutableString * cName = [aName mutableCopy];

    NSLog(@"aName 的指针:%p", aName);
    NSLog(@"bName 的指针:%p", bName);
    NSLog(@"cName 的指针:%p", cName);

    输出的结果:

    1
    2
    3
    aName 的指针:0x103d34070
    bName 的指针:0x103d34070
    cName 的指针:0x600000250dd0
  • mutable 对象的复制操作

    1
    2
    3
    4
    5
    6
    7
    8
    NSMutableString * aName = [NSMutableString stringWithString:@"帕帕"];
    NSString * bName = [aName copy];
    NSMutableString * cName = [aName mutableCopy];

    NSLog(@"aName 的指针:%p", aName);
    NSLog(@"bName 的指针:%p", bName);
    NSLog(@"cName 的指针:%p", cName);

    输出的结果:

    1
    2
    3
    aName 的指针:0x60000025e150
    bName 的指针:0x600000222900
    cName 的指针:0x60000025e450

通过上面两个例子以及它们的输出结果,我们可以得出下面这个表格:

imutable 对象 mutable 对象
copy 浅复制 深复制
mutableCopy 深复制 深复制

上面的规则对集合对象也是一样的:NSArray 和 NSMutableArray,NSDictionary 和 NSMutableDictionary,NSSet 和 NSMutableSet

单层深复制(one-level-deep)

1
2
3
4
5
6
7
8
9
10
11
12
NSMutableString * aString = [NSMutableString stringWithString:@"Hello"]

NSMutableArray * aArray = [NSMutableArray arrayWithObjects:aString, nil];
NSArray * bArray = [aArray copy];

NSMutableString * bString = bArray[0];
[bString appendString:@" 帕帕"];

NSLog(@"aArray 的指针:%p", aName);
NSLog(@"bArray 的指针:%p", bName);
NSLog(@"aArray 第一个元素的指针: %p,内容:%@", aArray[0], aArray[0]);
NSLog(@"bArray 第一个元素的指针: %p,内容:%@", bArray[0], bArray[0]);

输出结果:

1
2
3
4
aArray 的指针:0x60000025d9a0
bArray 的指针:0x60000002cf60
aArray 第一个元素的指针: 0x60000025d880,内容:Hello 帕帕
bArray 第一个元素的指针: 0x60000025d880,内容:Hello 帕帕

从 aArray 到 bArray 的 copy 操作之后,它们的指针地址发生了变化,按照我们之前的理解这是深拷贝深拷贝会把 aArray 的元素都拷贝一份,那为什么改变 bArray 的元素的值会导致 aArray 的元素的值也发生了变化呢?

集合对象的深拷贝

完全深复制

那我们要如何做到真正的深复制呢?我们可以简单的把上面的代码改一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
NSMutableString * aString = [NSMutableString stringWithString:@"Hello"]

NSMutableArray * aArray = [NSMutableArray arrayWithObjects:aString, nil];

// 只需要改动这一行代码
NSArray *bArray = [NSKeyedUnarchiver unarchiveObjectWithData:[NSKeyedArchiver archivedDataWithRootObject:aArray]];

NSMutableString * bString = bArray[0];
[bString appendString:@" 帕帕"];

NSLog(@"aArray 的指针:%p", aName);
NSLog(@"bArray 的指针:%p", bName);
NSLog(@"aArray 第一个元素的指针: %p,内容:%@", aArray[0], aArray[0]);
NSLog(@"bArray 第一个元素的指针: %p,内容:%@", bArray[0], bArray[0]);

输出结果:

1
2
3
4
aArray 的指针:0x600000259cb0
bArray 的指针:0x600000030ac0
aArray 第一个元素的指针: 0x604000452120,内容:Hello
bArray 第一个元素的指针: 0x604000452780,内容:Hello 帕帕

只要先对集合对象分别用 NSKeyedArchiver 和 NSKeyedUnarchiver 就可以真正完成对一个集合对象的深复制。

Copy 和 内存管理

之前我们说过 Objective-C 里面对于 Copy 的实现还是跟 C 语言的有点差别,那差别在什么地方呢?
内存中做复制操作是很耗费资源的,而我们都知道 Objective-C 高效的一个原因在于它的内存管理机制是引用计数。我们前面分析的深拷贝是对内容的拷贝,这一点跟 C 语言的一样。C 语言的浅拷贝是指针的拷贝,它依旧做了一次复制操作。而在 Objective-C 中,浅拷贝其实只是引用计数的增加,不信的话,我们可以看看下面的例子:

1
2
3
4
5
6
NSArray * aArray = [NSArray arrayWithObjects:@"帕帕", nil];
NSLog(@"aArray 的指针:%p,引用计数:%ld", aArray, CFGetRetainCount((__bridge CFTypeRef)(aArray)));
NSArray * bArray = [aArray copy];
NSLog(@"aArray 的指针:%p,引用计数:%ld", aArray, CFGetRetainCount((__bridge CFTypeRef)(aArray)));
NSMutableArray * cArray = [aArray mutableCopy];
NSLog(@"aArray 的指针:%p,引用计数:%ld", aArray, CFGetRetainCount((__bridge CFTypeRef)(aArray)));

输出结果:

1
2
3
aArray 的指针:0x604000443ba0,引用计数:2
aArray 的指针:0x604000443ba0,引用计数:3
aArray 的指针:0x604000443ba0,引用计数:3

为什么 aArray 刚出来的时候的引用计数是 2?因为 [NSArray arrayWithObjects:@"帕帕", nil] 本身就是一个对象,它的引用计数就是 1;然后我们又定义了 aArray 来引用这个对象,此时它的引用计数就增加了 1,变成了 2;之后我们对 aArray 进行了 copy 操作,发现它的引用计数变成了 3,所以这里的 copy 操作其实相当于 retaion;最后我们对 aArray 进行了 mutableCopy 操作,此时它的引用计数还是 3,没有发生变化,因为这个时候进行了内容复制。

所以在 Objective-C 中对一个 imutable 对象进行的 copy(浅复制)操作,其实都只会引起引用计数的变化,而不会在内存中做出任何拷贝操作,包括指针拷贝。

NSCopying 和 NSMutableCopying

如果我们有一个自定义的对象,并且对其进行 copy 操作的话,会发生什么:

1
2
3
4
5
6
7
8
9
// Person
@interface Person: NSObject
@property (nonatomic, copy) NSString * name;
@end
@implementation Person
@end

Person * aPerson = [Person new];
Person * bPerson = [aPerson copy];

Xcode 直接奔溃了:

1
2
// 崩溃
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Person copyWithZone:]: unrecognized selector sent to instance 0x60000000d5f0'

为什么我们对一个 Person 对象使用了 copy,Xcode 确报的是找不到 copyWithZone: 这个 selector 的错误。

这是因为 Objective-C 中规定,一个对象如果想要使用 copy 或 mutableCopy 的操作,必须要实现 NSCopyingNSMutableCopying 这两个协议。这两个协议规定了对象需要实现 copyWithZone:mutableCopyWithZone: 这两个方法,因为对一个对象做 copy 或 mutableCopy 最后都会去调用这两个方法来做最终的实现。
上面例子中的集合对象能够使用 copy 和 mutableCopy 操作是也因为它们都实现了 NSCopying 和 NSMutableCopying 协议。

我们来看看如何对一个普通的对象实现 NSCopying 协议:

1
2
3
4
5
6
7
8
9
10
11
12
13
@interface Person: NSObject <NSCopying>
@property (nonatomic, copy) NSString * name;
@property (nonatomic, strong) NSMutableArray * mArray;
@end

@implementation Person
- (instancetype)copyWithZone:(NSZone *)zone {
Person * person = [[self class] new];
person.name = [self.name copy];
person.mArray = [self.mArray mutableCopy];
return person;
}
@end

这样,我们就可以愉快的使用 [Person copy] 了。当然,这里 Person 的 mArray 也只是单层深复制,如果想要实现完全深复制的话,我们可以用 NSKeyedArchiver 和 NSKeyedUnarchiver 来完成对 mArray 的完全深复制

Block 和 Copy

简单说一下,在 Objective-C 中,Block 的 copy 是一种特殊的操作。因为 Block 是一种结构体,它无法实现 NSCopying 或 NSMutableCopying 协议,但是它却可以调用 copy 方法。这是由 Block 的结构体决定的:

Block 里面的 descriptor 有 copy 的函数指针,当对 Block 执行 copy 操作最后都会通过该函数指针进行真正的操作。这也是 Bloc看不需要实现 NSCopying 和 NSMutableCopying 就能调用 copy 方法的原因。

参考资料:

  1. https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/Collections/Articles/Copying.html
  2. https://www.zybuluo.com/MicroCai/note/50592

当我们要在 iOS 端实现一个 React Native 可用的 Component,比如:

1
<MapView onRegionChange={(event) => {}} zoomLevel={2} />

那么我们基本上就是要解决下面这三个问题:

  • 如何把 iOS 上的 UI 暴露给 React Native 端?
  • 如何在 React Native 给 iOS 的 UI 传值?
  • 如何在 React Native 中响应 iOS 的事件?

这三个问题可以在官方文档找到答案。

如何把 iOS 上的 UI 暴露给 React Native 端

首先你需要创建一个继承自 RCTViewManager 的子类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// RNTMapManager.m
#import <MapKit/MapKit.h>
#import <React/RCTViewManager.h>

// 继承 RCTViewManager
@interface RNTMapManager : RCTViewManager
@end


@implementation RNTMapManager

// 调用 RCT_EXPORT_MODULE 暴露该类的名字给 React Native 使用。如果你想自定义
// 暴露给 React Native 的名字时,你需要 RCT_EXPORT_MODULE(YOUR_CUSTOM_NAME)。
RCT_EXPORT_MODULE()

// 由于 RCTViewManager 是 NSObject,所以这里必须需要实现该方法来告诉
// React Native 去使用哪个 UIView
- (UIView *)view
{
return [MKMapView new];
}

@end

这样我们就可以在 React Native 使用 MapView 了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// MapView.js

import { requireNativeComponent } from 'react-native';

// requireNativeComponent 会自动把 iOS 上的 RNTMapManager 解析成 RNTMap。
// 如果去掉 iOS 上的 Manager 后缀会有什么影响?嗯,没有任何影响。
module.exports = requireNativeComponent('RNTMap', null);


// MyApp.js
import MapView from './MapView.js';

...

render() {
return <MapView />;
}

如何在 React Native 给 iOS 的 UI 传值

如果需要传值给 iOS 上的 UI,那么需要使用另外一个宏:

1
RCT_EXPORT_VIEW_PROPERTY(zoomEnabled, BOOL)

这时候就可以在 React Native 上使用了:

1
<MapView zoomEnable={true} />

这里需要注意的是,RCT_EXPORT_VIEW_PROPERTY 所暴露的属性必须是之前我们说的 UIView(即继承于 RCTViewManager 并通过 - (UIView *)view; 返回的 View)已经存在的属性。

除了上面的宏 RCT_EXPORT_VIEW_PROPERTY 可以暴露属性给 React Native 使用之外还有下面 5 种(这里先挖个坑,回头研究一下再说说下面五种的作用和区别):

  • RCT_REMAP_VIEW_PROPERTY
  • RCT_CUSTOM_VIEW_PROPERTY
  • RCT_EXPORT_SHADOW_PROPERTY
  • RCT_REMAP_SHADOW_PROPERTY
  • RCT_CUSTOM_SHADOW_PROPERTY

如何在 React Native 中响应 iOS 的事件

要想在 React Native 中响应 iOS 的事件,只需要暴露用 RCTBubblingEventBlockRCTDirectEventBlock 定义的属性即可,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// RNTMapView.h
#import <MapKit/MapKit.h>
#import <React/RCTComponent.h>

// 由于 MKMapView 没有任何 `RCTBubblingEventBlock` 或 `RCTDirectEventBlock` 所定义的
// 属性,所以这里需要定义 MKMapView 的子类 RNTMapView
@interface RNTMapView: MKMapView

// 需要暴露给 React Native 的事件属性
@property (nonatomic, copy) RCTBubblingEventBlock onRegionChange;

@end


// RNTMapView.m
#import "RNTMapView.h"

@implementation RNTMapView

@end

然后我们需要在 RCTViewManager 中暴露 onRegionChange 给 React Native 使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// RNTMapManager.m
#import <MapKit/MapKit.h>
#import <React/RCTViewManager.h>

#import "RNTMapView.h"

@interface RNTMapManager : RCTViewManager <MKMapViewDelegate>
@end

@implementation RNTMapManager

RCT_EXPORT_MODULE()
// 暴露 RNTMapView 中的 `onRegionChange` 属性
RCT_EXPORT_VIEW_PROPERTY(onRegionChange, RCTBubblingEventBlock)

- (UIView *)view {
// 这里需要返回 RNTMapView 而不是 MKMapView
return [RNTMapView new];
}

@end

重要的事说三遍:
使用 RCTBubblingEventBlockRCTDirectEventBlock 所定义的事件都必须加上前缀 on,否则 React Native 无法接收到事件
使用 RCTBubblingEventBlockRCTDirectEventBlock 所定义的事件都必须加上前缀 on,否则 React Native 无法接收到事件
使用 RCTBubblingEventBlockRCTDirectEventBlock 所定义的事件都必须加上前缀 on,否则 React Native 无法接收到事件

0%