ListView的效率提升

94 阅读4分钟

自己之前对ListView是如何提升效率一直都是一知半解,懵懵懂懂,现在开始要看RecyclerView了,想要搞明白,不搞清楚就很难受

参考链接android配适器Listview的ViewHolder和ConvertView的用法和原理-CSDN博客

1. 效率提升

1.1 改进之前

public class FruitAdapter extends ArrayAdapter<Fruit> {
    private int resourceId;

    public FruitAdapter(@NonNull Context context, int textViewResourceId, @NonNull List<Fruit> objects) {
        super(context, textViewResourceId, objects);
        resourceId = textViewResourceId;
    }

    // 重写getView
    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        Fruit fruit = getItem(position);
        View view = LayoutInflater.from(getContext()).inflate(resourceId, parent, false);
        ImageView fruitImage = (ImageView) view.findViewById(R.id.fruit_image);
        TextView fruitName = (TextView) view.findViewById(R.id.fruit_name);
        fruitImage.setImageResource(fruit.getImageId());
        fruitName.setText(fruit.getName());
        return view;
    }

    class ViewHolder {
        ImageView fruitImage;
        TextView fruitName;
    }
}

上面的代码中FruitAdapter定义了一个主构造函数,用于将Activity的实例、ListView子项布局的id和数据源传递进来。另外又重写了getView()方法,这个方法**在每个子项被滚动到屏幕内的时候会被调用 **。 在getView()方法中,首先使用LayoutInflater来为这个子项加载我们传入的布局。 LayoutInflater的inflate()方法接收3个参数,前两个参数我们已经知道是什么意思了, 第三个参数指定成false,表示只让我们在父布局中声明的layout属性生效,但不会为这个 View添加父布局。因为一旦View有了父布局之后,它就不能再添加到ListView中了。接下来调用View的findViewById()方法分别获取到ImageView和 TextView的实例,然后通过getItem()方法得到当前项的Fruit实例,并分别调用它们的 setImageResource()和setText()方法设置显示的图片和文字,最后将布局返回,这样自定义的适配器就完成了。

1.2 改进之后

之所以说ListView这个控件很难用,是因为它有很多细节可以优化,其中运行效率就是很重要 的一点。目前我们ListView的运行效率是很低的,因为在FruitAdapter的getView()方法 中,在每个子项被滚动到屏幕内的时候会被调用,每次都将布局重新加载了一遍,即每次都会调用LayoutInflater加载视图类,当ListView快速滚动的时候,这就会成为性能的瓶颈。 仔细观察你会发现,getView()方法中还有一个convertView参数,这个参数用于将之前加载好的布局进行缓存,以便之后进行重用。当 当前屏幕中的部分视图划出屏幕(即在当前屏幕中不可见的时候),安卓系统会将这些视图回收至缓存池中,我们可以借助这个参数来进行性能优化,即 最新滑进当前窗口的视图对象可以不用重新创建,而是复用缓存池中之前回收的视图。修改 FruitAdapter中的代码,如下所示:

public class FruitAdapter extends ArrayAdapter<Fruit> {
    
   ......
       
    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        Fruit fruit = getItem(position);
        View view;
        if (convertView == null) {
            view = LayoutInflater.from(getContext()).inflate(resourceId, parent, false);
        }else {
            view = convertView;
        }
        ImageView fruitImage = (ImageView) view.findViewById(R.id.fruit_image);
        TextView fruitName = (TextView) view.findViewById(R.id.fruit_name);
        fruitImage.setImageResource(fruit.getImageId());
        fruitName.setText(fruit.getName());
        return view;
    }
}

可以看到,现在我们在getView()方法中进行了判断:如果convertView为null,即缓存池中没有可以复用的视图对象,则使用 LayoutInflater去加载布局;如果不为null,即缓存池中有之前回收的,现在可以复用的视图对象,则直接对convertView进行重用,就不用每次都调用LayoutInflater加载视图类。这样就大 大提高了ListView的运行效率,在快速滚动的时候可以表现出更好的性能。

2. ViewHolder

不过,目前我们的这份代码还是可以继续优化的,虽然现在已经不会再重复去加载布局,但是 每次在getView()方法中仍然会调用View的findViewById()方法来获取一次控件的实例,findViewByld要像遍历树型结构那样去遍历xml文件内容,所以非常消耗系统资源, 我们可以借助一个ViewHolder来对这部分性能进行优化,修改FruitAdapter中的代码,如 下所示:

public class FruitAdapter extends ArrayAdapter<Fruit> {
    
    ......

    @NonNull
    @Override
    public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
    
        Fruit fruit = getItem(position); // 获取当前位置的Fruit对象
        View view;
        ViewHolder viewHolder;
        if (convertView == null) {
            // 如果convertView为空,说明是新的视图,需要加载布局并初始化ViewHolder
            view = LayoutInflater.from(getContext()).inflate(resourceId, parent, false);
            viewHolder = new ViewHolder();
            // 初始化水果图片视图
            viewHolder.fruitImage = (ImageView) view.findViewById(R.id.fruit_image);
            // 初始化水果名称视图
            viewHolder.fruitName = (TextView) view.findViewById(R.id.fruit_name); 
            view.setTag(viewHolder); // 将ViewHolder对象与视图关联
        } else {
            // 如果convertView不为空,说明是复用的视图,直接获取关联的ViewHolder
            view = convertView;
            viewHolder = (ViewHolder) view.getTag();
        }
        
        // 使用ViewHolder设置视图内容
        viewHolder.fruitImage.setImageResource(fruit.getImageId()); // 设置水果图片
        viewHolder.fruitName.setText(fruit.getName()); // 设置水果名称
        
        return view; // 返回准备好的视图
    }


    class ViewHolder {
        ImageView fruitImage;
        TextView fruitName;
    }
}

我们新增了一个内部类ViewHolder,用于对ImageView和TextView的控件实例进行缓存。当convertView为null的时候,创建一个 ViewHolder对象,并将控件的实例存放在ViewHolder里,这样后面复用的时候就不用再调用findViewById来查找了,然后调用View的setTag()方 法,将ViewHolder对象存储在View中。当convertView不为null的时候,则调用View的 getTag()方法,把ViewHolder重新取出。这样所有控件的实例都缓存在了ViewHolder里, 就没有必要每次都通过findViewById()方法来获取控件实例了。