前言
本篇是orzangleli的投稿,基于他之前开源的一款『弹幕控件』,开源之后,orzangleli根据issue的反馈进行了性能优化,更加完善了这个开源项目~
V1.0版本于4天前首发与我的掘金专栏,发布后大家的支持让我喜出望外,截止本文发稿Github上项目https://github.com/hust201010701/XDanmuku的Star数为151。很惭愧,就做了这么一点微小的工作。
不过,好景不长,在发布不久后Github上tz-xiaomage提交了一个题为体验不好,滑动很卡的Issue.当时我并没有很重视,以为是我程序中线程睡眠时间有点长导致的。然后amszsthl也在该Issue下评论
弹幕滚动的时候一卡一卡的。
这是我才开始认真思考,这不是偶然事件,应该是程序出问题了。
现在开始查找卡顿原因,以优化优化性能。
首先设置测试条件,之前我的测试条件是点击按钮,每点击一次就生成一个弹幕,可能是没有测试时间不够长,没有达到性能瓶颈,所以显示挺正常的,现在将增加更为严格的测试条件:每次点击按钮生成10条弹幕。
在未做任何优化时,每点击按钮一次,就生成10个弹幕,点了生成新的弹幕按钮大概10次左右,界面直接卡死。
打开Android Monitor窗口,切换到Monitors选项卡,查看Memory(AS默认显示的第一个为CPU,Memory在CPU上面,所以要滑动下滚轮才能看到)。内存直接飙升到12.62M,而且还在逐渐增加。
我之前的思路是这样的,根据弹幕的模型构造不同View,并对每一个View开启一个线程控制它的坐标向左移动。细心的读者可能会发现:
Q: 为什么不直接使用Android 动画来实现View的移动呢? A: Android中的动画本质上移动的不是原来的View,而是对View的影像进行移动,所以View的触摸事件都在原来的位置,这样就无法实现弹幕点击事件了。
每一个View都开启一个单独的线程控制其移动,实在是太占用内存了,想想我连续点击10次按钮,生成100个弹幕,相当于一瞬间有100个线程启动,并且每个线程都在间隔10ms轮询控制各自的坐标。
优化建议:使用一个线程控制所有的View的移动,由线程每个4ms发出一个Message,Handler接收到Message后对当前ViewGroup的所有chlid进行移动。在Handler中对view进行检测,如果view的右边界已经超出了屏幕范围,则把view从这个ViewGroup中移除。
在掘金上原文https://juejin.im/post/58eeed368d6d81006465670f)下与kaient的交流讨论中,得知缓存功能十分必要。
kaient : 我自己写的弹幕方法是:定义一个 View 或者 surfacview 做容器,弹幕就是 bitmap,这个 Bitmap 做成缓存,当划过屏幕后就放到缓存里,给下一个弹幕用。开三个线程,一个子线程负责从服务器取弹幕信息,一个子线程负责把弹幕信息转换成 Bitmap,一个子线程负责通知绘画 (只要是为了控制卡顿问题,参照了 B 站的开源弹幕)。缺点就是:每个 bitmap 的大小都是一样,高度随便设,宽度根据最长的弹幕长度来定 (产品说最长的弹幕是 1.5 屏,超过就省略号,所有我就设成 1.5 屏)。上面这个方案目前测试全屏 80 条弹幕同时显示基本不卡。
我想问弹幕控件增加缓存功能。我参照ListView
的BaseAdapter
的缓存复用技术,去掉了V1.0版本的DanmuConverter
,增加XAdapter
作为弹幕适配器,并且弹幕的Entity必须继承Model
。Model
中有一个int
型type
表示弹幕的类型区分,代码如下:
XAdapter代码如下:
好啦,关键就在这里啦:cacheViews
是一个按照类型分类的HashMap
,键的类型为int
型,也就是Model
中的type
,值的类型为Stack,是一个包含View的栈。
先看构造方法XAdapter()
,在这里我初始化了cacheViews
,并且根据int typeArray[] = getViewTypeArray();
获取所有的弹幕类型的type值组成的数组,getViewTypeArray()
是一个抽象方法,需要用户自行返回type值组成的数组。然后把每个弹幕类型对于的栈初始化,防止获取到null
.
public abstract View getView(M danmuEntity, View convertView);
则是模仿Adapter
的getView()
方法,它的功能是传入弹幕的Model,将Model上数据绑定到View上,并且返回View,是抽象方法,需要用户实现。
public abstract int getSingleLineHeight();
则是一个让用户确定每一行航道的高度的抽象函数,如果用户知道具体的值,可以直接返回具体值,否则建议用户对不同的View进行测量,取测量高度的最大值。
synchronized public void addToCacheViews(int type,View view)
的作用是向cacheViews
中添加缓存View对象。type
代表弹幕的类型,使用HaskMap
的get()
方法获取该类型的所有弹幕的栈,并使用push()
添加.
synchronized public View removeFromCacheViews(int type)
的作用是当用户使用了缓存数组中的View时,将此View从cacheViews
中移除。
synchronized public void shrinkCacheSize()
的作用是减小缓存数组的长度,因为缓存数组的长度不会减少,只有removeFromCacheViews
表面会减少缓存数组长度,实际上都这个从removeFromCacheViews
中返回的View移动到屏幕外后又会自动添加到缓存数组中,所以需要添加一个策略在不需要大量弹幕时减少缓存数组的长度,这个方法就是将缓存数组的长度减到一半的,什么时候减少缓存数组长度我们在后面谈。
public int getCacheSize()
的作用统计cacheViews
中缓存的View的总个数。
用户自定义DanmuAdapter,继承XAdapter,并实现其中的虚函数。
可以看到getView()
中的具体代码是不是似曾相识?没错,之前常写的BaseAdapter
里,几乎一模一样,所以我也不花时间介绍这个方法了。getSingleLineHeight
就是测量航道的高度的方法,可以看到我计算了两个布局的高度,并且取其中的较大值作为航道高度。getViewTypeArray()
则是很直接的返回你的弹幕的所有类型组成的数组。
下面到了关键了,如何去在我自定义的这个ViewGroup
中使用这个DanmuAdapter呢?
首先得设置setAdapter
,并获取航道高度,并开启View移动的线程。
再添加弹幕的方法addDanmu()
中:
这里的逻辑就是,如果xAdapter
的缓存栈中有View
那么就直接从xAdapter中使用xAdapter.removeFromCacheViews(model.getType())
获取,当然可能没有这个type
类型的弹幕缓存View
,如果没有,就返回null
.如果缓存数组中没有View了,那么就使用danmuView = xAdapter.getView(model,null);
让程序根据layout布局文件再生成一个View。
addTypeView
的定义如下:
首先使用super.addView(child)
添加child,然后设置child的位置。然后将InnerEntity类型的变量绑定到View上面,InnerEntity类型:
包含该View
的所处行数和View中绑定的Model
数据。考虑到用户可能会在DanmuAdapter
中对View
的tag
进行设置,所以不能直接使用setTag(Object object)
方法继续绑定InnerEntity
类型的变量了,这里可以使用setTag(int id,Object object)
方法,首先在string.xml
文件中定义一个id:<item type="id" name="tag_inner_entity"></item>
,然后使用child.setTag(R.id.tag_inner_entity,innerEntity);
则避免了和setTag(Object object)
的冲突。
启动的线程会自动的每隔4ms遍历一次,执行以下内容:
count
为计数器,每隔4ms计数一次,7500次后正好为30s,也就是30s检测一次弹幕,如果当前弹幕量小于缓存View
数量的一半,就调用shrinkCacheSize()
将xAdapter
中的缓存数组长度减少一半。
打开Android Monitors窗口,查看Memory,运行一段时间程序后,点击Initiate GC,手动回收可回收的内存垃圾,剩下的就是不可回收的内存了,点击Dump Java Heap按钮,等待一会会自动打开当前内存使用状态。我只关注Shallow Size,按照从大到小的顺序可以看到,byte[]占用了7,879,324个字节的内存,然后点开byte[]查看Instance,同样按照从到小的顺序,Shallow Size的前几名都是Bitmap,因此可能是Bitmap的内存回收没有做处理,的确,我在写测试案例时没有主要对bitmap的复用和回收,所以产生大量的内存泄露,简单起见,我引入Glide图片加载框架,使用Glide加载图片。
以上工作做完了,狂点生成弹幕按钮,内存也不见飙升,基本维持在4-5M左右。可见,优化效果明显,由之前的几十M内存优化到4-5M。
XDanmuku的第二个版本也就出来了。XDanmuku的V1.1版本,欢迎大家Star和提交Issues。
XDanmuku的V1.1版本 项目地址:https://github.com/hust201010701/XDanmuku
感谢以下用户的建议和反馈: