4. MethodChannel详解(iOS端)
好,咱们接着聊 MethodChannel 在 iOS 端的实现。说实话,iOS 端的 MethodChannel 写起来比 Android 端要清爽不少——Swift 的语法糖让代码看起来更干净。但干净归干净,坑可一点不少。
我个人习惯把 MethodChannel 的 iOS 实现分成三个步骤:创建通道、处理调用、返回结果。咱们一个一个来。
4.1 创建 MethodChannel
在 iOS 端,MethodChannel 的创建通常放在 AppDelegate 里。嗯,这里要注意——通道的名字必须和 Flutter 端完全一致,大小写都不能错。我见过太多人因为名字拼写不一致,调试了半天才发现问题。
// AppDelegate.swift
import Flutter
import UIKit
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// 获取 FlutterViewController
let controller = window?.rootViewController as! FlutterViewController
// 创建 MethodChannel
let batteryChannel = FlutterMethodChannel(
name: "com.example.flutter/battery",
binaryMessenger: controller.binaryMessenger
)
// 设置方法调用处理器
batteryChannel.setMethodCallHandler { [weak self] (call: FlutterMethodCall, result: @escaping FlutterResult) in
// 处理 Flutter 端的方法调用
self?.handleBatteryMethod(call: call, result: result)
}
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
private func handleBatteryMethod(call: FlutterMethodCall, result: @escaping FlutterResult) {
// 具体处理逻辑
}
}
这里有个细节:[weak self] 是必须的。为什么?因为如果不加 weak,AppDelegate 和 MethodChannel 会形成循环引用,导致内存泄漏。我在项目中遇到过这个问题,当时排查了好久才发现是引用循环导致的。
4.2 处理 Flutter 端的方法调用
Flutter 端调用方法时,iOS 端会收到一个 FlutterMethodCall 对象。这个对象包含两个关键信息:method(方法名)和 arguments(参数)。
说白了,这就是一个简单的分发逻辑。根据方法名调用不同的原生功能。
private func handleBatteryMethod(call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
case "getBatteryLevel":
getBatteryLevel(result: result)
case "startBatteryMonitoring":
startBatteryMonitoring(arguments: call.arguments, result: result)
case "stopBatteryMonitoring":
stopBatteryMonitoring(result: result)
default:
result(FlutterMethodNotImplemented)
}
}
private func getBatteryLevel(result: FlutterResult) {
let device = UIDevice.current
device.isBatteryMonitoringEnabled = true
guard device.batteryState != .unknown else {
result(FlutterError(
code: "UNAVAILABLE",
message: "电池信息不可用",
details: nil
))
return
}
let batteryLevel = Int(device.batteryLevel * 100)
result(batteryLevel)
}
你想想看,这个 switch 结构是不是很清晰?每个 case 对应一个方法,default 返回 FlutterMethodNotImplemented。我曾经见过有人用 if-else 链来处理,代码又长又乱,后来重构时全改成 switch 了。
4.3 返回结果给 Flutter
返回结果的方式有三种:成功、失败、未实现。咱们分别看看。
| 返回类型 | 调用方式 | Flutter 端接收 |
|---|---|---|
| 成功 | result(value) |
.then((value) => ...) |
| 失败 | result(FlutterError(...)) |
.catchError((error) => ...) |
| 未实现 | result(FlutterMethodNotImplemented) |
抛出 MissingPluginException |
成功返回很简单,直接传值就行。但失败返回就有点讲究了——FlutterError 有三个参数:code、message、details。code 是错误码,message 是错误描述,details 可以带一些额外的调试信息。
// 错误返回示例
result(FlutterError(
code: "PERMISSION_DENIED",
message: "用户拒绝了电池信息访问权限",
details: ["bundleId": Bundle.main.bundleIdentifier ?? "unknown"]
))
我个人习惯把错误码定义成常量,比如 let kErrorPermissionDenied = "PERMISSION_DENIED"。这样 Flutter 端可以根据错误码做不同的处理,而不是去解析字符串。
4.4 错误处理与异常传递
错误处理这块,说白了就是两件事:捕获原生异常、正确传递给 Flutter。
iOS 端常见的异常场景包括:
- 权限问题:用户拒绝了某个权限
- 硬件不可用:比如设备没有指纹传感器
- 参数错误:Flutter 传过来的参数格式不对
- 系统限制:比如 iOS 14 以上的某些 API 变更
我曾经遇到过一个坑:在 iOS 13 上跑得好好的代码,到了 iOS 14 就崩溃了。原因是 iOS 14 废弃了某个 API,而我没有做版本判断。从那以后,我养成了一个习惯——所有原生调用都包一层 do-catch。
private func handleMethodCall(call: FlutterMethodCall, result: @escaping FlutterResult) {
do {
switch call.method {
case "getBatteryLevel":
try getBatteryLevel(result: result)
case "startBatteryMonitoring":
try startBatteryMonitoring(arguments: call.arguments, result: result)
default:
result(FlutterMethodNotImplemented)
}
} catch let error as NSError {
// 统一错误处理
result(FlutterError(
code: "NATIVE_ERROR",
message: error.localizedDescription,
details: ["domain": error.domain, "code": error.code]
))
}
}
result 回调只能调用一次!如果你不小心调用了两次,Flutter 端会收到一个 FlutterError,提示 "Multiple results returned"。我在项目中见过有人把 result 放在异步回调里,结果回调被触发了两次,导致 Flutter 端崩溃。
4.5 异步操作的处理
很多原生操作是异步的,比如网络请求、传感器数据读取。这时候要注意——result 回调必须在异步操作完成后调用,不能提前返回。
private func startBatteryMonitoring(arguments: Any?, result: @escaping FlutterResult) {
// 模拟异步操作
DispatchQueue.global().async {
// 模拟耗时操作
Thread.sleep(forTimeInterval: 2.0)
DispatchQueue.main.async {
// 回到主线程调用 result
result("Battery monitoring started")
}
}
}
这里有个细节:UI 相关的操作必须在主线程调用。虽然 result 回调本身可以在任何线程调用,但如果你在回调里更新 UI,就必须回到主线程。我个人习惯统一在主线程调用 result,省得后面出问题。
4.6 实战经验总结
最后,分享几个我在项目中积累的经验:
- 通道命名规范:用反向域名格式,比如
com.yourcompany.flutter/feature。这样不容易冲突。 - 参数类型检查:Flutter 传过来的参数可能是
nil,一定要做非空判断。 - 日志记录:在方法调用的入口和出口都加日志,方便调试。
- 版本兼容:用
if #available(iOS 14.0, *)做版本判断,避免在新系统上崩溃。
嗯,iOS 端的 MethodChannel 就这些内容。说白了,核心就是创建通道、处理调用、返回结果。只要记住 result 只能调用一次、错误要正确传递、异步操作要处理好,基本就不会出大问题。下一章咱们聊聊 Android 端的实现,你会发现思路是一样的,但细节上有些差异。