weex android 性能,跨越适配性能那道坎,企鹅电竞Android weex优化
作者:龍泉,騰訊企鵝電競工程師
商業轉載請聯系騰訊WeTest獲得授權,非商業轉載請注明出處。
WeTest 導讀
企鵝電競從17年6月接入weex,到現在已經有一年半的時間,這段時間里面,針對遇到的問題,企鵝電競終端主要做了下面的優化:
image組件
預加載
預渲染
_
Image組件
weex的list組件和image組件非常容易出問題,企鵝電競本身又存在很多無限列表的weex頁面,list和image的組合爆發的內存問題,導致接入weex后app的內存問題導致的crash一直居高不下。
list組件問題
首先來說一下list,list對應的實現是WXListComponent,對應的view是BounceRecyclerView。RecyclerView應該大家都很熟悉,android support庫里面提供的高性能的替代ListView的控件,它的存在就是為了列表中元素復用。本來weex使用了RecyclerView作為list的實現,是一件皆大歡喜的事情,但是RecyclerView中有一種使用不當的情況,會導致view不可復用。
下圖描述了RecyclerView的復用流程:
[ 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各自的圓角方案說起。
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 process(Bitmap sourceBitmap, PlatformBitmapFactory bitmapFactory) {
CloseableReference 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 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大難題:
調試困難
頁面加載慢
調試困難
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**
總結
以上是生活随笔為你收集整理的weex android 性能,跨越适配性能那道坎,企鹅电竞Android weex优化的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python怎么执行csv文件_无法读取
- 下一篇: BugkuCTF-WEB题GET和POS