刚过完年,公司提新需求要把gif做到现有的app中,由于大神有更重要的事要忙,这事就落到了我头上。2周的时间,原本以为会很easy的解决掉这个需求,没想到踩了3个大坑,最后还是在大神的帮助下,拖了3天才完成功能发布版本的本着开源分享的精神把这次踩到的大坑给大家做个分享,也给自己做一个沉淀
大坑1:解析gif文件卡成一坨翔
大家都知道,Android开发模拟器为了节省内存,一般不支持直接显示gif图片,即使你强制设置了,也只会显示图片的第一帧。网上有大神用movie来解析gif,效果挺好的,但是仅限于在解析一个gif文件的情况下,这种方式是走的通的。如果你的gif是在listview中的话可就没那么舒服了,轻则卡成翔,重则关机重启拔电池······怎么办呢,介绍一个牛B的项目,用C++解析gif,使用ndk进行编译(不了解JNI和NDK的小伙伴们也不用担心,配好环境直接用就OK了,怎么配NDK有条件的google,没条件的baidu喽),项目地址:https://github.com/koral--/android-gif-drawable
大坑2:文件反复下载、暂停、取消、重下千万不要用数据库进行多线程下载
没有遇到过数据库锁死、线程同步、死锁的童鞋就直接跳过吧。gif文件小则几百K,大的话就是几M,文件必然要缓存到本地,但是千万不要使用数据库进行同一个文件的多线程下载,尤其是在任务反复提交、暂停、取消的情况下,数据库很容易锁死,线程间的同步也是很棘手的事,弄不好还容易出现死锁的情况,找问题都很困难。解决方案有2个,一种是使用现有的下载框架,还有当然是自己写下载框架了。要是自己写下载框架的话,线程池、线程同步是个绕不开的坎儿,一旦过去的那就是柳暗花明又一村啊······
大坑3:Android中adapter的getView方法对View的重用销毁真神奇
相信不少人都知道listview中view的重用的,也会用holder和tag提高list的效率,但是我如果问你view什么时候销毁你还知道吗?先说view的重用吧,每次在getView的时候都要把holder中view的属性重设一遍以防出现错位等奇葩bug,原因很简单:因为这个view是复用的,它保留了上一个使用者设置的属性(比如textcolor:green),如果再次getView中没有重新设置它的属性,那么相应属性是上个使用者留下的(textcolor:green),而不是新生成view的属性(textcolor:black),切记啊,要不都不清楚why
重点来了,listview中的view是什么时候销毁的?我先说个事实:如果listview中可见的view有N个,那么listview保留的的view个数有可能那个会比N多,但是多多少不确定。我遇到的大坑是:gif要在view被重用或是回收时释放掉,否则会越来越卡,直到OOM。重用的时候释放掉还好办,大家看代码:(重用释放gif的代码)
@Override
public View getView(int position, View convertView, ViewGroup parent) {
Holder holder;
if (convertView == null) {
convertView = LayoutInflater.from(mContext).inflate(R.layout.emoji_gif_water_fall_item, parent, false);
holder = new Holder();
holder.gifView = (GifImageView) convertView.findViewById(R.id.emoji_gif_water_fall_item);
holder.progressWheel = (ProgressWheel) convertView.findViewById(R.id.pw_spinner);
convertView.setTag(holder);
} else {
holder = (Holder) convertView.getTag();
// 如果GifView被复用,则将之前gifView中解析gif的线程释放掉
holder.gifView.setImageDrawable(null);
}
销毁listview的时候释放gif也好办:(销毁listview的代码)
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
if (object instanceof View) {
View view = (View) object;
GridView gridView = (GridView) view.findViewById(R.id.list);
if (gridView != null) {
// 清除所有绘制的gifView
int count = gridView.getChildCount();
for (int i = 0; i < count; i++) {
View itemView = gridView.getChildAt(i);
if (itemView != null) {
GifImageView gifView = (GifImageView) itemView.findViewById(R.id.emoji_gif_water_fall_item);
if (gifView != null) {
gifView.setImageDrawable(null);
}
}
}
gridView.setAdapter(null);
}
// 清除listview
container.removeView((View) object);
}
}
难办的就是我刚说的那几个多出来的不确定个数而且还不显示的view,打印log,检测内存都显示有view在后台运行,在死掉NNNN多脑细胞后我终于找到了这样一个类:( RecyclerListener),看下它的说明:
/**
* A RecyclerListener is used to receive a notification whenever a View is placed
* inside the RecycleBin's scrap heap. This listener is used to free resources
* associated to Views placed in the RecycleBin.
*
* @see android.widget.AbsListView.RecycleBin
* @see android.widget.AbsListView#setRecyclerListener(android.widget.AbsListView.RecyclerListener)
*/
public static interface RecyclerListener {
/**
* Indicates that the specified View was moved into the recycler's scrap heap.
* The view is not displayed on screen any more and any expensive resource
* associated with the view should be discarded.
*
* @param view
*/
void onMovedToScrapHeap(View view);
}
就是这个接口告诉我们listview中的view什么时候被回收,剩下的代码就不难了:
// 当gifView被回收时释放掉渲染gif的线程
mGridView.setRecyclerListener(new AbsListView.RecyclerListener() {
@Override
public void onMovedToScrapHeap(View view) {
if (view != null) {
GifImageView gifImageView = (GifImageView) view.findViewById(R.id.emoji_gif_water_fall_item);
if (gifImageView != null) {
gifImageView.setImageDrawable(null);
}
}
}
});
2周的工作固然辛苦,想想当时各种加班熬夜现在还菊花一紧,但是总算是顺利上线了。
开心生活,快乐编码!