跨越適配&性能那道坎,企鵝電競Android weex優化

做者:龍泉,騰訊企鵝電競工程師css

商業轉載請聯繫騰訊WeTest得到受權,非商業轉載請註明出處。html

原文連接:wetest.qq.com/lab/view/44…前端




WeTest 導讀

企鵝電競從17年6月接入weex,到如今已經有一年半的時間,這段時間裏面,針對遇到的問題,企鵝電競終端主要作了下面的優化:vue

  • image組件java

  • 預加載node

  • 預渲染android


Image組件

weex的list組件和image組件很是容易出問題,企鵝電競自己又存在不少無限列表的weex頁面,list和image的組合爆發的內存問題,致使接入weex後app的內存問題致使的crash一直居高不下。web


list組件問題

首先來講一下list,list對應的實現是WXListComponent,對應的view是BounceRecyclerView。RecyclerView應該你們都很熟悉,android support庫裏面提供的高性能的替代ListView的控件,它的存在就是爲了列表中元素複用。原本weex使用了RecyclerView做爲list的實現,是一件皆大歡喜的事情,可是RecyclerView中有一種使用不當的狀況,會致使view不可複用。chrome


下圖描述了RecyclerView的複用流程:apache


[ RecyclerView複用 ]


weex中的RecyclerView並無設置stableId,因此RecyclerView的全部複用都依賴於ViewHolder的ViewType,Weex的ViewType生成見下圖:


private int generateViewType(WXComponent component) {   long id;   try {     id = Integer.parseInt(component.getRef());     String type = component.getAttrs().getScope();     if (!TextUtils.isEmpty(type)) {       if (mRefToViewType == null) {         mRefToViewType = new ArrayMap<>();       }       if (!mRefToViewType.containsKey(type)) {         mRefToViewType.put(type, id);       }       id = mRefToViewType.get(type);     }   } catch (RuntimeException e) {     WXLogUtils.eTag(TAG, e);     id = RecyclerView.NO_ID;     WXLogUtils.e(TAG, "getItemViewType: NO ID, this will crash the whole render system of WXListRecyclerView");   }   return (int) id; }複製代碼


在沒有設置scope的狀況下,viewHolder的component的ref就是viewType,即全部的ViewHolder都是不一樣且不可複用的,此時的RecyclerView也就退化成了一個稍微複雜一點的ScrollView。


若是設置了scope屬性,但你絕對想不到,scope自己也是一個坑。下面直接上代碼:


// BasicListComponent.onBindViewHolder() public void onBindViewHolder(final ListBaseViewHolder holder, int position) {  ...     if (holder.getComponent() != null && holder.getComponent() instanceof WXCell) {     if(holder.isRecycled()) {       holder.bindData(component);       component.onRenderFinish(STATE_UI_FINISH);     }     ...   } } // ListBaseViewHolder.bindData() public void bindData(WXComponent component) {   if (mComponent != null && mComponent.get() != null) {     mComponent.get().bindData(component);     isRecycled = false;   } }複製代碼


上面代碼中,能夠看到,使用了scope,當複用Holder時,會把須要展現的component的數據綁定到複用的component中。那麼問題來了,若是我不是隻是想修改部分屬性,而是須要改變component的層級關係呢?例如從a->b->c修改爲a->c->b,那麼是否是隻能用不一樣的viewType或者是說變成下面的結構:a->b a->c b->b1 b->c1 c->c2 c->b2這樣的結構,可是view的實例多了,必然又會致使內存等各類問題。最爲致命的問題是,createViewHolder的時候,傳給ViewHolder的component實例就是原件,而非拷貝,當bindData執行了之後,就等用於你複用的那個component的數據被修改了,當你再滑回去的時候,GG。


因此scope屬性基本不可用,留給咱們的只有至關於scrollView的list。


還好,爲了解決list這麼戳的性能,有了recyclerList,從vue的語法層,支持了模板的複用。可是坑爹的是,0.17 、 0.18 版本recyclerList都有這樣那樣的問題,重構同窗以爲使用起來效率較低。0.19版本weex團隊fix了這些問題後,企鵝電競的前端同窗也正在嘗試往recyclerList去切換。



image組件問題


相信android開發們都清楚,圖片的問題永遠是大問題。OOM、GC等性能問題,常常就是伴隨着圖片操做。


