一、先搞懂咱们要解决的问题:Flutter混编里的“传消息瓶颈”
很多做跨端开发的朋友都用过Flutter,它能一套代码跑安卓和iOS,省了不少功夫。但实际做项目时,很少有人用纯Flutter写所有功能——总会有一些老代码、或者必须用原生系统能力的地方,比如调手机的指纹、蓝牙、老的支付SDK,这时候就得用“混合开发”:一部分用Flutter写,一部分用原生(安卓的Java/Kotlin、iOS的Objective-C/Swift)写。
那两边怎么传数据呢?官方给的方案是MethodChannel,简单说就是Flutter这边发个消息,原生那边接了处理,再给Flutter回个结果。但用久了就会发现问题:传大数据慢、传太频繁卡、甚至有时候消息还会丢?这就是咱们说的“瓶颈”。
举个最常见的例子:比如你做一个电商APP,Flutter写商品列表,原生那边有个“本地商品缓存”的功能,每次打开商品页,得从原生缓存里取100个商品的信息(每个商品有图片地址、价格、库存,加起来数据量不小)。用MethodChannel传的话,你会明显感觉页面加载慢,甚至有时候还卡主线程。
接下来咱们就拆这个问题,先看MethodChannel为啥会卡,再讲替代方案,最后讲怎么优化原生通信。
二、MethodChannel为啥会有瓶颈?(别光说“慢”,得说清楚为啥慢)
很多人只知道MethodChannel慢,但不知道慢在哪,咱们拆成3个核心原因,用大白话讲:
2.1 消息得“跨层翻译”,每一层都要耗时间
MethodChannel的原理是:Flutter这边先把数据转成原生能懂的格式(比如安卓是JSON,iOS是NSDictionary),然后发给原生的“消息桥”,原生那边再把这个数据转成自己能处理的格式(比如Java的对象),处理完再转回去,再发给Flutter,Flutter再转成Dart的对象。
这就像你要给外国朋友递一个装满零件的盒子,你得先把所有零件拆成零件清单(转格式),用翻译软件把清单翻译成英文(再转格式),朋友拿到清单再把零件拼起来(再转格式),处理完再把结果拆成清单、翻译、你再拼起来——每一步都要花时间,数据越大、步骤越多,时间越长。
2.2 同一时间只能传一个消息,像单车道堵车
MethodChannel的消息是“排队传”的,就像单车道的马路,同一时间只能过一辆车。如果你同时传10个消息,得等前一个传完才能传下一个。比如你要同时传商品数据、用户信息、订单数据,就得一个一个来,很容易堵。
2.3 大数据要“拆成小块”传,容易出错
如果传的数据特别大(比如一张高清图、一段视频),MethodChannel会自动把数据拆成很多小块,一块一块传。这就像你要搬一个大沙发,得拆成腿、靠背、扶手,一块一块搬,搬完再拼起来——中间只要有一块搬丢了,整个沙发就拼不起来,而且拆和拼的时间也很长。
三、替代方案:不用MethodChannel,换个传消息的法子
既然MethodChannel有这么多问题,咱们有没有别的法子?当然有,接下来讲2个最常用的替代方案,每个都给完整的例子。
3.1 方案一:用“共享内存”传数据(大数据首选)
啥是共享内存?就是Flutter和原生两边都能访问的一块“公共内存空间”,不用转格式、不用排队,直接读写。比如你要传大的商品数据,Flutter把数据写到共享内存里,原生直接去读,不用转格式,速度特别快。
咱们用安卓来举个完整的例子(iOS原理一样,只是代码不一样):
示例1:安卓端用共享内存传商品数据
首先,安卓这边用一个“共享内存类”,给Flutter留个接口,让Flutter能写数据,原生能读:
// 安卓端:商品共享内存类
class ProductSharedMemory {
// 共享内存的key,两边要一致
private const val SHARED_KEY = "PRODUCT_SHARED_DATA"
// 存储数据的容器,用内存缓存,不用转格式
private val dataCache = mutableMapOf<String, Any>()
// 给Flutter的接口:写数据
fun writeData(key: String, value: Any) {
dataCache[key] = value
}
// 给原生的接口:读数据
fun readData(key: String): Any? {
return dataCache[key]
}
// 给Flutter的接口:清空数据(可选,用完要清,不然占内存)
fun clearData(key: String) {
dataCache.remove(key)
}
}
然后,把这个类注册到Flutter的消息桥里,让Flutter能调用:
// 安卓端:注册到Flutter的消息桥
class FlutterEnginePlugin(flutterEngine: FlutterEngine) : FlutterPlugin, MethodCallHandler {
private lateinit var channel: MethodChannel
private val sharedMemory = ProductSharedMemory()
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "com.example.shared_memory")
channel.setMethodCallHandler(this)
}
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
when (call.method) {
// 处理Flutter的写数据请求
"writeSharedData" -> {
val key = call.argument<String>("key")!!
val value = call.argument<Any>("value")!!
sharedMemory.writeData(key, value)
result.success("write success")
}
// 处理Flutter的读数据请求
"readSharedData" -> {
val key = call.argument<String>("key")!!
val data = sharedMemory.readData(key)
result.success(data)
}
else -> result.notImplemented()
}
}
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
}
}
示例2:Flutter端调用共享内存
Flutter这边不用转格式,直接写数据:
// Flutter端:调用共享内存传商品数据
import 'package:flutter/services.dart';
// 定义和安卓端一致的消息通道
const MethodChannel sharedMemoryChannel = MethodChannel('com.example.shared_memory');
// 传100个商品数据的函数
Future<void> sendProductData(List<Map<String, dynamic>> productList) async {
try {
// 把商品数据直接写到共享内存,不用转格式
await sharedMemoryChannel.invokeMethod('writeSharedData', {
'key': 'product_list',
'value': productList
});
print('商品数据写入共享内存成功');
} on PlatformException catch (e) {
print('写入失败:${e.message}');
}
}
// 原生处理完后,Flutter读结果的函数
Future<dynamic> readResultData() async {
try {
final result = await sharedMemoryChannel.invokeMethod('readSharedData', {
'key': 'processed_product_result'
});
// 用完清空数据,释放内存
await sharedMemoryChannel.invokeMethod('clearData', {'key': 'processed_product_result'});
return result;
} on PlatformException catch (e) {
print('读取失败:${e.message}');
return null;
}
}
这个方案的好处是:传大数据速度特别快,不用转格式,适合传商品列表、高清图、视频这类大数据。坏处是:得自己管理内存(用完要清,不然占内存),小数据传的话反而麻烦。
3.2 方案二:用“事件总线”传数据(频繁传小数据首选)
啥是事件总线?就是一个“公共的消息中转站”,Flutter和原生两边都能往里面发消息,也能收消息,就像一个微信群,谁都能发消息,谁都能收消息。适合传频繁的小数据,比如用户的操作、状态变化。
咱们用iOS来举个完整的例子:
示例3:iOS端用事件总线传用户操作
iOS这边用系统自带的NotificationCenter当事件总线:
// iOS端:用户操作事件总线
class UserEventBus {
// 事件的key,两边要一致
private let userActionKey = "USER_ACTION_EVENT"
// 给Flutter的接口:发事件
func sendActionEvent(action: String, data: Any) {
// 往事件总线发消息
NotificationCenter.default.post(
name: NSNotification.Name(userActionKey),
object: nil,
userInfo: ["action": action, "data": data]
)
}
// 给原生的接口:收事件
func listenActionEvent(callback: @escaping (String, Any) -> Void) {
NotificationCenter.default.addObserver(
forName: NSNotification.Name(userActionKey),
object: nil,
queue: .main
) { notification in
guard let userInfo = notification.userInfo,
let action = userInfo["action"] as? String,
let data = userInfo["data"] else {
return
}
callback(action, data)
}
}
}
然后,把这个类注册到Flutter的消息桥里:
// iOS端:注册到Flutter的消息桥
class FlutterEventPlugin: NSObject, FlutterPlugin {
private let eventBus = UserEventBus()
static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(
name: "com.example.event_bus",
binaryMessenger: registrar.messenger()
)
let instance = FlutterEventPlugin()
registrar.addMethodCallDelegate(instance, channel: channel)
}
func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
case "sendEvent":
guard let args = call.arguments as? [String: Any],
let action = args["action"] as? String,
let data = args["data"] else {
result(FlutterError(code: "INVALID_ARGS", message: "参数错误", details: nil))
return
}
eventBus.sendActionEvent(action: action, data: data)
result("send success")
default:
result(FlutterMethodNotImplemented)
}
}
}
示例4:Flutter端调用事件总线
Flutter这边用EventBus(第三方库,专门处理事件的)来收消息:
// Flutter端:调用事件总线传用户操作
import 'package:flutter/services.dart';
import 'package:event_bus/event_bus.dart';
// 定义和iOS端一致的消息通道
const MethodChannel eventBusChannel = MethodChannel('com.example.event_bus');
// 定义Flutter端的事件总线
final EventBus flutterEventBus = EventBus();
// 给原生发用户操作事件的函数
Future<void> sendUserAction(String action, dynamic data) async {
try {
await eventBusChannel.invokeMethod('sendEvent', {
'action': action,
'data': data
});
print('用户操作事件发送成功');
} on PlatformException catch (e) {
print('发送失败:${e.message}');
}
}
// 收原生发的事件的函数
void listenNativeEvent() {
flutterEventBus.on<NativeEvent>().listen((event) {
print('收到原生事件:${event.action},数据:${event.data}');
// 处理原生事件,比如更新Flutter的UI
});
}
// 定义原生事件的类
class NativeEvent {
final String action;
final dynamic data;
NativeEvent(this.action, this.data);
}
这个方案的好处是:传频繁的小数据速度快,不用排队,适合传用户操作、状态变化这类小数据。坏处是:大数据传的话,还是要转格式,速度慢。
四、如果非要用MethodChannel,怎么优化?
如果项目已经用了MethodChannel,不想换方案,咱们也能优化,接下来讲3个实用的优化技巧:
4.1 技巧一:把数据转成“二进制”传,减少转格式的时间
MethodChannel默认传的是JSON(安卓)或NSDictionary(iOS),转格式慢。咱们可以把数据转成二进制(比如Uint8List)传,转格式的时间会大大减少。
比如传商品数据,Flutter端把商品列表转成二进制:
// Flutter端:把商品数据转成二进制
import 'dart:convert';
List<Map<String, dynamic>> productList = [/* 100个商品的列表 */];
// 转成JSON字符串,再转成二进制
Uint8List binaryData = utf8.encode(jsonEncode(productList));
// 传给原生
await methodChannel.invokeMethod('sendProductData', {'data': binaryData});
安卓端把二进制转成JSON字符串,再转成Java对象:
// 安卓端:把二进制转成商品对象
fun handleBinaryData(data: ByteArray) {
// 转成JSON字符串
val jsonStr = String(data, Charset.forName("UTF-8"))
// 转成Java对象(用Gson库)
val productList = Gson().fromJson(jsonStr, TypeToken<List<Product>>().type)
// 处理商品数据
}
这个技巧能减少转格式的时间,传大数据的速度能提升30%-50%。
4.2 技巧二:批量传数据,减少传的次数
如果要传多个小数据,不要一个一个传,要批量传。比如你要传用户的名字、年龄、性别,不要传3次,要把这3个数据打包成一个对象传1次。
比如:
// 不好的写法:一个一个传
await methodChannel.invokeMethod('sendName', {'name': '张三'});
await methodChannel.invokeMethod('sendAge', {'age': 25});
await methodChannel.invokeMethod('sendGender', {'gender': '男'});
// 好的写法:批量传
await methodChannel.invokeMethod('sendUserInfo', {
'name': '张三',
'age': 25,
'gender': '男'
});
这个技巧能减少传消息的次数,避免排队堵车。
4.3 技巧三:把处理逻辑放到“后台线程”,不卡主线程
MethodChannel默认是在主线程处理的,处理大数据的时候会卡主线程,导致UI卡顿。咱们可以把处理逻辑放到后台线程,主线程只负责传消息。
比如安卓端处理大数据,放到后台线程:
// 安卓端:把处理逻辑放到后台线程
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
when (call.method) {
"sendProductData" -> {
val binaryData = call.argument<ByteArray>("data")!!
// 放到后台线程处理
Thread {
val productList = Gson().fromJson(String(binaryData), TypeToken<List<Product>>().type)
// 处理商品数据,比如缓存到本地
saveProductToCache(productList)
// 处理完,主线程回结果
mainHandler.post {
result.success("处理成功")
}
}.start()
}
else -> result.notImplemented()
}
}
这个技巧能避免卡主线程,提升用户体验。
五、不同方案的对比和选择
咱们把前面讲的方案整理一下,方便大家选择: | 方案 | 适合场景 | 优点 | 缺点 | |---------------|---------------------------|-----------------------|-----------------------| | MethodChannel | 小数据、不频繁传 | 官方支持、简单易用 | 大数据慢、频繁传卡 | | 共享内存 | 大数据、偶尔传 | 速度快、不用转格式 | 要自己管理内存 | | 事件总线 | 小数据、频繁传 | 速度快、不用排队 | 大数据慢、要自己管理事件 |
选择建议:
- 如果传大数据(比如商品列表、高清图),选共享内存;
- 如果传频繁的小数据(比如用户操作、状态变化),选事件总线;
- 如果项目已经用了MethodChannel,不想换,就用前面讲的3个优化技巧。
六、注意事项
- 共享内存要用完清空,不然会占内存,导致APP卡顿;
- 事件总线要注意事件的key,两边要一致,不然收不到消息;
- 不管用哪种方案,传数据的时候要做异常处理,比如参数错误、传失败的情况;
- 安卓和iOS的方案原理一样,但代码不一样,要分别处理。
七、总结
Flutter混合开发里的MethodChannel瓶颈,本质是转格式慢、排队传、拆块传的问题。咱们可以用共享内存(大数据)、事件总线(频繁小数据)替代MethodChannel,也可以优化MethodChannel本身。选方案的时候,要根据自己的场景来,适合的才是最好的。
评论
围绕“Flutter混合开发MethodChannel瓶颈:替代方案与Dart原生通信性能优化”参与讨论