美文网首页安卓集中营android杂android成神之路
Android——Luban图片压缩算法学习

Android——Luban图片压缩算法学习

作者: 英勇青铜5 | 来源:发表于2016-10-13 19:36 被阅读7963次

    这个库单独使用感觉相当简单,作者封装的非常好,使用特方便

    • 源码地址以及使用教程:Luban

    本篇使用的代码是在RxJava——基础学习(三),简单实践基础上,添加了图片的点击事件。最近没有再学习RxJava,因为RxJava正处于过渡时期,2.0版本要发布了,修改还蛮大的,就想等2.0发布后,再继续学习RxJava


    1.简单使用 <p>

    使用RecyclerView将图片展示出来

    前两张图,是特意添加的两个比较大的,不同分辨率的图片,第3个图之后的就是手机截屏后的图,分辨率就是手机屏幕的分辨率

    • 第1个图5120 * 2880,大小为5.68M
    • 第2个图3840 * 2400,大小为1.08M
    • 第3个图1080 * 1920,大小为1.19M

    前两个图,不做任何处理,直接使用ImageView展示,在我的坚果手机百分百OOM


    点击每一个图片后,开启一个新的Activity来展示图片。在新的Activity中,使用Luban将图片进行压缩,得到压缩后的图片后,使用ImageView展示出来

    代码:

    private void showPicFileByLuban(@NonNull File file) {
        Luban.get(ShowPicActivity.this)
             .load(file)//目标图片
             .putGear(Luban.THIRD_GEAR)//压缩等级
             .setCompressListener(new OnCompressListener() {
                @Override
                public void onStart() {//开始压缩
                }
    
                @Override
                public void onSuccess(File file) {//压缩成功,拿到压缩的图片,在UI线程
                    Bitmap bitmap = BitmapFactory.decodeFile(file.getAbsolutePath());
                    mToolBar.setSubtitle(bitmap.getWidth() + "*" + bitmap.getHeight() + "-->" + bitmap.getByteCount());
                        iv.setImageBitmap(bitmap);
                    }
    
                    @Override
                    public void onError(Throwable e) {//压缩失败
                    }
                })
            .launch();//开启压缩
    }
    

    代码很简单,压缩后的是一个File,根据需求,对这个File再做处理

    点击图片后

    注意不同分辨率的图片压缩后的宽高

    这个库强大的地方在于针对不同的分辨率图片,压缩比例计算,控制图片文件的大小

    整个Demo的代码上传到了GithubPicStore

    使用很简单,则意味着源码做了大量的优化,设计巧妙,下面学习大神的代码


    2. 尝试学习源码 <p>

    根据使用过程用到的方法来进行学习源码,其中最重要就是关于压缩比例的计算,学习作者的封装的思路和技巧


    2.1 get(Context context)方法 <p>

    public static Luban get(Context context) {
        if (INSTANCE == null) INSTANCE = new Luban(Luban.getPhotoCacheDir(context));
        return INSTANCE;
    }
    

    这个方法用来创建Luban对象,Luban的构造方法是私有的并且需要一个File对象,在get()内,在new的时候,就调用了Luban.getPhotoCacheDir(context),这个方法是用来指定缓存目录的,缓存目录默认为:系统默认缓存文件夹下的luban_disk_cache文件夹

    Luban.getPhotoCacheDir(context)内又调用了getPhotoCacheDir(Context context, String cacheName)方法

    private static File getPhotoCacheDir(Context context, String cacheName) {
            File cacheDir = context.getCacheDir();
            if (cacheDir != null) {
                File result = new File(cacheDir, cacheName);
                if (!result.mkdirs() && (!result.exists() || !result.isDirectory())) {//result文件夹不能创建,或者创建了却不是一个文件夹
                    return null;
                }
                return result;
            }
            if (Log.isLoggable(TAG, Log.ERROR)) {
                Log.e(TAG, "default disk cache dir is null");
            }
            return null;
    }
    

    设置缓存目录的方法


    2.2 load(File file)设置压缩目标图片 <p>

    public Luban load(File file) {
        mFile = file;
        return this;
    }
    

    这个方法倒是比较容易理解,设置过目标图片文件后,又返回了Luban对象本身,这样就可以用方法链了


    2.3 putGear(int gear)设置压缩等级 <p>

     public Luban putGear(int gear) {
        this.gear = gear;
        return this;
    }
    

    有两个压缩等级:1档3档,默认为3档,设置其他的档位是无效的


    2.4 setComressListener()设置压缩进度监听<p>

     public Luban setCompressListener(OnCompressListener listener) {
        compressListener = listener;
        return this;
    }
    

    设置监听,OnCompressListener内部有3个方法

    public interface OnCompressListener {
        //压缩开始前
        void onStart();
        //压缩成功后
        void onSuccess(File file);
        //压缩失败
        void onError(Throwable e);
    }
    

    三个方法都在UI线程,可以直接用来更新UI


    2.5 launch()开启压缩方法 <p>

    这个方法是Luban中的核心方法,内部使用了RxJava,这个方法内的重点是根据压缩档位来进行不同的操作

    public Luban launch() {
            checkNotNull(mFile, "the image file cannot be null, please call .load() before this method!");//用来判断null
    
            if (compressListener != null) compressListener.onStart();
    
            if (gear == Luban.FIRST_GEAR)//1档
                Observable.just(mFile)
                        .map(new Func1<File, File>() {
                            @Override
                            public File call(File file) {
                                return firstCompress(file);
                            }
                        })
                        .subscribeOn(Schedulers.io())
                        .observeOn(AndroidSchedulers.mainThread())
                        .doOnError(new Action1<Throwable>() {
                            @Override
                            public void call(Throwable throwable) {
                                if (compressListener != null) compressListener.onError(throwable);
                            }
                        })
                        .onErrorResumeNext(Observable.<File>empty())
                        .filter(new Func1<File, Boolean>() {
                            @Override
                            public Boolean call(File file) {
                                return file != null;
                            }
                        })
                        .subscribe(new Action1<File>() {
                            @Override
                            public void call(File file) {
                                if (compressListener != null) compressListener.onSuccess(file);
                            }
                        });
            else if (gear == Luban.THIRD_GEAR)//3档
                Observable.just(mFile)
                        .map(new Func1<File, File>() {
                            @Override
                            public File call(File file) {
                                return thirdCompress(file);
                            }
                        })
                        .subscribeOn(Schedulers.io())
                        .observeOn(AndroidSchedulers.mainThread())
                        .doOnError(new Action1<Throwable>() {
                            @Override
                            public void call(Throwable throwable) {
                                if (compressListener != null) compressListener.onError(throwable);
                            }
                        })
                        .onErrorResumeNext(Observable.<File>empty())
                        .filter(new Func1<File, Boolean>() {
                            @Override
                            public Boolean call(File file) {
                                return file != null;
                            }
                        })
                        .subscribe(new Action1<File>() {
                            @Override
                            public void call(File file) {
                                if (compressListener != null) compressListener.onSuccess(file);
                            }
                        });
    
            return this;
        }
    

    这个方法内使用了RxJava,开启一个独立的线程来进行压缩,即使图片很大,也不会阻塞UI线程


    方法开始有一个判null的方法,这个方法单独封装在了一个辅助工具类内

    public static <T> T checkNotNull(T reference, @Nullable Object errorMessage) {
        if (reference == null) {//若null,就抛异常,并把异常提示信息显示出来
            throw new NullPointerException(String.valueOf(errorMessage));
        }
        return reference;
    }
    

    这个方法的重中之重是thirdCompress(file)firstCompress(file),两个方法看懂一个,另一个就比较容易理解了


    2.6 thirdCompress(file)3档压缩 <p>

    设计思路:

    压缩算法思路
    源码:
     private File thirdCompress(@NonNull File file) {
            String thumb = mCacheDir.getAbsolutePath() + "/" + System.currentTimeMillis();//压缩后图片缓存路径
    
            thumb = filename == null || filename.isEmpty() ? thumb : filename;//判null处理
    
            double size;//文件大小 单位为KB
            String filePath = file.getAbsolutePath();//文件的绝对路径
    
            int angle = getImageSpinAngle(filePath);//图片的角度,为了保持所有的图片都能够竖直显示在屏幕
            int width = getImageSize(filePath)[0];//图片的宽
            int height = getImageSize(filePath)[1];//图片的高
            int thumbW = width % 2 == 1 ? width + 1 : width;//临时宽,将宽变作偶数
            int thumbH = height % 2 == 1 ? height + 1 : height;//临时高,将高变作偶数
    
            width = thumbW > thumbH ? thumbH : thumbW;//将小的一边给width,最短边
            height = thumbW > thumbH ? thumbW : thumbH;//将大的一边给height,最长边
    
            double scale = ((double) width / height);//比例,图片短边除以长边为该图片比例
    
            if (scale <= 1 && scale > 0.5625) {//比例在[1,0.5625)间
                //判断最长边是否过界
                if (height < 1664) {//最长边小于1664px
                    if (file.length() / 1024 < 150) return file;//如果文件的大小小于150KB
    
                    size = (width * height) / Math.pow(1664, 2) * 150;//计算文件大小
                    size = size < 60 ? 60 : size;//判断文件大小是否小于60KB
                } else if (height >= 1664 && height < 4990) {//最长边大于1664px小于4990px
                    thumbW = width / 2;//最短边缩小2倍
                    thumbH = height / 2;//最长边缩小2倍
                    size = (thumbW * thumbH) / Math.pow(2495, 2) * 300;//计算文件大小
                    size = size < 60 ? 60 : size;//判断文件大小是否小于60KB
                } else if (height >= 4990 && height < 10240) {//如果最长边大于4990px小于10240px
                    thumbW = width / 4;//最短边缩小2倍
                    thumbH = height / 4;//最长边缩小2倍
                    size = (thumbW * thumbH) / Math.pow(2560, 2) * 300;//计算文件大小
                    size = size < 100 ? 100 : size;判断文件大小是否小于100KB
                } else {//最长边大于10240px
                    int multiple = height / 1280 == 0 ? 1 : height / 1280;//最长边与1280相比的倍数
                    thumbW = width / multiple;//最短边根据倍数压缩
                    thumbH = height / multiple;//最长边根据倍数压缩
                    size = (thumbW * thumbH) / Math.pow(2560, 2) * 300;//计算文件大小
                    size = size < 100 ? 100 : size;//判断文件大小是否小于100KB
                }
            } else if (scale <= 0.5625 && scale > 0.5) {//比例在[0.5625,00.5)区间
                if (height < 1280 && file.length() / 1024 < 200) return file;//最长边小于1280px并且文件大小在200KB内,就返回
    
                int multiple = height / 1280 == 0 ? 1 : height / 1280;//倍数,最长边与1280相比
                thumbW = width / multiple;//最短边根据倍数压缩
                thumbH = height / multiple;//最长边根据倍数压缩
                size = (thumbW * thumbH) / (1440.0 * 2560.0) * 400;//计算文件大小
                size = size < 100 ? 100 : size;//判断文件大小是否小于100KB
            } else {//比例小于0.5
                int multiple = (int) Math.ceil(height / (1280.0 / scale));//最长边乘以比例后与1280相比的结果向上取整
                thumbW = width / multiple;//最短边根据倍数压缩
                thumbH = height / multiple;//最长边根据倍数压缩
                size = ((thumbW * thumbH) / (1280.0 * (1280 / scale))) * 500;//计算文件大小
                size = size < 100 ? 100 : size;//判断文件大小是否小于100KB
            }
            //根据计算结果来进行压缩图片
            return compress(filePath, thumb, thumbW, thumbH, angle, (long) size);
        }
    

    thumbWwidth有区别,width是最短边,而thumbW是压缩目标的宽的大小

    拿到计算的结果后,调用了compress()方法

    注意:比例是短边除以长边


    compress()方法代码:

    private File compress(String largeImagePath, String thumbFilePath, int width, int height, int angle, long size) {
           //根据最终计算的宽高来压缩图片
           Bitmap thbBitmap = compress(largeImagePath, width, height);
          
        //根据拿到的图片角度,使用`Matrix`旋转图片
        //有的手机照片会存在旋转90°的情况
        thbBitmap = rotatingImage(angle, thbBitmap);
         //保存图片在缓存文件中
        return saveImage(thumbFilePath, thbBitmap, size);
    }
    

    compress(largeImagePath, width, height)就是Bitmap的二次采样,将Bitmap的宽高压缩到目标大小


    saveImage()代码:

        /**
         * 保存图片到指定路径
         * Save image with specified size
         *
         * @param filePath the image file save path 储存路径
         * @param bitmap   the image what be save   目标图片
         * @param size     the file size of image   期望大小
         */
        private File saveImage(String filePath, Bitmap bitmap, long size) {
            checkNotNull(bitmap, TAG + "bitmap cannot be null");//判`null`
    
            File result = new File(filePath.substring(0, filePath.lastIndexOf("/")));
    
            if (!result.exists() && !result.mkdirs()) return null;
    
            ByteArrayOutputStream stream = new ByteArrayOutputStream();
            int options = 100;
            bitmap.compress(Bitmap.CompressFormat.JPEG, options, stream);//进行质量压缩,是图片文件的大小达到计算目标的大小
    
            while (stream.toByteArray().length / 1024 > size && options > 6) {//若图片文件的大小大于目标大小,并且质量压缩率大于6
                stream.reset();
                options -= 6;
                bitmap.compress(Bitmap.CompressFormat.JPEG, options, stream);
            }
    
            try {
                FileOutputStream fos = new FileOutputStream(filePath);
                fos.write(stream.toByteArray());
                fos.flush();
                fos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
    
            return new File(filePath);
        }
    

    代码是看完了,可有细节并不明白,比如,比例0.5625,文件大小60KB,100KB,质量压缩率6,这些怎么得来的并不晓得。

    不过,主要是想学习作者封装的思路和设计,细节随着经验增长,再思考了


    3.最后 <p>

    代码也算是看了一遍,大体是懂了。不晓得作者郑梓斌Curzibn这位大神,看到我这种水平的分析他的代码,会不会觉得把他的一些好的设计给曲解了,我哪里考虑的不对,请留言指出啊 :)

    以后要多读别人的代码,多向大神们学习 ,本篇博客完整代码

    本人很菜,有错误请指出

    共勉 :)

    相关文章

      网友评论

      • 吴_旭东:[1, 0.5625) 边界值为:1664 n(n=1), 4990 n(n=2), 1280 * pow(2, n-1)(n≥3);
        请问一下,1664,4990怎么来的,还有n 表示什么~~
      • 白日梦__:压缩单图还可以,但是特别长或者特别宽的图片就非常模糊,多图会oom,还有一个压缩库compressor也是一样。我测试了一下就不用了。 你可以看看用ndk压缩图片,可以达到很好的效果。
        英勇青铜5: @白日梦__ 好的,多谢啦。ndk开发还不会。。
      • 过期的薯条:问题比较大的框架
        英勇青铜5: @过期的薯条 不建议使用
      • i卓:这项目确实如7楼所说问题挺多的,压缩单图还可以,但是多图确实会有几率oom,运用在项目中的话,问题蛮大
        英勇青铜5: @i卓 多谢指出问题不足
      • 1f49e0c77db2:各位大神 这个框架可以压缩网络图片么?
        1f49e0c77db2:@英勇青铜5 好的,3Q
        英勇青铜5: @LmCool 能获取bitmap应该就可以,可以去看看作者的最新的代码,要是打算用于项目中,最好和小组老大商量下
      • 8320a29e3068:1440/2560=1080/1920=720/1280=0.5625
        英勇青铜5: @8320a29e3068 感觉是对着的呢,应该就是这样
        8320a29e3068:@英勇青铜5 我也是瞎猜的:smirk:
      • 路途等待:我压缩出来为什么还是一兆多
        英勇青铜5: @路途等待 😀😀😀听起来压缩是蛮高效的呢
        路途等待:@英勇青铜5 我原图三兆多,不是太大吧
        英勇青铜5: @路途等待 估计原图就特别大吧,我当时就是简单了解了下,没有实际用过呢
      • 2739749286b8:写的不错啊 你一天上完班然后 下班写的吗? 我总感觉上完班都没精力写和学习新东西
        2739749286b8:@英勇青铜5 厉害了
        英勇青铜5:@毕文 no 上班没任务时写的。。。。
      • 小龍五:作者写错了吧?应该是郑梓斌吧.
        英勇青铜5:@1d028754cf83 :joy: :scream: :scream: 有点2b了,连作者都搞错了。。。多谢指出
      • ChienYi:學習學習
        英勇青铜5:@ChienYi :smile::smile::smile::smile:
      • 36a453c057aa:这个项目实际使用中有很多问题的,多图片压缩的时候会OOM,高或者宽特别大的时候压缩出来出有明显的失真,可以为了接近微信的压缩大小,压的太小了,学学他的代码思路还可以,在实际项目中用不起来
        英勇青铜5:@我是启昌 thx,多谢指出。我并没有用过,有人推荐,就学习一下。目前项目中并不打算考虑使用
      • dodo_lihao:学习了
        英勇青铜5:@dodo_lihao :smile::smile:
      • 橘子周二:看了你许多的文章,骚年看好你哦,Android的开源魅力是无穷的。
        英勇青铜5:@一块柠檬皮 :smile: :smile: :smile: 汉子一个 前面开玩笑的
        橘子周二:@英勇青铜5 咳咳,工作不分男女
        英勇青铜5:@一块柠檬皮 我要说我不是骚年 是个妹纸呢 :smirk: :smirk: :smirk:
      • 捡淑:mark

      本文标题:Android——Luban图片压缩算法学习

      本文链接:https://www.haomeiwen.com/subject/aqtgyttx.html