Code Monkey home page Code Monkey logo

kluban's Introduction

本项目是基于Luban算法,重构后实现的图片压缩框架

KLuban 使用Kotlin + 协程 + Flow(并行任务) + LiveData(监听回调) + Glide图片识别和内存优化 + 邻近(鲁班)、双线性采样图片算法压缩框架,欢迎改进 fork 和 star

集成和使用

Step 1.Add it in your root build.gradle at the end of repositories:

allprojects {
   repositories {
   	...
   	maven { url 'https://jitpack.io' }
   }
}

Step 2.Add the dependency

dependencies {
  implementation 'com.github.forJrking:KLuban:1.1.0'
}

Step 3.Api:

Luban.with(LifecycleOwner)               //(可选)Lifecycle,可以不填写内部使用ProcessLifecycleOwner
        .load(uri)                       //支持 File,Uri,InputStream,String,Bitmap 和以上数据数组和集合
        .setOutPutDir(path)              //(可选)输出目录文件夹
        .concurrent(true)                //(可选)多文件压缩时是否并行,内部优化线程并行数量防止OOM
        .useDownSample(true)             //(可选)压缩算法 true采用邻近采样,否则使用双线性采样(纯文字图片效果绝佳)
        .format(Bitmap.CompressFormat.PNG)//(可选)压缩后输出文件格式 支持 JPG,PNG,WEBP
        .ignoreBy(200)                   //(可选)期望大小,大小和图片呈现质量不能均衡所以压缩后不一定小于此值,
        .quality(95)                     //(可选)质量压缩系数  0-100
        .rename { "pic$it" }             //(可选)文件重命名
        .filter { it!=null }             //(可选)过滤器
        .compressObserver {
            onSuccess = { }
            onStart = {}
            onCompletion = {}
            onError = { e, _ -> }
        }.launch()

原框架问题分析和技术预估

Luban是基于Android原生API的图片压缩框架,主打特点是近乎微信的图像采样压缩算法。由于技术迭代,已经不能满足产品需求。下面为核心压缩实现,列出鲁班存在的问题:

File compress() throws IOException {
    BitmapFactory.Options options = new BitmapFactory.Options();
    //计算邻近采样主要算法
    options.inSampleSize = computeSize();
    //解码 主要OOM发生在此处
    Bitmap tagBitmap = BitmapFactory.decodeStream(srcImg.open(), null, options);
    ByteArrayOutputStream stream = new ByteArrayOutputStream();
    //判断为JPEG格式进行角度
    if (Checker.SINGLE.isJPG(srcImg.open())) {
      tagBitmap = rotatingImage(tagBitmap, Checker.SINGLE.getOrientation(srcImg.open()));
    }
    //根据传递的focusAlpha来处理输出图片格式  质量系数为60
    tagBitmap.compress(focusAlpha ? Bitmap.CompressFormat.PNG : Bitmap.CompressFormat.JPEG, 60, stream);
    tagBitmap.recycle();
	... 写入文件
    return tagImg;
 }
  • 解码前没有对内存做出预判
  • 质量压缩写死 60
  • 没有提供图片输出格式选择
  • JPEG图像没有alpha层,经过压缩后为应该使用JPG格式更加节省内存
  • 只提供邻近采样算法、双线性采样在某些场景更加合适(纯文字图像)
  • 不支持多文件合理并行压缩,输出顺序和压缩顺序不能保证一致
  • 检测文件格式和图像的角度多次重复创建InputStream,增加不必要开销,增加OOM风险
  • 可能出现内存泄漏,需要自己合理处理生命周期

改造技术分析

  • 针对质量压缩时候,需要外部传入质量系数
  • 解码前利用获取的图片宽高对内存占用做出计算,超出内存的使用RGB-565尝试解码
  • 压缩前获取图片真实格式,对输出文件智能判断,例如是否包含 alpha、输出格式
  • 参考Glide对字节数组的复用,以及InputStream的mark()reset()来优化重复打开开销
  • 利用LiveData来实现监听,自动注销监听。
  • 利用协程来实现异步压缩和并行压缩任务,可以在合适时机取消携程来终止任务

