Forwards

有空的博客


  • 首页

  • 归档

  • 关于

dart单线程模型

发表于 2023-01-13

Event Loop机制

  1. Dart是单线程的:即Dart代码是有序的,按照在main函数出现的次序一个接一个地执行,不会被其他代码中断;

  2. Dart也支持异步:单线程和异步并不冲突

    这里有个大前提,那就是我们的App绝大多数时间都在等待。比如,等待用户点击,等待网络请求返回、等待文件IO结果等等,而这些等待行为并不是阻塞的;所以,基于这些特点,单线程模型可以在等待的过程中做别的事情,等真正需要响应结果了,再去做对应的处理。

  3. Event Loop

    等待这个行为是通过Event Loop驱动的,事件队列Event Queue会把其他平行世界(比如Socket)完成的,需要主线程响应的事件放入其中。Dart有一个巨大的事件循环,在不断的轮询事件队列,取出事件,在主线程同步执行其回调函数,如下图所示:

    简化版 Event Loop

异步任务

在Dart中,实际上有两个队列,一个是事件队列Event Queue,另一个是微任务队列MicroTask Queue。Event Loop 完整版的流程图,应该如下所示:

Microtask Queue 与 Event Queue

在每次事件循环中,Dart总是先去第一个微任务队列中查询是否有可执行的任务,如果没有,才会处理后续的事件队列的流程。

  1. 定义

    Future:Dart为Event Queue的任务建议的一层封装,表示一个在未来时间才会完成的任务

  2. 使用

    把一个函数体放入Future,就完成了从同步任务到异步任务的包装。Future还提供了链式调用的能力,可以在异步任务执行完毕后依次执行链路上的其他函数体

  3. 执行流程

    在声明一个Future时,Dart会将异步任务的函数执行体放入Event Queue,然后立即返回,后续的代码将继续同步执行。而当同步执行的代码执行完毕后,Event Queue会按照加入事件队列的顺序,依次取出事件,最后同步执行Future的函数体及后续的then(then与Future函数体共用一个事件循环)

    但是,如果Future执行体已经执行完毕,但是这个Future的引用存在着,并且往里面加了一个then方法体,此时Dart会将后续加入的then方法体放入MicroTask Queue,尽快执行。

    案例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24

    Future(() => print('f1'));//声明一个匿名Future
    Future fx = Future(() => null);//声明Future fx,其执行体为null

    //声明一个匿名Future,并注册了两个then。在第一个then回调里启动了一个微任务
    Future(() => print('f2')).then((_) {
    print('f3');
    scheduleMicrotask(() => print('f4'));
    }).then((_) => print('f5'));

    //声明了一个匿名Future,并注册了两个then。第一个then是一个Future
    Future(() => print('f6'))
    .then((_) => Future(() => print('f7')))
    .then((_) => print('f8'));

    //声明了一个匿名Future
    Future(() => print('f9'));

    //往执行体为null的fx注册了了一个then
    fx.then((_) => print('f10'));

    //启动一个微任务
    scheduleMicrotask(() => print('f11'));
    print('f12');

    执行结果如下:

    1
    f12-->f11-->f1-->f10-->f2-->f3-->f5-->f4-->f6-->f9-->f7-->f8

异步函数

对于一个异步函数来说,其返回时内部执行动作并未结束,因此需要返回一个Future对象,供调用者使用。调用者根据Future对象,来决定:是在Future对象上注册一个then,等Future的执行体结束了再进行异步处理;还是一直同步等待Future执行体结束。

同步等待:需要在调用处使用await关键字,并且在调用处的函数体使用async关键字:Dart的await并不是阻塞等待,而是异步等待,并且await与async只对调用上下文的函数有效,并不向上传递

案例:

1
2
3
4
5
6
7
8
9
10
11
12
//声明了一个延迟2秒返回Hello的Future,并注册了一个then返回拼接后的Hello 2019
Future<String> fetchContent() =>
Future<String>.delayed(Duration(seconds:2), () => "Hello")
.then((x) => "$x 2019");
//异步函数会同步等待Hello 2019的返回,并打印
func() async => print(await fetchContent());

main() {
print("func before");
func();
print("func after");
}

执行结果如下:

1
func before-->func after-->Hello 2019

Mac环境下Flutter使用Jenkins构建自动化打包

发表于 2023-01-13

前提:在产品迭代过程中,开发人员需要频繁的提供安装包给测试人员,这不仅占用了大量的开发时间,还影响了工作效率和积极性,所以我们急需解放双手,这时候Jenkins自动化打包的优越性就体现了出来。

准备工作

搭建Flutter开发环境

在Mac上需要搭建Flutter开发环境,这部分不再赘述,可参考Flutter iOS真机调试、打包及上架

安装Jenkins

  1. 使用brew进行安装:brew install jenkins,
  2. 启动jenkins:brew services start jenkins
  3. 在浏览器中输入地址 http://0.0.0.0:8080 ,即可看到 Jenkins 页面
  4. 首次启动需要解锁Jenkins,安装推荐的插件,自定义设置用户名及密码

安装完成Jenkins面板界面如下:

jenkins_dashboard

环境配置

workspaceDir修改

自定义工作目录workspaceDir路径,打开安装目录下的config.xml:open $HOME/.jenkins/config.xml,修改workspaceDir的值

workspaceDir.png

配置JDK

在Manage Jenkins->Global Tool Configuration下的JDK设置JAVA_HOME

JDK配置

全局环境变量配置

在Manage Jenkins->Configure System下的全局属性设置ANDROID_HOME、PUB_HOSTED_URL、FLUTTER_STORAGE_BASE_URL、FLUTTER_HOME、PATH

Environment_variables

