Flutter 中的消息编解码器与后台进程开发
1. 消息编解码器
在 Flutter 开发中,MethodChannel 是最常用的平台通道,它能将 Dart 数据转换为原生编程语言数据,反之亦然,大大简化了数据转换的复杂性。除了 MethodChannel,还有 BasicMessageChannel 等其他原生与 Flutter 之间的通信方式,更多详情可查看官方教程: https://flutter.dev/docs/development/platform-integration/platform-channels 。
这些通信方式的实现依赖于 Flutter 标准的消息编解码器(Message codecs)。消息编解码器负责将数据从一种语言转换为另一种语言,有多种消息编解码器可供选择,必要时我们还能自定义。以下是几种常见的消息编解码器:
| 编解码器名称 | 描述 | Android 表示 | iOS 表示 |
| ---- | ---- | ---- | ---- |
| BinaryCodec | 未编码的二进制消息,使用 ByteData 表示 | java.nio.ByteBuffer | NSData |
| JSONMessageCodec | UTF - 8 编码的 JSON 消息 | 使用 org.json 库解码 | 使用 NSJSONSerialization 库解码 |
| StringCodec | UTF - 8 编码的字符串消息 | java.util.String | NSString |
| StandardMessageCodec | 使用 Flutter 标准二进制编码,解码值使用 List
和 Map
,与内容无关,能将 Dart 类型转换为 Android/iOS 类型 | - | - |
MethodChannel 默认使用 Flutter 提供的 StandardMessageCodec 来进行数据的序列化和反序列化。更多关于 StandardMessageCodec 类的信息可查看官方文档: https://api.flutter.dev/flutter/services/StandardMessageCodec-class.html 。
2. 创建后台进程
在移动应用开发中,并发编程很重要,因为长时间的操作可能会导致渲染卡顿等问题。Flutter 提供了一种简单的方式来创建隔离实例(isolate),即使用 compute() 函数。
2.1 Flutter 的 compute() 函数
compute() 函数用于创建新的隔离实例、向其发送消息并获取响应。其函数签名如下:
Future<R> compute <Q, R>( ComputeCallback<Q, R> callback, Q message, {
String debugLabel })
参数说明:
- callback:要在新隔离实例中执行的顶级函数,有泛型类型注解
,Q 表示回调的输入类型,R 表示计算结果类型。注意,回调参数必须是顶级函数,不能是闭包、类的实例方法或静态方法。
- message:Q 类型的参数值,将被发送到 callback。需要注意的是,隔离实例之间发送和接收的值有一定限制,这些限制约束了 Q 和 R 的可能取值。
- debugLabel:在开发过程中使用,为隔离实例命名,方便在 Observatory UI 工具中进行性能分析时区分。
compute() 函数适用于需要较长时间才能完成的计算任务,避免因计算时间过长导致帧丢失。对于短期计算,也可以使用 Futures。
2.2 SendPort 和 ReceivePort
传递给 compute() 函数的消息和其返回值有一定限制,这些限制来自隔离实例的通信层。隔离实例通过消息进行通信,消息通过 SendPort 和 ReceivePort 实例发送和接收。要向隔离实例端口发送消息,首先需要获取对应的 ReceivePort 实例,ReceivePort 类的 sendPort 访问器绑定到隔离实例,可用于发送消息。一个隔离实例可以通过 IsolateNameServer 类从另一个隔离实例获取 ReceivePort。
2.3 IsolateNameServer
IsolateNameServer 类是 Dart 隔离实例的全局注册表,可用于注册和查找 SendPort 和 ReceivePort。一个隔离实例可以通过 IsolateNameServer.registerPortWithName 方法注册其 ReceivePort,其他隔离实例可以使用 IsolateNameServer.lookupPortByName() 方法获取对应的 SendPort。
2.4 compute() 示例
以下是一个使用 compute() 函数的示例代码:
import 'dart:io';
void backgroundCompute(args) {
print('background compute callback');
print('calculating fibonacci from a background process');
int first = 0;
int second = 1;
for (var i = 2; i <= 50; i++) {
var temp = second;
second = first + second;
first = temp;
sleep(Duration(milliseconds: 200));
print("first: $first, second: $second.");
}
print('finished calculating fibo');
}
// 执行隔离实例
compute(backgroundCompute, null);
这个示例计算前 50 个斐波那契数并打印到设备日志中。需要注意的是,新创建的隔离实例是主 Flutter 应用隔离实例的子实例,当应用终止时,子隔离实例也会终止。
3. 完整的后台进程
虽然 compute() 函数很有用,但在某些情况下可能不满足需求。因为 compute() 函数创建的子隔离实例会在父隔离实例终止时终止。在一些场景中,我们需要代码独立于主应用运行,例如:
- 接收推送通知并更新信息,无需应用运行即可接收和处理远程推送通知。
- 监听用户位置变化或进入地理围栏。
- 从服务器获取信息。
- 上传文件到服务器,根据文件大小,操作可能需要很长时间。
对于需要代码独立于应用 UI 运行的场景,我们可以创建无头隔离实例(headless isolates),即不绑定到主应用隔离实例的隔离实例,主隔离实例终止时不影响其执行。目前,没有默认的 API 来处理这些场景,插件作者和开发者需要处理 Flutter 引擎的底层基础来创建后台隔离实例并建立层间通信。
创建后台进程的步骤如下:
1. 定义 Flutter 后台隔离实例的入口点,类似于应用的 main() 函数。
2. 启动后台隔离实例:
- 从应用端通过方法调用向应用的原生端发送请求,启动新的隔离实例。
- 在原生端创建所需的结构,并独立于发起请求的应用运行新的隔离实例。
- 后台隔离实例准备好并运行后,通知原生端可以与其通信。
3. 应用可以开始向原生端发送请求,原生端处理与 Flutter 结构相关的事情并委托给后台隔离实例。
通信可以简化为:主隔离实例和后台隔离实例之间的直接通信是可选的,且难以维护,目前最简单的方法是通过请求或操作系统触发,使后台隔离实例独立于主应用隔离实例运行。
以下是一个使用斐波那契算法的示例,当应用终止时,后台进程仍会继续运行并将日志打印到设备日志中。
3.1 初始化计算
从应用端点击计算按钮时,应初始化之前看到的进程。首先通过方法通道调用方法,示例采用插件结构,方便修改和使用。插件的唯一方法是 calculateInBackgroundProcess,从应用端调用:
HandsOnBackgroundProcess.calculateInBackgroundProcess();
以下是具体实现代码:
const pluginChannel =
MethodChannel('com.example.handson/plugin_channel');
class HandsOnBackgroundProcess {
static void calculateInBackgroundProcess() async {
final callbackHandle = PluginUtilities.getCallbackHandle(
backgroundIsolateMain
);
await pluginChannel.invokeMethod(
"initBackgroundProcess",
[callbackHandle.toRawHandle()]
);
}
}
具体步骤如下:
1. 定义名为 com.example.handson/plugin_channel 的方法通道,这是插件开发的常见第一步。
2. 在 calculateInBackgroundProcess() 方法中,使用 PluginUtilities.getCallbackHandle 获取新后台隔离实例入口点的句柄,将其传递给应用的原生端。
3. 调用 “initBackgroundProcess” 方法并传递句柄,该方法将完成隔离实例的相关工作。
3.2 后台隔离实例
传递给插件原生部分的 Dart 回调负责计算斐波那契数,代码如下:
void backgroundIsolateMain() {
print('background isolate entry point running');
const backgroundchannel = MethodChannel(
'com.example.handson/background_channel'
);
WidgetsFlutterBinding.ensureInitialized();
backgroundchannel.setMethodCallHandler((MethodCall call) async {
if (call.method == 'calculate') {
print('calculating fibonacci from a background process');
int first = 0;
int second = 1;
for (var i = 2; i <= 50; i++) {
var temp = second;
second = first + second;
first = temp;
sleep(Duration(milliseconds: 500));
print("first: $first, second: $second.");
}
print('finished calculating fibo');
backgroundchannel.invokeMethod("calculationFinished");
}
});
backgroundchannel.invokeMethod("backgroundIsolateInitialized");
}
与之前的代码相比,有以下变化:
1. 后台隔离实例首先设置一个名为 com.example.handson/background_channel 的方法通道,用于与在后台执行的原生代码(Android 上的服务和 iOS 上的后台执行)建立通信。
2. 设置 calculate 方法的处理程序,以便原生代码可以调用它来启动计算。
3. 设置方法通道后,调用 backgroundIsolateInitialized 通知原生端,此时 Dart 端准备就绪。
对于后台执行的 Dart 端,只需实现一次,然后针对每个平台(Android/iOS)设置隔离实例运行的环境。接下来将介绍如何添加 Android 特定代码来在后台运行 Dart 代码。
4. 添加 Android 特定代码以在后台运行 Dart 代码
在 Android 中,服务(Services)是在后台独立于主应用执行代码的理想方式。因此,我们需要创建一个服务方法,绑定新的后台隔离实例并运行。
4.1 HandsOnBackgroundProcess 插件类
首先设置插件,实现静态 registerWith 方法,通知 Flutter 引擎插件实例的存在:
class HandsOnBackgroundProcessPlugin(
private val context: Context
) : MethodChannel.MethodCallHandler{
companion object {
@JvmStatic
fun registerWith(registrar: PluginRegistry.Registrar) {
val channel = MethodChannel(
registrar.messenger(),
"com.example.handson/plugin_channel"
)
val plugin = HandsOnBackgroundProcessPlugin(
registrar.context()
)
channel.setMethodCallHandler(plugin)
}
}
override fun onMethodCall(call: MethodCall, result:
MethodChannel.Result?) {
val args = call.arguments() as? ArrayList<*>
if (call.method == "initBackgroundProcess") {
val callbackHandle = args?.get(0) as? Long ?: return
executeBackgroundIsolate(context, callbackHandle)
}
}
}
在 onMethodCall 方法中,处理 initBackgroundProcess 方法时,根据 StandardMessageCodec 类将从 Dart 传来的回调句柄解析为 Long 类型。执行后台隔离实例分两步:
private fun executeBackgroundIsolate(context: Context, callbackHandle:
Long) {
val preferences = context.getSharedPreferences(
SHARED_PREFERENCES_KEY,
IntentService.MODE_PRIVATE
)
preferences.edit().putLong(ARG_CALLBACK_KEY, callbackHandle).apply()
startBackgroundService(context)
}
private fun startBackgroundService(context: Context) {
val intent = Intent(
context,
BackgroundProcessService::class.java
)
context.startService(intent)
}
首先,将句柄值存储在共享首选项(SharedPreferences)文件中,然后通过 startBackgroundService() 方法请求执行后台服务。共享首选项用于在 Android 中以简单和私密的方式存储键值数据,因为不能向服务构造函数传递参数,所以使用它。更多关于共享首选项的信息可查看官方文档: https://developer.android.com/training/data-storage/shared-preferences 。
4.2 BackgroundProcessService 类
BackgroundProcessService 类是在隔离实例执行时运行的 Android 服务,应用关闭时隔离实例仍能正常运行。服务的生命周期由 Android 系统管理,我们需要根据系统提供的事件来执行隔离实例。
class BackgroundProcessService : Service(),
MethodChannel.MethodCallHandler {
override fun onCreate() {
super.onCreate()
createNotification()
FlutterMain.ensureInitializationComplete(applicationContext,
null)
startBackgroundIsolate()
}
// ...
}
onCreate 方法的具体操作如下:
1. 通过 createNotification() 方法设置通知,将服务置于前台模式运行。因为后台运行的服务在资源不足时更可能被系统终止,而前台服务优先级较高,不太容易被终止。
2. 调用 FlutterMain.ensureInitializationComplete(applicationContext, null) 确保 Flutter 引擎已初始化,可以使用平台通道等功能。
3. 调用 startBackgroundIsolate() 方法启动隔离实例。
startBackgroundIsolate() 方法是该类中最主要和复杂的方法,负责设置后台隔离实例运行所需的结构:
private fun startBackgroundIsolate() {
val preferences = applicationContext.getSharedPreferences(
SHARED_PREFERENCES_KEY,
MODE_PRIVATE
)
val callbackHandle = preferences.getLong(ARG_CALLBACK_KEY, 0L)
if (callbackHandle == 0L) return
val callback =
FlutterCallbackInformation.lookupCallbackInformation(
callbackHandle
) ?: return
sBackgroundFlutterView = FlutterNativeView(this, true)
val path = FlutterMain.findAppBundlePath(applicationContext)
val args = FlutterRunArguments()
args.bundlePath = path
args.entrypoint = callback.callbackName
args.libraryPath = callback.callbackLibraryPath
sBackgroundFlutterView?.runFromBundle(args)
backgroundChannel = MethodChannel(
sBackgroundFlutterView,
"com.example.handson/background_channel"
)
backgroundChannel?.setMethodCallHandler(this)
sPluginRegistrantCallback?.registerWith(
sBackgroundFlutterView?.pluginRegistry
)
}
具体步骤如下:
1. 从存储的共享首选项中获取 Dart 回调的句柄,使用 FlutterCallbackInformation.lookupCallbackInformation 方法检索运行所需的回调信息。
2. 实例化一个新的 FlutterNativeView 方法,为新隔离实例提供运行环境。第二个参数为 true 表示该视图将在后台运行,不需要绘制表面。
3. 使用 FlutterNativeView 的 runFromBundle() 方法执行隔离实例,该方法需要一个 FlutterRunArguments 实例来确定要运行的内容。
4. 运行后台隔离实例后,创建一个名为 com.example.handson/background_channel 的后台方法通道实例。
5. 最后,通过 sPluginRegistrantCallback 属性在 Flutter 注册表中注册插件实例。因为 Flutter 在使用插件时会自动在主线程中注册插件,而在服务中需要手动注册,所以使用 PluginRegistrantCallback 来实现。更多关于 Android 线程的信息可查看文档: https://flutter.dev/docs/get-started/flutter-for/android-devs#how-do-you-move-work-to-a-background-thread 。
通过以上步骤,我们可以在 Android 平台上实现 Flutter 后台进程的运行,确保在应用关闭时某些任务仍能继续执行。
Flutter 中的消息编解码器与后台进程开发
5. 总结与注意事项
在 Flutter 开发中,消息编解码器和后台进程的实现是非常重要的部分,它们为应用提供了更强大的功能和更好的性能。以下是对整个过程的总结以及一些需要注意的事项:
5.1 消息编解码器总结
消息编解码器是实现 Dart 与原生语言之间数据转换的关键。不同的编解码器适用于不同的数据类型和场景,例如:
- BinaryCodec 用于处理未编码的二进制消息。
- JSONMessageCodec 用于处理 UTF - 8 编码的 JSON 消息。
- StringCodec 用于处理 UTF - 8 编码的字符串消息。
- StandardMessageCodec 是最常用的编解码器,它能将 Dart 类型转换为 Android/iOS 类型,MethodChannel 默认使用它进行数据的序列化和反序列化。
在选择编解码器时,需要根据实际的数据类型和通信需求进行选择。
5.2 后台进程总结
后台进程的实现可以分为以下几个关键步骤:
1. 定义入口点 :定义 Flutter 后台隔离实例的入口点,类似于应用的 main() 函数。
2. 启动隔离实例 :通过方法通道从应用端向原生端发送请求,在原生端创建并运行新的隔离实例。
3. 建立通信 :在 Dart 端和原生端分别创建方法通道,用于双方的通信。
4. 平台适配 :针对不同的平台(如 Android 和 iOS),需要进行特定的配置和实现。例如在 Android 中,使用 Service 来实现后台运行。
5.3 注意事项
- 隔离实例限制 :传递给 compute() 函数的消息和返回值有一定限制,这些限制来自隔离实例的通信层。在使用时需要注意数据类型的选择和处理。
- 服务生命周期 :在 Android 中,Service 的生命周期由系统管理,需要根据系统提供的事件来执行隔离实例。同时,为了避免服务被系统终止,建议将服务设置为前台服务。
- 插件注册 :在后台隔离实例中,插件需要手动注册,因为 Flutter 会自动在主线程中注册插件。
6. 流程图展示
为了更清晰地展示创建后台进程的流程,下面是一个 mermaid 格式的流程图:
graph LR
classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px;
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px;
A([开始]):::startend --> B(定义后台隔离实例入口点):::process
B --> C(应用端发送启动请求):::process
C --> D{原生端接收到请求?}:::decision
D -- 是 --> E(原生端创建隔离实例结构):::process
E --> F(原生端运行隔离实例):::process
F --> G(隔离实例准备就绪):::process
G --> H(通知原生端可通信):::process
H --> I(应用端向原生端发送请求):::process
I --> J(原生端处理请求并委托给隔离实例):::process
J --> K([结束]):::startend
D -- 否 --> C(应用端发送启动请求):::process
这个流程图展示了从定义后台隔离实例入口点到应用端与隔离实例通信的整个过程,帮助我们更好地理解后台进程的创建和运行机制。
7. 未来展望
随着 Flutter 框架的不断发展,后台进程的实现可能会变得更加简单和高效。目前,虽然已经有了一些方法来实现后台进程,但仍然需要处理一些底层的细节和平台特定的问题。未来可能会有更多的默认 API 来支持后台进程的创建和管理,减少开发者的工作量。
同时,对于消息编解码器,也可能会有更多的优化和扩展,以支持更多的数据类型和更复杂的通信场景。这将使得 Flutter 在跨平台开发中更加灵活和强大。
总之,掌握消息编解码器和后台进程的开发是 Flutter 开发者的重要技能之一,随着技术的不断进步,我们可以期待更加便捷和高效的开发体验。
通过以上的介绍和示例,我们详细了解了 Flutter 中消息编解码器和后台进程的开发方法。在实际开发中,根据具体的需求和场景,合理运用这些技术,可以为应用带来更好的性能和用户体验。希望这篇博客能对 Flutter 开发者有所帮助。
超级会员免费看
2886

被折叠的 条评论
为什么被折叠?



