Universal-imageLoader缓存图片加载

Posted tuke_tuke

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Universal-imageLoader缓存图片加载相关的知识,希望对你有一定的参考价值。

一,先要配置ImageLoaderConfiguration这个类实现全局ImageLoader的实现情况

ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(
				this)
				.memoryCacheExtraOptions(480, 800)
				// max width, max height,即保存的每个缓存文件的最大长宽
				.discCacheExtraOptions(480, 800, null)
				//Can slow ImageLoader, use it carefully (Better don't use
				// it)/设置缓存的详细信息,最好不要设置这个
				.threadPoolSize(3)// 线程池内加载的数量
				
				.threadPriority(Thread.NORM_PRIORITY - 2)
				.denyCacheImageMultipleSizesInMemory()
				.memoryCache(new UsingFreqLimitedMemoryCache(2 * 1024 * 1024))
				// You can pass your own memory cache
				// implementation/你可以通过自己的内存缓存实现
				.memoryCacheSize(2 * 1024 * 1024)//内存大小
				.discCacheSize(50 * 1024 * 1024)//硬盘大小
				.discCacheFileNameGenerator(new Md5FileNameGenerator())
				// 将保存的时候的URI名称用MD5 加密
				.tasksProcessingOrder(QueueProcessingType.LIFO)
				.discCacheFileCount(100) // 缓存的文件数量  
				.discCache(new UnlimitedDiscCache(new File(Environment.getExternalStorageDirectory()+ "/myApp/imgCache")))  // 自定义缓存路径	  
				.defaultDisplayImageOptions(getDisplayOptions())
				.imageDownloader(new BaseImageDownloader(this, 5 * 1000, 30 * 1000))
				.writeDebugLogs() // Remove for release app
				.build();// 开始构建
		ImageLoader.getInstance().init(config);//全局初始化此配置<span style="margin: 0px; padding: 0px; border: currentcolor; font-family: Consolas, 'Courier New', Courier, mono, serif; line-height: 18px; background-color: inherit;"> </span>

二,使用ImageLoader进行图片加载的时候,先要实例化ImageLoader

protected ImageLoader imageLoader = ImageLoader.getInstance(); 
之后进行显示的图片的各种格式DisplayImageOptions 的设置

DisplayImageOptions options;
		options = new DisplayImageOptions.Builder()
				.showImageOnLoading(R.drawable.ic_launcher)// 设置图片在下载期间显示的图片
				.showImageForEmptyUri(R.drawable.ic_launcher)// 设置图片Uri为空或是错误的时候显示的图片
				.showImageOnFail(R.drawable.ic_launcher) // 设置图片加载/解码过程中错误时候显示的图片
				
				.cacheInMemory(true)// 设置下载的图片是否缓存在内存中
				.cacheOnDisc(true)// 设置下载的图片是否缓存在SD卡中
				
				.considerExifParams(true) // 是否考虑JPEG图像EXIF参数(旋转,翻转)
				.imageScaleType(ImageScaleType.EXACTLY_STRETCHED)// 设置图片以如何的编码方式显示
				.bitmapConfig(Bitmap.Config.RGB_565)// 设置图片的解码类型//
				// .delayBeforeLoading(int delayInMillis)//int
				// delayInMillis为你设置的下载前的延迟时间
				// 设置图片加入缓存前,对bitmap进行设置
				// .preProcessor(BitmapProcessor preProcessor)
				.resetViewBeforeLoading(true)// 设置图片在下载前是否重置,复位
				.displayer(new RoundedBitmapDisplayer(20))// 是否设置为圆角,弧度为多少
				.displayer(new FadeInBitmapDisplayer(100))// 是否图片加载好后渐入的动画时间
				.build();// 构建完成

按照你所需要的配置去设置,如果不需要的就可以不做配置。

三,三者的关系

ImageLoaderConfiguration是针对图片缓存的全局配置,主要有线程类、缓存大小、磁盘大小、图片下载与解析、日志方面的配置。

