如何使Volley NetworkImageView在离线情况下正常工作

4
我使用从互联网下载图像并在我的中显示。现在,当没有网络可用时,我希望使显示保存的图像。因为已经通过作为键缓存了图像,所以当我使用

时,它已经具有了图像的缓存。
Entry entry = SingletonRequestQueue.getInstance(context).getRequestQueue().getCache().get(imageURL);

entry.data 不为空。但我的问题是图片分辨率很高,我无法使用。

Bitmap b = BitmapFactory.decodeByteArray(entry.data, 0, entry.data.length);

因为这会导致很多延迟,而且我必须重新发明轮子,因为我必须再次创建asynctask,查看listview何时滚动以取消解码、回收bitmap、在内存中创建缓存、找到最佳的insample值等等。
所以更好的想法是做一些技巧,使Volley NetworkImageView在没有网络时使用自己的DiskLRUCache来显示它们。
有什么想法吗?
我的代码:
public class SingletonRequestQueue {


    private static SingletonRequestQueue mInstance;
    private RequestQueue mRequestQueue;
    private ImageLoader mImageLoader;
    private static Context mCtx;
    private LruBitmapCache mLruBitmapCache;

    private SingletonRequestQueue(Context context) {
        mCtx = context;
        mRequestQueue = getRequestQueue();
        mLruBitmapCache = new LruBitmapCache(LruBitmapCache.getCacheSize(context));
        mImageLoader = new ImageLoader(mRequestQueue,mLruBitmapCache);

    }

    public static synchronized SingletonRequestQueue getInstance(Context context) {
        if (mInstance == null) {
            mInstance = new SingletonRequestQueue(context);
        }
        return mInstance;
    }

    public RequestQueue getRequestQueue() {
        if (mRequestQueue == null) {

            // getApplicationContext() is key, it keeps you from leaking the
            // Activity or BroadcastReceiver if someone passes one in.

            mRequestQueue = Volley.newRequestQueue(mCtx.getApplicationContext(),new OkHttpStack());
//          mRequestQueue = Volley.newRequestQueue(mCtx.getApplicationContext());
        }
        return mRequestQueue;
    }

    public <T> void addToRequestQueue(Request<T> req) {
        getRequestQueue().add(req);
    } 

    public ImageLoader getImageLoader() {
        return mImageLoader;
    }

    public LruBitmapCache getLruBitmapCache() {
        return mLruBitmapCache;
    }

    public void setLruBitmapCache(LruBitmapCache lruBitmapCache) {
        mLruBitmapCache = lruBitmapCache;
    }


}

在我的适配器中:

public IssueListAdapter(Context context, int resource, List<Issue> objects) {
        super(context, resource, objects);
        this.context = context;
        this.mIssueList = objects;
        mImageLoader = SingletonRequestQueue.getInstance(context).getImageLoader();
}

public static class ViewHolder{

    public  NetworkImageView mNetworkImageView;
    public  TextView mFee;
    public  TextView mName;
}


@Override
public View getView(int position, View convertView, ViewGroup parent) {


    ViewHolder holder;

    if(convertView == null){
        holder = new ViewHolder();
        LayoutInflater inflater =    
                (LayoutInflater) context.getSystemService(Activity.LAYOUT_INFLATER_SERVICE);
        convertView = inflater.inflate(R.layout.gridview_issuelist_item, parent, false);

        holder.mNetworkImageView = (NetworkImageView)convertView.findViewById(R.id.NetworkImageView_MainActivity_issue_image);
        holder.mName = (TextView)convertView.findViewById(R.id.TextView_MainActivity_name);
        holder.mFee = (TextView)convertView.findViewById(R.id.TextView_MainActivity_fee);
        Utility.settingTypfaceFont(context, holder.mName);
        Utility.settingTypfaceFont(context, holder.mFee);

        convertView.setTag(holder);

    }else{
        holder = (ViewHolder)(convertView.getTag());
    }

    final Issue issue = mIssueList.get(position);
    holder.mName.setText(issue.getTitle());
    holder.mFee.setText(String.valueOf(issue.getFee()));
    String imageURL = issue.getPublicCover();

    holder.mNetworkImageView.setImageUrl(imageURL, mImageLoader);
    holder.mNetworkImageView.setDefaultImageResId(R.drawable.placeholder2);;

    /*
    Entry entry = SingletonRequestQueue.getInstance(context).getRequestQueue().getCache().get(imageURL);
    if(entry != null && entry.data != null){
        byte[] imageByte = entry.data;
        loadBitmap(imageByte, holder.mNetworkImageView,imageURL);
    }else{
        holder.mNetworkImageView.setImageUrl(imageURL, mImageLoader);
    }*/

    return convertView;
}

@Override
public int getCount() {
    if(mIssueList != null){
        return mIssueList.size();           
    }
    else{
        return 0;
    }
}

public List<Issue> getIssueList() {
    return mIssueList;
}

}

