weex android 性能,跨越适配性能那道坎,企鹅电竞Android weex优化
作者:龍泉,騰訊企鵝電競(jìng)工程師
商業(yè)轉(zhuǎn)載請(qǐng)聯(lián)系騰訊WeTest獲得授權(quán),非商業(yè)轉(zhuǎn)載請(qǐng)注明出處。
WeTest 導(dǎo)讀
企鵝電競(jìng)從17年6月接入weex,到現(xiàn)在已經(jīng)有一年半的時(shí)間,這段時(shí)間里面,針對(duì)遇到的問(wèn)題,企鵝電競(jìng)終端主要做了下面的優(yōu)化:
image組件
預(yù)加載
預(yù)渲染
_
Image組件
weex的list組件和image組件非常容易出問(wèn)題,企鵝電競(jìng)本身又存在很多無(wú)限列表的weex頁(yè)面,list和image的組合爆發(fā)的內(nèi)存問(wèn)題,導(dǎo)致接入weex后app的內(nèi)存問(wèn)題導(dǎo)致的crash一直居高不下。
list組件問(wèn)題
首先來(lái)說(shuō)一下list,list對(duì)應(yīng)的實(shí)現(xiàn)是WXListComponent,對(duì)應(yīng)的view是BounceRecyclerView。RecyclerView應(yīng)該大家都很熟悉,android support庫(kù)里面提供的高性能的替代ListView的控件,它的存在就是為了列表中元素復(fù)用。本來(lái)weex使用了RecyclerView作為list的實(shí)現(xiàn),是一件皆大歡喜的事情,但是RecyclerView中有一種使用不當(dāng)?shù)那闆r,會(huì)導(dǎo)致view不可復(fù)用。
下圖描述了RecyclerView的復(fù)用流程:
[ RecyclerView復(fù)用 ]
weex中的RecyclerView并沒(méi)有設(shè)置stableId,所以RecyclerView的所有復(fù)用都依賴于ViewHolder的ViewType,Weex的ViewType生成見(jiàn)下圖:
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;
}
在沒(méi)有設(shè)置scope的情況下,viewHolder的component的ref就是viewType,即所有的ViewHolder都是不同且不可復(fù)用的,此時(shí)的RecyclerView也就退化成了一個(gè)稍微復(fù)雜一點(diǎn)的ScrollView。
如果設(shè)置了scope屬性,但你絕對(duì)想不到,scope本身也是一個(gè)坑。下面直接上代碼:
// 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,當(dāng)復(fù)用Holder時(shí),會(huì)把需要展示的component的數(shù)據(jù)綁定到復(fù)用的component中。那么問(wèn)題來(lái)了,如果我不是只是想修改部分屬性,而是需要改變component的層級(jí)關(guān)系呢?例如從a->b->c修改成a->c->b,那么是不是只能用不同的viewType或者是說(shuō)變成下面的結(jié)構(gòu):a->b a->c b->b1 b->c1 c->c2 c->b2這樣的結(jié)構(gòu),但是view的實(shí)例多了,必然又會(huì)導(dǎo)致內(nèi)存等各種問(wèn)題。最為致命的問(wèn)題是,createViewHolder的時(shí)候,傳給ViewHolder的component實(shí)例就是原件,而非拷貝,當(dāng)bindData執(zhí)行了以后,就等用于你復(fù)用的那個(gè)component的數(shù)據(jù)被修改了,當(dāng)你再滑回去的時(shí)候,GG。
所以scope屬性基本不可用,留給我們的只有相當(dāng)于scrollView的list。
還好,為了解決list這么戳的性能,有了recyclerList,從vue的語(yǔ)法層,支持了模板的復(fù)用。但是坑爹的是,0.17 、 0.18 版本recyclerList都有這樣那樣的問(wèn)題,重構(gòu)同學(xué)覺(jué)得使用起來(lái)效率較低。0.19版本weex團(tuán)隊(duì)fix了這些問(wèn)題后,企鵝電競(jìng)的前端同學(xué)也正在嘗試往recyclerList去切換。
image組件問(wèn)題
相信android開(kāi)發(fā)們都清楚,圖片的問(wèn)題永遠(yuǎn)是大問(wèn)題。OOM、GC等性能問(wèn)題,經(jīng)常就是伴隨著圖片操作。
在0.17版本以前,WXImageView中bitmap的釋放都是在component的recycle中執(zhí)行,0.17版本之后,在detach時(shí)也會(huì)執(zhí)行recycle,但是WXImageView的recycle只是把ImageView的drawable設(shè)置為null,并沒(méi)有實(shí)際調(diào)用bitmap的recycle。
而企鵝電競(jìng)在版本運(yùn)行過(guò)程中發(fā)現(xiàn),僅僅把bitmapDrawable設(shè)置為null,不去調(diào)用bitmap的recycle,部分機(jī)型上面的oom問(wèn)題非常突出(這里一直沒(méi)想明白,為啥這部分機(jī)型會(huì)出現(xiàn)這個(gè)問(wèn)題,后面替換成fresco去管理就沒(méi)這個(gè)問(wèn)題了)。當(dāng)然,如果直接recycle bitmap,不設(shè)置bitmapDrawable,會(huì)直接導(dǎo)致crash。
回到企鵝電競(jìng)本身,企鵝電競(jìng)中的圖片管理使用了fresco,在接入weex以前,我們已經(jīng)針對(duì)fresco加載圖片做了一系列優(yōu)化,而且fresco本身已經(jīng)包含了三級(jí)緩存等功能。
接入weex后,首先想到的就是使用fresco的管線加載出bitmap后給WXImage使用。在這個(gè)過(guò)程中,先是遇到了對(duì)CloseableReference管理不恰當(dāng)導(dǎo)致bitmap 還在使用卻被recycle 掉了,然后又遇到了沒(méi)有執(zhí)行recycle導(dǎo)致bitmap無(wú)法釋放的坑。在長(zhǎng)列表中,圖片無(wú)法釋放的問(wèn)題被無(wú)限放大,經(jīng)常出現(xiàn)快速滑動(dòng)幾屏就oom的問(wèn)題。而且隨著業(yè)務(wù)發(fā)展使用WXImage無(wú)法播放gif和webp圖片也成為瓶頸。
后續(xù)版本中,企鵝電競(jìng)直接重寫(xiě)了image和img標(biāo)簽,使用Fresco的SimpleDraweeView替換了ImageView。該方案帶來(lái)的收益是bitmap不在需要自己管理,即oom問(wèn)題和bitmap recycle之后導(dǎo)致的crash問(wèn)題會(huì)大大減少,且fresco默認(rèn)就支持gif和webp圖片。但是,這個(gè)方案也有個(gè)致命的問(wèn)題:圓角。
圓角問(wèn)題得先從fresco和weex各自的圓角方案說(shuō)起。
fresco圓角方案具體可見(jiàn)RoundedBitmapDrawable,RoundedColorDrawable,RoundedCornersDrawable這3個(gè)類(lèi),fresco圓角屬性的改變最終都只是修改這3個(gè)類(lèi)的屬性,圓角也是基于draw時(shí)候修改canvas畫(huà)布內(nèi)容實(shí)現(xiàn),BtimapDrawable的裁減以及邊框的繪制都是在draw的時(shí)候繪制上去。
weex圓角方案具體可見(jiàn)ImageDrawable,實(shí)現(xiàn)方案為借助android的PaintDrawable,通過(guò)設(shè)置shader實(shí)現(xiàn)bitmapDrawable的裁減,但是邊框的繪制則依賴于backgroundDrawable。
而且在fresco中,封裝了多層的drawable,較難修改drawabl的 draw的邏輯,而且邊框參數(shù)的設(shè)置也不如weex眾多樣化。
針對(duì)兩者的差異性,企鵝電競(jìng)的解決方案是放棄fresco的圓角方案,通過(guò)fresco的后處理器裁減bitmap達(dá)到圓角的效果,邊框復(fù)用weex的background的方案。這個(gè)方案唯一的問(wèn)題后處理器中必須創(chuàng)建一份新的bitmap,但是通過(guò)復(fù)用fresco的bitmapPool,并不會(huì)導(dǎo)致內(nèi)存有過(guò)多的問(wèn)題。
下面貼一下后處理器處理圓角的關(guān)鍵代碼:
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繪制尺寸上限問(wèn)題,比如: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;
}
當(dāng)list和image組合在一起的時(shí)候,由于weex的image并沒(méi)有recycle掉bitmap,而且沒(méi)有bitmapPool的使用,會(huì)導(dǎo)致長(zhǎng)列表weex頁(yè)面占用內(nèi)存特別高。而替換為fresco的bitmap內(nèi)存管理模式后,由于weex導(dǎo)致的內(nèi)存crash問(wèn)題占比明顯從最開(kāi)始版本的2%下降到了0.1%-0.2%。
預(yù)加載
當(dāng)踩完大大小小的坑,緩解了內(nèi)存和crash問(wèn)題之后,企鵝電競(jìng)在weex使用上又遇到了2大難題:
調(diào)試?yán)щy
頁(yè)面加載慢
調(diào)試?yán)щy
weex的頁(yè)面并不能給前端的開(kāi)發(fā)同學(xué)絲滑的調(diào)試體驗(yàn)。最開(kāi)始前端同學(xué)是采用終端日志或者彈框的方式調(diào)試(心疼前端同學(xué)就這么學(xué)會(huì)了看android日志),后面通過(guò)再三跟weex團(tuán)隊(duì)的溝通,終于確定了weex和weex_debuger對(duì)應(yīng)的版本,前端同學(xué)可以在chrome上面調(diào)試weex頁(yè)面。
然而weex_deubgger并不是完美的解決方案,weex本身是jscore內(nèi)核,而weex_debugger只是通過(guò)chrome調(diào)試協(xié)議開(kāi)了個(gè)服務(wù),等同于使用的是chrome的內(nèi)核,內(nèi)核的不一致性無(wú)法保證調(diào)試的準(zhǔn)確性。連weex的開(kāi)發(fā)同學(xué)自己都說(shuō)了會(huì)遇到debug環(huán)境和正式環(huán)境結(jié)果不一致的情況。
解決方案也很簡(jiǎn)單,那就是可以在mac的xcode和safari上面調(diào)試。當(dāng)時(shí)由于替換mac的成功過(guò)高,就將就使用了weex_debugger的方案,后面怎么解決了相信大家心里有數(shù)。
頁(yè)面加載速度慢
隨著企鵝電競(jìng)業(yè)務(wù)的發(fā)展,很快前端同學(xué)就反饋過(guò)來(lái),怎么weex頁(yè)面打開(kāi)的速度這么慢,這個(gè)菊花轉(zhuǎn)了這么久。當(dāng)時(shí)的內(nèi)心是崩潰的,明明接入的時(shí)候好好的,一個(gè)頁(yè)面輕輕松松500-600ms就加載回來(lái)了,哪里會(huì)有問(wèn)題?
業(yè)務(wù)的發(fā)展速度永遠(yuǎn)是你想象不到的,2個(gè)版本不到的時(shí)間,企鵝電競(jìng)中的weex頁(yè)面輕輕松松從個(gè)位數(shù)突破到兩位數(shù),bundle大小也輕輕松松從幾十kb突破到了上百kb,由此帶來(lái)的問(wèn)題是打開(kāi)weex頁(yè)面后能明顯看到菊花轉(zhuǎn)動(dòng)了,甚至打開(kāi)速度上還不如直出的web頁(yè)面。
首先從數(shù)據(jù)報(bào)表中發(fā)現(xiàn),頁(yè)面打開(kāi)速度中,1s中有300-400ms是bundle從網(wǎng)絡(luò)下載的時(shí)間,那是不是把這段時(shí)間省了,頁(yè)面有輕輕松松回到毫秒級(jí)別打開(kāi)速度了。
下圖展示了預(yù)加載的整體流程。
[ 預(yù)加載流程 ]
預(yù)加載方案上線后,頁(yè)面成功節(jié)省了將近200ms的耗時(shí)。20M的LRUCache大小也是參考了http cache的默認(rèn)大小值,頁(yè)面打開(kāi)的預(yù)加載率在75%-80%。
預(yù)渲染
做了預(yù)加載之后,很快又發(fā)現(xiàn),就算沒(méi)有網(wǎng)絡(luò)請(qǐng)求,頁(yè)面打開(kāi)耗時(shí)還是超過(guò)了1s。這種情況下,現(xiàn)有的方案已經(jīng)無(wú)法繼續(xù)優(yōu)化頁(yè)面。這個(gè)時(shí)候突然有了個(gè)想法,weex本身是把前端的虛擬dom轉(zhuǎn)化為終端的各種view控件,那么為什么weex頁(yè)面的打開(kāi)會(huì)慢終端頁(yè)面打開(kāi)這么多呢?
定義問(wèn)題
解決問(wèn)題之前,先來(lái)定義一下問(wèn)題具體是什么。針對(duì)渲染速度慢,企鵝電競(jìng)對(duì)weex渲染的耗時(shí)定義如下:
· renderStart = 調(diào)用WXSdkInstance.render()的時(shí)間點(diǎn)
· httpFinish = httpAdapter請(qǐng)求回來(lái)之后調(diào)用WXSdkInstance.onHttpFinish()的時(shí)間點(diǎn)
· renderFinish = 回調(diào) IWXRenderListener.onRenderSuccess()的時(shí)間點(diǎn)
· 頁(yè)面打開(kāi)耗時(shí) = renderFinish - renderStart
· 網(wǎng)絡(luò)耗時(shí) = httpFinish - renderStart
· 渲染耗時(shí) = renderFinish - httpFinish
所以之前的預(yù)加載,已經(jīng)優(yōu)化了網(wǎng)絡(luò)耗時(shí),但是渲染耗時(shí)在頁(yè)面大了之后,依舊會(huì)有很大的性能問(wèn)題。
為了揭開(kāi)這個(gè)問(wèn)題的本質(zhì),先來(lái)看一下weex整體的框架:
[ weex框架圖: ]
JSFrameWork
提供給前端的sdk,對(duì)vue的dom操作做了各種封裝,JSFrameWork單獨(dú)打包到apk包中。
JavaScriptCore
使用與safari的JavaScript引擎,專(zhuān)門(mén)處理JavaScript的虛擬機(jī),對(duì)應(yīng)chrome的v8,功能可以大體聯(lián)想成java的jvm。
JSS
weex core的server端,封裝了對(duì)JavaScripteCore的調(diào)用,封裝了instance的沙盒,多進(jìn)程實(shí)現(xiàn)中,JSS和JavaScriptCore的執(zhí)行在另外的進(jìn)程,防止JS執(zhí)行異常導(dǎo)致主進(jìn)程崩潰。
JSC
weex core的client端,作為WeexFrameWork和JSS橋接層,另外從0.18版本開(kāi)始,cssLayout也下沉到了這一層。
WeexFrameWork
提供各種sdk接口的java調(diào)用,虛擬dom和Android控件樹(shù)的轉(zhuǎn)換,控件管理等。
了解完了weex框架,再把關(guān)注點(diǎn)轉(zhuǎn)移到j(luò)s build之后生成的jsBundle,細(xì)心的同學(xué)肯定能夠發(fā)現(xiàn),生成的jsBundle本質(zhì)上就是一個(gè)js方法,所以weex頁(yè)面render的過(guò)程本質(zhì)上是執(zhí)行一個(gè)js方法。
針對(duì)企鵝電競(jìng)關(guān)注的游戲首頁(yè),對(duì)整個(gè)weex框架加了完整的打點(diǎn),看到在nexus 6上面,對(duì)應(yīng)的耗時(shí)以及整體流程如下圖:
[ weex執(zhí)行流程以及耗時(shí) ]
可以看到性能的熱點(diǎn)主要在執(zhí)行js方法以及虛擬dom的執(zhí)行這兩個(gè)關(guān)鍵步驟上,根據(jù)打點(diǎn)來(lái)看,單個(gè)js方法和單個(gè)虛擬dom的執(zhí)行,耗時(shí)都很低。企鵝電競(jìng)抓了多次打點(diǎn),看到啟動(dòng)時(shí)候執(zhí)行js最慢的也僅僅是3ms,大多數(shù)執(zhí)行都在0.1ms - 0 ms這個(gè)區(qū)間。但是,再快的執(zhí)行耗時(shí),也架不住量多,同樣以企鵝電競(jìng)游戲首頁(yè)為例,啟動(dòng)的時(shí)候該頁(yè)面執(zhí)行的js方法多大2000+個(gè),這2000+個(gè)方法執(zhí)行再加上方法調(diào)度的耗時(shí),能成為性能熱點(diǎn)一點(diǎn)也不意外。而虛擬dom的執(zhí)行也同理,單次執(zhí)行經(jīng)過(guò)weex團(tuán)隊(duì)的優(yōu)化,執(zhí)行耗時(shí)基本在1ms-3ms之間,但是同樣的架不住量多以及線程調(diào)度的時(shí)間問(wèn)題。
預(yù)渲染方案
了解RN的同學(xué)應(yīng)該也知道,js方法的執(zhí)行和虛擬dom的執(zhí)行是這種框架的核心所在,想要撬動(dòng)整個(gè)核心,基本上難度等同于重寫(xiě)一個(gè)了。那么剩下的方案也就只有一個(gè):提前渲染。
[ 預(yù)渲染 ]
預(yù)渲染的方案修改了WeexFrameWork虛擬dom和Android控件樹(shù)轉(zhuǎn)換的部分,在預(yù)渲染時(shí),不生成真正的component和view結(jié)構(gòu),用抽象出來(lái)的ComponentNode存儲(chǔ)虛擬dom的操作,并在RealRender的時(shí)候?qū)ode轉(zhuǎn)換成一個(gè)個(gè)component以及View。
這個(gè)方案的基本原理就是典型的以提前消費(fèi)的空間換取時(shí)間,不去轉(zhuǎn)換真正的component和View原因是view在不同context中的不可復(fù)用性以及view本身會(huì)占用大部分內(nèi)存。
預(yù)渲染優(yōu)化數(shù)據(jù)
內(nèi)存消耗
提前渲染必然導(dǎo)致類(lèi)內(nèi)存的提前消耗,在huawei nove3上測(cè)試得到,預(yù)渲染游戲首頁(yè)時(shí)的峰值內(nèi)存會(huì)去到10M,但是在最后預(yù)渲染完成后GC會(huì)釋放這部分內(nèi)存,最終常駐內(nèi)存為0.3M。 真正渲染游戲首頁(yè)的內(nèi)存峰值會(huì)去到20M,最后的常駐內(nèi)存為5.6M。
可以看到預(yù)渲染對(duì)常駐內(nèi)存的消耗極少,但是由于虛擬dom執(zhí)行,導(dǎo)致峰值內(nèi)存偏高,在某些內(nèi)存敏感場(chǎng)景下,還是會(huì)有一定風(fēng)險(xiǎn)。
頁(yè)面打開(kāi)耗時(shí)
實(shí)驗(yàn)室中游戲首頁(yè)的正常加載數(shù)據(jù)為900ms(已經(jīng)預(yù)加載,無(wú)網(wǎng)絡(luò)耗時(shí)),經(jīng)過(guò)預(yù)渲染,頁(yè)面打開(kāi)僅需要150ms。
現(xiàn)網(wǎng)數(shù)據(jù):
[ 預(yù)渲染頁(yè)面打開(kāi)上報(bào) ]
最后,來(lái)兩張優(yōu)化前后的對(duì)比圖:
[ 預(yù)渲染: ]
[ 非預(yù)渲染: ]
_
“深度兼容測(cè)試”現(xiàn)已對(duì)外,騰訊專(zhuān)家為您定制自動(dòng)化測(cè)試腳本,覆蓋應(yīng)用核心場(chǎng)景,對(duì)上百款主流機(jī)型進(jìn)行適配兼容測(cè)試,提供詳細(xì)測(cè)試報(bào)告。
另有客戶端性能測(cè)試,一網(wǎng)打盡FPS、CPU等基礎(chǔ)性能數(shù)據(jù),詳細(xì)展示各類(lèi)渲染數(shù)據(jù),極速定位性能問(wèn)題。
**點(diǎn)擊:https://wetest.qq.com/cloud/deepcompatibilitytesting 即可體驗(yàn)。
如果使用當(dāng)中有任何疑問(wèn),歡迎聯(lián)系騰訊WeTest企業(yè)QQ:2852350015**
總結(jié)
以上是生活随笔為你收集整理的weex android 性能,跨越适配性能那道坎,企鹅电竞Android weex优化的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: python怎么执行csv文件_无法读取
- 下一篇: android 里程,鹰眼Android