一、先搞懂咱们要解决的问题: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 | 小数据、不频繁传 | 官方支持、简单易用 | 大数据慢、频繁传卡 | | 共享内存 | 大数据、偶尔传 | 速度快、不用转格式 | 要自己管理内存 | | 事件总线 | 小数据、频繁传 | 速度快、不用排队 | 大数据慢、要自己管理事件 |

选择建议:

  1. 如果传大数据(比如商品列表、高清图),选共享内存;
  2. 如果传频繁的小数据(比如用户操作、状态变化),选事件总线;
  3. 如果项目已经用了MethodChannel,不想换,就用前面讲的3个优化技巧。

六、注意事项

  1. 共享内存要用完清空,不然会占内存,导致APP卡顿;
  2. 事件总线要注意事件的key,两边要一致,不然收不到消息;
  3. 不管用哪种方案,传数据的时候要做异常处理,比如参数错误、传失败的情况;
  4. 安卓和iOS的方案原理一样,但代码不一样,要分别处理。

七、总结

Flutter混合开发里的MethodChannel瓶颈,本质是转格式慢、排队传、拆块传的问题。咱们可以用共享内存(大数据)、事件总线(频繁小数据)替代MethodChannel,也可以优化MethodChannel本身。选方案的时候,要根据自己的场景来,适合的才是最好的。