ImageLoader是具体下载图片,缓存图片,显示图片的具体执行类,它有两个具体的方法displayImage(...)、loadImage(...),但是其实最终他们的实现都是displayImage(...)。

DisplayImageOptions用于指导1,每一个Imageloader根据网络图片的状态(空白、下载错误、正在下载)显示对应的图片,2,是否将缓存加载到磁盘上,下载完后对图片进行怎么样的处理。

从三者的协作关系上看,他们有点像厨房规定、厨师、客户个人口味之间的关系。ImageLoaderConfiguration就像是厨房里面的规定,每一个厨师要怎么着装,要怎么保持厨房的干净,这是针对每一个厨师都适用的规定,而且不允许个性化改变。ImageLoader就像是具体做菜的厨师,负责具体菜谱的制作。DisplayImageOptions就像每个客户的偏好,根据客户是重口味还是清淡,每一个imageLoader根据DisplayImageOptions的要求具体执行。

1.纯粹为了加载默认配置的一个图片的

ImageLoader.getInstance().displayImage(imageUrl, imageView); // imageUrl代表图片的URL地址,imageView代表承载图片的IMAGEVIEW控件 

import java.io.File;

import android.app.Application;
import android.graphics.Bitmap;
import android.os.Environment;

import com.nostra13.universalimageloader.cache.disc.impl.UnlimitedDiscCache;
import com.nostra13.universalimageloader.cache.disc.naming.Md5FileNameGenerator;
import com.nostra13.universalimageloader.cache.memory.impl.UsingFreqLimitedMemoryCache;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
import com.nostra13.universalimageloader.core.ImageLoader;
import com.nostra13.universalimageloader.core.ImageLoaderConfiguration;
import com.nostra13.universalimageloader.core.assist.ImageScaleType;
import com.nostra13.universalimageloader.core.assist.QueueProcessingType;
import com.nostra13.universalimageloader.core.display.FadeInBitmapDisplayer;
import com.nostra13.universalimageloader.core.display.RoundedBitmapDisplayer;
import com.nostra13.universalimageloader.core.download.BaseImageDownloader;

public class MyApplication extends Application {
	@Override
	public void onCreate() {
		// TODO Auto-generated method stub
		super.onCreate();
		ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(
				this)
				.memoryCacheExtraOptions(480, 800)
				// max width, max height,即保存的每个缓存文件的最大长宽
				.discCacheExtraOptions(480, 800, null)
				//Can slow ImageLoader, use it carefully (Better don't use
				// it)/设置缓存的详细信息,最好不要设置这个
				.threadPoolSize(3)// 线程池内加载的数量
				
				.threadPriority(Thread.NORM_PRIORITY - 2)
				.denyCacheImageMultipleSizesInMemory()
				.memoryCache(new UsingFreqLimitedMemoryCache(2 * 1024 * 1024))
				// You can pass your own memory cache
				// implementation/你可以通过自己的内存缓存实现
				.memoryCacheSize(2 * 1024 * 1024)//内存大小
				.discCacheSize(50 * 1024 * 1024)//硬盘大小
				.discCacheFileNameGenerator(new Md5FileNameGenerator())
				// 将保存的时候的URI名称用MD5 加密
				.tasksProcessingOrder(QueueProcessingType.LIFO)
				.discCacheFileCount(100) // 缓存的文件数量  
				.discCache(new UnlimitedDiscCache(new File(Environment.getExternalStorageDirectory()+ "/myApp/imgCache")))  // 自定义缓存路径	  
				.defaultDisplayImageOptions(getDisplayOptions())
				.imageDownloader(new BaseImageDownloader(this, 5 * 1000, 30 * 1000))
				.writeDebugLogs() // Remove for release app
				.build();// 开始构建
		//全局初始化此配置 
		ImageLoader.getInstance().init(config);
	}