在0.17版本之前,WXImageView中bitmap的釋放都是在component的recycle中執行,0.17版本以後,在detach時也會執行recycle,可是WXImageView的recycle只是把ImageView的drawable設置爲null,並無實際調用bitmap的recycle。


而企鵝電競在版本運行過程當中發現,僅僅把bitmapDrawable設置爲null,不去調用bitmap的recycle,部分機型上面的oom問題很是突出(這裏一直沒想明白,爲啥這部分機型會出現這個問題,後面替換成fresco去管理就沒這個問題了)。固然,若是直接recycle bitmap,不設置bitmapDrawable,會直接致使crash。


回到企鵝電競自己,企鵝電競中的圖片管理使用了fresco,在接入weex之前,咱們已經針對fresco加載圖片作了一系列優化,並且fresco自己已經包含了三級緩存等功能。


接入weex後,首先想到的就是使用fresco的管線加載出bitmap後給WXImage使用。在這個過程當中,先是遇到了對CloseableReference管理不恰當致使bitmap 還在使用卻被recycle 掉了,而後又遇到了沒有執行recycle致使bitmap沒法釋放的坑。在長列表中,圖片沒法釋放的問題被無限放大,常常出現快速滑動幾屏就oom的問題。並且隨着業務發展使用WXImage沒法播放gif和webp圖片也成爲瓶頸。


後續版本中,企鵝電競直接重寫了image和img標籤,使用Fresco的SimpleDraweeView替換了ImageView。該方案帶來的收益是bitmap不在須要本身管理,即oom問題和bitmap recycle以後致使的crash問題會大大減小,且fresco默認就支持gif和webp圖片。可是,這個方案也有個致命的問題:圓角。


圓角問題得先從fresco和weex各自的圓角方案提及。


weex圓角(盒模型-border):https://weex.apache.org/cn/wiki/common-styles.html#shi-li

fresco圓角:https://www.fresco-cn.org/docs/rounded-corners-and-circles.html


fresco圓角方案具體可見RoundedBitmapDrawable,RoundedColorDrawable,RoundedCornersDrawable這3個類,fresco圓角屬性的改變最終都只是修改這3個類的屬性,圓角也是基於draw時候修改canvas畫布內容實現,BtimapDrawable的裁減以及邊框的繪製都是在draw的時候繪製上去。


weex圓角方案具體可見ImageDrawable,實現方案爲藉助android的PaintDrawable,經過設置shader實現bitmapDrawable的裁減,可是邊框的繪製則依賴於backgroundDrawable。


並且在fresco中,封裝了多層的drawable,較難修改drawabl的 draw的邏輯,並且邊框參數的設置也不如weex衆多樣化。


針對二者的差別性,企鵝電競的解決方案是放棄fresco的圓角方案,經過fresco的後處理器裁減bitmap達到圓角的效果,邊框複用weex的background的方案。這個方案惟一的問題後處理器中必須建立一份新的bitmap,可是經過複用fresco的bitmapPool,並不會致使內存有過多的問題。


下面貼一下後處理器處理圓角的關鍵代碼:

public CloseableReference<Bitmap> process(Bitmap sourceBitmap, PlatformBitmapFactory bitmapFactory) {       CloseableReference<Bitmap> bitmapRef = null;       try {           if (mInnerImageView instanceof FrescoImageView && sourceBitmap != null && !sourceBitmap.isRecycled()                 && sourceBitmap.getWidth() > 0 && sourceBitmap.getHeight() > 0) {               ...               // 解決Bitmap繪製尺寸上限問題,好比:Bitmap too large to be uploaded into a texture (1302x9325, max=8192x8192)               int maxSize = EGLUtil.getGLESTextureLimit();               int resizeWidth = mWidth;               int resizeHeight = mHeight;               float ratio = 0;               if (maxSize > 0 && (mWidth > maxSize || mHeight > maxSize)) {                   ratio = Math.max((float) mWidth / maxSize, (float) mHeight / maxSize);                   resizeWidth = (int) (mWidth / ratio);                   resizeHeight = (int) (mHeight / ratio);               }               float[] borderRadius = ((FrescoImageView) mInnerImageView).getBorderRadius();               if (checkBorderRadiusValid(borderRadius)) {                   Drawable imageDrawable = ImageDrawable.createImageDrawable(sourceBitmap, mInnerImageView.getScaleType(), borderRadius, resizeWidth, resizeHeight, false);                   imageDrawable.setBounds(0, 0, resizeWidth, resizeHeight);                   CloseableReference<Bitmap> tmpBitmapRef = bitmapFactory.createBitmap(resizeWidth, resizeHeight, sourceBitmap.getConfig());                   Canvas canvas = new Canvas(tmpBitmapRef.get());                   imageDrawable.draw(canvas);                   bitmapRef = tmpBitmapRef;               } else if (ratio != 0) {                   bitmapRef = bitmapFactory.createBitmap(sourceBitmap, 0, 0, resizeWidth, resizeHeight, sourceBitmap.getConfig());               }           }           if (bitmapRef == null) {               bitmapRef = bitmapFactory.createBitmap(sourceBitmap);           }       } catch (Throwable e) {           WeexLog.e(TAG, "process image error:" + e.toString());       }       return bitmapRef;   }複製代碼


當list和image組合在一塊兒的時候,因爲weex的image並無recycle掉bitmap,並且沒有bitmapPool的使用,會致使長列表weex頁面佔用內存特別高。而替換爲fresco的bitmap內存管理模式後,因爲weex致使的內存crash問題佔比明顯從最開始版本的2%降低到了0.1%-0.2%。



預加載

當踩完大大小小的坑,緩解了內存和crash問題以後,企鵝電競在weex使用上又遇到了2大難題:


1. 調試困難

2. 頁面加載慢

調試困難

weex的頁面並不能給前端的開發同窗絲滑的調試體驗。最開始前端同窗是採用終端日誌或者彈框的方式調試(心疼前端同窗就這麼學會了看android日誌),後面經過再三跟weex團隊的溝通,終於肯定了weex和weex_debuger對應的版本,前端同窗能夠在chrome上面調試weex頁面。


然而weex_deubgger並非完美的解決方案,weex自己是jscore內核,而weex_debugger只是經過chrome調試協議開了個服務,等同於使用的是chrome的內核,內核的不一致性沒法保證調試的準確性。連weex的開發同窗本身都說了會遇到debug環境和正式環境結果不一致的狀況。


解決方案也很簡單,那就是能夠在mac的xcode和safari上面調試。當時因爲替換mac的成功太高,就將就使用了weex_debugger的方案,後面怎麼解決了相信你們內心有數。


頁面加載速度慢

隨着企鵝電競業務的發展,很快前端同窗就反饋過來,怎麼weex頁面打開的速度這麼慢,這個菊花轉了這麼久。當時的心裏是崩潰的,明明接入的時候好好的,一個頁面輕輕鬆鬆500-600ms就加載回來了,哪裏會有問題?


業務的發展速度永遠是你想象不到的,2個版本不到的時間,企鵝電競中的weex頁面輕輕鬆鬆從個位數突破到兩位數,bundle大小也輕輕鬆鬆從幾十kb突破到了上百kb,由此帶來的問題是打開weex頁面後能明顯看到菊花轉動了,甚至打開速度上還不如直出的web頁面。


首先從數據報表中發現,頁面打開速度中,1s中有300-400ms是bundle從網絡下載的時間,那是否是把這段時間省了,頁面有輕輕鬆鬆回到毫秒級別打開速度了。


下圖展現了預加載的總體流程。



[ 預加載流程 ]


預加載方案上線後,頁面成功節省了將近200ms的耗時。20M的LRUCache大小也是參考了http cache的默認大小值,頁面打開的預加載率在75%-80%。



預渲染


作了預加載以後,很快又發現,就算沒有網絡請求,頁面打開耗時仍是超過了1s。這種狀況下,現有的方案已經沒法繼續優化頁面。這個時候忽然有了個想法,weex自己是把前端的虛擬dom轉化爲終端的各類view控件,那麼爲何weex頁面的打開會慢終端頁面打開這麼多呢?


定義問題

解決問題以前,先來定義一下問題具體是什麼。針對渲染速度慢,企鵝電競對weex渲染的耗時定義以下:


· renderStart = 調用WXSdkInstance.render()的時間點

· httpFinish = httpAdapter請求回來以後調用WXSdkInstance.onHttpFinish()的時間點

· renderFinish = 回調 IWXRenderListener.onRenderSuccess()的時間點

· 頁面打開耗時 = renderFinish - renderStart

· 網絡耗時 = httpFinish - renderStart

· 渲染耗時 = renderFinish - httpFinish


因此以前的預加載,已經優化了網絡耗時,可是渲染耗時在頁面大了以後,依舊會有很大的性能問題。


爲了揭開這個問題的本質,先來看一下weex總體的框架:

[ weex框架圖: ]

JSFrameWork

提供給前端的sdk,對vue的dom操做作了各類封裝,JSFrameWork單獨打包到apk包中。

JavaScriptCore

使用與safari的JavaScript引擎,專門處理JavaScript的虛擬機,對應chrome的v8,功能能夠大致聯想成java的jvm。

JSS

weex core的server端,封裝了對JavaScripteCore的調用,封裝了instance的沙盒,多進程實現中,JSS和JavaScriptCore的執行在另外的進程,防止JS執行異常致使主進程崩潰。

JSC

weex core的client端,做爲WeexFrameWork和JSS橋接層,另外從0.18版本開始,cssLayout也下沉到了這一層。

WeexFrameWork

提供各類sdk接口的java調用,虛擬dom和Android控件樹的轉換,控件管理等。


瞭解完了weex框架,再把關注點轉移到js build以後生成的jsBundle,細心的同窗確定可以發現,生成的jsBundle本質上就是一個js方法,因此weex頁面render的過程本質上是執行一個js方法。


針對企鵝電競關注的遊戲首頁,對整個weex框架加了完整的打點,看到在nexus 6上面,對應的耗時以及總體流程以下圖:


[ weex執行流程以及耗時 ]


能夠看到性能的熱點主要在執行js方法以及虛擬dom的執行這兩個關鍵步驟上,根據打點來看,單個js方法和單個虛擬dom的執行,耗時都很低。企鵝電競抓了屢次打點,看到啓動時候執行js最慢的也僅僅是3ms,大多數執行都在0.1ms - 0 ms這個區間。可是,再快的執行耗時,也架不住量多,一樣以企鵝電競遊戲首頁爲例,啓動的時候該頁面執行的js方法多大2000+個,這2000+個方法執行再加上方法調度的耗時,能成爲性能熱點一點也不意外。而虛擬dom的執行也同理,單次執行通過weex團隊的優化,執行耗時基本在1ms-3ms之間,可是一樣的架不住量多以及線程調度的時間問題。


預渲染方案

瞭解RN的同窗應該也知道,js方法的執行和虛擬dom的執行是這種框架的核心所在,想要撬動整個核心,基本上難度等同於重寫一個了。那麼剩下的方案也就只有一個:提早渲染。

[ 預渲染 ]


預渲染的方案修改了WeexFrameWork虛擬dom和Android控件樹轉換的部分,在預渲染時,不生成真正的component和view結構,用抽象出來的ComponentNode存儲虛擬dom的操做,並在RealRender的時候將node轉換成一個個component以及View。


這個方案的基本原理就是典型的以提早消費的空間換取時間,不去轉換真正的component和View緣由是view在不一樣context中的不可複用性以及view自己會佔用大部份內存。



預渲染優化數據


內存消耗

提早渲染必然致使類內存的提早消耗,在huawei nove3上測試獲得,預渲染遊戲首頁時的峯值內存會去到10M,可是在最後預渲染完成後GC會釋放這部份內存,最終常駐內存爲0.3M。 真正渲染遊戲首頁的內存峯值會去到20M,最後的常駐內存爲5.6M。


能夠看到預渲染對常駐內存的消耗極少,可是因爲虛擬dom執行,致使峯值內存偏高,在某些內存敏感場景下,仍是會有必定風險。


頁面打開耗時

實驗室中游戲首頁的正常加載數據爲900ms(已經預加載,無網絡耗時),通過預渲染,頁面打開僅須要150ms。


現網數據:


[ 預渲染頁面打開上報 ]


最後,來兩張優化先後的對比圖:


[ 預渲染: ]

[ 非預渲染: ]

「深度兼容測試」現已對外,騰訊專家爲您定製自動化測試腳本,覆蓋應用核心場景,對上百款主流機型進行適配兼容測試,提供詳細測試報告


另有客戶端性能測試,一網打盡FPS、CPU等基礎性能數據,詳細展現各種渲染數據,極速定位性能問題。


點擊:https://wetest.qq.com/cloud/deepcompatibilitytesting 便可體驗。


若是使用當中有任何疑問,歡迎聯繫騰訊WeTest企業QQ:2852350015

相關文章
相關標籤/搜索