ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Flutter 插件鸿蒙化适配:image_picker_plus 实战解析

Flutter 插件鸿蒙化适配:image_picker_plus 实战解析 在 OpenHarmony 上跑 Flutter 应用第一个让我感到生态缺口的三方库就是图片视频选择器。官方 image_picker 被社区适配后基本处于“能用但很基础”的状态单选一张图没问题但项目一旦需要多选、混合选视频、自定义相机、压缩参数透传就只能自己动手。后来我把 image_picker_plus 这个社区增强版完整拉下来从通道协议到 ArkTS 原生实现全部过了一遍才算在鸿蒙上把这条链路彻底打通。这篇指南就是那次适配的完整复盘通道怎么拆、原生怎么接、真机上有哪些坑、最后怎么过 XTS尽量都写清楚。如果你正在做 Flutter 插件的鸿蒙化或者被三方库缺失问题卡住这篇文章应该能帮你省掉不少弯路。1. 这次适配的出发点为什么是 image_picker_plus 而不是 image_picker1.1 官方 image_picker 在鸿蒙上到底缺了什么OpenHarmony 社区里其实早就有人把官方 image_picker 移植过来了基础的pickImage、pickVideo能跑通这没得说。但如果你在真机上实际用一轮会发现限制很明显只能单选limit参数在部分适配实现里根本没有透传到 ArkTS 层。图片和视频的选择状态是割裂的做不到“一次选择图文混选”。返回的数据结构固定拿不到fileSize、mimeType、缩略图路径这些扩展字段。没有base64输出能力做签名上报或者直接传给后端时很尴尬。自定义相机、裁剪这些高级能力完全没戏只能靠自己在 Dart 层包一层原生通道。也就是说社区版解决的是“能不能用”的问题距离“好不好用”还差得很远。而 Flutter 应用里图片选择几乎是最贴近业务的核心路径用户每天都会碰到。与其在业务层疯狂打补丁不如直接在插件层把它补完整。1.2 image_picker_plus 带来的增强能力与适配收益选择 image_picker_plus 的原因很直接它把官方 image_picker 的参数模型扩展得非常完整Dart 侧 API 设计清晰原生层只需要按要求实现通道协议即可不需要我在 Dart 层做二次封装。它相对官方版本的核心增强点包括getMedia支持一次调用同时选择多张图片和多个视频multiple和maxCount参数齐全。支持MediaSource.camera与MediaSource.gallery分流可以直接指定从相机采集。支持imageQuality、maxWidth、maxHeight、compressFormat等压缩参数透传。支持includeBase64可以直接拿到 base64 编码内容。返回结构里带fileSize、mediaType、mimeType业务侧不用再自己探测。这些能力如果全靠自己从零写 ArkTS 实现工作量会非常大而且容易在边界条件上翻车。基于现成 Dart 层协议做鸿蒙原生适配是最划算的路径。1.3 动手前先算账适配成本到底看哪几项不要拿到源码就开始写代码。适配前我建议先花半天时间把下面这几项摸清楚否则很容易做一半发现某个能力根本没法在鸿蒙上实现又得推翻重来。第一项是通道方法数量。打开插件源码里 Dart 侧的MethodChannel定义把所有方法名列出来。image_picker_plus 这类增强库的方法不会太多但每个方法的参数都比较复杂重点看哪些参数会影响原生行为。第二项是原生依赖。有些插件依赖 Android 的ActivityResultContracts、iOS 的PHPickerViewController这些在鸿蒙上都必须用PhotoAccessHelper、CameraPicker或者startAbility重新实现。依赖越接近系统资源管理适配成本越高。第三项是 UI 部分。如果插件内部有自定义相机预览、裁剪手势这种原生 UI就会涉及 PlatformView没有的话适配成本会低很多。image_picker_plus 的裁剪主要放在 Dart 层原生侧只需要保证返回原图这点很关键。2. 源码层面拆解image_picker_plus 的通道设计与数据协议2.1 从 Dart 调用到原生监听的完整链路适配任何 Flutter 插件第一步都是把 Dart 侧的调用链路读懂。image_picker_plus 的入口是ImagePickerPlus.instance调用getMedia()之后内部会把参数打包成Map通过固定的MethodChannel发给原生端。这里有个关键点通道名必须与原生侧完全一致。我在首次适配时就是没注意版本差异Dart 侧用的通道名和老的增强版仓库不一致导致真机上一直报MissingPluginException。排查了半天才发现是 fork 版本升级时改了常量名。建议拿到源码后全局搜索MethodChannel把通道名、方法名、参数 key 全部记录下来后续 ArkTS 侧就是围绕这张表做实现。通道通信这块细节决定成败漏一个参数key业务侧就拿不到对应数据。2.2 核心通道方法方法名、参数与返回值的对照表这里列出我在适配过程中实际处理的通道方法。注意不同版本的方法名可能有小差异以你拉下来的源码为准但方法设计逻辑大同小异。方法名主要参数返回内容pickMediaallowedTypesimage/video/all、maxCount、multiple、includeBase64、imageQuality媒体路径列表、宽度高度、文件大小getMediaFromSourcesourcecamera/gallery、mediaType、maxCount单个或一组媒体路径getAllMediamediaType、页码相关参数媒体资源列表常用于自定义选择器getThumbnailpath、width、height缩略图路径或 base64cleanTempFiles无清理结果布尔值参数 key 也需要逐一确认比如allowedTypes在 Dart 侧是一个枚举列表到了原生侧会转换成字符串数组例如[image, video]。ArkTS 侧取值时要注意类型判断别直接用as String去转数组否则会在运行时直接抛类型错误。2.3 返回 JSON 的结构约定与路径转换逻辑image_picker_plus 的返回结果不是简单的路径字符串而是一个结构化的 Map。以pickMedia为例单条媒体记录大概长这样{ path: /data/storage/el2/base/.../PickedMedia/xxx.jpg, filePath: /data/storage/el2/base/.../PickedMedia/xxx.jpg, base64: 可选默认不返回, width: 1920, height: 1080, fileSize: 204800, mediaType: image, mimeType: image/jpeg }这里有一个非常容易踩的坑path字段在 Android 和 iOS 上通常已经是 App 沙箱内的可读路径但鸿蒙的媒体库资源拿到的是file://media/...或者datashare:///...这种 URIFlutter 侧直接读这个路径是读不到文件的。所以原生侧必须做一步“URI 转沙箱文件”的操作把用户选中的资源复制到 App 的 cache 目录或临时目录然后把复制后的真实文件路径返回给 Dart。路径转换逻辑直接决定了返回的数据能不能被Image.network、File正常消费这一步绝对不能省。3. 鸿蒙工程准备插件目录、注册机制与依赖接入3.1 给 Flutter 插件工程加上 ohos 平台的目录骨架鸿蒙 Flutter 插件的工程形态和 Android/iOS 类似只是在插件根目录下多了一个ohos目录。最方便的方式是直接用命令行创建插件模板flutter create --templateplugin --platformsohos image_picker_plus_ohos如果你的插件是直接从已有源码改的没有ohos目录可以手动建出下面的结构image_picker_plus/ ├── ohos/ │ ├── entry/ │ │ └── src/main/ │ │ ├── ets/ │ │ │ └── plugins/ │ │ │ └── ImagePickerPlusPlugin.ets │ │ ├── resources/ │ │ └── module.json5 │ └── build-profile.json5 ├── lib/ ├── pubspec.yaml手动创建时注意module.json5里的deviceTypes要包含phone和tablet否则在平板上跑不起来。这个细节很多博客不会提但我在适配验证阶段确实因为漏配导致平板设备直接安装失败。3.2 插件入口FlutterPlugin 与 MethodCallHandler 的 ArkTS 骨架鸿蒙 Flutter 插件的入口类需要同时实现FlutterPlugin和MethodCallHandler两个接口。onAttachToEngine是插件和引擎绑定的地方要在这里创建MethodChannel并注册消息处理器。基本骨架如下import { FlutterPlugin, FlutterPluginBinding } from ohos/flutter_ohos/plugin; import { MethodChannel, MethodCall, MethodResult } from ohos/flutter_ohos/plugin; import { common } from kit.AbilityKit; export class ImagePickerPlusPlugin implements FlutterPlugin, MethodCallHandler { private channel: MethodChannel | undefined; private hostContext: common.UIAbilityContext | undefined; onAttachToEngine(binding: FlutterPluginBinding): void { this.hostContext binding.getApplicationContext() as common.UIAbilityContext; this.channel new MethodChannel(binding.getBinaryMessenger(), plugins.flutter.io/image_picker_plus); this.channel.setMethodCallHandler(this); } onDetachFromEngine(binding: FlutterPluginBinding): void { if (this.channel) { this.channel.setMethodCallHandler(null); this.channel undefined; } } async onMethodCall(call: MethodCall, result: MethodResult): Promisevoid { try { switch (call.method) { case pickMedia: // 具体实现见下文 break; case getMediaFromSource: break; default: result.notImplemented(); } } catch (err) { result.error(PLUGIN_ERROR, ${err}, ); } } }onDetachFromEngine里释放 channel 的操作不能省。Flutter 引擎在热重载或页面销毁时会重新绑定插件如果这里不置空下次 attach 时可能出现 channel 被重复注册的报错。这个坑在开发期不明显一旦进入长时间运行的状态就很容易偶现崩溃。3.3 pubspec.yaml 的 ohos 平台声明与依赖接入工程目录准备好后还需要在pubspec.yaml中声明鸿蒙平台支持。flutter段大概长这样flutter: plugin: platforms: ohos: pluginClass: ImagePickerPlusPlugin dartPluginClass: ImagePickerPlus这里的pluginClass对应 ArkTS 入口类的名字dartPluginClass对应 Dart 侧暴露给业务用的类名。配置好后在 Flutter 工程的pubspec.yaml里用 git 依赖或本地路径依赖引入dependencies: image_picker_plus: path: ../image_picker_plus依赖敲定后先跑一次flutter pub get然后在 DevEco Studio 里打开ohos目录同步工程。同步过程中如果报 SDK 版本不匹配去build-profile.json5里把compileSdkVersion改成你本机 OpenHarmony SDK 对应的版本即可。4. 核心通道实现用 ArkTS 接住拍选图的每个参数4.1 权限申请不要等 Dart 层先动手鸿蒙的相册权限不是安装时自动授予的需要在运行时动态申请。image_picker_plus 的 Dart 层不会替你申请权限原生侧必须自己处理。需要声明的权限有两个关键项一是读取图片视频二是相机如果用getMediaFromSource走相机。打开ohos/entry/src/main/module.json5{ module: { requestPermissions: [ { name: ohos.permission.READ_IMAGEVIDEO, reason: $string:read_image_video_reason, usedScene: { abilities: [EntryAbility] } }, { name: ohos.permission.CAMERA, reason: $string:camera_reason, usedScene: { abilities: [EntryAbility] } } ] } }权限申请代码用abilityAccessCtrlimport { abilityAccessCtrl, Permissions } from kit.AbilityKit; async function ensurePermission(context: common.UIAbilityContext): Promiseboolean { const atManager abilityAccessCtrl.createAtManager(); const permissions: ArrayPermissions [ohos.permission.READ_IMAGEVIDEO]; const res await atManager.requestPermissionsFromUser(context, permissions); for (const grant of res.authResults) { if (grant ! abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) { return false; } } return true; }这里最容易翻车的点是不要在 Dart 侧用permission_handler之类的库先申请一次再走原生通道。两道权限请求会连续弹窗用户很容易直接拒绝第二个然后原生层拿到Failed to get permission就只能报错。权限申请只放在原生层逻辑最清晰。4.2 用 PhotoAccessHelper 查询媒体库并构造资源列表鸿蒙侧访问相册的核心 API 是photoAccessHelper。第一次从 iOS/Android 转过来的时候会觉得 API 风格差挺多但只要理解它的模型就顺手了先拿PhotoAccessHelper实例再构造FetchOptions查询条件最后用fetchResult.getAllObjects()拿到资源数组。import { photoAccessHelper } from kit.MediaLibraryKit; import { dataSharePredicates } from kit.ArkData; async function queryMedia(context: common.UIAbilityContext, mediaTypes: Arraystring, maxCount: number): PromiseArrayphotoAccessHelper.PhotoAsset { const helper photoAccessHelper.getPhotoAccessHelper(context); const predicates new dataSharePredicates.DataSharePredicates(); const fetchOptions new photoAccessHelper.FetchOptions(); fetchOptions.fetchColumns [uri, media_type, width, height, size]; fetchOptions.predicate predicates; fetchOptions.fetchCount maxCount; const fetchResult await helper.getAssets(fetchOptions); return await fetchResult.getAllObjects(); }实际业务中一般不会把全部相册数据一次性拉出来image_picker_plus的maxCount参数可以直接映射到fetchCount。如果maxCount不传或传 0可以给一个默认上限比如 500避免在相册图片上万张时直接把内存打爆。需要注意的是PhotoAsset对象本身不是文件路径它只是一个资源句柄。要真正读取文件内容需要基于它的uri做后续操作。这个模型类似于 Android 的ContentResolver理解这一点后面路径转换就不会迷路。4.3 参数透传allowedTypes、maxCount、单选/多选的映射逻辑Dart 侧打包出来的参数到 ArkTS 侧后取值的类型要谨慎处理。以pickMedia为例allowedTypes是一个字符串数组maxCount可能是 intmultiple是 bool。ArkTS 侧的参数类型约束比 Dart 严格建议在入口处先做一次安全解析private parseCommonOptions(call: MethodCall): PickerOptions { const options new PickerOptions(); const args call.arguments as Mapstring, Object; const allowedTypes args.get(allowedTypes) as Arraystring | undefined; if (allowedTypes ! undefined) { options.allowedTypes allowedTypes; } const maxCount args.get(maxCount) as number | undefined; if (maxCount ! undefined) { options.maxCount maxCount; } const multiple args.get(multiple) as boolean | undefined; if (multiple ! undefined) { options.multiple multiple; } return options; }multiple这个参数很关键它决定了选择界面是单选还是多选。鸿蒙的系统相册选择器PhotoViewPicker本身就支持selectionMode单选用SelectionMode.SINGLE_SELECT多选用SelectionMode.MULTIPLE_SELECT直接把 Dart 层的参数映射过去就行。但有个细节maxCount只有在多选模式下才生效单选模式下传入maxCount大于 1 会被系统忽略。适配时最好在 Dart 侧就做好参数自检避免用户在单选模式下以为能多选结果只返回一张图产生困惑。4.4 从 URI 到 Flutter 可读路径沙箱复制与临时文件清理这是整个适配流程中最麻烦但最核心的一步。鸿蒙相册返回的是file://media/Photo/...或datashare:///...Flutter 的File无法直接消费这种 URI。我的做法是统一复制到 App 的缓存目录再把复制后的路径返回给 Dart。import { fileIo as fs } from kit.CoreFileKit; import { util } from kit.ArkTS; async function copyAssetToCache(context: common.UIAbilityContext, uri: string, extension: string): Promisestring { const cacheRoot ${context.cacheDir}/image_picker_plus/; if (!fs.accessSync(cacheRoot)) { fs.mkdirSync(cacheRoot); } const fileName ${Date.now()}_${util.generateRandomUUID()}.${extension}; const destPath ${cacheRoot}${fileName}; const srcFile fs.openSync(uri, fs.OpenMode.READ_ONLY); const destFile fs.openSync(destPath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE); fs.copyFileSync(srcFile.fd, destFile.fd); fs.closeSync(srcFile); fs.closeSync(destFile); return destPath; }复制这一步有性能损耗尤其是大体积视频。适配过程中建议同时把文件复制耗时作为一项指标记录。如果视频超过 500MB单线程复制可能要好几秒这时最好配合 EventChannel 把进度上报到 Dart 层否则用户点完选择后会面对一段没有反馈的空白时间很容易误以为应用卡死。临时文件清理也有讲究。image_picker_plus的cleanTempFiles方法就对应这个逻辑。我建议不要在每次选择完成后立刻全量清理因为业务层可能还要用这个路径做上传。更稳妥的做法是只清理创建时间超过 24 小时的临时文件这样既不会堆积也不会误删正在使用的文件。4.5 EventChannel 上报选择进度与取消事件如果你只做单选EventChannel 可以不上但只要涉及多选大盘或视频这一步我觉得非常有必要。Flutter 侧的EventChannel和MethodChannel是两套通道ImagePickerPlus 的原生实现里通常会同时建立这两个通道。ArkTS 侧创建方式类似import { EventChannel, EventSink } from ohos/flutter_ohos/plugin; export class ImagePickerPlusPlugin implements FlutterPlugin, MethodCallHandler { private eventChannel: EventChannel | undefined; private eventSink: EventSink | undefined; onAttachToEngine(binding: FlutterPluginBinding): void { this.eventChannel new EventChannel(binding.getBinaryMessenger(), plugins.flutter.io/image_picker_plus/events); this.eventChannel.setStreamHandler({ onListen: (sink: EventSink) { this.eventSink sink; }, onCancel: () { this.eventSink undefined; } }); } }当复制大文件、或者系统相册回调返回资源列表时可以通过eventSink.success(...)把进度推到 Dart 层。需要注意eventSink的判空onCancel之后继续success会抛异常。这种异步边缘情况在 ArkTS 里尤其常见因为 Dart 侧页面 pop 后EventChannel 会被取消但原生异步任务还在继续。5. 相机与裁剪PlatformView 方案与能力降级策略5.1 拉起系统相机拍照最简单的路径getMediaFromSource里sourcecamera的场景最简单的实现不是自己写相机预览而是直接拉起系统相机应用。鸿蒙上可以通过startAbility调起系统相机拍完照片后返回图片 URI。实现路径大致是检查ohos.permission.CAMERA权限。构造Want选择系统相机 Ability。在 UIAbility 的onCreate或回调里接收返回的图片 URI。将 URI 转成沙箱路径走 4.4 节的复制流程。这个方案优点是稳定、适配成本低缺点是无法定制相机 UI也没有办法内嵌到 Flutter 页面流里。如果你的业务只需要“拍一张照传上去”这个方案足够。5.2 裁剪页面要不要上 PlatformViewimage_picker_plus 在 Dart 层自带了一些裁剪交互能力不一定需要原生裁剪 UI。适配时我的建议是除非产品明确要求“原生相机预览 原生裁剪手势”否则不要在鸿蒙侧做 PlatformView。原因很简单FlutterOHOS 的 PlatformView 基于 XComponent 实现涉及原生视图和 Flutter 纹理的叠加复杂度和调试成本都比 Android 高不少。而且 image_picker_plus 的裁剪逻辑本身在 Dart 侧就是可用的强行搬到原生侧不仅重复造轮子还会引入大量不必要的坐标转换问题。如果你确实需要内嵌相机的自定义页面再考虑 XComponent PlatformView。但一定把需求边界确认清楚是要满屏相机还是只是选择器里的一个预览区域这两种模式的实现复杂度不是一个数量级。5.3 能力降级与错误码约定避免静默失败适配过程中不可能所有能力都一比一还原。对于实现不了的边缘能力一定不要静默吞掉要返回明确的错误码让业务层可以感知并降级。我在适配中约定了一套错误码体系错误码含义业务层建议PERMISSION_DENIED用户拒绝权限引导去设置页SOURCE_UNAVAILABLE本地无相机或无相册资源隐藏相关入口COPY_FAILED文件复制失败磁盘可能已满提示重试CANCELED用户取消选择正常流程不用报错NOT_SUPPORTED当前系统版本不支持某能力降级为普通单选错误码通过result.error(code, message, details)返回给 Dart 层Dart 层在PlatformException.code里能直接拿到。这样做的好处是业务侧可以统一处理不用像我早期那样到处用try-catch碰运气。6. 权限与生命周期真机调试最容易翻车的两个点6.1 权限声明、动态申请与用户拒绝的三重坑第一重坑是module.json5里漏配权限真机一运行到相册调用直接崩报错信息还不直观。排查方式是在 DevEco Studio 的日志里搜Permission或SecurityException基本都是声明缺失。第二重坑是动态申请被拒。用户第一次弹窗点了拒绝后第二次再进相册系统不会再弹窗。这时候需要引导用户去系统设置里手动授权。适配时要在PERMISSION_DENIED的分支里返回错误码业务层弹一个引导提示而不是反复触发系统权限弹窗。第三重坑是权限回调的异步时序。requestPermissionsFromUser是异步的如果用户在权限弹窗出现前就快速操作 UI回调可能在你处理到一半时才触发。处理方案是在权限申请完成前把选择器的入口按钮置灰避免竞态。6.2 UIAbility 生命周期对 Result 回调的影响这是我在真机调试里踩得最痛的一次。Flutter 页面通过getMedia拉起系统相册后当前 UIAbility 会进入后台状态。用户选完图返回时UIAbility 重新回到前台。如果这时context已经被释放或者切换了之前持有的hostContext就再也拿不到正确的资源目录。处理方式需要区分binding.getApplicationContext()拿到的是应用级 Context通常生命周期和应用一致相对安全但如果你在onMethodCall里临时取 page 级 Context页面销毁后就可能失效。适配时统一用应用级 Context避免持有 Activity/Page 级别的强引用。6.3 低内存回收导致 FlutterResult 丢失Flutter 插件的MethodResult如果在一个异步操作完成后才调用而这时 Flutter 侧页面已经被回收调用success()有一定概率直接抛异常。这个问题在 Android 上不太明显但鸿蒙上如果选择大视频并触发系统内存回收现象会明显很多。我的处理思路是在异步任务开始前对context判空并在onDetachFromEngine里把channel置空的同时把正在进行的异步任务标记为失效避免任务回调里再次触碰已释放的对象。ArkTS 侧没有 Java 那么强的强引用机制但也不要把MethodResult保存在一个会被低频内存回收扫掉的弱引用容器里该强引用就强引用任务终结时再手动释放。7. 验证与质量保障从单测到真机矩阵到 XTS 兼容性7.1 真机验证矩阵不同 API 版本与设备形态插件写完之后重要的是不要只在模拟器上跑通就以为完事。我在适配过程中搭建了一个小规模的验证矩阵主要覆盖三类设备手机设备OpenHarmony API 12 和 API 13 各一台覆盖权限弹窗、相册 UI 差异。平板设备重点测试多选模式下资源和缩略图的加载。折叠屏/大屏设备验证系统相册选择器在不同尺寸下的返回数据一致性。验证清单里至少包含这些用例单图选择、多图选择、图片视频混选、从相机拍摄、取消选择、拒绝权限后再次进入、选择超大视频后返回路径等。每一条都要记录返回的数据结构是否符合预期尤其是fileSize和mimeType。7.2 性能观察多选大图与 4K 视频的专项测试性能测试不用做得很重但要有针对性。我重点看了两个指标第一个是相册列表加载耗时。首次进入系统选择器如果相册里有上万张图片getAssets可能需要一两秒。这时可以通过限制fetchCount或增加分页来优化但这属于系统选择器的内部逻辑插件层能控制的空间不大。让我意外的是鸿蒙系统选择器本身性能不错大列表滑动没有明显掉帧。第二个是文件复制耗时。多选 5 张 4K 图片时总耗时大概在 2 到 3 秒选一个 1GB 的 4K 视频耗时接近 10 秒。这个数字必须通过 EventChannel 反馈给业务层否则用户会以为应用卡死。同时在复制大文件时建议给 Dart 层返回一个中间状态让 UI 显示 loading 动画。7.3 XTS 兼容性与稳定性检查的注意事项如果插件要对外发布或者用于商用项目XTS 兼容性认证基本是绕不开的环节。XTS 那套测试对插件的稳定性要求比较高尤其关注权限使用是否规范、沙箱访问是否越界、以及异常路径是否有未捕获的崩溃。根据我的经验XTS 测试最容易翻车的地方集中在两处一是权限声明与实际调用不一致。比如只在代码里申请了READ_IMAGEVIDEO但module.json5里没有声明或者声明了但usedScene没填对应的 Ability。XTS 对权限声明的一致性检查很严格建议对照应用实际行为逐一核对。二是临时文件没有按沙箱规范处理。Flutter 插件复制文件时如果写到了非法的公共目录XTS 会直接判定不合规。统一使用cacheDir或filesDir下的子目录不要拼接绝对路径基本就能过关。适配完成后建议跑一遍完整的 XTS 稳定性测试用例。如果报超时或内存泄漏优先检查onDetachFromEngine是否正确释放了 channel 和 eventSink这两处是插件级内存泄漏的高发地。整个适配过程下来我最大的体会是Flutter 插件做鸿蒙适配本质上不是翻译代码而是把 Dart 侧的数据协议和管理模型理解透了之后再用 ArkTS 重新实现一遍。通道协议没吃透就开始写原生代码大概率会在真机调试阶段被各种边界 case 反复折磨但把这层理清之后后面每一步都踏实很多。如果你也在做类似的三方库鸿蒙化建议先从通道方法清单和返回结构入手这两样东西就是整个适配过程的地图。
返回列表