一般在 Swift 中使用 泛型 的时候我们会这么写:
/// 类
class AClass<T> {}
/// 结构体
struct ASctuct<T> {}
/// 枚举
enum AEnum<T> {}
但是如果想在 协议 中使用泛型的时候这么写就会报错:
protocol AProtocol<T> {}
报错信息:
Protocols do not allow generic parameters; use associated types instead
虽然 泛型 可以在 类, 结构体, 枚举 中使用, 但是某些使用场景中, 如果在 协议 中加入 泛型 的话, 会使我们的代码更加灵活.
尽管 协议 中不支持 泛型, 但是却有个 associatedtype, 各种文章和书籍中都把它翻译为 关联类型. 我们可以使用 associatedtype 来达成 泛型 的目的.
正文
假设现在有如下 2 个接口:
/// 请求老师数据列表
/// - page: 分页页码
/// - limit: 分页页面容量
/// - return: 老师列表数据
[POST] https://example.com/teachlist
/// 请求老师所教授的科目
/// - id: 老师 id
/// - page: 分页页码
/// - limit: 分页页面容量
/// - return: 老师教授的科目数据列表
[POST] https://example.com/subjectlist
此处定义协议 PListable.
Parameters 为网络请求的参数类型, 由于其需要使用 JSONEncoder 对其进行编码, 因此需要实现 Encodable 协议.
Result 作为请求方法的返回类型, 由于需要使用 JSONDecoder 对请求到的 Data 进行解码, 因此需要实现 Decodable 协议.
requestURL 返回结果为网络请求的 URL 地址.
protocol PListable {
/// 参数类型
associatedtype Parameters: Encodable
/// 请求结果类型
associatedtype Result: Decodable
/// 请求地址
static var requestURL: URL? { get }
}
在协议的 extension 中实现了 static func plist(parameters: Parameters) -> Result? , 该方法为实现该协议的类型提供网络请求的功能实现.
extension PListable {
/// 分页的方式请求数据列表
/// - Parameter parameters: 参数对象
/// - Returns: 请求结果
static func plist(parameters: Parameters) -> Result? {
/*
网络请求代码
...
*/
/// 网络请求取到的数据
let data: Data = ...
/// 解析数据
return try? JSONDecoder().decode(Result.self, from: data)
}
}
此方法为了更加清晰的表达意图, 未使用 异步, 而是使用了 同步 的直接返回请求结果的写法.
如果了解 协程 的话, 应该就很容易理解这种写法了.
PLimit 结构为需要 page 和 limit 参数类型的接口提供参数. 依据 PListable 协议中 Parameters 的约束要求实现了 Encodable 协议.
struct PLimit: Encodable {
/// 分页页码
let page: Int
/// 分页数据容量
let limit: Int
}
PLimitWithId 结构对应的为需要 id, page, limit 参数类型的接口提供参数, 同样的实现了 Encodable 协议.
struct PLimitWithId: Encodable {
/// 数据查询依赖的 id
let id: Int
/// 分页页码
let page: Int
/// 分页数据容量
let limit: Int
}
Teacher 为接口 https://example.com/teachlist 返回的数据体部分的数据结构. 根据 PListable 协议中 Result 类型约束的要求实现了 Decodable 协议.
/// 老师对象
struct Teacher: Decodable {
/// 姓名
var name: String?
/// 教学科目列表
var subject: [Subject]?
}
Teacher 实现 PListable 协议, 并在 extension 中给 Parameters 类型关联为 PLimit, Result 类型关联为 [Teacher] 类型.
extension Teacher: PListable {
typealias Parameters = PLimit
typealias Result = [Teacher]
static var requestURL: URL? { URL(string: "http://example.com/teachlist") }
}
这样 Teacher 就可以调用 static func plist(parameters: Parameters) -> Result? 方法了, 并且其参数类型为 PLimit, 返回类型为 [Teacher] 返回一组 Teacher 类型的数据.
对应的, Subject 也与 Teacher 做相同的操作.
/// 科目对象
struct Subject: Decodable {
/// 科目名称
var name: String?
}
不同的是 Subject 中 Parameters 绑定为 PLimitWithId 类型, Result 绑定为 [Subject] 类型.
extension Subject: PListable {
typealias Parameters = PLimitWithId
typealias Result = [Subject]
static var requestURL: URL? { URL(string: "http://example.com/subjectlist") }
}
这样 Subject 就同样可以调用 static func plist(parameters: Parameters) -> Result? 方法了, 并且其参数类型为 PLimitWithId, 返回类型为 [Subject] 返回一组 Subject 类型的数据.
调用的代码如下:
Teacher.plist(parameters: PLimit(page: 0, limit: 20))
Subject.plist(parameters: PLimitWithId(id: 101, page: 0, limit: 20))
同时 protocol + associatedtype 还可以与 泛型 组合使用:
如果我们有如下 Animal 协议 和 结构体 Cat:
protocol Animal {
associatedtype `Type`
}
struct Cat<T> {}
extension Cat: Animal {
typealias `Type` = T
}
Cat 类型接收一个 T 类型的泛型, Cat 在实现 Animal 协议后, 可以把 T 设置为 Type 的关联类型.
虽然使用 class 的 继承 也能达到类似的效果, 但是 struct 和 enum 却不支持 继承.
通过 协议 任何实现 PListable 的类型都拥有了 分页获取数据 的能力.
在项目开发中我们往往可能还要有 Deleteable, Updateable … 等等诸多类型的接口, 如果我们都通过 protocol + associatedtype 的方式来为对应类型进行扩展, 不仅能够提升开发效率, 还能降低维护成本.
]]>目前的动态库很少,可以手动修改。如果动态库多,可以在Podfile里面添加下面的代码,然后执行pod install。
#填写不需要转换成静态库的动态库名字,这个需要我们手动排查。
dynamic_frameworks = ['AMSMB2','MJRefresh','IJKMediaFramework','UnrarKit']
post_install do |installer|
installer.pods_project.targets.each do |target|
if dynamic_frameworks.include?(target.name)
next
end
target.build_configurations.each do |config|
config.build_settings['MACH_O_TYPE'] = 'staticlib'
end
end
end
从Targets Support Files中找到Pods-NXPlayer-frameworks.sh脚本,把需要转换成静态库的行都注释掉。已经转换成静态库了,没有必要再往NXPlayer.app/Frameworks在拷贝一份。
if [[ "$CONFIGURATION" == "Debug" ]]; then
install_framework "${BUILT_PRODUCTS_DIR}/AMSMB2/AMSMB2.framework"
# install_framework "${BUILT_PRODUCTS_DIR}/Alamofire/Alamofire.framework"
# install_framework "${BUILT_PRODUCTS_DIR}/FilesProvider/FilesProvider.framework"
# install_framework "${BUILT_PRODUCTS_DIR}/GCDWebServer/GCDWebServer.framework"
# install_framework "${BUILT_PRODUCTS_DIR}/MBProgressHUD/MBProgressHUD.framework"
install_framework "${BUILT_PRODUCTS_DIR}/MJRefresh/MJRefresh.framework"
# install_framework "${BUILT_PRODUCTS_DIR}/PLzmaSDK/PLzmaSDK.framework"
# install_framework "${BUILT_PRODUCTS_DIR}/SQLite.swift/SQLite.framework"
# install_framework "${BUILT_PRODUCTS_DIR}/SSZipArchive/SSZipArchive.framework"
# install_framework "${BUILT_PRODUCTS_DIR}/SnapKit/SnapKit.framework"
install_framework "${BUILT_PRODUCTS_DIR}/UnrarKit/UnrarKit.framework"
fi
if [[ "$CONFIGURATION" == "Release" ]]; then
install_framework "${BUILT_PRODUCTS_DIR}/AMSMB2/AMSMB2.framework"
# install_framework "${BUILT_PRODUCTS_DIR}/Alamofire/Alamofire.framework"
# install_framework "${BUILT_PRODUCTS_DIR}/FilesProvider/FilesProvider.framework"
# install_framework "${BUILT_PRODUCTS_DIR}/GCDWebServer/GCDWebServer.framework"
# install_framework "${BUILT_PRODUCTS_DIR}/MBProgressHUD/MBProgressHUD.framework"
install_framework "${BUILT_PRODUCTS_DIR}/MJRefresh/MJRefresh.framework"
# install_framework "${BUILT_PRODUCTS_DIR}/PLzmaSDK/PLzmaSDK.framework"
# install_framework "${BUILT_PRODUCTS_DIR}/SQLite.swift/SQLite.framework"
# install_framework "${BUILT_PRODUCTS_DIR}/SSZipArchive/SSZipArchive.framework"
# install_framework "${BUILT_PRODUCTS_DIR}/SnapKit/SnapKit.framework"
install_framework "${BUILT_PRODUCTS_DIR}/UnrarKit/UnrarKit.framework"
fi
网络上关于冷启动和热启动的讨论很多,App要在冷启动的情况下,测试时间才是准确的。测试不能以一次时间为准,要多几次并取平均值。具体如下:
每测试完一次需要:卸载App,退出Instruments,退出Xcode。
再次测试需要:打开Xcode,按快捷键command + i,会自动安装App并启动Instruments,点击App Launch进行测试。
本文在转静态库之前进行了6次,总耗时12.35秒;转静态库之后进行了6次,总耗时9.112秒。时间虽然相差很少,但也算是优化了启动时间。
1.使用协议
将超类定义为协议而不是类
Pro:编译时检查每个“子类”(不是实际的子类)是否实现了所需的方法
Con:“超类”(协议)不能实现方法或属性
2.在方法的超级版本中断言
示例:
class SuperClass {
func someFunc() {
fatalError("Must Override")
}
}
class Subclass : SuperClass {
override func someFunc() {
}
}
Pro:可以在超类中实现方法和属性
Con:无编译时检查
下面的代码允许从类继承,还允许对协议的编译时进行检查:)
protocol ViewControllerProtocol {
func setupViews()
func setupConstraints()
}
typealias ViewController = ViewControllerClass & ViewControllerProtocol
class ViewControllerClass : UIViewController {
override func viewDidLoad() {
self.setup()
}
func setup() {
guard let controller = self as? ViewController else {
return
}
controller.setupViews()
controller.setupConstraints()
}
//.... and implement methods related to UIViewController at will
}
class SubClass : ViewController {
//-- in case these aren't here... an error will be presented
func setupViews() { ... }
func setupConstraints() { ... }
}
class SuperClass {}
protocol SuperClassProtocol {
func someFunc()
}
typealias SuperClassType = SuperClass & SuperClassProtocol
class Subclass: SuperClassType {
func someFunc() {
// ...
}
}
protocol SomeProtocol {
func someMethod()
}
class SomeClass: SomeProtocol {
func someMethod() {}
}
open class SuperClass {
private let abstractFunction: (SuperClass) -> Void
public init(abstractFunction: @escaping (SuperClass) -> Void) {
self.abstractFunction = abstractFunction
}
public func foo() {
// ...
abstractFunction(self)
}
}
public class SubClass: SuperClass {
public init() {
super.init(
abstractFunction: {
(_self: SuperClass) in
let _self: SubClass = _self as! SubClass
print("my implementation")
}
)
}
}
]]>#import <UIKit/UIKit.h>
#import <objc/runtime.h>
NS_ASSUME_NONNULL_BEGIN
__attribute__((objc_subclassing_restricted))//禁止该类被继承
@interface AAA : UIView
-(void)mustUseMethod;
@end
NS_ASSUME_NONNULL_END
2.提示子类必须调用父类方法
objc_requires_super 在父类的方法后面添加,那么子类调用该方法必须实现[super thisMethod],否则会黄色警告
@interface Father : NSObject
- (void)mustUseMethod __attribute__((objc_requires_super));
@end
@interface Child : TestObject
@end
@implementation BBB
-(void)mustUseMethod{
//[super mustUseMethod];
警告信息:(不报错)
Method possibly missing a [super mustUseMethod] call
}
@end
**3 constructor / destructor **
Objective-C最后还是转译成C语言,当然还有constructor / destructor
加上这两个属性的函数会在分别在可执行文件(或 shared library)load 和 unload 时被调用,可以理解为在 main()函数调用前和 return 后执行
实际上constructor会在+load之后执行
因为 dyld(动态链接器)最开始会先通知 objc runtime 去加载其中所有的类,每加载一个类时,它的 +load 随之调用,全部加载完成后,dyld 才会调用所有的 constructor 方法
__attribute__((constructor)) static void beforeMain() {
NSLog(@"before main");
}
__attribute__((destructor)) static void afterMain() {
NSLog(@"after main");
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"execute main");
}
return 0;
}
执行结果:
debug-objc[23391:1143291] before main
debug-objc[23391:1143291] execute main
debug-objc[23391:1143291] after main
constructor后边添加设置优先级,控制执行顺序
__attribute__((constructor(101))) static void beforeMain() { NSLog(@"before main");
}
__attribute__((constructor(100))) static void beforeMain1() { NSLog(@"before main1");
}
__attribute__((destructor)) static void afterMain() { NSLog(@"after main");
}
执行结果:
2019-10-12 15:48:24.015908+0800 Food[4561:144694] before main1
2019-10-12 15:48:24.016491+0800 Food[4561:144694] before main
2019-10-12 15:48:24.017145+0800 Food[4561:144694] execute main
4 overloadable 可以允许同名函数的产生
__attribute__((overloadable)) void testMethod(int age) {NSLog(@"%@", @(age));};
__attribute__((overloadable)) void testMethod(NSString *name) {NSLog(@"%@", name);};
__attribute__((overloadable)) void testMethod(BOOL gender) {NSLog(@"%@", @(gender));};
int main(int argc, char * argv[]) {
@autoreleasepool {
testMethod(18);
testMethod(@"lxz");
testMethod(YES);
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
执行结果:
2019-10-12 16:01:04.491366+0800 Food[4724:150619] 18
2019-10-12 16:01:04.491510+0800 Food[4724:150619] lxz
2019-10-12 16:01:04.491632+0800 Food[4724:150619] 1
5.objc_runtime_name属性可以在编译时,将Class或者Protocol指定为另一个名字
__attribute__((objc_runtime_name("TestObject")))
@interface AAA : NSObject
@end
int main(int argc, char * argv[]) {
@autoreleasepool {
NSLog(@"--%@",NSStringFromClass([AAA class]));
NSLog(@"----%@",NSClassFromString(@"TestObject"));
}
}
6.通过cleanup属性,可以指定给一个变量,当变量释放之前执行一个函数。指定的函数执行时间,是在dealloc之前。在指定的函数中,可以传入一个形参,参数就是cleanup修饰的变量,形参是一个地址。
static void releaseBefore(NSObject **object) {
NSLog(@"AAA-------%@", *object);
}
int main(int argc, char * argv[]) {
@autoreleasepool {
{
AAA *object __attribute__((cleanup(releaseBefore))) = [AAA new];
}
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
执行结果:
2019-10-12 16:20:24.230282+0800 Food[5057:162194] AAA-------<AAA: 0x6000021c81d0>
2019-10-12 16:20:24.230633+0800 Food[5057:162194] AAA-dealloc
7.如果某个变量未使用,会提示unused xxx,可以通过unused消除这个警告
int main(int argc, char * argv[]) {
@autoreleasepool {
AAA *object __attribute__((unused)) = [AAA new];
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
]]>每个团队都应该有统一的代码风格和规范,这带来的好处我相信不言而喻,具体我就不多说了,大家都懂的😁。如何更有效率的去做这件事呢,我这次就来说说如何更好的自动格式化你的代码。
大多数 iOS 开发者应该都知道 Xcode 的插件 Clang Format,它是基于 clang-format 命令行工具的一个 Xcode 插件,但是这款插件在Xcode9上已经无法使用了,因为Xcode9的插件机制已经变了。
现在可以使用这一款XcodeClangFormat,具体的使用方式点击链接,大家自行去看吧。这款有个缺点,就是不能像之前那款插件可以设置在保存时自动格式化(这其实也不能怪作者,Xcode新的机制不允许)。 不过使用这种插件还是不够方便,你还得手动选中文件或者代码再按快捷键来格式化,很容易忘,而导致把不规范的代码直接提交到仓库里了。
那么有没有一种方式,可以让我在敲代码的时候随心所欲,提交时又能提醒我然后自动帮我格式化吗?
这里我直接介绍一款神器Space Commander,它利用 Git Hooks ,在 commit 之前检查代码风格是否符合规范,只有符合规范的代码才允许提交,列出不符合规范的文件,同时提供 Shell 脚本来自动格式化。接下来我介绍下如何使用。
git clone https://github.com/square/spacecommander.git
BasedOnStyle: Chromium
IndentWidth: 4
AlignConsecutiveAssignments: true
AlignConsecutiveDeclarations: true
ObjCSpaceAfterProperty: true
PointerAlignment: Right
BreakBeforeBraces: Attach
#import "ViewController.h"
@interface ViewController ()
@property(nonatomic, copy) NSString* p;
@property(nonatomic, strong) UITextView * textview;
@end
@implementation ViewController
-(void)formatTest:(NSString *)param{
if (param) {
NSLog(@"sss");
}
int a=0;
int b = 1;
int c= 2;
NSLog(@"%d%d%d",a,b,c);
}
-(void)viewDidLoad{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
}
-(void)viewDidAppear:(BOOL)animated {
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
@end
复制代码
这一看,就很不规范吧,先让我们提交看看
提交的时候明确提示ViewController文件需要格式化,这时候我们可以使用format-objc-file.sh脚本单独格式化某个文件,也可以format-objc-files.sh格式化所有的暂存文件,甚至使用format-objc-files-in-repo.sh格式化整个仓库的文件。
再提交一遍
好,接下来我们在看看代码变成什么样子了
#import "ViewController.h"
@interface ViewController ()
@property (nonatomic, copy) NSString * p;
@property (nonatomic, strong) UITextView *textview;
@end
@implementation ViewController
- (void)formatTest:(NSString *)param {
if (param) {
NSLog(@"sss");
}
int a = 0;
int b = 1;
int c = 2;
NSLog(@"%d%d%d", a, b, c);
}
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
}
- (void)viewDidAppear:(BOOL)animated {
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
@end
复制代码
完美!😝
可以看到上面的命令太长了,必须要指定shell脚本的全路径才能执行,我们可以简化命令,如果你用的zsh,去修改 ~/.zshrc,如果是bash,则修改~/.bash_profile,
// 初始化
alias clangformatsetup="/你自己的路径/spacecommander/setup-repo.sh"
// 格式化对应文件
alias clangformatfile="/你自己的路径/spacecommander/format-objc-file.sh"
// 格式化所有暂存文件
alias clangformatfiles="/你自己的路径/spacecommander/format-objc-files.sh"
// 格式化整个仓库
alias clangformatall="/你自己的路径/spacecommander/format-objc-files-in-repo.sh
复制代码
如果你还想知道更多的用法,直接去spacecommander的github主页查看。
这是我第一次在掘金上写文章(别的地方也没写过多少😂),写的不好,大家海涵呐,多多提意见哈😁。
]]>目前的 CocoaPod 私服,很多公司使用 Git 仓库进行搭建,这导致的问题是,CocoaPod 的构建产出物通常较大,上传到 Git 仓库时,会导致 Git 仓库持续增大, Git Clone 的速度大大降低,进而导致软件部署,交付的时间变长,影响了研发上线的效率。
不仅如此,您可能还需要为安卓的开发者搭建 Gradle 仓库,Java 开发者搭建 Maven 私服,容器团队搭建 Docker 私服,各个私服独立维护,占用大量系统资源,维护成本呈几何指数增长。
JFrog Artifactory 能够解决这个问题,通过搭建 Artifactory,能够在内网建立统一全语言的私有制品仓库,支持 CocoaPod,Gradle,Maven,Docker 等等。程序员通过 Artifactory 可以实现全语言的依赖下载,并且可以将构建产出物上传到 Artifactory 进行管理。
下载 Artifactory
获得 Artifactory 的安装文件很简单,访问https://jfrog.com/download-artifactory-pro/, 然后在http://www.jfrogchina.com/artifactory/free-trial/ 申请免费试用版 License 即可。可以用 StandAlone 方案安装,无需配置数据库即可使用。也支持 RPM,Debian,Docker 的安装方式。
创建 CocoaPod 仓库
解压下载的安装包后,进入 bin 目录,执行 artifactory.sh文件,随后访问 localhost:8081即可进入 Artifactory 页面:
输入 License 信息,即可开始使用 Artifactory。创建仓库时,选择 CocoaPod:
在此,我们创建两个仓库,一个是 CocoaPod Local,目的是存储所有本地的CocoaPod 构建产出物,另一个是 CocoaPod Remote,能够作为外网 CocoaPod 源的本地代理,在内网提供服务。
在使用 CocoaPod 仓库之前,需要安装 cocoapod-art 插件:gem install cocoapod-art。安装完之后,选择 CocoaPod Remote 仓库,右上角点击 Set Me Up,会弹出如下对话框:
该对话框里会提示如何使用该仓库,包括如何安装 cocoapod-art 插件,如何在 pod 添加 Artifactory 作为源:
然后将Podfile 中添加该源作为 pod 的依赖解析源:
下载依赖,上传构建包到 Artifactory
完成之前步骤之后,再执行 pod install 的时候,可以看到依赖已经被缓存在远程仓库:
在打包 CocoaPod 项目时,我们执行 pod spec create jfrogapp,并且通过 JFrog 的Rest API 上传到 CocoaPod Local 仓库,供后面的测试,运维团队使用该构建包。
上传完成之后,可以看到构建的 tar 包已经被存储到 Artifactory 的 local 仓库,而不需要存储到 Git 仓库。
除了全语言的包管理支持,Artifactory 还支持构建包的元数据和漏洞扫描。通过元数据的能力,能够展示包相关的生命周期数据信息,例如需求 ID,和单元测试覆盖率,通过率等等指标。
总结
通过Artifactory CocoaPod 仓库的使用,能够快速在公司内网搭建一套 CocoaPod 私服,既可以代理外网依赖,也可以作为本地私服存储构建包,并且记录该构建包管理的需求 ID,单元测试,性能测试等结果,Artifactory 企业版也支持高可用架构的搭建,实现0宕机的私服服务,更重要的是您也可以将 Maven,Docker,NPM 等30多种语言包都存储在 Artifactory 进行全公司统一管理,标准化交付流水线,提高软件交付的速度。
试用 JFrog Artifactory 地址:
]]>需要在Xcode的 Targets->Signing & Capabilities
勾选 Automatically manage signing。用这种方式,所有的工作包括AppId、证书、描述文件(Provisioning Profile)的创建都由Xcode包办了,非常的方便。
这种方式对个人开发者非常友好,但是对团队开发来说有比较大的弊端,具体如下。
如图所示:
比如说,添加新设备后,如果描述文件配置里面没有勾选这个设备,这个设备是无法安装我们的应用的,所有每次都需要确保描述文件的Select All是否已勾选。
首先,需要某个团队成员先在 Apple Developer 后台 分别创建开发环境和生产环境的证书和配置文件,然后将这些文件下载安装到本地。当其他人参与开发时,需要这个人将相关的文件导出给其他人。
然后需要在 Xcode上 取消勾选 Automatically manage signing
,同时设置对应的证书和描述文件(Provisioning Profile)。
这种方式的优点是所有开发人员都共用一份证书和描述文件,缺点也非常明显:每次证书过期或者添加新的设备后,都需要手动去更新,然后重新分发其他人,操作起来非常麻烦。
那么,有没有这么一个方案:在一个公共的地方存取这些证书和配置文件,自动化去处理整个流程呢?这就是我们今天要讲的 match 工具。
match 是 fastlane 工具套装其中的一个工具,它是 codesigning.guide 概念的实现。 它提供了一种全新的管理证书的方式,使团队所有成员共享一份代码签名,以减免不必要的证书创建、配置文件失效等问题。
下面我将详细介绍 match 的使用流程。
创建一个私有git仓库来存储证书和描述文件。建议在git账号中配置好SSH Key,这样就可以省去身份校验这一步。至于如何配置SSH Key,请参考:使用 SSH 连接到 GitHub。
另外,当有多个App时,建议一个git分支对应一个App,这样,我们所有App的证书都在一个仓库里面,便于管理。
在终端定位到项目目录,注意先看项目目录下是否存在fastlane目录,如果没有的话,先执行 fastlane init
命令来初始化fastlane服务(后面会用到)。然后再执行 fastlane match init
命令,首先会提示让你选择存储方式,我们选git,然后再输入git仓库地址,最后会生成一个 Matchfile 的配置文件。接下来,我们修改一下 Matchfile。
git_url("git@github.com:YourUserName/certificates.git") # git仓库地址
storage_mode("git") # 存储方式
git_branch("app1") # git分支名称,暂时以app名称作为分支名
# 默认的Profile类型, 可以为: appstore, adhoc, enterprise or development
type("development")
# bundleId,可以填多个bundleId,如App内包含 extension
app_identifier(["tools.fastlane.app", "tools.fastlane.app2"])
ENV["MATCH_PASSWORD"] = "your match password" # 导出和打开 .p12文件的密码
# username("user@fastlane.tools") # Your Apple Developer Portal username
# For all available options run `fastlane match --help`
# Remove the # in the beginning of the line to enable the other options
# The docs are available on https://docs.fastlane.tools/actions/match
复制代码
配置完 Matchfile 后,大部分教程都是让你通过以下三条命令来同步证书和配置文件。
fastlane match development
fastlane match adhoc
fastlane match appstore
复制代码
但是这种方式使用起来非常不方便,特别是用于自动化构建脚本,因为每次都要输入AppleId和密码;有的团队的做法是在Matchfile配置一个公用的AppleId,这样就不用每次输入账户和密码。
git_url("git@github.com:YourUserName/certificates.git") # git仓库地址
# 省略其他内容...
username("APPLE_ID") # 公用的AppleId
ENV["FASTLANE_PASSWORD"] = "AppleId密码"
复制代码
但是,还会遇到另外一个问题,需要进行双重验证,要求输入6位验证码,所以我个人更推荐用 App Store Connect API 的方式,下面我将对这种方式做详细介绍。
App Store Connect API 是官方提供的一套 REST API,可让您在 App Store Connect 中执行的操作自动化。它主要提供了以下功能(包含了证书和描述文件的管理):
为什么要使用 App Store Connect API?
因为如果按照常规的方式的话,需要先用账号密码登录,登录过程中还要做双重验证,非常的不方便。 你可能会说 fastlane 不是还有一个强大的 spaceship 工具么,但是它还是无法绕过双重验证这个流程。具体可以看下这个文章:Spaceship VS App Store Connect API,这里就不做详细介绍了。
而 App Store Connect API 是通过 JSON Web Tokens (JWT) 进行授权,无需登录开发者账号,也无需做双重验证,非常适合在脚本内做自动化操作。
fastlane 内部也集成了 App Store Connect API,它那边也是推荐使用 API key的方式进行身份验证,具体请参考文档:Using App Store Connect API。
创建 App Store Connect API 密钥
用有管理员权限的AppleID登录 AppStoreConnect 后台,选择 用户和访问->秘钥,点击添加按钮来生成API秘钥。
然后下载API秘钥(一个.p8文件),保存到项目的fastlane目录。注意:私钥只能下载一次,永远不会过期,保管好,如果丢失了,去App Store Connect后台撤销密钥,否则别人拿到也可以用。
为了更加方便使用,我们通过 fastlane 来配置几个常用的命令,将以下内容添加到你的 fastlane目录下的 Fastfile 文件中:
# 定义一个全局变量api_key,下面都会要用到这个 api_key
# key_id 和 issuer_id 都可以在 AppStoreConnect后台 -> 用户和访问 -> 秘钥 这里找到
api_key = app_store_connect_api_key(
key_id: "D383SF739",
issuer_id: "6053b7fe-68a8-4acb-89be-165aa6465141",
key_filepath: "./AuthKey_D383SF739.p8", # 上面下载的p8文件路径
duration: 1200, # optional (maximum 1200)
in_house: false # optional but may be required if using match/sigh
)
desc "下载所有需要的证书和描述文件到本地,不会重新创建证书和描述文件(只读方式)"
lane :match_all do
match(api_key: api_key, type: "development", readonly: true)
match(api_key: api_key, type: "adhoc", readonly: true)
match(api_key: api_key, type: "appstore", readonly: true)
end
desc "同步证书,如果证书过期或新增了设备,会重新创建证书和描述文件"
desc "该方法仅限管理员使用,其他开发成员只需要使用 match_all 方法即可"
lane :force_match do
match(api_key: api_key, type: "development", force_for_new_devices: true)
match(api_key: api_key, type: "adhoc", force_for_new_devices: true)
match(api_key: api_key, type: "appstore")
end
desc "注册设备,并更新描述文件"
lane :sync_devices do
# devices.txt模板:
# http://devimages.apple.com/downloads/devices/Multiple-Upload-Samples.zip
register_devices(api_key: api_key, devices_file: "./devices.txt")
match(api_key: api_key, type: "development", force_for_new_devices: true)
match(api_key: api_key, type: "adhoc", force_for_new_devices: true)
end
# 构建测试包
lane :beta do
# 先同步adhoc证书和描述文件
match(api_key: api_key, type: "adhoc", readonly: true)
# 省略其他步骤...
build_app(scheme: "MyApp",
workspace: "Example.xcworkspace",
include_bitcode: true)
end
lane :release do
# 先同步appstore证书和描述文件
match(api_key: api_key, type: "appstore", readonly: true)
# 省略其他步骤...
build_app(scheme: "MyApp")
# 上传应用到AppStore
upload_to_app_store(
api_key: api_key,
force: true, # Skip HTMl report verification
skip_screenshots: true,
skip_metadata: true,
submit_for_review: false,
)
end
复制代码
通过上面这个模板我定义了以下几个常用的命令:
fastlane match_all
:下载所有需要的证书和描述文件到本地,不会重新创建证书和描述文件(只读方式)fastlane force_match
:强制同步证书和描述文件,如果证书过期或新增了设备,会重新创建证书和描述文件sync_devices
:注册设备,会同步更新描述文件,需要先在 devices.txt 文件录入新增的设备UDID。fastlane beta
:构建测试包,先通过 match 确保adhoc证书和描述文件都是最新且有效的fastlane release
:构建且上传到AppStore,先通过 match 确保 appstore证书和描述文件都是最新且有效的。当我们有新同事入职,或者需要在新的电脑上配置开发证书和描述文件,我们仅仅只需要一条 fastlane match_all
命令即可。
很少会有这种需求,如果确实需要清空所有证书和描述文件的话,可以通过 fastlane match_nuke
工具来处理:
desc "清空所有的证书和描述文件,慎用"
lane :nuke_all do
match_nuke(api_key: api_key, type: "development")
match_nuke(api_key: api_key, type: "adhoc")
match_nuke(api_key: api_key, type: "appstore")
end
复制代码
注意:清空完所有的证书和描述文件后,已安装的测试包是无法使用的,谨慎使用。
1. 通过Xcode来查看
先通过USB在电脑上连接iOS设备,然后在Xcode中打开菜单:Window -> Device and Simulators,上面显示的 Identifier 这一项就是我们所需要的设备UDID。
2. 通过第三方工具
如果当前无法使用Xcode来查看,如其他地区的同事,可以使用第三方工具。大部分提供应用分发的平台都支持获取UDID,如 Fir获取UDID、蒲公英-快速获取iOS设备的UDID。
原理:描述文件里面包含了所有的支持安装的设备的UDID,所以我们只需要看描述文件里面是否包含该设备的UDID就行了。在我们构建IPA包时,里面会嵌入一个叫 embedded.mobileprovision 文件(其实就是描述文件),判断我们的设备UDID是否在包含这个文件中,就能判断是否能安装(当然这只是其中的一个条件,其他的没在本文范围内,不做过多介绍)。
但是,这个文件是无法直接打开查看的,因为它经过了特殊的编码,其实质是一个plist文件,我们可以通过以下方式来查看它:
1. 通过 security 命名解码查看
security cms -D -i embedded.mobileprovision > result.plist
open result.plist
复制代码
2. 使用预览插件 ProvisionQL 查看
可以通过 brew 来安装 ProvisionQL,安装命令为: brew install --cask provisionql
。 在文件扩展名为 .ipa
、 .xcarchive
或 .mobileprovision
上可通过空格键来快速预览。
类型 | 作用 |
---|---|
环境光传感器 | 感应光照强度 |
距离传感器 | 感应靠近设备屏幕的物体 |
磁力计传感器 | 感应周边磁场 |
内部温度传感器 | 感应设备内部温度(非公开) |
湿度传感器 | 感应设备是否进水(非微电子传感器) |
陀螺仪 | 感应持握方式 |
加速计 | 感应设备运动 |
其中陀螺仪、加速计和磁力计的数据获取均依赖于 CMMotionManager
。
CMMotionManager 是 Core Motion 库的核心类,负责获取和处理手机的运动信息,它可以获取的数据有
CMMotionManager 有 “push” 和 “pull” 两种方式获取数据,push 方式实时获取数据,采样频率高,pull 方式仅在需要数据时采集数据,Apple 更加推荐这种方式获取数据。
将 CMMotionManager 采集频率 interval 设置好以后,CMMotionManager 会在一个操作队列里从特定的 block 返回实时数据更新,这里以设备运动数据 DeviceMotion 为例,代码如下
CMMotionManager *motionManager = [[CMMotionManager alloc] init];
motionManager.deviceMotionUpdateInterval = 1/15.0;
if (motionManager.deviceMotionAvailable) {
[motionManager startDeviceMotionUpdatesToQueue:[NSOperationQueue currentQueue]
withHandler: ^(CMDeviceMotion *motion, NSError *error){
double x = motion.gravity.x;
double y = motion.gravity.y;
double z = motion.gravity.z;
//NSLog(@"roll:%f, pitch:%f, yew:%f", motion.attitude.roll, motion.attitude.pitch, motion.attitude.yaw);
NSLog(@"x:%f, y:%f, z:%f", x, y, z);
}];
}
首先要注意尽可能在 app 中只创建一个 CMMotionManager 对象,多个 CMMotionManager 对象会影响从加速计和陀螺仪接受数据的速率。其次,在启动接收设备传感器信息前要检查传感器是否硬件可达,可以用
deviceMotionAvailable 检测硬件是否正常,用 deviceMotionActive 检测当前 CMMotionManager 是否正在提供数据更新。
暂停更新也很容易,直接调用 stopXXXUpdates 即可。
仍以 DevideMotion 为例,pull 方式代码如下
CMMotionManager *motionManager = [[CMMotionManager alloc] init];
motionManager.deviceMotionUpdateInterval = 1/15.0;
if (motionManager.deviceMotionAvailable) {
[motionManager startDeviceMotionUpdates];
double x = motionManager.deviceMotion.gravity.x;
double y = motionManager.deviceMotion.gravity.y;
double z = motionManager.deviceMotion.gravity.z;
NSLog(@"x:%f, y:%f, z:%f", x, y, z);
}
但是这样的方式获取的数据实时性不高,第一次获取可能没有数据,同时要注意不能过于频繁的获取,否则可能引起崩溃。
下面是 CMMotionManager 监听的各类运动信息的简单描述。首先需要明确,iOS 设备的运动传感器使用了如下的坐标系
image
而 DeviceMotion 信息具体对应 iOS 中的 CMDeviceMotion 类,它包含的数据有
attitude 用于标识空间位置的欧拉角(roll、yaw、pitch)和四元数(quaternion)
CMDeviceMotion.attitude
属性是CMAttitude类型,表示设备的空间姿态。CMAttitude包含pitch、roll、yaw信息(以弧度为单位):
其中绕 x 轴运动称作 pitch(俯仰),绕 y 轴运动称作 roll(滚转),绕 z 轴运动称作 yaw(偏航)。
当设备正面向上、顶部指向正北、水平放置时,pitch、yaw 和 roll 值均为 0,其他变化如下
rotationRate 标识设备旋转速率,具体变化如下
gravity 用于标识重力在设备各个方向的分量,具体值的变化遵循如下规律:重力方向始终指向地球,而在设备的三个方向上有不同分量,最大可达 1.0,最小是 0.0。
userAcceleration 用于标识设备各个方向上的加速度,注意是加速度值,可以标识当前设备正在当前方向上减速 or 加速。
magneticField 用于标识设备周围的磁场范围和精度,heading 用于标识北极方向。但是要注意,这两个值的检测需要指定 ReferenceFrame,它是一个 CMAttitudeReferenceFrame 的枚举,有四个值
其中前两个 frame 下磁性返回非法负值,只有选择了 CMAttitudeReferenceFrameXMagneticNorthZVertical 或 CMAttitudeReferenceFrameXTrueNorthZVertical 才有有效值,这两个枚举分别指代磁性北极和地理北极。
距离传感器可以检测有物理在靠近或者远离屏幕,使用如下
[UIDevice currentDevice].proximityMonitoringEnabled = YES;
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(proximityStateDidChange:) name:UIDeviceProximityStateDidChangeNotification object:nil];
- (void)proximityStateDidChange:(NSNotification *)note
{
if ([UIDevice currentDevice].proximityState) {
NSLog(@"Coming");
} else {
NSLog(@"Leaving");
}
}
目前没有找到相应的 API,可以采取的思路是通过摄像头获取每一帧,进行光线强度检测
NSData *imageData = [AVCaptureStillImageOutput jpegStillImageNSDataRepresentation:imageDataSampleBuffer];
CFDictionaryRef metadataDict = CMCopyDictionaryOfAttachments(NULL, imageDataSampleBuffer, kCMAttachmentMode_ShouldPropagate);
NSDictionary *metadata = [[NSDictionary alloc] initWithDictionary:(__bridge NSDictionary*)metadataDict];
CFRelease(metadataDict);
NSDictionary *exifMetadata = [[metadata objectForKey:(NSString *) kCGImagePropertyExifDictionary] mutableCopy];
float brightnessValue = [[exifMetadata objectForKey:(NSString *) kCGImagePropertyExifBrightnessValue] floatValue];
NSLog(@"%f",brightnessValue);
]]>flutter
可以native之间可以通过Platform Channels APIs进行通信,API主要有以下三种:
BasicMessageChannel用于在flutter和native
互相发送消息,一方给另一方发送消息,收到消息之后给出回复。照例我们先看一下API的基本使用流程,然后再看代码实现
流程也是一样的,只是将[flutter]与[native]反调
flutter需要完成以下工作
相对与其他Channel类型的创建,MessageChannel的创建除了channel名以外,还需要指定编码方式:
BasicMessageChannel(String name, MessageCodec<T> codec, {BinaryMessenger binaryMessenger})
发送的消息会以二进制的形式进行处理,所以要针对不同类型的数进行二进制编码
编码类型 | 消息格式 |
---|---|
BinaryCodec | 发送二进制消息时 |
JSONMessageCodec | 发送Json格式消息时 |
StandardMessageCodec | 发送基本型数据时 |
StringCodec | 发送String类型消息时 |
class _MyHomePageState extends State<MyHomePage> {
static const _channel = BasicMessageChannel('com.example.messagechannel/interop', StringCodec());
String _platformMessage;
void _sendMessage() async {
final String reply = await _channel.send('Hello World form Dart');
print(reply);
}
@override
initState() {
super.initState();
// Receive messages from platform
_channel.setMessageHandler((String message) async {
print('Received message = $message');
setState(() => _platformMessage = message);
return 'Reply from Dart';
});
// Send message to platform
_sendMessage();
}
class MainActivity: FlutterActivity() {
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
GeneratedPluginRegistrant.registerWith(flutterEngine)
val channel = BasicMessageChannel(
flutterEngine.dartExecutor.binaryMessenger,
"com.example.messagechannel/interop",
StringCodec.INSTANCE)
// Receive messages from Dart
channel.setMessageHandler { message, reply ->
Log.d("Android", "Received message = $message")
reply.reply("Reply from Android")
}
// Send message to Dart
Handler().postDelayed({
channel.send("Hello World from Android") { reply ->
Log.d("Android", "$reply")
}
}, 500)
}
}
Andorid端回复的消息会在Flutter端显示
]]>flutter
可以与native之间进行通信,帮助我们使用native提供的能力。通信是双向的,我们可以从Native层调用flutter层的dart代码,同时也可以从flutter层调用Native的代码。我们需要使用Platform Channels APIs进行通信,主要包括下面三种:
其中最常用的是MethodChanel,MethodChanel的使用与在Android的JNI调用非常类似,但是MethodChanel更加简单,而且相对于JNI的同步调用MethodChanel的调用是异步的:
从flutter架构图上可以看到,flutter与native的通信发生在Framework和Engine之间,framewrok内部会将MethodChannel以BinaryMessage的形式与Engine进行数据交换。关于BinaryMessage在这里不做过多介绍,主要以介绍Channel的使用为主。
我们先看一下MethodChanel使用的基本流程:
与flutter调用native的顺序完全一致,只是[native]与[flutter]角色反调
首先在flutter端实现以下功能:
import 'package:flutter/services.dart';
class _MyHomePageState extends State<MyHomePage> {
static const MethodChannel _channel = const MethodChannel('com.example.methodchannel/interop');
static Future<dynamic> get _list async {
final Map params = <String, dynamic> {
'name': 'my name is hoge',
'age': 25,
};
final List<dynamic> list = await _channel.invokeMethod('getList', params);
return list;
}
@override
initState() {
super.initState();
// Dart -> Platforms
_list.then((value) => print(value));
}
在native(android)端实现以下功能
class MainActivity: FlutterActivity() {
companion object {
private const val CHANNEL = "com.example.methodchannel/interop"
private const val METHOD_GET_LIST = "getList"
}
private lateinit var channel: MethodChannel
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
GeneratedPluginRegistrant.registerWith(flutterEngine)
channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
channel.setMethodCallHandler { methodCall: MethodCall, result: MethodChannel.Result ->
if (methodCall.method == METHOD_GET_LIST) {
val name = methodCall.argument<String>("name").toString()
val age = methodCall.argument<Int>("age")
Log.d("Android", "name = ${name}, age = $age")
val list = listOf("data0", "data1", "data2")
result.success(list)
}
else
result.notImplemented()
}
}
因为结果返回是异步的,所以既可以像上面代码那样在MethodCallHandler里通过result.success返回结果,也也可以先保存result的引用,在之后的某个时间点再调用sucess,但需要特别注意的是无论何时调用result.sucess,必须确保其在UI线程进行:
@UiThread void success(@Nullable Object result)
android调用flutter的代码实现与flutter调用android是类似的,只不过要注意所以的调用都要在UI线程进行。
先实现android部分的代码:
channel.invokeMethod("callMe", listOf("a", "b"), object : MethodChannel.Result {
override fun success(result: Any?) {
Log.d("Android", "result = $result")
}
override fun error(errorCode: String?, errorMessage: String?, errorDetails: Any?) {
Log.d("Android", "$errorCode, $errorMessage, $errorDetails")
}
override fun notImplemented() {
Log.d("Android", "notImplemented")
}
})
result.success(null)
flutte部分则主要实现MethodCallHandler的注册:
Future<dynamic> _platformCallHandler(MethodCall call) async {
switch (call.method) {
case 'callMe':
print('call callMe : arguments = ${call.arguments}');
return Future.value('called from platform!');
//return Future.error('error message!!');
default:
print('Unknowm method ${call.method}');
throw MissingPluginException();
break;
}
}
@override
initState() {
super.initState();
// Platforms -> Dart
_channel.setMethodCallHandler(_platformCallHandler);
}
]]>flutter
可以native之间可以通过Platform Channels APIs进行通信,API主要有以下三种:
其中EventChannel用于从native向flutter发送通知事件,例如flutter通过其监听Android的重力感应变化等。与MethodChannel不同,EventChannel是native到flutter的单向调用,调用是多播(一对多)的,可以类比成Android的Brodcast。
我们照例先看一下API使用的基本流程:
class _MyHomePageState extends State<MyHomePage> {
static const EventChannel _channel = const EventChannel('com.example.eventchannel/interop');
StreamSubscription _streamSubscription;
String _platformMessage;
void _enableEventReceiver() {
_streamSubscription = _channel.receiveBroadcastStream().listen(
(dynamic event) {
print('Received event: $event');
setState(() {
_platformMessage = event;
});
},
onError: (dynamic error) {
print('Received error: ${error.message}');
},
cancelOnError: true);
}
void _disableEventReceiver() {
if (_streamSubscription != null) {
_streamSubscription.cancel();
_streamSubscription = null;
}
}
@override
initState() {
super.initState();
_enableEventReceiver();
}
@override
void dispose() {
super.dispose();
_disableEventReceiver();
}
调用StreamSubscriptoin#cancel时,监听被取消。
android需要完成以下功能
class MainActivity: FlutterActivity() {
private lateinit var channel: EventChannel
var eventSink: EventSink? = null
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
GeneratedPluginRegistrant.registerWith(flutterEngine)
channel = EventChannel(flutterEngine.dartExecutor.binaryMessenger, "com.example.eventchannel/interop")
channel.setStreamHandler(
object : StreamHandler {
override fun onListen(arguments: Any?, events: EventSink) {
eventSink = events
Log.d("Android", "EventChannel onListen called")
Handler().postDelayed({
eventSink?.success("Android")
//eventSink?.endOfStream()
//eventSink?.error("error code", "error message","error details")
}, 500)
}
override fun onCancel(arguments: Any?) {
Log.w("Android", "EventChannel onCancel called")
}
})
}
}
]]>上一篇我和大家一起学习了CMMotionManager获取加速度数据、陀螺仪数据、磁场数据的方式。
今天我们一起学习感知设备移动数据,与上述方式完全相同。
程序也可通过如下两种方式来感知设备移动数据:
> 使用基于代码块的方式获取设备移动数据。
> 使用周期性主动请求的方式获取设备移动数据。
获取设备移动数据时,CMMotionManager将会返回一个CMDeviceMotion对象,该对象包含如下属性:
> attitude:该属性返回该设备的方位信息。该属性的返回值是一个CMAttitude类型的对象,该对象包含roll、pitch、yaw3个欧拉角的值。
欧拉角:用来确定定点转动刚体位置的3个一组独立角参量,由章动角θ、旋进角(即进动角)ψ和自转角j组成,为欧拉首先提出而得名。
不了解欧拉角的同学可以去百度一下。
> rotationRate:该属性返回原始的陀螺仪信息,该属性值为CMRotationRate结构体变量。基本等同于前面介绍的陀螺仪数据。
> gravity:该属性返回地球重力对该设备在X、Y、Z轴上施加的重力加速度。
> userAcceleration:该属性返回用户外力对该设备在X、Y、Z轴上施加的重力加速度。
> magneticField:该属性返回校准后的磁场信息。该属性值是一个CMCalibratedMagneticField结构体变量。CMCalibratedMagneticField类型的变量包括field和accuracy两个字段,其中field代表X、YZ、轴上的磁场强度,accuracy则代表磁场强度的精度。
因为CMAttitude类型的变量用于表示该设备的控件方位。其中roll、pitch、yaw这3个角度的意义如下。
> yaw角度:表示手机顶部转过的夹角。当手机绕着Z轴旋转时,该角度值发生改变。
例如,当该角度为0时,表明手机并未发生旋转,该角度为π/2时,代表手机逆时针转过90°。
> pitch角度:表示手机顶部或尾部翘起的角度。当手机绕着X轴倾斜时,该角度值发生变化。该角度的取值范围是-π~π。
假设将手机屏幕朝上水平放在桌子上,如果桌子是完全水平的。该角度应该是0。
假如从手机顶部开始抬起。直到将手机沿X轴旋转180°(屏幕向下水平放在桌面上),在这个旋转过程中,该角度值会从0变化到π。也就是说,从手机顶部抬起时,该角度值会逐渐增大,直到等于π。
如果从手机底部开始抬起,直到将手机沿X轴旋转180°(屏幕向下水平放在桌面上),该角度值会从0变化到-π。也就是说,从手机底部抬起时,该角度值会逐渐减小,直到等于-π。
> roll角度:表示手机左侧或右侧翘起的角度。当手机绕着Y轴倾斜时,该角度值发生变化。该角度的取值范围在-π/2~π/2。
假设将手机屏幕朝上水平放在桌面上,如果桌面是完全水平的,该角度值应为0。
假如将手机左侧逐渐抬起,直到将手机沿Y轴旋转90°(手机与桌面垂直),在这个旋转过程中,该角度值会从0变化到π/2。也就是说,从手机左侧抬起时,该角度值会逐渐增大,直到等于π/2。
如果从手机右侧开始抬起,直到将手机沿Y轴旋转90°(手机与桌面垂直),该角度值会从0变化到-π/2。也就是说,从手机左侧抬起时,该角度值会逐渐减少,直到等于-π/2。
主要的属性我已介绍完毕,下面我们开始实战演练。
#import "ViewController.h"
#import <CoreMotion/CoreMotion.h>
@interface ViewController ()
{
NSTimer *updateTimer;
}
@property (strong, nonatomic) CMMotionManager *motionManager;
@property (weak, nonatomic) IBOutlet UILabel *showField;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
//创建CMMotionManager对象
self.motionManager = [[CMMotionManager alloc] init];
//如果可以获取设备的动作信息
if (self.motionManager.deviceMotionAvailable) {
//开始更新设备的动作信息
[self.motionManager startDeviceMotionUpdates];
} else {
NSLog(@"该设备的deviceMotion不可用");
}
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
//使用定时器周期性获取设备移动信息
updateTimer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(updateDisplay) userInfo:nil repeats:YES];
[updateTimer fire];
}
- (void)updateDisplay {
if (self.motionManager.deviceMotionAvailable) {
//获取设备移动信息
CMDeviceMotion *deviceMotion = self.motionManager.deviceMotion;
NSMutableString *str = [NSMutableString stringWithFormat:@"devuceMotion信息为:\n"];
[str appendString:@"---attitude信息---\n"];
[str appendFormat:@"attitude的yaw:%+.2f\n",deviceMotion.attitude.yaw];
[str appendFormat:@"attitude的pitch:%+.2f\n",deviceMotion.attitude.pitch];
[str appendFormat:@"attitude的roll:%+.2f\n",deviceMotion.attitude.roll];
[str appendFormat:@"---rotationRate信息---\n"];
[str appendFormat:@"rotationRate的X:%+.2f\n",deviceMotion.rotationRate.x];
[str appendFormat:@"rotationRate的Y:%+.2f\n",deviceMotion.rotationRate.y];
[str appendFormat:@"rotationRate的Z:%+.2f\n",deviceMotion.rotationRate.z];
[str appendFormat:@"---gravity信息---\n"];
[str appendFormat:@"gravity的X:%+.2f\n",deviceMotion.gravity.x];
[str appendFormat:@"gravity的Y:%+.2f\n",deviceMotion.gravity.y];
[str appendFormat:@"gravity的Z:%+.2f\n",deviceMotion.gravity.z];
[str appendString:@"---magneticField信息---\n"];
[str appendFormat:@"magneticField的X:%+.2f\n",deviceMotion.magneticField.field.x];
[str appendFormat:@"magneticField的Y:%+.2f\n",deviceMotion.magneticField.field.y];
[str appendFormat:@"magneticField的Z:%+.2f\n",deviceMotion.magneticField.field.z];
self.showField.text = str;
}
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
@end
运行效果:
很简单,大家快动手试一试吧~
]]>path_provider是flutter提供的一个获取应用存储路径的插件,它封装了统一的api来获取Android和ios两个平台的应用存储路径,提供的api如下:
我们通过File和Directory来创建文件和文件夹时首先要获取到应用的相关路径,不然会报错;
File对象和Directory对象封装在dart:io中,使用时需要先引入该库:
import 'dart:io';
// 创建一个文件夹
Directory tempDir = await getTemporaryDirectory();
Directory directory = new Directory('${tempDir.path}/test');
if (!directory.existsSync()) {
directory.createSync();
print('文档初始化成功,文件保存路径为 ${directory.path}');
}
// 创建一个文件
Directory tempDir = await getTemporaryDirectory();
File file = new File('${tempDir.path}/test.txt');
if (!file.existsSync()) {
file.createSync();
print('test.txt创建成功');
}
Directory对象提供listSync()方法获取文件夹里的内容,该方法返回一个数组;
// 打印出test文件夹下文件的路径
Directory tempDir = await getTemporaryDirectory();
Directory directory = new Directory('${tempDir.path}/test');
directory.listSync().forEach((file) {
print(file.path);
});
文件和文件夹都通过delete删除,delete异步,deleteSync同步;如果一个文件夹是非空的删除会报错,删除非空文件夹需要先清空该文件夹:
Directory directory = new Directory(path);
if (directory.existsSync()) {
List<FileSystemEntity> files = directory.listSync();
if (files.length > 0) {
files.forEach((file) {
file.deleteSync();
});
}
directory.deleteSync();
}
File file = new File('${cache}/test.txt');
// 读物文件内容
String content = file.readAsString();
print(content);
// 写入文件
file.writeAsString('文件内容');
flutter对json序列化需要引入 dart:convert 库:
import 'dart:convert' as convert;
通过jsonEncode/jsonDecode来转换json对象:
var json = {
'name': 'xiaoming',
'age': 22,
'address': 'hangzhou'
}
File jsonFile = new File('$cahce/test.json');
// json文件写入
jsonFile.writeAsString(convert.jsonEncode(json));
// json文件读取
var jsonStr = await jsonFile.readAsString();
var json = convert.jsonDecode(jsonStr);
print(json['name']); // xiaoming
print(json['age']); // 22
print(json['address']); // hangzhou
// 将test目录下的info.json复制到test2目录下的info2.json中
File info1 = new File('$cache/test/info.json');
info1.copySync('$cache/test2/info2.json');
引入包archive包:
import 'package:archive/archive.dart';
import 'package:archive/archive_io.dart';
压缩:
var encode = ZipFileEncoder();
encode.zipDirectory(path, filename: path + '.zip');
encode.close();
import 'package:archive/archive.dart';
void main() {
Directory appDocDirectory = await getExternalStorageDirectory();
var encoder = ZipFileEncoder();
encoder.create(appDocDirectory.path+"/"+'jay.zip');
encoder.addFile(File(selectedAdharFile));
encoder.addFile(File(selectedIncomeFile));
encoder.close();
}
压缩前使用ZipFileEncoder先声明处理压缩的对象,调用该对象的zipDirectory方法压缩文件,该方法接受两个参数,第一个是要压缩文件/文件夹的路径,第二个是压缩包的保存路径;
解压:
List<int> bytes = File('test.zip').readAsBytesSync();
Archive archive = ZipDecoder().decodeBytes(bytes);
]]>1,创建一个MethodChannel _channel = MethodChannel(‘opengl_texture’);
用来和iOS端通信,主要是从iOS端获取_textureID,
2,把Texture(textureId: _textureID,)添加到widget上,这个用来播放视频的,原理是从底层获取iOS端的CVpixelbuffer,
把CVpixelbuffer渲染到flutter页面上。
代码如下:
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
MethodChannel _channel = MethodChannel('com.ios.texture');
bool _isTextureOK = false;
int _textureID = -1;
@override
void initState() {
super.initState();
}
void getTexture() async {
_textureID = await _channel.invokeMethod('newTexture');
setState(() {
_isTextureOK = true;
});
}
Widget getTextureWidget(BuildContext context) {
return Container(
// color: Colors.red,
width: 300,
height: 300,
child: Texture(textureId: _textureID,),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Stack(
children: [
Positioned.fill(
child: Center(
//在这里加载纹理Texture
child: _isTextureOK ? getTextureWidget(context) : Text('video'),
)
),
Positioned(
left: 0,
bottom: 0,
child: FlatButton(
onPressed: (){
getTexture();
},
child: Text("getTexture")
)),
Positioned(
right: 0,
bottom: 0,
child: FlatButton(
onPressed: (){
_channel.invokeMethod('open');
},
child: Text("open camera")
)),
],
),
);
}
}
- (void)registerWithRegistrar:(NSObject FlutterPluginRegistrar \*)registrar
#import "AppDelegate.h"
#import "TexturePlugin.h"
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[TexturePlugin registerWithRegistrar:[self registrarForPlugin:@"TexturePlugin"]];
return [super application:application didFinishLaunchingWithOptions:launchOptions];
}
@end
TexturePlugin.h文件
#import <Flutter/Flutter.h>
NS_ASSUME_NONNULL_BEGIN
@interface TexturePlugin : NSObject <FlutterPlugin>
@end
NS_ASSUME_NONNULL_END
TexturePlugin.m文件
#import "TexturePlugin.h"
#import "GLRender.h"
#import "ViddeoController.h"
@interface TexturePlugin ()<ViddeoControllerDelegate>
{
ViddeoController *video ;
int64_t _textureId;//这个是创建纹理得到的ID
}
@property (nonatomic, strong) NSObject<FlutterTextureRegistry> *textures;
@property (nonatomic, strong) GLRender *glRender;
@end
@implementation TexturePlugin
- (instancetype) initWithTextures:(NSObject<FlutterTextureRegistry> *)textures {
if (self = [super init]) {
video = [[ViddeoController alloc] init];
video.delegate = self;
_textures = textures;
}
return self;
}
//协议方法
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
//创建一个FlutterMethodChannel,用来和flutter通信。
FlutterMethodChannel *channel = [FlutterMethodChannel methodChannelWithName:@"com.ios.texture" binaryMessenger:[registrar messenger]];
//创建这个插件对象,把实现了<FlutterPluginRegistrar>协议的对象传给TexturePlugin
TexturePlugin *instance = [[TexturePlugin alloc] initWithTextures:registrar.textures];
//把channel的代理设置给instance;
[registrar addMethodCallDelegate:instance channel:channel];
}
//FlutterMethodChannel代理,
- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result
{
if ([call.method isEqualToString:@"newTexture"]) {
//收到flutter获取纹理的信号
_glRender = [[GLRender alloc] init];
//生成textureId
_textureId = [_textures registerTexture:_glRender];
//把textureId反馈给flutter
result(@(_textureId));
}else if ([call.method isEqualToString:@"open"]){
//开启手机摄像头,
[video cameraButtonAction:YES];
}
}
//把摄像头的视频封装成CVpixelBufferRef
- (void)video:(CVImageBufferRef)imageBuffer
{
[_glRender createCVBufferWith:imageBuffer];
//刷新frame,告诉flutter去读取新的CVpixelBufferRef
[self.textures textureFrameAvailable:_textureId];
// CVPixelBufferRelease(imageBuffer);
}
@end
GLRender.h文件
#import <Foundation/Foundation.h>
#import <Flutter/Flutter.h>
NS_ASSUME_NONNULL_BEGIN
@interface GLRender : NSObject <FlutterTexture>
- (instancetype)init;
- (void)createCVBufferWith:(CVPixelBufferRef )target;
@end
NS_ASSUME_NONNULL_END
GLRender.m文件
@implementation GLRender
{
CVPixelBufferRef _target;
}
- (CVPixelBufferRef)copyPixelBuffer {
// 实现FlutterTexture协议的接口,每次flutter是直接读取我们映射了纹理的pixelBuffer对象
return _target;
}
- (void)createCVBufferWith:(CVPixelBufferRef )target
{
_target = target;
}
合并后
//
// TexturePlugin.m
// Runner
//
// Created by jonasluo on 2019/12/11.
// Copyright © 2019 The Chromium Authors. All rights reserved.
//
#import "TexturePlugin.h"
#import "ViddeoController.h"
@interface GLTexture : NSObject<FlutterTexture>
@property(nonatomic)CVPixelBufferRef target;
@end
@implementation GLTexture
- (CVPixelBufferRef)copyPixelBuffer {
// 实现FlutterTexture协议的接口,每次flutter是直接读取我们映射了纹理的pixelBuffer对象
return _target;
}
@end
@interface TexturePlugin ()<ViddeoControllerDelegate,FlutterPlugin>
{
ViddeoController *video ;//用来把摄像头的视频转码成cvpixelbuffer
int64_t _textureId;
GLTexture *_glTexture;
}
@property (nonatomic, strong) NSObject<FlutterTextureRegistry> *textures;//其实是FlutterEngine
@end
@implementation TexturePlugin
- (instancetype) initWithTextures:(NSObject<FlutterTextureRegistry> *)textures {
if (self = [super init]) {
video = [[ViddeoController alloc] init];
video.delegate = self;
_textures = textures;
}
return self;
}
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
FlutterMethodChannel *channel = [FlutterMethodChannel methodChannelWithName:@"com.ios.texture" binaryMessenger:[registrar messenger]];
TexturePlugin *instance = [[TexturePlugin alloc] initWithTextures:registrar.textures];
[registrar addMethodCallDelegate:instance channel:channel];
}
- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result
{
if ([call.method isEqualToString:@"newTexture"]) {
_glTexture = [[GLTexture alloc] init];
_textureId = [_textures registerTexture:_glTexture];
result(@(_textureId));
}else if ([call.method isEqualToString:@"open"]){
[video cameraButtonAction:YES];
}
}
- (void)video:(CVImageBufferRef)imageBuffer
{
_glTexture.target = imageBuffer;
[self.textures textureFrameAvailable:_textureId];
}
@end
另一个共享cvpixelbuffer的
#import "GLRender.h"
#import <OpenGLES/EAGL.h>
#import <OpenGLES/ES2/gl.h>
#import <OpenGLES/ES2/glext.h>
#import <CoreVideo/CoreVideo.h>
#import <UIKit/UIKit.h>
@implementation GLRender
{
EAGLContext *_context;
CGSize _size;
CVOpenGLESTextureCacheRef _textureCache;
CVOpenGLESTextureRef _texture;
CVPixelBufferRef _target;
GLuint _program;
GLuint _frameBuffer;
}
- (CVPixelBufferRef)copyPixelBuffer {
// 实现FlutterTexture协议的接口,每次flutter是直接读取我们映射了纹理的pixelBuffer对象
return _target;
}
- (instancetype)init
{
if (self = [super init]) {
_size = CGSizeMake(1000, 1000);
[self initGL];
[self loadShaders];
}
return self;
}
- (void)initGL {
_context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
[EAGLContext setCurrentContext:_context];
// 先调用上面的函数创建共享内存的pixelBuffer和texture对象
[self createCVBufferWith:&_target withOutTexture:&_texture];
// 创建帧缓冲区
glGenFramebuffers(1, &_frameBuffer);
glBindFramebuffer(GL_FRAMEBUFFER, _frameBuffer);
// 将纹理附加到帧缓冲区上
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, CVOpenGLESTextureGetName(_texture), 0);
glViewport(0, 0, _size.width, _size.height);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
NSLog(@"failed to make complete framebuffer object %x", glCheckFramebufferStatus(GL_FRAMEBUFFER));
}
}
- (void)createCVBufferWith:(CVPixelBufferRef *)target withOutTexture:(CVOpenGLESTextureRef *)texture {
// 创建纹理缓存池,这个不是重点
CVReturn err = CVOpenGLESTextureCacheCreate(kCFAllocatorDefault, NULL, _context, NULL, &_textureCache);
if (err) {
return;
}
CFDictionaryRef empty;
CFMutableDictionaryRef attrs;
empty = CFDictionaryCreate(kCFAllocatorDefault, NULL, NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
attrs = CFDictionaryCreateMutable(kCFAllocatorDefault, 1, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
// 核心参数是这个,共享内存必须要设置这个kCVPixelBufferIOSurfacePropertiesKey
CFDictionarySetValue(attrs, kCVPixelBufferIOSurfacePropertiesKey, empty);
// 分配pixelBuffer对象的内存,注意flutter需要的是BGRA格式
CVPixelBufferCreate(kCFAllocatorDefault, _size.width, _size.height, kCVPixelFormatType_32BGRA, attrs, target);
// 映射上面的pixelBuffer对象到一个纹理上
CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault, _textureCache, *target, NULL, GL_TEXTURE_2D, GL_RGBA, _size.width, _size.height, GL_BGRA, GL_UNSIGNED_BYTE, 0, texture);
CFRelease(empty);
CFRelease(attrs);
}
- (void)deinitGL {
glDeleteFramebuffers(1, &_frameBuffer);
CFRelease(_target);
CFRelease(_textureCache);
CFRelease(_texture);
}
- (void)createCVBufferWith:(CVPixelBufferRef )target
{
_target = target;
}
#pragma mark - shader compilation
- (BOOL)loadShaders
{
GLuint vertShader, fragShader;
NSString *vertShaderPathname, *fragShaderPathname;
_program = glCreateProgram();
vertShaderPathname = [[NSBundle mainBundle] pathForResource:@"Shader" ofType:@"vsh"];
if (![self compileShader:&vertShader type:GL_VERTEX_SHADER file:vertShaderPathname]) {
NSLog(@"failed to compile vertex shader");
return NO;
}
fragShaderPathname = [[NSBundle mainBundle] pathForResource:@"Shader" ofType:@"fsh"];
if (![self compileShader:&fragShader type:GL_FRAGMENT_SHADER file:fragShaderPathname]) {
NSLog(@"failed to compile fragment shader");
return NO;
}
glAttachShader(_program, vertShader);
glAttachShader(_program, fragShader);
if (![self linkProgram:_program]) {
NSLog(@"failed to link program: %d", _program);
if (vertShader) {
glDeleteShader(vertShader);
vertShader = 0;
}
if (fragShader) {
glDeleteShader(fragShader);
fragShader = 0;
}
if (_program) {
glDeleteProgram(_program);
_program = 0;
}
return NO;
}
if (vertShader) {
glDetachShader(_program, vertShader);
glDeleteShader(vertShader);
}
if (fragShader) {
glDetachShader(_program, fragShader);
glDeleteShader(fragShader);
}
NSLog(@"load shaders succ");
return YES;
}
- (BOOL)compileShader:(GLuint *)shader type:(GLenum)type file:(NSString *)file
{
GLint status;
const GLchar *source;
source = (GLchar*)[[NSString stringWithContentsOfFile:file encoding:NSUTF8StringEncoding error:nil] UTF8String];
if (!source) {
NSLog(@"failed to load shader. type: %i", type);
return NO;
}
*shader = glCreateShader(type);
glShaderSource(*shader, 1, &source, NULL);
glCompileShader(*shader);
#if defined(DEBUG)
GLint logLength;
glGetShaderiv(*shader, GL_INFO_LOG_LENGTH, &logLength);
if (logLength > 0) {
GLchar *log = (GLchar *)malloc(logLength);
glGetShaderInfoLog(*shader, logLength, &logLength, log);
NSLog(@"Shader compile log:\n%s", log);
free(log);
}
#endif
glGetShaderiv(*shader, GL_COMPILE_STATUS, &status);
if (status == 0) {
glDeleteShader(*shader);
return NO;
}
return YES;
}
- (BOOL)linkProgram:(GLuint)prog
{
GLint status;
glLinkProgram(prog);
glGetProgramiv(prog, GL_LINK_STATUS, &status);
if (status == 0) {
return NO;
}
return YES;
}
- (BOOL)validateProgram:(GLuint)prog
{
GLint logLength, status;
glValidateProgram(prog);
glGetProgramiv(prog, GL_INFO_LOG_LENGTH, &logLength);
if (logLength > 0) {
GLchar *log = (GLchar *)malloc(logLength);
glGetProgramInfoLog(prog, logLength, &logLength, log);
NSLog(@"program validate log : \n%s", log);
free(log);
}
glGetProgramiv(prog, GL_VALIDATE_STATUS, &status);
if (status == 0) {
return NO;
}
return YES;
}
@end
]]>作为开发者的我们,经常会做一些上传图片和和保存图片啦的功能,但是由于一些图片非常大,我们在上传或者保存的时候会占用大量的网络资源和本地资源,那么我们需要做的就是对图片进行压缩。
昨天在写
最新Flutter 微信分享功能实现【Flutter专题23】mp.weixin.qq.com/s/PGpgau6mJLAbfKMVYqTuOg
的时候用到一个知识点,就是图片压缩
当时我用了flutter_image_compress
可能大家都知道Dart 已经有图片压缩库了。为什么要使用原生?
还不是因为他的效率问题,
所以今天就和大家来说一说它的具体用法吧。
安装
dependencies:
flutter_image_compress: ^1.0.0-nullsafety
使用的地方导入
import 'package:flutter_image_compress/flutter_image_compress.dart';
/// 图片压缩 File -> Uint8List
Future<Uint8List> testCompressFile(File file) async {
var result = await FlutterImageCompress.compressWithFile(
file.absolute.path,
minWidth: 2300,
minHeight: 1500,
quality: 94,
rotate: 90,
);
print(file.lengthSync());
print(result.length);
return result;
}
/// 图片压缩 File -> File
Future<File> testCompressAndGetFile(File file, String targetPath) async {
var result = await FlutterImageCompress.compressAndGetFile(
file.absolute.path, targetPath,
quality: 88,
rotate: 180,
);
print(file.lengthSync());
print(result.lengthSync());
return result;
}
/// 图片压缩 Asset -> Uint8List
Future<Uint8List> testCompressAsset(String assetName) async {
var list = await FlutterImageCompress.compressAssetImage(
assetName,
minHeight: 1920,
minWidth: 1080,
quality: 96,
rotate: 180,
);
return list;
}
/// 图片压缩 Uint8List -> Uint8List
Future<Uint8List> testComporessList(Uint8List list) async {
var result = await FlutterImageCompress.compressWithList(
list,
minHeight: 1920,
minWidth: 1080,
quality: 96,
rotate: 135,
);
print(list.length);
print(result.length);
return result;
}
还有另外两种方式
安装
flutter_native_image: ^0.0.6
文档地址
https://pub.flutter-io.cn/packages/flutter_native_image
用法
Future<File> compressFile(File file) async{
File compressedFile = await FlutterNativeImage.compressImage(file.path,
quality: 5,);
return compressedFile;
}
您可以以字节为单位获取文件长度,并以千字节或兆字节等计算。
像这样:file.readAsBytesSync().lengthInBytes -> 文件大小以字节为单位的文件大小
(file.readAsBytesSync().lengthInBytes) / 1024 -> 文件大小以千字节为单位的文件大小
(file.readAsBytesSync().lengthInBytes) / 1024 / 1024 -> 文件大小以兆字节为单位
今天的文章介绍了图片压缩的三种用法,分别对应三个不同的库,大家可以去实践,来对比一下那个库的性能更好。
好的,我是坚果,
如何在 Flutter 中创建自定义图标【Flutter专题22】mp.weixin.qq.com/s/1h19t1EAaGTmrFI8gaDLWA
有更多精彩内容,期待你的发现.
]]>常见的状态管理由以下几个:
[AppStorage](https://link.zhihu.com/?target=https%3A//developer.apple.com/documentation/swiftui/appstorage)
[Binding](https://link.zhihu.com/?target=https%3A//developer.apple.com/documentation/swiftui/binding)
[Environment](https://link.zhihu.com/?target=https%3A//developer.apple.com/documentation/swiftui/environment)
[EnvironmentObject](https://link.zhihu.com/?target=https%3A//developer.apple.com/documentation/swiftui/environmentobject)
[FetchRequest](https://link.zhihu.com/?target=https%3A//developer.apple.com/documentation/swiftui/fetchrequest)
[ObservedObject](https://link.zhihu.com/?target=https%3A//developer.apple.com/documentation/swiftui/observedobject)
[State](https://link.zhihu.com/?target=https%3A//developer.apple.com/documentation/swiftui/state)
[StateObject](https://link.zhihu.com/?target=https%3A//developer.apple.com/documentation/swiftui/stateobject)
在开发中,他们的用法可以用下边这个图概括:
如果View依赖了这些数据,当数据改变的时候,View就会刷新。我们主要讲解ObservedObject
和StateObject
。
class MyViewModel: ObservableObject {
@Published var name: String = "张三"
}
struct ContentView: View {
@ObservedObject var dataModel: MyViewModel
var body: some View {
Text(dataModel.name)
}
}
上边的代码是最常见的一种用法,dataModel
为ContentView
提供数据,那么@ObservedObject
是怎么一回事呢?看它的定义:
@propertyWrapper @frozen public struct ObservedObject<ObjectType> : DynamicProperty where ObjectType : ObservableObject {
@dynamicMemberLookup @frozen public struct Wrapper {
public subscript<Subject>(dynamicMember keyPath: ReferenceWritableKeyPath<ObjectType, Subject>) -> Binding<Subject> { get }
}
public init(initialValue: ObjectType)
public init(wrappedValue: ObjectType)
public var wrappedValue: ObjectType
public var projectedValue: ObservedObject<ObjectType>.Wrapper { get }
}
通过分析上边的代码,我们发现下边几个重要信息:
ObjectType : ObservableObject
表示它的类型必须实现ObservableObject
协议,这个协议我们下边会讲到projectedValue: ObservedObject<ObjectType>.Wrapper
,说明我们可以用$dataModel
来访问这个projectedValue
,它的返回值是Wrapper
类型,再看上边struct Wrapper
的定义,它是一个@dynamicMemberLookup
,@dynamicMemberLookup
的实现原理我们后续再详细讲解,大家只需要知道,当我们想要一个Bind
类型的数据是,可以这样TextField("输入文字", text: $dataModel.name)
其中,上边的重点是ObservableObject
协议,我们再看看它的定义:
public protocol ObservableObject : AnyObject {
/// The type of publisher that emits before the object has changed.
associatedtype ObjectWillChangePublisher : Publisher = ObservableObjectPublisher where Self.ObjectWillChangePublisher.Failure == Never
/// A publisher that emits before the object has changed.
var objectWillChange: Self.ObjectWillChangePublisher { get }
}
extension ObservableObject where Self.ObjectWillChangePublisher == ObservableObjectPublisher {
/// A publisher that emits before the object has changed.
public var objectWillChange: ObservableObjectPublisher { get }
}
ObservableObject
继承自AnyObject
,这说明了实现该协议必须是class类型,而不能是struct类型。
该协议要求返回一个objectWillChange
属性,该属性必须实现Publisher
协议,上边代码中的ObservableObject
扩展已经实现了该协议,它返回的类型为ObservableObjectPublisher
,我们再看看它的定义:
final public class ObservableObjectPublisher : Publisher {
/// The kind of values published by this publisher.
public typealias Output = Void
/// The kind of errors this publisher might publish.
///
/// Use `Never` if this `Publisher` does not publish errors.
public typealias Failure = Never
/// Creates an observable object publisher instance.
public init()
final public func receive<S>(subscriber: S) where S : Subscriber, S.Failure == ObservableObjectPublisher.Failure, S.Input == ObservableObjectPublisher.Output
final public func send()
}
可以看出ObservableObjectPublisher
是一个很普通的Publisher
,它是一个自定义的Publisher
,对外只暴露了一个send
方法,用于通知数据发生变更,这个Publisher
并不会输出任何数据。
到目前为止,我们已经知道,只要实现了ObservableObject
协议,就能获得一个objectWillChange
,它是一个Publisher,只要调用objectWillChange.send()就可以触发View的刷新
。
我们先实现这个协议,代码如下:
class MyViewModel: ObservableObject {
@Published var name: String = "张三"
var age: Int = 20
func click() {
age = 30
objectWillChange.send()
}
}
如果我们用@Published
来包装某个属性,那么当属性的值变化时,就会自动调用objectWillChange.send()
,否则我们需要手动调用。
我们再看一下@Published
的定义:
@propertyWrapper public struct Published<Value> {
public init(wrappedValue: Value)
public init(initialValue: Value)
/// A publisher for properties marked with the `@Published` attribute.
public struct Publisher : Publisher {
/// The kind of values published by this publisher.
public typealias Output = Value
/// The kind of errors this publisher might publish.
///
/// Use `Never` if this `Publisher` does not publish errors.
public typealias Failure = Never
public func receive<S>(subscriber: S) where Value == S.Input, S : Subscriber, S.Failure == Published<Value>.Publisher.Failure
}
public var projectedValue: Published<Value>.Publisher { mutating get set }
}
大家只需要记住一点,它的projectedValue
是一个Publisher,要想获取到这个projectedValue
,使用$
符号,因为它是一个Publisher,所有我们就可以随意使用Combine中的内容了:
$name
.map {
"姓名是: \($0)"
}
.sink(receiveValue: {
print($0)
})
@StateObject
和@ObservedObject
都是用来包装实现了ObservableObject
协议的属性,唯一的区别就是该属性的生命周期的管理问题。
@StateObject
的生命周期由View管理,只初始化一次,View销毁它就销毁@ObservedObject
的生命周期由我们手动管理,通常由父传给子本文并没有详细地讲解SwiftUI中的全部状态管理,只讲到了跟Combine有关系的状态,其中,最核心的是ObservableObject
协议,在真实的开发中,它绝对是最常用的技术,我们自定义的View Model中,通过组合使用一系列的pipline来操作数据,当作为Source for Truth的数据变更后,View自动进行刷新。
https://github.com/agelessman/FuckingSwiftUI
没有写过完整SwiftUI项目的同学,应该没怎么使用过Combine,可以这么说,**Combine就是专门用于处理数据的利器,**如果你学会了这些知识,那么你写SwiftUI程序的效率绝对会成倍的增加。
前边已经写了很多篇文章详细介绍了Combine中的Publisher,Operator,Subscriber,相信大家已经对Combine有了一个基本的了解,今天就带领大家一起研究一下Combine的实际应用。
大家可以在这里找到SwiiftUI和Combine的合集:FuckingSwiftUI
本文演示demo下载地址:CombineDemoTest
上图演示了一个开发中最常见的场景,实时地根据用户的输入进行搜索,这样一个功能表面上看起来非常简单,其实内部逻辑细节很多:
先看一下首页的代码:
struct ContentView: View {
@StateObject private var dataModel = MyViewModel()
@State private var showLogin = false;
var body: some View {
GeometryReader { geometry in
VStack(alignment: .leading, spacing: 0) {
ZStack {
HStack(spacing: 10) {
Group {
if dataModel.loading {
ActivityIndicator()
} else {
Image(systemName: "magnifyingglass")
}
}
.frame(width: 30, height: 30)
TextField("请输入要搜索的repository", text: $dataModel.inputText)
.textFieldStyle(RoundedBorderTextFieldStyle())
Button("登录") {
self.showLogin.toggle()
}
}
.padding(.vertical, 10)
.padding(.horizontal, 15)
}
.frame(width: geometry.size.width, height: 44)
.background(Color.orange)
List(dataModel.repositories) { res in
GithubListCell(repository: res)
}
}
}
.sheet(isPresented: $showLogin) {
LoginView()
}
}
}
上边代码非常简单,没有任何数据相关的处理逻辑,这些处理数据的逻辑全都在MyViewModel
中进行,妙的地方在于,如果View中依赖了MyViewModel
后,那么当MyViewModel
数据改编后,View自动刷新。
@StateObject
初始化dataModel
,让View管理其生命周期GeometryReader
可以获取到父View的frameGithubListCell
是每个仓库cell的封装,代码就不贴上来了,可以下载代码查看重点来了,我们看看MyViewModel
中的内容:
final class MyViewModel: ObservableObject {
@Published var inputText: String = ""
@Published var repositories = [GithubRepository]()
@Published var loading = false
var cancellable: AnyCancellable?
var cancellable1: AnyCancellable?
let myBackgroundQueue = DispatchQueue(label: "myBackgroundQueue")
init() {
cancellable = $inputText
// .debounce(for: 1.0, scheduler: myBackgroundQueue)
.throttle(for: 1.0, scheduler: myBackgroundQueue, latest: true)
.removeDuplicates()
.print("Github input")
.map { input -> AnyPublisher<[GithubRepository], Never> in
let originalString = "https://api.github.com/search/repositories?q=\(input)"
let escapedString = originalString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
let url = URL(string: escapedString)!
return GithubAPI.fetch(url: url)
.decode(type: GithubRepositoryResponse.self, decoder: JSONDecoder())
.map {
$0.items
}
.replaceError(with: [])
.eraseToAnyPublisher()
}
.switchToLatest()
.receive(on: RunLoop.main)
.assign(to: \.repositories, on: self)
cancellable1 = GithubAPI.networkActivityPublisher
.receive(on: RunLoop.main)
.assign(to: \.loading, on: self)
}
}
在这里,我会大概讲解主要代码的用途,不会太过详细,因为这些内容在之前的文章中已经详细讲过了。
$inputText
:当我们在用@Published
装饰过的属性前边加一个$
符号后,就能获取一个Publisher.debounce(for: 1.0, scheduler: myBackgroundQueue)
: 当有输入时,debounce就会开启一个1秒的时间窗口,如果在1秒内收到了新的数据,则再开启一个新的1秒的时间窗口,之前的窗口作废,直到1秒内没有新的数据,然后发送最后收到的数据,它的核心思想是可以控制频繁的数据发送问题.throttle(for: 1.0, scheduler: myBackgroundQueue, latest: true)
: throttle会开启一系列连续的1秒的时间窗口,每次达到1秒的临界点就发送最近的一个数据,注意,当收到第一个数据时,会立刻发送。.removeDuplicates()
可以去重,比如,当最近收到的两个数据都是swift时,第二个就会被忽略.print("Github input")
可以打印pipline的过程,可以给输出信息加上前缀.map
: 上边map的逻辑是把输入的字符串映射成一个新的Publisher,这个新的Publisher会请求网络,最终输出我们封装好的数据模型GithubRepositoryResponse.self
.decode
用于解析数据.replaceError(with: [])
用于替换错误,如果网络请求出错,则发送一个空的数组.switchToLatest()
用于输出Publisher的数据,如果map返回的是Publisher,就要使用switchToLatest
切换输出.receive(on: RunLoop.main)
用于切换线程.assign(to: \.repositories, on: self)
: assign可以直接使用KeyPath的形式为属性复制,它是一个Subscriber大家看到了吗? 在一个完整的处理过程中,用到了很多Operators,通过组合使用这些Operator,几乎能实现任何需求。
我们再看看 GithubAPI的封装:
enum GithubAPIError: Error, LocalizedError {
case unknown
case apiError(reason: String)
case networkError(from: URLError)
var errorDescription: String? {
switch self {
case .unknown:
return "Unknown error"
case .apiError(let reason):
return reason
case .networkError(let from):
return from.localizedDescription
}
}
}
struct GithubAPI {
/// 加载
static let networkActivityPublisher = PassthroughSubject<Bool, Never>()
/// 请求数据
static func fetch(url: URL) -> AnyPublisher<Data, GithubAPIError> {
return URLSession.shared.dataTaskPublisher(for: url)
.handleEvents(receiveCompletion: { _ in
networkActivityPublisher.send(false)
}, receiveCancel: {
networkActivityPublisher.send(false)
}, receiveRequest: { _ in
networkActivityPublisher.send(true)
})
.tryMap { data, response in
guard let httpResponse = response as? HTTPURLResponse else {
throw GithubAPIError.unknown
}
switch httpResponse.statusCode {
case 401:
throw GithubAPIError.apiError(reason: "Unauthorized")
case 403:
throw GithubAPIError.apiError(reason: "Resource forbidden")
case 404:
throw GithubAPIError.apiError(reason: "Resource not found")
case 405..<500:
throw GithubAPIError.apiError(reason: "client error")
case 500..<600:
throw GithubAPIError.apiError(reason: "server error")
default: break
}
return data
}
.mapError { error in
if let err = error as? GithubAPIError {
return err
}
if let err = error as? URLError {
return GithubAPIError.networkError(from: err)
}
return GithubAPIError.unknown
}
.eraseToAnyPublisher()
}
}
GithubAPIError
是对各种Error的一个封装,有兴趣可以看看Alamofirez中的AFErrornetworkActivityPublisher
是一个Subject,本质上也是一个Publisher,用于发送网络加载的通知事件,大家可以看上边视频左上角的loading,就是用networkActivityPublisher
实现的URLSession.shared.dataTaskPublisher(for: url)
是最常见的网络请求Publisher.handleEvents
可以监听pipline中的事件.tryMap
是一种特殊的Operator,它主要用于数据映射,但允许throw异常.mapError
用于处理错误信息,,在上边的代码中,我们做了错误映射的逻辑,错误映射的核心思想是把各种各样的错误映射成自定义的错误类型.eraseToAnyPublisher()
用于磨平Publisher的类型,这个就不多做介绍了总结一下,很多同学可能无法立刻体会到上边代码的精妙之处,响应式编程的妙处就在于我们提前铺设好数据管道,数据就会自动在管道中流动,实在是秒啊。
如果说网络请求是对异步数据的处理,那么模拟登录就是对多个数据流的处理,让我们先简单看一下UI代码:
struct LoginView: View {
@StateObject private var dataModel = LoginDataModel()
@State private var showAlert = false
var body: some View {
VStack {
TextField("请输入用户名", text: $dataModel.userName)
.textFieldStyle(RoundedBorderTextFieldStyle())
if dataModel.showUserNameError {
Text("用户名不能少于3位!!!")
.foregroundColor(Color.red)
}
SecureField("请输入密码", text: $dataModel.password)
.textFieldStyle(RoundedBorderTextFieldStyle())
if dataModel.showPasswordError {
Text("密码不能少于6位!!!")
.foregroundColor(Color.red)
}
GeometryReader { geometry in
Button(action: {
self.showAlert.toggle()
}) {
Text("登录")
.foregroundColor(dataModel.buttonEnable ? Color.white : Color.white.opacity(0.3))
.frame(width: geometry.size.width, height: 35)
.background(dataModel.buttonEnable ? Color.blue : Color.gray)
.clipShape(Capsule())
}
.disabled(!dataModel.buttonEnable)
}
.frame(height: 35)
}
.padding()
.border(Color.green)
.padding()
.animation(.easeInOut)
.alert(isPresented: $showAlert) {
Alert(title: Text("登录成功"),
message: Text("\(dataModel.userName) \n \(dataModel.password)"),
dismissButton: nil)
}
.onDisappear {
dataModel.clear()
}
}
}
具体涉及到SwiftUI的知识就不再复述了,套路都是相同的,在上边的UI代码中,我们直接拿LoginDataModel
来使用,所有的业务逻辑都封装在LoginDataModel
之中。
class LoginDataModel: ObservableObject {
@Published var userName: String = ""
@Published var password: String = ""
@Published var buttonEnable = false
@Published var showUserNameError = false
@Published var showPasswordError = false
var cancellables = Set<AnyCancellable>()
var userNamePublisher: AnyPublisher<String, Never> {
return $userName
.receive(on: RunLoop.main)
.map { value in
guard value.count > 2 else {
self.showUserNameError = value.count > 0
return ""
}
self.showUserNameError = false
return value
}
.eraseToAnyPublisher()
}
var passwordPublisher: AnyPublisher<String, Never> {
return $password
.receive(on: RunLoop.main)
.map { value in
guard value.count > 5 else {
self.showPasswordError = value.count > 0
return ""
}
self.showPasswordError = false
return value
}
.eraseToAnyPublisher()
}
init() {
Publishers
.CombineLatest(userNamePublisher, passwordPublisher)
.map { v1, v2 in
!v1.isEmpty && !v2.isEmpty
}
.receive(on: RunLoop.main)
.assign(to: \.buttonEnable, on: self)
.store(in: &cancellables)
}
func clear() {
cancellables.removeAll()
}
deinit {
}
}
仔细观察上边的代码,它是声明式的,对各个数据的处理是如此的清晰:
userNamePublisher
来处理用户名的逻辑passwordPublisher
来处理密码的逻辑CombineLatest
来合并用户名和密码的数据,用于控制登录按钮的状态它确实是声明式的,如果从上往下看,它很像一份说明书,而不是一堆变量的计算。
在此,我也懒得写非Combine的对照代码了,大家可以仔细理解代码,细细品味其中韵味。
本文写的不算复杂,也不算全面,并非一个完整的实战内容,只是让大家看一下Combine在真实开发场景的例子。本教程后续还有3篇文章,分别讲解如何自定义Publisher,Operator和Subscriber,算是进阶内容,大家拭目以待吧。
]]>本文虽然主要讲解如何自定义Subscriber,但在真实的开发中是没有必要这样做的,从上图可以看出,Subscriber一共做了3件事:
一般来说,当Subscriber订阅了某个Publisher并收到subscription(订阅凭证)后,会立刻发送request,然后就等待数据就行了。
如果想控制订阅的时机,比如说点击了某个按钮后再订阅,那么就在点击了按钮后调用.sink()
就可以了,没必要自定义sink
如果想控制发送request的时机,比如说延时5秒发送请求,那也没必要自定义sink,只需延时5秒调用.sink()
就可以
如果想处理数据,那么在闭包里操作就行了,没必要把这个处理细节封装起来
本文只是探讨sink的自定义问题, 目的是让大家学习一下Combine中sink的实现方式。
我们先看看Combine中Sink类的定义:
extension Subscribers {
/// A simple subscriber that requests an unlimited number of values upon subscription.
final public class Sink<Input, Failure> : Subscriber, Cancellable, CustomStringConvertible, CustomReflectable, CustomPlaygroundDisplayConvertible where Failure : Error {
/// The closure to execute on receipt of a value.
final public var receiveValue: (Input) -> Void { get }
/// The closure to execute on completion.
final public var receiveCompletion: (Subscribers.Completion<Failure>) -> Void { get }
final public var description: String { get }
final public var customMirror: Mirror { get }
/// A custom playground description for this instance.
final public var playgroundDescription: Any { get }
public init(receiveCompletion: @escaping ((Subscribers.Completion<Failure>) -> Void), receiveValue: @escaping ((Input) -> Void))
final public func receive(subscription: Subscription)
final public func receive(_ value: Input) -> Subscribers.Demand
final public func receive(completion: Subscribers.Completion<Failure>)
/// Cancel the activity.
final public func cancel()
}
}
从上边的代码可以看出,Sink
是一个实现了Subscriber
,Cancellable
等多个协议的类,因此下边的这些方法都是协议中的方法。
我们比较关心的是Subscriber
协议,既然Sink
实现了该协议,那么我们就可以用它的实例对象来订阅Publisher,像下边这样使用:
let publisher = PassthroughSubject<Int, Never>()
let sink = Subscribers.Sink<Int, Never>(receiveCompletion: {
print($0)
}, receiveValue: {
print($0)
})
publisher.subscribe(sink)
publisher.send(1)
上边的代码等价于:
publisher
.sink(receiveCompletion: {
print($0)
}, receiveValue: {
print($0)
})
我觉得有必要讲解一下为什么上边的代码是等价的,关键在于上边代码中的sink方法:
extension Publisher {
public func sink(receiveCompletion: @escaping ((Subscribers.Completion<Self.Failure>) -> Void), receiveValue: @escaping ((Self.Output) -> Void)) -> AnyCancellable
}
可以看出,首先它是Publisher
协议的方法,因此,所有的Publishers都可以调用,其次,该方法内部只是创建了一个Subscribers.Sink
,然后将其返回即可,代码如下:
extension Publisher {
public func testSink(receiveCompletion: @escaping ((Subscribers.Completion<Self.Failure>) -> Void),
receiveValue: @escaping ((Self.Output) -> Void)) -> AnyCancellable {
let sink = Subscribers.Sink<Self.Output, Self.Failure>(receiveCompletion: {
receiveCompletion($0)
}, receiveValue: {
receiveValue($0)
})
self.subscribe(sink)
return AnyCancellable(sink)
}
}
在上边的代码中,我特意把sink
写成了testSink
做个区分,可以看出,本质上就是在testSink
函数内创建了一个Sink
的实例,因此,我们可以像下边这样使用:
let publisher = PassthroughSubject<Int, Never>()
cancellable = publisher.testSink(receiveCompletion: {
print($0)
}, receiveValue: {
print($0)
})
publisher.send(1)
大家仔细品一品,.sink()
只是对外暴露出的一个简单的函数接口,真正的核心是Sink
,因为它实现了Subscriber
和Cancellable
协议。
那么重点来了,我们就来看看Sink
在这些协议方法中做了什么事?
extension Subscribers {
final public class CustomSink<Input, Failure>: Subscriber, Cancellable where Failure: Error {
let receiveCompletion: (Subscribers.Completion<Failure>) -> Void
let receiveValue: (Input) -> Void
var subscription: Subscription?
init(receiveCompletion: @escaping ((Subscribers.Completion<Failure>) -> Void),
receiveValue: @escaping ((Input) -> Void)) {
self.receiveCompletion = receiveCompletion
self.receiveValue = receiveValue
}
public func receive(subscription: Subscription) {
self.subscription = subscription
self.subscription?.request(.unlimited)
}
public func receive(_ input: Input) -> Subscribers.Demand {
receiveValue(input)
return .none
}
public func receive(completion: Subscribers.Completion<Failure>) {
receiveCompletion(completion)
subscription = nil
}
public func cancel() {
subscription?.cancel()
subscription = nil
}
}
}
CustomSink
就是我们自定义的实现了Subscriber
和Cancellable
协议的类,代码很容易理解,我就不做更多介绍了。值得注意的有以下2点:
receive(_ input: Input)
函数的返回值类型是Subscribers.Demand
,为什么需要给一个返回值呢?原因是当CustomSink
通过该方法收到数据后,可以返回一个值,告诉Publisher当达到接受的最大值时还可以接收更多的值,举个例子,比如说假设我们自定义的CustomSink
接收值不是无限的,而是最多接收3个,那么在发送request时,代码是这样的self.subscription?.request(.max(3))
,这种情况下最多只能接收3个值,我们可以改动一下代码,当receive(_ input: Input)
收到第3个值的时候,我们返回return .max(1)
,这样就能接收4个值了self.subscription?.request(.max(3))
我们首先把request中的参数设置为最大接收3个值,然后试一下:
let publisher = PassthroughSubject<Int, Never>()
cancellable = publisher.customSink(receiveCompletion: {
print($0)
}, receiveValue: {
print($0)
})
publisher.send(1)
publisher.send(2)
publisher.send(3)
publisher.send(4)
publisher.send(5)
打印结果:
1
2
3
说明最多只能接收3个数据,然后,我们修改一下代码。改动如下:
extension Subscribers {
final public class CustomSink<Input, Failure>: Subscriber, Cancellable where Failure: Error {
...
var count = 0
...
public func receive(_ input: Input) -> Subscribers.Demand {
receiveValue(input)
count += 1
if count == 3 {
return .max(1)
} else {
return .none
}
}
...
}
}
我只增加了一个count
属性来记录当前接收数据的个数,当等于3时,返回了一个return .max(1)
,根据我们上边的解释,这时候就可以额外接收一个数据,打印如下:
1
2
3
4
大家明白了吗?这种方式很灵活,在某些场景下可以像上边那样来增加新的接收的参数。
接下来只需要在Publisher
下暴露出一个接口就可以了:
extension Publisher {
public func customSink(receiveCompletion: @escaping ((Subscribers.Completion<Self.Failure>) -> Void),
receiveValue: @escaping ((Self.Output) -> Void)) -> AnyCancellable {
let sink = Subscribers.CustomSink<Self.Output, Self.Failure>(receiveCompletion: {
receiveCompletion($0)
}, receiveValue: {
receiveValue($0)
})
self.subscribe(sink)
return AnyCancellable(sink)
}
}
总起来说,自定义Subscriber是一件非常简单的事,也是一件不必要的事,Subscriber最核心的思想只是接收数据和事件,对数据和事件不做任何逻辑。
]]>自定义Operator是整个Combine教程中难度最高的内容,因为它连接了Publisher和Subscriber,起到了一个中间桥梁的作用。
那么难点在哪里呢?我希望读者朋友能够带着下边3个问题来仔细读这篇文章:
上边3个问题就是本文的核心,下边的讲解的代码来自CombineExt
查看全部Combine教程,请访问:FuckingSwiftUI
所谓的组合就是指使用已有的Publisher和Operator组合成具有新功能的Operator,举个例子:
public extension Publisher where Output: Collection {
func mapMany<Result>(_ transform: @escaping (Output.Element) -> Result) -> Publishers.Map<Self, [Result]> {
map { $0.map(transform) }
}
}
上边代码中的.mapMany()
就是通过组合生成的一个新的Operator,它的用法如下:
let intArrayPublisher = PassthroughSubject<[Int], Never>()
intArrayPublisher
.mapMany(String.init)
.sink(receiveValue: { print($0) })
intArrayPublisher.send([10, 2, 2, 4, 3, 8])
// Output: ["10", "2", "2", "4", "3", "8"]
可以看出,.mapMany()
的功能就是按照给出的规则映射Collection中的所有元素,上边的代码是非常简单的,我们可以模仿这种模式来组合生成任何其他的Operator。
有意思的一点是,.mapMany()
输出类型通过代码public extension Publisher where Output: Collection
约束成了Collection
。也就是说该Operator的输入数据必须是Collection。
当然,大多数情况下没必要像上边这样写代码,这个看个人的喜好,上边的代码与下边的代码等价:
let intArrayPublisher = PassthroughSubject<[Int], Never>()
cancellable = intArrayPublisher
.map {
$0.map { String($0) }
}
.sink(receiveValue: { print($0) })
intArrayPublisher.send([10, 2, 2, 4, 3, 8])
我们将会使用CombineExt中的amb
来演示如何自定义Operator,要想弄明白本文的内容,前提条件是对Combine有一定的了解,对CombineExt有一定的研究,迫切想知道如何自定义Operator。再回到amb
,它是一个非常有意思的Operator,我们先看看它的用法:
let subject1 = PassthroughSubject<Int, Never>()
let subject2 = PassthroughSubject<Int, Never>()
subject1
.amb(subject2)
.sink(receiveCompletion: { print("amb: completed with \($0)") },
receiveValue: { print("amb: \($0)") })
subject2.send(3) // Since this subject emit first, it becomes the active publisher
subject1.send(1)
subject2.send(6)
subject1.send(8)
subject1.send(7)
subject1.send(completion: .finished)
// Only when subject2 finishes, amb itself finishes as well, since it's the active publisher
subject2.send(completion: .finished)
打印结果:
amb: 3
amb: 6
amb: completed with .finished
从上边的代码可以看出,subject1
和subject2
谁先发送数据谁就会被激活,另一个则被忽略,这种行为很像是淘汰赛,只有第一名才会被保留。
这个Operator特别适合讲解如何自定义Operator,因为它的用法不算复杂,接下来我们就进入正题。
要想讲述清楚amb
的创作过程,我们需要反向推演,我们先看看当我们调用了下边代码后,是怎样的一个过程:
subject1
.amb(subject2)
public extension Publisher {
func amb<Other: Publisher>(_ other: Other)
-> Publishers.Amb<Self, Other> where Other.Output == Output, Other.Failure == Failure {
Publishers.Amb(first: self, second: other)
}
}
从上边的代码中,我们可以分析出以下几点信息:
amb()
函数的入参必须是一个Publisher,这算是一个约束条件amb()
函数的返回值是Publishers.Amb
,同样也是一个Publisher,后边给出的约束条件约束了这两个Publisher的输入和输出类型必须相同从上边的代码可以看出,所谓的Operator就是Publisher协议的一个extension,因此我们能够获取到当前的Publisher,然后这个函数中需要返回一个Publisher,这样就实现了链式调用。
因此,现在的问题指向了Publishers.Amb
,我们需要解决的问题是:如何处理上边提到的淘汰逻辑?如何响应Subscriber的订阅和请求?
我们看看Publishers.Amb
的代码:
public extension Publishers {
struct Amb<First: Publisher, Second: Publisher>: Publisher where First.Output == Second.Output, First.Failure == Second.Failure {
public func receive<S: Subscriber>(subscriber: S) where Failure == S.Failure, Output == S.Input {
subscriber.receive(subscription: Subscription(first: first,
second: second,
downstream: subscriber))
}
public typealias Output = First.Output
public typealias Failure = First.Failure
private let first: First
private let second: Second
public init(first: First,
second: Second) {
self.first = first
self.second = second
}
}
}
代码看起来非常简单,只是持有了这2个Publisher,由于Amb
实现了Publisher协议,那么重点就在于如何处理订阅的逻辑了:
Subscription(first: first,
second: second,
downstream: subscriber)
在以前的文章中,我们提到过,Subscription是沟通Publisher和Subscriber的一座桥梁,因此,这个Subscription里边的逻辑就显得非常重要。
我们看看它的代码:
private extension Publishers.Amb {
class Subscription<Downstream: Subscriber>: Combine.Subscription where Output == Downstream.Input, Failure == Downstream.Failure {
private var firstSink: Sink<First, Downstream>?
private var secondSink: Sink<Second, Downstream>?
private var preDecisionDemand = Subscribers.Demand.none
private var decision: Decision? {
didSet {
guard let decision = decision else { return }
switch decision {
case .first:
secondSink = nil
case .second:
firstSink = nil
}
request(preDecisionDemand)
preDecisionDemand = .none
}
}
init(first: First,
second: Second,
downstream: Downstream) {
self.firstSink = Sink(upstream: first,
downstream: downstream) { [weak self] in
guard let self = self,
self.decision == nil else { return }
self.decision = .first
}
self.secondSink = Sink(upstream: second,
downstream: downstream) { [weak self] in
guard let self = self,
self.decision == nil else { return }
self.decision = .second
}
}
func request(_ demand: Subscribers.Demand) {
guard decision != nil else {
preDecisionDemand += demand
return
}
firstSink?.demand(demand)
secondSink?.demand(demand)
}
func cancel() {
firstSink = nil
secondSink = nil
}
}
}
上边的代码比较长,我们拆分一下,我们先看初始化方法:
init(first: First,
second: Second,
downstream: Downstream) {
self.firstSink = Sink(upstream: first,
downstream: downstream) { [weak self] in
guard let self = self,
self.decision == nil else { return }
self.decision = .first
}
self.secondSink = Sink(upstream: second,
downstream: downstream) { [weak self] in
guard let self = self,
self.decision == nil else { return }
self.decision = .second
}
}
downstream在这里就是Subscriber,Sink
我们先别管,下边会解释,现在只需要把它当作一个新的桥梁,它能够连接Publisher和Subscriber。
上边firstSink的Sink
初始化函数中的闭包的调用时机是: 当第一次收到first这个Publisher的事件时调用,不管是收到数据还是收到完成事件,这个我们在后续讲解Sink
的时候会讲解。
同理,secondSink跟firstSink差不多,在上边的初始化函数中,我们就找到了上边第一个问题的答案,当第一次收到first或second的事件后,就为decision赋值了,decision是一个enum,因此他是可以区分是first还是second。
private enum Decision {
case first
case second
}
到目前为止,大家应该仍然是糊涂的,因为大家对Sink还不是很了解, 我们必须先把这个Sink讲解了才能继续下去:
class Sink<Upstream: Publisher, Downstream: Subscriber>: Subscriber {
typealias TransformFailure = (Upstream.Failure) -> Downstream.Failure?
typealias TransformOutput = (Upstream.Output) -> Downstream.Input?
private(set) var buffer: DemandBuffer<Downstream>
private var upstreamSubscription: Subscription?
private let transformOutput: TransformOutput?
private let transformFailure: TransformFailure?
init(upstream: Upstream,
downstream: Downstream,
transformOutput: TransformOutput? = nil,
transformFailure: TransformFailure? = nil) {
self.buffer = DemandBuffer(subscriber: downstream)
self.transformOutput = transformOutput
self.transformFailure = transformFailure
upstream.subscribe(self)
}
func demand(_ demand: Subscribers.Demand) {
let newDemand = buffer.demand(demand)
upstreamSubscription?.requestIfNeeded(newDemand)
}
func receive(subscription: Subscription) {
upstreamSubscription = subscription
}
func receive(_ input: Upstream.Output) -> Subscribers.Demand {
...
}
func receive(completion: Subscribers.Completion<Upstream.Failure>) {
...
}
func cancelUpstream() {
upstreamSubscription.kill()
}
deinit { cancelUpstream() }
}
我省略了一些不重要的代码,我们仔细分析下上边的代码:
Sink
实现了Subscriber
协议,这说明了它本身就是一个订阅者,通常我们用它订阅upstream,这么做的目的是方便操作upstream输出的数据和request。DemandBuffer
我们之前的文章已经讲过了,它做数据管理,只复杂把数据发送给downstreamtransformOutput
和transformFailure
数据转换函数,我们这里不讲了Sink的核心思想就是通过亲自订阅上游的Publisher来接收数据和事件,通过DemandBuffer来管理这些数据和事件,当需要时,发送给下游的订阅者。
上边Sink的设计很重要,它是一个中间过程,本质上是因为它本身就是一个Subscriber订阅者,因此不仅能够获取到上游的数据,还剩自己控制发送rquest。
我们再重复一遍这个过程,先看下图:
当执行下边代码时,究竟发生了什么?
subject1
.amb(subject2)
.sink(receiveCompletion: { print("amb: completed with \($0)") },
receiveValue: { print("amb: \($0)") })
subject1就是上图中的Publisher,.amb()
返回了上图中的Amb, 当上边代码中调用了.sink()
后,Amb
就收到了订阅,会调用下边的代码:
public extension Publishers {
struct Amb<First: Publisher, Second: Publisher>: Publisher where First.Output == Second.Output, First.Failure == Second.Failure {
public func receive<S: Subscriber>(subscriber: S) where Failure == S.Failure, Output == S.Input {
subscriber.receive(subscription: Subscription(first: first,
second: second,
downstream: subscriber))
}
}
}
当收到订阅后,需要返回一个subscription,也就是订阅凭证,因为后边的Subscriber需要使用这个凭证来发送请求或者取消pipline。
由于上图中绿色的.sink()
是系统方法,我们无法看到实现,但是,我们知道,当.sink()
收到订阅凭证后就会发送request,也就是上图中的紫色虚线。
请注意,Amb
里边的内容完全是我们自定义的,所以我们能够完全控制,当收到.sink()
的request后,会调用Subscription
下边的函数:
private extension Publishers.Amb {
class Subscription<Downstream: Subscriber>: Combine.Subscription where Output == Downstream.Input, Failure == Downstream.Failure {
...
func request(_ demand: Subscribers.Demand) {
guard decision != nil else {
preDecisionDemand += demand
return
}
firstSink?.demand(demand)
secondSink?.demand(demand)
}
...
}
}
.sink()
传过来的demand的值是.unlimited
,表示不限制数据的接收个数,观察上边的代码,decision
表示当前使用的Publisher是哪个,subject1或者subject2谁第一个发送数据,decision就指向谁。
由于这个request是收到订阅凭证后立刻发出的,这时候subject1和subject2都没有发送数据,因此decision
为nil,上边的代码就把.sink()
传过来的demand保存在preDecisionDemand属性中了,后边会把这个demand透传给胜出的Publisher(subject1或subject2)。
那么重点来了,subject1,subject2竞争的代码在什么地方呢?答案是放在了上边Subscription初始化方法中了:
private extension Publishers.Amb {
class Subscription<Downstream: Subscriber>: Combine.Subscription where Output == Downstream.Input, Failure == Downstream.Failure {
private var firstSink: Sink<First, Downstream>?
private var secondSink: Sink<Second, Downstream>?
private var preDecisionDemand = Subscribers.Demand.none
private var decision: Decision? {
didSet {
guard let decision = decision else { return }
switch decision {
case .first:
secondSink = nil
case .second:
firstSink = nil
}
request(preDecisionDemand)
preDecisionDemand = .none
}
}
init(first: First,
second: Second,
downstream: Downstream) {
self.firstSink = Sink(upstream: first,
downstream: downstream) { [weak self] in
guard let self = self,
self.decision == nil else { return }
self.decision = .first
}
self.secondSink = Sink(upstream: second,
downstream: downstream) { [weak self] in
guard let self = self,
self.decision == nil else { return }
self.decision = .second
}
}
...
}
}
还记得Subscription什么时候初始化吗?就是当收到.sink()
的订阅后创建的。上边的init()
很简单,分别创建了两个Sink,firstSink
代表subject1,secondSink
代表subject2。
在上边的小节中,我们已经知道,Sink的闭包参数的调用时机是当收到第一个参数时调用,再结合上边的代码,我们就可以看出,当firstSink或者secondSink其中一个第一次收到数据后,就决定了decision的值,并且在decision的didSet
中,这时候就选中了哪个Publisher作为发送数据的Publisher,另一个则赋值为nil,之后我们重新调用了request(preDecisionDemand)
,把之前保存的demand透传给胜出的Publisher。
此时此刻,我们的头脑中应该有两个疑问:
firstSink?.demand(demand)
是如何实现把demand透传subject1的?这两个问题的核心都指向了Sink,注意,这个Sink很有意思,本文的最上边也讲到了,它实现了Subscriber
协议,这一点很重要,我们看看它的初始化方法中干了啥?
init(upstream: Upstream,
downstream: Downstream,
transformOutput: TransformOutput? = nil,
transformFailure: TransformFailure? = nil) {
self.buffer = DemandBuffer(subscriber: downstream)
self.transformOutput = transformOutput
self.transformFailure = transformFailure
upstream.subscribe(self)
}
看明白了吗?由于Sink本身就是一个Subscriber,因此,它订阅了传进来的上游Publisher。
func receive(subscription: Subscription) {
upstreamSubscription = subscription
}
并且能够拿到上游Publisher传过来的subscription,因此可以使用这个subscription发送request。
到此为止,上边的两个问题的答案已经呼之欲出了。
总结一下,Amb
中自定义的Subscription
作为沟通下游.sink()
的桥梁接收request,Subscription
中持有的Sink
订阅了上游的Publisher,它作为Publisher和.sink()
的中间桥梁,透传demand和数据。
那么回到开头的3个问题,你有答案了吗?
当初特别好奇,Combine中的Operator是如何实现的?因为它确实比较特殊,它的上游是Publisher或者Operator,下游是Operator或Subscriber。本文讲解的内容可以作为一个套路来学习,如果需要自定义Operator,可以参考这篇文章。
]]>这篇文章的主要代码来源于CombineExt
/// 请求数据
static func fetch(url: URL) -> AnyPublisher<Data, GithubAPIError> {
return URLSession.shared.dataTaskPublisher(for: url)
.handleEvents(receiveCompletion: { _ in
networkActivityPublisher.send(false)
}, receiveCancel: {
networkActivityPublisher.send(false)
}, receiveRequest: { _ in
networkActivityPublisher.send(true)
})
.tryMap { data, response in
guard let httpResponse = response as? HTTPURLResponse else {
throw GithubAPIError.unknown
}
switch httpResponse.statusCode {
case 401:
throw GithubAPIError.apiError(reason: "Unauthorized")
case 403:
throw GithubAPIError.apiError(reason: "Resource forbidden")
case 404:
throw GithubAPIError.apiError(reason: "Resource not found")
case 405..<500:
throw GithubAPIError.apiError(reason: "client error")
case 500..<600:
throw GithubAPIError.apiError(reason: "server error")
default: break
}
return data
}
.mapError { error in
if let err = error as? GithubAPIError {
return err
}
if let err = error as? URLError {
return GithubAPIError.networkError(from: err)
}
return GithubAPIError.unknown
}
.eraseToAnyPublisher()
}
上边的代码就是一个使用Operator组合的例子,我们并没有自定义任何Publisher,但最后我们生成了一个AnyPublisher<Data, GithubAPIError>
类型的Publisher。
大家仔细想想,这种通过组合来实现某种功能的方式和自定义Publisher是不是没啥区别?也就是说,在开发中,尽可能的使用这种组合的方式解决问题。
我们这一小节将会演示一个完整的自定义Publisher的例子,Create
这个Publisher的使用方法跟Combine中的Record
很像,这是一个非常完美的示例,功能相同确能看到实现代码。Record
的用法如下:
let recordPublisher = Record<String, MyCustomError> { recording in
recording.receive("你")
recording.receive("好")
recording.receive("吗")
recording.receive(completion: Subscribers.Completion.finished)
}
而Create
的用法如下:
AnyPublisher<String, MyError>.create { subscriber in
// Values
subscriber.send("Hello")
subscriber.send("World!")
// Complete with error
subscriber.send(completion: .failure(MyError.someError))
// Or, complete successfully
subscriber.send(completion: .finished)
return AnyCancellable {
// Perform cleanup
}
}
在学习新技术的时候,我们要慢慢学会通过观察代码的使用方法,来尝试推断代码的设计思想。 我们尝试分析一下上边代码的思想:
AnyPublisher<String, MyError>.create
表明create
是AnyPublisher
的一个静态函数,该函数接收一个闭包作为参数subscriber
至少有两个方法:send()
和send(completion:)
,一个用于发送数据。一个用于发送完成事件AnyCancellable
接下来,我们从代码层次来进一步分析上边代码的具体实现过程。
// MARK: - Publisher
@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
public extension Publishers {
/// A publisher which accepts a closure with a subscriber argument,
/// to which you can dynamically send value or completion events.
///
/// You should return a `Cancelable`-conforming object from the closure in
/// which you can define any cleanup actions to execute when the pubilsher
/// completes or the subscription to the publisher is canceled.
struct Create<Output, Failure: Swift.Error>: Publisher {
public typealias SubscriberHandler = (Subscriber) -> Cancellable
private let factory: SubscriberHandler
/// Initialize the publisher with a provided factory
///
/// - parameter factory: A factory with a closure to which you can
/// dynamically push value or completion events
public init(factory: @escaping SubscriberHandler) {
self.factory = factory
}
public func receive<S: Combine.Subscriber>(subscriber: S) where Failure == S.Failure, Output == S.Input {
subscriber.receive(subscription: Subscription(factory: factory, downstream: subscriber))
}
}
}
当我们要自定义Publisher时,从宏观应该考虑以下2点:
Publishers
的extension
,方便导出类型,比如上边代码中,导出的类型就是Publishers.Create
Publisher
协议,其中最核心的是要给subscriber发送一个Subscription
,这个Subscription
是最核心的内容,我们下边详细讲解在上边的代码中,Create
通过一个闭包(SubscriberHandler = (Subscriber) -> Cancellable
)来进行初始化,我们先研究一下这个闭包,不难看出,闭包的参数是个Subscriber
类型,我们看看它的代码:
public extension Publishers.Create {
struct Subscriber {
private let onValue: (Output) -> Void
private let onCompletion: (Subscribers.Completion<Failure>) -> Void
fileprivate init(onValue: @escaping (Output) -> Void,
onCompletion: @escaping (Subscribers.Completion<Failure>) -> Void) {
self.onValue = onValue
self.onCompletion = onCompletion
}
/// Sends a value to the subscriber.
///
/// - Parameter value: The value to send.
public func send(_ input: Output) {
onValue(input)
}
/// Sends a completion event to the subscriber.
///
/// - Parameter completion: A `Completion` instance which indicates whether publishing has finished normally or failed with an error.
public func send(completion: Subscribers.Completion<Failure>) {
onCompletion(completion)
}
}
}
上边的代码,包含了以下几条信息:
onValue
和onCompletion
,不能在外部调用subscriber.send("Hello")
时,本质上是调用了onValue
subscriber.send(completion: .finished)
时,本质上是调用了onCompletion
总结一下:Subscriber对外暴露了两个函数接口,调用后,会触发闭包,至于闭包中的操作,我们在下文中会讲到。
接来下就是重点了,我们需要自定义Subscription
,数据的处理逻辑都在它里边,它起到了一个承上启下的核心功能。
private extension Publishers.Create {
class Subscription<Downstream: Combine.Subscriber>: Combine.Subscription where Output == Downstream.Input, Failure == Downstream.Failure {
private let buffer: DemandBuffer<Downstream>
private var cancelable: Cancellable?
init(factory: @escaping SubscriberHandler,
downstream: Downstream) {
self.buffer = DemandBuffer(subscriber: downstream)
let subscriber = Subscriber(onValue: { [weak self] in _ = self?.buffer.buffer(value: $0) },
onCompletion: { [weak self] in self?.buffer.complete(completion: $0) })
self.cancelable = factory(subscriber)
}
func request(_ demand: Subscribers.Demand) {
_ = self.buffer.demand(demand)
}
func cancel() {
self.cancelable?.cancel()
}
}
}
所谓的自定义Subscription
,就是实现Combine.Subscription
协议,它有2个目的:
func request(_ demand: Subscribers.Demand)
接收订阅者的数据请求func cancel()
接收订阅者的取消请求仔细观察上边的代码就能够发现,Subscription
的Output和Failure类型必须和下游的订阅者匹配上才行,并且引入了一个private let buffer: DemandBuffer<Downstream>
属性作为数据的缓存单元。
self.buffer = DemandBuffer(subscriber: downstream)
上边这行代码显示,DemandBuffer使用downstream进行初始化,别忘了downstream是一个订阅者,也就是subscriber,在这里,大家只需要理解,DemandBuffer持有了subscriber就可以了。
let subscriber = Subscriber(onValue: { [weak self] in _ = self?.buffer.buffer(value: $0) },
onCompletion: { [weak self] in self?.buffer.complete(completion: $0) })
这行代码就和前边讲过的Subscriber
联系上了,它的onValue
闭包绑定了self?.buffer.buffer(value: $0)
,也就是当调用subscriber.send("Hello")
后,实际上的操作是self?.buffer.buffer(value: "Hello")
,同样的道理,它的onCompletion
闭包绑定了self?.buffer.complete(completion: $0)
,也就是当调用subscriber.send(completion: .finished)
后,实际上的操作是self?.buffer.complete(completion: .finished)
。
self.cancelable = factory(subscriber)
这行代码才是Create
初始化参数闭包的真正调用的地方。
大家应该已经发现了吧?自定义Subscription的核心在于如何做好数据管理,我们还需要搞明白DemandBuffer这个东西的实现原理,它在后边两篇文章中,也起到了核心作用。
最后,我们分析一波DemandBuffer的源码:
class DemandBuffer<S: Subscriber> {
private let lock = NSRecursiveLock()
private var buffer = [S.Input]()
private let subscriber: S
private var completion: Subscribers.Completion<S.Failure>?
private var demandState = Demand()
init(subscriber: S) {
self.subscriber = subscriber
}
func buffer(value: S.Input) -> Subscribers.Demand {
precondition(self.completion == nil,
"How could a completed publisher sent values?! Beats me ♂️")
switch demandState.requested {
case .unlimited:
return subscriber.receive(value)
default:
buffer.append(value)
return flush()
}
}
func complete(completion: Subscribers.Completion<S.Failure>) {
precondition(self.completion == nil,
"Completion have already occured, which is quite awkward ")
self.completion = completion
_ = flush()
}
func demand(_ demand: Subscribers.Demand) -> Subscribers.Demand {
flush(adding: demand)
}
private func flush(adding newDemand: Subscribers.Demand? = nil) -> Subscribers.Demand {
lock.lock()
defer { lock.unlock() }
if let newDemand = newDemand {
demandState.requested += newDemand
}
// If buffer isn't ready for flushing, return immediately
guard demandState.requested > 0 || newDemand == Subscribers.Demand.none else { return .none }
while !buffer.isEmpty && demandState.processed < demandState.requested {
demandState.requested += subscriber.receive(buffer.remove(at: 0))
demandState.processed += 1
}
if let completion = completion {
// Completion event was already sent
buffer = []
demandState = .init()
self.completion = nil
subscriber.receive(completion: completion)
return .none
}
let sentDemand = demandState.requested - demandState.sent
demandState.sent += sentDemand
return sentDemand
}
}
上边的代码虽然看上去很长,但内容并不多,我们可以把它分为3个部分:
我们先看看初始化的代码:
private let lock = NSRecursiveLock()
private var buffer = [S.Input]()
private let subscriber: S
private var completion: Subscribers.Completion<S.Failure>?
private var demandState = Demand()
init(subscriber: S) {
self.subscriber = subscriber
}
从属性let lock = NSRecursiveLock()
不难看出,它为数据的操作增加了安全性,这是非常有必要的,因为pipline专门处理异步数据流。在平时的开发中,我们也可以使用这个锁来保证安全地操作数据,用法如下:
lock.lock()
defer { lock.unlock() }
从属性var buffer = [S.Input]()
可以看出,它内部把数据保存在一个数据之中,数据的类型是Subscriber的输入类型。
从属性let subscriber: S
可以看出,它持有了Subscriber,这个在后边的代码中会用到。
之所以把var completion: Subscribers.Completion<S.Failure>?
这个属性保存起来,主要目的是数组buffer中不能存放该类型的数据,因此需要额外保存。
var demandState = Demand()
表示当前的请求状态,它是一个独立的struct,源码如下:
private extension DemandBuffer {
/// A model that tracks the downstream's
/// accumulated demand state
struct Demand {
var processed: Subscribers.Demand = .none
var requested: Subscribers.Demand = .none
var sent: Subscribers.Demand = .none
}
}
这里的代码可能会让人疑惑,看一下Subscribers.Demand
的定义:
/// A requested number of items, sent to a publisher from a subscriber through the subscription.
@frozen public struct Demand : Equatable, Comparable, Hashable, Codable, CustomStringConvertible {
/// A request for as many values as the publisher can produce.
public static let unlimited: Subscribers.Demand
/// A request for no elements from the publisher.
///
/// This is equivalent to `Demand.max(0)`.
public static let none: Subscribers.Demand
/// Creates a demand for the given maximum number of elements.
///
/// The publisher is free to send fewer than the requested maximum number of elements.
///
/// - Parameter value: The maximum number of elements. Providing a negative value for this parameter results in a fatal error.
@inlinable public static func max(_ value: Int) -> Subscribers.Demand
}
由于它实现了Equatable
,Comparable
和Hashable
这3个协议,所有完全可以把它看作是一个数字,可以进行运算,也可以进行比较,.none
可以看成0,.unlimited
可以看成最大值,也可以用.max
指定一个值。
那么这个值的作用是什么呢?很简单,它表示Subscriber(订阅者)能接受数据的最大个数。 我们看到下边这样的打印结果:
receive subscription: (PassthroughSubject)
request unlimited
就说明Subscriber(订阅者)可以接收任何数量的数据,没有限制。我们再回到前边的代码,var demandState = Demand()
的初始状态都是.none
。
接下来,我们看看第2部分的代码,主要是暴露出的接口,用于给外部调用:
func buffer(value: S.Input) -> Subscribers.Demand {
precondition(self.completion == nil,
"How could a completed publisher sent values?! Beats me ♂️")
switch demandState.requested {
case .unlimited:
return subscriber.receive(value)
default:
buffer.append(value)
return flush()
}
}
func complete(completion: Subscribers.Completion<S.Failure>) {
precondition(self.completion == nil,
"Completion have already occured, which is quite awkward ")
self.completion = completion
_ = flush()
}
func demand(_ demand: Subscribers.Demand) -> Subscribers.Demand {
flush(adding: demand)
}
buffer()
用于处理缓存数据的部分逻辑,当收到外部的调用后,如果请求是不受限的,就直接发送数据给Subscriber,否则,把数据拼接到数组中,然后调用flush()
complete()
用于接收外部的完成事件,保存后调用flush()
demand()
是一个非常奇妙且重要的方法,它的目的是响应一个Demand请求,然后调用flush()
处理这个响应,本质上这个函数中的参数的Demand请求就是Subscriber(订阅者)的请求。我发现用文字讲知识确实比较费劲,没有视频好,大家再看看上边的自定义Subscription的代码:
func request(_ demand: Subscribers.Demand) {
_ = self.buffer.demand(demand)
}
Subscription实现了Combine.Subscription协议,func request(_ demand: Subscribers.Demand)
正是协议中的方法,该方法会被Subscriber(订阅者)调用。
大家如果有任何疑问,可以留言。我们再看看第3部分的内容:
private func flush(adding newDemand: Subscribers.Demand? = nil) -> Subscribers.Demand {
lock.lock()
defer { lock.unlock() }
if let newDemand = newDemand {
demandState.requested += newDemand
}
// If buffer isn't ready for flushing, return immediately
guard demandState.requested > 0 || newDemand == Subscribers.Demand.none else { return .none }
while !buffer.isEmpty && demandState.processed < demandState.requested {
demandState.requested += subscriber.receive(buffer.remove(at: 0))
demandState.processed += 1
}
if let completion = completion {
// Completion event was already sent
buffer = []
demandState = .init()
self.completion = nil
subscriber.receive(completion: completion)
return .none
}
let sentDemand = demandState.requested - demandState.sent
demandState.sent += sentDemand
return sentDemand
}
上边代码很简单,就是当requested > 0的时候,把数据发送给Subscriber,DemandBuffer持有Subscriber的目的就是为了后边调用subscriber.receive(buffer.remove(at: 0))
。我们再重新分析一下Subscription的过程:
首先,我们初始化:
init(factory: @escaping SubscriberHandler,
downstream: Downstream) {
self.buffer = DemandBuffer(subscriber: downstream)
let subscriber = Subscriber(onValue: { [weak self] in _ = self?.buffer.buffer(value: $0) },
onCompletion: { [weak self] in self?.buffer.complete(completion: $0) })
self.cancelable = factory(subscriber)
}
初始化成功后,buffer
中的.flush()
函数并不会把数据透传给Subscriber,当收到订阅接收到订阅者的request后调用下边的代码:
func request(_ demand: Subscribers.Demand) {
_ = self.buffer.demand(demand)
}
然后调用buffer
中的.demand()
函数,.demand()
函数又调用.flush()
,最终遍历数组,把数据全部透传给Subscriber。
如果大家有点蒙,只能多看代码和上边的解释,再细细品一下。
最后,我们回到起始的地方,再回过头来看下边的代码:
public extension AnyPublisher {
init(_ factory: @escaping Publishers.Create<Output, Failure>.SubscriberHandler) {
self = Publishers.Create(factory: factory).eraseToAnyPublisher()
}
static func create(_ factory: @escaping Publishers.Create<Output, Failure>.SubscriberHandler)
-> AnyPublisher<Output, Failure> {
AnyPublisher(factory)
}
}
自定义Publisher的关键是自定义Subscription,Subscription又通过DemandBuffer管理数据,DemandBuffer的核心思想是把数据放入到数组中,然后通过func demand(_ demand: Subscribers.Demand) -> Subscribers.Demand
释放数据。