演講實錄——React-native技術在騰訊課堂中的實踐及優化 [復制鏈接]

2019-5-13 10:10
九霄逆鱗 閱讀:199 評論:0 贊:1
Tag:  

本文來自2019安卓巴士開發者大會現場實錄,由于錄入匆忙,內容可能存在偏差,歡迎大家掃描文末二維碼查看現場實錄視頻和下載大會完整PPT。演講人:王華杰

大家好!我叫王華杰,來自騰訊在線教育部,主要做騰訊課堂app的開發工作,這次分享的題目是RN技術在騰訊課堂中的實踐和優化。


首先我們看一下RN在騰訊課堂的使用情況。騰訊課堂App首頁共有4個TAG,其中“首頁”和“分類”兩個頁面是采用RN技術開發,一開始騰訊課堂首頁TAG是native代碼實現,后來才換成RN技術來實現,那么為什么騰訊課堂會在首頁這種重要場景選擇用RN技術來實現呢?



首先我們來看一下移動開發中的技術選型,在RN出來之前,移動開發基本有兩種技術方案,一種是H5方案,在app中嵌入H5的頁面,一種是直接采用native代碼寫的頁面,這兩種方案各有優點和缺點。H5實現的頁面呢,他具有開發效率非常高,而且是跨平臺支持的,H5寫一套頁面可以同時跑在Android和iOS上,還有重要的優點就是可動態發布,不需要跟隨app的版本,但是他的缺點就是,體驗相對差了點,性能也比原生的頁面差,調用原生能力也比較差。而native技術開發的頁面呢,開發效率比較低,而且跨平臺比較難,基本是android和iOS各寫一套代碼。動態更新也很困難,一般都是要通過發版本來解決,而且發版本后新版本的普及也需要很長時間的。但是采用native技術開發的頁面也有他的好處,首先他體驗好,性能高,調用原生能力也非常強。



那么有沒有一種解決方案能夠把兩者的優勢綜合在一起呢?答案是有的,那就是RN。RN是采用javascript語言來寫具有原生體驗app一種手段,他的基本原理就是,用js來描述UI邏輯,渲染則是使用Native的view來渲染上屏,這樣他就綜合了H5和native的優點,具有開發效率高,可跨平臺和動態發布,而且高性能體驗好。騰訊課堂是怎么引入RN的呢。



騰訊課堂對RN有一個很大的拓展和改造,原生的RN針對的業務場景比較單一,一般bundle是打包成一個包,穩定性和性能不夠好。啟動速度相對native有比較大的差距,官方的組件API比較少,整體不夠優秀。我們在原生基礎上做了拓展和改造,首先是對多業務場景做了優化,還有它的穩定性,我們解決了大部分穩定性問題,而且提高了頁面的加載速度。功能上支持了業務的分包加載,還支持bundle發布管理,還有動態更新,還定制了很多高性能的組件和API。



整體架構,主要分三部分:工具部分包括代碼的和打包分包工具。Native能力部分我們會提供很多高性能的API和組件,API有存儲、下載等。發布部分有發布系統,會做一個離線包的管理。還有差異更新,出現異常的監控和降級。還有性能監控。


騰訊課堂中除了有RN技術實現的頁面,也有很多采用H5技術實現的頁面,但是有些native能力需要同時向RN和H5提供,由于兩種方案的技術差異,會造成兩種提供方式,就是我們需要為h5寫一套native-api,同時也為RN寫一套native api。H5中的,js調用native能力的時候通過h5的bridge調用native-api, RN中則是通過rn的bridge來來源native-api,那么怎么把同一份native-api同時提供給H5和RN呢?



先看一下H5的bridge實現,我們打開一個web頁面時,在開發者選項里可以看到有很多網絡請求,而這些請求webview都是能攔截到的,那么我們基于這種方式來實現調用native的能力,就是js發起一個網絡請求,h5bridge作為協議頭,后面帶有module,method這些參數,webview判斷到這個請求是h5bridge協議的,就把這個請求攔截到并解析協議參數然后去調用native的api。



