前言
本文将从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
用的是View
的tag
, 其实我们还可以用这个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
也来自该博客)