属性对应值分别如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<globalNodeProperties>
<hudson.slaves.EnvironmentVariablesNodeProperty>
<envVars serialization="custom">
<unserializable-parents/>
<tree-map>
<default>
<comparator class="java.lang.String$CaseInsensitiveComparator"/>
</default>
<int>5</int>
<string>ANDROID_HOME</string>
<string>/Users/yardi_wuhan/Library/Android/sdk</string>
<string>FLUTTER_HOME</string>
<string>/Users/yardi_wuhan/fvm/default</string>
<string>FLUTTER_STORAGE_BASE_URL</string>
<string>https://storage.flutter-io.cn</string>
<string>PATH</string>
<string>$PATH:$FLUTTER_HOME/bin:$HOME/.pub-cache/bin</string>
<string>PUB_HOSTED_URL</string>
<string>https://pub.flutter-io.cn</string>
</tree-map>
</envVars>
</hudson.slaves.EnvironmentVariablesNodeProperty>
</globalNodeProperties>

自动化打包

Android

  1. 新建一个Project,并选择Freestyle project

    create_jenkins_project

  2. 填写描述

  3. 源码管理——配置git,填写仓库地址Repository URL,并添加凭据Credentials

    如果你使用的是 https,那么需要配置认证,我这里使用的是 ssh,所以不需要配置认证,认证的方式需要添加凭据,参考如下所示,

    在Private Key下填写私钥值(.ssh目录下的id_ed25519),对应git账号应上传公钥值(.ssh目录下的id_ed25519.pub)

    add_ssh_credentials

  4. 配置参数化构建过程

    可增加构建环境ENV参数等,示例如下

    build_parameter

  5. 构建打包脚本

    在构建脚本中可获取上述传入的构建参数值(${BUILD_ENV})

    build_step_shell

  6. 归档成品(artifacts)

    archive_artifacts

  7. 配置完成后,可在构建页面开始构建,如下所示

    android_project

  8. 构建完成后可在首页下载构建完成后的安装包

iOS

[通过在打包机器上配置证书和mobile provision等文件的方式来完成打包认证]

  1. 手动配置证书

  2. 配置描述文件

  3. 开始打包

    新建Project—>填写描述—>配置git—>配置参数化构建过程这几个步骤和Android项目类似,不同的是iOS的构建脚本,如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    cd app_common
    flutter clean
    flutter pub get
    cd ../patient
    flutter clean
    flutter pub get
    mv sample.env .env
    security unlock-keychain -p Yardi5550586
    flutter build ipa --release --dart-define=ENV=${BUILD_ENV}
    ExportOptionsPath=$JENKINS_HOME/jobs/$JOB_NAME/ExportOptions.plist
    ArchivePath=$WORKSPACE/patient/build/ios/archive/Runner.xcarchive
    PackagePath=$WORKSPACE/patient/build/ios/archive/build
    xcodebuild -exportArchive -exportOptionsPlist $ExportOptionsPath -archivePath $ArchivePath -exportPath $PackagePath -allowProvisioningUpdates

    关键命令解释:

    1. security unlock-keychain -p xxxxxx:在开始打包之前,需要先解锁下keychain,这里的xxxx就是对应Mac上的密码
    2. flutter build ipa --release --dart-define=ENV=${BUILD_ENV}:指定release模式,开始编译iOS代码,并在 build/ios/archive 文件夹下生成一个 Xcode 构建归档(.xcarchive 文档)
    3. 执行完Archive之后,就可以进入export阶段,exportArchive之前需要先准备一个ExportOptions.plist文件到指定目录,这个文件可以先在打包机上用Xcode执行一次完整的Export流程,在对应的archive文件夹下存在对应的ExportOptions.plist
    4. 接着通过指定命令exportArchive,指定ExportOptions.plist,最终输出到PackagePath,得到一个ipa文件

    上述命令均为ad-hoc模式,如果是指定app-store模式,则需准备app-store对应的ExportOptions.plist文件,最终才能得到一个app-store模式的ipa文件

插件推荐

切换为中文

插件:Locale plugin,Localization: Chinese (Simplified)版本

使用:在Manage Jenkins->Configure System下的Local设置默认语言(Default Language):zh_CN

蒲公英上传和二维码显示

插件:Upload to pgyer和description setter plugin

使用:在构建完成后可以将apk/ipa文件上传至蒲公英,并以二维码形式展示在构建历史处

注意:Jenkins默认是plain text模式,所以不会对蒲公英上传成功后返回的html信息进行解析,所以装完description setter plugin后还需在全局安全设置(Config Global Security)中,将标记格式器(Markup Formatter)的设置更改为Safe HTML即可

1
<a href="${appBuildURL}"><img src="${appQRCodeURL}" width="118" height="118"/></a>

jenkins_upload_pgy

参考文章:

Flutter 搭建 iOS 命令行服务打包发布全保姆式流程

WebRTC入门——协议篇

发表于 2022-06-20

定义

WebRTC(Web Real-Time Communication)是一项实时通讯技术,它允许网络应用或者站点,在不借助中间媒介的情况下,建立浏览器之间点对点(Peer-to-Peer)的连接,实现视频流和(或)音频流或者其他任意数据的传输。WebRTC包含的这些标准能使用户在无需安装任何插件或者第三方软件的情况下,实现点对点(Peer-to-Peer)的数据分享和电话会议。

WebRTC包含了若干相互关联的API和协议以达到这个目标,下图即为WebRTC 1对1音视频实时通话过程示意图,本文也将围绕示意图展开学习内容

WebRTC 1对1音视频实时通话

协议

NAT

