Android网络图片请求+二级缓存实现

序言

对于android学习者,对于网络请求势必都经历这样的一个过程,通过HttpClient或者HttpUrlConnection,来发其请求然后通过Handler进行数据的传递,非常的麻烦,然后后来你知道了有Volley,OKHttp,来让我们尝试动手写个网络请求的小工具吧,来对其进行一个剖析。

图片请求网络框架

对于图片的请求,我们需要设置一个缓存,通过缓存策略来减少网络请求,从而减少电量消耗和流量消耗,缓存策略通过二级缓存策略,内存作为一级缓存,磁盘作为二级缓存,缓存采用LRU的方式来进行管理,到得不到指定的内容之后,向网络发起请求来获得图片。这么一听貌似很简单的呀,来我们一个坑一个坑的踩过去。

现在我要根据URL来找一个图片,那么先从内存中取。

 public Bitmap loadBitmap(String uri,int reqWidth,int reqHeight){
        Bitmap bitmap = loadBitmapFromMemCache(uri);
        if(bitmap!=null)
            return bitmap;
        try{
            bitmap = loadBitmapFromDiskCache(uri,reqWidth,reqHeight);
            if(bitmap!=null)
                return bitmap;
            bitmap = loadBitmapFromHttp(uri,reqWidth,reqHeight);
        }catch (IOException e){
            e.printStackTrace();
        }
        if(bitmap==null&&mIsDiskLruCacheCreated){
            bitmap = downloadBitmapFromUrl(uri);
        }
    }
  • loadBitmapFromMemCache:先是从内存中拉取

private Bitmap loadBitmapFromMemCache(String url){
        final String key = hashKeyFormUrl(url);
        Bitmap bitmap = getBitmapFromMemCache(key);
        return bitmap;
    }

通过Url我们向内存中缓存进行查找,我们首先将url进行一个哈希,对其哈希的原因很明显,我们的url中可能还有一个特殊字符,影响我们的使用,所以我们一般采用其MD5值来作为key,这个函数这里我们先不去看,现在知道其作用即可。我们所关心的重点是getBitmapFromMemcache(),如何来实现这个方法。

 private Bitmap getBitmapFromMemCache(String key){
        return mMemoryCache.get(key);
    }

从一个对象中来拿,这个就是我们用来管理内存的LruCache,如何创建这个对象呢?我们创建的时候,还需要重写它的sizeOf方法。

mMemoryCache = new LruCache(cacheSize) {
            @Override
            protected int sizeOf(String key, Bitmap value) {
                return value.getRowBytes() * value.getHeight() / 1024;
            }
        };

显然,我们可以通过put方法将我们的图片缓存进去,到此,我们关于从内存缓存中如何取图片已经完成了,然后是当我们的在内存中找不到图片从磁盘的缓存中查找的时候。

  • loadBitmapFromDiskCache
    从磁盘加载

 private Bitmap loadBitmapFromDiskCache(String url,int reqWidth,int reqHeight) throws IOException{
        if(Looper.myLooper()==Looper.getMainLooper()){
            Log.w(TAG,"load bitmap from the UI Thread is not recommended!");
        }
        if(mDiskLruCache ==null){
            return null;
        }
        Bitmap bitmap = null;
        String key = hashKeyFormUrl(url);
        DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
        if(snapshot!=null){
            FileInputStream fileInputStream = (FileInputStream)snapshot.getInputStream(DISK_CACHE_INDEX);
            FileDescriptor fileDescriptor = fileInputStream.getFD();
            bitmap = mBitmapHelper.decodeBitmapFromFileDescriptor(fileDescriptor,reqWidth,reqHeight);
            if(bitmap!=null){
                addBitmapToMemoryCache(key,bitmap);
            }
        }
        return bitmap;
    }

首先判断是否在是在主线程,从磁盘读取文件,不建议在主线程中对其读取,容易导致ANR。接着我们来看下我们的DisKLruCache的实现,将在下篇文章中进行剖析。从磁盘中查找图片得不到的时候,我们会发起网络请求。

  • loadBitmapFromHttp
    从网络中获取图片

private Bitmap loadBitmapFromHttp(String url,int reqWidth,int reqHeight) throws IOException{
        if(Looper.myLooper()==Looper.getMainLooper()){
            throw new RuntimeException("can not visit network from UI thread");
        }
        if(mDiskLruCache == null){
            return null;
        }
        String key = hashKeyFormUrl(url);
        DiskLruCache.Editor editor = mDiskLruCache.edit(key);
        if(editor!=null){
            OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
            if(downloadUrlToStream(url,outputStream)){
                editor.commit();
            }else{
                editor.abort();
            }
            mDiskLruCache.flush();
        }
        return loadBitmapFromDiskCache(url,reqWidth,reqHeight);
    }

首先我们要进行线程的检测,判断是否处于主线程,然后调用了downloadUrlToStream()来从网络中获取数据流,然后将该数据流转交给DiskLruCache,也就是将图片文件写进我们的磁盘缓存中,然后调用从磁盘缓存中加载图片。来看下如何实现根据提供的url来获取一个数据流的。

  • downloadUrlToStream

 //将网络流转化为数据流
    public boolean downloadUrlToStream(String urlString,OutputStream outputStream){
        HttpURLConnection urlConnection = null;
        BufferedOutputStream out = null;
        BufferedInputStream in = null;
        try{
            final URL url = new URL(urlString);
            urlConnection = (HttpURLConnection)url.openConnection();
            in = new BufferedInputStream(urlConnection.getInputStream(),IO_BUFFER_SIZE);
            out = new BufferedOutputStream(outputStream,IO_BUFFER_SIZE);
            int b;
            while((b=in.read())!=-1){
                out.write(b);
            }
            return true;
        }catch (IOException e){
            Log.e(TAG,"downloadBitmap failed"+e);
        }finally {
            if(urlConnection!=null){
                urlConnection.disconnect();
            }
            IOUtils.close(out);
            IOUtils.close(in);
        }
        return false;
    }

