ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

RN for OpenHarmony开发实践:装备搜索功能从零实现到性能优化

RN for OpenHarmony开发实践:装备搜索功能从零实现到性能优化 最近在折腾RN for OpenHarmony把之前写的英雄联盟助手App从Android端往OpenHarmony上迁移结果一路踩坑踩到怀疑人生。尤其是装备搜索这个看似不起眼的功能真做起来才发现里面有大量细节需要打磨。这篇就把整个装备搜索的实现过程拆开捋一遍包括数据层设计、搜索逻辑、UI组件、原生能力交互以及各种奇奇怪怪的渲染和兼容问题希望能帮到正打算入坑RN for OpenHarmony的朋友。1. 项目背景与整体设计思路1.1 为什么选择RN for OpenHarmony先说说为什么非要折腾RN for OpenHarmony。我的英雄联盟助手App本身是基于React Native开发的已经积累了大量的业务代码和组件库。如果为了适配OpenHarmony从零写一套原生应用代价实在太大了。RN for OpenHarmony这个方案的出现恰好解决了跨端复用的问题让我能在不重写业务逻辑的前提下把现有代码迁移过去。从技术实现上看RN for OpenHarmony是OpenHarmony社区在React Native框架基础上做的适配层核心思路是把原有RN的渲染引擎、组件系统和JavaScriptCore/V8运行时映射到OpenHarmony的ArkUI框架上。它保留了React的声明式UI开发模型和组件化思想同时对ArkUI的原生能力做了一层Bridge封装。这就意味着你在RN里写的View、Text、FlatList这些组件在OpenHarmony上会被映射成对应的ArkUI组件比如Column、Text、List。这套方案的适用范围主要还是那些业务逻辑复杂、需要快速迭代和跨端复用的App。纯OpenHarmony原生应用当然性能更好但如果你的团队已经深扎在RN生态里完全用原生ArkTS重写一个功能繁杂的App时间成本和维护成本都相当可观。实际体验下来RN for OpenHarmony在绝大多数业务场景下的性能表现都能接受只有在高频列表滚动、复杂动画这类场景下才能感觉到和纯原生之间存在一些差距。1.2 装备搜索功能的定位与核心需求回到产品层面英雄联盟助手App的装备搜索功能实际用户需求分三类。第一类是新手玩家他们知道某个英雄但不清楚出什么装备需要根据英雄推荐装备第二类是熟悉游戏但记不清装备具体属性的玩家会直接搜装备名查价格、合成路径、属性加成第三类是进阶玩家会按照特定属性条件找装备比如找所有加移速的物理攻击装备。对应的功能设计就是三个维度装备名称搜索支持模糊匹配玩家输入无尽可能能定位到无尽之刃标签/属性筛选按攻击、法术、防御、移动速度、攻速等分类过滤推荐出装根据英雄关键词关联推荐装备组合本文重点讲装备名称搜索和属性筛选这两块推荐出装涉及数据建模的内容太多会在后面单独开篇聊。1.3 为什么先做装备搜索可能有人会问迁移的时候为什么先做装备搜索而不是先把所有页面都搬过去原因很简单装备搜索几乎涵盖了RN开发中最核心的技术点列表渲染FlatList/FlashList搜索防抖与状态管理网络请求与图片加载原生模块调用本地数据源查询页面跳转与参数传递把这些点跑通整个App迁移的技术路线就基本验证了。而且装备相关的数据不敏感、不会频繁更新很适合用来做压测和海量数据渲染测试。2. 环境搭建与工程初始化2.1 RN for OpenHarmony环境配置环境搭建这块官方文档写了不少但实际配置起来还有一些坑值得说清楚。我用的是OpenHarmony 5.0 Release版本配合DevEco Studio 5.0RN for OpenHarmony的SDK版本是0.72.x的适配版。需要准备的关键环境如下Node.js 18.0或以上版本OpenHarmony SDK通过DevEco Studio安装React Native for OpenHarmony npm包在项目里配置hvigor构建工具随DevEco Studio附带安装完DevEco Studio之后记得在项目的build-profile.json5里确认buildMode设置为releasetargetSdkVersion和compatibleSdkVersion要跟装好的SDK版本对齐。app: { signingConfigs: [], buildModeSet: [ { name: debug }, { name: release } ], products: [ { name: default, signingConfig: default, compatibleSdkVersion: 5.0.0(12), runtimeOS: OpenHarmony } ] }2.2 创建RN项目并桥接OpenHarmony工程这里有两种做法。一种是用社区的脚手架工具react-native-ohos/generator来生成项目模板它会自动生成一个包含OpenHarmony原生工程的RN项目另一种是手动给已有的RN项目添加OpenHarmony工程目录。我推荐用脚手架工具因为手动配置的坑太多了。生成命令大概是这样npx react-native-ohos/generator init MyLolAssistant生成的项目结构里会多出一个harmony目录这个就是OpenHarmony的原生工程所在位置。entry/src/main/ets下面是入口代码entry/src/main/cpp下面是C桥接层也就是RN运行时与OpenHarmony系统能力交互的通道。提示如果遇到同步gradle依赖超时的情况很多情况下是网络问题。建议换个网络环境或者配置镜像源再试试。另外DevEco Studio首次打开工程的时候会自动下载hvigor和SDK相关依赖这个过程可能比较久需要耐心等待。2.3 页面导航框架选型装备搜索功能涉及从首页跳到搜索页、从搜索页再跳到装备详情页必须提前把导航方案定下来。RN生态里最常用的是react-navigation原生栈导航它的性能和体验在RN for OpenHarmony上表现基本可用。不过有一个要注意的地方是react-navigation的底层依赖了react-native-screens这个原生库来做屏幕切换优化。RN for OpenHarmony对这个库的适配版本需要注意兼容性我一开始用了最新版直接报错后来在社区仓库换了对应OpenHarmony的适配分支才跑通。如果不想踩这个坑可以直接用纯JS版本的react-navigation/stack代价是页面切换动画流畅度会稍微差一点。在实践中我发现NavigationContainer在OpenHarmony上有一点特殊处理由于OpenHarmony的页面栈管理方式跟Android/iOS的后台任务回收机制不一样建议在进入页面栈较深的时候主动使用navigation.replace而不是navigate避免无意义的历史页面累积导致内存使用率过高。3. 装备数据层设计与本地存储3.1 装备数据模型设计装备数据模型是整个搜索功能的地基。我的数据来源是维护在Git仓库里的JSON文件里面收录了英雄联盟目前版本的全部装备数据大概300多件装备每条记录包含以下关键字段字段类型说明示例idstring装备唯一编号3031namestring装备名称无尽之刃nameEnstring英文名称Infinity Edgeiconstring图标URLhttps://xxx.com/icon/3031.pngpricenumber总价格3400combinePricenumber合成价格1300categorystring[]装备分类标签[攻击, 暴击]attributesobject属性加成{attackDamage: 70, critChance: 0.2}componentsstring[]合成部件ID列表[1038, 1036]specialEffectsstring特殊效果说明暴击伤害提升...在设计这个数据模型时有一个关键决策是属性字段选用扁平结构还是嵌套结构。为了减少搜索时的遍历开销我最终选了扁平结构加索引的方式——把需要搜索的字段名称、分类标签、属性名单独拿出来维护成轻量索引避免每次搜索都要遍历完整的装备详情JSON。3.2 本地数据持久化方案装备数据在每次启动时都从服务器拉取太慢了而且会消耗不必要的流量。我的做法是首次启动时从远程拉取最新装备JSON写入本地缓存之后启动直接读取本地缓存后台异步做增量更新如果本地没有缓存或者版本过低才走全量下载RN for OpenHarmony的AsyncStorage封装了OpenHarmony的轻量级偏好数据库适合存小体量JSON数据。装备数据经过压缩后大概在200KB左右用AsyncStorage存储没问题。import AsyncStorage from react-native-async-storage/async-storage; const ITEM_CACHE_KEY lol_assistant/items_v2; const ITEM_VERSION_KEY lol_assistant/items_version; export async function loadItems() { try { const cached await AsyncStorage.getItem(ITEM_CACHE_KEY); if (cached) { return JSON.parse(cached); } } catch (e) { // 解析失败回退到内置数据 } return DEFAULT_ITEMS; } export async function saveItems(items) { const version items.meta ? items.meta.version : 2024.11; await AsyncStorage.setItem(ITEM_VERSION_KEY, version); await AsyncStorage.setItem(ITEM_CACHE_KEY, JSON.stringify(items)); }注意AsyncStorage的存储并不是事务性的如果数据比较大建议先用setItem写入临时key再通过multiSet原子性地更新正式key避免中途崩溃导致缓存损坏。3.3 内存索引与搜索加速300多条数据量其实不大即便线性遍历也能在几十毫秒内完成搜索。但考虑到搜索结果页面需要即时响应而且用户可能在一个搜索会话内连续键入十几个字符如果每次键入都触发全量遍历渲染UI会明显卡顿。为了追求更快的搜索体验我在数据加载完成后会构建一个预处理索引。具体做法是对每个装备名称做分词处理建立关键词片段 - 装备ID集合的映射对分类标签和属性名建立同样的倒排索引搜索时先查索引拿到候选ID集合再根据交集和相关性分值排序这种做法的好处是搜索主体在内存中完成不依赖外部搜索引擎代码量小性能也可控。对300多条数据来说搜索耗时稳定在10毫秒以内。4. 装备搜索核心功能实现4.1 搜索框组件与输入防抖设计搜索框看起来简单但交互细节很多。我最终实现的效果是顶部固定搜索输入框输入时自动联想匹配下方是搜索结果列表支持点击进入详情页空状态时展示历史搜索和热门装备。在输入框实现上我需要处理几个关键点输入内容实时响应但不能每敲一个字符就立刻搜索中文输入法组合输入期间不能触发搜索清空按钮要能一键删除并恢复初始状态防抖的常见实现无非是setTimeout加清理但中文输入法的问题容易被忽略。安卓端默认的onChangeText在拼音组合阶段会持续触发这个阶段不应该执行搜索。我通过onTextInput和比较textInputEvent.nativeEvent.text的value变化来判断是否是组合输入。const searchTimer useRef(null); const handleInputChange (text) { setKeyword(text); if (searchTimer.current) { clearTimeout(searchTimer.current); } searchTimer.current setTimeout(() { performSearch(text); }, 300); };防抖时间我最终调到了300毫秒太短会在快速输入时频繁触发搜索太长又会让搜索结果出现明显滞后感。实测下来300毫秒是比较平衡的取值。4.2 搜索核心逻辑匹配、排序与过滤搜索结果的质量直接影响用户对App的信任度。搜索逻辑由三部分组成首先是匹配规则。输入关键词后按优先级顺序进行匹配精确匹配装备名称与关键词完全一致排在结果首位前缀匹配装备名称以关键词开头排第二位模糊包含匹配装备名称包含关键词排第三位属性/标签匹配关键词命中了装备分类或效果标签排第四位其次是排序规则。相同匹配级别内按装备的price从高到低排序因为高价装备通常意味着更后期的成型装备展示优先级更高。最后是过滤规则。用户可以通过分类标签攻击、法术、防御、移动速度等进一步缩小范围搜索结果必须是同时满足关键词匹配和标签过滤的装备。const SEARCH_PRIORITY { EXACT: 0, PREFIX: 1, INCLUDE: 2, TAG: 3 }; export function searchItems(items, keyword , filters []) { const trimmed keyword.trim().toLowerCase(); if (!trimmed filters.length 0) { return items; } const result []; for (const item of items) { const nameMatched trimmed ? item.name.toLowerCase().includes(trimmed) : false; const tagMatched filters.length 0 ? filters.every(f item.category.includes(f)) : true; if (!nameMatched !tagMatched) continue; if (filters.length 0 !tagMatched) continue; let priority SEARCH_PRIORITY.INCLUDE; if (trimmed) { if (item.nameEn item.nameEn.toLowerCase() trimmed) { priority SEARCH_PRIORITY.EXACT; } else if (item.name keyword || item.nameEn.toLowerCase().startsWith(trimmed)) { priority SEARCH_PRIORITY.PREFIX; } else if (item.category.some(c c.includes(trimmed))) { priority SEARCH_PRIORITY.TAG; } } if (trimmed !item.name.toLowerCase().includes(trimmed) !item.nameEn.toLowerCase().startsWith(trimmed) !item.category.some(c c.includes(trimmed))) { continue; } result.push({ item, priority }); } result.sort((a, b) { if (a.priority ! b.priority) return a.priority - b.priority; return b.item.price - a.item.price; }); return result.map(r r.item); }4.3 搜索结果列表与图片加载优化列表渲染是整个搜索功能的性能瓶颈所在。RN传统的FlatList在OpenHarmony上虽然可用但渲染300多张图片和文本混排的cell滚动时会出现明显的卡顿。通过实验对比我最终选择了FlashList它对列表cell的渲染做了异步批处理和回收在OpenHarmony环境下的表现比FlatList稳定不少。图片加载是另一个容易忽视的问题。装备图标来自远程服务器如果在列表快速滚动时并发加载大量图片网络带宽和内存都会被拖垮。我的处理策略是使用react-native-fast-imageOpenHarmony适配版加载远程图片支持内存和磁盘缓存给列表cell设置windowSize和maxToRenderPerBatch参数控制渲染窗口大小列表滚动时暂停加载低优先级图片静止后再恢复关于react-native-fast-image的OpenHarmony适配版社区有专门的repo但需要注意版本跟RN for OpenHarmony要配套否则会在Native端直接crash。4.4 空状态、错误态与加载态处理很多开发者在做搜索功能的时候只考虑了有结果的情况空状态和错误态往往被忽略。但在实际使用中空状态的处理直接影响用户体验。我实现了三种状态初始态未输入关键词、未选筛选条件时展示历史搜索记录和热门装备推荐空结果态搜索无匹配时展示未找到相关装备并推荐几个热门搜索词供用户点击错误态网络或数据加载失败时展示重试按钮和错误提示这里有个值得分享的细节。空结果页的推荐搜索词我是根据当前输入关键词的热度来动态调整的。比如用户输入穿透会推荐最后的轻语、幽梦之灵这类带穿甲的装备用户输入移速会推荐疾射火炮、三相之力等。这种基于当前语境的下钻设计能让用户即使首次搜索失败也觉得App懂他。5. 原生能力协同与功能扩展5.1 调用系统电话功能装备搜索场景里有一个比较特殊的需求是用户如果对装备数据有疑问想直接联系客服。由于我的客服系统是电话热线制就需要在App内提供一键呼叫的能力。在RN for OpenHarmony里调用系统电话可以通过react-native-linking库来实现。思路跟Android端一致就是用Linking.openURL(tel:4001234567)拉起系统拨号盘。import { Linking } from react-native; const callSupport async () { try { const url tel:4001234567; const supported await Linking.canOpenURL(url); if (supported) { await Linking.openURL(url); } else { // 弹Toast提示设备不支持 } } catch (error) { // 处理异常 } };要注意的是OpenHarmony在权限管理上对拨打电话这个能力是有管控的。如果你的应用是普通分发渠道可能没有ohos.permission.CALL_PHONE权限这种情况下canOpenURL会返回false。这并不代表功能不可用而是在App的module.json5里需要声明这个权限同时用户首次使用时也要弹窗授权。5.2 接入扫码识装备的探索与实践这个功能算是我自己的一个脑洞灵感来自之前看到的搜索热词rn 使用react-native-vision-camera扫码。既然英雄联盟助手是查装备的那能不能直接扫码识别装备这里的扫码不是二维码而是比赛直播画面上的装备图标。玩家在看直播时看到职业选手出了一件装备想立刻知道这是什么装备、怎么合成、属性如何直接对着屏幕扫一下如果App能识别出装备图标并自动匹配体验会非常极致。技术路线其实不复杂。先对直播画面截图用图像检测识别出装备栏区域再按格子裁切出小图最后跟本地装备图标库做相似度比对。RN端负责相机权限、画面预览、截屏采集图像比对和装备匹配放到服务端做。相机库用的是React Native Vision Camera在OpenHarmony上的适配版本核心代码大概这样import { Camera, useCameraDevice, useFrameProcessor } from react-native-vision-camera; function ScanScreen() { const device useCameraDevice(back); const frameProcessor useFrameProcessor((frame) { worklet; // 将帧数据发给服务端进行图标识别 const result detectItems(frame); if (result) { navigateToItemDetail(result.itemId); } }, []); if (device null) { return LoadingView /; } return ( Camera style{StyleSheet.absoluteFill} device{device} isActive{true} frameProcessor{frameProcessor} frameProcessorFps{5} / ); }这个功能目前还在内测阶段识别准确率大概在85%左右。不过已经有不少用户反馈说这功能太顶了说明方向是对的。5.3 大视频/图片选择组件的集成经验搜索场景里还有一个隐藏需求是用户可能想把自己的排位对局截图发给客服截图里带着自己的出装和战绩让客服帮忙分析出装是否合理。这就涉及到了图片选择器的能力。社区里的react-native-multiple-image-picker支持同时选择多张图片和视频底层封装了OpenHarmony的PhotoAccessHelper能力。适配过程中我踩了一个大坑这个组件默认是为Android/iOS设计的在OpenHarmony上需要指定使用photoAccessHelper的API版本否则会出现无法读取相册的问题。正确的打开方式是import ImagePicker from react-native-multiple-image-picker; const selectMedia async () { const options { mediaType: image, maxCount: 9, isPreview: true, isCamera: false, // OpenHarmony适配参数 isOpenHarmony: true, mediaTypeOpenHarmony: IMAGE }; const result await ImagePicker.openPicker(options); // result里是选中的图片文件路径和元数据 };特别注意OpenHarmony上访问相册需要先申请ohos.permission.READ_IMAGEVIDEO权限。虽然组件的openPicker内部会自动请求一次权限但建议在页面进来时提前声明避免用户在授权弹窗上犹豫导致权限授权失败流程中断。6. 渲染异常与性能优化实战6.1 OpenHarmony画面渲染异常排查实录搜索热词里出现了openharmony画面渲染异常这确实是RN for OpenHarmony开发中绕不开的话题。我项目里就遇到了一个典型的渲染异常当搜索结果列表快速滚动时部分装备图片会短暂黑屏或闪白甚至某些装备名称文本会出现重叠。排查过程是这样推进的第一步先确认问题是否跟网络图片加载有关。把远程图片URL全部替换成本地静态图片之后问题依然存在说明不是图片加载的问题。第二步确认是否跟列表复用机制有关。禁用FlashList的recycleItems属性后闪烁问题有所缓解但性能下降明显。第三步深挖发现问题的根因在于RN for OpenHarmony的渲染引擎跟原生ArkUI组件树同步时没有正确处理cell卸载时的资源释放。当cell被回收时如果图片请求还没完成异步回调到一块已经被释放的内存区域就会造成渲染异常。最终解决方案是给图片组件加了一个取消未完成请求的清理逻辑class SafeImage extends React.Component { componentWillUnmount() { if (this.loadTask) { this.loadTask.cancel(); } } render() { return ( FastImage {...this.props} onLoadStart{() { this.loadTask new AbortController(); }} / ); } }这样在cell被回收时提前取消图片加载从根源上杜绝了问题。6.2 OpenHarmony x86模拟器与真机差异开发过程中我用的主力模拟器是x86架构的OpenHarmony镜像。x86模拟器跑起来确实方便但有几个坑值得提前知道x86模拟器对部分原生模块的模拟不完整比如相机、传感器这类硬件相关的API在模拟器上经常返回空数据图像渲染的GPU能力跟真机差异很大模拟器上看起来正常的动画真机上可能掉帧x86模拟器的网络栈跟真机不完全一致一些依赖socket长连接的库行为可能不同所以我的建议是逻辑开发阶段用模拟器涉及渲染效果、硬件调用和性能调优的阶段一定切真机验证。我自己就因为模拟器上一切正常结果在真机上发现列表滚动掉帧严重浪费了整整两天时间。6.3 内存占用优化与列表性能调参搜索页面的内存占用是决定应用是否卡顿的关键指标。在做性能测试时我发现搜索页面在反复进出、多次搜索之后内存占用会持续上涨。最终的优化手段包括搜索结果列表的initialNumToRender设置成10避免一次性渲染太多cellwindowSize设为5控制视口外提前渲染的数量图片缓存策略从cacheOnly改为web让内存和磁盘缓存配合详情页返回时手动清空搜索结果页的图片缓存这几个参数调整完之后首页到搜索页的切换耗时从450ms降低到了260ms左右内存占用峰值也从180MB降到了120MB整体体感流畅了很多。7. 常见问题与调试经验速查整理一份开发过程中高频踩坑的速查表方便大家对照排查。问题现象可能原因解决方案页面跳转卡顿react-navigation版本不兼容换用适配OpenHarmony的分支版本图片加载失败缺少INTERNET权限在module.json5里声明ohos.permission.INTERNET中文输入法搜索紊乱组合输入阶段触发了搜索增加输入法组合判断用onTextInput做区分列表滚动白屏cell卸载未取消图片请求在componentWillUnmount中取消未完成的加载模拟器上功能正常真机异常x86模拟器能力限制渲染和硬件类功能以真机调试为准异步存储数据丢失AsyncStorage非事务性写入使用multiSet原子操作或双层缓存拨打电话无法拉起缺少CALL_PHONE权限在module.json5声明权限并提示用户授权弹出分享面板崩溃原生模块版本不匹配检查react-native-share的OpenHarmony适配版本再补充一个调试技巧。RN for OpenHarmony的调试模式跟标准RN略有不同在DevEco Studio里可以同时看到ArkTS层和C层的日志输出。搜索功能这类涉及JS与Native交互的逻辑问题建议大家把harmony工程里的hilog日志级别调到Debug能看到RN运行时与ArkUI通信的详细信息排查效率会高很多。8. 后续规划与个人心得装备搜索功能已经在我的RN for OpenHarmony版本上跑通了目前正在做的下一步是整合对局战绩查询模块同样依赖RN的原生能力桥接和列表渲染能力跑通之后就相当于把整个App的核心链路全部复刻到了OpenHarmony生态上。最后分享一点自己在这一轮迁移中的切身体会。跨端框架的好处是业务代码能复用但每个平台的系统能力和渲染机制终归有差异指望一行不改就能跑通所有平台在RN for OpenHarmony这里还是不太现实的。关键是搞清楚这些差异在哪里、怎么在框架层和应用层做补偿和适配。装备搜索这个功能虽然小但该踩的坑一个没落下数据缓存、防抖、列表复用、原生能力调用、渲染异常排查全都经历了一遍。把这些基础打牢后面做更复杂的功能心里就有底了。
返回列表