	private DisplayImageOptions getDisplayOptions() {
		DisplayImageOptions options;
		options = new DisplayImageOptions.Builder()
				.showImageOnLoading(R.drawable.ic_launcher)// 设置图片在下载期间显示的图片
				.showImageForEmptyUri(R.drawable.ic_launcher)// 设置图片Uri为空或是错误的时候显示的图片
				.showImageOnFail(R.drawable.ic_launcher) // 设置图片加载/解码过程中错误时候显示的图片
				
				.cacheInMemory(true)// 设置下载的图片是否缓存在内存中
				.cacheOnDisc(true)// 设置下载的图片是否缓存在SD卡中
				
				.considerExifParams(true) // 是否考虑JPEG图像EXIF参数(旋转,翻转)
				.imageScaleType(ImageScaleType.EXACTLY_STRETCHED)// 设置图片以如何的编码方式显示
				.bitmapConfig(Bitmap.Config.RGB_565)// 设置图片的解码类型//
				// .delayBeforeLoading(int delayInMillis)//int
				// delayInMillis为你设置的下载前的延迟时间
				// 设置图片加入缓存前,对bitmap进行设置
				// .preProcessor(BitmapProcessor preProcessor)
				.resetViewBeforeLoading(true)// 设置图片在下载前是否重置,复位
				.displayer(new RoundedBitmapDisplayer(20))// 是否设置为圆角,弧度为多少
				.displayer(new FadeInBitmapDisplayer(100))// 是否图片加载好后渐入的动画时间
				.build();// 构建完成
		return options;
	}
}
public class MainActivity extends Activity {
	private ImageLoader loader;
	private ImageView iv_img;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		loader = ImageLoader.getInstance();
		iv_img = (ImageView) this.findViewById(R.id.iv_img);
		String uri = "file:///" + "本地路径";
//		loader.displayImage(
//				"http://s1.jikexueyuan.com/current/static/images/logo.png",
//				iv_img);
		loader.displayImage(
				"http://s1.jikexueyuan.com/current/static/images/logo.png",
				iv_img, new ImageLoadingListener() {

					@Override
					public void onLoadingStarted(String arg0, View arg1) {
						Log.i("info", "onLoadingStarted");
					}

					@Override
					public void onLoadingFailed(String arg0, View arg1,
							FailReason arg2) {
						Log.i("info", "onLoadingFailed");
					}

					@Override
					public void onLoadingComplete(String arg0, View arg1,
							Bitmap arg2) {
						Log.i("info", "onLoadingComplete");
					}

					@Override
					public void onLoadingCancelled(String arg0, View arg1) {
						Log.i("info", "onLoadingCancelled");
					}
				});
	}

2.加载自定义配置的一个图片的

ImageLoader.getInstance().displayImage(imageUrl, imageView,options); // imageUrl代表图片的URL地址,imageView代表承载图片的IMAGEVIEW控件 , options代表DisplayImageOptions配置文件 
		options = new DisplayImageOptions.Builder()
				.showImageOnLoading(R.drawable.ic_stub) // 设置图片下载期间显示的图片
				.showImageForEmptyUri(R.drawable.ic_empty) // 设置图片Uri为空或是错误的时候显示的图片
				.showImageOnFail(R.drawable.ic_error) // 设置图片加载或解码过程中发生错误显示的图片
				.cacheInMemory(true) // 设置下载的图片是否缓存在内存中
				.cacheOnDisk(true) // 设置下载的图片是否缓存在SD卡中
				.displayer(new RoundedBitmapDisplayer(20)) // 设置成圆角图片
				.build();

3.图片加载时候带加载情况的监听

imageLoader.displayImage(imageUrl, imageView, options, new ImageLoadingListener() {  
    @Override  
    public void onLoadingStarted() {  
       //开始加载的时候执行  
    }  
    @Override  
    public void onLoadingFailed(FailReason failReason) {        
       //加载失败的时候执行  
    }   
    @Override   
    public void onLoadingComplete(Bitmap loadedImage) {  
       //加载成功的时候执行  
    }   
    @Override   
    public void onLoadingCancelled() {  
       //加载取消的时候执行  
  
    }});  


四,ListView和GridView中的图片缓存

BaseActivity.java

package com.xwj.imageloaderdemo;

