普通视图
Codable编解码数据时的一些边界问题
Combine:订阅、绑定和内存管理
Combine:错误处理
Combine:核心概念
在上一篇概览 中说过,Combine有三个核心概念:Publisher、Operator和Subscriber。Publisher 和 Subscriber 分别代表事件的发布者和订阅者,Operator兼具两者的特性,它同时遵循Subscriber和 Publisher协议,用来对上游数据进行操作。
只要理解了这三个核心概念,你就可以很好的使用Combine,所以从这个角度来说,我们可以将Combine简单的理解为下面的形式:
Combine = Publishers + Operators + Subscribers
Publisher
定义:
public protocol Publisher<Output, Failure> {
associatedtype Output
associatedtype Failure : Error
func receive<S>(subscriber: S) where S : Subscriber, Self.Failure == S.Failure, Self.Output == S.Input
}
publisher被订阅后,会根据subscriber的请求提供数据,一个没有任何subscriber的publisher不会发出任何数据。
Publisher可以发布三种事件:
- Output:事件流中出现的新值
- Finished: 事件流中所有元素发布结束,事件流完成使命并终结
- Failure: 事件流中发生了错误,事件流到此终结
Finished和Failure事件被定义在Subscribers.Completion中
extension Subscribers {
@frozen public enum Completion<Failure> where Failure : Error {
case finished
case failure(Failure)
}
}
三种事件都不是必须的。Publisher 可能会发出一个或多个 output 值,也可能不发出任何值;它可能永远不会停止,也可能会通过发出failure 或 finished 事件来表示终结。
最终将会终止的事件流被称为有限事件流,而不会发出 failure 或者 finished 的事件流则被称为无限事件流。比如,一次网络请求就是一个有限事件流,而某个按钮的点击事件流就是无限事件流。
Subject
Subject也是一个Publisher
public protocol Subject : AnyObject, Publisher {
func send(_ value: Self.Output)
func send(completion: Subscribers.Completion<Self.Failure>)
}
Subject 暴露了两个 send 方法,外部调用者可以通过这两个方法来主动地发布 output 值、failure 事件或 finished 事件。Subject可以将传统的指令式编程中的异步事件和信号转换到响应式的世界中去。
Combine内置了两种Subject类型:
-
PassthroughSubject简单地将 send 接收到的事件转发给下游的其他 Publisher 或 Subscriber,不会持有最新的output;如果在订阅前执行send操作,是无效的。
let publisher1 = PassthroughSubject<Int, Never>()
print("开始订阅")
publisher1.sink(
receiveCompletion: { complete in
print(complete)
},
receiveValue: { value in
print(value)
})
publisher1.send(1)
publisher1.send(2)
publisher1.send(completion: .finished)
// 输出:
// 开始订阅
// 1
// 2
// finished
调整一下 sink 订阅的时机,将它延后到 publisher.send(1) 之后,那么订阅者将会从 2 的事件开始进行响应:
let publisher2 = PassthroughSubject<Int, Never>()
publisher2.send(1)
print("开始订阅")
publisher2.sink(
receiveCompletion: { complete in
print(complete)
},
receiveValue: { value in
print(value)
})
publisher2.send(2)
publisher2.send(completion: .finished)
// 输出:
// 开始订阅
// 2
// finished
-
CurrentValueSubject会包装和持有一个值,并在设置该值时发送事件并保留新的值。在订阅发生的瞬间,会把当前保存的值发送给订阅者;接下来对值的每次设置都将触发订阅响应。
let publisher3 = CurrentValueSubject<Int, Never>(0)
print("开始订阅")
publisher3.sink(
receiveCompletion: { complete in
print(complete)
},
receiveValue: { value in
print(value)
})
publisher3.value = 1
publisher3.value = 2
publisher3.send(completion: .finished)
// 输出:
// 开始订阅
// 0
// 1
// 2
// finished
Subscriber
定义:
public protocol Subscriber<Input, Failure> : CustomCombineIdentifierConvertible {
associatedtype Input
associatedtype Failure : Error
func receive(subscription: Subscription)
func receive(_ input: Self.Input) -> Subscribers.Demand
func receive(completion: Subscribers.Completion<Self.Failure>)
}
想要订阅某个 Publisher,Subscriber 中的这两个类型必须与 Publisher 的 Output 和 Failure 一致。
Combine 中也定义了几个比较常见的 Subscriber,可以供我们直接使用。
sink
sink的完整函数签名为
func sink(receiveCompletion: @escaping ((Subscribers.Completion<Self.Failure>) -> Void), receiveValue: @escaping ((Self.Output) -> Void)) -> AnyCancellable
receiveCompletion 用来接收 failure 或者 finished 事件,receiveValue 用来接收 output 值。
let just = Just("Hello word!")
_ = just.sink(receiveCompletion: {
print("Received completion", $0)
}, receiveValue: {
print("Received value", $0)
})
如果说Subject提供了一条从指令式异步编程通向响应式世界的道路的话,那么sink就补全了另外一侧。sink可以作为响应式代码和基于闭包的指令式代码之间的桥梁,让你可以通过它从响应式的世界中回到指令式的世界。因为receiveValue闭包会将值带给你,想要对它做什么就随你愿意了。
assign
func assign<Root>(to keyPath: ReferenceWritableKeyPath<Root,Self.Output>, on object:Root) -> AnyCancellable
assign 接受一个 class 对象以及对象类型上的某个键路径 (key path)。每当 output 事件到来时,其中包含的值就将被设置到对应的属性上去。
定义一个MyObject类
class MyObject {
var value: String = "123" {
didSet {
print(value)
}
}
}
使用 assign(to:on:)修改MyObject实例对象属性的值
let obj = MyObject()
let _ = ["456"].publisher.assign(to: \.value, on: obj)
assign 还有一个变体, assign(to:) 可将 Publisher 发出的值用于 @Published 属性包装器包装过的属性
class MyObject {
@Published var value = 0
}
let objc = MyObject()
objc.$value.sink {
print($0)
}
(0 ..< 5).publisher.assign(to: &objc.$value)
value 属性用 @Published包装,除了可作为常规属性访问之外,它还为属性创建了一个 Publisher。使用 @Published 属性上的 $ 前缀来访问其底层 Publisher,订阅该 Publisher,并打印出收到的每个值。最后,我们创建一个 0..<5 的 Int Publisher 并将它发出的每个值 assign 给 object 的 value Publisher。 使用 & 来表示对属性的 inout 引用,这里的 inout 来源于函数签名:
func assign(to published: inout Published<Self.Output>.Publisher)
这里有一个值得注意的地方,如果使用 assign(to: .value, on: self) 并存储生成的 AnyCancellable,可能会引起引用循环:MyObject 类实例持有生成的 AnyCancellable,而生成的 AnyCancellable 同样保持对 MyObject 类实例的引用。因此,推荐使用 assign(to:) 来替代 assign(to:on:) ,以避免此问题的发生,因为assign(to:) ::不返回 AnyCancellable,在内部完成了生命周期的管理,在 @Published 属性释放时会取消订阅。::
Operator
关于Operator的介绍,在概览中已经做了相对详细的介绍。
Operator 可以作为上游 Publisher 的输入,同时它们也可以成为新的 Publisher,输出处理过的数据给下游。我们可以把不同的操作符组合起来形成一个处理链:当链条最上端的 Publisher 发布事件或数据时,链条内的 Operator 会对这些数据和事件一步一步地进行处理,最终达到 subscriber 指定的结果。
关于常用操作符,这篇文章 介绍的十分全面,可做参考。
参考
Combine: 概览
Combine: Debugging
使用 LLDB 提高开发中的调试效率
UITableView 性能优化
SwiftUI 学习之propertyWrapper
通过 Xcode 启动终端
使用过其他 IDE 的同学应该知道,大多数都集成了终端,而 Xcode 却没有。当我们要对一个项目执行命令行时,总要将项目拖到终端窗口中,这样很不方便。那如何使用 Xcode 一键启动终端并 cd 到当前项目目录呢?这篇文章就来介绍一种方案。
新建脚本文件:
编辑脚本文件:
#!/usr/bin/env bash
open -a iTerm "`pwd`"
脚本文件默认没有执行权限,启动终端,使用命令行
chmod +x <脚本路径>
赋予脚本权限。
在 Xcode → Preferences → Behaviors 中添加自定义 Open Terminal 选项,设置脚本路径及惯用快捷键
![]()
完成之后就可以在 Xcode中通过快捷键一键调起终端并 cd 到项目目录。
配置起来很简单,使用起来很划算哦😏
iOS 线程保活
- 小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
开发中,经常会遇到将耗时操作放到子线程中执行,来提升应用性能的场景。当子线程中的任务执行完毕后,线程就被立刻销毁。
如果开发中需要经常在子线程中执行任务,那么频繁的创建和销毁线程就会造成资源的浪费,这不符合我们的初衷。 此时就需要我们对线程进行保活,保证线程在应该处理事情的时候醒来,空闲的时候休眠。
我们知道 RunLoop 可以在需要处理事务的时候醒来执行任务,空闲的时候休眠来节省资源,利用这个特性就可以来处理线程的保活,控制线程的生命周期。
从探索到成功
- (void)viewDidLoad {
[super viewDidLoad];
LSThread *thread = [[LSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
[thread start];
}
- (void)run {
NSLog(@"func -- %s thread -- %@", __func__, [NSThread currentThread]);
[[NSRunLoop currentRunLoop] run];
NSLog(@"--- 结束 ---");
}
LSThread 继承自 NSThread ,重写了 dealloc 方法
- (void)dealloc {
NSLog(@"%s", __func__);
}
执行之后的结果:
![]()
可以看到线程没能保活:
- 虽然启动了 RunLoop,依然执行了下面的结束 log
- 线程在执行完毕之后被销毁了
为了保证线程执行完毕不被销毁,可以强引用线程
self.thread = [[LSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
[self.thread start];
但是这样并不能解决 RunLoop 问题。那么已经启动了 RunLoop,为什么并没有保持它的持续运行呢?
我们来看一下 run 方法的定义
If no input sources or timers are attached to the run loop, this method exits immediately.
意思是如果没有sources或timers附加到RunLoop,那么这个方法会立即退出。
那么我们给 RunLoop 添加一个 source 或者 timer 应该就可以解决这个问题了
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
再次运行并没有执行结束 log,线程保活成功。
![]()
下面继续来完善我们的需求。当持有线程的控制器销毁时,新建的子线程也应该跟着被销毁,在控制器里添加 dealloc
- (void)dealloc {
NSLog(@"--- 销毁控制器 --- %s", __func__);
}
在控制器出现时,创建子线程,控制器消失时控制台输出如下
![]()
控制器和子线程都没有被销毁。查看代码
self.thread = [[LSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
这里控制器强引用了 thread,thread 又在内部持有了控制器 self,造成了引用循环。
那么要打破这个引用循环可以使用 Block
self.thread = [[LSThread alloc] initWithBlock:^{
NSLog(@"func -- %s thread -- %@", __func__, [NSThread currentThread]);
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
NSLog(@"--- 结束 ---");
}];
执行结果
![]()
控制器被成功释放,但是子线程并没有被销毁,那么这个子线程变成了一个全局性质的。到这里就要说一下 RunLoop 的启动了。
RunLoop 有三种启动方式
- (void)run;
- (void)runUntilDate:(NSDate *)limitDate;
- (void)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;
run 方法内部会在NSDefaultRunLoopMode模式下重复调用runMode:beforeDate:方法。
runUntilDate: 方法可以设置超时时间,在超时时间到达之前,RunLoop会一直运行,在此期间RunLoop会处理来自sources的数据,并且 像 run 一样,也会在NSDefaultRunLoopMode模式下重复调用runMode:beforeDate:方法。
runMode:beforeDate: 方法RunLoop会运行一次,超时时间到达或者第一个source被处理,则RunLoop就会退出。
关于 run 方法 Apple 文档中有说如果希望退出 RunLoop,不应使用此方法。
如果RunLoop没有input sources或者附加的timer,RunLoop就会退出。虽然这样可以将RunLoop退出,但是Apple不建议我们这么做,系统内部有可能会在当前线程的RunLoop中添加一些输入源,所以通过手动移除input source或者timer这种方式,并不能保证RunLoop一定会退出。
那么问题就很明了了,我们不应该使用 run 方法来启动 RunLoop,因为它创建的是一个不会退出的循环,使用这个方法的子线程自然无法被销毁。我们可以像run 一样利用runMode:beforeDate: 方法来创建一个符合我们条件的子线程:
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
把它放到一个 while 循环中,利用一个是否停止 RunLoop 的全局标记来辅助处理线程的生命周期问题
__weak typeof(self) weakSelf = self;
self.thread = [[LSThread alloc] initWithBlock:^{
NSLog(@"func -- %s thread -- %@", __func__, [NSThread currentThread]);
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
// [[NSRunLoop currentRunLoop] run];
while (!weakSelf.isStopedThread) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
NSLog(@"--- 结束 ---");
}];
停止 RunLoop 的方法
- (void)stop {
self.isStopedThread = YES;
CFRunLoopStop(CFRunLoopGetCurrent());
}
这里有一点需要注意,停止操作一定要在我们的目标线程执行,比如我们直接调用 stop 方法并不能达到我们预期的效果,这是因为stop 默认在主线程执行,没有拿到目标线程,停止无效。
- (void)stopAction {
[self performSelector:@selector(stop) onThread:self.thread withObject:nil waitUntilDone:YES];
self.thread = nil;
}
在当前线程调用stop ,我们的目的就达到了,顺利的结束了 RunLoop,线程也跟着销毁了。
![]()
Flutter 生命周期
如果曾对 iOS 中的 ViewController 有过接触,那就很容易理解生命周期在 UI 绘制中的重要作用。Flutter 中也存在生命周期,它的回调方法都体现在 State 中,源码参考。
Flutter 的生命周期分为页面(Widget)和 APP 两块。理解Flutter生命周期, 对写出一个合理的 Widget 和一个健壮的 APP 至关重要。
页面的生命周期
以 StatefulWidget 为例,来看一下 Flutter 页面的生命周期是怎样的。 Widget 的生命周期大体上可以分为三个阶段:
1. 初始化
createState
//这个方法是必须重写的
@override
_LifecycleWidgetState createState() => _LifecycleWidgetState();
当构建一个 StatefulWidget 时这个方法会被首先调用,而且这个方法是必须要重写的。
initState
@override
void initState() {
super.initState();
}
这个方法调用发生在 createState之后,是除构造方法之外,调用的第一个方法,它的作用类似于 Android 的 onCreate()和 iOS 的 viewDidLoad()。这个方法中通常会做一些初始化工作,比如 channel 的初始化、监听器的初始化等。
与 dispose() 相对应。
2. 状态改变
-
didChangeDependencies@override void didChangeDependencies() { super.didChangeDependencies(); }这个方法要求必须要调用父类的方法
super.didChangeDependencies,当依赖的 State 的对象改变时会调用。- 在第一次构建 Widget 时,在
initState()之后立即调用此方法。 - 如果 StatefulWidgets 依赖于 InhertedWidget,那么当当前 State 所依赖 InheritedWidget 中的变量改变时会再次调用它。
- 在第一次构建 Widget 时,在
-
build
@override
Widget build(BuildContext context) {
return Container();
}
是一个必须实现的方法,在这里实现要呈现的页面内容。它会在didChangeDependencies()之后立即调用,另外当调用 setState() 后也会再次调用这个方法
didUpdateWidget
@override
void didUpdateWidget(covariant LifecycleWidget oldWidget) {
super.didUpdateWidget(oldWidget);
}
调用 setState 将 Widget 的状态改变时 didUpdateWidget 会被调用,Flutter 会创建一个新的 Widget 来绑定这个 State,并在这个方法中传递旧的 Widget ,因此如果想比对新旧 Widget 并且对 State 做一些调整,可以使用它。
另外如果某些 Widget 上涉及到 controller 的变更,那么一定要在这个回调方法中移除旧的 controller 并创建新的 controller 监听。
3. 销毁
-
deactivate@override void deactivate() { super.deactivate(); }这个方法不常用,它会在组件被移除时调用,而且是在
dispose调用之前 -
dispose@override void dispose() { super.dispose(); }与
initState()对应。组件销毁时调用,通常该方法中执行一些释放资源的工作,如监听器的移除,channel 的销毁等,相当于 iOS 的
dealloc。方法参考
App 的生命周期
App 中会有比如从前台切入到后台再从后台切回到前台的场景,在 iOS 中这些生命周期都可以在 AppDelegate 中被体现,那么 Flutter 中我们该如何处理这些场景?对于 App 级别的生命周期与上述 Widget 生命周期相比,稍有不同。
如果想监听 App 的生命周期需要使用 WidgetsBinding 来监听 WidgetsBindingObserver,WidgetsBindingObserver是一个 Widgets 绑定观察器,通过它来监听应用的生命周期,使用混入的方式绑定观察器,并且需要在 dispose 回调方法中移除这个监听。
重写 didChangeAppLifecycleState ,当生命周期发生变化的时候会回调这个方法,
class _LifecycleAPPState extends State<LifecycleAPP>
with WidgetsBindingObserver {
@override
void initState() {
//添加监听
WidgetsBinding.instance!.addObserver(this);
super.initState();
}
@override
Widget build(BuildContext context) {
return Container();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
print(state);
}
@override
void dispose() {
// 移除监听
WidgetsBinding.instance!.removeObserver(this);
super.dispose();
}
}
Flutter 封装了一个枚举 AppLifecycleState 来描述 APP 的生命周期:
enum AppLifecycleState {
resumed, // 进入前台
inactive, // app 处于非活跃状态,并且未接收到用户输入的时候调用。比如接听来电
paused,// 进入后台
detached, // app 仍寄存在Flutter引擎上,但与原生平台分离
}
我们可以拿到这个 state 在 didChangeAppLifecycleState()来做一些我们需要的处理逻辑。