Swift 中 Protocol 和 泛型

前言

一般在 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 协议

此处定义协议 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 的方式来为对应类型进行扩展, 不仅能够提升开发效率, 还能降低维护成本.

04/28/2023 18:05 下午 posted in  apple

动态库转静态库

把动态库转静态库,减少了动态库数量,除了可以减小加载动态库阶段的耗时,还能额外减少包大小。
并不是所有的动态库都适合转成静态库。实践中发现,如果库中有Resources文件夹,最好不要转换。转换后Bundle发生了变化,有些资源就会访问不到。当然也有解决方案:把动态库的资源都拷贝到Main Bundle中,这样也会有其它方面的问题,不在这里叙说。
项目中的动态库都是Pods管理的,选择我们使用的库,然后点击Build Settings->找到Mach-O Type修改为Static Library。

目前的动态库很少,可以手动修改。如果动态库多,可以在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秒。时间虽然相差很少,但也算是优化了启动时间。

04/22/2023 20:47 下午 posted in  Cocoapods

必须由子类重写的Swift类方法

您有两个选择:

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")
            }
        )
    }
}
12/31/2022 08:47 上午 posted in  apple

『ios』不常用的__attribute__

1.写一个类不想他被子类继承
attribute((objc_subclassing_restricted))

#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]));
    }
}
10/13/2022 14:58 下午

代码自动格式化

前言

每个团队都应该有统一的代码风格和规范,这带来的好处我相信不言而喻,具体我就不多说了,大家都懂的😁。如何更有效率的去做这件事呢,我这次就来说说如何更好的自动格式化你的代码。

现状

大多数 iOS 开发者应该都知道 Xcode 的插件 Clang Format,它是基于 clang-format 命令行工具的一个 Xcode 插件,但是这款插件在Xcode9上已经无法使用了,因为Xcode9的插件机制已经变了。
现在可以使用这一款XcodeClangFormat,具体的使用方式点击链接,大家自行去看吧。这款有个缺点,就是不能像之前那款插件可以设置在保存时自动格式化(这其实也不能怪作者,Xcode新的机制不允许)。 不过使用这种插件还是不够方便,你还得手动选中文件或者代码再按快捷键来格式化,很容易忘,而导致把不规范的代码直接提交到仓库里了。
那么有没有一种方式,可以让我在敲代码的时候随心所欲,提交时又能提醒我然后自动帮我格式化吗?

该怎么做

这里我直接介绍一款神器Space Commander,它利用 Git Hooks ,在 commit 之前检查代码风格是否符合规范,只有符合规范的代码才允许提交,列出不符合规范的文件,同时提供 Shell 脚本来自动格式化。接下来我介绍下如何使用。

  • 1 clone Space Commander
    git clone https://github.com/square/spacecommander.git
  • 2 在项目中安装Space Commander
    cd到你的项目根目录,执行setup-repo.sh脚本发(在你clone下来的项目中,所以要全路径),执行完后会在项目根目录多一个隐藏文件.clang-format,这是一个替身,指向Space Commander仓库中的.clang-format文件,里面默认包含了一系列代码规则,如果你想要用自己的规则,可以去Space Commander仓库中改真身,也可以用新的.clang-format文件替换掉这个替身。
  • 3 让我们提交代码试试
    BasedOnStyle: Chromium
    IndentWidth: 4
    AlignConsecutiveAssignments: true
    AlignConsecutiveDeclarations: true
    ObjCSpaceAfterProperty: true
    PointerAlignment: Right
    BreakBeforeBraces: Attach
    这是我自定义的一些规则,具体.clang-format的写法请参照这个
    好,让我们写一下代码
#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主页查看。

总结

这是我第一次在掘金上写文章(别的地方也没写过多少😂),写的不好,大家海涵呐,多多提意见哈😁。

10/12/2022 16:44 下午

使用Artifactory 1分钟搭建 CocoaPod 私服

痛点

目前的 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 地址:

http://www.jfrogchina.com/artifactory/free-trial/

10/11/2022 07:41 上午

使用fastlane match自动化管理证书和描述文件

转载:https://juejin.cn/post/7121617118100979748
在我们进行团队开发的时候,避免不了证书和描述文件的管理,常规的有自动管理和手动管理两种方式。

1. 常规的管理方式

1.1 自动管理签名