Network Address Translation(网络地址转换),是一种将IP地址空间映射到另一个空间的方法,方法是在数据包通过流量路由设备传输时修改数据包IP报头中的网络地址信息。在IPv4地址耗尽的情况下,它已成为保护全球地址空间必不可少的工具。

为什么要有NAT:这是因为现实中公网的ip地址过少,很多单位、学校都不能满足为每个主机分配一个公网地址,因此会通过NAT技术将内网的主机地址映射为同一公网地址的不同端口,进行外网访问。

NAT原理

NAT的缺点

NAT解决了内网环境下多主机上网的问题,但是也造成了难以从外网访问内网主机的问题。

NAT的分类

  • 圆锥形NAT:相当于在NAT服务器上打了个洞,所有外部主机都可以按照这个公网的IP:port发送数据,并且顺利找到内网主机

    圆锥形NAT

  • 受限锥形NAT:NAT会记录内网主机访问的外网IP,只有来自同一个IP的请求才能转发到内网主机。

    受限锥形NAT

  • 端口受限型NAT:NAT会记录内网主机访问的外网IP和端口,只有来自同一个IP且同一个端口的请求才能转发到内网主机

    端口受限型NAT

  • 对称型NAT:每次请求的连接都会使用不同的公网端口;注意,对称型NAT和其他三个最大的不同点在于每次请求的端口都会变化,这就导致了NAT穿越的难度增加

    对称型NAT

NAT穿透

我们如何实现NAT穿透:方式就是在NAT后面的主机向公网指定端口发送一个包,这样就会在NAT服务器上留下一个端口。其他主机只要知道了这个端口,就可以向内网主机发送数据了,这个行为形象的称之为“打洞”。

  • 一方是圆锥形NAT:

    当一方是圆锥形NAT,它只要向某个公网服务器(例如STUN服务器)发送一个包,这样它在公网的IP和端口就确定了。无论对端是哪种类型的NAT,只要向这个IP和端口发送数据即可连通

  • 双方是受限锥型NAT或者端口受限型NAT

    双方都向公网服务器发送一个包,确定自己的IP和端口,并通过公网服务器发送给对方,接着双方再向对方地址发送一个包,就可以实现通信

  • 一方是对称型NAT

    1. 当对方是圆锥型NAT:只要让对方先发送请求即可

    2. 当对方是受限锥型NAT:需要在双方交换完地址后,对方先发送一个包,确保指定IP的数据可接收,本机再向对方IP和端口发送数据,完成连接

    3. 当对方是端口受限型NAT:需要在双方交换完地址后,本机先发送一个包,在NAT服务器上打洞。但是此时对端并不知道本机这次打洞的端口,所以需要向各个端口都进行发包探测。此时本机需要不停的向对端发包,直至某一个包被接收。此时双方都确定了对方的IP和端口,完成通信。

    4. 当对方是对称型NAT:没有办法连通,因为其中一个主机在探测时,自身的端口都会变化,这样对方无法确定本机端口。因此只能通过公网服务器进行中转(例如TURN服务器)

ICE

Interactive Connectivity Establishment(交互式连接设施),是一个允许你的浏览器和对端浏览器建立连接的协议框架。

在实际的网络中,有很多原因导致简单的从A端到B端的直连不能如愿完成,这需要绕过阻止建立连接的防火墙,给你的设备分配一个唯一可见的地址(通常情况下我们的大部分设备没有一个固定的公网地址),如果路由器不允许主机直连,还得通过一台服务器转发数据,ICE通过使用以下几种技术完成上述工作。

STUN

Session Traversal Utilities for NAT(NAT会话穿透应用程序),是一种网络协议,它允许位于NAT(或多重NAT)后的客户端找出自己的公网地址,查出自己位于哪种类型的NAT之后以及NAT为某一个本地端口所绑定的Internet端端口。这些信息被用来在两个同时处于NAT路由器之后的主机之间创建UDP通信

一旦客户端得知了Internet端的UDP端口,通信就可以开始了。如果NAT是完全圆锥形的,那么双方中的任何一方都可以发起通信;如果NAT是受限圆锥型NAT或端口受限型NAT,双方必须一起开始传输,如果是对称型NAT则不能使用

STUN是一个client-server协议,一个VoIP电话或软件包可能会包括一个STUN客户端,这个客户端会向STUN服务器发送请求,之后服务器会向STUN客户端报告NAT路由器的公网IP地址以及NAT为允许传入流量传回内网而开通的端口

STUN示意图

TURN

Traversal Using Relay NAT(NAT的中继穿透方式),是一种资料传输协议(data-tranfer protocol),允许在TCP或UDP的连接在线跨越NAT或防火墙。

TURN是一个client-server协议。TURN的NAT穿透方法与STUN类似,都是通过获取应用层的公有地址达到NAT穿透,但在实现TURN client的终端必须在通信开始前与TURN server进行交互,并要求TURN server产生relay port,也就是relayed-transport-address。这时TURN server会创建peer,即远程端点(remote endpoints),开始进行中继(relay)的动作,TURN client会利用relay port将资料发送至peer,再由peer转传到另一方的TURN client。很显然这种方式是开销很大的,所以只有在没得选择的情况下采用。

TURN示意图

SDP

Session Description Protocol(会话描述协议),是一个描述多媒体连接内容的协议,例如分辨率,格式,编码,加密算法等。所以在数据传输时两端都能够理解彼此的数据,本质上,这些描述内容的元数据并不是媒体流本身

从技术上讲,SDP并不是一个真正的协议,而是一种数据格式,用来描述在设备之间共享媒体的连接。

信令与信令服务器

信令

WebRTC是一个完全对等技术,用于实时交换音频、视频和数据。如其他地方所讨论的,必须进行一种发现和媒体格式协商,以使不同网络上的两个设备相互定位,这个过程被称为信令,并涉及两个设备连接到第三方共同商定的服务器,这两台设备可以相互定位,并交换协商消息。

