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

作者 信马归风
2017.06.06 02:10 字数 6314 阅读 875评论 28

裁剪篇和选取篇,结合本文食用风味更佳~

压缩目标

在讲压缩之前先要明确我们的目标 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有如下四种模式

| 模式 | 组成 | 占用内存 | |:-------------:|:-------------:|:-----:| | ALPHA8 | Alpha由8位组成 | 一个像素占用1个字节 | | ARGB4444 | 4个4位组成即16位 |一个像素占用2个字节 | | ARGB8888 | 4个8位组成即32位 | 一个像素占用4个字节 | | RGB565 | R为5位,G为6位,B为5位共16位 |一个像素占用2个字节 |

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

完整代码
 /**
     * @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);
            }

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

发表评论

说点什么吧!留下邮箱让我好回复你。 必填项已用*标注