细数图片上传功能用到的知识点(图片压缩篇)

先附上裁剪篇和选取篇的链接,结合本文食用风味更佳~
选取篇裁剪篇

压缩目标

在讲压缩之前先要明确我们的目标

  1. 对图片进行处理,使其满足我们对图片分辨率的要求;
  2. 尽可能减小图片文件的大小,来节省上传时间和用户流量;
  3. 避免oom;

压缩图片相关函数

明确目标之后首先我们来编写我们可能需要用到的函数

读取图片

从file或者uri中读取bitmap的这一步,我们要对图片进行第一次的处理。生成bitmap可以使用这个方法BitmapFactory.decodeStream(),由于目标图片可能分辨率很大,如果这里不进行处理很容易造成oom。
这里我们可以利用两个方式来降低bitmap所占用的内存。

inSampleSize

利用BitmapFactory.Options中的inSampleSize属性可以减小图片的分辨率。若inSampleSize=x得到的bitmap属性就是原始分辨率的1/x。inSampleSize值只能为2的倍数。也就是说当inSampleSize的值为2,4,6,8的时候才有用。这种方式并不能精确的得到我们想要的分辨率,但是作为初步的压缩还是非常合适的。
那么计算inSampleSize的值可以先获取原始图片的大小,再根据我们自己的目标大小来进行初步压缩。要获取原始图片大小,我们可以利用BitmapFactory.OptionsinJustDecodeBounds属性。

inPreferredConfig

利用BitmapFactory.Options中的inPreferredConfig属性可以改变图片的默认模式,bitmap有如下四种模式

模式 组成 占用内存
ALPHA_8 Alpha由8位组成 一个像素占用1个字节
ARGB_4444 4个4位组成即16位 一个像素占用2个字节
ARGB_8888 4个8位组成即32位 一个像素占用4个字节
RGB_565 R为5位,G为6位,B为5位共16位 一个像素占用2个字节

Android默认的图片模式为ARGB_8888,但是如果我们的图片不需要太高的质量并且没有透明通道。我们完全可以使用RGB_565这种模式。

完整代码
 /**
     * @param
     * @param uri
     * @param targetWidth  限制宽度
     * @param targetHeight 限制高度
     * @return
     * @throws Exception
     */
    public static Bitmap getBitmapFromUri(Context context, Uri uri, float targetWidth, float targetHeight) throws Exception {
        Bitmap bitmap = null;
        InputStream 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;
        //计算压缩值
        float ratio = 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系统为我们提供的函数extractThumbnail(Bitmap source, int width, int height),这个函数的内部实现很有意思,有空大家可以先看看。而如果我们的压缩要保证图片的等比例处理,需要合理的去计算新的width和height。计算方法如下

 float widthRadio = (float) bitmap.getWidth() /(float) maxWidth;
            float heightRadio = (float) bitmap.getHeight() / (float)maxHeight;
            float radio = widthRadio > heightRadio ? widthRadio : heightRadio;
            if (radio > 1) {
                bitmap = ThumbnailUtils.extractThumbnail(bitmap, (int) (bitmap.getWidth() / radio), (int) (bitmap.getHeight() / radio));
            }

保存bitmap并压缩文件大小

得到了合适分辨率的bitmap,我们接下来就需要对图片的大小进行压缩和保存了。接下来问题就来了,一张图片应该占用多大的空间呢?我的办法就是引入一个参数表明1像素占用的大小来处理图片。压缩图片大小我们可以使用bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos); 注意只有jpeg格式的图片才能被这个函数压缩!第二个参数表明图片的压缩质量,100表示不压缩。我们无法估算这个参数对图片最终大小的影响,所以我们只能采用循环的方式来处理我们的图片。


    /**
     * @param image
     * @param outputStream
     * @param limitSize    单位byte 由单位像素占用大小计算得出
     * @throws IOException
     */
    public static void compressImage(Bitmap image, OutputStream outputStream, float limitSize) throws Exception {

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        image.compress(Bitmap.CompressFormat.JPEG, 100, baos);
        int options = 100;
      //ignoreSize 以下的图片不进行压缩,发现小图的的压缩效率不高而且质量损毁的十分严重。
        while (baos.toByteArray().length > limitSize&&baos.toByteArray().length> ignoreSize) {
            baos.reset();
            image.compress(Bitmap.CompressFormat.JPEG, options, baos);
            //每次减少的量,可以进行调整。由于compress这个函数占用时间很长所以我们应当尽量减少循环次数
            options -= 15;
            Log.i("lzc","currentSize"+(baos.toByteArray().length/1024));
        }
        image.compress(Bitmap.CompressFormat.JPEG, options, outputStream);
        baos.close();
        outputStream.close();
    }

压缩

编写完成压缩相关函数,接下来我们就要考虑这些函数的调用方式了。很明显这些操作都是耗时操作,不能放在主线程中执行。而且我们有压缩多张图片的需求,考虑到内存问题,我们应该使用service单独开进程来对图片压缩。
另外,多张图片的压缩是顺序,还是并发执行的问题值得我们考虑。顺序执行可以减少内存占用而并发执行可以减少压缩时间
我选择了并发执行,毕竟压缩之后还要紧接上传,不宜让用户等待过久。我们来建立我们的service开启线程池来处理压缩图片流程。

