React?Native?的動(dòng)態(tài)列表方案探索詳解
背景
時(shí)至2022,精細(xì)化運(yùn)營已經(jīng)成為了各大App廠商的強(qiáng)需求,阿里的 DinamicX、Tangram 大家應(yīng)該都很熟悉了,很多App廠商也自研了一些類似框架,基于DSL的動(dòng)態(tài)化方案雖然有性能上的一些優(yōu)勢,但是畢竟不是圖靈完備,一些需要邏輯動(dòng)態(tài)下發(fā)的需求實(shí)現(xiàn)成本偏高,或由于DSL本身限制無法實(shí)現(xiàn),針對這個(gè)問題我們使用RN進(jìn)行了一下探索嘗試, 利用我們已經(jīng)相對完善的RN基建,結(jié)合客戶端列表能力低成本的實(shí)現(xiàn)了一套的動(dòng)態(tài)化能力,同時(shí)兼顧一定的性能體驗(yàn)。
基于 ReactNative 的動(dòng)態(tài)列表方案簡單來說就是將 ReactNative 容器內(nèi)嵌在 RecyclerView 的 ViewHolder 中,由于頁面主體框架還是由 Native 開發(fā)和渲染,所以首屏加載速度得到了保證,局部的RN實(shí)現(xiàn)也使頁面獲得動(dòng)態(tài)化的能力,從而在性能、”完備邏輯執(zhí)行“的動(dòng)態(tài)化能力之間取得了一個(gè)平衡點(diǎn),根據(jù)我們使用經(jīng)驗(yàn)對幾種動(dòng)態(tài)化方案排序如下:
- 整體性能體驗(yàn)排序: 純 Native > 基于DSL動(dòng)態(tài)化方案 >= ReactNative 動(dòng)態(tài)列表方案 > 純 ReactNative 頁面 > H5
- 動(dòng)態(tài)能力排序: H5 = 純 ReactNative 頁面 > ReactNative 動(dòng)態(tài)列表方案 > 基于DSL動(dòng)態(tài)化方案 > 純 Native
- 實(shí)現(xiàn)能力排序: 純 Native >= RN 動(dòng)態(tài)列表方案 = 純 ReactNative 頁面 > H5 > 基于DSL的動(dòng)態(tài)化方案
從以上排序中可以看出 ReactNative 動(dòng)態(tài)列表方案整體處于中等或中等偏上的一個(gè)位置,在實(shí)現(xiàn)能力上遠(yuǎn)勝余基于 DSL 動(dòng)態(tài)方案,和 Native 能力基本對等,可以實(shí)現(xiàn)一些復(fù)雜的UI交互效果,并且相比于純 RN 實(shí)現(xiàn)的頁面首屏速度會(huì)有非常大的優(yōu)勢,另外不需要對頁面整體框架進(jìn)行更改就能比較方便的嵌入,在開發(fā)維護(hù)成本上 RN 動(dòng)態(tài)列表方案相比各種基于DSL的動(dòng)態(tài)化方案會(huì)有比較明顯的優(yōu)勢,不需要額外的開發(fā)組件管理平臺(tái),排查問題時(shí)也不用去讀難懂的 dsl,最重要的是 RN 具有圖靈完備的能力,所以綜合來看使用 RN 內(nèi)嵌到 Native RecyclerView 來實(shí)現(xiàn) Native 頁面部分動(dòng)態(tài)化的方式算是一種性價(jià)比相對較高的方式了,值得一試。
技術(shù)方案介紹
這里從 Android 視角分享下我們這套方案實(shí)現(xiàn)的一些技術(shù)細(xì)節(jié)、原理以及遇到的問題。首先我們常用的一些術(shù)語:
moduleName
是 RN 離線包的唯一 key,相當(dāng)于離線包的名字;componentName
是 RN 中 registerComponent 的 component,對應(yīng)一個(gè) RN 實(shí)現(xiàn)的業(yè)務(wù)的執(zhí)行入口;- 卡片指云音樂首頁中每個(gè) viewholder 內(nèi)部的展示內(nèi)容,展示的 UI 樣式是卡片樣式;
- RN 引擎指以 RN Bridge 為主的整個(gè) JS 離線包運(yùn)行時(shí)環(huán)境。
整體方案架構(gòu)如下:
從圖中可以看出整體方案采用數(shù)據(jù)驅(qū)動(dòng)的方式,服務(wù)端通過數(shù)據(jù)中攜帶的類型、component、moduleName等字段來唯一指定是否是使用 RN 來渲染,執(zhí)行 RN 離線包中的哪個(gè) component 邏輯
整體方案上有幾個(gè)細(xì)節(jié)點(diǎn):
- 采用數(shù)據(jù)驅(qū)動(dòng)的方式,接入頁面無須關(guān)注具體展示數(shù)據(jù),只需要將數(shù)據(jù)透傳到 RN 的 JS 側(cè)即可
- 由于 RN 需要將離線包加載后才能執(zhí)行 JS 生成客戶端視圖,在 RecyclerView 綁定數(shù)據(jù)時(shí)才開始加載 RN 的離線包勢必會(huì)拖慢整個(gè)模塊的展示,所以這里我們做了整個(gè)離線包的預(yù)加載
- 首頁列表中每個(gè) ViewHolder 的展示元素我們叫做一個(gè)卡片,目前采取的策略是多個(gè)卡片放在一個(gè) RN 的離線包中,通過同一個(gè) RN 容器來分別展示,避免多個(gè)容器消耗過多的資源。
下面從數(shù)據(jù)流角度拆解整個(gè)方案,整體方案可以分為服務(wù)端數(shù)據(jù)定義和下發(fā),容器數(shù)據(jù)透傳,JS側(cè)數(shù)據(jù)解析三個(gè)主要步驟:
- 服務(wù)端數(shù)據(jù)定義和下發(fā)
由于是服務(wù)端接口驅(qū)動(dòng) RecyclerView 中內(nèi)容展示,接口下發(fā)數(shù)據(jù)中需要有type字段標(biāo)識(shí)使用RN還是Native展示,可以服用Native展示樣式標(biāo)記字段,由于RN中具體展示的樣式和運(yùn)行哪些 JS 代碼直接相關(guān),所以服務(wù)端下發(fā)的數(shù)據(jù)中需要帶上對應(yīng)的 moduleName 和 componentName,整體數(shù)據(jù)結(jié)構(gòu)定義如下:
[ { "type":"rn", "rnInfo":{ "moduleName":"bizDiscovery", "component":"hotSong", "otherInfo":{ } }, "data":{ "songInfo":{ } } }, { "type":"dragonball", "data":{ "showInfo":{ } } }]
獲取到數(shù)據(jù)之后只需要按照 RecyclerView 正常的使用方法將數(shù)據(jù)和不同的 ViewHolder 綁定即可
- 容器數(shù)據(jù)透傳
RN 容器直接直接內(nèi)嵌在 ViewHolder 中,在 viewHolder 中只需要定義承載 RN JS 渲染視圖的 ViewGroup container,RN Bridge 創(chuàng)建好 ReactRootView 后將創(chuàng)建好的 ReactRootView 調(diào)用 add 方法添加到 container 中即可,數(shù)據(jù)傳遞是透傳的方式通過 RN 的 initialProperty 傳入到 JS 側(cè),在 JS 側(cè)解析和使用,數(shù)據(jù)傳遞代碼如下:
mReactRootView?.startReactApplication(reactInstanceManager, componentName, initialProperties)
這里面需要注意的點(diǎn)是,由于所有使用RN展示的卡片都是對應(yīng)的相同的 RecyclerView type 即相同的 ViewHolder,所以在 RecyclerView 復(fù)用時(shí)可能會(huì)出現(xiàn)兩種情況:
1. 只有一個(gè) RN 卡片,上下滑動(dòng) RecyclerView 時(shí)發(fā)生復(fù)用,這時(shí)基本不用處理,
2. 存在兩種不同類型的 RN 卡片,復(fù)用時(shí)會(huì)運(yùn)行完全不同的離線包代碼,這種情況會(huì)導(dǎo)致 JS 側(cè)重新執(zhí)行渲染邏輯生成全新的視圖,上下滾動(dòng)時(shí)如果每次都出現(xiàn) JS 側(cè)重新渲染,會(huì)極大的影響滑動(dòng)時(shí)性能,造成滑動(dòng)卡頓掉幀,針對這種問題我們對 RN 的 ReactRootView 也做了緩存,
整體架構(gòu)如下:
從圖中可以看到 ViewHolder 中的 container 和 RN 的 ReactRootView 是一對多的關(guān)系,RN 的 ReactRootView 在第一次初始化完成后還是掛在 RN 管理的虛擬視圖樹中,在 RecyclerView 滑動(dòng)切換不同的展示類型時(shí)只需要從 ViewHolder 的 container 中移除不展示的ReactRootView 再重新 add 需要展示的 ReactRootView,不需要 JS 側(cè)重新執(zhí)行,重新 add ReactRootView 之后還需要將當(dāng)前的數(shù)據(jù)再傳入 JS 側(cè)以適配相同樣式的卡片展示不同數(shù)據(jù)的需求。這里面的原理是一般情況下我們一個(gè) RN Bridge 只會(huì)創(chuàng)建一個(gè) ReactRootView,但是查看 RN 源碼,RN 其實(shí)支持一個(gè) RN Bridge 綁定多個(gè) RootView 的能力,代碼如下:
public void addRootNode(ReactShadowNode node) { mThreadAsserter.assertNow(); int tag = node.getReactTag(); mTagsToCSSNodes.put(tag, node); mRootTags.put(tag, true); }
一個(gè) ReactRootView 即一棵視圖樹,RN在更新客戶端視圖時(shí)都會(huì)遍歷所有的 ReactRootView,代碼如下:
protected void updateViewHierarchy() { .... try { for (int i = 0; i < mShadowNodeRegistry.getRootNodeCount(); i++) { int tag = mShadowNodeRegistry.getRootTag(i); ReactShadowNode cssRoot = mShadowNodeRegistry.getNode(tag); if (cssRoot.getWidthMeasureSpec() != null && cssRoot.getHeightMeasureSpec() != null) { ... try { notifyOnBeforeLayoutRecursive(cssRoot); } finally { Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); } calculateRootLayout(cssRoot); ... try { applyUpdatesRecursive(cssRoot, 0f, 0f); } finally { } ... } } } finally { Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); } }
所以即使使用多個(gè) ReactRootView RN 的渲染邏輯也可以正常執(zhí)行,這里一個(gè) ReactRootView 即對應(yīng) JS 實(shí)現(xiàn)中的一個(gè) Component,我們在運(yùn)行 RN 業(yè)務(wù)代碼會(huì)看到 startApplication 的實(shí)現(xiàn)在 ReactRootView 中,startApplication 傳入的參數(shù)就是 Component,對應(yīng)代碼如下:
public class ReactRootView extends FrameLayout implements RootView, ReactRoot { public void startReactApplication( ReactInstanceManager reactInstanceManager, String moduleName, @Nullable Bundle initialProperties, @Nullable String initialUITemplate) { ... } }
到此客戶端側(cè)的重點(diǎn)實(shí)現(xiàn)基本完成了,接下來就是JS側(cè)。
- JS 側(cè)寫法變化
JS 側(cè)的對于卡片開發(fā)的寫法和正常的 RN 開發(fā)基本相同,唯一的區(qū)別是需要同時(shí)注冊多個(gè) component,客戶端每個(gè)業(yè)務(wù)卡片啟動(dòng)時(shí)只需要啟動(dòng)對應(yīng)的 Component 即可,代碼示例如下:
AppRegistry.registerComponent('hotTopic', () => EStyleTheme(HotTopic)); AppRegistry.registerComponent('musicCalendar', () => EStyleTheme(MusicCalendar)); AppRegistry.registerComponent('newSong', () => EStyleTheme(NewSong));
- JS 和 Native 通信
至此整個(gè)渲染流程都已經(jīng)介紹完成,卡片已經(jīng)可以正常展示,不過既然RN具有圖靈完備的能力,勢必會(huì)有一些用戶交互導(dǎo)致的UI變化,比如點(diǎn)擊卡片上的 ”叉“ 的不感興趣操作,點(diǎn)擊后需要通知客戶端彈出客戶端的不感興趣組件,多個(gè)卡片對應(yīng)同一個(gè) JS 引擎,JS 和 Native 的通信通道也是復(fù)用的,怎么決定由哪個(gè)卡片來彈出呢,我們的做法是在卡片第一次渲染時(shí)就使用時(shí)間戳的哈希值生成唯一的 key,將這個(gè) key 作為 Native 側(cè)和 JS 側(cè)區(qū)分不同業(yè)務(wù)的唯一標(biāo)識(shí),和具體展示的業(yè)務(wù)卡片關(guān)聯(lián)起來在雙側(cè)都存儲(chǔ)起來,這樣后續(xù)每次通信時(shí)雙側(cè)就可以通過 key 來確認(rèn)通信的對象,確保不會(huì)導(dǎo)致通信混亂。
- RN 引擎預(yù)熱
在整個(gè) RN 的執(zhí)行周期中離線包加載一般也會(huì)消耗比較多的時(shí)間,所以為了盡可能的提升性能,我們還對頁面卡片對應(yīng)的整個(gè)離線包進(jìn)行了預(yù)熱,即提前將離線包加載到內(nèi)存中并準(zhǔn)備好業(yè)務(wù)邏輯的運(yùn)行時(shí)環(huán)境,預(yù)熱只需要?jiǎng)?chuàng)建好 ReactInstanceManager 并調(diào)用createReactContextInBackground() 即可,調(diào)用后整個(gè)離線包會(huì)被交給 JS 引擎進(jìn)行預(yù)處理,代碼如下:
ReactInstanceManager.builder() .setApplication(ApplicationWrapper.getInstance()) .setJSMainModulePath("index.android") .addPackage(MainReactPackage()) ... .build() .createReactContextInBackground()
這里還需要注意的一個(gè)點(diǎn)是代碼調(diào)試能力,采用內(nèi)嵌的方式如果原來頁面已經(jīng)有搖一搖這種手勢, RN 原生的調(diào)試菜單會(huì)無法呼出,這里需要增加額外的交互方式來解決,我們在卡片上增加了一個(gè)懸浮按鈕。
到此整體框架就都已介紹完畢,在框架之外內(nèi)存占用和合理的異常處理也是需要考慮的重點(diǎn)。
內(nèi)存
在整體技術(shù)實(shí)現(xiàn)之外,我們另外關(guān)注的一個(gè)重點(diǎn)就是內(nèi)存占用,我們對以RN Bridge為核心的RN容器內(nèi)存占用進(jìn)行了統(tǒng)計(jì),使用Profiler工具獲取數(shù)據(jù)如下:
無RN容器(native/java) | 1 RN容器(native/java) | 2 RN容器(native/java) | 3 RN容器(native/java) | 5 RN容器(native/java) | |
---|---|---|---|---|---|
紅米k30pro 6G | 148/54.6 | 154/56 | 157/55.7 | 153/56.7 | 208/59.8 |
谷歌Pixel 2XL 4G | 137.8/60 | 163/73 | 176/83 | 186/91 | 196/101 |
紅米k30 8G | 118/52 | 143/56 | 136/55 | 138/56 | 142/60 |
整體看來在5個(gè)以內(nèi)RN容器的情況整體內(nèi)存并沒有增加很多,內(nèi)存占用整體在可控狀態(tài),由于此方案采用了一個(gè) RN Bridge 對應(yīng)多個(gè)卡片的方式,所以相當(dāng)于只新增一個(gè)Bridge,對內(nèi)存影響較小,實(shí)際線上運(yùn)行也沒有新增 OOM 問題。
異常處理
- 出現(xiàn)異常如何處理
不管是 JS 寫法原因還是 ReactNative 本身的穩(wěn)定性原因,總有一定概率會(huì)有異常出現(xiàn),這時(shí)需要合理的邏輯處理保證功能和用戶體驗(yàn)不會(huì)受到比較大的影響,我們當(dāng)前的處理策略是異常監(jiān)聽還是使用 NativeExceptionHandler 來監(jiān)聽 SoftException 和 FatalException,異常時(shí)在統(tǒng)一的回調(diào)中通知上層業(yè)務(wù)(recyclerView 層),然后根據(jù)具體的業(yè)務(wù)情況,由業(yè)務(wù)層統(tǒng)一消除或者重建 RN 容器,保證體驗(yàn)不受影響或者影響較小,以云音樂首頁使用場景為例目前卡片總 PV 約 1 億,錯(cuò)誤率不到萬分之一,整體運(yùn)行情況穩(wěn)定,無相關(guān)用戶反饋。
- RN版本升級導(dǎo)致和數(shù)據(jù)不兼容如何處理
RN 使用離線包策略,為保證用戶能正常獲取到離線包和保證離線包能快速高效的更新,我們采取了兜底包集成、更新信息服務(wù)端接口搭車等策略,不過受限于用戶的機(jī)型地區(qū)、網(wǎng)絡(luò)狀態(tài)等原因還是存在一定概率的更新不成功,對于這種情況我們將當(dāng)前 RN 離線包支持的卡片信息保存在離線包的配置文件中,通過離線包獲取的接口暴露給業(yè)務(wù)方,業(yè)務(wù)在運(yùn)行離線包前可以根據(jù)配置信息對網(wǎng)絡(luò)請求結(jié)果進(jìn)行過濾,保證新版數(shù)據(jù)匹配舊版的離線包時(shí)不會(huì)導(dǎo)致異常。
未來規(guī)劃
短期內(nèi)我們希望將 RN 動(dòng)態(tài)列表方案結(jié)合我們已有的 RN 低代碼能力,實(shí)現(xiàn)首頁運(yùn)營動(dòng)態(tài)搭建發(fā)布,另一方面主要在性能提升,我們目前還是使用的 RN 0.60.5 版本,JS 的執(zhí)行效率和當(dāng)前版本的多線程框架是我們的最大的瓶頸,之后我們會(huì)在新架構(gòu)上進(jìn)行更多的嘗試。
以上就是React Native 的動(dòng)態(tài)列表方案探索詳解的詳細(xì)內(nèi)容,更多關(guān)于React Native 動(dòng)態(tài)列表的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
react中使用better-scroll滾動(dòng)插件的實(shí)現(xiàn)示例
滾動(dòng)在很多地方都可以使用,本文主要介紹了react中使用better-scroll滾動(dòng)插件的實(shí)現(xiàn)示例,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-07-07react antd checkbox實(shí)現(xiàn)全選、多選功能
目前好像只有table組件有實(shí)現(xiàn)表格數(shù)據(jù)的全選功能,如果說對于list,card,collapse等其他組件來說,需要自己結(jié)合checkbox來手動(dòng)實(shí)現(xiàn)全選功能,這篇文章主要介紹了react antd checkbox實(shí)現(xiàn)全選、多選功能,需要的朋友可以參考下2024-07-07在react項(xiàng)目中使用antd的form組件,動(dòng)態(tài)設(shè)置input框的值
這篇文章主要介紹了在react項(xiàng)目中使用antd的form組件,動(dòng)態(tài)設(shè)置input框的值,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-10-10react-router v4如何使用history控制路由跳轉(zhuǎn)詳解
這篇文章主要給大家介紹了關(guān)于react-router v4如何使用history控制路由跳轉(zhuǎn)的相關(guān)資料,文中通過示例代碼介紹的的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧。2018-01-01React網(wǎng)絡(luò)請求發(fā)起方法詳細(xì)介紹
在編程開發(fā)中,網(wǎng)絡(luò)數(shù)據(jù)請求是必不可少的,這篇文章主要介紹了React網(wǎng)絡(luò)請求發(fā)起方法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)吧2022-09-09React-native橋接Android原生開發(fā)詳解
本篇文章主要介紹了React-native橋接Android原生開發(fā)詳解,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-01-01React項(xiàng)目中className運(yùn)用及問題解決
這篇文章主要為大家介紹了React項(xiàng)目中className運(yùn)用及問題解決,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12React中常見的動(dòng)畫實(shí)現(xiàn)的幾種方式
本篇文章主要介紹了React中常見的動(dòng)畫實(shí)現(xiàn)的幾種方式,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-01-01