图片压缩之优化篇

之前曾经对Android中图片中的压缩方式进行分析和总结。详见图片压缩篇。基本涵盖了基础的压缩方法和思路。
但是在实际应用运用仍有许多地方需要优化地方才能够被应用。本文将就以下角度进行思考和优化:

  1. 一般性应用于朋友圈之类的图片依照怎样的参数进行哪些方面的优化处理?
  2. 如何有效的减少压缩时间?
  3. 如何避免压缩过程中的oom?

那我们就开始吧!


合理的压缩参数

首先我们要考虑我们应该用哪些参数来控制我们的压缩过程。下面是我的建议

最大宽度,最大高度

用于控制图片的最终分辨率。我们根据最宽高来进行等比例压缩。这个值我一般设置的为最大宽为1080
最大高为1920。这样的设置能满足一般照片之类的图片的要求,但是对于超长图和超宽图就会出现,压缩过度的问题,所以我们需要对超长图和超宽图进行单独的计算和处理。
首先要判断是否为长图,我这里的判断标准为宽/高高/宽 大于3。若判断为长图则修改最大宽高为极限宽高。10000这个数值也是新浪微博对于压缩长图的最大处理值。

    public static int imgMemoryMaxWidth = 10000;
    public static int imgMemoryMaxHeight = 10000;
    private static final float longImgRatio = 3f;

    public static void preDoOption(Context context, Uri uri, PressImgService.Option option) {
        if(!option.specialDealLongImg)
            return;
        try {
            FileUntil.ImgMsg imgMsg = FileUntil.getImgWidthAndHeight(context, uri);
            if (imgMsg == null) {
                return;
            }
            float ratio = (float) imgMsg.width / imgMsg.height;
            //超宽图
            if (ratio > longImgRatio) {
                option.maxWidth = imgMemoryMaxWidth;
            }
            //超长图
            else if (1 / ratio > longImgRatio) {
                option.maxHeight = imgMemoryMaxHeight;
            }


        } catch (IOException e) {
            e.printStackTrace();
        }
    }

压缩率

即图片 文件大小/(图片宽*图片高) 。这个比率其实不能够最为一个绝对标准,尤其是在图片特别小的情况下,所以我当图片小于50k的时候就不在压缩了。

private final static float ignoreSize = 1024 * 50;

压缩流程的优化

之前曾说过图片压缩主要是分为两个部分,其一是压缩图片的分辨率 其二是压缩图片的质量。这两种方式结合才能让我们得到体积小清晰度较高的图片。一般分为以下几个步骤

从Uri或者文件获取图片的bitmap

这里需要注意的是,我们要在这里进行第一次关于图片分辨率的压缩。因为源文件的分辨率是未知的,不做任何限制直接获取bitmap,很可能直接oom。处理方式为利用inJustDecodeBounds属性只获取图片宽高,然后计算一个inSampleSize。注意,这个压缩只能作为初步压缩,因为inSampleSize只能为2的倍数才有效,最终图片很难得到精确的尺寸。
代码如下

  //根据uri 获取bitmap
    public static Bitmap getBitmapFromUri(Context context, Uri uri, float targetWidth, float targetHeight) throws Exception, OutOfMemoryError {
        Bitmap bitmap = null;
        int ratio = 1;
        InputStream input = null;
        if (targetWidth != -1 && targetHeight != -1) {
            input = context.getContentResolver().openInputStream(uri);
            BitmapFactory.Options onlyBoundsOptions = new BitmapFactory.Options();
            onlyBoundsOptions.inJustDecodeBounds = true;
            onlyBoundsOptions.inDither = true;
            onlyBoundsOptions.inPreferredConfig = Bitmap.Config.RGB_565;
            BitmapFactory.decodeStream(input, null, onlyBoundsOptions);
            if (input != null) {
                input.close();
            }
            int originalWidth = onlyBoundsOptions.outWidth;
            int originalHeight = onlyBoundsOptions.outHeight;
            if ((originalWidth == -1) || (originalHeight == -1))
                return null;
            float widthRatio = originalWidth / targetWidth;
            float heightRatio = originalHeight / targetHeight;
            ratio = (int) (widthRatio > heightRatio ? widthRatio : heightRatio);
            if (ratio < 1)
                ratio = 1;
        }

        BitmapFactory.Options bitmapOptions = new BitmapFactory.Options();
        bitmapOptions.inSampleSize = (int) ratio;
        bitmapOptions.inDither = true;
        bitmapOptions.inPreferredConfig = Bitmap.Config.RGB_565;
        input = context.getContentResolver().openInputStream(uri);
        bitmap = BitmapFactory.decodeStream(input, null, bitmapOptions);
        if (input != null) {
            input.close();
        }
        return bitmap;
    }