源码分析和优化

借鉴Glide图片识别

不能准确识别文件是什么格式的图像文件,解码时候指定不合适的格式会浪费内存,而且输出时候可能导致透明层丢失等问题。来看看Glide怎么做。当我们修改图片后缀或者没有后缀,Glide依旧可以正常解码显示图像。它是怎么做到的,主要依靠ImageHeaderParserUtils这个类:

public final class ImageHeaderParserUtils {
	... //通过ImageHeaderParser获取ImageType和图片角度
  public static ImageType getType(List<ImageHeaderParser> parsers,
     InputStream is,ArrayPool byteArrayPool)
   public static int getOrientation(List<ImageHeaderParser> parsers,
     InputStream is,final ArrayPool byteArrayPool)
    ...
}
//接口
interface ImgHeaderParser {
		//获取图片类型
    fun getType(input: InputStream): ImageType
		//获取图片原始方向
    fun getOrientation(input: InputStream): Int
}
//实现类//内部通过InputStream 来读取字节来判断文件格式
DefaultImageHeaderParserExifInterfaceImageHeaderParser

做优化只需分析下调用链,拷贝需要的类即可。(由于源码较多,容易犯困,这里只简单说明功能和改造思路,有兴趣的自己阅读。)

//ImgHeaderParser解析用到InputStream,glie中实际实现是 RecyclableBufferedInputStream ,主要作用包装InputStream为其实现字节数组复用
//以及支持 mark()\reset() 作用就是在流中做标记可以重复使用流对象降低开销,这个在后面内存优化中我们再说

//简单改造下 suffix:图片的后缀,hasAlpha:图片是否包含透明层,format:输出时候支持的格式
enum class ImageType(val suffix: String, val hasAlpha: Boolean, val format: Bitmap.CompressFormat) {
    GIF("gif", true, Bitmap.CompressFormat.PNG),
    JPEG("jpg", false, Bitmap.CompressFormat.JPEG),
    RAW("raw", false, Bitmap.CompressFormat.JPEG),
    /* PNG type with alpha.  */
    PNG_A("png", true, Bitmap.CompressFormat.PNG),
    /* PNG type without alpha.  */
    PNG("png", false, Bitmap.CompressFormat.PNG),
    /* WebP type with alpha.  */
    WEBP_A("webp", true, Bitmap.CompressFormat.WEBP),
    /* WebP type without alpha.  */
    WEBP("webp", false, Bitmap.CompressFormat.WEBP),
    /* Unrecognized type.  */
    UNKNOWN("jpeg", false, Bitmap.CompressFormat.JPEG);
}