import com.nostra13.universalimageloader.core.ImageLoader;

import android.app.Activity;
import android.os.Bundle;

public abstract class BaseActivity extends Activity {
	
	protected ImageLoader imageLoader;

    /**
     * 初始化布局资源文件
     */
    public abstract int initResource();

    /**
     * 初始化组件
     */
    public abstract void initComponent();

    /**
     * 初始化数据
     */
    public abstract void initData();

    /**
     * 添加监听
     */
    public abstract void addListener();


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(initResource());
        imageLoader = ImageLoader.getInstance();
        initComponent();
        initData();
        addListener();
    }

}
ImageGridActivity.java

package com.xwj.imageloaderdemo;

import android.R.integer;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.BaseAdapter;
import android.widget.GridView;
import android.widget.ImageView;

import com.nostra13.universalimageloader.core.DisplayImageOptions;
import com.nostra13.universalimageloader.core.display.RoundedBitmapDisplayer;
import com.xwj.util.Constants;

public class ImageGridActivity extends BaseActivity{
	private GridView mGridGv;
	private DisplayImageOptions options;	// 设置图片显示相关参数
	private String[] imageUrls;		// 图片路径
	
	@Override
	public int initResource() {
		return R.layout.activity_gridview;
	}

	@Override
	public void initComponent() {
		mGridGv = (GridView) findViewById(R.id.gv_image);
	}

	@Override
	public void initData() {
		Bundle bundle = getIntent().getExtras();
		imageUrls = bundle.getStringArray(Constants.IMAGES);
		
		options = new DisplayImageOptions.Builder()
				.showImageOnLoading(R.drawable.ic_stub) // 设置图片下载期间显示的图片
				.showImageForEmptyUri(R.drawable.ic_empty) // 设置图片Uri为空或是错误的时候显示的图片
				.showImageOnFail(R.drawable.ic_error) // 设置图片加载或解码过程中发生错误显示的图片
				.cacheInMemory(true) // 设置下载的图片是否缓存在内存中
				.cacheOnDisk(true) // 设置下载的图片是否缓存在SD卡中
				.displayer(new RoundedBitmapDisplayer(20)) // 设置成圆角图片
				.build();
		
		mGridGv.setAdapter(new ItemGridAdapter());
	}

	@Override
	public void addListener() {
		mGridGv.setOnItemClickListener(new OnItemClickListener() {

			@Override
			public void onItemClick(AdapterView<?> parent, View view,
					int position, long id) {
				
			}
		});
	}
	
	class ItemGridAdapter extends BaseAdapter{

		@Override
		public int getCount() {
			return imageUrls.length;
		}

		@Override
		public Object getItem(int position) {
			return imageUrls[position];
		}

		@Override
		public long getItemId(int position) {
			return position;
		}

		@Override
		public View getView(int position, View convertView, ViewGroup parent) {
			ViewHolder viewHolder = null;
			if (convertView == null) {
				viewHolder = new ViewHolder();
				convertView = getLayoutInflater().inflate(R.layout.item_grid, parent, false);
				viewHolder.image = (ImageView) convertView.findViewById(R.id.iv_grid_image);
				convertView.setTag(viewHolder);
			} else {
				viewHolder = (ViewHolder) convertView.getTag();
			}
			
			imageLoader.displayImage(imageUrls[position], viewHolder.image, options);
			
			return convertView;
		}
		
		public class ViewHolder {
			public ImageView image;
		}
		
	}

}

ImageListActivity.java

package com.xwj.imageloaderdemo;

import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;

import com.nostra13.universalimageloader.core.DisplayImageOptions;
import com.nostra13.universalimageloader.core.display.RoundedBitmapDisplayer;
import com.xwj.util.Constants;

public class ImageListActivity extends BaseActivity {

	private ListView mListImageLv;
	private DisplayImageOptions options; // 设置图片显示相关参数
	private String[] imageUrls; // 图片路径

	@Override
	public int initResource() {
		return R.layout.activity_list;
	}

