尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

Flutter鸿蒙化适配:设备预检库 will_it_run 升级实践

发布时间:2026/9/28 22:36:42

资讯中心
01
ARTICLE

Flutter鸿蒙化适配:设备预检库 will_it_run 升级实践

Flutter鸿蒙化适配:设备预检库 will_it_run 升级实践
做 Flutter 应用分发的人应该都经历过这种场景线上反馈群突然冒出一条“一打开就闪退”你手上全是旗舰测试机折腾半天复现不出来。最后借来一台三年前的老安卓机装上包一启动秒崩——原因往往很简单设备太老、内存不够、CPU 指令集不支持应用在最底层的那几行初始化就挂了。如果应用能在启动的瞬间像急诊科的分诊护士一样先给设备做一次快速体检再决定让用户进入完整功能还是降级模式很多“不明原因崩溃”根本轮不到发生。这也是我当初关注 will_it_run 这个 Flutter 三方库的原因。will_it_run 要解决的就是这个“先见之明”的问题在应用启动时收集设备能力对比应用的最低运行要求给出“能跑、勉强跑、不能跑”的结论。最近我在做鸿蒙化适配目标是把它在 HarmonyOS NEXT / OpenHarmony 环境上跑起来并升级成一个运行环境预检中台。这篇文章就是把整个适配过程、API 差异和踩坑记录完整拆开讲一遍适合正在做 Flutter 鸿蒙化改造的同学也适合 Android 团队初次接触鸿蒙设备能力模型的场景。1. will_it_run 干了什么活App 启动前的“环境安检”逻辑1.1 一个库的自我定位从“能装”到“能跑”的差距应用市场把包装到用户手机里和这个包在用户手机上真正能运行起来是两码事。APK 装得上去只说明签名、系统版本、指令集这几个最基础的条件满足但应用跑起来之后需要多少内存、需要哪些传感器、需要多大的存储余量、需要什么样的渲染能力这些都是安装包层面的检查覆盖不到的。will_it_run 的做法是把这些运行时需求做成一条条“检测规则”在 main() 里第一步就执行而不是等用户把页面都打开好几个了才说“哎呀你这设备不行”。这个库在我项目里的调用形态大致是这样的final verdict await WillItRun.check( minMemoryMB: 2048, minOsVersion: NEXT, minScreenWidth: 360, requiredSensors: [gyroscope], requiredCapabilities: [gles3.0], ); if (!verdict.shouldRun) { runApp(UnsupportedDevicePage(verdict.reason)); return; } runApp(const MainApp());这段代码看起来简单背后其实是一整套设备巡检机制。真正有价值的地方在于它会给出“为什么不能跑”的具体原因是内存不够还是传感器缺失还是 GPU 能力不达标。这样你就可以在降级页面上告诉用户他这台设备缺了什么而不是甩一句“您的设备不支持”。1.2 预检清单都包含哪些项从我的实践来看一个合格的运行环境预检清单至少要覆盖下面几类will_it_run 的检测项也基本围绕这些维度展开检测维度具体指标在预检中的作用拿不到数据时的兜底策略CPU 能力核心数、频率、指令集判断是否满足计算密集型模块的底线按最低档位处理宁可保守内存总内存、可用内存判断能否完成 AOT 初始化与页面常驻直接判定为不可运行并按需降级存储首存储卷剩余空间判断是否有空间写日志、缓存、数据库视为空间不足提示清理系统版本主版本号、SDK API 版本判断跨版本 API 兼容性按最低支持版本处理显示能力分辨率、逻辑宽度、折屏状态判断布局模式和渲染负载按最小分辨率缩放布局传感器陀螺仪、加速度计、NFC 等判断核心功能是否可用关闭对应功能入口网络当前网络类型、连通状态判断首屏数据拉取方式进入离线模式权限定位、相机、麦克风等授权状态判断关键功能是否可以启动主动进入授权引导流程这些项看起来都不复杂但越是简单的东西在鸿蒙上适配的时候越容易踩坑。因为“拿数据”这件事在 Android 和鸿蒙上完全不是一套 API尤其鸿蒙自己的权限模型跟 Android 动态权限差异非常大后面我会专门讲。1.3 为什么“预检”必须放在业务逻辑前面有朋友问过我我也可以在每个用到传感器、用到 GPU 的页面上做一次能力检查效果不是一样吗不一样。分页检查的问题是“炸点后置”——用户已经点进来了甚至已经输入了一堆东西才遇到不可用的功能体验上就是“死胡同”。预检前置的真正价值是给用户一个体面的入口。比如一个 AR 测量功能强依赖陀螺仪和相机权限如果能在启动时就把“这台设备不支持 AR”告诉用户那他就不会点进 AR 页面之后看到一片黑屏或旋转菊花。你的应用也会因为少了很多“闪退”“卡死”的反馈在应用市场的评分上好看得多。所以 will_it_run 这种库的设计起点不是技术而是“在用户还没开始失望之前就把失望的可能性排除掉”。理解了这个定位后面做鸿蒙化适配的时候你就知道哪些地方必须认真处理哪些地方可以稍微放一放。2. 鸿蒙与 Android 的设备能力 API差异远比想象的大2.1 系统信息获取从 Build 全家桶到 ohos.deviceInfo在 Android 上拿设备信息基本是 Build 类全家桶加一行代码的事Build.VERSION.SDK_INT、Build.MODEL、Build.HARDWARE再加上ActivityManager.getMemoryInfo()拿内存PackageManager拿特征。这些 API 稳定到你已经不需要思考它们是否存在只要闭着眼调用就行。鸿蒙就不一样了。设备信息要走ohos.deviceInfo而且它不是像 Build 那样一个静态对象把所有字段都摆在明面上而是通过系统能力接口获取import deviceInfo from ohos.deviceInfo; let deviceType deviceInfo.deviceType; let osVersion deviceInfo.osVersion; let sdkApiVersion deviceInfo.sdkApiVersion; let hardwareModel deviceInfo.hardwareModel;表面上是换了一个模块名的问题实际上对应关系要一张映射表才说得清。拿内存来说Android 的ActivityManager.getMemoryInfo().availMem对应到鸿蒙是ohos.system.getFreeMem()而且这个 freeMem 的语义跟 Android 的 availMem 并不完全一致。Android 的 availMem 是“进程可用的内存”鸿蒙的 freeMem 在某些版本上把系统缓存也排除了数值往往比你在系统设置里看到的“可用内存”更小。Android 获取方式鸿蒙获取方式需要注意的差异Build.VERSION.RELEASEdeviceInfo.osVersion鸿蒙版本号和 OpenHarmony API 版本是两个概念Build.MODELdeviceInfo.hardwareModelAndroid 上可能是厂商自定义字符串鸿蒙上更规范些Build.SUPPORTED_ABISprocess.getCpuAbi()鸿蒙需要额外判断 cpuAbi 是否包含 arm64-v8aActivityManager.MemoryInfosystem.getTotalMem() / getFreeMem()freeMem 的统计口径在不同 API 版本有差异这个差异带来的适配工作量不大但很容易让人掉以轻心。我第一次跑通鸿蒙分支后发现设备信息里 osVersion 居然是空的排查了半天才发现鸿蒙的osVersion在 API 12 之前需要用deviceInfo.sdkApiVersion替代否则拿到的是 null。这一类“字段存在但不返回数据”的问题在后面踩坑部分我会完整还原。2.2 运行时权限从动态申请到 AccessToken 模型Android 的动态权限是向前台发一个系统弹窗用户点了再回调onRequestPermissionsResult整个流程是“先弹窗、后回调、再继续”。will_it_run 在 Android 上检查权限时只要提前申请过权限就可以直接查询checkSelfPermission的返回值逻辑很顺。鸿蒙的权限模型我头一次接触的时候是有点懵的。它把权限分成system_grant系统预授权和user_grant用户授权两类而且不是所有权限都能让你在运行时弹窗请求。比如读取设备信息的权限在鸿蒙上属于 system_grant只要应用安装时配置好了就不需要运行时申请但相机、麦克风、精确定位这类权限属于 user_grant必须走正式的授权流程而且授权状态要用 AccessToken 来查。如果要检查一个权限是否已授权鸿蒙侧需要借助ohos.security.accessTokenimport abilityAccessCtrl from ohos.security.accessToken; let atManager abilityAccessCtrl.createAtManager(); let status await atManager.verifyAccessTokenSync( tokenID, ohos.permission.CAMERA ); // 0 表示已授权-1 表示未授权这个流程意味着预检逻辑里不能简单地把“权限未授予”直接判成“设备不支持”。你需要把权限检查拆成两步先判断这个权限是 system_grant 还是 user_grantsystem_grant 没授权就是配置问题user_grant 没授权就进入引导申请流程。如果 preflight 把“相机未授权”当成“不支持拍照”那很多鸿蒙用户会被错误挡在门外。2.3 存储、内存、网络的鸿蒙实现存储和网络这两个维度Android 上各有成熟的获取方式鸿蒙上也都提供了对应接口但坑点在于“路径”和“口径”。存储方面Android 的getDataDir和Environment.getExternalStorageDirectory可以直接给出应用目录和外部存储路径然后StatFs算可用空间。鸿蒙上的空间统计依赖ohos.storage.statistics而且要区分“用户卷”和“系统卷”。实际上预检关心的用户可用空间指的是设备用户目录下所有文件的剩余空间而不是某个 mount point 的剩余空间。我一开始想偷懒直接拿到 Volume 的总大小减小小头的 freeSize结果发现不同的文件系统挂载点统计口径不一致最后老老实实调用了statistics.getFreeSizeOfVolume接口按用户卷的路径去取。网络方面Android 的ConnectivityManager.getActiveNetwork加上getNetworkCapabilities能判断网络类型和验证状态。鸿蒙对应的是ohos.net.connectionimport connection from ohos.net.connection; let netHandle await connection.getDefaultNet(); let netCap await connection.getNetCapabilities(netHandle); if (netCap.bearerTypes[0] 0) { // BEARER_CELLULAR 蜂窝网络 } else if (netCap.bearerTypes[0] 1) { // BEARER_WIFI }这里容易踩的坑是鸿蒙的网络状态获取是异步的而且getDefaultNet在没有任何网络连接时会抛异常必须用 try-catch 包住否则预检流程会直接崩掉那就本末倒置了。Android 上getActiveNetwork在无网络时会返回 null你还需要额外判空两种平台的异常形态完全不同适配时一定要写两层兜底。3. 鸿蒙化适配实操从 fork 源码到跑通预检流程3.1 准备一套鸿蒙可运行的 Flutter 生态分支在动 will_it_run 源码之前得先让整个 Flutter 工程能在鸿蒙设备上跑起来。目前主流的做法是使用 OpenHarmony 社区的 Flutter 分支比如 flutter_flutter 的 harmony 分支配合 DevEco Studio 作为集成构建环境。整体步骤是这样下载 flutter_flutter 的 harmony 分支配置到OHOS_SDK环境变量注意这个分支的版本号比普通 Flutter SDK 要“特殊”一些用flutter --version能看到分支标识。用 DevEco Studio 打开 Flutter 工程DevEco 会识别 pubspec 和 flutter_module生成鸿蒙侧的 entry 工程壳。在 pubspec.yaml 里把项目依赖切换到适配鸿蒙的版本。很多插件在 pub 上的官方版本不支持鸿蒙要么用 fork 版本要么从git源拉取。运行flutter build hap --release或者直接在 DevEco 里构建产物是.hap包可以装到 HarmonyOS NEXT 真机上。这里有个容易忽略的点鸿蒙分支的 Flutter SDK 版本可能不支持最新的 Dart SDK 特性如果你的代码里用了较新的语法编译会报“version not supported”。所以建议先锁定一个稳定的 Flutter 鸿蒙分支版本再对照项目里需要适配的三方库逐个解决依赖冲突。3.2 用条件编译把平台差异隔离在能力层will_it_run 的核心逻辑里有很多类似Platform.isAndroid的调用适配鸿蒙时你如果把每个判断点散落到业务代码里后面会非常被动。更好的做法是在库内部建一个能力抽象层让所有平台判断都收敛到一个文件里。我用的方式是在编译期用一个自定义环境变量来区分目标平台const bool isOhos bool.fromEnvironment(OHOS); String get platformName { if (isOhos) return ohos; if (Platform.isAndroid) return android; if (Platform.isIOS) return ios; return unknown; }这么写的好处是鸿蒙分支下面dart:io的Platform类面对的是 Flutter 鸿蒙引擎模拟出来的环境有些字段返回的值不完全可靠。用编译期常量bool.fromEnvironment(OHOS)不仅能明确告诉编译器当前是鸿蒙环境还能让 tree-shaking 更彻底把无关平台的代码从产物里剔除。在具体检测项的实现上我会把“平台入口”和“具体检测逻辑”分开。例如“获取可用内存”这个方法在 Android 分支走device_info_plus或者 MethodChannel在鸿蒙分支走鸿蒙原生的ohos.system。这样 will_it_run 的判断逻辑不用改变的只是能力实现层。3.3 核心检测项的鸿蒙原生实现先说我用的插件方案。will_it_run 底层的设备信息收集在 Android 上当时选的是device_info_plus这个社区标准插件。适配鸿蒙时device_info_plus有几个鸿蒙 fork 版本可用但能力并不完整deviceType、hardwareModel这些字段往往映射不全。更稳妥的办法是自己在 will_it_run 的鸿蒙原生侧写一个轻量的插件只在鸿蒙上注册。鸿蒙 Flutter 插件的 native 侧本质上就是一个 OpenHarmony ArkTS 模块需要在Index.ets里注册 MethodChannelimport { FlutterPlugin } from ohos/flutter_plugin_bindings; export class WillItRunPlugin implements FlutterPlugin { private methodChannel: MethodChannel | null null; onAttachToEngine(flutterEngine: FlutterEngine) { this.methodChannel new MethodChannel( flutterEngine, will_it_run/device_profile ); this.methodChannel.setMethodCallHandler((call, result) { if (call.method getDeviceProfile) { // 收集鸿蒙设备画像 const profile { osVersion: deviceInfo.osVersion, sdkApiVersion: deviceInfo.sdkApiVersion, hardwareModel: deviceInfo.hardwareModel, totalMem: system.getTotalMem(), freeMem: system.getFreeMem(), }; result.success(profile); } else { result.notImplemented(); } }); } onDetachFromEngine(flutterEngine: FlutterEngine) { this.methodChannel?.setMethodCallHandler(null); this.methodChannel null; } }Dart 侧对应地做一个通道封装代码可以放到path_to_will_it_run/lib/ohos_impl.dart里Android 和 iOS 走原实现鸿蒙走这个新实现。3.4 统一输出设备画像预检的最终产出是一份“设备画像”就是一份 JSON 数据。把检测项整理成统一 schema 很重要因为中台化之后规则引擎、云端下发都要依赖这份数据。我在 will_it_run 里定义的 schema 大致是这样的{ os: { family: ohos, version: NEXT-4.0.0.116, apiLevel: 12 }, hardware: { cpuAbi: [arm64-v8a], cpuCount: 8, deviceModel: HUAWEI P60, deviceType: phone, totalMemoryMB: 8192, freeMemoryMB: 2048, isFoldable: false }, storage: { totalGB: 256, freeGB: 88 }, display: { widthPx: 1220, heightPx: 2732, logicalWidthDp: 430, logicalHeightDp: 960 }, network: { type: wifi, connected: true }, permissions: { camera: false, location: true, microphone: false } }这个 schema 有两个关键点。第一个是os.family它一定要区分ohos和android因为同一个应用如果做了鸿蒙化适配可能同时存在 Android 包和鸿蒙包但设备画像的判定策略完全不同。第二个是permissions字段配合前面说的 system_grant/user_grant中台才能判断是“该引导授权”还是“真的不支持”。4. 适配过程中踩到的坑与完整排查链路4.1 Bug 1device_info_plus 在鸿蒙上拿不到系统版本现象will_it_run 跑起来之后预检输出里 osVersion 一直是 null但 Android 上一切正常。排查链路我先在 Dart 侧打日志发现Platform.version返回的值跟普通 Android 设备不一样是 Flutter 鸿蒙引擎模拟出来的字符串并不能直接拿来当系统版本判断。再检查 device_info_plus 的 fork 版本是否注册了鸿蒙实现。鸿蒙插件的注册机制跟 Android 插件不同需要在鸿蒙工程的 EntryAbility 里显式注册到 FlutterEngine 上否则 MethodChannel 调过去没有 native 端响应会静默失败。我打开 DevEco 的日志一看果然PlatformException都没有抛只是返回 null。进一步看代码发现 fork 版 device_info_plus 在鸿蒙上只实现了AndroidDeviceInfo的少数几个字段version.release没有映射到deviceInfo.osVersion而是试图从deviceInfo.displayVersion拿那个字段在部分真机上是空字符串。修复我在 will_it_run 的鸿蒙实现里不再依赖 fork 版插件改为自己调ohos.deviceInfo并且在 Dart 层做了字段级兜底osVersion ?? sdkApiVersion.toString()。后面再遇到平台字段缺失优先打印原始返回值别急着在 Dart 侧打补丁。4.2 Bug 2折叠屏可用内存误判导致高端旗舰被当成低端机现象真机是刚发布的折叠屏旗舰配置完全不低但 will_it_run 判定为“低配设备”把功能降级了。排查链路我先对比同一台设备在 Android 和鸿蒙两种引擎下拿到的内存数据。Android 侧免费内存约 5.2GB鸿蒙侧只有 1.8GB差距大到不正常。查鸿蒙的ohos.system.getFreeMem()文档发现它返回的是系统当前空闲内存不代表应用可用。鸿蒙对系统级进程的常驻内存和缓存占用算得很狠所以数值比 Android 的availMem小很多。另外发现折叠屏在展开状态下系统会预留一部分内存给外屏渲染和大屏分屏任务进一步降低了“空闲内存”。修复预检逻辑里不再拿“空闲内存”作为唯一判断标准而是用“总内存 是否折叠屏 应用常驻内存预算”综合判断。对鸿蒙设备只要总内存大于 8GB 且设备类型是 tablet/折叠屏就自动放宽内存阈值。绝不在中台层用一套内存阈值同时套 Android 和鸿蒙。提示设备能力判定里最忌讳的是一刀切阈值。折叠屏、平板、投屏设备在同一套规则下都会出现“低端机误判”这类判定应该放到规则引擎里做多级降级而不是在采集层做死判断。4.3 Bug 3预检过程中的权限弹窗把启动流程卡死现象首次启动应用预检还没跑完页面就卡在启动页转圈用户点任何地方都没反应必须杀掉进程重进。排查链路复现后发现will_it_run 在检查定位权限时发现未授权主动调起了授权弹窗。用户点“暂不”原生侧返回的结果没有走正常的 success 回调而是抛了一个 cancel 异常。这个异常在我的 Dart 侧代码里没有被 catch导致整个预检 Future 一直挂在 await 上startup 流程卡死。更深层的原因是鸿蒙的 user_grant 权限弹窗只能同时弹一个而且不能设置“在本次启动内多次请求”。我把定位、相机、麦克风三个权限放在预检阶段一次性检查鸿蒙系统只允许我弹第一个后面两个直接静默拒绝。修复把权限请求改成“单权限请求 超时兜底”每个权限 5 秒超时异常全部 catch 住。同时把 will_it_run 的预检状态机改成“即使权限检查失败也继续返回当前收集到的数据”不让权限问题阻塞整个启动流程。这个 bug 给我的教训是预检组件自己的稳定性比预检内容的完整性更重要。如果预检流程本身会崩、会挂那这个库就是在给主应用添乱还不如不接。5. 从“适配库”到“预检中台”架构升级与后续扩展5.1 中台化的抽象层次will_it_run 单库形态是“一个 SDK 嵌进 App”适合个人项目或单一应用。但如果公司里有多个 Flutter 应用都做鸿蒙化每个应用各自维护一份设备模型和规则逻辑就会重复造轮子。这时候我的做法是把 will_it_run 改造成三层结构第一层是采集层负责跟系统 API 打交道产出标准设备画像 JSON这部分就是 will_it_run 鸿蒙化适配的核心。第二层是判定层把“检查项规则”从代码里抽出来做成可配置的规则文件。比如“最低 3GB 内存”不再写死在代码里而是作为规则下发。第三层是策略层负责处理“判定结果之后的动作”比如降级渲染、关闭动画、切换到低配模式、展示引导页。这层甚至不一定要在客户端执行可以上报给服务端让运营后台决定策略。这样抽象完后will_it_run 在单个应用里的角色变小了但它变成了中台的采集 SDK服务于整个公司多个应用的运行环境预检。5.2 用 EventChannel 把预检结果导出给业务层单库模式下will_it_run 的判定结果会直接返回给 main() 调用处。中台化之后业务层往往需要异步感知“设备能力变化”比如用户在外接显示器插拔之后屏幕逻辑分辨率变了或者从平板模式切回了手机模式。这种场景用 EventChannel 更合适。我在鸿蒙适配版本里增加了一个deviceProfile事件通道业务层可以订阅设备画像的实时更新const eventChannel EventChannel(will_it_run/profile_events); StreamDeviceProfile watchProfile() { return eventChannel .receiveBroadcastStream() .map((dynamic data) DeviceProfile.fromJson(data as Map)) .asBroadcastStream(); }配合原生侧的 EventSink在系统广播捕获到设备配置变化时把新画像推给 Dart 层。这样业务层就不用每次手动调 will_it_run 的接口而是“感知”到设备环境变化并自动适配。5.3 后续还能怎么扩展鸿蒙化适配跑通之后will_it_run 的扩展空间其实很大。我目前已经在推进的两个方向供你参考。第一个是规则引擎服务化。把“最低内存”“最低 API 版本”等配置放到服务端通过启动时的轻量请求下发这样客户端不用发版就能动态调整判定策略。鸿蒙系统的 API 版本更新很快NEXT 每个大版本都可能引入新的能力规则下发能避免“老包不认识新系统”的尴尬。第二个是设备画像中台的数据回溯。预检中台长期收集设备画像之后你可以看到“用户侧的内存分布”“鸿蒙设备占比”“折叠屏用户比例”这些数据对决定项目最低支持版本、功能灰度范围、甚至硬件采购方向都有帮助。比如你发现目标用户里有 20% 的折叠屏设备那预检规则就值得专门为折叠屏优化。对于 web 端 Flutter 引擎启动慢的问题预检中台也可以延伸去做“启动链路预检环境探测”把“设备能力”和“浏览器能力”合并成一份运行环境报告。总的来说会都会遇到类似的问题与其把能力判断散落在各个业务代码里不如在入口设一个关卡把设备画像和判定逻辑做成一个可持续演进的底座。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。