信令服务器

两个设备之间建立WebRTC连接需要一个信令服务器来实现双方通过网络进行连接。信令服务器的作用是作为一个中间人帮助双方在尽可能少的暴露隐私的情况下建立连接。

WebRTC并没有提供信令传递机制,可以使用任何方式比如WebSocket或者XMLHttpRequest等等,来交换彼此的令牌信息。最重要的是信令服务器并不需要理解和解释信令数据内容。

总结

了解清楚各种协议之后,我们得有个大体的概念,就是需要大体的知道webRTC协议各个模块是如何联系在一起的:

以A和B进行视频通话为例,现在决定采用webRTC协议,实现p2p的连接,也就是A和B之间能直接进行媒体流的传输,不需要外加的媒体服务器进行转发。

  1. 在访问外网的时候,需要知道对方的IP和port,我们才能访问到指定的设备。而A和B在他们各自的路由局域网内,是不知道这个IP和port,它们各自的IP和端口是由路由分配的,这个分配IP和port的协议就是NAT(网络地址转换)。
  2. A和B之间需要实现p2p连接,就需要知道自己本身的IP和端口,然后告诉对方自己的这些信息,这样才能实现互联,这时STUN(NAT会话穿透应用程序)服务器就派上用场了,A和B分别向stun服务器发送请求,stun服务器会返回他们各自的IP和port,但是NAT有四种类型(圆锥型NAT、受限锥型NAT,端口受限型NAT,对称型NAT),如果是对称型NAT,通过stun服务器是不能获取到IP和端口的,这时A和B之间要进行媒体的交流,就得靠TURN(NAT的中继方式)了,turn服务器是个中转站,A和B之间通信的所有媒体流,都是经过turn服务器进行转发的。
  3. 是采用stun还是turn服务器,这个会交由ICE(交互式连接设施)来帮助我们决策,ICE是一个框架,主要任务就是帮助我们建立双方的连接。
  4. 那么通过stun/turn服务器,A和B都知道自己的IP和port,那这个信息要如何告诉对方呢,这个就需要通过信令服务器了。A和B之间建立媒体连接,还需要知道对方各自处理流媒体的能力,这个信息也是通过信令服务器来转发的。信令服务器并不需要关心发送的内容,只需要负责信息的转发即可。

参考

WebRTC_API

理解NAT穿越

WebRtc学习之旅 —— Android端应用开发

Flutter状态管理:Provider

发表于 2022-06-20

InheritedWidget

简介

InheritedWidget提供了一种在widget树中从上到下共享数据的方式,能实现组件跨级传递数据。比如我们在应用的根widget中通过InheritedWidget共享了一个数据,那么我们便可以在任意子widget中来获取该共享的数据!

应用:如Flutter SDK中正是通过InheritedWidget来共享Theme和Local(当前语言环境)信息

数据传输

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class ShareDataWidget extends InheritedWidget {
final int data; //需要在子树中共享的数据,保存点击次数
const ShareDataWidget({
Key? key,
required this.data,
required Widget child,
}) : super(key: key, child: child);

//定义一个便捷方法,方便子树中的widget获取共享数据
static ShareDataWidget? of(BuildContext context, {bool listen = true}) {
ShareDataWidget? shareDataWidget;
if (listen) {
shareDataWidget =
context.dependOnInheritedWidgetOfExactType<ShareDataWidget>();
} else {
shareDataWidget =
context.getElementForInheritedWidgetOfExactType<ShareDataWidget>()
as ShareDataWidget?;
}
return shareDataWidget;
}

@override
bool updateShouldNotify(covariant ShareDataWidget oldWidget) {
return oldWidget.data != data;
}
}

原理:

  1. dependOnInheritedWidgetOfExactType方法将在下文解析

  2. getElementForInheritedWidgetOfExactType方法:

    1
    2
    3
    4
    5
    @override
    InheritedElement getElementForInheritedWidgetOfExactType<T extends InheritedWidget>() {
    final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[T];
    return ancestor;
    }

    通过上述两个函数既可以获取到InheritedElement实例,继而拿到了共享的数据

刷新机制

InheritedElement和Element之间有一些交互,实际上自带了一套刷新机制

  • InheritedElement存子节点Element:_dependents,这个变量用来存储需要刷新的子Element

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    class InheritedElement extends ProxyElement {
    InheritedElement(InheritedWidget widget) : super(widget);

    final Map<Element, Object?> _dependents = HashMap<Element, Object?>();

    @override
    void debugDeactivated() {
    assert(() {
    assert(_dependents.isEmpty);
    return true;
    }());
    super.debugDeactivated();
    }

    @protected
    Object? getDependencies(Element dependent) {
    return _dependents[dependent];
    }

    @protected
    void setDependencies(Element dependent, Object? value) {
    _dependents[dependent] = value;
    }

    @protected
    void updateDependencies(Element dependent, Object? aspect) {
    setDependencies(dependent, null);
    }
    }
  • InheritedElement刷新子Element

    1. 在notifyClients方法中,循环_dependents存储的Element,传入notifyDependent
    2. 在notifyDependent中,传入Element调用自身didChangeDependencies方法
    3. Element的didChangeDependencies方法会调用markNeedsBuild,来刷新自身
  • InheritedWidget的子节点是如何将自身Element添加到_dependents

    dependOnInheritedWidgetOfExactType()源码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    @override
    InheritedWidget dependOnInheritedWidgetOfExactType({ Object aspect }) {
    assert(_debugCheckStateIsActiveForAncestorLookup());
    final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[T];
    if (ancestor != null) {
    return dependOnInheritedElement(ancestor, aspect: aspect) as T;
    }
    _hadUnsatisfiedDependencies = true;
    return null;
    }

    @override
    InheritedWidget dependOnInheritedElement(InheritedElement ancestor, { Object aspect }) {
    assert(ancestor != null);
    _dependencies ??= HashSet<InheritedElement>();
    _dependencies.add(ancestor);
    ancestor.updateDependencies(this, aspect);
    return ancestor.widget;
    }

