前一阵看了些Universal-Image-Loager的源码。我觉得看源码很累的一个原因就是除了看怎么实现,就是去揣测为什么这么实现。这个揣测的过程很容易走马观花,看到后面似懂非懂。
人懒到一个地步一句话来说是能躺着就绝对不坐着,能坐着就绝对不蹲着,能蹲着就绝对不站着。有时候看源码也是,能看懂就不会想着去debug,debug能看明白的就懒得去动手写写。
看和写的感受是不一样的。看的是结果,写的是过程。
第三方库的使用让开发变得很方便,大量图片请求的实现,大多数不再是说实现的核心,而是直接说使用什么第三方。即便如此也没关系,只要知道别人是怎么实现的就好了,这很重要。Universal-Image-Loader是一个强大的图片加载开源框架,应该都被说烂用烂了吧。这篇主要是为了去发现问题,从0开始。
看代码的时候很多为什么,为什么要这么写,怎么就想到会有这些问题,怎么就想到用这种方法去解决。想找原因还是从最简单的实现一个图片列表开始找吧。不用任何框架,也不考虑什么缓存,单纯的去写一个网络请求显示图片列表这样一个功能。
新建一个PhotoListActivity,这个类只显示一个listview:
package com.aliao.learninguil.activity; import android.os.Bundle; import android.support.v7.app.AppCompatActivity; import android.widget.ListView; import com.aliao.learninguil.Constants; import com.aliao.learninguil.R; import com.aliao.learninguil.adapter.PhotoListAdapter; import com.aliao.learninguil.entity.ImageInfo; import java.util.ArrayList; import java.util.List; /** * Created by ALiao on 2015/7/13. */ public class PhotoListActivity extends AppCompatActivity { private ListView mListView; private PhotoListAdapter mAdapter; private List<ImageInfo> mImageInfos = new ArrayList<>(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_photolist); for (int i = 0; i< Constants.IMAGES.length; i++){ ImageInfo imageInfo = new ImageInfo(); imageInfo.setUrl(Constants.IMAGES[i]); imageInfo.setName("item-" + i); mImageInfos.add(imageInfo); } mListView = (ListView) findViewById(R.id.photoList); mAdapter = new PhotoListAdapter(mImageInfos, mListView); mListView.setAdapter(mAdapter); } }
listview的item的布局是左边显示一个ImageVeiw,右边显示一个textview:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="horizontal" android:layout_width="match_parent" android:layout_height="match_parent"> <ImageView android:id="@+id/iv_img" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <TextView android:id="@+id/tv_imagename" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </LinearLayout>
要想在listview的每个item中显示一张网络请求的图片,那么在PhotoListAdapter中要启动线程进行网络请求。
package com.aliao.learninguil.adapter; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.os.AsyncTask; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; import android.widget.ImageView; import android.widget.ListView; import android.widget.TextView; import com.aliao.learninguil.R; import com.aliao.learninguil.entity.ImageInfo; import com.aliao.learninguil.utils.L; import java.io.IOException; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; import java.util.List; public class PhotoListAdapter extends BaseAdapter { private List<ImageInfo> imageInfos; private ListView mListView; public PhotoListAdapter(List<ImageInfo> imageInfos, ListView listView) { this.imageInfos = imageInfos; mListView = listView; } @Override public int getCount() { return imageInfos.size(); } @Override public Object getItem(int position) { return imageInfos.get(position); } @Override public long getItemId(int position) { return position; } @Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder holder; if (convertView == null){ convertView = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_photolist, parent, false); holder = new ViewHolder(); holder.imgView = (ImageView) convertView.findViewById(R.id.iv_img); holder.imgName = (TextView) convertView.findViewById(R.id.tv_imagename); convertView.setTag(holder); }else { holder = (ViewHolder) convertView.getTag(); } ImageInfo imageInfo = imageInfos.get(position); holder.imgName.setText(imageInfo.getName()); holder.imgView.setTag(imageInfo.getUrl()); loadAndSetImage(imageInfo.getUrl()); return convertView; } class ViewHolder{ ImageView imgView; TextView imgName; } private void loadAndSetImage(String url) { new LoadImageAsyncTask().execute(url); } class LoadImageAsyncTask extends AsyncTask<String, Void, Bitmap>{ private String mImageUrl; @Override protected Bitmap doInBackground(String... params) { mImageUrl = params[0]; Bitmap bitmap = loadBitmap(mImageUrl); return bitmap; } @Override protected void onPostExecute(Bitmap bitmap) { super.onPostExecute(bitmap); ImageView imageView = (ImageView) mListView.findViewWithTag(mImageUrl); if (imageView != null){ imageView.setImageBitmap(bitmap); } } } private Bitmap loadBitmap(String imageUrl) { HttpURLConnection connection = null; Bitmap bitmap = null; try { URL url = new URL(imageUrl); connection = (HttpURLConnection) url.openConnection(); bitmap = BitmapFactory.decodeStream(connection.getInputStream()); return bitmap; } catch (MalformedURLException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); }finally { if (connection != null){ connection.disconnect(); } } return bitmap; } }
这是实现一个图片列表最基本的代码了。实现的效果图如下:
看起来显示的效果还不错,但是当我们上下滑动的时候明显会感受到不够流畅。
回到PhotoListAdapter类,每调用一次getView方法,就会去启动线程进行网络请求。我们期望的效果是当页面显示5个item的时候,就进行5次网络请求,这是最理想的状态。但是实际上getView调用的次数比预想的要多得多,也就意味着会伴随多余的网络请求。有哪些情况会导致多余的网络请求呢(这里的多余是表示,除了当前屏幕显示的图片以外,进行了额外的其他图片的网络请求)
情况一:没有设置item的高度
listview item的布局文件中看到,并没有去设置ImageView的高度,item的高度是自动扩展的:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="horizontal" android:layout_width="match_parent" android:layout_height="match_parent"> <ImageView android:id="@+id/iv_img" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <TextView android:id="@+id/tv_imagename" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </LinearLayout>
所以,当进入图片列表页时,由于还未加载出图片,默认是textview的高度,一屏能够显示了27个item,即调用了27次getView,也就是开启线程进行网络请求的操作就要执行27次。但随着前面图片陆续加载完毕,一屏最终显示5个item,但是剩余的22次网络请求还在继续。通常我们希望一屏显示多少张图片就去请求多少张,当要看更多的图片的时候再去请求。而这22次网络请求既然都执行了,是不是往下滑动的时候他就可以直接显示出已加载完的图片?答案是否定的,因为除了当前屏幕显示的item,其他的item都被回收了,通过findViewWithTag(imageUrl)已经找不到对应的imageView(为null)。即使图片已经请求成功,但是由于当前屏幕没有对应的imageview,也无法设置图片。
@Override protected void onPostExecute(Bitmap bitmap) { super.onPostExecute(bitmap); ImageView imageView = (ImageView) mListView.findViewWithTag(mImageUrl); if (imageView != null){ imageView.setImageBitmap(bitmap); } }
所以继续向下滑的时候,还是会重复请求先前已经请求过的图片。
如果提前已知item的显示高度,例如设置ImageView的高度为100dp或者设置默认图片,那么一屏能够显示的item数量是确定的,就可以做到一屏显示多少个item,进行相应数量的网络请求。
情况二:如果想看第三屏的图片列表,滑过去的前两屏的图片已经进行了网络请求,但并没必要
对于我们不想查看而快速滑过的图片是没有必要浪费资源去加载的。但是由于把加载图片的操作放在了getView()中,只要滑动屏幕都会调用getView(),我们希望当listview滑动停止时再去加载。listview的滚动监听事件的回调函数可以监听到滚动的状态,onScrollStateChanged(AbsListView view, int scrollState)中的scrollState有三种表示listvuew的滚动状态,分别是:
SCROLL_STATE_IDLE( = 0 ) :停止滚动
SCROLL_STATE_TOUCH_SCROLL( = 1 ) :正在滚动
SCROLL_STATE_FLING( = 2 ) :手指做了抛的动作
当scrollState的状态为SCROLL_STATE_IDLE的时候,去下载图片。图片下载的时机确定了,那么当滑动停止时当前屏幕显示的可视图片的地址该怎么获取。imageInfos对象列表存放了所有图片信息,知道了当前屏幕可视图片的position位置,也就可以通过索引获取到图片地址了。另一个滚动监听的回调onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount)(滚动时一直回调)帮助我们获取到当前屏幕图片的位置信息。参数中
firstVisibleItem :当前屏幕第一张可见item的下标(从0开始)
visibleItemCount :当前屏幕所有可见item的总数
totalItemCount :列表项总数
具体的代码实现如下:
package com.aliao.learninguil.adapter; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.os.AsyncTask; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.widget.AbsListView; import android.widget.BaseAdapter; import android.widget.ImageView; import android.widget.ListView; import android.widget.TextView; import com.aliao.learninguil.R; import com.aliao.learninguil.entity.ImageInfo; import com.aliao.learninguil.utils.L; import java.io.IOException; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; import java.util.HashSet; import java.util.List; import java.util.Set; public class PhotoListAdapter extends BaseAdapter implements AbsListView.OnScrollListener{ private List<ImageInfo> imageInfos; private ListView mListView; private int mFirstVisibleItem; private int mVisibleItemCount; private boolean mFirstEnter; private Set<LoadImageAsyncTask> taskCollection; public PhotoListAdapter(List<ImageInfo> imageInfos, ListView listView) { this.imageInfos = imageInfos; mListView = listView; mListView.setOnScrollListener(this); mFirstEnter = true; taskCollection = new HashSet<>(); } @Override public int getCount() { return imageInfos.size(); } @Override public Object getItem(int position) { return imageInfos.get(position); } @Override public long getItemId(int position) { return position; } @Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder holder; if (convertView == null){ convertView = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_photolist, parent, false); holder = new ViewHolder(); holder.imgView = (ImageView) convertView.findViewById(R.id.iv_img); holder.imgName = (TextView) convertView.findViewById(R.id.tv_imagename); convertView.setTag(holder); }else { holder = (ViewHolder) convertView.getTag(); } ImageInfo imageInfo = imageInfos.get(position); holder.imgName.setText(imageInfo.getName()); holder.imgView.setTag(imageInfo.getUrl()); // L.d("position = "+position+", url = "+imageInfo.getUrl()); // loadAndSetImage(imageInfo.getUrl()); return convertView; } class ViewHolder{ ImageView imgView; TextView imgName; } @Override public void onScrollStateChanged(AbsListView view, int scrollState) { /** * scrollState = SCROLL_STATE_IDLE( = 0 )停止混动 * scrollState = SCROLL_STATE_TOUCH_SCROLL( = 1 )正在滚动 * scrollState = SCROLL_STATE_FLING( = 2 ) 手指做了抛的动作 */ L.d("-------》onScrollStateChanged scrollState = "+scrollState); if (scrollState == SCROLL_STATE_IDLE){ loadAndSetImage(mFirstVisibleItem, mVisibleItemCount); }else { //当listview再次滑动时取消所有正在下载的任务 // cancelAllTasks(); } } public void cancelAllTasks() { if (taskCollection != null){ for (LoadImageAsyncTask task : taskCollection){ task.cancel(false); } } } @Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { L.d("onScroll firstVisibleItem = "+firstVisibleItem+", visibleItemCount = "+visibleItemCount+", totalItemCount = "+totalItemCount); mFirstVisibleItem = firstVisibleItem;//第一张可见图片的下标 mVisibleItemCount = visibleItemCount;//一屏可见图片的总数 //首次进入程序时,onScrollStateChanged方法并不会被调用,所以在这里首次进入程序时启动下载任务 if (mFirstEnter && visibleItemCount > 0){ loadAndSetImage(mFirstVisibleItem, mVisibleItemCount); mFirstEnter = false; } } private void loadAndSetImage(int firstVisibleItem, int visibleItemCount) { for (int i = firstVisibleItem; i< firstVisibleItem + visibleItemCount; i++){ ImageInfo imageInfo = imageInfos.get(i); L.d("position = "+i+", url = "+imageInfo.getUrl()); LoadImageAsyncTask task = new LoadImageAsyncTask(); task.execute(imageInfo.getUrl()); taskCollection.add(task); } } class LoadImageAsyncTask extends AsyncTask<String, Void, Bitmap>{ private String mImageUrl; @Override protected Bitmap doInBackground(String... params) { mImageUrl = params[0]; Bitmap bitmap = loadBitmap(mImageUrl); return bitmap; } @Override protected void onPostExecute(Bitmap bitmap) { super.onPostExecute(bitmap); ImageView imageView = (ImageView) mListView.findViewWithTag(mImageUrl); if (imageView != null){ imageView.setImageBitmap(bitmap); } } } private Bitmap loadBitmap(String imageUrl) { HttpURLConnection connection = null; Bitmap bitmap = null; try { URL url = new URL(imageUrl); connection = (HttpURLConnection) url.openConnection(); bitmap = BitmapFactory.decodeStream(connection.getInputStream()); L.d("------------------------------------loadBitmap "); return bitmap; } catch (MalformedURLException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); }finally { if (connection != null){ connection.disconnect(); } } return bitmap; } }
除了当listview再次滑动的时候,取消所有正在下载的任务,当Activity销毁时,在onDestroy方法中可以调用PhotoListAdapter中的cancelAllTasks()来取消所有还未完成的下载任务。
到目前为止通过确定list item的高度以及对启动图片下载任务的时机的修改,做到了只下载所要查看到的图片,避免多余的网络请求,减少了网络请求的负荷和节省了手机流量。
解决了上面的问题后,代码又完善健壮了一步,想想接下来还会有什么问题。
参考:
版权声明:本文为博主原创文章,未经博主允许不得转载。