我们向该函数传递了我们所需要的两个参数,一个url,一个是输出流,我们通过UrlConnection来获取了一个和网络的连接,获取了数据流,然后根据流来读取,之后写入到我们的输出流,如果你对Java研究并不是很深入,可能听到流,会有写模糊,对其底层的细节也想了解,别急,后续文章会继续来讲。这样我们实现了将数据写入到我们的磁盘缓存。再回到我们最初,我们对于这次湖区图片后面还做了一个判断,你可能会感觉到疑惑了,为什么还要做这么一次操作。不应该是100%的可以从网络中获取到图片的吗?其实不然,这个时候,我们可能会在网络加载等各方面问题上出现了状况,这个时候,我们选择从网络重新进行加载。

//根据Url下载图片
    private Bitmap downloadBitmapFromUrl(String urlString){
        Bitmap bitmap = null;
        HttpURLConnection urlConnection = null;
        BufferedInputStream in = null;
        try{
            final URL url = new URL(urlString);
            urlConnection = (HttpURLConnection) url.openConnection();
            in = new BufferedInputStream(urlConnection.getInputStream(),IO_BUFFER_SIZE);
            bitmap = BitmapFactory.decodeStream(in);
        }catch (final IOException e){

        }finally {
            if(urlConnection!=null){
                urlConnection.disconnect();
            }
            IOUtils.close(in);
        }
        if(bitmap!=null)
        
        return bitmap;
    }

如此一个图片缓存的框架结束了,当然从网络加载过来的图片我们的不可能是将其全部加载到内存,我饿们需要根据其大小做一个显示的处理,处理方式。
获取图片的宽高,根据我们需要的宽高进行一个缩放比对,修改了其属性之后,然后将其设置该Bitmap的属性。从而减小其体积。

public static Bitmap decodeBitmapFromFileDescriptor(FileDescriptor fd,int reqWidth,int reqHeight){
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeFileDescriptor(fd,null,options);
        options.inSampleSize = calculateInSampleSize(options,reqWidth,reqHeight);
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeFileDescriptor(fd,null,options);
    }

    public static int calculateInSampleSize(BitmapFactory.Options options,int reqWidth,int reqHeight){
        final int height = options.outHeight;
        final int width = options.outWidth;
        int inSampleSize = 1;
        if(height>reqHeight||width>reqHeight){
            final int halfHeight = height/2;
            final int halfWidth = width/2;
            while((halfHeight/inSampleSize)>=reqHeight&&(halfWidth/inSampleSize)>=reqWidth){
                inSampleSize *=2;
            }
        }
        return inSampleSize;
    }

这里我们提供了另一个方式,支持ImageView绑定一个View

 //实现异步加载
    public void bindBitmap(final String uri,final ImageView imageView,final int reqWidth,final int reqHeight){
        imageView.setTag(uri);
        Bitmap bitmap = loadBitmapFromMemCache(uri);
        if(bitmap!=null){
            imageView.setImageBitmap(bitmap);
            return;
        }
        Runnable loadBitmapTask = new Runnable() {
            @Override
            public void run() {
                Bitmap bitmap = loadBitmap(uri,reqWidth,reqHeight);
                if(bitmap!=null){
                    LoaderResult result = new LoaderResult(imageView,uri,bitmap);
                    //get the message from messge pool avoid to create new message
                    mMainHandler.obtainMessage(MESSAGE_POST_RESULT,result).sendToTarget();
                }
            }
        };
        THREAD_POOL_EXECUTOR.execute(loadBitmapTask);
    }

这个方法,先是从内存中查找,如果找到,则进行设置,如果没有找到,则从网络发起请求,创建了一个Runnable,然后将我们上述的从内存,网络中的请求封装到其中,丢给线程池来处理,这个时候问题就来了,丢给线程池之后,我们和UI线程就不在一个线程了,这个时候,需要我们进行线程的切换,如何来操纵我们view,或者是将我们的结果传递出去,实现方式是。对结果类进行了一个封装,将我们的view和结果封装为一个结果类。

private static class LoaderResult{
        public ImageView imageView;
        public String uri;
        public Bitmap bitmap;

        public LoaderResult(ImageView imageView,String uri,Bitmap bitmap){
            this.imageView = imageView;
            this.uri = uri;
            this.bitmap = bitmap;
        }
    }

得到了结果,将其通过消息发送的方式,传递给我们的主线程,然后在Handler中进行处理。处理方式。

//  handler实现交互
    private Handler mMainHandler = new Handler(Looper.getMainLooper()){
        @Override
        public void handleMessage(Message msg) {
            LoaderResult result = (LoaderResult)msg.obj;
            ImageView imageView = result.imageView;
            //imageView.setImageBitmap(result.bitmap);
            String uri = (String) imageView.getTag();
            if (uri.equals(result.uri)) {
                imageView.setImageBitmap(result.bitmap);
            }else{
                //set a default background
            }
        }
    };

这里,我们给ImageView设置一个tag用来标记,然后通过对其进行判断,然后将图片设置在上面。至此我们一个图片请求的框架就写好了,当然还是需要在进行一些优化的。接下来我们进行普通网络请求(非图片)库的一个封装封装。然后是对于两个缓存工具类的剖析。

你可能感兴趣的:(android,网络请求)