当前位置: 代码迷 >> Android >> android 上载图片
  详细解决方案

android 上载图片

热度:43   发布时间:2016-05-01 19:31:39.0
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.
  相关解决方案