`
phenom
  • 浏览: 406123 次
  • 性别: Icon_minigender_1
  • 来自: 福州
社区版块
存档分类
最新评论

android 下载图片

 
阅读更多
一个网络程序下载图片通常是一个大麻烦,如何处理好下载,才是关键的问题,这关系到程序的性能,甚至崩溃,出现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.
分享到:
评论
2 楼 u012911704 2013-12-26  
没大看懂 程序依然OOM=.=
1 楼 yun2223 2013-01-24  
我也福州啊呵呵 楼主

相关推荐

Global site tag (gtag.js) - Google Analytics