创建service时建立线程池

  @Override
    public void onCreate() {
        super.onCreate();
        fileHashtable.clear();
        executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                9, TimeUnit.SECONDS,
                new SynchronousQueue<Runnable>());
    }

每次startServcie向线程池增加一个事件


    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        executorService.execute(new PressRunnable(intent));
        return super.onStartCommand(intent, flags, startId);
    }

处理事件和压缩

//压缩图片线程
private class PressRunnable implements Runnable {
        private Intent intent;

        public PressRunnable(Intent intent) {
            this.intent = intent;
        }

        @Override
        public void run() {
            onHandleIntent(intent);
        }
    }


//处理intent得到参数
    protected void onHandleIntent(Intent intent) {
        if (intent != null) {
            final String action = intent.getAction();
            if (ACTION_FOO.equals(action)) {
                final Uri uri = intent.getParcelableExtra(EXTRA_PARAM1);
                final ChoicePhotoManager.Option option = (ChoicePhotoManager.Option) intent.getSerializableExtra(EXTRA_PARAM2);
                realCount = intent.getIntExtra(EXTRA_PARAM3, 1);
                int position = intent.getIntExtra(EXTRA_PARAM4, 0);
                handleActionFoo(uri, option, position);
            }
        }
    }


//压缩图片
    private void handleActionFoo(Uri uri, ChoicePhotoManager.Option option, int position) {
        File file = new File(FileUntil.UriToFile(uri, this));
        if (!file.exists())
            return;
//创建新文件
        File newFile = FileUntil.createTempFile(FileName + UUID.randomUUID() + ".jpg");
  
//压缩图片并保存到新文件
        FileUntil.compressImg(this, file, newFile, option.pressRadio, option.maxWidth, option.maxHeight);

//读取原始图片的旋转信息,并给以现有图片
        FileUntil.setFilePictureDegree(newFile, FileUntil.readPictureDegree(file.getPath()));
        fileHashtable.put(position, newFile);
        synchronized (PressImgService.class) {
            count++;
            if (realCount == count) {
                callFinish();
            }
        }
    }

//压缩完毕关闭servcie 回传压缩后图片的uri
    private void callFinish() {
        Intent intent = new Intent();
        intent.setAction(callbackReceiver);
        Uri[] uris = new Uri[fileHashtable.keySet().size()];
        for (Map.Entry<Integer, File> integerFileEntry : fileHashtable.entrySet()) {
            uris[integerFileEntry.getKey()] = Uri.fromFile(integerFileEntry.getValue());
        }
        for (int i = 0; i < realCount; i++) {
            Log.i("lzc", "position---asd" + i);
        }
        intent.putExtra("data", uris);
        sendBroadcast(intent);
        fileHashtable.clear();
        count = 0;
        stopSelf();
    }

注意

上面的代码很长,但是要注意的只有两点。

  1. 要注意读取之前文件的旋转信息,并赋值给新的文件。这样,新的图片才能得到正确的旋转角度。
    用到的函数如下。
//读取文件的旋转信息
 public static int readPictureDegree(String path) {
        int degree = 0;
        try {
            ExifInterface exifInterface = new ExifInterface(path);
            int orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
            switch (orientation) {
                case ExifInterface.ORIENTATION_ROTATE_90:
                    degree = 90;
                    break;
                case ExifInterface.ORIENTATION_ROTATE_180:
                    degree = 180;
                    break;
                case ExifInterface.ORIENTATION_ROTATE_270:
                    degree = 270;
                    break;
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return degree;
    }

  //为文件设置旋转信息
    public static void setFilePictureDegree(File file, int degree) {
        try {
            ExifInterface exifInterface = new ExifInterface(file.getPath());
            int orientation = ExifInterface.ORIENTATION_NORMAL;
            switch (degree) {
                case 90:
                    orientation = ExifInterface.ORIENTATION_ROTATE_90;
                    break;
                case 180:
                    orientation = ExifInterface.ORIENTATION_ROTATE_180;
                    break;
                case 270:
                    orientation = ExifInterface.ORIENTATION_ROTATE_270;
                    break;
            }
            exifInterface.setAttribute(ExifInterface.TAG_ORIENTATION, orientation + "");
            exifInterface.saveAttributes();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

2.由于是多线程并发,所以我们需要对几个关键模块加上同步锁。第一个是多张图片完成压缩记的计数

    synchronized (PressImgService.class) {
            count++;
            if (realCount == count) {
                callFinish();
            }

第二个地方是我们图片读取的地方,否则会产生多张图拼接到一起的问题。

   synchronized (PressImgService.class) {
                bitmap = getBitmapFromUri(context, Uri.fromFile(file), maxWidth, maxHeight);
            }

这样我们细数图片上传功能用到的知识点的三篇文章就全部讲完了。
撒花,完结!

细数图片上传功能用到的知识点(图片选取&拍照篇)
细数图片上传功能用到的知识点(裁剪篇)
细数图片上传功能用到的知识点(图片压缩篇)

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

推荐阅读更多精彩内容