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, *) 做版本判断,避免在新系统上崩溃。
💡 小技巧:如果你有多个 MethodChannel,可以考虑创建一个 ChannelManager 来统一管理。这样代码更整洁,也方便做全局的日志和错误处理。

嗯,iOS 端的 MethodChannel 就这些内容。说白了,核心就是创建通道、处理调用、返回结果。只要记住 result 只能调用一次、错误要正确传递、异步操作要处理好,基本就不会出大问题。下一章咱们聊聊 Android 端的实现,你会发现思路是一样的,但细节上有些差异。