处理bitmap至合适的分辨率

处理bitmap的分辨率我们可以利用Android自带的ThumbnailUtils.extractThumbnail()方法来进行处理。这里可以对bitmap的宽高进行精确的压缩。

 Bitmap originBitmap = FileUntil.getBitmapFromUri(context, Uri.fromFile(file), maxWidth, maxHeight);
            if (originBitmap == null)
                return false;
            float widthRadio = (float) originBitmap.getWidth() / (float) maxWidth;
            float heightRadio = (float) originBitmap.getHeight() / (float) maxHeight;
            float radio = widthRadio > heightRadio ? widthRadio : heightRadio;
            if (radio > 1) {
                bitmap = ThumbnailUtils.extractThumbnail(originBitmap, (int) (originBitmap.getWidth() / radio), (int) (originBitmap.getHeight() / radio));
                originBitmap.recycle();

            } else
                bitmap = originBitmap;

压缩bitmap的生成流的大小,并存储为文件

之后我们需要对bitmap进行存储,并且压缩图片文件大小。基于compress()函数的quality参数。这里对quality参数进行了动态化处理。

  //保存bitmap 为文件
    public static File saveImageAndGetFile(Bitmap bitmap, File file, float limitSize) {
        if (bitmap == null || file == null) {
            return null;
        }
        FileOutputStream fos = null;
        try {
            fos = new FileOutputStream(file);
            if (limitSize != -1) {
                PressImgUntil.compressImageFileSize(bitmap, fos, limitSize);
            } else {
                bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos);
            }
        } catch (Exception e) {
            e.printStackTrace();
            return null;

        } finally {
            try {
                if (fos != null)
                    fos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return file;
    }
---------------------------------------------------------

 /**  压缩文件大小
     * @param image
     * @param outputStream
     * @param limitSize    单位byte
     * @throws IOException
     */
    public static void compressImageFileSize(Bitmap image, OutputStream outputStream, float limitSize) throws Exception {
        ByteArrayOutputStream imgBytes = new ByteArrayOutputStream();
        int options = 100;
        image.compress(Bitmap.CompressFormat.JPEG, options, imgBytes);
        while (imgBytes.size() > limitSize && imgBytes.size() > ignoreSize && options > 20) {
            imgBytes.reset();
            int dx = 0;
            float dz = (float) imgBytes.size() / limitSize;
            if (dz > 2)
                dx = 30;
            else if (dz > 1)
                dx = 25;
            else
                dx = 20;
            options -= dx;
            image.compress(Bitmap.CompressFormat.JPEG, options, imgBytes);
//            Log.i("lzc", "compressImageFileSize   " + options + "---" + imgBytes.size() +"---"+image.getWidth()+"---"+image.getHeight());
        }

        outputStream.write(imgBytes.toByteArray());
        imgBytes.close();
        outputStream.close();
    }

基于线程池的动态任务分配

之前的文章曾讲过压缩图片基于线程池的处理。由于压缩过程会消耗大量的内存。所有中间提到一个矛盾:同时进行的任务数越多,总体的压缩速度越快,但是oom的风险也随之增加。我通过两种方式来尝试解决这个问题:

单独的压缩进程

将压缩进程单独的放到某个进程中,这样就能够获取更多的可用内存。但是这样就需要我们进行一些跨进程的通信,来控制压缩过程与接收压缩回调,这里可以基于Messenger机制来实现。一个PressImgService类来负责压缩业务。一个PressImgManager来控制压缩和接收回调。

动态控制线程池中的任务

尽管我们新开了进程,能够获得较大的内存,但是仍然有oom的风险。所以我打算动态的计算每一个压缩任务能占用的内存,然后根据内存剩余往线程池中添加线程。
获取内存信息

  public static MemoryMessage getMemoryMsg(Context context) {
        ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
        int memClass = activityManager.getMemoryClass();//64,以m为单位
        int largeClass = activityManager.getLargeMemoryClass();//64,以m为单位
        long freeMemory = Runtime.getRuntime().freeMemory();
        long totalMemory = Runtime.getRuntime().totalMemory();
        long maxMemory = Runtime.getRuntime().maxMemory();
        return new MemoryMessage(memClass, freeMemory, totalMemory, maxMemory,largeClass);
    }

首先我取总内存的2/3为可用内存

   FunUntil.MemoryMessage memoryMessage = FunUntil.getMemoryMsg(this);
        availableMemory = (memoryMessage.maxMemory - memoryMessage.totalMemory + memoryMessage.freeMemory) * 2 / 3;

计算每个任务的占用内存,这也是个不精确的值,但是已经很接近了。


    //计算压缩消耗内存
    public static int calcPressTaskMemoryUse(Context context, PressImgService.PressMsg pressMsg) {
        int memory = 0;
        final int imgMemoryRatio = 2;
        int targetWidth = pressMsg.option.maxWidth;
        int targetHeight = pressMsg.option.maxHeight;
        Uri uri = Uri.fromFile(pressMsg.originFile);
        try {
            //获取图片宽高
            FileUntil.ImgMsg imgMsg = FileUntil.getImgWidthAndHeight(context, uri);
            if (imgMsg == null)
                return 0;
            //长宽比例
            float widthRatio = (float) imgMsg.width / targetWidth;
            float heightRatio = (float) imgMsg.height / targetHeight;
            int ratio = (int) (widthRatio > heightRatio ? widthRatio : heightRatio);
            if (ratio < 1)
                ratio = 1;
            //第一次处理后宽高
            int originWidth = 0;
            int originHeight = 0;
            ratio = Integer.highestOneBit(ratio);
            originWidth = imgMsg.width / ratio;
            originHeight = imgMsg.height / ratio;
            //计算内存
            memory += originWidth * originHeight * imgMemoryRatio;

            //计算第二次处理
            float secondWidthRadio = (float) originWidth / (float) targetWidth;
            float secondHeightRadio = (float) originHeight / (float) targetHeight;
            float secondRadio = secondWidthRadio > secondHeightRadio ? secondWidthRadio : secondHeightRadio;
            if (secondRadio > 1) {
                memory += (originWidth / secondRadio) * (originHeight / ratio) * imgMemoryRatio;
            }
            memory += targetWidth * targetHeight * PressImgService.pressRadio;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
        return memory;
    }

将压缩任务保存成队列,每当有任务结束,按占用内存大小排序,重新分发等待中的任务。

  //分发任务
    private void dispatchTask() {
        if (pressMsgList.size() != 0) {
            Collections.sort(pressMsgList);
            if (pressMsgList.size() != 0)
                Log.i("lzc", "availableMemory  " + availableMemory);
            dispatchCore(pressMsgList);
        }

    }


    //核心分发
    public void dispatchCore(List<PressMsg> prepareMsgList) {
        int current = 0;
        while (current <= prepareMsgList.size() - 1) {
            if (prepareMsgList.get(current).userMemory > availableMemory)
                current++;
            else {
                PressMsg addTask = prepareMsgList.remove(current);
                startThread(addTask.uuid, addTask);
            }
        }
    }


    //开始执行压缩
    private void startThread(String uuid, PressMsg pressMsg) {
        if (shutDownSet.contains(uuid))
            return;
        try {
            pressMsg.currentStatus = 0;
            executorService.execute(new PressRunnable(pressMsg));
            availableMemory -= pressMsg.userMemory;
        } catch (Exception e) {
            e.printStackTrace();
            errorUUID(uuid, pressMsg, "线程启动异常!");
        }
    }

通过这样的方式基本可以完全避免oom的情况。

总结

利用上述的优化方式,基本可以实现9张图片在1-5秒的压缩完成。一般9张相册照片都在1秒左右即可压缩完成。如30000多像素的超长图,9张在5秒内也可以压缩完成。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容