需要在Xcode的 Targets->Signing & Capabilities 勾选 Automatically manage signing。用这种方式,所有的工作包括AppId、证书、描述文件(Provisioning Profile)的创建都由Xcode包办了,非常的方便。

Automatically manage signing.png

这种方式对个人开发者非常友好,但是对团队开发来说有比较大的弊端,具体如下。

1.1.1 每个开发成员都有单独的证书和描述文件,会导致存在大量重复的文件,管理起来非常混乱

如图所示: 16577654793275.png

1.1.2 证书的创建是有上限的,可能会导致其他人无法创建证书。

截屏2022-07-14 上午10.26.37.png

1.1.3 每次添加新设备或证书过期时,都必须手动更新和下载最新的配置文件集

比如说,添加新设备后,如果描述文件配置里面没有勾选这个设备,这个设备是无法安装我们的应用的,所有每次都需要确保描述文件的Select All是否已勾选。

16577663758021.png

1.2 手动管理签名

首先,需要某个团队成员先在 Apple Developer 后台 分别创建开发环境和生产环境的证书和配置文件,然后将这些文件下载安装到本地。当其他人参与开发时,需要这个人将相关的文件导出给其他人。

16577730394088.png

然后需要在 Xcode上 取消勾选 Automatically manage signing,同时设置对应的证书和描述文件(Provisioning Profile)。

截屏2022-07-14 下午2.16.50.png

这种方式的优点是所有开发人员都共用一份证书和描述文件,缺点也非常明显:每次证书过期或者添加新的设备后,都需要手动去更新,然后重新分发其他人,操作起来非常麻烦。

那么,有没有这么一个方案:在一个公共的地方存取这些证书和配置文件,自动化去处理整个流程呢?这就是我们今天要讲的 match 工具。

2. 使用 fastlane match 自动化管理证书和配置文件

matchfastlane 工具套装其中的一个工具,它是 codesigning.guide 概念的实现。 它提供了一种全新的管理证书的方式,使团队所有成员共享一份代码签名,以减免不必要的证书创建、配置文件失效等问题。

2.1 使用 match 有什么优势?

  • 所有的团队成员共享同一份证书和配置文件,减少了管理和维护成本
  • 简化请求证书,生成描述文件,注册设备等一系列繁杂工作
  • 能自动识别已过期的证书和失效的描述文件,对这些文件进行重置
  • 对新开发者极其友好,match使用git管理所有的证书和描述文件,所以只要新人拥有git的访问权限,安装了fastlane,就能快速同步现有的证书配置,远离证书配置的大坑

2.2 如何使用 match ?

下面我将详细介绍 match 的使用流程。

2.2.1 准备一个私有的 git仓库

创建一个私有git仓库来存储证书和描述文件。建议在git账号中配置好SSH Key,这样就可以省去身份校验这一步。至于如何配置SSH Key,请参考:使用 SSH 连接到 GitHub

另外,当有多个App时,建议一个git分支对应一个App,这样,我们所有App的证书都在一个仓库里面,便于管理。

2.2.2 初始化 match

在终端定位到项目目录,注意先看项目目录下是否存在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 的方式,下面我将对这种方式做详细介绍。

截屏2022-07-18 上午10.45.28.png

2.2.3 创建 App Store Connect API 秘钥

App Store Connect API 是官方提供的一套 REST API,可让您在 App Store Connect 中执行的操作自动化。它主要提供了以下功能(包含了证书和描述文件的管理):

16578505249608.png

为什么要使用 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秘钥。

16578593228349.png

然后下载API秘钥(一个.p8文件),保存到项目的fastlane目录。注意:私钥只能下载一次,永远不会过期,保管好,如果丢失了,去App Store Connect后台撤销密钥,否则别人拿到也可以用。

2.2.4 创建自动化脚本

为了更加方便使用,我们通过 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
复制代码

通过上面这个模板我定义了以下几个常用的命令:

  1. fastlane match_all:下载所有需要的证书和描述文件到本地,不会重新创建证书和描述文件(只读方式)
  2. fastlane force_match:强制同步证书和描述文件,如果证书过期或新增了设备,会重新创建证书和描述文件
  3. sync_devices:注册设备,会同步更新描述文件,需要先在 devices.txt 文件录入新增的设备UDID。
  4. fastlane beta:构建测试包,先通过 match 确保adhoc证书和描述文件都是最新且有效的
  5. fastlane release:构建且上传到AppStore,先通过 match 确保 appstore证书和描述文件都是最新且有效的。

当我们有新同事入职,或者需要在新的电脑上配置开发证书和描述文件,我们仅仅只需要一条 fastlane match_all 命令即可。

3. 其他

3.1 如何撤销所有的证书和描述文件

很少会有这种需求,如果确实需要清空所有证书和描述文件的话,可以通过 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
复制代码

注意:清空完所有的证书和描述文件后,已安装的测试包是无法使用的,谨慎使用。

3.2 如何查看设备的UDID

1. 通过Xcode来查看

先通过USB在电脑上连接iOS设备,然后在Xcode中打开菜单:Window -> Device and Simulators,上面显示的 Identifier 这一项就是我们所需要的设备UDID。

2. 通过第三方工具

如果当前无法使用Xcode来查看,如其他地区的同事,可以使用第三方工具。大部分提供应用分发的平台都支持获取UDID,如 Fir获取UDID蒲公英-快速获取iOS设备的UDID

3.3 如何判断某个测试包是否能在设备上运行

原理:描述文件里面包含了所有的支持安装的设备的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 上可通过空格键来快速预览。

Quick Look for ipa & provision

4. 参考资料

10/09/2022 10:38 上午

iOS传感器与CMMotionManager

iOS 中常见传感器如下所示

类型 作用
环境光传感器 感应光照强度
距离传感器 感应靠近设备屏幕的物体
磁力计传感器 感应周边磁场
内部温度传感器 感应设备内部温度(非公开)
湿度传感器 感应设备是否进水(非微电子传感器)
陀螺仪 感应持握方式
加速计 感应设备运动

其中陀螺仪、加速计和磁力计的数据获取均依赖于 CMMotionManager

CMMotionManager

CMMotionManager 是 Core Motion 库的核心类,负责获取和处理手机的运动信息,它可以获取的数据有

  • 加速度,标识设备在三维空间中的瞬时加速度
  • 陀螺仪,标识设备在三个主轴上的瞬时旋转
  • 磁场信息,标识设备相对于地球磁场的方位
  • 设备运动数据,标识关键的运动相关属性,包括设备用户引起的加速度、姿态、旋转速率、相对于校准磁场的方位以及相对于重力的方位等,这些数据均来自于 Core Motion 的传感器融合算法,从这一个数据接口即可获取以上三种数据,因此使用较为广泛

CMMotionManager 有 “push” 和 “pull” 两种方式获取数据,push 方式实时获取数据,采样频率高,pull 方式仅在需要数据时采集数据,Apple 更加推荐这种方式获取数据。

push 方式

将 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 即可。

pull 方式

仍以 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 类,它包含的数据有

1. attitude

attitude 用于标识空间位置的欧拉角(roll、yaw、pitch)和四元数(quaternion)

CMDeviceMotion.attitude属性是CMAttitude类型,表示设备的空间姿态。CMAttitude包含pitch、roll、yaw信息(以弧度为单位):

  • pitch:以X轴为轴的转动角度。
  • roll:以Y轴为轴的转动角度。
  • yaw:以Z轴为轴的转动角度。

其中绕 x 轴运动称作 pitch(俯仰),绕 y 轴运动称作 roll(滚转),绕 z 轴运动称作 yaw(偏航)。

当设备正面向上、顶部指向正北、水平放置时,pitch、yaw 和 roll 值均为 0,其他变化如下

  • 设备顶部上扬,pitch 由 0 递增 pi/2,顶部下沉,由 0 递减 pi/2
  • 设备顶部左偏 180 度范围内,yaw 由 0 递增 pi,右偏递减
  • 设备左部上旋,roll 由 0 递增 pi,左部下旋,roll 由 0 递减

2. rotationRate

rotationRate 标识设备旋转速率,具体变化如下

  • pitch 增加,x > 0,pitch 减少,x < 0
  • roll 增加,y > 0,row 减少,y < 0
  • yaw 增加,z > 0,yaw 减少,z < 0

3. gravity

gravity 用于标识重力在设备各个方向的分量,具体值的变化遵循如下规律:重力方向始终指向地球,而在设备的三个方向上有不同分量,最大可达 1.0,最小是 0.0。

