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

仲远
• 阅读 4898

序言

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

点赞
收藏
评论区
推荐文章
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
Wesley13 Wesley13
4年前
java将前端的json数组字符串转换为列表
记录下在前端通过ajax提交了一个json数组的字符串,在后端如何转换为列表。前端数据转化与请求varcontracts{id:'1',name:'yanggb合同1'},{id:'2',name:'yanggb合同2'},{id:'3',name:'yang
Wesley13 Wesley13
4年前
Volley设计与实现分析
Volley设计与实现分析我们平时在开发Android应用的时候,不可避免地经常要通过网络来进行数据的收发,而多数情况下都是会用HTTP协议来做这些事情。Android系统主要提供了HttpURLConnection和ApacheHttpClient这两种方式来帮我们进行HTTP通信。对于这两种方式,Googl
Wesley13 Wesley13
4年前
Volley网络请求
第一步:导依赖dependencies{compile'eu.the4thfloor.volley:com.android.volley:2015.05.28'}第二步代码实现1\.创建一个RequestQueue对象。2\.创建一个StringRequest对象。3\.将StringRequest对象添加到Reques
CuterCorley CuterCorley
4年前
uni-app入门教程(5)接口的基本使用
前言本文主要介绍uniapp提供的一些基础接口,包括:网络请求接口,用于通过指定的请求方法,携带特定的数据,向特定的地址请求并返回请求结果;图片处理接口,包括选择、预览、获取信息、保存到本地等接口;文件处理接口,包括文件上传和下载接口;数据缓存接口,包括以同步或异步的方式保存、获取或删除数据的接口。一、网络请求小程序要想正常运转,都需要与服务器端进
希望的天 希望的天
4年前
Retrofit封装Okhttp逻辑原理
总结自retrofit封装了Okhttp本身并不能进行网络请求。只能在Android使用的网络请求框架。1.png2.pngrequest:统一完成(post/get/...)回调陷阱:完成上一步网络请求才能进行下一步网络请求。3.pngRetrofit简化了网络请求。优化了网络请求的使用。4.png5.png7.pngbuild设计模式:参数》5个;
Stella981 Stella981
4年前
Retrofit网络框架入门使用
1.简单介绍retrofit事实上就是对okhttp做了进一步一层封装优化。我们仅仅须要通过简单的配置就能使用retrofit来进行网络请求了。Retrofit能够直接返回Bean对象,比如假设我们进行一个网络接口的请求。返回来一串json字符串。那么这个时候一般我们都要拿到这个json字符串后进行解析得到相应的Bean对象,Ret
Stella981 Stella981
4年前
OkHttp三问—百度真题
来吧,今天说说常用的网络框架OKHttp,也是现在Android所用的原生网络框架(Android4.4开始,HttpURLConnection的底层实现被Google改成了OkHttp),GOGOGO!OKHttp有哪些拦截器,分别起什么作用OkHttp怎么实现连接池OkHttp里面用到
Stella981 Stella981
4年前
Retrofit源码解析(上)
简介Retrofit是Square公司开发的一款针对Android网络请求的框架,官网地址http://square.github.io/retrofit/,在官网上有这样的一句话介绍retrofit,AtypesafeHTTPclientforAndroidandJava。我们知道Retrofit底层是基于OKHttp实现的。对ok
Stella981 Stella981
4年前
Android 网络通信框架Volley简介(Google IO 2013)
1\.什么是Volley在这之前,我们在程序中需要和网络通信的时候,大体使用的东西莫过于AsyncTaskLoader,HttpURLConnection,AsyncTask,HTTPClient(Apache)等,今年的GoogleI/O2013上,Volley发布了。Volley是Android平台上的网络通信库,能使网络通信更快,更简单,更健
Stella981 Stella981
4年前
Android Volley完全解析(一),初识Volley的基本用法
1\.Volley简介我们平时在开发Android应用的时候不可避免地都需要用到网络技术,而多数情况下应用程序都会使用HTTP协议来发送和接收网络数据。Android系统中主要提供了两种方式来进行HTTP通信,HttpURLConnection和HttpClient,几乎在任何项目的代码中我们都能看到这两个类的身影,使用率
仲远
仲远
Lv1
一年将尽夜,万里未归人。
文章
3
粉丝
0
获赞
0