Android - Laggy ListView с помощью ImageView, LRUcache и ViewHolder
У меня есть ListView
в котором в каждой строке есть текст и два ImageView
: одно и то же для каждой строки, другое зависит от текущего элемента.
Это мой адаптер:
mArrayAdapter(Context context, int layoutResourceId, ArrayList<Exhibition> data) {
super(context, layoutResourceId, data);
this.context = context;
this.layoutResourceId = layoutResourceId;
this.list = data;
this.originalList = data;
viewHolder = new ViewHolder();
final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
final int cacheSize = maxMemory / 8;
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
return bitmap.getByteCount() / 1024;
}
};
}
@Override
@NonNull
public View getView(final int position, View convertView, @NonNull final ViewGroup parent) {
View row;
final Exhibition ex;
if(convertView==null){
row = LayoutInflater.from(getContext()).inflate(R.layout.row,parent,false);
viewHolder.expand = (ImageView)row.findViewById(R.id.expand);
row.setTag(viewHolder);
}
else {
row = convertView;
viewHolder = (ViewHolder)row.getTag();
}
ex = list.get(position);
descr = (TextView)row.findViewById(R.id.descr);
ttl = (TextView)row.findViewById(R.id.title);
city = (TextView)row.findViewById(R.id.city);
dates = (TextView)row.findViewById(R.id.dates);
museum = (TextView)row.findViewById(R.id.location);
header = (ImageView)row.findViewById(R.id.hd);
ttl.setText(ex.name);
descr.setText(ex.longdescr);
museum.setText(ex.museum);
city.setText(ex.city);
final Bitmap bitmap = getBitmapFromMemCache(ex.key);
if (bitmap != null) {
header.setImageBitmap(bitmap);
} else {
header.setImageBitmap(ex.getHeader());
addBitmapToMemoryCache(ex.key,ex.header);
}
SimpleDateFormat myFormat = new SimpleDateFormat("dd/MM/yyyy", Locale.ITALY);
Date start = new Date(ex.getStart()), end = new Date(ex.getEnd());
String startEx = myFormat.format(start);
String endEx = myFormat.format(end);
String finalDate = getContext().getResources().getString(R.string.ex_date, startEx, endEx);
dates.setText(finalDate);
viewHolder.expand.setId(position);
if(position == selectedId){
descr.setVisibility(View.VISIBLE);
ttl.setMaxLines(Integer.MAX_VALUE);
dates.setMaxLines(Integer.MAX_VALUE);
museum.setMaxLines(Integer.MAX_VALUE);
city.setMaxLines(Integer.MAX_VALUE);
}else{
descr.setVisibility(View.GONE);
ttl.setMaxLines(1);
dates.setMaxLines(1);
museum.setMaxLines(1);
city.setMaxLines(1);
}
viewHolder.expand.setOnClickListener(this.onCustomClickListener);
return row;
}
public void setDescr(int p){
selectedId = p;
}
public void setOnCustomClickListener(final View.OnClickListener onClickListener) {
this.onCustomClickListener = onClickListener;
}
public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
if (getBitmapFromMemCache(key) == null) {
mMemoryCache.put(key, bitmap);
}
}
public Bitmap getBitmapFromMemCache(String key) {
return mMemoryCache.get(key);
}
@Override
public int getCount()
{
return list.size();
}
@Override
public boolean isEnabled(int position)
{
return true;
}
@Override
public Exhibition getItem (int pos){
return list.get(pos);
}
void resetData() {
list = originalList;
}
private class ViewHolder {
ImageView expand,header;
}
@Override
@NonNull
public Filter getFilter() {
if (valueFilter == null) {
Log.d("SEARCH1","New filter");
valueFilter = new ValueFilter();
}
return valueFilter;
}
private class ValueFilter extends Filter {
@Override
protected FilterResults performFiltering(CharSequence constraint) {
FilterResults results = new FilterResults();
if(constraint == null || constraint.length() == 0){
results.values = originalList;
results.count = originalList.size();
}
else {
List<Exhibition> nExhList = new ArrayList<>();
for(Exhibition e : list){
Log.d("NAMEE",e.name + " " + constraint.toString());
if (e.getName().toUpperCase().contains(constraint.toString().toUpperCase()) || e.getCity().toUpperCase().contains(constraint.toString().toUpperCase())
||e.getMuseum().toUpperCase().contains(constraint.toString().toUpperCase()) || e.getLongDescription().toUpperCase().contains(constraint.toString().toUpperCase())
|| e.getDescription().toUpperCase().contains(constraint.toString().toUpperCase()) || e.getCategory().toUpperCase().contains(constraint.toString().toUpperCase())){
nExhList.add(e);
}
}
results.values= nExhList;
results.count=nExhList.size();
}
return results;
}
@Override
protected void publishResults(CharSequence constraint,
FilterResults results) {
if(results.count==0){
notifyDataSetInvalidated();
}
else{
list = (ArrayList<Exhibition>)results.values;
notifyDataSetChanged();
}
}
}
Первый ImageView
это Bitmap
хранится в Exhibition
переменная. Второй изменяет видимость текста, чтобы получить эффект, похожий на расширяемый (потому что сейчас я не могу преобразовать ListView
к ExpandableListView
). Я пробовал разные вещи, такие как кэш, AsyncTask
, удалив пользовательский клик слушателя, положи все в ViewHolder
но прокрутка полна микролагов. Что-то не так в адаптере, что я не понимаю?
2 ответа
Есть несколько вещей, которые вы можете сделать, чтобы улучшить производительность.
Выяснить, что именно медленно
Узнайте о профилировании, которое может сказать вам, какие функции вызывают чаще всего и / или для выполнения которых требуется больше всего времени. Таким образом, вы можете решить, куда тратить время на исправление или изменение кода.
См. https://developer.android.com/studio/profile/android-profiler и https://developer.android.com/studio/profile/
Шаблон ViewHolder
Вы неправильно используете шаблон ViewHolder. В вашем коде у вас есть один экземпляр ViewHolder
в адаптере viewHolder
поле. Затем вы используете это поле внутри getView()
функционировать как обычная локальная переменная.
Вы тогда звоните row.findViewById()
несколько раз, даже если convertView
не было null
, findViewById()
вызовы медленны, и преимущество держателя представления состоит в том, что вам нужно вызывать его только один раз для каждого представления после раскрытия (в нулевой ветви convertView== if
).
Вместо этого вы должны иметь по 1 держателю вида на просмотр строки. Обратите внимание, что вы не создаете новый ViewHolder
назначить с setTag()
, но вы используете тот же самый. Тогда вместо переменных, таких как descr
, ttl
, city
, должны быть поля ViewHolder
и, следовательно, быстро ссылаться.
Создание ненужных объектов
Распределение памяти также медленно.
Вы также каждый раз создаете объекты getView()
Вызывается, что вы можете вместо этого создать один раз и просто использовать повторно.
Одним из таких примеров является SimpleDateFormat
это можно создать один раз в конструкторе адаптера и просто использовать для создания текста.
Посмотрите, как вы можете избежать создания так много String
объекты также. Форматирование с помощью строкового буфера или чего-то подобного. Вы не показываете исходный код Exhibition
класс, так что не ясно, почему существует необходимость создания Date
объект с результатом вызова getStart()
а также getEnd()
,
Если поля "начало" и "конец" Exhibition
объекты никогда не используются как long
s, рассмотрите возможность превращения их в неизменные Date
s во время анализа JSON, а не каждый раз, когда они используются.
Потенциальные медленные вызовы в потоке пользовательского интерфейса
Исходный код для Exhibition
класс не показан, поэтому мы не можем сказать, что Exhitition.getHeader()
функция делает. Если имеется загрузка и / или декодирование растрового изображения, перемещение его в фоновый поток (и обновление после того, как растровое изображение будет готово) улучшит ListView
прокрутка производительности.
Ненужные звонки
Есть звонки, которые выполняются, даже если они не нужны. Например, назначение слушателя по щелчку в конце getView()
, Вы можете сойти с установки только один раз, когда вы делаете инфляцию (когда convertView
является null
) поскольку все строки используют один и тот же слушатель.
Избегайте заполнения памяти
Вы упомянули, что каждый Exhibition
объект имеет Bitmap
поле, которое устанавливается при разборе JSON. Это означает, что все растровые изображения постоянно находятся в памяти. Это означает, что в этом случае кэш-память LRU не требуется, поскольку всегда существует сильная ссылка на растровые изображения.
Это также означает, что с увеличением количества элементов в списке увеличивается объем необходимой памяти. Поскольку используется больше памяти, сбор мусора (GC) должен происходить чаще, и GC работает медленно и может вызвать заикание или зависание. Профилирование может сказать вам, происходит ли зависание, которое вы испытываете из-за GC.
Кэш растровых изображений был бы полезен, если бы в памяти было только несколько растровых изображений, необходимых для элементов, которые в данный момент видны в списке, и еще несколько. Если нужное растровое изображение не найдено в кэше, его следует загрузить с диска или загрузить из сети.
PS
Имейте в виду, что у вас есть общественность setOnCustomClickListener()
функция, которая только назначает ссылку на поле. Если вы вызываете это с новым слушателем, ваш текущий код будет использовать старый слушатель во всех строках, которые не были обновлены и обновлены с новой ссылкой.
Чтобы сделать ваш список гладким, вы можете попробовать следующие варианты,
- Вместо того, чтобы использовать свой собственный способ кэширования растровых изображений, вы можете попробовать использовать популярную библиотеку, такую как Glide, Picasso или какой-либо другой с открытым исходным кодом.
- Старайтесь избегать трудоемкой операции в getView, например, при преобразовании даты вы можете перейти на уровень объекта при построении объекта модели. Это будет один раз на объект.
- Вы можете попробовать Recyclerview вместо ListView