	@Override
	public void initComponent() {
		mListImageLv = (ListView) findViewById(R.id.lv_image);
	}

	@Override
	public void initData() {
		Bundle bundle = getIntent().getExtras();
		imageUrls = bundle.getStringArray(Constants.IMAGES);

		// 使用DisplayImageOptions.Builder()创建DisplayImageOptions
		options = new DisplayImageOptions.Builder()
				.showImageOnLoading(R.drawable.ic_stub) // 设置图片下载期间显示的图片
				.showImageForEmptyUri(R.drawable.ic_empty) // 设置图片Uri为空或是错误的时候显示的图片
				.showImageOnFail(R.drawable.ic_error) // 设置图片加载或解码过程中发生错误显示的图片
				.cacheInMemory(true) // 设置下载的图片是否缓存在内存中
				.cacheOnDisk(true) // 设置下载的图片是否缓存在SD卡中
				.displayer(new RoundedBitmapDisplayer(20)) // 设置成圆角图片
				.build(); // 构建完成

		mListImageLv.setAdapter(new ItemListAdapter());
	}

	@Override
	public void addListener() {

	}

	class ItemListAdapter extends BaseAdapter {

		@Override
		public int getCount() {
			return imageUrls.length;
		}

		@Override
		public Object getItem(int position) {
			return imageUrls[position];
		}

		@Override
		public long getItemId(int position) {
			return position;
		}

		@Override
		public View getView(int position, View convertView, ViewGroup parent) {
			ViewHolder viewHolder = null;
			if (convertView == null) {
				convertView = getLayoutInflater().inflate(R.layout.item_list,
						null);
				viewHolder = new ViewHolder();
				viewHolder.image = (ImageView) convertView
						.findViewById(R.id.iv_image);
				viewHolder.text = (TextView) convertView
						.findViewById(R.id.tv_introduce);
				convertView.setTag(viewHolder);
			} else {
				viewHolder = (ViewHolder) convertView.getTag();
			}

			/**
			 * imageUrl 图片的Url地址 imageView 承载图片的ImageView控件 options
			 * DisplayImageOptions配置文件
			 */
			imageLoader.displayImage(imageUrls[position],
					viewHolder.image, options);
			viewHolder.text.setText("Item " + (position + 1)); // TextView设置文本

			return convertView;
		}

		public class ViewHolder {
			public ImageView image;
			public TextView text;
		}

	}

}
ImageLoaderApplication.java

package com.xwj.imageloaderdemo;

import java.io.File;

import android.app.Application;
import android.content.Context;

import com.nostra13.universalimageloader.cache.disc.impl.UnlimitedDiscCache;
import com.nostra13.universalimageloader.cache.disc.naming.Md5FileNameGenerator;
import com.nostra13.universalimageloader.cache.memory.impl.UsingFreqLimitedMemoryCache;
import com.nostra13.universalimageloader.core.ImageLoader;
import com.nostra13.universalimageloader.core.ImageLoaderConfiguration;
import com.nostra13.universalimageloader.core.assist.QueueProcessingType;
import com.nostra13.universalimageloader.core.download.BaseImageDownloader;
import com.nostra13.universalimageloader.utils.StorageUtils;

public class ImageLoaderApplication extends Application {
	public void onCreate() {
		super.onCreate();
		initImageLoader(getApplicationContext());
	}

