ListView 加載網絡圖片是我們經常用到的方式,如果每次滾動ListView就去網絡下載圖片會非常影響性能(因爲網絡下載是比較慢的)而且非常耗費流量,所以這裏介紹一種使用“內存雙緩存+硬盤緩存”的方式來加載圖片。
實現的效果如下:
這裏使用了滾動時不去網絡下載圖片,停止時才加載,所以滾動時顯示默認的,注意觀察
設計思想
內存讀取速度 > 文件讀取速度 > 從網絡獲取的速度
基本代碼邏輯如下
// 從內存緩存中獲取圖片
Bitmap result = memoryCache.getBitmapFromCache(url);
if (result == null) {
// 從文件緩存中獲取
result = fileCache.getImage(url);
if (result == null) {
// 從網絡獲取
result = ImageGetFromHttp.downloadBitmap(url);
if (result != null) {
fileCache.saveBitmap(result, url);
memoryCache.addBitmapToCache(url, result);
}
} else {
// 添加到內存緩存
memoryCache.addBitmapToCache(url, result);
}
}
內存緩存中使用了LruCache,LRU算法請參考:http://blog.csdn.net/luoweifu/article/details/8297084/
我們在內存緩存中再將內存分爲兩層,強引用緩存和軟引用緩存。
對強引用和軟引用做簡單的介紹(具體內容請看:http://blog.csdn.net/u010583599/article/details/51970515
① 強引用:強引用是使用最普遍的引用。如果一個對象具有強引用,那垃圾回收器絕不會回收它。當內存空間不足,Java虛擬機寧願拋出OutOfMemoryError錯誤,使程序異常終止,也不會靠隨意回收具有強引用的對象來解決內存不足的問題。
② 軟引用:如果一個對象只具有軟引用,則內存空間足夠,垃圾回收器就不會回收它;如果內存空間不足了,就會回收這些對象的內存。只要垃圾回收器沒有回收它,該對象就可以被程序使用。
程序介紹
主界面是一個ListView,該ListView的Item佈局爲
Item_layout.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="wrap_content"
android:orientation="horizontal" >
<ImageView
android:id="@+id/iv_icon"
android:layout_width="64dp"
android:layout_height="64dp"
android:src="@drawable/ic_launcher"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:paddingLeft="5dp"
android:orientation="vertical">
<TextView
android:id="@+id/tv_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="15sp"
android:text="新聞的標題"/>
<TextView
android:id="@+id/tv_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxLines="3"
android:text="新聞的內容"/>
</LinearLayout>
</LinearLayout>
主Activity完成的功能是,請求並解析慕課網的接口的JSon字符串,創建ListView的適配器。
MainActivity.java
/**
* 主類,訪問網絡接口獲取JSon字符串,並解析字符串,生成對象集合,創建listView適配器
*
*/
public class MainActivity extends Activity {
private ListView listView;
//網絡接口來自慕課網,獲取一個Json字符串並解析
private static final String URL = "http://www.imooc.com/api/teacher?type=4&num=30";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
listView = (ListView)findViewById(R.id.listview);
new NewsAsyncTask().execute(URL);
}
private List<NewsBean> getJsonData(String url){
List<NewsBean> newsBeansList = new ArrayList<NewsBean>();
try {
//獲得json數據並解析Json字符串
String jsonString = readStream(new URL(url).openStream());
JSONObject jsonObject;
NewsBean newsBean;
jsonObject = new JSONObject(jsonString);
JSONArray jsonArray = jsonObject.getJSONArray("data");
for(int i = 0;i< jsonArray.length();i++){
jsonObject = jsonArray.getJSONObject(i);
newsBean = new NewsBean();
newsBean.newsIconUrl = jsonObject.getString("picSmall");
newsBean.newsTitle = jsonObject.getString("name");
newsBean.newsContent = jsonObject.getString("description");
newsBeansList.add(newsBean);
}
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (JSONException e) {
e.printStackTrace();
}
return newsBeansList;
}
//獲取網絡輸入流數據,返回一個Json字符串
private String readStream(InputStream is){
InputStreamReader isr;
String result = "";
try{
String line = "";
isr = new InputStreamReader(is,"utf-8");
BufferedReader br = new BufferedReader(isr);
while((line = br.readLine()) != null){
result += line;
}
}catch(IOException e){
e.printStackTrace();
}
return result;
}
/**
* 使用AsyncTask來訪問網絡獲取JSon數據
*
*/
class NewsAsyncTask extends AsyncTask<String , Void, List<NewsBean>>{
@Override
protected List<NewsBean> doInBackground(String... params) {
return getJsonData(params[0]);
}
@Override
protected void onPostExecute(List<NewsBean> result) {
super.onPostExecute(result);
//創建並給listView設置適配器
NewsAdapter adapter = new NewsAdapter(getApplicationContext(), result,listView);
listView.setAdapter(adapter);
}
}
}
爲了解析Json字符串,我們需要創建一個實體類
NewsBean.java
public class NewsBean {
public String newsIconUrl;
public String newsTitle;
public String newsContent;
public NewsBean(String newsIconUrl,String newsTitle,String newsContent){
this.newsIconUrl = newsIconUrl;
this.newsTitle = newsTitle;
this.newsContent = newsContent;
}
public NewsBean(){
}
}
ListView 適配器類中我們完成了圖片的下載,並且進行了控制,ListView滑動時不進行任何的下載,停止狀態才進行網絡下載,並且進行了優化,防止圖片加載時錯位、重複、閃爍
NewsAdapter.javapublic class NewsAdapter extends BaseAdapter implements OnScrollListener{
private List<NewsBean> mList;
private LayoutInflater mInflater;
//圖片加載類
private ImageLoader imageLoader;
//listView開始下載和結束下載的位置
private int mStart,mEnd;
//所有的URL的數組
public static String[] URLS;
private boolean mFirstIn;//第一次啓動
public NewsAdapter(Context context,List<NewsBean> mList,ListView listView){
mInflater = LayoutInflater.from(context);
this.mList = mList;
imageLoader = new ImageLoader(listView,context);
//獲取所有的URL並初始化數組
URLS = new String[mList.size()];
for(int i = 0;i<mList.size();i++){
URLS[i] = mList.get(i).newsIconUrl;
}
listView.setOnScrollListener(this);
mFirstIn = true;
}
@Override
public int getCount() {
return mList.size();
}
@Override
public Object getItem(int position) {
return mList.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder viewHolder = null;
if(convertView == null){
viewHolder = new ViewHolder();
convertView = mInflater.inflate(R.layout.item_layout, null);
viewHolder.ivIcon = (ImageView)convertView.findViewById(R.id.iv_icon);
viewHolder.tvTitle = (TextView)convertView.findViewById(R.id.tv_title);
viewHolder.tvContent = (TextView)convertView.findViewById(R.id.tv_content);
convertView.setTag(viewHolder);
}else{
viewHolder = (ViewHolder) convertView.getTag();
}
//設置默認的圖片
// viewHolder.ivIcon.setImageResource(R.drawable.ic_launcher);
String url = mList.get(position).newsIconUrl;
/*因爲Itme是重複利用的,ListView滑動到第2行會異步加載某個圖片,但是加載很慢,加載過程中listView已經滑動到了第14行,
* 且滑動過程中該圖片加載結束,第2行已不在屏幕內,根據緩存原理,第2行的view可能被第14行復用,這樣我
* 們看到的就是第14行顯示了本該屬於第2行的圖片,造成顯示重複。如果14行的圖片也加載結束則會造成閃爍,先顯示前一張,再顯示後一張
* 爲了防止圖片加載時錯位,這裏加上tag,把imageView和url標識綁定,在異步顯示的位置,判斷當前任務的url和item設置的url是否
* 相同,只有相同纔去加載圖片
*/
viewHolder.ivIcon.setTag(url);
//在滾動的時候加載圖片,如果緩存中都沒有,則使用默認的圖片
imageLoader.showImagesFromCache(viewHolder.ivIcon, url);
viewHolder.tvTitle.setText(mList.get(position).newsTitle);
viewHolder.tvContent.setText(mList.get(position).newsContent);
return convertView;
}
class ViewHolder{
public TextView tvTitle,tvContent;
public ImageView ivIcon;
}
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
if(scrollState == SCROLL_STATE_IDLE){
//當前狀態處於停止狀態,加載可見項
imageLoader.loadImages(mStart, mEnd);
}else{
//停止任務
imageLoader.cancelAllTasks();
}
}
/**
* 由於我們使用的是滾動狀態改變時纔去下載圖片,但是第一次進入的時候要加載第一屏的圖片
* listview初始化後會調用onScroll方法,我們在這裏去加載第一屏的圖片並把第一次進入
* 狀態位置爲false
*/
@Override
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
//start 爲第一個可見的item的位置
mStart = firstVisibleItem;
//end 爲第一個可見的位置加上可見的item的數量
mEnd = firstVisibleItem + visibleItemCount;
if(mFirstIn && visibleItemCount > 0){
//第一次顯示的時候調用,加載圖片
imageLoader.loadImages(mStart, mEnd);
mFirstIn = false;
}
}
}
內存緩存類 ImageMemoryCache.java
/**
* @author meng.li
* 內存緩存圖片類,這裏使用了兩層內存緩存
*/
public class ImageMemoryCache {
/**
* 從內存讀取數據速度是最快的,爲了更大限度使用內存,這裏使用了兩層緩存。
* 強引用緩存不會輕易被回收,用來保存常用數據
* 不常用的數據轉入軟引用緩存,不會影響GC的回收。
*/
private static final int SOFT_CACHE_SIZE = 15; //軟引用緩存容量
private static LruCache<String, Bitmap> mLruCache; //硬引用緩存
private static LinkedHashMap<String, SoftReference<Bitmap>> mSoftCache; //軟引用緩存
public ImageMemoryCache(Context context) {
//獲取最大可用內存
int maxMemory = (int) Runtime.getRuntime().maxMemory();
Log.i("mengli","maxMemory = "+ maxMemory);
//強引用緩存容量,爲系統可用內存的1/4
int cacheSize = maxMemory/4;
mLruCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap value) {
if (value != null)
//每次加入緩存時會調用
return value.getByteCount();
else
return 0;
}
@Override
protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
if (oldValue != null)
//LRU算法會把最近使用的元素壓入棧頂,所以棧底就是被移除的元素
// 強引用緩存容量滿的時候,會根據LRU算法把最近最久沒有被使用的圖片轉入此軟引用緩存
mSoftCache.put(key, new SoftReference<Bitmap>(oldValue));
}
};
mSoftCache = new LinkedHashMap<String, SoftReference<Bitmap>>(SOFT_CACHE_SIZE, 0.75f, true) {
// private static final long serialVersionUID = 6040103833179403725L;
@Override
protected boolean removeEldestEntry(Entry<String, SoftReference<Bitmap>> eldest) {
if (size() > SOFT_CACHE_SIZE){
return true;
}
return false;
}
};
}
/**
* 從緩存中獲取圖片
*/
public Bitmap getBitmapFromCache(String url) {
Bitmap bitmap;
//先從強引用緩存中獲取
synchronized (mLruCache) {
bitmap = mLruCache.get(url);
if (bitmap != null) {
//如果找到的話,把元素移到LinkedHashMap的最前面,從而保證在LRU算法中最後被刪除
mLruCache.remove(url);
mLruCache.put(url, bitmap);
return bitmap;
}
}
//如果強引用緩存中找不到,到軟引用緩存中找
synchronized (mSoftCache) {
SoftReference<Bitmap> bitmapReference = mSoftCache.get(url);
if (bitmapReference != null) {
bitmap = bitmapReference.get();
if (bitmap != null) {
//將圖片移回硬緩存
mLruCache.put(url, bitmap);
mSoftCache.remove(url);
return bitmap;
} else {
//沒找到,可能改bigmap已經被回收了,刪除url
mSoftCache.remove(url);
}
}
}
return null;
}
/**
* 添加圖片到緩存
*/
public void addBitmapToCache(String url, Bitmap bitmap) {
if (bitmap != null) {
synchronized (mLruCache) {
mLruCache.put(url, bitmap);
}
}
}
public void clearCache() {
mSoftCache.clear();
}
}
文件緩存類 ImageFileCache.java
/**
* @author meng.li
* 文件緩存類
*/
public class ImageFileCache {
//緩存目錄名
private static final String CACHDIR = "ImgeCache";
private static final String WHOLESALE_CONV = ".cach";
private static final int MB = 1024*1024;
//緩存大小
private static final int CACHE_SIZE = 10;
//剩餘最小空間大小
private static final int FREE_SD_SPACE_NEEDED_TO_CACHE = 10;
public ImageFileCache() {
//清理文件緩存
removeCache(getDirectory());
}
/** 從緩存中獲取圖片 **/
public Bitmap getImage(final String url) {
final String path = getDirectory() + "/" + getFileNameFromUrl(url);
File file = new File(path);
if (file.exists()) {
Bitmap bmp = BitmapFactory.decodeFile(path);
if (bmp == null) {
file.delete();
} else {
//獲取時圖片時需要更新文件的最後修改時間
updateFileTime(path);
return bmp;
}
}
return null;
}
/** 將圖片存入文件緩存 **/
public void saveBitmap(Bitmap bm, String url) {
if (bm == null) {
return;
}
//判斷sdcard上的空間 ,如果不足10M返回
if (FREE_SD_SPACE_NEEDED_TO_CACHE > freeSpaceOnSd()) {
//SD空間不足
return;
}
String filename = getFileNameFromUrl(url);
String dir = getDirectory();
File dirFile = new File(dir);
if (!dirFile.exists())
dirFile.mkdirs();
//創建文件
File file = new File(dir +"/" + filename);
try {
file.createNewFile();
OutputStream outStream = new FileOutputStream(file);
//將圖片進行壓縮並寫入文件,100表示不壓縮
bm.compress(Bitmap.CompressFormat.JPEG, 100, outStream);
outStream.flush();
outStream.close();
} catch (FileNotFoundException e) {
Log.w("ImageFileCache", "FileNotFoundException");
} catch (IOException e) {
Log.w("ImageFileCache", "IOException");
}
}
/**
* 計算存儲目錄下的文件大小,
* 當文件總大小大於規定的CACHE_SIZE或者sdcard剩餘空間小於FREE_SD_SPACE_NEEDED_TO_CACHE的規定
* 那麼刪除40%最近沒有被使用的文件
*/
private boolean removeCache(String dirPath) {
File dir = new File(dirPath);
File[] files = dir.listFiles();
if (files == null) {
return true;
}
//沒有掛載外部存儲設備
if (!android.os.Environment.getExternalStorageState().equals(
android.os.Environment.MEDIA_MOUNTED)) {
return false;
}
int dirSize = 0;
for (int i = 0; i < files.length; i++) {
//遍歷目錄下的所有文件,如果包含.cach 則累加size
if (files[i].getName().contains(WHOLESALE_CONV)) {
dirSize += files[i].length();
}
}
//如果緩存目錄的文件大小 大於規定的緩存大小或者剩餘內存不足10M,則刪除40%最久未使用的文件
if (dirSize > CACHE_SIZE * MB || FREE_SD_SPACE_NEEDED_TO_CACHE > freeSpaceOnSd()) {
int removeFactor = (int) ((0.4 * files.length) + 1);
//對文件按時間排序
Arrays.sort(files, new FileLastModifSort());
for (int i = 0; i < removeFactor; i++) {
if (files[i].getName().contains(WHOLESALE_CONV)) {
files[i].delete();
}
}
}
if (freeSpaceOnSd() <= CACHE_SIZE) {
return false;
}
return true;
}
/** 修改文件的最後修改時間 **/
public void updateFileTime(String path) {
File file = new File(path);
long newModifiedTime = System.currentTimeMillis();
file.setLastModified(newModifiedTime);
}
/** 計算sdcard上的剩餘空間 **/
private int freeSpaceOnSd() {
StatFs stat = new StatFs(Environment.getExternalStorageDirectory().getPath());
double sdFreeMB = ((double)stat.getAvailableBlocks() * (double) stat.getBlockSize()) / MB;
return (int) sdFreeMB;
}
/**從url中獲取文件名 **/
private String getFileNameFromUrl(String url) {
return url.substring(url.lastIndexOf("/")+1)+WHOLESALE_CONV;
}
/** 獲得緩存目錄 **/
private String getDirectory() {
String dir = getSDPath() + "/" + CACHDIR;
return dir;
}
/** 取SD卡路徑 **/
private String getSDPath() {
File sdDir = null;
boolean sdCardExist = Environment.getExternalStorageState().equals(
android.os.Environment.MEDIA_MOUNTED); //判斷sd卡是否存在
if (sdCardExist) {
sdDir = Environment.getExternalStorageDirectory(); //獲取根目錄
}
if (sdDir != null) {
return sdDir.toString();
} else {
return "";
}
}
/**
* 根據文件的最後修改時間進行排序,Java中對對象進行排序要實現Comparator 接口,自己實現比較規則
* 1 表示大於,0表示相等,-1表示小於
*/
private class FileLastModifSort implements Comparator<File> {
public int compare(File arg0, File arg1) {
if (arg0.lastModified() > arg1.lastModified()) {
return 1;
} else if (arg0.lastModified() == arg1.lastModified()) {
return 0;
} else {
return -1;
}
}
}
}
通過圖片我們可以看到,圖片被寫入了文件
圖片下載類,使用多線程下載圖片用了AsyncTask封裝類
public class ImageLoader {
/**
* 使用多線程的方式去加載圖片
*/
private ImageView imageView;
private String mUrl;
//內存緩存
private ImageMemoryCache memoryCache;
//文件緩存
private ImageFileCache fileCache;
private ListView mListView;
//任務集合,用來處理多個下載線程
private Set<NewsAsyncTask> mTasks;
public ImageLoader(ListView listView,Context context){
mListView = listView;
mTasks = new HashSet<ImageLoader.NewsAsyncTask>();
memoryCache = new ImageMemoryCache(context);
fileCache = new ImageFileCache();
}
/**
* 用於從一個url獲取bitmap
*/
public Bitmap getBitmapFromURL(String urlString){
Bitmap bitmap;
InputStream is = null;
try {
URL url = new URL(urlString);
HttpURLConnection connection = (HttpURLConnection)url.openConnection();
is = new BufferedInputStream(connection.getInputStream());
bitmap = BitmapFactory.decodeStream(is);
connection.disconnect();
return bitmap;
} catch (Exception e) {
}finally{
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
//在滾動的時候顯示緩存的圖片,如果沒有緩存圖片則顯示默認的圖片
public void showImagesFromCache(ImageView imageView,String url){
//從緩存取出圖片
Bitmap result = memoryCache.getBitmapFromCache(url);
if (result == null) {
// 文件緩存中獲取
result = fileCache.getImage(url);
}
if(result == null){
imageView.setImageResource(R.drawable.ic_launcher);
}else{
imageView.setImageBitmap(result);
}
}
//取消加載圖片
public void cancelAllTasks(){
if(mTasks != null){
for(NewsAsyncTask task: mTasks){
task.cancel(false);
}
}
}
public void loadImages(int start,int end){
//加載從start到end的圖片
for(int i = start;i<end;i++){
String url = NewsAdapter.URLS[i];
//從內存緩存取出圖片
Bitmap bitmap = memoryCache.getBitmapFromCache(url);
//如果緩存沒有,則從文件中讀取
if(bitmap == null){
//從文件獲取圖片
bitmap = fileCache.getImage(url);
//文件中也爲空,則必須從網絡下載圖片
if(bitmap == null){
//使用AsyncTask下載圖片,這裏會耗費流量
NewsAsyncTask task = new NewsAsyncTask(url);
task.execute(url);
//添加一個任務
mTasks.add(task);
}else{
//文件中獲取到了圖片,則把圖片加入到內存中
memoryCache.addBitmapToCache(url, bitmap);
}
}
if(bitmap != null){
//根據url去獲取 對應的imageView對象,防止顯示混亂
ImageView imageView = (ImageView) mListView.findViewWithTag(url);
imageView.setImageBitmap(bitmap);
}
}
}
private class NewsAsyncTask extends AsyncTask<String, Void, Bitmap>{
private String mUrl;
public NewsAsyncTask(String url){
mUrl = url;
}
@Override
protected Bitmap doInBackground(String... params) {
//從網絡獲取圖片
Bitmap bitmap = getBitmapFromURL(params[0]);
if(bitmap != null){
//把bitmap加入到緩存
memoryCache.addBitmapToCache(params[0], bitmap);
//把bitmap 加入到文件
fileCache.saveBitmap(bitmap,params[0]);
}
return bitmap;
}
@Override
protected void onPostExecute(Bitmap result) {
super.onPostExecute(result);
ImageView imageView = (ImageView) mListView.findViewWithTag(mUrl);
if(imageView != null && result != null){
imageView.setImageBitmap(result);
}
//下載任務完成則移除這個任務
mTasks.remove(this);
}
}
}
AndroidManifest.xml 注意加上權限
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.newsdemo"
android:versionCode="1"
android:versionName="1.0" >
<uses-sdk
android:minSdkVersion="22"
android:targetSdkVersion="22" />
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@android:style/Theme.Black.NoTitleBar" >
<activity
android:name=".MainActivity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
菜鳥一隻,剛踏上追求技術的不歸路,記錄所學,希望大家指點!
代碼下載 :http://download.csdn.net/detail/u010583599/9583583本文參考:
http://blog.csdn.net/a79412906/article/details/10180583
http://www.imooc.com/learn/406