NetworkImageView 使用了两级缓存:开发人员创建的内存缓存和 Volley 自身创建的磁盘缓存。首先在内存缓存中检查图像,然后在磁盘缓存中检查,如果没有找到,则才会发出网络请求。因此,在离线模式下,无需进行任何额外的操作即可使其正常工作。您能否分享您的代码以查看是否存在任何问题? - Manish Mulimani
@ManishMulimani 非常感谢您的帮助,我已经更新了我的问题。问题在于当我关闭应用程序并再次打开时,只显示图像的占位符,图像下方的所有文本都正确加载,但是图像没有加载。再次感谢。 - mmlooloo
ImageLoader已经有isCached(String requestUrl, int maxWidth, int maxHeight)方法,如果缓存检查不同于getRequestQueue().getCache(),则需要重新实现ImageLoader并添加一个Context成员,并使用您问题中的第一行来检查缓存的图像。src代码ImageLoader:https://android.googlesource.com/platform/frameworks/volley/+/master/src/com/android/volley/toolbox/ImageLoader.java - Yazan
DiskBasedCache 的默认最大缓存大小为5MB。由于您正在使用非常高分辨率的图像(3000x2500,即7.5MB),因此可以增加缓存大小。使用 DiskBasedCache(File rootDirectory, int maxCacheSizeInBytes) 构造函数来指定磁盘大小,例如 DiskBasedCache(cacheDir, 20 * 1024 * 1024) 表示20MB的缓存。请告诉我是否有所帮助。 - Manish Mulimani
@ManishMulimani 我已经将其设置为/** 默认磁盘使用最大值(字节)。*/private static final int DEFAULT_DISK_USAGE_BYTES = 20 * 1024 * 1024; - mmlooloo
通过从缓存中显式获取的 Entry 实例,您可以使用 entry.expired() 检查其是否过期,并使用 entry.refreshNeeded() 检查是否需要刷新。在 NetworkImageView 的情况下,如果条目已过期,则会发起网络请求。 - Manish Mulimani
6个回答

3
我更喜欢使用Volley / retrofit与Android-Universal-Image-Loader/Picasso一起使用,图片加载库确实在加载和缓存图像方面做得非常好。
它们默认情况下只需一行代码即可处理所有内容:
Picasso.with(context).load("http://i.imgur.com/DvpvklR.png").into(imageView);

此外,您可以对图像进行动画、调整大小,并在它们加载时添加占位符。

1
谢谢(+1) @Pedram,我已经知道这些了,但现在我不想使用其他库,而且我的图片尺寸是3000*2500,我认为这些库会在这个分辨率下出问题:-) - mmlooloo
我认为你需要两个版本的图片,一个是缩略图用于在“ListView”中显示,另一个是原始大小的图片。我建议使用Picasso生成图像的缩略图,并下载真实大小的图像,可以选择使用Volley并使用DiskLruCache进行缓存 https://github.com/JakeWharton/DiskLruCache。我查看了Volley的DiskBasedCache源代码,但并不感兴趣,请阅读此线程https://dev59.com/Y-o6XIcBkEYKwwoYKRHd 祝你好运 - M. Reza Nasirloo
@mmlooloo 我顺便找到了这个,试试看吧 https://dev59.com/rn_aa4cB1Zd3GeqPyx31#26565940 - M. Reza Nasirloo
感谢您的所有建议 :-) - mmlooloo

2
只需在BasicNetwork类中添加此行代码。
if (!ConnectivityUtils.isNetworkEnabled(CardApplication.getContext()) && request instanceof ImageRequest) {
            VolleyLog.e("Cached response", "No Network Connectivity for Url=", request.getUrl());
            return new NetworkResponse(HttpStatus.SC_NOT_MODIFIED,
                    request.getCacheEntry().data, responseHeaders, true);
        }

对于数据请求过期,您可以使用自己的HttpHeaderParser更改Cached.Entry

这里有一个链接详细解释了这个问题。


http://mobilewebwizard.in/2015/01/offline-caching-with-volley-using-cache-controlled-header/ - Vipin Sahu
欢迎您。实际上,您不应该缓存数据请求,这就是为什么我添加了request instanceof ImageRequest的原因。 - Vipin Sahu

2
当您在离线状态下重新启动应用程序时,您唯一可以依赖的是磁盘缓存(即DiskBasedCache)。Volley的本地缓存包括网络数据和响应头。但在这种情况下,我们只需要关注Cache-Control头。例如,如果服务器返回的头是"Cache-Control:max-age=604800",那么这告诉Volley将响应资源缓存604800秒(源自HttpHeaderParser.parseCacheHeaders())。然后,下次检索相同URL的数据时,会检查是否超过了缓存过期时间,最终决定从网络或本地检索。
根据您的描述,我认为您的服务器端向您传递了一个类似于Cache-Control:must-revalidate|proxy-revalidate|no-cache|no-store的值,这就是为什么在离线状态下无法重用最后检索到的数据的原因。
现在有一个问题出现了:一旦我们能够操纵缓存过期时间,我们就能够将其增加到足够大的值,以确保我们在离线状态下使用该数据。
不幸的是,Volley不支持我们这样做。因此,如果您可以让服务器端提供一个可行的max-age来解决这个问题呢?
如果不能,我建议您更换到另一个库,该库可以满足此要求。实际上有一个可以成为您朋友的库,那就是Netroid。它基于Volley并提供了一些改进,这不会使您的当前代码发生太大变化。使用它,控制过期时间将变得更加容易,并且还将提供更多功能。
mImageLoader = new SelfImageLoader(mRequestQueue, mLruBitmapCache) {
    @Override
    public void makeRequest(ImageRequest request) {
        // you can manipulate the cache expire time with one line code.
        request.setCacheExpireTime(TimeUnit.DAYS, 10);

        // you can even according to the different request to
        // set up the corresponding expire time.
        if (request.getUrl().contains("/for_one_day/")) {
            request.setCacheExpireTime(TimeUnit.DAYS, 1);
        } else {
            request.setCacheExpireTime(TimeUnit.DAYS, 10);
        }
    }
};

完整的代码在项目的示例模块中,希望这对您有所帮助。

谢谢,但我认为您没有仔细阅读我的问题,数据已经在我的disckcache中,因为我可以通过Bitmap b = BitmapFactory.decodeByteArray(entry.data, 0, entry.data.length);获取图像。 - mmlooloo
哦,是的,我非常抱歉,希望你能得到正确的答案;-) - VinceStyling

1

你好 @mmlooloo,我创建了一个使用 DiskLRUCacheVolley 的项目。这是我的存储库链接 DiskLRUCache using Volley。希望它能帮助你展示保存的图像。谢谢。


1
谢谢你的帮助(+1),但 Volley 已经缓存了我的图像,我只想让它从 DiskLRUCache 中获取。我正在使用 NetworkImageView 来处理列表视图和解码等所有问题,但在离线时它无法工作。 - mmlooloo
1
Volley仅使用内存缓存。因此,如果我们想要离线显示它,我们需要实现自己的DiskLRU缓存。请检查我的解决方案代码。 - androidcodehunter
2
不是的,它有DiskLRU,请查看源代码 :-) - mmlooloo
1
据我所知,Volley不支持DiskCache。好的,我会检查一下Volley的源代码。 - androidcodehunter
2
@Sharif,你错了。Volley会将每个响应都缓存在磁盘上。它会将图片缓存在您提供的ImageLoader的内存中。顺便说一下 - 像您在项目中所做的那样为ImageLoader类提供磁盘缓存是错误的。您应该提供一个内存缓存。您正在减慢应用程序的速度并创建磁盘冗余,因为Volley已经将每个响应都缓存在磁盘上。 - Itai Hanski

0
  1. 在互联网上搜索“android如何检查网络连接性”
  2. 实现并在缓存实现中进行检查(例如LruCache )。 if(networkAvailable()){getFromNetwork()} else {getFromCache()}

逻辑OK? 那就试试看吧。
似乎您的缓存实现类是LruBitmapCache

那么在该类中如何检查连接性呢?

public Bitmap getBitmap(String url) {
  if(networkAvailable()/* this is your impl */){
    // dont use cache
    return null;
  }
  return getFromCache();  // or something like that;
}

你不是因为没有联网才不使用缓存的... 缓存是为了加快性能并尽可能减少网络调用的时间。 - Martin Konecny

0

如果我理解你的意思正确的话,你希望在应用程序运行期间,将提供给你的ImageLoader类(由你的NetworkImageView使用)的内存缓存持久化,而且不会丢失其作为内存缓存的特性。

这个内存缓存在正常操作中会保留正确大小的位图 - 即使网络断开,你也希望它仍然可用。

因此,这里有个想法:每次关闭你的应用程序时,将缓存中的图片持久化到文件中。下次加载应用程序时,在创建内存缓存时,检查磁盘上是否存在已持久化的版本,如果有,则从磁盘中填充内存缓存。

对于何时持久化图像和何时删除图像,你可以采取多种方法来决定。

这里是一种方法:创建一个混合内存/磁盘缓存。它与你当前的内存缓存工作方式完全相同,只有以下几点不同:

每次调用putBitmap()时,除了正常操作外,还要在后台线程/AsyncTask中将位图的编码版本保存到磁盘上。
每当从缓存中删除位图(我假设您对缓存有某种空间限制)时,在后台线程/AsyncTask中删除磁盘上的文件。
创建一个“loadFromDisk”任务,在每次创建内存缓存(每个应用程序运行一次)时在后台执行,以使用磁盘上可用的图像填充内存缓存。
您无法避免解码位图,但可以减小大小并避免处理大型位图的调整大小问题。
这对您有帮助吗?

这对你有帮助吗?不是的,因为你没有仔细阅读我的问题。Volley使用DiskLRUCache和内存缓存。我的数据在DiskLRUCache中。 - mmlooloo
我已经阅读了你的问题。你写道,保存在磁盘缓存中的图像太大了,你不想费力去解码和调整它们的大小。我为你提供了一种跳过调整大小部分的方法。很抱歉,保存位图的唯一方法是通过编码 - 因此,如果你想要持久性,解码是无法避免的。也许你没有理解我的答案。 - Itai Hanski
也许我的注释不好,但我不想自己解码,我想让Volley解码它。 - mmlooloo

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接