企鵝電競從17年6月接入weex,到如今已經有一年半的時間,這段時間裏面,針對遇到的問題,企鵝電競終端主要作了下面的優化:vue
image組件java
預加載node
預渲染android
weex的list組件和image組件很是容易出問題,企鵝電競自己又存在不少無限列表的weex頁面,list和image的組合爆發的內存問題,致使接入weex後app的內存問題致使的crash一直居高不下。web
首先來講一下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去切換。
相信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框架圖: ]
提供給前端的sdk,對vue的dom操做作了各類封裝,JSFrameWork單獨打包到apk包中。
使用與safari的JavaScript引擎,專門處理JavaScript的虛擬機,對應chrome的v8,功能能夠大致聯想成java的jvm。
weex core的server端,封裝了對JavaScripteCore的調用,封裝了instance的沙盒,多進程實現中,JSS和JavaScriptCore的執行在另外的進程,防止JS執行異常致使主進程崩潰。
weex core的client端,做爲WeexFrameWork和JSS橋接層,另外從0.18版本開始,cssLayout也下沉到了這一層。
提供各類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