可以看到在调用dependOnInheritedWidgetOfExactType时,InheritedWidget和依赖它的子widget注册了依赖关系,之后当InheritedWidget发生变化时,就会更新依赖它的子widget,也就是会调用这些子widget的didChangeDependencies()和build()。

  • 在dependOnInheritedElement方法中,会传入InheritedElement实例ancestor
  • ancestor会调用updateDependencies方法,将自身的Element实例传入,这样就添加到_dependents中了

    可以发现:调用dependOnInheritedWidgetOfExactType() 和 getElementForInheritedWidgetOfExactType()的区别就是前者会注册依赖关系,而后者不会

didChangeDependencies

State的生命周期之一,它会在“依赖”发生变化时被Flutter框架调用,而这个“依赖”指的就是子widget是否使用了父widget中InheritedWidget的数据!如果使用了,则代表子widget有依赖;如果没有使用则代表没有依赖。这种机制可以使子widget在所依赖的InheritedWidget变化时来更新自身!在数据发生变化时只对使用该数据的widget更新是合理并且性能友好的。

应该在didChangeDependencies中做什么?

如果需要在依赖改变后执行一些昂贵的操作,比如网络请求,这时最好的方式就是在此方法中执行,这样可以避免每次build()都执行这些昂贵操作。

Provider

Provider是对InheritedWidget组件的上层封装,使其更易用,更易复用。

Provider原理

原理:基于发布者-订阅者模式,Model变化后会自动通知ChangeNotifierProvider(订阅者),ChangeNotifierProvider内部会重新构建InheritedWidget,而依赖该InheritedWidget的子widget就会更新。

使用Provider 优点:

  1. 业务代码更关注数据了,只要更新Model,则UI会自动更新,而不用在状态改变后去手动调用setState()来显示更新页面
  2. 数据改变的消息传递被屏蔽了,我们无需手动去处理状态改变事件的发布和订阅,这一切都被封装在Provider中了
  3. 在大型复杂应用中,尤其是需要全局共享的状态非常多时,使用Provider将会大大简化代码逻辑,降低出错的概率,提高开发效率

参考:

【源码篇】Flutter Provider的另一面(万字图文+插件)

数据共享(InheritedWidget)

理解Flutter引擎线程模式

发表于 2022-04-23

Flutter体系结构

flutter_system_overview

Framework

Framework是我们直接接触到的,它使用dart实现,包括Material Design风格的Widget,CuperTino风格的Widget,文本/图片/按钮等基础的Widgets,Rendering渲染、Animation动画、Painting图形绘制、Gestures手势等。

Engine

Engine引擎层使用C++实现,主要包括:Skia,Dart和shell。

  • Skia是开源的二维图形库,提供了适用于多种软硬件平台的通用API

  • shell:不同的平台有不同的shell

Embedder

Embedder是一个嵌入层,即把Flutter嵌入到各个平台上去,这里做的主要工作包括渲染Surface设置,线程设置,以及插件等,为Engine创建和管理线程,作用是把Engine的task runners运行在嵌入层管理的线程上。

从这里可以看出,Flutter的平台相关层很低,平台只是提供一个画布,剩余的所有渲染相关的逻辑都在Flutter内部,这就使得它具有了很好的跨端一致性。

Flutter 线程模型

Flutter Engine是不创建和管理线程的,是由Embedder为Engine创建和管理线程(包括线程里的消息循环),Flutter在Engine中定义了四种Task Runners,Task Runners是需要运行在平台提供的线程上,这四种Task Runners分别是

  • Platform Task Runner
  • UI Task Runner
  • GPU Task Runner
  • IO Task Runner

Platform Task Runner

  1. Flutter Engine的主Task Runner,运行Platform Task Runner的线程叫Platform Thread,Platform Thread必须要运行在平台的Main Thread上。
  2. 一个Flutter Engine实例对应一个Platform Thread:一个Flutter应用启动的时候会创建一个Engine实例,Engine创建的时候会创建一个线程供Platform Runner使用
  3. 跟Flutter Engine的所有交互(接口调用)以及处理来自平台的消息都必须发生在Platform Thread,试图在其他线程中调用Flutter Engine会导致异常
  4. 阻塞Platform Thread不会直接导致Flutter 应用的卡顿(跟iOS Android主线程不同),但是建议复杂计算逻辑操作不要放在Platform Thread,而是放在其他线程(不包括上述的四个线程)。长时间卡住Platform Thread应用可能会被系统Watchdot强行杀死

UI Task Runner

  1. UI Task Runner被Flutter Engine用于执行Dart root isolate代码,Root isolate比较特殊,它绑定了Flutter需要的函数方法,运行应用的main code,UI Task Runner运行所在的线程对应到平台的线程,其实是子线程
  2. 调度提交渲染帧:

    1. Root isolate通知Flutter Engine有帧需要渲染
    2. Flutter Engine通知平台,需要在下一个vsync的时候得到通知
    3. 平台等待下一个vsync
    4. 对创建的对象和Widgets进行Layout并生成一个Layer Tree,这个Tree马上被提交到Flutter Engine
  3. Root Isolate处理来自Navite Plugins的消息响应,Timers,Microtasks和异步IO操作

  4. 阻塞这个线程会直接导致Flutter应用卡顿掉帧,繁重计算建议其放到独立的Isolate执行

