ListView 是学安卓时候,第一个用到适配器的控件,相信使用它进行数据展示的时候,初学者都不会有太多问题。但是,一个控件单单只是会用它的数据展示,并不能满足大多数公司项目的需求。为了改善用户体验,我们需要让控件使用更少的内存,滑动更流畅,运行使用更少的时间。前段时间面试也遇到了这个问题,但是太久没用,确实忘了,所以今天特意总结了一下自己遇到的,顺便在网上也补充了其他的案例。
1.convertView
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_listview,parent,false);
} else {
// 复用
}
return convertView;
}
这个对象在重写 Adapter.getView 方法时会遇到,默认值为 null。假设一个设备的屏幕最多可以显示 N 条 Item,那么当进行滑动操作时,第一条 Item 完全划出界面后,这个 convertView 会被回收复用,刷新数据后,用于第 N+1 条 Item 的显示。
converView 的复用主要用于减少 LayoutInflater.from().inflate() 方法的调用,减少 Item 布局解析的次数,从而达到优化的目的。
2. ViewHolder
public class Adapter extends BaseAdapter {
…… // 其他代码
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder viewHolder = null;
if (convertView == null) {
convertView = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_listview,parent,false);
viewHolder = new ViewHolder(convertView);
convertView.setTag(viewHolder);
} else {
viewHolder = (ViewHolder) convertView.getTag();
}
// 数据绑定
return convertView;
}
private static class ViewHolder{
private final ImageView imageView;
private final TextView textView;
public ViewHolder(View convertView){
imageView = ((ImageView) convertView.findViewById(R.id.item_listview_imageview));
textView = ((TextView) convertView.findViewById(R.id.item_listview_title));
}
}
}
为了避免造成内存泄漏,ViewHolder 通常是静态内部类,且构造方法通常需要传入 convertView 。使用ViewHolder的原因是findViewById方法耗时较大,如果控件个数过多,会严重影响性能,而使用ViewHolder主要是为了可以省去这个时间。ViewHolder 总是和 converView 一起使用,我们在 ViewHolder 里存储布局中的对象,并把 ViewHolder 和 convertView 关联起来,在 convertView 复用的过程中也复用了对应的 ViewHolder,从而减少 findViewById() 方法的调用次数,达到优化的目的。
3.加载图片
无论在哪里,加载图片都是一个非常消耗内存的操作,尤其是当图片非常大的时候。因此,根据实际情况,我们需要对图片进行二次采样。
/**
* 根据图片字节数组,对图片可能进行二次采样,不致于加载过大图片出现内存溢出
* @param bytes
* @return
*/
public static Bitmap getBitmapByBytes(byte[] bytes){
//对于图片的二次采样,主要得到图片的宽与高
int width = 0;
int height = 0;
int sampleSize = 1; //默认缩放为1
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true; //仅仅解码边缘区域
//如果指定了inJustDecodeBounds,decodeByteArray将返回为空
BitmapFactory.decodeByteArray(bytes, 0, bytes.length, options);
//得到宽与高
height = options.outHeight;
width = options.outWidth;
//图片实际的宽与高,根据默认最大大小值,得到图片实际的缩放比例
while ((height / sampleSize > Cache.IMAGE_MAX_HEIGHT)
|| (width / sampleSize > Cache.IMAGE_MAX_WIDTH)) {
sampleSize *= 2;
}
//不再只加载图片实际边缘
options.inJustDecodeBounds = false;
//并且制定缩放比例
options.inSampleSize = sampleSize;
return BitmapFactory.decodeByteArray(bytes, 0, bytes.length, options);
}
除此之外,对 ListView 来说图片加载应该发生在静止状态时,因为在滑动时加载图片势必会影响 ListView 的滑动体验。我们常常通过以下方法优化滑动中出现因图片加载出现的卡顿现象:
/**
* 监听 listview 滑动
*/
mListView.setOnScrollListener(new AbsListView.OnScrollListener() {
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
if (scrollState == SCROLL_STATE_IDLE) {
// 加载图片
}else{
// 停止加载图片
}
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
}
});
4.事件管控
以点击事件为例,一般情况下,item 中的 ImageView 或者 Button 的点击事件,我们会这样写在 getView 方法中:
@Override
public View getView(int position, View convertView, ViewGroup parent) {
// convertView 复用代码和其他代码
// TextView 点击事件
viewHolder.textView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 点击事件
}
});
return convertView;
}
但是这样就导致了对于同一个 ViewHolder 中同一个控件的点击事件在每次调用 getView 方法时都会新建一个点击事件,浪费了内存资源。因此,我们参照对于 findViewById 的优化方法,将子控件的点击事件放到 ViewHolder 当中去,来减少消耗。
private static class ViewHolder implements View.OnClickListener{
private final ImageView imageView;
private final TextView textView;
private int position;
public ViewHolder(View convertView){
imageView = ((ImageView) convertView.findViewById(R.id.item_listview_imageview));
textView = ((TextView) convertView.findViewById(R.id.item_listview_title));
}
public void setPosition(int pos){
this.position = pos;
}
@Override
public void onClick(View v) {
switch (v.getId()) {
// 对各个控件的点击事件进行管理
}
}
}
由于子控件的 Button 、ImageButton、CheckBox等控件会和父布局争抢焦点,导致 ListView 无法滑动,因此 Item 布局中对于有点击事件的各个控件应该加上属性:android:descendantFocusability
属性的值有三种:
- beforeDescendants:viewgroup会优先其子类控件而获取到焦点
- afterDescendants:viewgroup只有当其子类控件不需要获取焦点时才获取焦点
- blocksDescendants:viewgroup会覆盖子类控件而直接获得焦点
一般我们用第三种。
5.将ListView的scrollingCache和animateCache设置为false
scrollingCache: scrollingCache本质上是drawing cache,你可以让一个View将他自己的drawing保存在cache中(保存为一个bitmap),这样下次再显示View的时候就不用重画了,而是从cache中取出。默认情况下drawing cahce是禁用的,因为它太耗内存了,但是它确实比重画来的更加平滑。而在ListView中,scrollingCache是默认开启的,我们可以手动将它关闭。
animateCache: ListView默认开启了animateCache,这会消耗大量的内存,因此会频繁调用GC,我们可以手动将它关闭掉
6.减少 ItemView 的布局层级
这是所有 layout 都必须遵循的,布局层级过深会直接导致 View 的测量与绘制浪费大量的时间
7.adapter 中的 getView 方法尽量少使用逻辑
不要在 getView 方法中做过于复杂的逻辑,可以想办法抽离到别的地方
8.ListView 嵌套带滑动功能的布局
当 listView 内部部需要 scroller 效果时,不要急于将内部布局取消 scroller。可以想想,有没有可能将上部和下部的布局利用 setHeadView 加到 ListView 里让成为一个整体。这样 ListView 的加载效果会更好一些
9.ListView 数据刷新
在用 RecyclerView 的时候,我们会发现和 ListView 比起来,在数据刷新功能上, RecyclerView 多了局部刷新。事实上,在很多场景下(如朋友圈点赞),我们不需要将所有数据刷新,因此,在特殊情况下,我们可以通过仿照 RecyclerView 的实现创建对应方法,避免 notifyDatasiteChanged 的滥用。