那么RN的bridge呢,在RN中帶調用native的通道已經是有了,像UIManager.createView這些都是調用native的api,目前RN支持的參數類型有:整形,浮點型,字符串,數組,map,function,但是對function的支持有一些局限性:數組和map里面不支持function類型,function僅能在參數列表的末尾,最多兩個(就是成功和失敗的回調),而且function只能執行1次。



那么提供給H5和RN的接口怎么統一呢?假如js需要調用native的存儲接口,js調用native時,H5調用走h5brige, RN中,我們寫了個NativeBridge的RN module他,通過這個module來調用。這個NaitiveBridge的調用會轉換為h5的h5bridge://Storage/get/…或者RN的NativeBridgeModule.call(“Storage”, “get”,  …),就是把模塊名字,方法名字和參數傳到native進行分發到對應的native-api里處理



傳入一個大參數一個map, 有key和一個success的回調,這樣api格式很像小程序的api, 比較直觀優美。但是這種調用在原生RN上是不能支持的,因為RN的調用不能再map里嵌套函數



那么騰訊課堂怎么支持這個功能的呢?首先參數轉換成:NativeModules.NativeBridge.call(“Storage”, “get”,  “[…]”),“Storage”作為模塊名,get作為方法名傳入,還有一個為參數列表的字符串。參數怎么轉換呢?我們隊參數列表進行轉成json字符串,對函數類型做了特殊處理就是把function用一個自增的id替換,并把這個id和function對象關聯起來,最終的調用會轉化為NativeModules.NativeBridge.call("Storage", "get",  "[{"key": "key", "success": 1}]"),回調的時候只需要把funcId傳回來就可以了,根據funcId就可以找到對應的function對象,然后執行。在native這個module怎么定義呢?1、繼承ExportedModule,并把Module注冊到h5或者RN中bridge中,2、使用Exported注解導出方法,3、這個模塊方法即可同時暴露給H5與RN。經過這種方式的統一,我們解決了RN bridge的局限性,我們可以再object和array中潛入function,可以對function進行多次回調。 



接下來講一下分包和模塊化的加載。首先我們來看一下騰訊課堂的首頁加載耗時分布,從圖中可以看出 js inti和bundle加載時耗時比較多,因為騰訊整個的bundle接近2M,所以加載比較耗時,因為它有很多業務,所以它的bundle會比較大。其中RN框架至少有500多K。多業務場景下bundle冗余,比如說我加載首頁,那么沒必要把其他頁面的js也加載進來的,基礎bundle基本不更新,業務bundle體積小,易動態更新,所有分包還是有必要的。



主要的思路就是把bundle中的業務代碼和框架代碼分離。這樣就可以在app啟動時先預加載框架的js代碼,打開頁面時再加載對應頁面的js代碼,提高頁面的加載速度。



先看一下, RN目前的打包方式,RN支持有兩種打包方式,普通的bundle打包方式,就是把所有的js模塊輸出到一個js bundle文件里面,加載時完整加載整個bundle文件,還有一種打包方式就是unbudle, unbundle又分兩種,一種是assetde unbunlde,一種是asset file的bunblde, 每個模塊是一個文本文件,打包的產物中有幾百個文件,這個是android的unbundle打包方式,因為ios在大量小文件時有io瓶頸,所有ios 采用的indexd-file的unbundle打包方式, 將所有模塊輸出到一個帶索引表的二進制文件,文件頭中記錄各個模塊在文件中的相對位置,加載時按模塊加載。



使用普通bundle打包之后Bundle文件的結構如下,也主要包含3部分


頭部:全局定義,主要是define,require等全局模塊的定義

中間:模塊定義,RN框架和業務的各個模塊定義

尾部:引擎初始化和入口函數執行。



業界主要有兩種分包方案,一種是基于RN bundle打包方式,將bundle文件拆分成2部分(框架部分+業務模塊部分),目前主流拆包方式,按需加載粒度:業務包。另一種是基于RN unbundle assets的打包方式,將bundle文件拆分成各模塊部分,可實現按需加載, 按需加載粒度:模塊


騰訊課堂中RN的分包方案是怎么樣的呢?基于RN unbundle 帶索引表的打包方式,將bundle文件拆分成2部分(框架部分+業務模塊部分),可實現按需加載,按需加載粒度:模塊。