GPU Task Runner

  1. GPU Task Runner被用于执行设备GPU的相关调用:UI Task Runner创建的Layer Tree信息是平台不相关的,具体如何实现绘制取决于具体平台和方式,可以是OpenGL,Vulkan、软件绘制或者其他Skia配置的绘图实现。
  2. UI Task Runner和GPU Task Runner跑在不同的线程:基于Layer Tree的处理时长和GPU帧显示到屏幕的耗时,GPU Task Runner可能会延迟下一帧在UI Task Runner的调度。存在这种可能,UI Task Runner在已经准备好下一帧的情况下,GPU Task Runner却还在向GPU提交上一帧。这种延迟调度机制确保不让UI Task Runner分配过多的任务给GPU Task Runner。
  3. GPU Task Runner的过载会导致Flutter应用的卡顿:建议为每一个Engine实例都新建一个专用的GPU Runner线程

IO Task Runner

  1. IO Task Runner主要功能是从图片存储中读取压缩的图片格式,将图片数据进行处理,为GPU Task Runner的渲染做好准备。在Textture的准备过程中,IO Runner首先要读取压缩的图片二进制数据,将其解压转换成GPU能够处理的格式,然后将数据上传到GPU。
  2. IO Task Runner不会直接导致Flutter应用卡顿,但是可能会导致图片和其他一些资源加载的延迟,间接影响性能,所以还是建议为IO Task Runner创建一个专用的线程

各个平台默认的Runner线程实现

  1. Platform Task Runner
Android iOS
主线程 主线程
  1. UI Task Runner
Android iOS
子线程 子线程
  1. GPU Task Runner
Android iOS
子线程 子线程
  1. IO Task Runner
Android iOS
子线程 子线程

Flutter Platform Channel原理

发表于 2022-04-04

概述:本文不讲述如何编写代码,只学习其原理,如何编写可参考Flutter-Plugin开发与发布

Flutter平台特定的API支持不依赖于代码生成,而是依赖于灵活的消息传递的方式:

应用的Flutter部分通过平台通道(Platform Channel)将传递的数据编码成消息的形式,跨线程发送到其应用程序的所在的宿主(iOS或Android)

宿主监听的平台通道,并接收该消息,然后它会调用特定于该平台的API(使用原生编程语言),并将结果数据用过同样方式原路发送回客户端,即应用程序的Flutter部分

整个过程的消息和响应是异步传递的,所以不会直接阻塞用户界面

官方架构图:

Flutter Platform Channel

流程图

MethodChannel调用流程

MethodChannel调用流程

小结:

  1. Dart层使用codec对根据方法名(channel method name)和参数(channel method param)构建得到的对象进行编码,然后通过dart的类似JNI的本地接口,调用SendPlatformMessage,传递给c++层
  2. c++层通过持有java对象flutterJNI的方法调用将消息传递到java层
  3. java层解码接收到的消息,并根据channel name获取到对应的handlerInfo,调用相应的逻辑处理

MethodChannel返回流程

MethodChannel返回流程

小结:

  1. java层得到结果后进行编码,通过JNI将响应结果返回给c++层
  2. c++层将结果通过发送时保存的dart响应方法对象回调给dart层
  3. dart层通过回调方法对结果数据进行处理,然后通过codec解码数据做后续操作

总结:MethodChannel的执行流程涉及到主线程和UI线程的交互,代码从Dart层到C++层再到Java层,执行完相应逻辑后原路返回,从Java层到C++层再到Dart层

参考:

深入理解Flutter的Platform Channel机制

全面解析Flutter Platform Channel原理

Flutter iOS真机调试、打包及上架

发表于 2022-01-27

1. 在Mac上搭建Flutter环境

这部分只需参考官网搭建即可

注意事项

  • 多个Path环境变量设置:
1
2
3
4
5
6
7
export PUB_HOSTED_URL=https://pub.flutter-io.cn
export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn
JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home
FLUTTER_HOME=/Users/yardi_wuhan/Library/FlutterSDK
PATH=$JAVA_HOME/bin:$FLUTTER_HOME/bin:$PATH
CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar
export JAVA_HOME PATH CLASSPATH
  • 如果你使用的是zsh,终端启动时 ~/.bash_profile 将不会被加载,解决办法就是修改 ~/.zshrc ,在其中添加:source ~/.bash_profile

2. Ruby、CocoaPods安装

CocoaPods 就是iOS 项目的开发 第三方库的管理工具。

CocoaPods 是用 ruby 实现的,要想使用它首先需要有ruby环境。 虽然 Mac 系统默认可以运行ruby。但是ruby版本过低是无法正常支持CocoaPods的使用

  1. 更换Ruby源

    1
    2
    3
    4
    5
    6
    # 查看现有的源
    gem source -l
    # 移除
    gem sources --remove https://rubygems.org/
    # 添加 ruby-china 的源
    gem sources -a https://gems.ruby-china.org/
  2. 安装CocoaPods:sudo gem install cocoapods

  3. 切换清华源安装CocoaPods

    1
    2
    3
    4
    5
    6
    7
    8
    # 移除原仓库镜像
    pod repo remove master
    # 使用清华源安装到本地 cd ~/.cocoapods/repos/master
    git clone https://mirrors.tuna.tsinghua.edu.cn/git/CocoaPods/Specs.git
    # 设置一下
    pod setup
    # 查看仓库信息
    pod repo
  4. 安装完成之后进入到项目的iOS目录下,执行pod install

