刚写完这篇文章突然想起来是不是可以使用接口来代替Picture这样可以更加通用,于是试了一下果然可以.所以读者们可以将Picture替换成IPicture接口.
public interface IPicture {
String getFileName();
}
本项目使用的Model是Picture,使用Glide.with().from(Picture.class).load()
加载图片的话,网络图片等都将通过这个自定义GlideModel加载,所以加载网络图片时请继续使用Glide.with().load()
在写本文章的时候Glide版本还是3.7.x,Glide 4.x 实现同样的功能.将使用另一篇博文.
发现问题
由于Glide相对于picasso有更多的优势,所以最近把项目图片加载库切换到了Glide,本地图片存放在外置SD卡中,使用的是Facebook
的conceal进行加密. 接下来就悲剧了,Glide 只能加载File,byte[],url
,等类型的数据,但是conceal 只能的到解密数据流, 所以必须将InputStream
解析成byte[]
才能被Glide加载.
解析字节流在UI线程(当然你也可以将它放到非UI线程)中执行,每次刷新数据(如选中操作,上下滑动等)都会导致所有图片的数据重新从文件系统(加载-解密-解析-显示)整个过程,导致用户界面卡顿.
代码如下 :
InputStream inputStream = new FileInputStream(file);
// 解密并转化为字节数组,由于这一段在UI线程中所以卡顿很严重,而且每次刷新数据都会重新加载-解密-解析-显示
byte[] data = input2byte(crypto.getCipherInputStream(inputStream, entity));
// 加载字节数组到ImageView
Glide.with(Context).load(data).into(image);
解决问题
怎么样才能做到像Glide加载普通文件一样,在异步的线程里加载图片,而且只需要一次加载解析,其他情况在缓存中查找呢? 查找了相关资料发现Glide 还能够加载ModelType
类型数据,ModelType
类型的加载需要自定义GlideModule
并在清单文件中注册,首先来定义我们的GlideModule
并注册.
- 自定义
GlideModule
,由于我的数据的存储路径是可以直接拿到的,因此我的modelClass类型为Picture类型,也可以定义为其他类型 比如Picture等等
public class CustomGlideModule implements GlideModule {
@Override public void applyOptions(Context context, GlideBuilder builder) {
// 设置别的get/set tag id,以免占用View默认的
ViewTarget.setTagId(R.id.glide_tag_id);
// 图片质量低,够用就行
builder.setDecodeFormat(DecodeFormat.PREFER_RGB_565);
}
@Override public void registerComponents(Context context, Glide glide) {
// 注册我们的ImageLoader,定义modelClass类型为Picture因为加密文件的路径能够直接拿到,也可以定义为其他类型 比如Picture等等
// 第二个参数是我们要将第一个参数数据映射到的类型,这里的意思是(将字符串类型的文件地址,转化为输入流)
// 第三个参数是我们的Loader(加载器)的工厂方法,决定我们使用什么加载器来加载文件
glide.register(Picture.class, InputStream.class, new ImageLoader.Factory());
}
}
- 在清单文件中注册我们自定义GlideModule
<meta-data
android:name="xxx.glide.CustomGlideModule"
android:value="GlideModule" />
- 自定义的加密文件加载类,用于加载加密文件,这样自定义的数据加载类我们不需要管理缓存问题,Glide会为我们处理好.
public class ImageDataFetcher implements DataFetcher<InputStream> {
private volatile boolean mIsCanceled;
private final Picture mFilePath;
private InputStream mInputStream;
public ImageDataFetcher(Picture filePath) {
mFilePath = filePath;
}
/**
* 这个方法是在非UI线程中执行,我们利用此方法来加载我们的加密数据
*
* @param priority
* @throws Exception
*/
@Override public InputStream loadData(Priority priority) throws Exception {
if (mIsCanceled) {
return null;
}
mInputStream = fetchStream(mFilePath.getFileName());
return mInputStream;
}
/**
* 返回解密后的数据流
*
* @param file 文件名
* @return inputStream
*/
private InputStream fetchStream(String file) {
InputStream inputStream = new FileInputStream(new File(file));
// 返回解密数据流
return ConcealUtil.getCipherInputStream(inputStream);
}
/**
* 处理完成之后清理工作
*/
@Override public void cleanup() {
if (mInputStream != null) {
try {
mInputStream.close();
} catch (IOException e) {
Log.e("Glide", "Glide", e);
} finally {
mInputStream = null;
}
}
}
/**
* 该文件的唯一ID
*/
@Override public String getId() {
return mFilePath.getFileName();
}
/**
* 在UI线程中调用,取消加载任务
*/
@Override public void cancel() {
mIsCanceled = true;
}
}
- 图片加载器,重载getResourceFetcher方法,返回我们刚刚定义好的图片加载类.
public class ImageLoader implements ModelLoader<Picture, InputStream> {
public ImageLoader() {
}
@Override
public DataFetcher<InputStream> getResourceFetcher(Picture model, int width, int height) {
return new ImageDataFetcher(model);
}
/**
* ModelLoader工厂,在向Glide注册自定义ModelLoader时使用到
*/
public static class Factory implements ModelLoaderFactory<Picture, InputStream> {
@Override
public ModelLoader<Picture, InputStream> build(Context context, GenericLoaderFactory factories) {
// 返回ImageLoader对象
return new ImageLoader();
}
@Override public void teardown() {
}
}
}
- 最后我们可以这样使用了
Glide.with(mActivity)
.from(Picture.class) // 设置数据源类型为我们的ImageFid
.fitCenter()
.diskCacheStrategy(DiskCacheStrategy.RESULT) // 设置本地缓存,缓存源文件和目标图像
.placeholder(R.drawable.ic_default_image)
.load(pictures.get(position).getFilePath())
.into(photoView);
- 祭出我们的Util代码
public class ConcealUtil {
private static Crypto crypto = null;
private static Entity entity = null;
/**
* 初始化
*
* @param context context
* @param e 密码
*/
public static void init(Context context, String e) {
entity = Entity.create(e);
crypto = new Crypto(new SharedPrefsBackedKeyChain(context, CryptoConfig.KEY_256),
new SystemNativeCryptoLibrary(), CryptoConfig.KEY_256);
if (!crypto.isAvailable()) {
destroy();
}
}
public static void destroy() {
crypto = null;
entity = null;
}
private static void check() {
if (crypto == null || entity == null) {
throw new RuntimeException("请初始化.....");
}
}
public static OutputStream getCipherOutputStream(File file) {
if (file.exists()) return null;
try {
OutputStream fileStream = new FileOutputStream(file);
return getCipherOutputStream(fileStream);
} catch (IOException e) {
Timber.e(e);
}
return null;
}
public static OutputStream getCipherOutputStream(OutputStream fileStream) {
check();
try {
return crypto.getCipherOutputStream(fileStream, entity);
} catch (IOException e) {
Timber.e(e);
} catch (CryptoInitializationException e) {
Timber.e(e);
} catch (KeyChainException e) {
Timber.e(e);
}
return null;
}
public static InputStream getCipherInputStream(String file) {
return getCipherInputStream(new File(file));
}
public static InputStream getCipherInputStream(File file) {
check();
if (!file.exists()) return null;
try {
InputStream inputStream = new FileInputStream(file);
return crypto.getCipherInputStream(inputStream, entity);
} catch (FileNotFoundException e) {
Timber.e(e);
} catch (KeyChainException e) {
Timber.e(e);
} catch (CryptoInitializationException e) {
Timber.e(e);
} catch (IOException e) {
Timber.e(e);
}
return null;
}
/**
* 保存字节流
*
* @param data 数据
*/
public static void saveFile(byte data[], String path) {
String fileName = System.currentTimeMillis() + ".jpg";
File image = new File(path, fileName);
try {
OutputStream outStream = getCipherOutputStream(image);
if (outStream != null) {
outStream.write(data);
outStream.flush();
outStream.close();
}
} catch (IOException e) {
Timber.e(e);
}
}
}
注意缓存策略如果对数据保密要求严格的话请设置为NONE
别忘了引入响应的库文件
dependencies {
...
compile 'com.facebook.conceal:conceal:1.1.2@aar'
compile 'com.github.bumptech.glide:glide:+'
}