基于unbundle index-file方案進行分包。分包前的bundle結構,包含:模塊配置表,啟動代碼,和很多模塊。模塊配置表中會記錄每個模塊在文件中的偏移量和大小。那么怎么分包呢,分包就是把bundle文件中業務模塊抽離出來成為業務bundle,剩下的為主bundle, 主bundle的模塊配置表記錄了主bundle的模塊信息和啟動代碼,業務bundle的模塊配置表記錄業務模塊的信息。這里需要注意的一點是,由于分包后可以單獨打包,模塊id不能采用原來的自增id, 主要是為了避免id沖突,所以需要修改id的生成方式,比如直接用模塊的路徑作為id。



分包后的bundle怎么加載呢?首先加載所有bundle的模塊配置表,這樣就支持每個模塊在哪個文件的哪個位置了,接下來加載啟動代碼運行,運行過程中會有require加載模塊,如果這個模塊沒有加載,會走到nativeRequire,根據moduleid從模塊配置表里找到對應的模塊在文件中的位置然后加載對應的模塊。



經過分包之后,騰訊課堂的首頁從983kb能夠降到578kb左右。課堂的分類從983kb降到400多kb。首頁可以從1.2秒減少到800毫秒。經過分包,對bundle的加載速度是有很大提升的。



接下來看一下發布和動態更新。代碼在git倉庫中管理,打包系統把業務代碼打包后上傳到AK離線發布系統,根據前面發布的版本,生成一個差量包。App在檢查更新的時候會帶上當前bundle的版本號,發布系統會對版本號進行匹配,找到對應的差量包給app更新,匹配不到差量包就使用全量包更新。



如何做到及時刷新?進入閃屏時會檢查是否有更新,為了不耽誤閃屏跳轉,最多只等檢查更新1s中,如果有更新的話,把等待時間延長到3s中,下載更新包,然后閃屏跳轉。然后看看本地的bundle文件是否已經更新了,如果更新了,就重新創建RNContext如果沒有更新,則直接加載RN頁面,在app切到后臺時,會觸發檢查更新,當然這里會有頻率限制。



對于異常處理和降級是怎么處理的。目前騰訊課堂一開始發現很多crash,基本上是發生在RN源碼里面,解決不了會做try-catch,會做bundle的版本回退,或者降級到h5的頁面。bundle加載過程中的Crash,我們會把這個異常try-catch,再進行處理。如果是業務加載過程中出現的JS異常的話,我們都可以做上報。有一些native模塊的報錯。還有比較常見的一個是JNI層的so加載報錯,主要是安卓的so路徑可能不對,可以從APK里面重新取它的so然后加載。還有React View層報錯, 通過try-catch,可能會有一些功能上的不正常。可以判斷出當前用戶的異常次數,如果異常短期之內超過3次之后會直接降到H5。如果發現某個機型有異常,我們可以對某個機型進行降級到H5。



經過異常處理,騰訊課堂的Crash率從0.1%降到0.01%,已經非常低了。


講一下未來的探索。因為RN現在的性能還是有待提高的,像一些高性能List和啟動速度都是可以提高的。現在RN在做框架的重構,性能上有很大的提高。穩定性,RN的款方Crash率比較高。目前騰訊課堂首頁有H5版本,有RN版本,兩套代碼,我們希望以后只需要一套代碼就可以支持RN和H5。目前Flutter采用dart語言開發,對于習慣了js開發的前端開發者來說還不夠友好,所以可以探索RN的Flutter模式。



謝謝大家!




現場PPT分享:

      關注【安卓巴士Android開發者門戶】公眾號,后臺回復“420”獲取講師完整PPT。


大會現場視頻小程序:



歡迎前往安卓巴士博客區投稿,技術成長于分享

期待巴友留言,共同探討學習


分享到:
我來說兩句
facelist
您需要登錄后才可以評論 登錄 | 立即注冊
所有評論(0)

領先的中文移動開發者社區
18620764416
7*24全天服務
意見反饋:[email protected]

掃一掃關注我們

Powered by Discuz! X3.2© 2001-2019 Comsenz Inc.( 粵ICP備15117877號 )

两码中特期期