3. 证书相关申请

  1. 登录apple开发者平台

  2. Register a new identifier(常见app的功能如第三方应用登录(Associated Domains)、推送(Push Notifications)可提前勾选Capabilities)

    1
    2
    3
    4
    注意事项:
    1. Bundle ID的区别
    - Explicit App ID「明确的 App ID」,一般格式是:com.company.appName;这种 id 只能用在一个app上,每一个新应用都要创建并只有一个。
    - Wildcard App ID「通配符 App ID」, 一般格式是:com.domainname.* ;这种 id 可以用在多个应用上,虽然方便,但是使用这种id的应用不能使用通知功能,所以不常用。
  3. 在mac上通过钥匙串应用创建两个证书请求文件(CSR文件),分别对应Development环境和Distribution环境

  1. Create a New Certificate(证书)

    1. 分别申请开发证书(iOS Development)和分发证书(iOS Distribution),开发证书用于开发和调试应用程序,可用于真机调试;生产证书用于打包上传App Store或者蒲公英,用于验证开发者身份。如果项目集成了推送功能,还需配置推送证书,推送证书同样也分两种:开发环境和生产环境(Apple Push Notification service SSL :Sandbox & Production),同时生成的p12文件需上传到服务端后台(如阿里云移动推送后台)

    2. 生成后的证书需要下载并通过钥匙串导入到Mac

      证书导入

      1
      2
      注意事项:
      打开钥匙串应用,在点击登录-->我的证书页面下,双击证书导入即可
  1. Register a New Provisioning Profile(描述文件)

    可参考iOS 证书配置

4. Xcode中的配置

Xcode中的配置

  1. 点击TARGETS中的Runner,选中Build Settings栏,在Code Signing Identity中配置已下载导入的iOS证书

  2. 选中Signing & Capabilities栏,Provisioning Profile选择下载相应的描述文件

    1
    注意事项:Automatically manage signing不选中
  3. 到此即可连接iOS真机在Android Studio中运行

    1
    注意事项:需要将真机的UDID添加到apple开发者平台的设备列表(Devices)

5. 打包及上架

  1. 运行命令:flutter build ipa --release
  2. 在 Xcode 中打开 build/ios/archive/MyApp.xcarchive
  3. 点击 Distribute App 按钮,选择分发的方式:App Store Connect(用于App Store上架)、Ad Hoc(用于蒲公英的分发平台)

参考文章:

在macOS上搭建Flutter开发环境

CocoaPods 换源 git 安装 与 使用

App Bundle ID 基本信息介绍

iOS 证书配置

构建和发布为 iOS 应用

一步快速获取 iOS 设备的 UDID

Flutter Plugin开发与发布

发表于 2022-01-27

以开发腾讯云基础版人脸核身Flutter插件为例,总结Flutter Plugin开发和发布流程。

项目地址:https://github.com/Ucoon/wb_cloud_face

1. 创建 package

  • 方式一:通过Android Studio直接创建Flutter Project(选择Plugin类型)

    可选择使用的语言和插件支持的平台

    选择语言

  • 方式二:通过命令行创建Flutter Plugin Project(使用--template=plugin)

    1
    flutter create --org tech.ucoon --template=plugin wb_cloud_face

    可以使用-i为iOS指定开发语言,使用-a为Android指定开发语言,如:

    1
    flutter create --org tech.ucoon --template=plugin -i swift -a kotlin wb_cloud_face

2. 实现包package

  1. 在lib下的dart文件中定义接口:openCloudFaceService

    1
    2
    3
    4
    5
    6
    7
    static Future<WbCloudFaceVerifyResult> openCloudFaceService({
    required WbCloudFaceParams params,
    }) async {
    final res = await _channel.invokeMethod(
    'openCloudFaceService', params.toJson());
    return WbCloudFaceVerifyResult.fromJson(json.decode(res));
    }
  2. 添加Android平台实现的代码(android目录下)

    注意事项:

    • import io.flutter.embedding.engine.plugins.FlutterPlugin;导入plugin相关代码报红

      参考stack overflow的回答

      1
      You should open the project in android studio from the example/android location.
    • 插件中引入第三方本地aar文件时,需在插件/android/build.gradle中添加flatDir

      1
      2
      3
      4
      5
      6
      7
      8
      rootProject.allprojects {
      repositories {
      ...
      flatDir {
      dirs project(':wb_cloud_face').file('libs')
      }
      }
      }
  3. 添加iOS平台实现的代码(iOS目录下)

    注意事项:

    • 在podspec文件(podspec是一个描述pod库版本文件)添加说明:name,version、summary、homepage、author

    • 引用第三方静态库Framework:在iOS目录下新建Framework文件夹,把需要的三方库拷贝到Framework文件夹下,并在podspec文件中配置如下三个字段

      1
      2
      3
      s.vendored_frameworks //第三方静态库的.framework文件路径
      s.vendored_libraries//第三方静态库的.a文件路径
      s.resource //第三方静态库的.bundle文件路径

    至此plugin开发完成

3. 发布Plugin

  1. 在发布之前,检查pubspec.yaml(检查description、version、homepage字段)、README.md(使用教程)以及CHANGELOG.md(版本记录)文件,以确保其内容的完整性和正确性。
  2. 然后, 运行 dry-run 命令以查看是否都准备OK了: flutter packages pub publish --dry-run
  3. 最后, 运行发布命令: flutter packages pub publish

注意事项:

  • 发布插件压缩之后的包必须小于100M

    1
    Your package must be smaller than 100 MB after gzip compression. If it’s too large, consider splitting it into multiple packages, using a .pubignore file to remove unnecessary content, or cutting down on the number of included resources or examples.

    针对这种情况,可考虑搭建Flutter pub私有仓库,或者以github方式引用插件

参考资料:

开发Packages和插件

