ListView[一]

ListView基本使用

Posted by 袁平 on December 12, 2018

前言

本文将从ListView的基本使用入手, 介绍常见的ListView性能优化问题

ListView采用适配器模式, 关于ListView的优化主要体现在Adapter上; 本文也主要从Adapter入手, 分析常见的优化问题

文章代码基于kotlin


正文


一. 最差版本

ListView的性能问题主要是其通常用于展示大量数据造成的; 我们手指上下滑动的时候伴随着Adapter.getView()方法的不断调用, getView()的主要作用是去解析XML生成View视图, 但是这一过程是一个耗时操作, 如果每次滑动都去inflate的话, 势必会造成界面的卡顿, 如下即是这种情况: 每次都去inflate一个Item, 然后再findViewById()再进行设置

// 最差版本
class MyAdapter(val context: Context) : BaseAdapter() {

    var data = ArrayList<String>()
    private val mInflater by lazy {
        context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
    }

    override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
        val view = mInflater.inflate(R.layout.item_main, parent, false)
        view.setOnClickListener{
            Toast.makeText(context,data[position],Toast.LENGTH_SHORT).show()
        }
        view.findViewById<TextView>(R.id.item_text).text = data[position]
        return view
    }

    // 其他方法按照常规写, 故此省略
    ...
}

二. 优化一

为了避免每次滑动的时候都要inflate, Google官方提供了ListView的缓存机制, 即将滑动出屏幕外面的Item缓存下来, 因为通常情况下ListView要展示的数据类型都是相同的, 所以可以复用同样的Item界面, 只是展示数据不同; 我们注意到getView()的参数中有一个convertView, 这个便是被缓存下来的View, 所以我们第一次优化可以如下: 主要就是判断convertView是否为空, 如果为空, 说明没有缓存的Item, 此时就需要去inflate, 否则, 直接更改展示数据即可

class MyAdapter(val context: Context) : BaseAdapter() {

    var data = ArrayList<String>()
    private val mInflater by lazy {
        context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
    }

    override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
        if (convertView == null) {
            val view = mInflater.inflate(R.layout.item_main, parent, false)
            view.findViewById<TextView>(R.id.item_text).text = data[position]
            return view
        }
        convertView.apply {
            findViewById<TextView>(R.id.item_text).text = data[position]
        }
        return convertView
    }

    // 其他方法按照常规写, 故此省略
    ...
}

三. 优化二

到这里, 其实最耗时的部分已经被优化了, 那么我们是否还能够进行优化呢? 我们注意到每次调用getView的时候, 虽然避免了每次都去inflate, 但是我们仍然每次都调用了findViewById(), 那么这部分我们是否也可以避免掉呢, 当然是可以的, 此时我们的ViewHolder就派上用场啦~

这里我们使用了一个内部类, 将每次findViewById找到的View保存下来, 因为我们前面说过了, ListView展示的时候, 通常数据类型都是相同的, 即界面相同, 只是每个Item展示的数据不同而已, 因此也就没有必要每次都去执行一遍findViewById(), 这也是ViewHolder所做的主要的优化

另外, 还需要提一下的是这里保存ViewHolder用的是Viewtag, 其实我们还可以用这个tag保存许多我们需要的值, 而不用局限与ViewHolder

class MyAdapter(val context: Context) : BaseAdapter() {

    var data = ArrayList<String>()
    private val mInflater by lazy {
        context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
    }

    override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
        var holder: MyViewHolder
        if (convertView == null) {
            val view = mInflater.inflate(R.layout.item_main, parent, false)
            holder = MyViewHolder(view)
            holder.textView.text = data[position]
            view.tag = holder
            return view
        }
        holder = convertView.tag as MyViewHolder
        holder.textView.text = data[position]
        return convertView
    }
    
    // 其他方法按照常规写, 故此省略
    ...

    class MyViewHolder(val view: View){
        val textView: TextView by lazy {
            view.findViewById<TextView>(R.id.item_text)
        }
    }
}

四. 优化三

将异步操作放在子线程中执行; 其实这个也不算ListView自身特殊的优化, 而是由Android自身特性所决定的

如下; 当加载大图时, 使用异步的AsyncTask去网络下载好后再展示到界面

class MyAdapter(private val context: Context) : BaseAdapter() {

    private val mInflater by lazy {
        context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
    }

    override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
        val view: View = convertView ?: mInflater.inflate(R.layout.item_main, parent, false)
        val holder: ViewHolder = view.tag as ViewHolder? ?: ViewHolder(view)
        ImageLoader(holder).execute(ImageUrl.url[position])
        return view
    }

    // 其他方法按照常规写, 故此省略
    ...

    class ImageLoader(private var holder: ViewHolder) : AsyncTask<String, Void, Bitmap>() {

        private val cache: LruCache<String, Bitmap> by lazy {
            val maxMemory = Runtime.getRuntime().maxMemory()
            val cacheSize = maxMemory / 8
            object: LruCache<String, Bitmap>(cacheSize.toInt()) {
                override fun sizeOf(key: String?, value: Bitmap?): Int {
                    return value?.allocationByteCount ?: 0
                }
            }
        }

        override fun doInBackground(vararg params: String?): Bitmap? {
            val urlStr = params[0]
            var bitmap = cache.get(urlStr)
            if (bitmap != null)
                return bitmap
            val url = URL(urlStr)
            var connection: HttpURLConnection? = null
            try {
                connection = url.openConnection() as HttpURLConnection
                connection.connectTimeout = 5000
                connection.readTimeout = 5000
                bitmap = BitmapFactory.decodeStream(connection.inputStream)
                cache.put(urlStr, bitmap)
            } catch (e: Exception) {
                Log.d("@HusterYP","error", e)
            } finally {
                connection?.disconnect()
            }
            return bitmap
        }

        override fun onPostExecute(result: Bitmap?) {
            holder.imageView.setImageBitmap(result)
        }
    }
}

上述代码其实还有问题; 只是解决了异步加载, 但是由于View视图的缓存复用, 当滑动的时候会出现Item展示图片不断改变的现象; 关于此问题的解决可以参见博客 (注: 上述代码图片URL也来自该博客)