	public static void initImageLoader(Context context) {
		//缓存文件的目录
		File cacheDir = StorageUtils.getOwnCacheDirectory(context, "imageloader/Cache"); 
		ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(context)
				.memoryCacheExtraOptions(480, 800) // max width, max height,即保存的每个缓存文件的最大长宽 
				.threadPoolSize(3) //线程池内加载的数量
				.threadPriority(Thread.NORM_PRIORITY - 2)
				.denyCacheImageMultipleSizesInMemory()
				.diskCacheFileNameGenerator(new Md5FileNameGenerator()) //将保存的时候的URI名称用MD5 加密
				.memoryCache(new UsingFreqLimitedMemoryCache(2 * 1024 * 1024)) // You can pass your own memory cache implementation/你可以通过自己的内存缓存实现
				.memoryCacheSize(2 * 1024 * 1024) // 内存缓存的最大值
				.diskCacheSize(50 * 1024 * 1024)  // 50 Mb sd卡(本地)缓存的最大值
				.tasksProcessingOrder(QueueProcessingType.LIFO)
				// 由原先的discCache -> diskCache
				.diskCache(new UnlimitedDiscCache(cacheDir))//自定义缓存路径  
				.imageDownloader(new BaseImageDownloader(context, 5 * 1000, 30 * 1000)) // connectTimeout (5 s), readTimeout (30 s)超时时间  
				.writeDebugLogs() // Remove for release app
				.build();
		//全局初始化此配置  
		ImageLoader.getInstance().init(config);
	}
}

manifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.xwj.imageloaderdemo"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
        android:minSdkVersion="8"
        android:targetSdkVersion="21" />

    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.INTERNET" />

    <application
        android:name="com.xwj.imageloaderdemo.ImageLoaderApplication"
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name="com.xwj.imageloaderdemo.MainActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity android:name="com.xwj.imageloaderdemo.ImageListActivity"></activity>
        <activity android:name="com.xwj.imageloaderdemo.ImageGridActivity"></activity>
    </application>

</manifest>
本地图片Uri:
String imageUri = "http://site.com/image.png"; // from Web  
String imageUri = "file:///mnt/sdcard/image.png"; // from SD card  
String imageUri = "content://media/external/audio/albumart/13"; // from content provider  
String imageUri = "assets://image.png"; // from assets  
String imageUri = "drawable://" + R.drawable.image; // from drawables (only images, non-9patch)  

2.1.DiskCache 磁盘缓存

1) UnlimitedDiscCache不限制缓存大小;

2) UnlimitedDiscCache很简单它只是简单的继承了BaseDiscCache并未对BaseDiscCache做任何扩展;
3) LimitedAgeDiscCache限制缓存时间;

4) LimitedAgeDiscCache该类实现了在缓存中删除被加载超过规定时间的文件:满足以下条件的时候就从缓存中删除文件:系统当前时间-文件的最新修改时间 > maxFileAge;

2.2.MemoryCache内存缓存

1) 通过实现接口MemoryCacheAware <String,Bitmap>来实现自己的缓存

2) UsingFreqLimitedCache (最少被用到的对象会被删除)

3) UsingAgeLimitedCache (最早被添加的对象会被删除)

4) LargestLimitedCache (空间占用最大的对象会被删除)

5) FIFOLimitedCache (根据先进先出的原则上删除多余对象)

2.3.不要忘记缓存的清理

一般要在整个应用结束的时候调用下面两个方法,分别情况内存缓存和磁盘缓存,提高用户体验 :
i. imageLoader.clearMemoryCache();
ii. imageLoader.clearDiskCache();


4.如果经常出现OOM(别人那边看到的,觉得很有提的必要)
①减少配置之中线程池的大小,(.threadPoolSize).推荐1-5
使用.bitmapConfig(Bitmap.config.RGB_565)代替ARGB_8888;
③使用.imageScaleType(ImageScaleType.IN_SAMPLE_INT)或者 try.imageScaleType(ImageScaleType.EXACTLY);
④避免使用RoundedBitmapDisplayer.他会创建新的ARGB_8888格式的Bitmap对象;
⑤使用.memoryCache(new WeakMemoryCache()),不要使用.cacheInMemory();

参考:

http://www.open-open.com/lib/view/open1433940304473.html

http://www.cnblogs.com/kissazi2/p/3886563.html

http://blog.csdn.net/vipzjyno1/article/details/23206387


以上是关于Universal-imageLoader缓存图片加载的主要内容,如果未能解决你的问题,请参考以下文章

Android-Universal-ImageLoader

关于universal imageloader缓存你需要知道的秘密

关于universal imageloader缓存你需要知道的秘密

flutter 图片缓存

Flutter——FadeInImage本地缓存图片

vue360图片没法缓存