Flutter Plugin引用iOS三方静态库Framework

腾讯云人脸核身SDK文档

Android中的缓存策略

发表于 2020-03-15

缓存策略

一般来说,缓存策略主要包含缓存的添加,获取和删除这三类操作。

1
删除缓存的原因:不管是内存缓存还是硬盘缓存,它们的缓存大小都是有限制的,当缓存容量满了之后,再想向其添加缓存,这个时候就需要删除一些旧的缓存并添加新的缓存。因此,如何定义缓存的新旧这就是一种策略,不同的策略对应着不同的缓存算法

LRU算法

LRU(Least Recently Used),近期最少使用算法,它的核心思想是当缓存满了,会优先淘汰那些近期最少使用的缓存对象。采用LRU算法的缓存有两种:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

## LruCache

### LruCache的介绍

核心思想:采用一个```LinkedHashMap```以强引用的方式存储外界的缓存对象,```LinkedHashMap```提供get和put方法来完成缓存的获取和添加操作,当缓存满时,```LruCache```会移除较早使用的缓存对象,然后再添加新的缓存对象。

1. 强引用、软引用和弱引用的区别:

- 强引用:直接的对象引用;
- 软引用:当一个对象只有软引用存在时,系统内存不足时此对象会被gc回收;
- 弱引用:当一个对象只有弱引用存在时,此对象会随时被gc回收。

2. ```LinkedHashMap```:是哈希表和链表的实现,它是线程不安全的,继承自```HashMap```,实现```Map<K, V>```接口。内部维护了一个双向链表,在插入、访问、修改数据时,会增加节点、或调整链表的节点顺序,双向链表结构可以保证迭代顺序是插入顺序。

```LinkedHashMap```的构造函数

```java
/**
* @param accessOrder the ordering mode
* true: access-order; false: insertion-order
*/
public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder){
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}

1
2
3
4
5
6
7
8
9
10
11

另外```LruCache```是线程安全的,下面为其构造函数:

```java
public LruCache(int maxSize){
if(maxSize <= 0){
throw new IllegalArgumentException("maxSize <= 0");
}
this.maxSize = maxSize;
this.map = new LinkedHashMap(0, 0.75f, true);
}

LruCache的使用

1
2
3
4
5
6
7
8
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
int cacheSize = maxMemory / 8;
LruCache<String, Bitmap> memoryCache = new LruCache<String, Bitmap>(cacheSize){
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getRowBytes() * value.getHeight() / 1024;
}
};
  1. 设置LruCache缓存的大小
  2. 重写sizeOf方法

LruCache的实现原理

Android非静态内部类导致内存泄漏原因深入剖析

发表于 2020-03-08

内存泄漏 Memory Leak

内存泄漏(Memory leak)是在计算机科学中,由于疏忽或错误造成程序未能释放已经不再使用的内存。内存泄漏并非指内存在物理上的消失,而是应用程序分配某内存后,由于设计错误,导致在释放该段内存之前就失去了对该段内存的控制,从而造成了内存的浪费。

内存泄漏的危害

App可能因为大量的内存泄漏导致内存耗尽,引发Crash,如果内存未耗尽,App也会由于内存空间不足,出现频繁的GC,每一次GC都是非常耗时的阻塞性操作,会造成设备非常严重的卡顿。

示例

Handler:我们经常会在activity中这样使用handler:

1
2
3
4
5
private class MyHandler extends Handler{
...
}
//使用
MyHandler myHandler = new MyHandler(this);

由于myHandler是Handler的非静态匿名内部类的实例,而这个非静态匿名内部类对其外部类存在一个隐式引用,其外部类在销毁之前,如果该非静态匿名内部类的handleMessage还未处理完成,将会导致外部类的内存资源无法正常释放,造成了内存泄漏。

非静态内部类创建静态实例

非静态内部类可以自由使用外部类的所有变量和方法,非静态内部类默认持有外部类的引用,此时如果在外部类创建静态static的非静态匿名内部类的实例(声明为static静态成员变量),或者在非静态内部类创建了一个静态实例, 这样就导致内部类的生命周期和应用程序ClassLoader一样长,导致外部类无法正常销毁。

分析

  1. 为什么非静态内部类对外部类会存在一个隐式引用?
  2. 为什么非静态内部类存在异步任务,可能会导致其对应的外部类内存资源无法正常释放?
  3. 为什么非静态内部类中创建了一个静态实例,会导致内存泄漏?

隐式引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
private void testMethod(){

}
private class MyHandler extends Handler{
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
testMethod();
//MainActivity.this 这个就是隐式引用
MainActivity.this.testMethod();
}
}
}

在非静态匿名内部类中,我们可以访问到外部类的testMethod方法,这个就是隐式引用的作用

对比非静态匿名内部类的源码和编译后的字节码

其中,args_size:代表着隐式引用this的个数,一个是dispatchMessage的,一个是Test$MyHandler的,可以看出非静态匿名内部类中确实持有外部类的引用。

对比非静态内部类的源码和编译后的字节码

同样的,可以看出非静态内部类中确实持有外部类的引用。

结论

非静态内部类和非静态匿名内部类中确实持有外部类的引用,静态内部类中未持有外部类的引用。隐式引用是导致内存泄漏的根本原因。

解决思路

  1. 去除隐式引用(通过静态内部类来去除隐式引用)
  2. 手动管理对象(修改静态内部类的构造方式,手动引入其外部类引用)
  3. 当内存不可用时,不执行不可控代码(Android可以用WeakReference包裹外部类实例)
12…4

Forwards

Keep coding

37 日志
GitHub 简书
© 2023 Forwards
由 Hexo 强力驱动
|
主题 — NexT.Mist v5.1.4