4. userAcceleration

userAcceleration 用于标识设备各个方向上的加速度,注意是加速度值,可以标识当前设备正在当前方向上减速 or 加速。

5. magneticField & heading

magneticField 用于标识设备周围的磁场范围和精度,heading 用于标识北极方向。但是要注意,这两个值的检测需要指定 ReferenceFrame,它是一个 CMAttitudeReferenceFrame 的枚举,有四个值

  • CMAttitudeReferenceFrameXArbitraryZVertical
  • CMAttitudeReferenceFrameXArbitraryCorrectedZVertical
  • CMAttitudeReferenceFrameXMagneticNorthZVertical
  • CMAttitudeReferenceFrameXTrueNorthZVertical

其中前两个 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);
07/29/2022 13:21 下午 posted in  apple

Flutter与Native通信(三)BasicMessageChannel

flutter可以native之间可以通过Platform Channels APIs进行通信,API主要有以下三种:

  • [MethodChanel]:用于传递方法调用(method invocation)
  • [EventChannel]:用于事件流的发送(event streams)
  • [BasicMessageChannel]:用于传递字符串和半结构化的消息

BasicMessageChannel用于在flutter和native互相发送消息,一方给另一方发送消息,收到消息之后给出回复。照例我们先看一下API的基本使用流程,然后再看代码实现

1.BasicMessageChannel的基本流程

flutter向native发送消息

  1. [flutter]创建BasicMessageChannel
  2. [native]通过BasicMessageChannel#MessageHandler注册Handler
  3. [flutter]通过BasicMessageChannel#send发送消息
  4. [native]BasicMessageChannel#MessageHandler#onMessage中接收消息,然后reply

native向flutter发送消息

流程也是一样的,只是将[flutter]与[native]反调

2.代码实现

flutter端

flutter需要完成以下工作

  • 创建BasicMessageChannel
  • 通过BasicMessageChannel#send发送消息

相对与其他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();
  }

native(android)端

  • android端完成以下工作:
  • 创建BasicMessageChannel
  • 通过setHandler注册MessageHandler
  • MessageHandler#onMessage回调中接收到message后,通过reply进行回复
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端显示

07/23/2022 10:06 上午 posted in  Flutter

Flutter与Native通信(一)MethodChannel

flutter可以与native之间进行通信,帮助我们使用native提供的能力。通信是双向的,我们可以从Native层调用flutter层的dart代码,同时也可以从flutter层调用Native的代码。我们需要使用Platform Channels APIs进行通信,主要包括下面三种:

  • [MethodChanel]:用于传递方法调用(method invocation)
  • [EventChannel]:用于事件流的发送(event streams)
  • [MessageChannel]:用于传递字符串和半结构化的消息

其中最常用的是MethodChanel,MethodChanel的使用与在Android的JNI调用非常类似,但是MethodChanel更加简单,而且相对于JNI的同步调用MethodChanel的调用是异步的:

1. MethodChanel的基本流程

从flutter架构图上可以看到,flutter与native的通信发生在Framework和Engine之间,framewrok内部会将MethodChannel以BinaryMessage的形式与Engine进行数据交换。关于BinaryMessage在这里不做过多介绍,主要以介绍Channel的使用为主。

我们先看一下MethodChanel使用的基本流程:

flutter调用native

  1. [native] 使用MethodChannel#setMethodCallHandler注册回调
  2. [flutter] 通过MethodChannel#invokeMethod发起异步调用
  3. [native] 调用native方法通过Result#success 返回Result,出错时返回error
  4. [flutter] 收到native返回的Result

native调用flutter

与flutter调用native的顺序完全一致,只是[native]与[flutter]角色反调

2. 代码实现

flutter调用native

首先在flutter端实现以下功能:

  • 创建MethodChannel,并注册channel名,一般使用“包名/标识”作为channel名
  • 通过invokeMethod发起异步调用,invokeMethod接受两个参数:
    • method:调用的native方法名
    • arguments:nativie方法参数,有多个参数时需要以map形式指定
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)端实现以下功能

  • 创建MethodChannel,必须跟flutter中使用相同的注册字符串
  • 设置MethodCallHander,methodCall中传递来自flutter的参数
  • 通过result返回给flutter结果
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)

native调用flutter

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);
  }
07/23/2022 10:03 上午 posted in  Flutter