Channel通道的定义与实现
前言
需求是这个样子的,应用需要收集用户的头像,这个简单,直接用官方的 Camera 插件拍照即可。
但是有些用户会传递一些非人脸的图片,或多人脸的图片,导致业务无法继续,所以需要移动端在收集人脸的同时校验人脸数量。
具体效果如下:
如何实现?其实也简单,Pub里面有很多开源的框架。比较出名的例如 google_mlkit_face_detection
支持 Android 与 iOS ,本身也是很优秀的框架了,由于部分原因并没有选择第三方的框架。
换个方向,类似的功能其实各自的原生API已经有对应的实现,我们直接自己写Channel不是就可以了吗?不需要重复导一个比较重的库去实现这一个相对简单的功能。
那么如何定义与使用Channel呢?
一、Channel的类型与实现
我们常说的 Channel 全名叫 Platform Channel,它是Flutter和原生通信的工具,有三种类型:
- MethodChannel:用于传递方法调用(method invocation),Flutter和平台端进行直接方法调用时候可以使用。
- BasicMessageChannel:用于传递字符串和半结构化的信息,Flutter和平台端进行消息数据交换时候可以使用。
- EventChannel:用于数据流(event streams)的通信,Flutter和平台端进行事件监听、取消等可以使用。
官方Demo在此【传送门】
简单的理解:
-
MethodChannel: Flutter可以通过它调用原生方法,具体的实现由各自原生平台实现,这种方案也是最常用的。
-
EventChannel: 用于在事件流中将消息传递给Flutter端,常用于原生平台的监听数据(比如传感器)传递给Flutter端展示或处理。
-
BasicMessageChannel:是一种简单的双向消息通信渠道,它允许Flutter和原生平台通过字符串或字节流发送消息,并返回一个响应,它是最基础的可以实现 MethodChannel 和 EventChannel 的功能。
1.1 MethodChannel 实现示例
这里以我们上面说的人脸数量检测为例,我们现在Flutter中定义对应的MethodChannel,定义它的通道名与方法名。
定义如下:
ini
final _platform = MethodChannel('face_detection');
String response = await _platform.invokeMethod('checkFace');
当然一般我们会封装在一个类中便于统一管理。
那么在Android中的实现呢?
MethodChannel的构建需要两个参数,一个是BinaryMessenger,通常从Flutter Engine中获取,可以通过普通的Engine构建,也可以通过EngineCache预热引擎来获取,当然也可以使用EngineGroup来获取,如果在FlutterActivity里面,可以直接在configureFlutterEngine回调中获取。另一个参数是name,用于标识这个Channel。
具体的实现如下:
kotlin
class MainActivity : FlutterActivity() {
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor, "face_detection")
.setMethodCallHandler { call, result ->
if (call.method == "checkFace") {
val imagePath = call.arguments as String
val faceCount = detectFaces(imagePath)
result.success(faceCount)
} else {
result.notImplemented()
}
}
}
private fun detectFaces(imagePath: String): Int {
return CheckFaceUtils.checkFace(imagePath)
}
}
CheckFaceUtils:Android原生API实现的人脸检测,代码如下:
arduino
public class CheckFaceUtils {
public static Bitmap rotateBitmapIfNeeded(Bitmap bitmap) {
int width = bitmap.getWidth();
int height = bitmap.getHeight();
// 判断是否需要旋转
if (width > height) {
Matrix matrix = new Matrix();
matrix.postRotate(270); // 旋转90度
return Bitmap.createBitmap(bitmap, 0, 0, width, height, matrix, true);
} else {
return bitmap;
}
}
/**
* 检查BitMap中包含的人脸数量
*/
public static int checkFace(String imagePath) {
Bitmap b = BitmapFactory.decodeFile(imagePath);
if (b != null) {
//处理横竖Bitmap的旋转
b = rotateBitmapIfNeeded(b);
// 检测前必须转化为RGB_565格式。文末有详述连接
Bitmap bitmap = b.copy(Bitmap.Config.RGB_565, true);
b.recycle();
// 设置你想检测的数量,数值越大错误率越高,所以需要置信度来判断,但有时候置信度也会出问题
int MAX_FACES = 5; // I found it can detect number of face at least 27,
FaceDetector faceDet = new FaceDetector(bitmap.getWidth(), bitmap.getHeight(), MAX_FACES);
// 将人脸数据存储到faceArray中
FaceDetector.Face[] faceArray = new FaceDetector.Face[MAX_FACES];
// 返回找到图片中人脸的数量,同时把返回的脸部位置信息放到faceArray中,过程耗时,图片越大耗时越久
int findFaceNum = faceDet.findFaces(bitmap, faceArray);
Log.w("FaceSDKUtils", "找到脸部数量:" + findFaceNum);
bitmap.recycle();
return findFaceNum;
} else {
Log.w("FaceSDKUtils", "目标文件不是图片,无法获取Bitmap");
return -1;
}
}
}
iOS 的实现,也是类似的思路,只是人脸检测的代码比Android还要简单,具体代码如下:
swift
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
var channel:FlutterMethodChannel!
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
self.initPlatformMethods()
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
func initPlatformMethods(){
self.channel = FlutterMethodChannel.init(name: "face_detection", binaryMessenger: self.window.rootViewController as! FlutterBinaryMessenger)
self.channel.setMethodCallHandler { call, result in
if (call.method == "checkFace"){
result(self.checkFace(path: call.arguments as! String));
}
}
}
func checkFace(path:String) -> Int{
var image = CIImage.init(image: .init(contentsOfFile: path)!)
var detector = CIDetector.init(ofType: CIDetectorTypeFace, context: nil, options: [CIDetectorAccuracy:CIDetectorAccuracyHigh])
var features = detector!.features(in: image!);
return features.count;
}
}
它是在应用初始化的时候就注册了。
Log如下:
1.2 EventChannel实现示例
我们以监听原生平台的重力加速度传感器的值为例,把原生平台的数据以 Stream 的方式传递给 Flutter 端。
先定义一个对象用于传递
kotlin
class AccelerometerReadings {
final double x;
final double y;
final double z;
AccelerometerReadings(this.x, this.y, this.z);
}
Flutter的代码实现如下:
vbnet
child: StreamBuilder<AccelerometerReadings>(
stream: EventChannel('eventChannelDemo').receiveBroadcastStream().map(
(dynamic event) => AccelerometerReadings(
event[0] as double,
event[1] as double,
event[2] as double,
),
),
builder: (context, snapshot) {
if (snapshot.hasError) {
return Text((snapshot.error as PlatformException).message!);
} else if (snapshot.hasData) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'x轴: ' + snapshot.data!.x.toStringAsFixed(3),
style: textStyle,
),
Text(
'y轴: ' + snapshot.data!.y.toStringAsFixed(3),
style: textStyle,
),
Text(
'z轴: ' + snapshot.data!.z.toStringAsFixed(3),
style: textStyle,
)
],
);
}
当然了,这是简单的使用,其实一般都是封装到一个类中便于统一管理。
Android端的实现:
kotlin
class MainActivity : FlutterActivity() {
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
val sensorManger: SensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager
val accelerometerSensor: Sensor = sensorManger.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
EventChannel(flutterEngine.dartExecutor, "eventChannelDemo").setStreamHandler(AccelerometerStreamHandler(sensorManger, accelerometerSensor))
}
}
具体传感器的代码实现:
kotlin
class AccelerometerStreamHandler(sManager: SensorManager, s: Sensor) : EventChannel.StreamHandler, SensorEventListener {
private val sensorManager: SensorManager = sManager
private val accelerometerSensor: Sensor = s
private lateinit var eventSink: EventChannel.EventSink
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
if (events != null) {
eventSink = events
sensorManager.registerListener(this, accelerometerSensor, SensorManager.SENSOR_DELAY_UI)
}
}
override fun onCancel(arguments: Any?) {
sensorManager.unregisterListener(this)
}
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {}
override fun onSensorChanged(sensorEvent: SensorEvent?) {
if (sensorEvent != null) {
val axisValues = listOf(sensorEvent.values[0], sensorEvent.values[1], sensorEvent.values[2])
eventSink.success(axisValues)
} else {
eventSink.error("DATA_UNAVAILABLE", "Cannot get accelerometer data", null)
}
}
}
1.3 BasicMessageChannel 实现示例
之前我们都是演示的 Flutter 拿原生平台的数据,这里我们以原生 Android 平台拿 Flutter 的数据为例,演示Android平台拿到 Flutter 项目中的图片资源。
当然只是示例啊,真实项目很少这么干...
同样的先在 Flutter 端先定义 BasicMessageChannel 对象,指明通道名,并发送数据:
ini
final channelToAndroid = BasicMessageChannel<ByteData>(
'image_data_from_flutter',
BinaryCodec(),
);
// 获取assets中的图片对应的ByteData数据,并发送给原生
rootBundle.load(Assets.imagesBlackBack).then((value) async {
ByteData? res = await channelToAndroid.send(value);
Log.d('res :$res');
if (res != null) {
// 将 ByteData 转换为字节数组
Uint8List bytes = res.buffer.asUint8List();
// 将字节数组转换为字符串
String stringData = utf8.decode(bytes);
// 在控制台输出接收到的字符串
Log.d('Received string from Android: $stringData');
}
});
需要注意的是,这里使用 BinaryCodec,数据格式为 ByteData,如果是想传送 String 字符串类型,那么就可以指定为 StringCodec() 。
那么在原生中的实现:
kotlin
class MainActivity : FlutterActivity() {
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
BasicMessageChannel<ByteBuffer>(
flutterEngine.dartExecutor, "image_data_from_flutter", BinaryCodec.INSTANCE
).setMessageHandler { message, reply ->
//转换为Android需要的ByteArray
val byteBuffer = message as ByteBuffer
val imageByteArray = ByteArray(byteBuffer.capacity())
byteBuffer.get(imageByteArray)
Log.d("TAG","imageByteArray:$imageByteArray")
//收到之后如果想回复给Flutter端,也可以加一下代码
val str = "感谢Flutter老哥送上的图片数据"
val strBytes = str.toByteArray(Charset.forName("UTF-8"))
val byteBuffer2 = ByteBuffer.allocateDirect(strBytes.size)
byteBuffer2.put(strBytes)
byteBuffer2.flip()
reply.reply(byteBuffer2)
}
}
}
这样就可以完成一个简单的类似请求与响应的效果,那么如何做到双端通信呢?
其实我们在原生端创建了两个 BasicMessageChannel 对象,分别用于从 Flutter 端接收数据(channelFromFlutter)和向 Flutter 端发送数据(channelToFlutter)。在 Flutter 端也创建了两个相应的 BasicMessageChannel 对象,用于和原生端进行双向通信。
我们可以先定义 Flutter 端的两个通道,代码如下:
dart
final channelFromAndroid = BasicMessageChannel<String>(
'image_data_to_flutter',
StringCodec(),
);
final channelToAndroid = BasicMessageChannel<String>(
'image_data_from_flutter',
StringCodec(),
);
// 获取assets中的图片对应的ByteData数据,并发送给原生
String? res = await channelToAndroid.send('我是来自Flutter的字符串');
Log.d('收到来自Android的Reply :$res');
channelFromAndroid.setMessageHandler((receivedData) async {
// 在这里处理来自 Android 端的数据
Log.d('收到来自Android发过来的数据: $receivedData');
return "收到了感谢Android老铁发来得到数据";
});
下面就是定义 Android 端的两个通道,代码如下:
kotlin
class MainActivity: FlutterActivity() {
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
BasicMessageChannel(
flutterEngine.dartExecutor, "image_data_from_flutter", StringCodec.INSTANCE
).setMessageHandler { message, reply ->
//转换为Android需要的ByteArray
Log.d("TAG","收到来自Flutter的字符串:$message")
//收到之后如果想回复给Flutter端,也可以加一下代码
reply.reply("感谢Flutter老哥送来的数据,已经收到了")
}
BasicMessageChannel(
flutterEngine.dartExecutor, "image_data_to_flutter", StringCodec.INSTANCE
).send("这个字符串是我主动Send给Flutter的,你能收到吗?") {
Log.d("TAG", "Flutter的回复收到了:$it");
}
}
}
效果如下:
当然这是 Demo 效果,真实场景会把 BasicMessageChannel 抽取出来根据具体逻辑判断是使用 reply 还是使用主动的 send 来进行消息的传递。
三、自定义插件的自动实现
对于三种 Channel 我们都已经了解了,都是在我们自己项目中手动注册的,为什么我看一些第三方的插件都没有 FlutterActivity ,他们都没有手动注册,他们是怎么实现 Channel 的注册的?
网上的博客文章或者一些AI工具会告诉你 Channel 是这么写的,需要实现 FlutterPlugin 接口,类似如下:
kotlin
class FaceDetectionChannel : FlutterPlugin, MethodChannel.MethodCallHandler {
private var context: Context? = null
private var channel: MethodChannel? = null
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
if (call.method == "detectFaces") {
val imagePath = call.arguments as String
val faceCount = detectFaces(imagePath) // 调用你的人脸检测方法,返回人脸数量
result.success(faceCount)
} else {
result.notImplemented()
}
}
private fun detectFaces(imagePath: String): Int {
return FaceSDKUtils.checkFace(imagePath)
}
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
channel = MethodChannel(binding.binaryMessenger, "face_detection_channel")
channel?.setMethodCallHandler(this)
context = binding.applicationContext
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
channel?.setMethodCallHandler(null)
}
}
但是这样并不能真正的注册,这种是插件的写法,当我们在 pubspec.yaml 文件中依赖一个插件的时候,插件中指定了 pluginClass,会自动调用 onAttachedToEngine 方法,所以它才能达到'自动初始化'
的效果。
如果是在自己的项目中写 Channel 则大可不必这么写,直接在 FlutterActivity 或 FlutterApplication 中手动注册即可。
那么我就想写一个这样的,以本地插件的形式导入到项目行不行?当然可以。
第一步我们需要在本地插件的 pubspec.yaml 中指定 plgun
yaml
name: face_detect
description: 找到Path中人脸数量
version: 0.0.1
homepage:
environment:
sdk: '>=3.0.2 <4.0.0'
flutter: ">=1.20.0"
dependencies:
platform: ^3.0.0
flutter:
sdk: flutter
dev_dependencies:
test: ^1.17.4
mockito: ^5.0.7
# The following section is specific to Flutter.
flutter:
plugin:
platforms:
android:
package: com.newki.facedetect
pluginClass: FaceDetectionChannel
ios:
pluginClass: FaceDetectionChannel
在lib中只需要定义 Channel 的 Flutter 端代码:
dart
class FaceDetectionChannel {
//初始化MethodChannel对象
static const MethodChannel _channel = MethodChannel('face_detection_channel');
/// 各种平台自行实现检测人脸数量
static Future<int> detectFaces(String imagePath) async {
try {
Log.d('Flutter -> detectFaces -> imagePath:$imagePath');
final int faceCount = await _channel.invokeMethod('detectFaces', imagePath);
return faceCount;
} on PlatformException catch (e) {
Log.e(e.message ?? 'detectFaces->未知异常');
return -1;
}
}
}
Android 的具体实现上面已经给出,下面是iOS的代码:
swift
import Flutter
class FaceDetectionChannel {
static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(name: "face_detection_channel", binaryMessenger: registrar.messenger())
let instance = FaceDetectionChannel()
registrar.addMethodCallDelegate(instance, channel: channel)
}
func detectFaces(imagePath: String) -> Int {
let image = CIImage(contentsOfURL: URL(fileURLWithPath: imagePath))
let detector = CIDetector(ofType: CIDetectorTypeFace, context: nil, options: [CIDetectorAccuracy: CIDetectorAccuracyHigh])
let features = detector?.features(in: image)
return features?.count ?? 0
}
}
extension FaceDetectionChannel: FlutterPlugin {
static func register(with registrar: FlutterPluginRegistrar) {
FaceDetectionChannel.register(with: registrar)
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
if call.method == "detectFaces" {
let arguments = call.arguments as? [String: Any]
let imagePath = arguments?["imagePath"] as? String ?? ""
let faceCount = detectFaces(imagePath: imagePath)
result(faceCount)
} else {
result(FlutterMethodNotImplemented)
}
}
}
使用的时候直接在自己的项目的 pubspec.yaml 文件中引入自己的本地插件:
yaml
face_detect:
path: face_detect_plugin
这样就可以实现自动 Channel 的注册与实现了。当然你也可以把这个插件发布到Pub 上,这样就可以开源出去供其他人使用了,如何发布项目到 Pub? 很多教程自己可以搜索一下并不复杂...
总结
本文总结了三种 Channel 的具体使用示例,并且说明了自己项目中的 Channel 与插件中的 Channel 初始化的不同方式。
为什么我们一定要掌握 Channel 的使用?
-
平台特定功能的调用:在跨平台开发中,某些平台特有的功能可能无法直接使用 Flutter 提供的现成解决方案。这时,您可以通过 Channel 实现与原生代码的通信,调用平台特定的功能。
-
性能优化:有时候,一些性能要求较高的任务可能需要在原生代码中执行。通过使用 Channel,您可以将这些任务委托给原生层,从而提升应用的性能和响应速度。
-
第三方库支持:尽管 Flutter 生态圈非常强大,但仍然有一些功能不可或缺的第三方库可能没有对应的 Flutter 插件。通过 Channel,您可以轻松地集成和使用这些第三方库,拓宽了应用的功能范围。
-
访问硬件功能:某些硬件功能(如相机、传感器等)可能需要直接与原生平台进行交互。通过 Channel,您可以调用原生平台的 API 来实现对硬件功能的访问和控制。
-
多平台适配:未来,随着 Flutter 对其他平台的支持增加,例如鸿蒙 App,您掌握 Channel 的使用也会在适配其他平台时非常有帮助,让您更快速地实现对应平台的功能。
等等总之,掌握 Channel 的使用能够让开发者更灵活、高效地进行跨平台开发。除了处理特定功能和第三方库的问题,还有一些其他场景,例如处理设备传感器、操作文件系统等,也可以使用 Channel 来实现。因此,建议在需要跨平台功能和性能优化时深入学习和掌握 Channel 的使用。
那么本期内容就到这里,如讲的不到位或错漏的地方,希望同学们可以评论区指出。
由于代码比较简单,本文全部贴出,如果感觉本文对你有一点点点的启发,还望你能点赞
支持一下,你的支持是我最大的动力啦。
Ok,这一期就此完结。