1、ListView基本概念
列表显示需要三个元素:
- ListView:用来展示列表的View。
- 适配器:用来把数据映射到ListView上
- 数据:具体的将被映射的字符串,图片或基本组件
适配器类型分为三种:ArrayAdapter,SimpleAdapter和SimpleCursorAdapter。
1.1、ArrayAdapter
ArrayAdapter是BaseAdapter的派生类,在BaseAdapter的基础上,添加了一项重大的功能:可以直接使用泛型构造。
ArrayAdapter内部维护了一个List<T>,getItem(position)时直接获取List.get(position)。
可添加text和object。
我们先来看一个简单的例子:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ListView listView = (ListView) this.findViewById(R.id.list);
UserAdapter adapter = new UserAdapter(this, R.layout.list_item);
adapter.add(new User(10, "小智", "男"));
adapter.add(new User(10, "小霞", "女"));
listView.setAdapter(adapter);
} @Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.main, menu);
return true;
} class UserAdapter extends ArrayAdapter<User> {
private int mResourceId; public UserAdapter(Context context, int textViewResourceId) {
super(context, textViewResourceId);
this.mResourceId = textViewResourceId;
} @Override
public View getView(int position, View convertView, ViewGroup parent) {
User user = getItem(position);
LayoutInflater inflater = getLayoutInflater();
View view = inflater.inflate(mResourceId, null);
TextView nameText = (TextView) view.findViewById(R.id.name);
TextView ageText = (TextView) view.findViewById(R.id.age);
TextView sexText = (TextView) view.findViewById(R.id.sex); nameText.setText(user.getName());
ageText.setText(user.getAge());
sexText.setText(user.getSex()); return view;
}
} class User {
private int mAge;
private String mName;
private String mSex; public User(int age, String name, String sex) {
this.mAge = age;
this.mName = name;
this.mSex = sex;
} public String getName() {
return this.mName;
} public String getAge() {
return this.mAge + "";
} public String getSex() {
return this.mSex;
}
}
这里自定义了一个ArrayAdapter,有关于Adapter的使用在之前的SimpleAdapter中已经涉及到了,所以这里直接就是以自定义的ArrayAdapter作为例子。
我们这里需要将学生的信息罗列出来,需要三个TextView:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" > <TextView
android:id="@+id/name"
android:layout_width="wrap_content"
android:layout_height="wrap_content" /> <TextView
android:id="@+id/age"
android:layout_width="wrap_content"
android:layout_height="wrap_content" /> <TextView
android:id="@+id/sex"
android:layout_width="wrap_content"
android:layout_height="wrap_content" /> </LinearLayout>
在自定义ArrayAdapter的时候,最神奇的地方就是我们可以指定ArrayAdapter绑定的数据类型,可以是基本数据类型,也可以是自定义的对象类型,像是这次的User类型。对于自定义的ArrayAdapter的构造方法,存在很多形式,这次是传进一个View的资源Id,但是我们也可以指定绑定的数据类型。
ArrayAdapter的神奇之处就是我们竟然可以像是操作Array一样来操作ArrayAdapter!像是例子中的添加操作,而其他的适配器都是需要传进一个容器的。ArrayAdapter为什么可以处理对象类型的数据呢?其实,ArrayAdapter是使用数组中对象的toString()方法来填充指定的TextView,所以我们可以通过重写对象的toString()方法来自定义ListView的显示。
@Override
public View getView(int position, View convertView, ViewGroup parent) {
User user = getItem(position);
LayoutInflater inflater = getLayoutInflater();
View view = inflater.inflate(mResourceId, null); TextView text = (TextView) view.findViewById(R.id.info);
text.setText(user.toString());
return view;
} class User {
private int mAge;
private String mName;
private String mSex; public User(int age, String name, String sex) {
this.mAge = age;
this.mName = name;
this.mSex = sex;
} @Override
public String toString() {
return "姓名:" + mName + " " + "年龄:" + mAge + " " + "性别:" + mSex;
}
}
这样我们可以只在一行中显示所有数据。
使用ArrayAdapter最大的疑问就是我们是否需要将一个现成的容器传入ArrayAdapter中?原本ArrayAdapter本身就用一般容器的基本操作,像是添加新的元素等,但它本身并不能完成当成容器使用,我们更多的时候是要将一个容器中的元素交给ArrayAdapter,由后者决定它的显示形式。
class UserAdapter extends ArrayAdapter<User> {
private int mResourceId; public UserAdapter(Context context, int textViewResourceId,
List<User> users) {
super(context, textViewResourceId, users);
this.mResourceId = textViewResourceId;
} @Override
public View getView(int position, View convertView, ViewGroup parent) {
User user = getItem(position);
LayoutInflater inflater = getLayoutInflater();
View view = inflater.inflate(mResourceId, null); TextView text = (TextView) view.findViewById(R.id.info);
text.setText(user.toString());
return view;
}
}
List<User> users = new ArrayList<User>();
users.add(new User(10, "小智", "男"));
users.add(new User(10, "小霞", "女"));
UserAdapter adapter = new UserAdapter(this, R.layout.list_item, users);
listView.setAdapter(adapter);
如果我们将ArrayAdapter绑定的数据类型定义为Object,我们可以*的传入任何类型的容器而不需要任何有关类型转换的操作!
ArrayAdapter不仅仅是可以显示TextView,它当让也像是其他Adapter一样,可以显示任何其他非TextView的组件:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ListView listView = (ListView) this.findViewById(R.id.list);
List<Object> users = new ArrayList<Object>();
users.add(10);
users.add(11);
UserAdapter adapter = new UserAdapter(this, R.layout.list_item,
R.id.info, users);
listView.setAdapter(adapter);
} @Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.main, menu);
return true;
} class UserAdapter extends ArrayAdapter<Object> {
private int mResourceId; public UserAdapter(Context context, int resourceId,
int textViewResourceId, List<Object> users) {
super(context, resourceId, textViewResourceId, users);
this.mResourceId = resourceId;
} @Override
public View getView(int position, View convertView, ViewGroup parent) {
Object user = getItem(position);
LayoutInflater inflater = getLayoutInflater();
View view = inflater.inflate(mResourceId, null); TextView text = (TextView) view.findViewById(R.id.info);
text.setText(user.toString());
return view;
}
}
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" > <Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="点击" /> <TextView
android:id="@+id/info"
android:layout_width="wrap_content"
android:layout_height="wrap_content" /> </LinearLayout>
如果我们的布局中需要其他组件,必须指定该布局中用于显示ArrayAdapter中数据的TextView的Id。
如果只是方便绑定数据的话,其实是没有必要专门独立个ArrayAdapter出来,只要覆写getView()就可以,正如使用容器就是为了方便大量数据的处理一样的道理,使用ArrayAdapter也是为了处理数据较大的情况,像是超过100条或者频繁动态增删数据时,就可以使用ArrayAdapter,而且,为了方便我们刷新UI,ArrayAdapter也提供了setNotifyOnChange()方法,这样可以降低UI的处理量,使得刷新UI更加快速,主要是通过停止对add,insert,remove和clear的操作来实现这点。
1.2、SimpleAdapter
固定接口,直接传入一个List<? extends Map<String, ?>> data,无add接口。
要构造一个SimpleAdapter,需要以下的参数:
1.Context context:上下文,这个是每个组件都需要的,它指明了SimpleAdapter关联的View的运行环境,也就是我们当前的Activity。
2.List<? extends Map<String, ?>> data:这是一个由Map组成的List,在该List中的每个条目对应ListView的一行,每一个Map中包含的就是所有在from参数中指定的key。
3.int resource:定义列表项的布局文件的资源ID,该资源文件至少应该包含在to参数中定义的ID。
4.String[] from:将被添加到Map映射上的key。
5.int[] to:将绑定数据的视图的ID跟from参数对应,这些被绑定的视图元素应该全是TextView。
private ListView mListView;
private LinearLayout mLayout; @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); mLayout = new LinearLayout(this);
mLayout.setOrientation(LinearLayout.VERTICAL);
mListView = new ListView(this);
LinearLayout.LayoutParams param = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT);
mLayout.addView(mListView, param);
setContentView(mLayout);
Map<String, String> keyValuePair = new HashMap<String, String>();
keyValuePair.put("Name", "小智");
keyValuePair.put("Age", "10");
List<Map<String, String>> list = new ArrayList<Map<String, String>>();
list.add(keyValuePair); ListAdapter adapter = new SimpleAdapter(this, list,
android.R.layout.simple_list_item_2, new String[] { "Name",
"Age" }, new int[] { android.R.id.text1,
android.R.id.text2 }); mListView.setAdapter(adapter);
} @Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
上面的例子中我们是手动的添加视图,然后使用的是系统默认的视图元素,像是android.R.id.text1。当然,我们也可以自定义TextView的样式,而且,说是应该全是TextView,也只是应该,并不是绝对的:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main); ListView listView = (ListView) this.findViewById(R.id.list);
List<Map<String, ?>> list = new ArrayList<Map<String, ?>>();
for (int i = 0; i < 5; i++) {
Map<String, String> keyValuePair = new HashMap<String, String>();
keyValuePair.put("Text", "Text" + i);
keyValuePair.put("Button", "Button" + i);
list.add(keyValuePair);
} ListAdapter adapter = new SimpleAdapter(this, list, R.layout.listitem,
new String[] { "Text", "Button" }, new int[] { R.id.text,
R.id.button }); listView.setAdapter(adapter);
从这里我们可以看到,要想使用ListView,我们应用程序的主界面必须包含ListView,然后ListView的内容可以自己定义,而不仅仅是TextView。
要想知道这是什么回事,我们就要知道SimpleAdapter是如何绑定数据到视图的,这个过程我们甚至可以自定义:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main); ListView listView = (ListView) this.findViewById(R.id.list);
List<Map<String, String>> list = new ArrayList<Map<String, String>>();
for (int i = 0; i < 3; i++) {
Map<String, String> keyValuePair = new HashMap<String, String>();
keyValuePair.put("text", "text" + i);
list.add(keyValuePair);
} CustomSimpleAdapter adapter = new CustomSimpleAdapter(this, list,
R.layout.listitem); listView.setAdapter(adapter);
class CustomSimpleAdapter extends SimpleAdapter {
private int mResource;
private List<? extends Map<String, ?>> mData; public CustomSimpleAdapter(Context context,
List<? extends Map<String, ?>> data, int resource) {
super(context, data, resource, null, null);
this.mResource = resource;
this.mData = data;
} @Override
public View getView(int position, View convertView, ViewGroup group) {
LayoutInflater layoutInflater = getLayoutInflater();
View view = layoutInflater.inflate(mResource, null);
TextView text = (TextView) view.findViewById(R.id.text);
text.setText(mData.get(position).get("text").toString());
if (position == 2) {
text.setTextColor(Color.RED);
}
return view;
}
}
要想实现自定义的ListView,最主要的是实现getView(),因为SimpleAdapter的数据绑定就是发生在这里。
现在我们可以总结一下SimpleAdapter的数据绑定是怎样的:利用传入的view(该view包含ListView每行要渲染的视图元素)的ResourceID得到该view,然后通过每个vie所在的索引,也就是它们的行数,得到data中相应内容的key,接着就是利用这些key的value填充这些视图元素,最后返回view作为ListView每行的内容显示出来。
由此可见,from和to并不是必须的,要想实现ListView,前三个参数才是必要的,也许大家会看到网上有些例子为了实现自定义的SimpleAdapter,会覆写它的许多方法,其实如果单纯只是想要利用SimpleAdapter来实现自定义的ListView,只要覆写getView()就行,其他的完全可以交给SimpleAdapter原先的方法来做,除非我们有特殊的要求。
SimpleAdapter并不仅仅用在ListView上,事实上,Spinner同样可以使用:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main); Spinner spinner = (Spinner) this.findViewById(R.id.spinner);
List<Map<String, ?>> list = new ArrayList<Map<String, ?>>();
for (int i = 0; i < 5; i++) {
Map<String, String> keyValuePair = new HashMap<String, String>();
keyValuePair.put("Text", "Text" + i);
list.add(keyValuePair);
} SimpleAdapter adapter = new SimpleAdapter(this, list,
R.layout.listitem, new String[] { "Text" },
new int[] { R.id.text }); spinner.setAdapter(adapter);
}
1.3、 SimpleCursorAdapter
SimpleCursorAdapter,用于将Cursor中的columns与XML文件中定义的TextView或者ImageView进行匹配的Adapter。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main); Map<String, String> map = new HashMap<String, String>(); ListView listView = (ListView) this.findViewById(R.id.list);
Cursor cursor = getContentResolver().query(
ContactsContract.Contacts.CONTENT_URI, null, null, null, null);
if(cursor != null){ startManagingCursor(cursor); }
ListAdapter adapter = new SimpleCursorAdapter(this,
android.R.layout.simple_list_item_1, cursor,
new String[] { PhoneLookup.DISPLAY_NAME },
new int[] { android.R.id.text1 });
listView.setAdapter(adapter);
stopManagingCursor();
}
这只是简单的获取联系人姓名的例子而已,当然,为了能够运行该例子,我们需要添加下面的权限:
<uses-permission android:name="android.permission.READ_CONTACTS" />
这里有一个方法很值得我们注意:startManagingCursor()。它的使用是基于这样的前提:游标结果集里有很多的数据记录,像是通讯录这样的结果集,肯定符合要求。使用该方法的目标主要是把获取的Cursor对象交给Activity管理,这样Cursor的生命周期就和Activity自动同步了,这样在Activity结束的时候就能自动结束Cursor的使用。使用前最好是先判断Cursor是否为空,以免发生错误,而且使用后也要用stopManagingCursor()方法来停掉它。
SimpleCursorAdapter除了数据来源指定是数据库之外,就和SimpleAdapter的用法几乎一样了。
1.4、ListView工作原理
ListView针对每个item,要求adapter返回一个视图(getView),也就是说ListView在开始绘制的时候,系统首先调用getCount()函数,根据其返回值得到ListView的长度,然后根据这个长度调用getView()一行一行的绘制ListView的每一项。如果getCount()返回值是0,则列表一行都不会显示,如果返回1,就只显示一行。如果有几千几万甚至更多的item要显示怎么办?为每个item创建一个新的View?不可能。实际上Android早已经缓存了这些视图,如下图所示。
- 如果有很多item时,只有可见的项目存在内存中,其他的在Recycler中。
- ListView先请求一个type1视图(getView()),然后请求其他可见的项目。convertView在getView中是null的。
- 当item1滚出屏幕,且一个新的项目从屏幕低端上来时,ListView再请求一个type1视图。convertView此时不是空值了,它的值是item1.只需设定新的数据然后返回convertView,不必重新创建一个视图。
下面通过示例代码来具体演示一下。
public class MultipleItemsList extends ListActivity {
private MyCustomAdapter mAdapter;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mAdapter = new MyCustomAdapter();
for (int i = 0; i < 50; i++) {
mAdapter.addItem("item " + i);
}
setListAdapter(mAdapter);
} private class MyCustomAdapter extends BaseAdapter {
private ArrayList<String> mData = new ArrayList<String>();
private LayoutInflater mInflater; public MyCustomAdapter() {
mInflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
} public void addItem(final String item) {
mData.add(item);
notifyDataSetChanged();
} @Override
public int getCount() {
return mData.size();
} @Override
public String getItem(int position) {
return (String) mData.get(position);
} @Override
public long getItemId(int position) {
return position;
} @Override
public View getView(int position, View convertView, ViewGroup parent) {
System.out.println("getView " + position + " " + convertView);
ViewHolder holder = null;
if (convertView == null) {
convertView = mInflater.inflate(R.layout.listview, null);
holder = new ViewHolder();
holder.textView = (TextView) convertView.findViewById(R.id.text);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
holder.textView.setText(mData.get(position));
return convertView;
}
} public static class ViewHolder {
public TextView textView;
}
}
执行程序,查看日志:
getView 被调用 9 次 ,convertView 对于所有的可见项目是空值(如下):
然后稍微向下滚动List,直到item10出现:
convertView仍然是空值,因为recycler中没有视图(item1的边缘仍然可见,在顶端)再滚动列表,继续滚动:
convertView不是空值了!item1离开屏幕到Recycler中去了,然后item11被创建,再滚动下:
此时的convertView非空了,在item11离开屏幕之后,它的视图(…0f8)作为convertView容纳item12了,
2、ListView优化
2.1、复用convertView,减少findViewById的次数
- 复用convertView
Android系统本身为我们考虑了ListView的优化问题,在复写的Adapter的类中,比较重要的两个方法是getCount()和getView()。界面上有多少个条目显示,就好调用多少次的getView()方法;因此如果在每次调用的时候,如果不进行优化,每次都会使用View.inflate(...)的方法,都要将xml文件解析并显示到界面上,这是非常消耗资源的:因为有新的内容产生就会有旧的内容销毁,因此可以复用旧的内容。
优化:在getView()方法中,系统为我们提供了一个复用view的历史缓存对象convertView,当显示第一屏的时候,每一个item都会新创建一个view对象,这些view都是可以被复用的。所以可以在convertView不为null的时候,对其进行复用。
2.缓存item条目的引用——ViewHolder
findViewById()这个方法是比较耗性能的操作,因此这个方法要找到指定的布局文件,进行不断地解析每个节点:从最顶端的节点进行一层一层的解析查询,找到后再一层一层的返回,如果在左边没找到,就会继续解析右边。因此可以对findViewById进行优化处理,需要注意的是:
特点:xml文件被解析的时候,只要被创建出来了,其孩子的id就不会改变了。根据这个特点,可以将孩子id存人到制定的集合中,每次可以直接取出集合中对应的元素就可以了。
优化:在创建view对象的时候,减少布局文件转化成view对象的次数;即在创建view对象的时候,把所有孩子全部找到,并把孩子的引用存起来。具体方法如下:
1、定义存储控件引用的类ViewHolder
这里的ViewHolder类是否需要定义成static,要根据实际情况而定,如果item不是很多的话,可以使用,这样在初始化的时候只加载一次。不过如果item过多的话,建议不要使用。因此static是Java中的一个关键字,当用它来修饰成员变量时,该变量就属于该类,而不是该类的实例。所以用static修饰的变量生命周期很长,如果用它来引用一些资源消耗过多的实例,会出现问题。
class ViewHolder{
//定义item中相应的控件
}
2、创建自定义的类:ViewHolder holder = null;
3、将子View添加到holder中。在创建新的listView时,创建新的ViewHolder,把所有孩子全部找到,并把孩子的引用存起来;通过view.setTag(holder)将引用设置到view中;通过holder,将孩子view设置到此holder中,从而减少以后查询的次数。
4、在复用listView中的条目时,通过view.getTag(),将view对象转化为holder,即转化成相应的引用,方便在下次使用的时候存入集合。通过view.getTag(holder)获取引用。
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View view;
ViewHolder holder;
// 判断convertView的状态,来达到复用效果
if (null == convertView) {
// 如果convertView为空,则表示第一次显示该条目,需要创建一个view
view = View.inflate(MainActivity.this, R.layout.listview_item,
null);
//新建一个viewholder对象
holder = new ViewHolder();
//将findviewbyID的结果赋值给holder对应的成员变量
holder.tvHolder = (TextView) view.findViewById(R.id.tv_item);
// 将holder与view进行绑定
view.setTag(holder);
} else {
// 否则表示可以复用convertView
view = convertView;
holder = (ViewHolder) view.getTag();
}
// 直接操作holder中的成员变量即可,不需要每次都findViewById
holder.tvHolder.setText(list.get(position));
return view;
}
根据Google的文档,实际优化效果在百分之5左右。
2.2、ListView中数据的分批及分页加载
需求:ListView有一万条数据,如何显示;如果将十万条数据加载到内存,很消耗内存。
解决方法:优化查询的数据,先获取几条数据显示到界面上。
进行分批处理——优化了用户体验。如:1000条新闻的List集合,一次加载20条,等到翻页到底部时,再添加下面20条。这样用户一次只需要等待20条数据的传输时间,不需要一次等待好几分钟把数据都加载玩再在ListView上显示。
进行分页处理——优化了内存空间。如:假如有10万条数据,如果顺利读到最后这个List集合中还是会累积海量条数据,有可能会造成OOM。这时要用到分页,比如可以将这10万条数据分为1000页,每一页100条数据,每一页加载时都覆盖掉上一页中List集合中的内容,然后每一页内再使用分批加载。
说明:一般数据都是从数据库中获取的,实现分批(分页)加载数据,就需要在对应的DAO中有相应的分批(分页)获取数据的方法,如findPartDatas()
- 准备数据:在dao中添加分批加载数据的方法findPartDatas(),在适配数据的时候先加载第一批的数据,需要加载第二批的时候,设置监听检测何时加载第二批。
- 设置ListView的滚动监听器:setOnScrollListener(new OnScrollListener{...})
- 在监听器中有两个方法:滚动状态发生变化的方法(onScrollStateChanged)和listView被滚动时调用的方法(onScroll)
- 在滚动状态发生改变的方法中,有三种状态:
手指按下移动的状态: SCROLL_STATE_TOUCH_SCROLL: // 触摸滑动
惯性滚动(滑翔(flgin)状态): SCROLL_STATE_FLING: // 滑翔
静止状态: SCROLL_STATE_IDLE: // 静止
- 对不同的状态进行处理:分批加载数据,只关心静止状态;关心最后一个可见的条目,如果最后一个可见条目就是数据适配器(集合)里的最后一个,此时可加载更多的数据。在在每次加载的时候,计算出滚动的数量,当滚动的数量大于等于总数量的时候,可以提示用户无更多数据。
2.3、复杂ListView的处理:
说明:ListView的界面显示是通过getCount和getView这两个方法来控制的。getCount返回有多少个条目;getView返回每个位置条目显示的内容。
提供思路:对于含有多个类型的item的优化处理:由于ListView只有一个Adapter的入口,可以定义一个总的Adapter入口,存放各种类型的Adapter。
- 定义两个(或多个)集合:每个集合中存入的是对应不同类型的内容
- 在初始化数据(填充数据)中初始化两个集合
- 在数据适配器中,复写对应的方法。getCount():计算所有需要显示的条目的个数,这里包括listView和textView;getView():对显示在不同位置的条目进行if处理。
- 数据类型的判断:需要注意的是,在复用view的时候,需要对convertView进行类型判断,是因为这里含有各种不同类型的view,在view滚动显示的时候,对于不同类型的view不能复用,所以需要类型判断。
2.4、ListView中图片的优化:
- 处理图片的方式:如果自定义Item中有涉及到图片等等的,一定要注意处理图片,图片占的内存是ListView项中最恶心的,处理图片的方法大致有以下几种:
- 不要直接拿路径就去循环decodeFile();使用Option保存图片大小、不要加载图片到内存中
- 拿到的图片一定要经过边界压缩
- 在ListView中取图片时也不要直接拿个路径去取图片,而是以WeakReference、SoftReference、WeakHashMap等来存储图片信息(注:是图片信息不是图片)
- 在getView中做图片转换时,产生的中间变量一定及时释放
- 异步加载图片基本思想:
- 先从内存缓存中获取图片显示(内存缓存)
- 获取不到的话从SD卡中获取(SD卡缓存)
- 都获取不到的话从网络下载图片并保存到SD卡同时加入内存并显示
原理:
优化一:先从内存中加载,没有则开启线程从SD卡或网络中获取,这里注意从SD卡获取图片是放在子线程里执行的,否则快速滑屏的话会不够流畅。
优化二:与此同时,在adapter里有个busy变量,表示listview是否处于滑动状态,如果是滑动状态则仅从内存中获取图片,没有的话无需再开启线程去外存或网络获取图片
优化三:ImageLoader里的线程使用了线程池,从而避免了过多线程频繁创建和销毁。不能每次总是new一个线程去执行,可以使用AsyncTask类。在从网络获取图片时,先是将其保存到sd卡,然后再加载到内存,这么做的好处是在加载到内存时可以做个压缩处理,以减少图片所占内存。
Tips:这里可能出现图片错位问题:
图片错位问题的本质源于我们的listview使用了缓存convertView,假设一种场景,一个listview一屏显示九个item,那么在拉出第十个item时,事实上该item是重复使用了第一个item,也就是说在第一个item从网络中下载图片并最终要显示的时候,其实该item已经不在当前显示区域内了,此时显示的后果将可能在第十个item上输出图像,这就导致了图片错位的问题。所以解决之道在于可见则显示,不可见则不显示。在ImageLoader里有个imageView的map对象,就是用于保存当前显示区域图像对应的url集,在显示前判断处理一下即可。
3.内存缓冲机制:
首先限制内存图片缓冲的堆内存大小,每次有图片往缓存里添加时,判断是否超过限制大小,超过的话就从中取出最少使用的图片并将其移除。
当然这里如果不采用这种方式,换做软引用也是可行的,二者目的皆是最大程度的利用已存在于内存中的图片缓存,避免重复制造垃圾增加GC负担。
OOM溢出往往皆因内存瞬时大量增加而垃圾回收不及时造成。只不过二者区别在于LinkedHashMap里的图片缓存在没有移除出去之前是不会被GC回收的,而SoftReference里的图片缓存在没有其他引用保存时随时都会被GC回收。所以在使用LinkedHashMap这种LRU算法缓存更有利于图片的有效命中,当然二者配合使用的话效果更佳,即从LinkedHashMap里移除出的缓存放到SoftReference里,这就是内存的二级缓存。
2.5、ListView的其他优化:
- 尽量避免在BashAdapter中使用static来定义全局静态变量。
static是Java中的一个关键字,当用它来修饰成员变量时,那么该变量就属于该类,而不是该类的实例。所以用static修饰的变量,它的生命周期是很长的,如果用它来引用一些资源耗费过多的实例(比如Context的情况最多),这时就要尽量避免使用了。 - 尽量使用getApplicationContext:
如果为了满足需求下必须使用Context的话,Context尽量使用Application Context,因为Application的Context的生命周期比较长,引用它不会出现内存泄露的问题。 - 尽量避免在ListView适配器中使用线程:
因为线程产生内存泄露的主要原因在于线程生命周期的不可控制。之前使用的自定义ListView中适配数据时使用AsyncTask自行开启线程的,这个比用Thread更危险,因为Thread只有在run函数不结束时才出现这种内存泄露问题,然而AsyncTask内部的实现机制是运用了线程执行池(ThreadPoolExcutor),这个类产生的Thread对象的生命周期是不确定的,是应用程序无法控制的,因此如果AsyncTask作为Activity的内部类,就更容易出现内存泄露的问题。
解决方法如下:- 将线程的内部类,改为静态内部类。
- 在线程内部采用弱引用保存Context引用
参考: http://mobile.51cto.com/abased-410889.htm
http://www.cnblogs.com/wenjiang/p/3196205.html
http://www.cnblogs.com/wenjiang/p/3189082.html