android-----帶你一步一步優化ListView(三)
前兩篇我們介紹了一般的優化ListView方法以及DiskLruCache優化ListView,見android-----帶你一步一步優化ListView(一)和android-----帶你一步一步優化ListView(二),這一篇我們將從記憶體快取的角度來完成ListView的優化,使用的是LruCache,它的主要演算法原理是把最近使用的物件用強引用儲存在 LinkedHashMap 中,並且把最近最少使用的物件在快取值達到預設定值之前從記憶體中移除,並沒有一個固定的快取大小是符合所有應用程式的,我們應該根據自己應用程式的狀況設定出合理的快取大小,下面我們通過例項來學習一下LruCache具體怎麼使用:
首先定義顯示ListView的佈局檔案:listview.xml
接著定義每個item顯示的樣式item.xml<?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" > <ListView android:id="@+id/listView" android:layout_width="match_parent" android:layout_height="match_parent"> </ListView> </LinearLayout>
接下來就是最重要的ListViewAdapter介面卡內容了,程式碼有點長度,先貼出來,接下來慢慢解釋:<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" > <ImageView android:id="@+id/imageView" android:layout_width="50dp" android:layout_height="50dp" /> <TextView android:id="@+id/textView" android:layout_width="100dp" android:layout_height="50dp" android:layout_toRightOf="@id/imageView" android:layout_marginTop="20dp" android:layout_marginRight="70dp" /> </RelativeLayout>
public class ListViewAdapter extends BaseAdapter{
//儲存所有將要訪問的路徑
public List<String> list;
public LruCache<String, Bitmap> lruCache;
public LayoutInflater inflater;
public ListView listView;
public int reqWidth;
public int reqHeight;
public Set<ImageAsyncTask> tasks = new HashSet<ListViewAdapter.ImageAsyncTask>();
public ListViewAdapter(Context context,List<String> list,LruCache<String, Bitmap> lruCache,ListView listView,ImageView imageView)
{
this.list = list;
this.lruCache = lruCache;
this.inflater = LayoutInflater.from(context);
this.listView = listView;
LayoutParams params = imageView.getLayoutParams();
reqWidth = params.width;
reqHeight = params.height;
}
@Override
public int getCount() {
return list.size();
}
@Override
public String getItem(int position) {
return list.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View view = null;
ViewHolder holder = null;
if(convertView == null)
{
view = inflater.inflate(R.layout.item, null);
holder = new ViewHolder();
holder.imageView = (ImageView) view.findViewById(R.id.imageView);
holder.textView = (TextView) view.findViewById(R.id.textView);
view.setTag(holder);
}else
{
view = convertView;
holder = (ViewHolder) view.getTag();
}
//為ImageView以及TextView設定Tag防止出現亂序
holder.imageView.setTag(position);
holder.textView.setTag(position+"#");
return view;
}
/**
* 添加當前key值對應的Bitmap到LruCache中
* @param key
* @param bitmap
*/
public void addBitmapToMemoryCache(String key,Bitmap bitmap)
{
if(getBitmapFromMemoryCache(key) == null)
{
lruCache.put(key, bitmap);
}
}
/**
* 從LruCache中獲取對應於key的Bitmap物件
* @param key
* @return
*/
public Bitmap getBitmapFromMemoryCache(String key)
{
return lruCache.get(key);
}
/**
* 載入某一位置上的圖片
* @param url
* @param position
*/
public void loadImage(int position)
{
//獲取到position位置對應的url
String url = getItem(position);
Bitmap bitmap = null;
bitmap = getBitmapFromMemoryCache(url);
if(bitmap != null)
{
//表示快取中存在對應的於此url的Bitmap,則直接獲得該Bitmap並且顯示到ListView上面
ImageView imageView = (ImageView) listView.findViewWithTag(position);
TextView textView = (TextView) listView.findViewWithTag(position+"#");
if(imageView != null)
imageView.setImageBitmap(bitmap);
if(textView != null)
textView.setText("從快取中獲取的");
}else
{
//開啟執行緒從網路中載入圖片
ImageAsyncTask task = new ImageAsyncTask(listView, position);
task.setOnImageLoadListener(new OnImageLoadListener() {
@Override
public void onSuccessLoad(Bitmap bitmap) {
System.out.println("載入圖片成功");
}
@Override
public void onFailureLoad() {
System.out.println("載入圖片失敗");
}
});
tasks.add(task);
task.execute(url);//開啟執行緒載入圖片
}
}
/**
* 暫停所有任務(為了防止在滑動的時候仍然有執行緒處於請求狀態)
*/
public void cancelTask()
{
if(tasks != null)
{
for(ImageAsyncTask task: tasks)
task.cancel(false);//暫停任務
}
}
/**
* 對圖片進行壓縮處理
* @param in
* @param reqWidth
* @param reqHeight
* @return
*/
public static Bitmap decodeSampleBitmapFromStream(InputStream in,int reqWidth,int reqHeight)
{
//設定BitmapFactory.Options的inJustDecodeBounds屬性為true表示禁止為bitmap分配記憶體
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
byte[] data = inputStreamToByteArray(in);
//這次呼叫的目的是獲取到原始圖片的寬、高,但是這次操作是沒有寫記憶體操作的
Bitmap beforeBitmap = BitmapFactory.decodeByteArray(data, 0, data.length, options);
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
//設定這次載入圖片需要載入到記憶體中
options.inJustDecodeBounds = false;
Bitmap afterBitmap = BitmapFactory.decodeByteArray(data, 0, data.length, options);
return afterBitmap;
}
/**
* 計算出壓縮比
* @param options
* @param reqWith
* @param reqHeight
* @return
*/
public static int calculateInSampleSize(BitmapFactory.Options options,int reqWidth,int reqHeight)
{
//通過引數options來獲取真實圖片的寬、高
int width = options.outWidth;
int height = options.outHeight;
int inSampleSize = 1;//初始值是沒有壓縮的
if(width > reqWidth || height > reqHeight)
{
//計算出原始寬與現有寬,原始高與現有高的比率
int widthRatio = Math.round((float)width/(float)reqWidth);
int heightRatio = Math.round((float)height/(float)reqHeight);
//選出兩個比率中的較小值,這樣的話能夠保證圖片顯示完全
inSampleSize = widthRatio < heightRatio ? widthRatio:heightRatio;
}
return inSampleSize;
}
/**
* 將InputStream轉換為Byte陣列
* @param in
* @return
*/
public static byte[] inputStreamToByteArray(InputStream in)
{
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len;
try {
while((len = in.read(buffer)) != -1)
{
outputStream.write(buffer, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
}finally{
try {
in.close();
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return outputStream.toByteArray();
}
static class ViewHolder
{
ImageView imageView;
TextView textView;
}
class ImageAsyncTask extends AsyncTask<String, Void, Bitmap>
{
public ListView listView;
public OnImageLoadListener listener;
public int position;
public ImageAsyncTask(ListView listView,int position)
{
this.listView = listView;
this.position = position;
}
public void setOnImageLoadListener(OnImageLoadListener listener)
{
this.listener = listener;
}
@Override
protected Bitmap doInBackground(String... params) {
String urlString = params[0];
URL url = null;
InputStream in = null;
HttpURLConnection connection = null;
Bitmap bitmap = null;
try {
url = new URL(urlString);
connection = (HttpURLConnection) url.openConnection();
in = connection.getInputStream();
bitmap = decodeSampleBitmapFromStream(in, reqWidth, reqHeight);
//將當前Bitmap新增到快取中
if(bitmap != null)
lruCache.put(urlString, bitmap);
} catch (Exception e) {
e.printStackTrace();
}
return bitmap;
}
@Override
protected void onPostExecute(Bitmap result) {
if(result != null)
{
listener.onSuccessLoad(result);
ImageView imageView = (ImageView) listView.findViewWithTag(position);
TextView textView = (TextView)listView.findViewWithTag(position+"#");
if(imageView != null)
imageView.setImageBitmap(result);
if(textView != null)
textView.setText("從網路中獲取的");
}else
listener.onFailureLoad();
}
}
}
其中39行的getView方法程式碼就是我們平常使用ListView的常規寫法,複用convertView以及對每個view設定viewHolder,為防止圖片加載出現亂序,第55、56行我們分別對ImageView以及TextView設定了Tag標誌;
第65行的addBitmapToMemoryCache用於向LruCache中添加當前Bitmap點陣圖,因為LruCache本身實際上是由LinkedHashMap實現的,所有呼叫的是put方法;
第78行的getBitmapFromMemoryCache方法用於從LruCache快取中讀取指定key值的Bitmap點陣圖;
第88行的loadImage就是我們的核心程式碼,首先第91行會通過呼叫getItem方法來獲得當前item條目所要載入圖片的url,隨後93行呼叫getBitmapFromMemoryCache檢視快取中是否存在指定key值的Bitmap,第94行進行判斷,如果存在Bitmap的話,則進入if語句塊中,97、98行通過findViewWithTag來獲得對應條目的ImageView以及TextView,並且將當前獲取到的快取圖片顯示到當前item;如果當前快取中不存在的話,則進入103行的else語句塊中,106行定義一個ImageAsyncTask圖片載入執行緒,並在119行將當前執行緒加入到Set<ImageAsyncTask>型別的tasks集合中,便於我們隨後對載入圖片執行緒進行控制,第120行呼叫execute方法,將當前需要載入圖片的url傳入;
execute接下會呼叫ImageAsyncTask的doInBackground方法來載入圖片,這裡的圖片載入方法和之前android-----帶你一步一步優化ListView(二) 是一致的,不清楚的朋友可以看看上一篇部落格,另外這裡面也用到了圖片壓縮技術,不太懂的可以看看android-----解決Bitmap記憶體溢位的一種方法(圖片壓縮技術),在圖片壓縮完成之後會在第245行將當前Bitmap新增到LruCache中,隨後在onPostExecute中進行更新UI的操作即可啦;
最後就是MainActivity方法了:
public class MainActivity extends Activity implements OnScrollListener {
public LruCache<String, Bitmap> memoryCache;
public int start_index;
public int end_index;
public ListViewAdapter adapter;
public boolean isInit = true;
public String[] images = { "http://img.my.csdn.net/uploads/201407/26/1406383299_1976.jpg",
"http://img.my.csdn.net/uploads/201407/26/1406383291_6518.jpg",
"http://img.my.csdn.net/uploads/201407/26/1406383291_8239.jpg",
"http://img.my.csdn.net/uploads/201407/26/1406383290_9329.jpg",
"http://img.my.csdn.net/uploads/201407/26/1406383290_1042.jpg",
"http://img.my.csdn.net/uploads/201407/26/1406383275_3977.jpg",
"http://img.my.csdn.net/uploads/201407/26/1406383265_8550.jpg",
"http://img.my.csdn.net/uploads/201407/26/1406383264_3954.jpg",
"http://img.my.csdn.net/uploads/201407/26/1406383264_4787.jpg",
"http://img.my.csdn.net/uploads/201407/26/1406383264_8243.jpg",
"http://img.my.csdn.net/uploads/201407/26/1406383248_3693.jpg",
"http://img.my.csdn.net/uploads/201407/26/1406383243_5120.jpg",
"http://img.my.csdn.net/uploads/201407/26/1406383242_3127.jpg",
"http://img.my.csdn.net/uploads/201407/26/1406383242_9576.jpg",
"http://img.my.csdn.net/uploads/201407/26/1406383242_1721.jpg",
"http://img.my.csdn.net/uploads/201407/26/1406383219_5806.jpg",
"http://img.my.csdn.net/uploads/201407/26/1406383214_7794.jpg",
"http://img.my.csdn.net/uploads/201407/26/1406383213_4418.jpg",
"http://img.my.csdn.net/uploads/201407/26/1406383213_3557.jpg",
"http://img.my.csdn.net/uploads/201407/26/1406383210_8779.jpg"};
public List<String> list = new ArrayList<String>();
public ListView listView;
public ImageView imageView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.listview);
memoryCache = getLruCache();
for(int i = 0;i < 50;i++)
{
int index = i %(images.length);
list.add(images[index]);
}
listView = (ListView) findViewById(R.id.listView);
LayoutInflater inflater = LayoutInflater.from(this);
View view = inflater.inflate(R.layout.item, null);
imageView = (ImageView) view.findViewById(R.id.imageView);
adapter = new ListViewAdapter(this, list, memoryCache, listView, imageView);
listView.setOnScrollListener(this);
listView.setAdapter(adapter);
}
/**
* 獲得LruCache物件
* @return
*/
public LruCache<String, Bitmap> getLruCache()
{
LruCache<String, Bitmap> cache = null;
//獲得可用的記憶體大小
int maxMemory = (int)(Runtime.getRuntime().maxMemory()/1024);
//將其1/8設定成我們的記憶體快取大小
int cacheSize = maxMemory/8;
cache = new LruCache<String, Bitmap>(cacheSize){
@Override
protected int sizeOf(String key, Bitmap value) {
//返回當前圖片的大小(getByteCount用於返回當前點陣圖所佔用的記憶體位元組數)
return value.getByteCount() / 1024;
}
};
return cache;
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
start_index = firstVisibleItem;
end_index = start_index + visibleItemCount;
if(isInit == true && visibleItemCount > 0)
{
for(int i = start_index;i < end_index;i++)
{
adapter.loadImage(i);
}
isInit = false;
}
}
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
if(scrollState == AbsListView.OnScrollListener.SCROLL_STATE_IDLE)
{
//表示停止滑動,這時候就可以載入圖片
for(int i = start_index;i < end_index;i++)
{
adapter.loadImage(i);
}
}else
{
adapter.cancelTask();
}
}
}
這裡比較關鍵的程式碼就是第55行的getLruCache方法了,首先會通過Runtime.getRuntime().maxMemory()獲得當前可用的記憶體大小,之後的cacheSize就是我們自己設定的LruCache所用的記憶體大小,第66行的sizeof方法用於返回當前圖片的大小;
同樣,類似於上一篇中DiskLruCache的使用,這裡我們也設定了ListView的滑動事件,保證其在滑動的過程中不會進行載入圖片的請求操作,滑動停止再去載入圖片;
另外提示一句,不要忘記新增訪問網路的許可權:
<uses-permission android:name="android.permission.INTERNET"></uses-permission>
點選下載原始碼!!!!!
我把上一篇部落格和這篇的程式碼整合成了一個完整的帶有DiskLruCache和LruCache快取的ListView版本: