4天前發(fā)布了 XDanmuku V1.0 版本——《可能是目前輕量級(jí)彈幕控件中功能強(qiáng)大的一款》,發(fā)布后大家的支持讓筆者喜出望外。
不過(guò),好景不長(zhǎng),在發(fā)布不久后Github上tz-xiaomage提交了一個(gè)題為體驗(yàn)不好,滑動(dòng)很卡的Issue。當(dāng)時(shí)我并沒(méi)有很重視,以為是我程序中線程睡眠時(shí)間有點(diǎn)長(zhǎng)導(dǎo)致的。然后amszsthl也在該Issue下評(píng)論
彈幕滾動(dòng)的時(shí)候一卡一卡的。
這是我才開(kāi)始認(rèn)真思考,這不是偶然事件,應(yīng)該是程序出問(wèn)題了。
現(xiàn)在開(kāi)始查找卡頓原因,以優(yōu)化優(yōu)化性能。
首先設(shè)置測(cè)試條件,之前我的測(cè)試條件是點(diǎn)擊按鈕,每點(diǎn)擊一次就生成一個(gè)彈幕,可能是沒(méi)有測(cè)試時(shí)間不夠長(zhǎng),沒(méi)有達(dá)到性能瓶頸,所以顯示挺正常的,現(xiàn)在將增加更為嚴(yán)格的測(cè)試條件:每次點(diǎn)擊按鈕生成10條彈幕。
在未做任何優(yōu)化時(shí),每點(diǎn)擊按鈕一次,就生成10個(gè)彈幕,點(diǎn)了生成新的彈幕按鈕大概10次左右,界面直接卡死。
打開(kāi)Android Monitor窗口,切換到Monitors選項(xiàng)卡,查看Memory(AS默認(rèn)顯示的個(gè)為CPU,Memory在CPU上面,所以要滑動(dòng)下滾輪才能看到)。內(nèi)存直接飆升到12.62M,而且還在逐漸增加。
我之前的思路是這樣的,根據(jù)彈幕的模型構(gòu)造不同View,并對(duì)每一個(gè)View開(kāi)啟一個(gè)線程控制它的坐標(biāo)向左移動(dòng)。細(xì)心的讀者可能會(huì)發(fā)現(xiàn):
Q: 為什么不直接使用Android 動(dòng)畫(huà)來(lái)實(shí)現(xiàn)View的移動(dòng)呢?
A: Android中的動(dòng)畫(huà)本質(zhì)上移動(dòng)的不是原來(lái)的View,而是對(duì)View的影像進(jìn)行移動(dòng),所以View的觸摸事件都在原來(lái)的位置,這樣就無(wú)法實(shí)現(xiàn)彈幕點(diǎn)擊事件了。
每一個(gè)View都開(kāi)啟一個(gè)單獨(dú)的線程控制其移動(dòng),實(shí)在是太占用內(nèi)存了,想想我連續(xù)點(diǎn)擊10次按鈕,生成100個(gè)彈幕,相當(dāng)于一瞬間有100個(gè)線程啟動(dòng),并且每個(gè)線程都在間隔10ms輪詢控制各自的坐標(biāo)。
優(yōu)化建議:使用一個(gè)線程控制所有的View的移動(dòng),由線程每個(gè)4ms發(fā)出一個(gè)Message,Handler接收到Message后對(duì)當(dāng)前ViewGroup的所有chlid進(jìn)行移動(dòng)。在Handler中對(duì)view進(jìn)行檢測(cè),如果view的右邊界已經(jīng)超出了屏幕范圍,則把view從這個(gè)ViewGroup中移除。
Handler handler = new Handler() { @Override public void handleMessage(Message msg) { super.handleMessage(msg); if (msg.what == 1) { for(int i=0;i<danmucontainerview.this.getchildcount();i++){ view="" if(view.getx()+view.getwidth()="">= 0)
view.offsetLeftAndRight((int)(0 - speed)); else{ //添加到緩存中 ...
DanmuContainerView.this.removeView(view);
}
}
}
}
};
在《可能是目前輕量級(jí)彈幕控件中功能強(qiáng)大的一款》文章下與kaient的交流討論中,得知緩存功能十分必要。
kaient :
我自己寫(xiě)的彈幕方法是:定義一個(gè) View 或者 surfacview 做容器,彈幕就是 bitmap,這個(gè) Bitmap 做成緩存,當(dāng)劃過(guò)屏幕后就放到緩存里,給下一個(gè)彈幕用。開(kāi)三個(gè)線程,一個(gè)子線程負(fù)責(zé)從服務(wù)器取彈幕信息,一個(gè)子線程負(fù)責(zé)把彈幕信息轉(zhuǎn)換成 Bitmap,一個(gè)子線程負(fù)責(zé)通知繪畫(huà) (只要是為了控制卡頓問(wèn)題,參照了 B 站的開(kāi)源彈幕)。缺點(diǎn)就是:每個(gè) bitmap 的大小都是一樣,高度隨便設(shè),寬度根據(jù)長(zhǎng)的彈幕長(zhǎng)度來(lái)定 (產(chǎn)品說(shuō)長(zhǎng)的彈幕是 1.5 屏,超過(guò)就省略號(hào),所有我就設(shè)成 1.5 屏)。上面這個(gè)方案目前測(cè)試全屏 80 條彈幕同時(shí)顯示基本不卡。
我想問(wèn)彈幕控件增加緩存功能。我參照ListView的BaseAdapter的緩存復(fù)用技術(shù),去掉了V0.1版本的DanmuConverter,增加XAdapter作為彈幕適配器,并且彈幕的Entity必須繼承Model。Model中有一個(gè)int型type表示彈幕的類(lèi)型區(qū)分,代碼如下:
public class Model { int type ; public int getType() { return type;
} public void setType(int type) { this.type = type;
}
}
XAdapter代碼如下:
public abstract class XAdapter<M>{ private HashMap<Integer,Stack<View>> cacheViews ; public XAdapter()
{
cacheViews = new HashMap<>(); int typeArray[] = getViewTypeArray(); for(int i=0;i<typeArray.length;i++){
Stack<View> stack = new Stack<>();
cacheViews.put(typeArray[i],stack);
}
} public abstract View getView(M danmuEntity, View convertView); public abstract int[] getViewTypeArray(); public abstract int getSingleLineHeight();
synchronized public void addToCacheViews(int type,View view) { if(cacheViews.containsKey(type)){
cacheViews.get(type).push(view);
} else{ throw new Error("you are trying to add undefined type view to cacheViews,please define the type in the XAdapter!");
}
}
synchronized public View removeFromCacheViews(int type) { if(cacheViews.get(type).size()>0) return cacheViews.get(type).pop(); else return null;
} //縮小緩存數(shù)組的長(zhǎng)度,以減少內(nèi)存占用 synchronized public void shrinkCacheSize() { int typeArray[] = getViewTypeArray(); for(int i=0;i<typeArray.length;i++){ int type = typeArray[i];
Stack<View> typeStack = cacheViews.get(type); int length = typeStack.size(); while(typeStack.size() > ((int)(length/2.0+0.5))){
typeStack.pop();
}
cacheViews.put(type,typeStack);
}
} public int getCacheSize()
{ int totalSize = 0; int typeArray[] = getViewTypeArray();
Stack typeStack = null; for(int i=0;i<typeArray.length;i++){ int type = typeArray[i];
typeStack = cacheViews.get(type);
totalSize += typeStack.size();
} return totalSize;
}
}
好啦,關(guān)鍵就在這里啦:cacheviews是一個(gè)按照類(lèi)型分類(lèi)的hashmap,鍵的類(lèi)型為int型,也就是model中的type,值的類(lèi)型為stack,是一個(gè)包含view的棧。
先看構(gòu)造方法XAdapter(),在這里我初始化了cacheViews,并且根據(jù)int typeArray[] = getViewTypeArray();獲取所有的彈幕類(lèi)型的type值組成的數(shù)組,getViewTypeArray()是一個(gè)抽象方法,需要用戶自行返回type值組成的數(shù)組。然后把每個(gè)彈幕類(lèi)型對(duì)于的棧初始化,防止獲取到null.
public abstract View getView(M danmuEntity, View convertView);則是模仿Adapter的getView()方法,它的功能是傳入彈幕的Model,將Model上數(shù)據(jù)綁定到View上,并且返回View,是抽象方法,需要用戶實(shí)現(xiàn)。
public abstract int getSingleLineHeight();則是一個(gè)讓用戶確定每一行航道的高度的抽象函數(shù),如果用戶知道具體的值,可以直接返回具體值,否則建議用戶對(duì)不同的View進(jìn)行測(cè)量,取測(cè)量高度的大值。
synchronized public void addToCacheViews(int type,View view)的作用是向cacheViews中添加緩存View對(duì)象。type代表彈幕的類(lèi)型,使用HaskMap的get()方法獲取該類(lèi)型的所有彈幕的棧,并使用push()添加.
synchronized public View removeFromCacheViews(int type)的作用是當(dāng)用戶使用了緩存數(shù)組中的View時(shí),將此View從cacheViews中移除。
synchronized public void shrinkCacheSize()的作用是減小緩存數(shù)組的長(zhǎng)度,因?yàn)榫彺鏀?shù)組的長(zhǎng)度不會(huì)減少,只有removeFromCacheViews表面會(huì)減少緩存數(shù)組長(zhǎng)度,實(shí)際上都這個(gè)從removeFromCacheViews中返回的View移動(dòng)到屏幕外后又會(huì)自動(dòng)添加到緩存數(shù)組中,所以需要添加一個(gè)策略在不需要大量彈幕時(shí)減少緩存數(shù)組的長(zhǎng)度,這個(gè)方法就是將緩存數(shù)組的長(zhǎng)度減到一半的,什么時(shí)候減少緩存數(shù)組長(zhǎng)度我們?cè)诤竺嬲劇?
public int getCacheSize()的作用統(tǒng)計(jì)cacheViews中緩存的View的總個(gè)數(shù)。
用戶自定義DanmuAdapter,繼承XAdapter,并實(shí)現(xiàn)其中的虛函數(shù)。
public class DanmuAdapter extends XAdapter<DanmuEntity> {
final int ICON_RESOURCES[] = {R.drawable.icon1, R.drawable.icon2, R.drawable.icon3, R.drawable.icon4, R.drawable.icon5}; Random random; private Context context; DanmuAdapter(Context c){
super(); context = c; random = new Random(); }
@Override
public View getView(DanmuEntity danmuEntity, View convertView) {
ViewHolder1 holder1 = null; ViewHolder2 holder2 = null; if(convertView == null){
switch (danmuEntity.getType()) {
case 0:
convertView = LayoutInflater.from(context).inflate(R.layout.item_danmu, null); holder1 = new ViewHolder1(); holder1.content = (TextView) convertView.findViewById(R.id.content); holder1.image = (ImageView) convertView.findViewById(R.id.image); convertView.setTag(holder1); break; case 1:
convertView = LayoutInflater.from(context).inflate(R.layout.item_super_danmu, null); holder2 = new ViewHolder2(); holder2.content = (TextView) convertView.findViewById(R.id.content); holder2.time = (TextView) convertView.findViewById(R.id.time); convertView.setTag(holder2); break; }
}
else{
switch (danmuEntity.getType()) {
case 0:
holder1 = (ViewHolder1)convertView.getTag(); break; case 1:
holder2 = (ViewHolder2)convertView.getTag(); break; }
}
switch (danmuEntity.getType()) {
case 0:
Glide.with(context).load(ICON_RESOURCES[random.nextInt(5)]).into(holder1.image); holder1.content.setText(danmuEntity.content); holder1.content.setTextColor(Color.rgb(random.nextInt(256), random.nextInt(256), random.nextInt(256))); break; case 1:
holder2.content.setText(danmuEntity.content); holder2.time.setText(danmuEntity.getTime()); break; }
return convertView; }
@Override
public int[] getViewTypeArray() {
int type[] = {0,1}; return type; }
@Override
public int getSingleLineHeight() {
//將所有類(lèi)型彈幕的布局拿出來(lái),找到高度大值,作為彈道高度
View view = LayoutInflater.from(context).inflate(R.layout.item_danmu, null); //指定行高
view.measure(0, 0); View view2 = LayoutInflater.from(context).inflate(R.layout.item_super_danmu, null); //指定行高
view2.measure(0, 0); return Math.max(view.getMeasuredHeight(),view2.getMeasuredHeight()); }
class ViewHolder1{
public TextView content; public ImageView image; }
class ViewHolder2{
public TextView content; public TextView time; }
}
可以看到getView()中的具體代碼是不是似曾相識(shí)?沒(méi)錯(cuò),之前常寫(xiě)的BaseAdapter里,幾乎一模一樣,所以我也不花時(shí)間介紹這個(gè)方法了。getSingleLineHeight就是測(cè)量航道的高度的方法,可以看到我計(jì)算了兩個(gè)布局的高度,并且取其中的較大值作為航道高度。getViewTypeArray()則是很直接的返回你的彈幕的所有類(lèi)型組成的數(shù)組。
下面到了關(guān)鍵了,如何去在我自定義的這個(gè)ViewGroup中使用這個(gè)DanmuAdapter呢?
public void setAdapter(XAdapter danmuAdapter) {
xAdapter = danmuAdapter;
singleLineHeight = danmuAdapter.getSingleLineHeight(); new Thread(new MyRunnable()).start();
}
首先得設(shè)置setAdapter,并獲取航道高度,并開(kāi)啟View移動(dòng)的線程。
再添加彈幕的方法addDanmu()中:
public void addDanmu(final Model model){ if (xAdapter == null) { throw new Error("XAdapter(an interface need to be implemented) can't be null,you should call setAdapter firstly");
}
View danmuView = null; if(xAdapter.getCacheSize() >= 1){
danmuView = xAdapter.getView(model,xAdapter.removeFromCacheViews(model.getType())); if(danmuView == null)
addTypeView(model,danmuView,false); else addTypeView(model,danmuView,true);
} else {
danmuView = xAdapter.getView(model,null);
addTypeView(model,danmuView,false);
} //添加監(jiān)聽(tīng) danmuView.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { if(onItemClickListener != null)
onItemClickListener.onItemClick(model);
}
});
}
這里的邏輯就是,如果xAdapter的緩存棧中有View那么就直接從xAdapter中使用xAdapter.removeFromCacheViews(model.getType())獲取,當(dāng)然可能沒(méi)有這個(gè)type類(lèi)型的彈幕緩存View,如果沒(méi)有,就返回null.如果緩存數(shù)組中沒(méi)有View了,那么就使用danmuView = xAdapter.getView(model,null);讓程序根據(jù)layout布局文件再生成一個(gè)View。
addTypeView的定義如下:
public void addTypeView(Model model,View child,boolean isReused) { super.addView(child);
child.measure(0, 0); //把寬高拿到,寬高都是包含ItemDecorate的尺寸 int width = child.getMeasuredWidth(); int height = child.getMeasuredHeight(); //獲取佳行數(shù) int bestLine = getBestLine();
child.layout(WIDTH, singleLineHeight * bestLine, WIDTH + width, singleLineHeight * bestLine + height);
InnerEntity innerEntity = null;
innerEntity = (InnerEntity) child.getTag(R.id.tag_inner_entity); if(!isReused || innerEntity==null){
innerEntity = new InnerEntity();
}
innerEntity.model = model;
innerEntity.bestLine = bestLine;
child.setTag(R.id.tag_inner_entity,innerEntity);
spanList.set(bestLine, child);
}
首先使用super.addView(child)添加child,然后設(shè)置child的位置。然后將InnerEntity類(lèi)型的變量綁定到View上面,InnerEntity類(lèi)型:
class InnerEntity{ public int bestLine; public Model model;
}
包含該View的所處行數(shù)和View中綁定的Model數(shù)據(jù)??紤]到用戶可能會(huì)在DanmuAdapter中對(duì)View的tag進(jìn)行設(shè)置,所以不能直接使用setTag(Object object)方法繼續(xù)綁定InnerEntity類(lèi)型的變量了,這里可以使用setTag(int id,Object object)方法,首先在string.xml文件中定義一個(gè)id:<item type="id" name="tag_inner_entity"></item>,然后使用child.setTag(R.id.tag_inner_entity,innerEntity);則避免了和setTag(Object object)的沖突。
啟動(dòng)的線程會(huì)自動(dòng)的每隔4ms遍歷一次,執(zhí)行以下內(nèi)容:
private class MyRunnable implements Runnable { @Override public void run() { int count = 0;
Message msg = null; while(true){ if(count < 7500){
count ++;
} else{
count = 0; if(DanmuContainerView.this.getChildCount() < xAdapter.getCacheSize() / 2){
xAdapter.shrinkCacheSize();
System.gc();
}
} if(DanmuContainerView.this.getChildCount() >= 0){
msg = new Message();
msg.what = 1; //移動(dòng)view handler.sendMessage(msg);
} try {
Thread.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
count為計(jì)數(shù)器,每隔4ms計(jì)數(shù)一次,7500次后正好為30s,也就是30s檢測(cè)一次彈幕,如果當(dāng)前彈幕量小于緩存View數(shù)量的一半,就調(diào)用shrinkCacheSize()將xAdapter中的緩存數(shù)組長(zhǎng)度減少一半。
打開(kāi)Android Monitors窗口,查看Memory,運(yùn)行一段時(shí)間程序后,點(diǎn)擊Initiate GC,手動(dòng)回收可回收的內(nèi)存垃圾,剩下的就是不可回收的內(nèi)存了,點(diǎn)擊Dump Java Heap按鈕,等待一會(huì)會(huì)自動(dòng)打開(kāi)當(dāng)前內(nèi)存使用狀態(tài)。我只關(guān)注Shallow Size,按照從大到小的順序可以看到,byte[]占用了7,879,324個(gè)字節(jié)的內(nèi)存,然后點(diǎn)開(kāi)byte[]查看Instance,同樣按照從到小的順序,Shallow Size的前幾名都是Bitmap,因此可能是Bitmap的內(nèi)存回收沒(méi)有做處理,的確,我在寫(xiě)測(cè)試案例時(shí)沒(méi)有主要對(duì)bitmap的復(fù)用和回收,所以產(chǎn)生大量的內(nèi)存泄露,簡(jiǎn)單起見(jiàn),我引入Glide圖片加載框架,使用Glide加載圖片。
以上工作做完了,狂點(diǎn)生成彈幕按鈕,內(nèi)存也不見(jiàn)飆升,基本維持在4-5M左右??梢?jiàn),優(yōu)化效果明顯,由之前的幾十M內(nèi)存優(yōu)化到4-5M。
本站文章版權(quán)歸原作者及原出處所有 。內(nèi)容為作者個(gè)人觀點(diǎn), 并不代表本站贊同其觀點(diǎn)和對(duì)其真實(shí)性負(fù)責(zé),本站只提供參考并不構(gòu)成任何投資及應(yīng)用建議。本站是一個(gè)個(gè)人學(xué)習(xí)交流的平臺(tái),網(wǎng)站上部分文章為轉(zhuǎn)載,并不用于任何商業(yè)目的,我們已經(jīng)盡可能的對(duì)作者和來(lái)源進(jìn)行了通告,但是能力有限或疏忽,造成漏登,請(qǐng)及時(shí)聯(lián)系我們,我們將根據(jù)著作權(quán)人的要求,立即更正或者刪除有關(guān)內(nèi)容。本站擁有對(duì)此聲明的最終解釋權(quán)。