影悦视频播放器这个项目,是我近半年做的最折腾、也最涨经验的一个实战。项目本身不复杂——用 Flutter 把视频播放器从双端重写一遍,再适配到 HarmonyOS 6.0,重点啃下“推荐视频”这个信息流场景。但真正做起来,问题远比想象多:桥接层怎么设计、视频画面走纹理还是原生视图、列表滚动怎么不卡、切页面状态怎么不丢、鸿蒙打包怎么不崩、Android 侧资源压缩那个邪门报错怎么解。这篇文章不聊虚的,把我这些决策过程、踩坑实录和最后的解决方案完整写出来,适合正在做 Flutter 视频类应用、或者准备把 Flutter 应用适配到鸿蒙的开发者参考。
1. 项目全貌与技术选型的背后逻辑
1.1 为什么用 Flutter 做视频播放器
影悦的视频播放器原本是 Android 和 iOS 双端原生实现的,业务上要快速覆盖鸿蒙,同时还要持续迭代推荐流、弹幕、倍速这些 UI 密集型功能,双端重复开发的成本已经扛不住了。选 Flutter 而不是 React Native,核心原因有两点:一是 Flutter 的自绘引擎能保证两端的 UI 一致性,推荐视频列表这种强交互界面,用 Flutter 做信息流非常顺手;二是 Flutter 的纹理机制和平台通道足够成熟,可以绕过原生视图嵌入这一层,直接把视频帧送到 Flutter 侧渲染。
但要先说清楚一个底层事实:视频画面本身 Flutter 画不了。Flutter 自绘引擎擅长的是 UI,不是视频解码。视频数据必须走原生解码器,解码后的画面通过共享纹理或者原生视图接入 Flutter 的渲染树。所以 Flutter 做播放器的核心架构思路是“UI 走 Flutter,解码走原生,画面走纹理”,这条主线定了,后面所有技术选型都有了一个判断依据。
1.2 HarmonyOS 6.0 下视频内核到底选谁
影悦要适配的是 HarmonyOS 6.0,这意味着不能用 Android 那套 MediaPlayer 直接平替,整个应用要跑在鸿蒙的应用框架上。播放器内核我们当时有三个候选:系统自带的 AVPlayer、第三方 V8 视频播放器内核、自己基于 FFmpeg 封装一层中间件。三者的取舍我直接给一张对比表,都是我实际测试后的体感:
| 方案 | 协议覆盖 | 硬解稳定性 | 精细控制能力 | 鸿蒙适配深度 | 维护成本 |
|---|---|---|---|---|---|
| 系统 AVPlayer | 广泛,常规格式够用 | 稳定,系统级调度 | 弱,音轨/字幕/低延迟首帧受限 | 官方支持,最省心 | 低 |
| V8 播放器内核 | 很全,软硬解自动切换 | 稳定但闭源 | 较强,接口完整 | 看厂商适配进度 | 中,出问题难排查 |
| 自研 FFmpeg 中件 | 最全 | 需要自己调 | 最强 | 自己负责 | 高,播放器 bug 全得自己扛 |
项目最终选了系统 AVPlayer 打底,配合自研的一套播放控制器。理由非常实在:推荐视频流的核心指标是首帧秒开和平滑连播,AVPlayer 在系统级资源调度上有天然优势,而且不需要为闭源内核的未知 bug 买单。我们的业务没有极端格式需求——常见 mp4、HLS、部分 flv,AVPlayer 够用了。V8 这类第三方内核虽然集成快、能力全,但鸿蒙生态下的适配深度和售后响应是黑盒,考虑到线上稳定性,我建议中小团队先用系统内核把业务跑通,后续真要上多音轨、冷门封装格式再考虑换。
补充一句,热词里提到的“v8视频播放器”和“蛙趣视频类似软件推荐”,本质都是播放器内核和产品形态的参考。我专门去拆过同类产品的信息流设计,发现它们的推荐视频模块基本都在做同一件事:横向是可以横滑的精品推荐列表,纵向则是无限刷的信息流,播放器实例常驻,点进去自动连播。这个产品形态决定了我们客户端必须实现播放器实例池化、预加载、状态恢复三件套,后面我详细说。
1.3 推荐视频功能的产品级理解
“推荐视频”这四个字,产品和技术眼里完全不是一回事。产品看的是点击率、人均播放时长、次日留存;客户端技术看的是首包耗时、封面加载成功率、滑动帧率、曝光上报成功率、Crash 率。做推荐视频功能,不能只会写一个 ListView 然后往里面塞数据,必须理解整条链路:
服务端推荐系统负责排序,它依据的素材全部来自客户端上报的曝光、播放时长、点赞、评论等行为数据。客户端要做的是两件事:第一,把推荐结果高效地渲染出来;第二,把用户的真实行为准确、不漏报、不重复地上报回去。这中间还夹着一层本地优化——比如根据用户当前网络状况决定封面图加载策略,根据播放器空闲实例决定下一个视频的预加载数量。
所以“推荐视频实现解析”这个题目,真正要拆的是客户端这条基础设施链:数据拉取怎么设计、列表怎么渲染不卡、播放状态怎么跨页面保持、曝光埋点怎么不被刷量。下面几章我就按这条链路,把那几个深夜调试的场景一个个还原出来。
2. Flutter 与 HarmonyOS 6.0 的通道实战
2.1 MethodChannel 与 EventChannel 在鸿蒙侧的适配
Flutter 和原生通信只有两种通道形态:MethodChannel 适合一问一答的调用,比如初始化播放器、执行 seek、切换倍速;EventChannel 适合持续的数据流推送,比如播放进度、缓冲百分比、播放器状态变化、网络带宽估算。在 HarmonyOS 6.0 上,Flutter 侧代码不用动,但原生侧要用 ArkTS 实现通道的逻辑。
先说一个很多人栽过的坑:EventChannel 在鸿蒙侧的订阅生命周期和 Dart 侧不一样。Dart 端在页面销毁时取消了订阅,鸿蒙侧 StreamHandler 可能还在继续往通道里塞事件,白耗性能和内存。我们的 ArkTS 端是这样处理的:
// ArkTS 侧实现 EventChannel StreamHandler let eventChannel = new EventChannel("com.yingyue.player/progress"); eventChannel.setStreamHandler({ onListen: (eventSink) => { this.eventSink = eventSink; this.startProgressTimer(); // 开启定时器,每 500ms 推送一次 }, onCancel: () => { this.stopProgressTimer(); // 取消订阅以后必须停掉定时器 this.eventSink = null; } });Dart 侧对应的订阅代码:
static const EventChannel _progressChannel = EventChannel('com.yingyue.player/progress'); Stream<dynamic> _progressStream() { return _progressChannel.receiveBroadcastStream(); }另一个容易踩的细节是通道注册时机。MethodChannel 和 EventChannel 在鸿蒙侧如果注册得太晚——比如 MainActivity 的onCreate里注册的时机晚于 Flutter 引擎加载——Dart 侧第一波调用会直接返回空或者抛MissingPluginException。我们项目的做法是把通道注册整体前移,放在应用入口的显式初始化阶段,并且加了重试保护:Dart 侧调用失败后自动延迟 200ms 重试一次,实测能覆盖大部分插件加载时序问题。
2.2 视频画面输出:PlatformView 与 Texture 选型
这是整个项目里最关键的决策,没有之一。Flutter 要在鸿蒙上显示视频画面,只有两条路:把原生视频视图作为 PlatformView 嵌入 Flutter 树,或者把解码后的视频帧通过共享纹理同步到 Flutter 的 Texture widget 上。
我先试的 PlatformView。在 Android 上跑倒是正常,鸿蒙 6.0 接入后问题一堆:合成模式下视频视图和 Flutter UI 的层级关系偶尔错乱,滚动列表中 PlatformView 会打断滑动手势,最要命的是快速滑动时原生视图创建和销毁的抖动,列表的流畅度指标直接跌破 45 帧。我当时的判断是,鸿蒙的混合合成模式还不够成熟,不能把视频这种高频刷新内容交给它。
换了 Texture 方案后体感完全不同。核心做法是把播放器的解码输出帧纹理 ID 暴露给 Flutter 侧,Flutter 通过Texture(textureId: id)这个 widget 消费纹理,视频帧走共享内存更新,不经过原生视图合成。播放器内部还是原生解码,只是画面输出层从 Surface 换成了 Texture。这样做有三个直接收益:列表滚动不再被原生视图打断、视频画面可以正常被 Flutter 的动画和裁剪处理、内存占用更可控。
Texture 方案也有代价。最大的坑是纹理尺寸变化必须重新上报。视频分辨率旋转、比例变化、切换清晰度,都会导致纹理尺寸改变,如果不及时更新,画面在 Flutter 侧会出现拉伸或留黑边。另外鸿蒙的解码器输出普遍是 NV12 格式,Flutter 侧纹理需要 RGBA 格式,中间存在一次色彩空间转换,这个转换有额外开销。我们实测下来,720P 以下的转换开销可以忽略,1080P 以上如果做了高清硬解,建议走 YUV 直通纹理避免格式转换,能省下不少 GPU 带宽。
2.3 登录类插件的鸿蒙适配思路参考
项目中还用到了 okta 这类身份认证服务,这也是个典型的 Flutter 原生插件鸿蒙化案例。思路其实不复杂:原生 SDK 的逻辑保留在 ArkTS 层,通过 MethodChannel 暴露登录、登出、刷新 token 这些方法;回调类事件——比如登录结果、token 过期提醒、多端登录冲突——统一从 EventChannel 往 Dart 侧广播。
我之前看了 okta 这类插件在鸿蒙上的适配流程,踩过的坑主要在几个地方:
- 权限声明:账号登录可能涉及读取设备信息、网络状态、剪贴板验证码等权限,鸿蒙在
module.json5里申请权限必须在安装前声明,漏了权限会在运行时莫名失败。 - 回跳 schema:OAuth 类登录都依赖重定向回跳,鸿蒙上需要在应用配置里注册自定义 scheme,而且这个 scheme 必须和签名证书的 bundleName 匹配,否则回跳不生效。
- 沙箱路径:鸿蒙对应用沙箱限制严格,token 缓存文件必须写到应用私有目录,不能假设 Android 的老路径还能用。
- 签名一致性:token 换发、加密解密都依赖签名校验,debug 证书和 release 证书切换后,本地缓存的 token 可能全部失效,开发期建议给测试环境做独立签名。
这块经验对我们项目的启示是:鸿蒙插件的适配不只是把 Java/Kotlin 翻译成 ArkTS,关键是把生命周期和沙箱模型对齐。按照这个思路走,后续接其他原生 SDK 都有了一套可复用的方法论。
3. 推荐视频列表的完整实现
3.1 数据层:Future、微任务队列与并发加载
推荐流的第一眼体验取决于数据加载。热词里有一条“flutter future的then回调 是放入微任务队列吗”,这个问题我在优化推荐流时踩得很深。
Dart 的事件循环分为事件队列和微任务队列。Future.then的回调确实是放进微任务队列的,这意味着它会在当前事件处理完之后、下一个事件开始之前执行。单次回调没问题,但如果推荐流一页有 20 个视频,每个视频的 JSON 解析、图片信息解码、播放地址拼接回调全都在微任务队列里排着,那么在你的FutureBuilder重建 UI 之前,微任务队列里可能已经排了几百个任务,UI 帧就被堵住了。
我实际排查到的问题就是:列表快速往下滑的时候,compute解析 JSON 的回调结果回来,触发页面 setState,但紧接着图片缓存的多个异步回调又把微任务队列占满,下一帧渲染直接延迟,滑动卡成狗。解决思路有两个:
第一,重活不丢给微任务。推荐流返回的大 JSON 用Isolate.run解析,避免在 UI isolate 里做任何超过 1ms 的工作:
final List<VideoItem> items = await Isolate.run(() { return parseRecommendList(jsonString); // 纯计算,不碰 UI });第二,图片解码不能全量触发。封面图先按列表宽度解码,划出可视区域就取消请求。图片缓存库底层虽然自带线程池,但解码后的位图回主线程依然要过微任务队列,所以必须做“按需解码”,不能让一屏之外的图片抢占微任务。
顺带说一句,如果你面试被问到 Flutter 异步,能说出“event loop 分事件队列和微任务队列,Future.then回调解体进微任务,重计算必须切 isolate”,基本就能筛掉一批背书选手了。
3.2 列表层:Feed 卡片的渲染优化
推荐视频列表的每一个卡片,结构上其实比想象中重:封面图、标题、作者、时长角标、播放按钮、热度标签,有的还带渐变遮罩和圆角。如果每个卡片都走完整布局、完整绘制,列表滚动的压力非常大。
我们的优化方案是分五步走的:
- 固定卡片高度。列表用
ListView.builder,给itemExtent设固定高度,让 Flutter 跳过子项布局的测量过程,滚动时省掉大量 layout 计算。 - 裁剪边界。圆角、阴影这些效果尽量收敛在
RepaintBoundary内部,避免一个卡片的绘制溢出污染整条列表的绘制区域。实测一个列表加入RepaintBoundary后,滚动时的重绘面积可以减小 40% 以上。 - 封面按宽解码。这是最立竿见影的一条。推荐流的封面图原始分辨率动辄 1920x1080,直接解码塞进宽 300 的卡片里,内存浪费和绘制开销都是灾难。用
cacheWidth按列表宽度解码,内存直接下降一个量级。 - 局部动画局部重建。播放按钮的旋转、进度条的平滑更新、点赞动画,都用
AnimatedBuilder包在最小组件上,避免一个动画触发整个列表页 rebuild。 - 不要在滚动中启动新资源。预加载的逻辑只在滚动停顿和快到底部时触发,滚动过程中不做网络新请求,保证滚动手势的优先级。
这套组合拳打完之后,推荐流从当初的持续掉帧,优化到鸿蒙 6.0 真机稳定在 60 帧,滚动体感是顺滑的。有一个细节印象很深:去掉卡片上的过度模糊效果之后,掉帧问题直接少了一半以上。视觉效果差一点,换来流畅度,这个交易非常值。
3.3 状态层:Navigator 切换页面后不丢播放状态
热词里有条“flutter navigator切换页面后,会丢失状态吗”,我可以下个明确结论:Flutter 的 State 不会因为路由切换而丢失,但视频播放这种需要原生侧持续运行的状态,并不会因为 State 还在就自动恢复。
推荐流的典型场景是:用户在列表页点进某个视频的详情页,详情页返回后,希望列表还停在刚才的位置,最好列表里的那个小窗视频画面仍然在。这里考验的是三件事:
第一,列表位置保持。用PageStorageKey给 ListView 绑定一个稳定 key,Flutter 会帮你记住滚动偏移,返回时直接恢复到原位。
第二,列表项不销毁。列表页的 State 需要加AutomaticKeepAliveClientMixin,否则路由被覆盖后列表项可能会被回收,回到页面时封面图、加载状态全没了,体验上就是闪一下重新加载。
第三,播放器实例生命周期管理。Flutter 自带的VideoPlayerController默认绑定 State 生命周期,路由销毁时控制器也跟着销毁,返回时重新创建,播放进度全丢。这就是“切个详情页回来,视频就从头播了”的根源。
我们最终的解法是做了一个独立的播放管理器,用顶层 Scope 持有播放器的生命周期,列表页和详情页都只监听它的状态。列表页需要视频画面时就挂一个Texture,离开列表页就取消挂载,但播放器实例本身还在后台继续跑。这个架构的收益很明显:切详情页不打断播放,返回列表页立即恢复画面,而且点击“下一个”时播放器实例是现成的,直接无缝续播。
3.4 曝光与上报:推荐算法的基础设施
推荐视频如果没有曝光上报,整个推荐系统都是瞎的。我们定义的曝光逻辑是:视频卡片进入可视区域,且停留超过 500ms,算一次有效曝光;如果用户的停留时长超过 5 秒,则额外记一笔“有效播放”事件。
客户端上报要做到两点:不漏报、不重复报。漏报好理解,用户滑得飞快,卡片瞬间划过,如果不做防抖,大量曝光事件丢失。我们的做法是曝光事件先进入本地队列,做 1 秒的合并窗口,等滚动停顿后再批量上报,既保证不漏,又不会在快速滚动时疯狂打接口。重复报更是必须防的坑,同一个视频在本次会话里只看了一次,但因为列表复用被上报了三次,推荐后台的热度数据直接就废了。我们的策略是本地维护一个本次会话去重表,用视频 ID 做 key,重复进入不去重,但主动滑动后重新进入也算新的曝光——这两者要区分清楚。
本地还做了一层轻量的“插队推荐”:根据用户对每个视频的有效播放时长统计,给视频算一个本地热度分,在服务端推荐列表的空位里插入一小部分本地高分视频。这不算真正的推荐算法,但确实能提升用户点击率——因为服务端推荐的更新频率再高,也赶不上用户本地即时行为的反馈速度。这部分数据在用户下次启动时上报,完整地回传给服务端,闭环才算走通。
4. 渲染、打包与上线阶段的硬骨头
4.1 Impeller 与鸿蒙渲染管线的取舍
热词里有“flutter impeller”,这是 Flutter 新渲染引擎的代号。Android 上 Flutter 3.27 之后默认启用 Impeller,iOS 上早就全面切了。Impeller 的好处是提前把 shader 编译好,避免 Skia 在运行时“着色器编译抖动”导致的掉帧。视频类应用的滚动列表对这种抖动尤其敏感,所以 Android 侧我们直接保持默认的 Impeller 渲染。
但 HarmonyOS 6.0 上的 Flutter 适配版渲染管线,目前主流还是基于 Skia 或自研的兼容实现。这意味着在鸿蒙上跑 Flutter,之前那些针对 Impeller 的性能假设要重新审视。我建议的开发策略是分三层:
| 渲染特性 | Impeller 表现 | 鸿蒙 Skia 表现 | 开发建议 |
|---|---|---|---|
BackdropFilter毛玻璃 | 提前编译,开销可控 | 滚动时着色器编译,掉帧重灾区 | 避免高频使用,可用静态遮罩替代 |
| 复杂阴影/模糊 | 开销较低 | 绘制压力大 | 减少大面积阴影投影 |
| 自定义 shader | 需要迁移到新接口 | 兼容性好 | 统一封装,后续引擎切换时改动集中 |
| 文字和字体图标 | 字体族解析差异大 | 同样存在差异 | 不要硬编码 fontFamily,用系统默认 |
一个真事:我们初版推荐流在卡片底部做了一个毛玻璃拖尾效果,Android 侧完全没事,HarmonyOS 6.0 真机上这个区域一出现就掉到 50 帧以下。排查下来就是 Skia 每次滚动重新编译 shader 的损耗。后来把毛玻璃改成静态半透明渐变遮罩,视觉差异很小,帧率直接拉回 60。
字体这块也踩过。鸿蒙上某些字体族解析和 Android 不一致,图标字体在部分设备上会变成“豆腐块”,排查了挺久才发现是字体文件在系统字体回退时的兼容问题。结论就是别赌字体渲染,图标一律用矢量图形或者系统自带的 material icons,永远比自定义字体靠谱。
4.2 鸿蒙 HAP 打包与签名常见坑
热词里有一条“flutter打包 java.lang.assertionerror: java.lang.exception: could not close i”,这其实是 Android 侧的打包问题——AGP 在做资源压缩时没能关闭输入流,多半是 JDK 版本和 AGP 版本不匹配,或者构建目录里有残留文件锁。这种问题在持续集成环境里尤其常见,我们的解决姿势是:升级到项目适配的稳定 JDK 版本、清理build目录、如果还报就检查是否有杀毒软件锁住了构建缓存文件。
鸿蒙侧的 HAP 打包则是另一套体系。鸿蒙用的构建工具是 hvigor,配置在build-profile.json5里。我们踩过的坑有四个:
- 签名证书过期。鸿蒙 debug 证书按设备维度签发,有时效限制,隔一段时间就报“signature verification failed”。最稳妥的做法是在项目文档里直接记录证书的过期时间,到期前统一换新。
- bundleName 与证书不匹配。换签名证书后如果 bundleName 不一致,安装直接失败。这个问题通常在团队协作时出现,不同开发者电脑上的证书不同,建议 CI 统一管理证书。
- module.json5 配置错误。
abilities节点如果没配好入口 ability,应用装上去没有桌面图标,看起来像是“安装失败”。检查一下skills和entity配置是否包含了系统入口的隐式意图。 - 网络权限必加。推荐流要拉网络,鸿蒙上
ohos.permission.INTERNET必须写入requestPermissions,漏了以后接口全挂,而且错误提示很隐晦,只显示“网络异常”。
还有一个小坑是打包产物路径。HAP 的输出目录如果包含中文或者空格,hdc install会时不时解析失败,CI 环境里直接全部统一成英文路径。
4.3 服务卡片与“类 LiveActivity”的延伸设想
热词里还有一条“flutter实现liveactivity”,LiveActivity 是 iOS 的灵动岛能力,Flutter 本身不直接支持,需要原生侧实现。受这个思路启发,我们在鸿蒙 6.0 上做了一个类似的事情:服务卡片。在“正在播放”场景里,用户按 Home 键退出 App 后,桌面卡片可以继续显示当前播放的视频标题、进度条和封面缩略图。
实现方式不算复杂:ArkTS 侧实现一个卡片服务,Flutter 侧通过 EventChannel 把状态变化同步给原生,原生再更新卡片数据源。这里要特别注意任务的保活策略——应用退到后台一段时间后,系统可能挂起任务,卡片数据就不再实时更新了。我们的做法是:正常播放时每 30 秒同步一次进度;应用进入后台后停止高频同步,切歌或暂停的瞬间做一次即时同步。既保证了卡片新鲜度,又不至于被系统判定为异常耗电。
这个延伸功能给推荐流的用户留存帮了大忙。推荐视频本来就讲究“杀时间”,用户在桌面看到正在播放的视频,点一下就能回到 App 继续看,整个召回链路就补上了。
5. 复盘:踩过的坑、排查实录与给你的建议
5.1 典型问题速查表
下面这些问题是项目过程中真实遇到且花了不少精力解决的,我整理成一张速查表,方便你以后直接对照排查:
| 问题现象 | 根本原因 | 解决方案 |
|---|---|---|
| 视频列表滑动卡顿 | 封面图全尺寸解码、布局未固定 | cacheWidth按宽解码、itemExtent固定高度 |
| EventChannel 订阅后 Dart 收不到事件 | 通道注册时机晚或通道名不一致 | 全局注册前移 + 首轮调用失败重试机制 |
| 鸿蒙安装后桌面无图标 | module.json5abilities 入口配置错误 | 检查skills和入口 ability 配置 |
Android 打包Could not close input stream | JDK 与 AGP 版本不兼容、缓存锁 | 升级 JDK、清理 build 目录、CI 环境检查 |
| 返回详情页视频黑屏 | 播放器控制器随页面销毁 | 播放管理器独立于页面生命周期,顶层持有 |
| 文字图标变“豆腐块” | 字体族解析差异 | 不硬编码 fontFamily,用系统默认或矢量图标 |
| 列表快速滑动曝光数据爆炸 | 曝光事件未合并去重 | 1 秒合并窗口 + 会话内去重 |
| 推荐流第一帧慢 | 播放器实例每次新建 | 播放器实例池化,提前预热 2 个实例 |
5.2 如果让我重做一遍,我会提前做的三件事
第一件事,视频画面方案一开始就定 Texture,而不是先去试 PlatformView。虽然理论上 PlatformView 在某些低版本场景下够用,但在鸿蒙这种混合合成还不完善的平台上,PlatformView 的坑几乎无法绕过。我在这里调了两周,最后全部推翻换 Texture,两周时间换一个刻骨铭心的教训:高频画面更新走纹理,轻量原生交互才走 PlatformView。
第二件事,播放器实例池要早期搭。推荐流的连播体验,最怕的是用户点“下一个”之后要等一秒才出画面。现在我们维护了一个播放器实例池,常驻 2 个播好地址的实例在后台,点击下一个直接切换纹理,首帧基本零延迟。这个架构最初因为“没必要”而推迟,后来用户报告连续切换视频卡顿时才补上,早该在设计期就入局。
第三件事,曝光上报的数据校验在客户端就要做全。第一版上线后,运营反馈推荐后台的热度数据有明显异常,排查下来就是曝光事件在弱网环境下被请求重试机制反复提交,单条视频一个小时内上报了十几次。后来在本地加了严格的事件幂等去重之后,数据准确性才恢复正常。这个教训的代价是让运营同学白加班了好几天,挺过意不去的。
5.3 我最后悔没早知道的三个细节
细说三个特别容易被忽略、但实际影响很大的细节。
第一个是列表里的视频画面不要和列表滚动抢资源。在推荐流里,多个视频卡片同时进入可视区域时,如果每个卡片都尝试启动解码,GPU 和 CPU 会在瞬间被拉满,滚动直接卡顿。我们后来做了滚动仲裁:只有完全居中且停留超过 150ms 的卡片才有资格启动解码,其他卡片只显示封面。这个策略让资源竞争降了一个量级。
第二个是视频首帧秒开不只是网络问题。如果你的播放器每次都从 0 字节开始拉流,首帧延迟一定会高。我们做了播放地址的 Range 请求支持,服务端配合返回六秒内的关键帧数据,客户端收到 1-2 个关键帧就直接上屏,首帧耗时从 1.2 秒压到 400ms 左右。这个优化往往被团队当作“网络优化”忽略,其实它是播放器架构的一部分。
第三个是 HarmonyOS 6.0 上做 Flutter 开发,真机调试和模拟器行为差异很大。模拟器上纹理、PlatformView、服务卡片都可能表现正常,但真机上的渲染管线和系统调度完全不同。我强烈建议你把模拟器当作语法校验工具,把真机当作唯一验收环境,特别是帧率、内存、发热这些指标,模拟器数据没有参考价值。
整个项目做下来,我最大的感受是:Flutter 做推荐视频这种强信息流场景,是合适的;但合适的前提是,你愿意在平台通道、纹理渲染、状态管理、数据上报这些“脏活”上下足够功夫。框架帮你解决的是 UI 一致性和开发效率,视频播放、系统适配、数据闭环这些事情,还是得靠一身实战经验一点点磨。如果你正在走这条路,希望这篇文章能帮你少踩几个我已经替你踩过的坑。