一个网络程序下载图片通常是一个大麻烦,如何处理好下载,才是关键的问题,这关系到程序的性能,甚至崩溃,出现oome.
如果你还在使用ui线程下载图片,赶紧看看如何在另一个线程下载图片的相关文章吧,ui线程要做的事只是显示.
看上去使用AsyncTask是个好办法,方便操作,一般不会有非ui线程处理ui的问题.虽然它有线程池的概念,但是我也发现,还是会发起上千次甚至w次的线程请求,在一个ListView滚动过程中,然而需要下载的图片却只有不到二十张,这显然是内存的浪费了.出现oome也是必然的,一方面可能是图片本身占用内存较多,一方面是线程占用的内存资源.
所以在AsyncTask里做好缓存检测是很有必要的,检测到已经下载过的文件 就不需要下载.但是这样的问题在于,doInBackground里面检查的话,已经是新建了一个线程了.虽然这样可以减少下载量,但是新建一个线程也是要消耗资源的.
现在介绍一种自定义线程池的办法来处理下载的问题.
/** * 图片下载线程池,暂时可允许最多三个线程同时下载。 * * @author archko */public class DownloadPool extends Thread{public static final int MAX_THREAD_COUNT=3;定义最大同时下载线程private int mActiveThread=0;当前活动的线程数private App mApp;继承了Application,在manifest文件里配置的.private List<DownloadPiece> mQuery;下载队列.}先看看App里如何处理的:public DownloadPool mDownloadPool = null;onCreate()方法里对线程池初始化.if (this.mDownloadPool != null) { return; } Log.d(TAG, "initDownloadPool."); DownloadPool downloadPool = new DownloadPool(this); this.mDownloadPool = downloadPool; this.mDownloadPool.setPriority(Thread.MIN_PRIORITY); this.mDownloadPool.setName("DownloadPool"); this.mDownloadPool.start();需要注意的是mDownloadPool是在程序启动后一直在运行的,然后就是它的构造方法了:public DownloadPool(App app) { this.mQuery=new ArrayList<DownloadPiece>(); this.mApp=app; }关于DownloadPiece内容:一个内容类:public class DownloadPiece { Handler handler; public String name; //md5加密过的名字, public String uri;//图片的url public int type;// public String dir;//存储目录}这样一个线程池构造一半了.它如何处理下载事宜呢?接着就是定义它的run方法了.while (true) {//正常状态下一直运行. synchronized (this) { notifyAll(); if ((GetCount()!=0)&&(GetThreadCount()!=MAX_THREAD_COUNT)) {如果队列不为空,且当前下载线程数量不等于最大线程数量就新建下载线程下载图片 ,如果条件不满足,就等待,其它线程notify后它就可以下载了,这避免了一次性建太多的线程. DownloadPiece piece=Pop(); Handler handler=piece.handler; FrechImg_Impl(handler, piece.name, piece.type, piece.uri, piece.dir); } else { Log.d(TAG, "wait."+GetThreadCount()); try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } }它处理下载的方法是FrechImg_Impl(),这个方法会建一个线程来下载图片的.而线程池只是管理下载线程用的.if (uri==null||name==null) { Log.d(TAG, "名字不存在。"); return; } String filename=dir+name; File file=new File(filename); if (file.exists()) { Log.d(TAG, "文件已经存在了不需要下载:"+uri); Bundle bundle=new Bundle(); bundle.putString("name", filename); FetchImage.SendMessage(handler, type, bundle, uri); return; } synchronized (this) { mApp.mDownloadPool.ActiveThread_Push(); String str3=Uri.encode(uri, ":/"); HttpGet httpGet=new HttpGet(str3); httpGet.setHeader("User-Agent", Twitter.USERAGENT); FetchImage fetchImage=new FetchImage(mApp, handler, httpClient, httpGet, type, name, uri, dir); fetchImage.setPriority(2); fetchImage.start(); }FetchImage是一个下载图片的线程.FrechImg_Impl做的事从代码来看,先检查下载的url是否合法,然后检查下载的文件是否存在,不满足下再下载,但先push,把当前的线程数量加1,然后启动线程下载.public void ActiveThread_Push() { synchronized (this) { mActiveThread++; } }你也许会问,下载完了如何处理.这里有一个handler,这个是在你将url放入下载队列时带来的回调方法,这样也可以避免了非ui线程的问题,下载完成后就可以在handler中处理你的ui了.其它的同步方法:public DownloadPiece Get(int paramInt) { synchronized (this) { int size=this.mQuery.size(); if (paramInt<=size) { return mQuery.get(paramInt); } } return null; } public int GetCount() { synchronized (this) { return mQuery.size(); } } public int GetThreadCount() { synchronized (this) { return mActiveThread; } } public DownloadPiece Pop() { synchronized (this) { DownloadPiece downloadPiece=(DownloadPiece) this.mQuery.get(0); this.mQuery.remove(0); return downloadPiece; } }public void ActiveThread_Pop() { synchronized (this) { int i=this.mActiveThread-1; this.mActiveThread=i; notifyAll(); } }看到这,已经比较明朗了,你可以不关心队列中的数据哪里来的,因为上面只处理了如何下载.下载需要一个url队列,所以需要提供一个public方法:public void Push(Handler handler, String uri, int type, String dir) { } String name=Util().getMd5(uri);//文件存储的名字自定义. synchronized (this) { for(DownloadPiece piece:mQuery) { if(piece.uri.equals(uri)) { notifyAll(); Log.d(TAG, "已经存在url:"+uri); return; } }//在这里检查了一次下载队列中的url,如果存在,就不需要再下载了,前面提到会检查一次文件是否已经下载的问题,如果你有存储,但我觉得在这里检查一次会比检查文件快一些. DownloadPiece piece=new DownloadPiece(handler, uri, name, type, dir); mQuery.add(piece);把数据放入队列. notifyAll(); } }以上就是线程池的全部内容了.至于 下载线程,你可以自定义处理了.需要FetchImage构造方法.把一些参数传过去,继承extends Thread.public void run() { App app=(App) this.mContext.getApplicationContext(); HttpResponse response; /*synchronized (app.mDownloadPool) { if(app.mDownloadPool.GetThreadCount()==DownloadPool.MAX_THREAD_COUNT &&app.mDownloadPool.GetCount()>8) { Log.d(TAG, "当前的线程数为3,且等待下载的数量大于8,清除数据."); app.mDownloadPool.PopPiece(); } }*/如果这段没有,也可以,因为我觉得,当ListView滚动时,下载线程中可能有不再可见的内容,这时优先想看到的应该是当前显示的内容,所以把队列中的其它内容清除了,保留一小部分,可以再快地看到当前的ListView可见部分的图片内容.因为多线程,所以时刻记着同步处理操作. try { HttpParams httpParameters=new BasicHttpParams(); HttpConnectionParams.setConnectionTimeout(httpParameters, MicroBlog.CONNECT_TIMEOUT); HttpConnectionParams.setSoTimeout(httpParameters, MicroBlog.READ_TIMEOUT); DefaultHttpClient httpClient=new DefaultHttpClient(httpParameters); response=httpClient.execute(httpget); int code=response.getStatusLine().getStatusCode(); if (code==200) { byte[] bytes=EntityUtils.toByteArray(response.getEntity()); String filePath=SaveIconToFile(mName, bytes); Bundle bundle=new Bundle(); bundle.putString("name", filePath); FetchImage.SendMessage(mHandler, mType, bundle, uri); } else { Log.d(TAG, "下载图片失败:"+uri); } } catch (IOException e) { Log.d(TAG, "uri:"+uri+" exception:"+e.toString()); //e.printStackTrace(); } finally { // 默认把它移出,下载失败后不再下载。 app.mDownloadPool.ActiveThread_Pop(); } }SendMessage发送消息下载完成 .这里需要handler,就是构造时传来的参数了.DownloadPool:public void PopPiece() { synchronized (this) { int size=mQuery.size(); mQuery=mQuery.subList(size-5, size); } }为什么选择8和这里的保留5个url,因为我的一个ListView显示的图片可能一般情况下可见区会有5张图片,所以保留5个,不至于第一张不下载.如果上限数量变大,就是需要等待更多的图片下载完成后才会下载当前的图片,8这个上限没有太多 的根据,暂时定义的.SaveIconToFile()就是保存图片了.public String SaveIconToFile(String name, byte[] data) { String str2=dir+"/"+name; //Log.d(TAG, "str2:"+str2); FileOutputStream outputStream=null; try { outputStream=new FileOutputStream(str2); /*Options options=new Options(); options.inJustDecodeBounds=true; BitmapFactory.decodeByteArray(data, 0, data.length, options); int heightRatio=(int) Math.ceil(options.outHeight/(float) 800); int widthRatio=(int) Math.ceil(options.outWidth/(float) 480); if (heightRatio>1&&widthRatio>1) { if (heightRatio>widthRatio) { // Height ratio is larger, scale according to it options.inSampleSize=heightRatio; } else { // Width ratio is larger, scale according to it options.inSampleSize=widthRatio; } } options.inDither=true; options.inJustDecodeBounds=false; options.inPreferredConfig=Config.RGB_565; Bitmap bitmap=BitmapFactory.decodeByteArray(data, 0, data.length, options);*/这段注释了,我不需要下载的图片压缩存储,因为压缩了图片的质量就少了许多,也可以压缩处理,但你控制好你的图片质量,如果你需要显示高清的原图,就不要压缩,上面的代码是宽高大约是480*800以上时会压缩,现在主流手机分辨率就是这个,我觉得只有超过了才需要压缩.当然,你可以通过传来更多的参数来处理是否压缩,压缩的质量等.Bitmap bitmap=BitmapFactory.decodeByteArray(data, 0, data.length); bitmap.compress(CompressFormat.PNG, 100, outputStream); outputStream.flush(); bitmap.recycle(); } catch (Exception e) { }finally { if(outputStream!=null){ try { outputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } return str2; }外部调用:Activity中((App) mContext.getApplicationContext()).mDownloadPool.Push( mHandler, url, mCacheDir+"/picture/");至于其它参数可以自己看着办吧,处理的事务不同需要的东西不同.不必太在意我传的是什么.但是mHandler回调还是必须的.Handler mHandler=new Handler() { @Override public void handleMessage(Message msg) { int what=msg.what; Bundle bundle=msg.getData(); String imgUrl=(String) msg.obj; Bitmap bitmap=BitmapFactory.decodeFile(bundle.getString("name")); if (bitmap!=null&&!bitmap.isRecycled()) {这样就可以处理了.如果你的图片不需要存储在文件系统中,你可以直接把下载的流解析成Bitmap,然后通过Handler传过来.}}这里的bitmap可以存储在一个map中,这样你就有了一个内存缓存了,然后在调用((App) mContext.getApplicationContext()).mDownloadPool.Push( mHandler, url, mCacheDir+"/picture/");前可以先检查下内存缓存中是否已经存在了图片.BmpCache bmpCache=BmpCache.getInstance(); bmpCache.save(imgUrl, bitmap);这样保存,缓存就不列出了,网上搜索下到处是,可以用一个简单点的.图片下载一直是比较麻烦的问题,如何缓存,如何处理图片都需要小心操作,因为图片的可用内存有限,太多时oome容易出现.
我的微博程序用上面的线程池处理后普通下载,gif图片另外处理,即使同时下载与解析gif动态图2m左右,帧数大约150的也没有出现oome.
线程池下载图片只是通用的一个方法.如果你需要下载一张大图,而下载不是同时进行的,建议还是单独写一个下载的方法,容易控制.因为下载8m或更大的图片也是个问题,还有下载后的存储,不宜用这种方法.
更多时候不是抱怨系统为何只提供这么少的内存供图片使用,先反省下自己的处理方式.
希望可以帮助别更多的人.
2011.12.11.