内存和性能优化

  1. 内存占用优化

    图片处理中内存占用主要分2个部分,图片解码后Bitmap占用的内存空间,解码过程中产生字节数组。

    图像较好的展示效果和内存占用不能同时拥有,我们可以先处理解码过程中的资源开销,因为Glide已经实现这个思路和技术,我们套用即可。

    • 字节数组池化

      在解码过程中创建大量的Byte[],我们知道Glide为了内存和性能做出很多优化,对Byte[]做了池化,只需要调用如下

      val byteArrayPool = com.bumptech.glide.Glide.get(context).arrayPool
      byteArrayPool.get(bufferSize, ByteArray::class.java)

      但是由于有些项目可能没有引入Glide,为了做出兼容,一般我们会拷贝代码来使用。这样显然不合适,我们这里采用反射检测的方式来使用Glide已经实现的功能。

      1. 首先在我们的lib中使用compileOnly ("com.github.bumptech.glide:glide:4.11.0@aar")引入用于编译可以通过
      2. 紧接着实现一个工具类,获取和销毁字节数组,注意在调用Glide.get(Checker.context).arrayPool必须使用显示指定全包名的方式,不然项目在类加载ArrayProvide会加载其导包的类,如果没有使用Glide就会抛出类加载异常。最终实现如下:
      object ArrayProvide {
          private val hasGlide: Boolean by lazy {
              try {
                  //检查是否引入glide
                  Class.forName("com.bumptech.glide.Glide")
                  true
              } catch (e: Exception) {
                  false
              }
          }
      
          @JvmStatic
          fun get(bufferSize: Int): ByteArray = if (hasGlide) {
              //反射判断是否包含glide
              val byteArrayPool = com.bumptech.glide.Glide.get(Checker.context).arrayPool
              byteArrayPool.get(bufferSize, ByteArray::class.java)
          } else {
              ByteArray(bufferSize)
          }
      
          @JvmStatic
          fun put(buf: ByteArray) {
              if (hasGlide && buf.isNotEmpty()) {
                  //反射判断是否包含glide
                  val byteArrayPool = com.bumptech.glide.Glide.get(Checker.context).arrayPool
                  byteArrayPool.put(buf)
              }
          }
      }
      1. 替换所有 new byte[]的使用的地方,后续项目中有其他需要优化字节数组获取的地方也可以使用这个类。
    • 解码过程中内存预判

      2.3-7.1之间,Bitmap的像素存储在Dalvik的Java堆上,利用图片解码前可以获取真实宽高和图片的位图配置,计算JVM内存占用,做出代码执行结果预判。如果内存不足就终止。

      //判断图片解码位图配置  内存不足就不进行压缩,抛出异常捕获而不是让其OOM程序崩溃
      val isAlpha = compressConfig == Bitmap.Config.ARGB_8888
      if (!hasEnoughMemory(width / options.inSampleSize, height / options.inSampleSize, isAlpha)) {
         //TODO 8.0一下内存不足使用降级策略
       if (!isAlpha || !hasEnoughMemory(width / options.inSampleSize, height / options.inSampleSize, false)) {
           throw IOException("image memory is too large")
       } else {
           Checker.logger("memory warring 降低位图像素")
         	//减低像素 减低内存
           options.inPreferredConfig = Bitmap.Config.RGB_565
       }
      }
  2. InputStream优化

    鲁班在获取图片格式和图片解码前获取必要的图片宽高,以及获取图片原始角度都使用InputStreamProvider.open()这个方法

    public abstract class InputStreamAdapter implements InputStreamProvider {
      private InputStream inputStream;
      public InputStream open() throws IOException {
        ...
        inputStream = openInternal();
        return inputStream;
      }....
      public abstract InputStream openInternal() throws IOException;
    }

    查看Luban.java中的具体实现

    class Luban{
      ...
    	public Builder load(final String string) {
    	  mStreamProviders.add(new InputStreamAdapter() {
    	    @Override
    	    public InputStream openInternal() throws IOException {
            //每次都重新创建流对象
    	      return new FileInputStream(string);
    	    }....
    }

    这里每次都使用新的流对象,资源开销较大。再看Glide使用了RecyclableBufferedInputStream,内部对使用InputStream的地方进行包装,然后调用mark()reset()来优化重复打开的开销。我们copy源码BufferedInputStreamWrap改造下:

    abstract class InputStreamAdapter<T> : InputStreamProvider<T> {
    		//BufferedInputStreamWrap 来自Glide内部对字节数组复用和 mark\reset做了优化 字节拷贝
        private lateinit var inputStream: BufferedInputStreamWrap
    
        @Throws(IOException::class)
        abstract fun openInternal(): InputStream
    
        @Throws(IOException::class)
        override fun rewindAndGet(): InputStream {
            if (::inputStream.isInitialized) {
                inputStream.reset()
            } else {
                inputStream = BufferedInputStreamWrap(openInternal())
                inputStream.mark(MARK_READ_LIMIT)
            }
            return inputStream
        }
    
        override fun close() {
            if (::inputStream.isInitialized) {
                try {
                    inputStream.close()
                } catch (ignore: IOException) {
                    ignore.printStackTrace()
                }
            }
        }
    }

Flow使用和自定义线程调度控制并发任务数量

  1. 首先我们把压缩图片的的方法添加 suspend 声明为挂起函数
   suspend fun compress(): File = withContext(customerDispatcher) {
      //解析和压缩
      return@withContext file
   }
  1. Flow的协程选择

    由于LiveData需要使用LifecycleOwner,这里使用flow的协程为LifecycleOwner.lifecycleScope,由于协程几个线程调度,在并行执行图片压缩时候,一旦图片过多同时执行解码的图片数量不可控,就会导致内存占用瞬间增加极可能导致OOM。这里我们需要自定义协程线程调度。

  2. 自定义线程调度

    //可以使用协程的扩展方法 .asCoroutineDispatcher()
    例如:Executors.newFixedThreadPool(2).asCoroutineDispatcher()

    当然这样还不够好,我使用了自定义线程池方式这样可以在不同版本的手机上使用不同策略,而且提供自定义线程名称,在线上可以用来方便定位异常业务

    companion object {
        //主要作用用于并行执行时候可以限制执行任务个数 防止OOM
        internal val supportDispatcher: ExecutorCoroutineDispatcher
        init {
            //Android O之后Bitmap内存放在native  https://www.jianshu.com/p/d5714e8987f3
            val corePoolSize = when {
                Build.VERSION.SDK_INT >= Build.VERSION_CODES.O -> {
                    (Runtime.getRuntime().availableProcessors() - 1).coerceAtLeast(1)
                }
                Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> { 2 }
                else -> { 1 }
            }
            val threadPoolExecutor = ThreadPoolExecutor(corePoolSize, corePoolSize,
                    5L, TimeUnit.SECONDS, LinkedBlockingQueue<Runnable>(), CompressThreadFactory())
            // DES:预创建线程 threadPoolExecutor.prestartAllCoreThreads()
            // DES:让核心线程也可以回收
            threadPoolExecutor.allowCoreThreadTimeOut(true)
            // DES:转换为协程调度器
            supportDispatcher = threadPoolExecutor.asCoroutineDispatcher()
        }
    }
  3. Flow并行2种方式

    //控制同时在此自定义协程调度内执行任务数量2个
    val customerDispatcher = Executors.newFixedThreadPool(2).asCoroutineDispatcher()
    suspend fun compressV(int: Int): String = withContext(customerDispatcher) {
       //模拟压缩文件
       println("compress 开始:$int \tthread:${Thread.currentThread().name}")
       Thread.sleep(300)
       val toString = int.toString() + "R"
       println("compress 结束:${toString} \tthread:${Thread.currentThread().name}")
       return@withContext toString
    }
  • flatMapMerge操作符

    @Test
     fun testFlowFlat() = runBlocking<Unit> {
      val time = measureTimeMillis {
          listOf(1, 2, 3).asFlow().flatMapMerge {
              flow {
                  println("emit: $it  t:${Thread.currentThread().name}")
                  delay(500)
                  emit(it)
              }.flowOn(customerDispatcher)
          }.onStart {
              println("onStart: t:${Thread.currentThread().name}")
          }.onCompletion {
              println("onCompletion: $it  t:${Thread.currentThread().name}")
          }.catch {
              println("catch: $it  t:${Thread.currentThread().name}")
          }.collect {
              println("success: $it  t:${Thread.currentThread().name}")
          }
      }
      println("time: $time")
     }
     print:
     onStart: t:main @coroutine#1
     emit: 2  t:pool-2-thread-2 @coroutine#7
     emit: 1  t:pool-2-thread-1 @coroutine#6
     emit: 3  t:pool-2-thread-1 @coroutine#8
     success: 2  t:main @coroutine#1
     success: 1  t:main @coroutine#1
     success: 3  t:main @coroutine#1
     onCompletion: null  t:main @coroutine#1
     time: 561
     //结果确实实现了并行,但是 collect 中的结果因为并行不确定性收集结果不是原有顺序
  • map + async + await()

    @Test
     fun testBinFa() = runBlocking {
      val time = measureTimeMillis {
          listOf(1, 2, 3).asFlow().map { i ->
              async { compressV(i) }
          }.buffer().flowOn(Dispatchers.Unconfined).map {
              it.await()
          }.onStart {
              println("onStart: t:${Thread.currentThread().name}")
          }.onCompletion {
              println("onCompletion: $it  t:${Thread.currentThread().name}")
          }.catch {
              println("catch: $it  t:${Thread.currentThread().name}")
          }.collect {
              println("success: $it  t:${Thread.currentThread().name}")
          }
      }
      println("Collected in $time ms")
    }
    print:
    onStart: t:main @coroutine#1
    compress 开始:1 	thread:pool-1-thread-1 @coroutine#3
    compress 开始:2 	thread:pool-1-thread-2 @coroutine#4
    compress 结束:2R 	thread:pool-1-thread-2 @coroutine#4
    compress 结束:1R 	thread:pool-1-thread-1 @coroutine#3
    compress 开始:3 	thread:pool-1-thread-2 @coroutine#5
    success: 1R  t:main @coroutine#1
    success: 2R  t:main @coroutine#1
    compress 结束:3R 	thread:pool-1-thread-2 @coroutine#5
    success: 3R  t:main @coroutine#1
    onCompletion: null  t:main @coroutine#1
    Collected in 678 ms
    //success结果显示 可以并行且有序

图片压缩算法

  • 邻近采样 使用鲁班算法不多介绍

    优点不用加载Bitmap进入内存,用于压缩拍照图片较好。缺点在某些场景压缩效果会丢失像素细节。

  • 双线性采样

    matrix.setScale(scale, scale)
    Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)

    优点对于纯文字类图片压缩,显示效果优于邻近采样。缺点先加载进入内存,如果内存占用过大容易OOM

    图片压缩算法优劣:QQ音乐技术团队-Android中图片压缩分析

压缩效果请查看Luban

kluban's People

Contributors

fingdo avatar forjrking avatar txca avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

kluban's Issues

heic图片压缩后旋转

heic图片查看exif是有数据的,我看了源码的实现,是通过glide的一部分核心获取角度的,由于是heic,格式不同,所以取到的ImageType是UNKNOWN,所以角度获取到的是未知。但是glide显示却是正常的,KLuban直接拿了这个数据就有问题

编译失败, 2 files found with path 'META-INF/library_release.kotlin_module'.

2 files found with path 'META-INF/library_release.kotlin_module'.
Adding a packagingOptions block may help, please refer to
https://google.github.io/android-gradle-dsl/current/com.android.build.gradle.internal.dsl.PackagingOptions.html
for more information

  • 错误原因

implementation的另一个model也使用了 model 名称为library,且没有自定义命名。

  • 解决办法:

model build.gradle

...
    compileOptions {
        kotlinOptions.freeCompilerArgs += ['-module-name', "com.forjrking.lubankt.library"]
    }
...

当上传某些图片的时候会出现崩溃

设备信息:小米 MIX 2S MIUI12开发版 20.9.4
Android 10

报错信息:
com.forjrking.lubankt.io.BufferedInputStreamWrap$InvalidMarkException: Mark has been invalidated, pos: 2497492 markLimit: 5242880
at com.forjrking.lubankt.io.BufferedInputStreamWrap.reset(BufferedInputStreamWrap.java:318)
at com.forjrking.lubankt.Checker$getOrientation$reader$1.getOrientation(Checker.kt:117)
at com.forjrking.lubankt.Checker.getOrientationInternal(Checker.kt:138)
at com.forjrking.lubankt.Checker.getOrientation(Checker.kt:121)
at com.forjrking.lubankt.CompressEngine.compress(CompressEngine.kt:48)
at com.forjrking.lubankt.Builder$compress$2.invokeSuspend(Luban.kt:270)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:56)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
at java.lang.Thread.run(Thread.java:919)
at com.forjrking.lubankt.CompressThreadFactory$newThread$thread$1.run(Luban.kt:385)

报错图片:
微信图片_20201208153531

16K图片压缩问题

我在测试16K的图片压缩时发现,压缩虽然能够完成,但是图片的画面完全丢失

请教compressObserver对应的java写法

调用方法
Luban.with() .load(bitmap) .ignoreBy(500) .format(Bitmap.CompressFormat.WEBP) .compressObserver { onStart = { //Log.d(TAG, "onStart: ") } onCompletion = { Log.d(TAG, "onCompletion") } onSuccess = { Toast.makeText(this@MainActivity, "file" + it.name, Toast.LENGTH_LONG).show() mIv!!.setImageURI(Uri.fromFile(it)) } onError = { a, b -> Log.e(TAG, a.toString()) } }.launch()
中的compressObserver实现不会写,请教该怎么写 - -、
我目前是这么写的
Luban.Companion.with().load("").compressObserver(new Function1<CompressResult<String, File>, Unit>() { @Override public Unit invoke(CompressResult<String, File> stringFileCompressResult) { return null; } }).launch();

压缩超长图片,特别耗时的问题

作者您好,首先感谢开源并持续维护这么好用的压缩库。
我在使用的过程中发现,压缩正常大小图片(50005000左右)速度正常,大概耗时不到1秒钟。
但如果是超长或超宽图片,会非常耗时。在荣耀30Pro上,1080
35120(11.58MB)的图片压缩,耗时6秒钟。
希望作者可以优化下。
期待回复,感谢!

大神 java版本调用 如何在compressObserver中获取压缩后的File? 对kotlin不太熟悉 可以出个java版本的demo吗

Luban.Companion.with(ProcessLifecycleOwner.get())
.load(bitmap)
.setOutPutDir(PathUtils.getExternalAppPicturesPath())
.useDownSample(true) //(可选)压缩算法 true采用邻近采样,否则使用双线性采样(纯文字图片效果绝佳)
.format(Bitmap.CompressFormat.JPEG)//(可选)压缩后输出文件格式 支持 JPG,PNG,WEBP
.ignoreBy(250) //(可选)期望大小,大小和图片呈现质量不能均衡所以压缩后不一定小于此值,
.quality(100) //(可选)质量压缩系数 0-100
.compressObserver(new Function1<CompressResult<Bitmap, File>, Unit>() {
@OverRide
public Unit invoke(CompressResult<Bitmap, File> bitmapFileCompressResult) {
bitmapFileCompressResult.setOnSuccess(new Function1<File, Unit>() {
@OverRide
public Unit invoke(File file) {
return null;
}
});
return null;
}
})
.launch();

在项目中使用遇到一个问题

您好,我在项目中引用了一个图片选择库,并且剪裁之后使用您的库进行压缩,然而打印日志发现了一个问题。剪裁出的圆形图片压缩后,file依然是原文件,使用其他剪裁出来的图片可以正常压缩,想请教一下是什么原因。
这是我用的图片库地址:https://github.com/yangpeixing/YImagePicker

压缩图片抛InvalidMarkException

型号:魅族 PRO6
版本号:Flyme 7.3.0.0A
android版本 7.1.1

//不加载进内存
options.inJustDecodeBounds = true
 //不加载进内存解析一次 获取宽高
BitmapFactory.decodeStream(srcStream.rewindAndGet(), null, options)

在这之后,markPos变成-1。想不通,为啥这个decodeStream会读取整个文件流导致超出marklimit。
这个文件流是一个大图片

压缩完后图片花屏错位

image
Luban.with(ProcessLifecycleOwner.get()) .load(filePath) .concurrent(true) .useDownSample(true) .ignoreBy(50*1000) .quality(80) .get()
1.0.7版本,在部分机型(oppo)偶现,图片右下角白色马赛克为水印,非压缩问题。

请问有非androidX的版本吗?

如题,因为在为公司写一个标准化的SDK,该SDK会对接多个APP平台,所以有些项目还未把androidX做迁移,所以是否可以提供一个非androidX的版本呢?

能解答一下,这不部分的计算是根据啥么?

fun calculateQuality(context: Context): Int {
        val dm = DisplayMetrics()
        (context.getSystemService(Context.WINDOW_SERVICE) as WindowManager)
            .defaultDisplay.getMetrics(dm)
        val density = dm.density
        return if (density > 3f) {
            DEFAULT_LOW_QUALITY
        } else if (density > 2.5f && density <= 3f) {
            DEFAULT_QUALITY
        } else if (density > 2f && density <= 2.5f) {
            DEFAULT_HEIGHT_QUALITY
        } else if (density > 1.5f && density <= 2f) {
            DEFAULT_X_HEIGHT_QUALITY
        } else {
            DEFAULT_XX_HEIGHT_QUALITY
        }
    }

按我的理解,应该越低dpi的手机才应该压缩的越狠吧

rename 返回的是整个路径不是文件名

源代码:

 //组合一个名字给输出文件
            val cacheFile = "$mOutPutDir/${System.nanoTime()}.${type.suffix}"
            //重命名接口
            val outFile = if (mRenamePredicate != null) {
                File(mRenamePredicate!!.invoke(cacheFile))
            } else {
                File(cacheFile)
            }

而且有了 setOutPutDir 设置输出目录
rename这里应该就是只修改文件名

另:
我觉得压缩后文件名应该使用原文件名,这样比较合理;
上传后展示上传的文件名,用时间戳是区分不出来哪个对哪个的。

上传部分图片时会抛出异常:InvalidMarkException: Mark has been invalidated, pos: 1423393 markLimit: 5242880

并不是所有图片都会抛异常,只有小部分图片会,但不知道是什么原因,希望可以排查一下
W/System.err: com.forjrking.lubankt.io.BufferedInputStreamWrap$InvalidMarkException: Mark has been invalidated, pos: 1423393 markLimit: 5242880
W/System.err: at com.forjrking.lubankt.io.BufferedInputStreamWrap.reset(BufferedInputStreamWrap.java:318)
W/System.err: at com.forjrking.lubankt.Checker$getOrientation$reader$1.getOrientation(Checker.kt:137)
W/System.err: at com.forjrking.lubankt.Checker.getOrientationInternal(Checker.kt:147)
W/System.err: at com.forjrking.lubankt.Checker.getOrientation(Checker.kt:141)
W/System.err: at com.forjrking.lubankt.Checker.getRotateDegree(Checker.kt:167)
W/System.err: at com.forjrking.lubankt.CompressEngine.compress(CompressEngine.kt:48)
W/System.err: at com.forjrking.lubankt.AbstractFileBuilder$compress$2.invokeSuspend(Luban.kt:294)
W/System.err: at com.forjrking.lubankt.AbstractFileBuilder$compress$2.invoke(Unknown Source:10)
W/System.err: at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:91)
W/System.err: at kotlinx.coroutines.BuildersKt__Builders_commonKt.withContext(Builders.common.kt:165)
W/System.err: at kotlinx.coroutines.BuildersKt.withContext(Unknown Source:1)
W/System.err: at com.forjrking.lubankt.AbstractFileBuilder.compress(Luban.kt:272)
W/System.err: at com.forjrking.lubankt.SingleRequestBuild$asyncRun$1$1.invokeSuspend(Luban.kt:320)
W/System.err: at com.forjrking.lubankt.SingleRequestBuild$asyncRun$1$1.invoke(Unknown Source:10)
W/System.err: at kotlinx.coroutines.flow.SafeFlow.collectSafely(Builders.kt:61)
W/System.err: at kotlinx.coroutines.flow.AbstractFlow.collect(Flow.kt:212)
W/System.err: at kotlinx.coroutines.flow.internal.ChannelFlowOperatorImpl.flowCollect(ChannelFlow.kt:207)
W/System.err: at kotlinx.coroutines.flow.internal.ChannelFlowOperator.collectTo$suspendImpl(ChannelFlow.kt:169)
W/System.err: at kotlinx.coroutines.flow.internal.ChannelFlowOperator.collectTo(Unknown Source:0)
W/System.err: at kotlinx.coroutines.flow.internal.ChannelFlow$collectToFun$1.invokeSuspend(ChannelFlow.kt:60)
W/System.err: at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
W/System.err: at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
W/System.err: at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
W/System.err: at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
W/System.err: at java.lang.Thread.run(Thread.java:923)

图片地址:
谷歌网盘

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.