很久以前对于图片的压缩一直就使用Luban这个第三方库,主要是它提供的api比较方便,支持同步和异步压缩,也可以同时压缩多张图片,比较好用,效果也比较好。它的内部会根据图片的宽高比例,选择一个合适的压缩算法,对图片进行采样率压缩
和质量压缩
,效果接近微信朋友圈压缩的效果。最近新项目中使用到了,就顺便来学习下它的内部实现原理。
一. 简单使用
源码版本 1.1.8
1 . 同步压缩
Luban.with(context).load(list).get()
2 . 异步压缩
Luban.with(context)
.load(photos)
.setCompressListener(new OnCompressListener() {
@Override
public void onStart() { }
@Override
public void onSuccess(File file) {
Log.i(TAG, file.getAbsolutePath());
showResult(originPhotos, file);
}
@Override
public void onError(Throwable e) { }
})
.launch();
Luban里面的配置都是可选的,这样对于使用者的可定制性和可扩展性就比较好了。
二.同步压缩
对于同步的使用一般都是在非UI线程中进行,可以直接得到压缩后的文件对象。来看下它的实现过程,首先调用Luban.with(context)
,它会在内部会创建一个Builder
对象
public static Builder with(Context context) {
//很明显就可以知道它内部是使用build构建者模式来完成链式调用的
return new Builder(context);
}
返回这个Builder对象,接着会调用load方法设置文件路径,它有几个重载方法
选择参数为List的重载方法进行分析
public <T> Builder load(List<T> list) {
for (T src : list) {
if (src instanceof String) {
load((String) src);
} else if (src instanceof File) {
load((File) src);
} else if (src instanceof Uri) {
load((Uri) src);
} else {
throw new IllegalArgumentException("Incoming data type exception, it must be String, File, Uri or Bitmap");
}
}
return this;
}
它内部会去遍历这个List,然后根据不同的type去load不同的url,这里以File的重载进行分析
public Builder load(final File file) {
mStreamProviders.add(new InputStreamAdapter() {
@Override
public InputStream openInternal() throws IOException {
return new FileInputStream(file);
}
@Override
public String getPath() {
return file.getAbsolutePath();
}
});
return this;
}
可以看到内部会创建一个InputStreamAdapter
对象,它会被添加到mStreamProviders
中,mStreamProviders
是一个List,类型为InputStreamProvider
,而InputStreamAdapter
是InputStreamProvider
接口的子类。看下它们内部的实现(很简单)
/**
* 通过此接口获取输入流,以兼容文件、FileProvider方式获取到的图片
* <p>
* Get the input stream through this interface, and obtain the picture using compatible files and FileProvider
*/
public interface InputStreamProvider {
InputStream open() throws IOException;
void close();
String getPath();
}
/**
* Automatically close the previous InputStream when opening a new InputStream,
* and finally need to manually call {@link #close()} to release the resource.
*/
public abstract class InputStreamAdapter implements InputStreamProvider {
private InputStream inputStream;
@Override
public InputStream open() throws IOException {
//关闭之前的连接
close();
//获取一个新的连接
inputStream = openInternal();
return inputStream;
}
public abstract InputStream openInternal() throws IOException;
@Override
public void close() {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException ignore) {
}finally {
inputStream = null;
}
}
}
}
其实从注释就可以看出这两个类的作用,InputStreamProvider
是为了向内部提供输入流,而InputStreamAdapter
是为了自动关闭之前没有关闭的输入流,这样可以避免内存泄漏,但是最后一次的输入流需要手动调用,这个后面会说。一般来说使用后面一个向内部提供输入流。
接着回到创建InputStreamAdapter
的地方,它有两个方法需要实现,其中openInternal
的返回值是一个InputStream
,参数file
会被包装到一个FileInputStream
中作为返回值,另一个方法是getPath
,也就是返回file
的绝对路径。
所以load方法就是将file对象进行包装,放到一个InputStreamProvider
中,然后放到List中供给内部使用。
load
方法分析完之后,接着就是调用get
获取压缩后的文件,它的返回值是一个List,真正的图片压缩过程就在这一步骤,来一步步分析它的实现过程。
public List<File> get() throws IOException {
return build().get(context);
}
这个build()方法内部会创建一个Luban
类,真正的压缩实现就转移到了Luban
类中,来看下get(context)
的实现。
private List<File> get(Context context) throws IOException {
List<File> results = new ArrayList<>();
Iterator<InputStreamProvider> iterator = mStreamProviders.iterator();
while (iterator.hasNext()) {
results.add(compress(context, iterator.next()));
iterator.remove();
}
return results;
}
这个方法大致的意思就是去遍历刚刚提到的mStreamProviders
集合,然后去压缩里面提供的输入流,压缩完毕之后就会被添加到result中,作为返回值。
来看下最重要的方法compress
private File compress(Context context, InputStreamProvider path) throws IOException {
try {
return compressReal(context,path);
} finally {
path.close(); //就是刚刚说到最后一次需要手动close的输入流
}
}
//压缩单个文件的实现逻辑
private File compressReal(Context context, InputStreamProvider path) throws IOException {
File result;
File outFile = getImageCacheFile(context, Checker.SINGLE.extSuffix(path));//1
if (mRenameListener != null) {//2
String filename = mRenameListener.rename(path.getPath());
outFile = getImageCustomFile(context, filename);
}
if (mCompressionPredicate != null) {//3
if (mCompressionPredicate.apply(path.getPath())
&& Checker.SINGLE.needCompress(mLeastCompressSize, path.getPath())) {
result = new Engine(path, outFile, focusAlpha).compress();//4
} else {
result = new File(path.getPath());
}
} else {
result = Checker.SINGLE.needCompress(mLeastCompressSize, path.getPath()) ?
new Engine(path, outFile, focusAlpha).compress() :
new File(path.getPath());
}
return result;
}
首先看注释1,就是获取压缩后的图片路径方法
private File getImageCacheFile(Context context, String suffix) {
if (TextUtils.isEmpty(mTargetDir)) {
mTargetDir = getImageCacheDir(context).getAbsolutePath();
}
String cacheBuilder = mTargetDir + "/" +
System.currentTimeMillis() +
(int) (Math.random() * 1000) +
(TextUtils.isEmpty(suffix) ? ".jpg" : suffix);
return new File(cacheBuilder);
}
它内部会根据mTargetDir来判断是使用内部默认的路径或用户自己设置的路径,外部设置路径的方法为setTargetDir
,如果没有设置就使用默认的路径。
接着看注释2,如果设置了mRenameListener,就使用它的名字作为目标压缩文件的名称,可以看到这个listener是用来设置压缩后的文件名
private File getImageCustomFile(Context context, String filename) {
if (TextUtils.isEmpty(mTargetDir)) {
mTargetDir = getImageCacheDir(context).getAbsolutePath();
}
String cacheBuilder = mTargetDir + "/" + filename;
return new File(cacheBuilder);
}
接着看注释3,它用来过滤那些文件不需要压缩,如果不需要压缩的文件就直接返回原图,值得一提的还有Checker.SINGLE.needCompress
也可以判断图片是否需要压缩
boolean needCompress(int leastCompressSize, String path) {
if (leastCompressSize > 0) {
File source = new File(path);
return source.exists() && source.length() > (leastCompressSize << 10);
}
return true;
}
leastCompressSize就是通过ignoreBy设置,它的单位是KB,然后通过leastCompressSize << 10
转化为Byte,当操作设置的leastCompressSize大小小于待压缩文件大小,就判断条件为需要压缩。
接着看注释4,也就是真正压缩过程
File compress() throws IOException {
BitmapFactory.Options options = new BitmapFactory.Options();
//获取inSampleSize的值 computeSize的具体原理就是通过各种比例边界条件来返回不同的
//inSampleSize,通过在微信朋友圈发送近100张不同分辨率图片,对比原图与微信压缩后的
//图片逆向推算出来的压缩算法。关于具体描述实现过程可以看它的DESCRIPTION.md文件
options.inSampleSize = computeSize();
//采样率压缩获取压缩后的Bitmap
Bitmap tagBitmap = BitmapFactory.decodeStream(srcImg.open(), null, options);
//创建一个字节流
ByteArrayOutputStream stream = new ByteArrayOutputStream();
//回正图片方向
if (Checker.SINGLE.isJPG(srcImg.open())) {
tagBitmap = rotatingImage(tagBitmap, Checker.SINGLE.getOrientation(srcImg.open()));
}
//质量压缩,压缩后的结果会存储到字节流stream中,根据是否保留透明通道来选择PNG或是JPEG图片
tagBitmap.compress(focusAlpha ? Bitmap.CompressFormat.PNG : Bitmap.CompressFormat.JPEG, 60, stream);
tagBitmap.recycle();
//写到fos中,写成功就可以返回这个压缩后的文件
FileOutputStream fos = new FileOutputStream(tagImg);
fos.write(stream.toByteArray());
fos.flush();
fos.close();
stream.close();
return tagImg;
}
实现过程注释已经注释得很清楚了,就不多说了,总结来说就是根据一个宽高比例来计算inSampleSize
,然后进行采样率压缩,接着进行质量压缩。
整个同步压缩过程基本流程也就是这样,我们可以在方法调用的过程中通过链式调用设置一些监听,接着看异步压缩的过程。
三.异步压缩
异步压缩和同步压缩调用基本一样,只不过异步调用是通过launch启动异步过程,通过listener来获取回调结果。
public void launch() {
build().launch(context);
}
private void launch(final Context context) {
if (mStreamProviders == null || mStreamProviders.size() == 0 && mCompressListener != null) {
mCompressListener.onError(new NullPointerException("image file cannot be null"));
}
Iterator<InputStreamProvider> iterator = mStreamProviders.iterator();
while (iterator.hasNext()) {
final InputStreamProvider path = iterator.next();
AsyncTask.SERIAL_EXECUTOR.execute(new Runnable() {
@Override
public void run() {
try {
mHandler.sendMessage(mHandler.obtainMessage(MSG_COMPRESS_START));
File result = compress(context, path);
mHandler.sendMessage(mHandler.obtainMessage(MSG_COMPRESS_SUCCESS, result));
} catch (IOException e) {
mHandler.sendMessage(mHandler.obtainMessage(MSG_COMPRESS_ERROR, e));
}
}
});
iterator.remove();
}
}
异步调用是通过线程池来实现的,线程池是直接使用AsyncTask里的SERIAL_EXECUTOR来实现的,每当添加异步任务时候,都会添加到一个队列中,然后排队执行。在这个异步任务中首先发送一个含有MSG_COMPRESS_START的Message,在主线程中回调Listener,然后进行压缩,之后就会发送一个含有MSG_COMPRESS_SUCCESS的Message,并把结果返回。如果出错就会在catch中发送一个MSG_COMPRESS_ERROR的Message,然后回调相应的listener。
异步不一样的就是这些地方,其它的和同步过程一样。
四.总结
对于一个好的框架不仅是可以实现多么牛的效果,而且代码质量也应该好,并且给调用者使用应该简单方便,这个框架其实整体读起来比较简单,但是里面设计的结构却是值得学习的,还是值得一读的。但是感觉有个缺点就是有的业务需要将图片压缩到某个范围却是实现不了,因为它里面是根据宽高比,自动计算了一个采样率,然后进行压缩。有利有弊,还要看具体的业务选择。