細數圖片上傳功能用到的知識點(圖片壓縮篇)

先附上裁剪篇和選取篇的鏈接,結合本文食用風味更佳~
選取篇裁剪篇

壓縮目標

在講壓縮之前先要明確我們的目標

  1. 對圖片進行處理,使其滿足我們對圖片分辨率的要求;
  2. 盡可能減小圖片文件的大小,來節(jié)省上傳時間和用戶流量;
  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個字節(jié)
ARGB_4444 4個4位組成即16位 一個像素占用2個字節(jié)
ARGB_8888 4個8位組成即32位 一個像素占用4個字節(jié)
RGB_565 R為5位,G為6位,B為5位共16位 一個像素占用2個字節(jié)

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系統(tǒng)為我們提供的函數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表示不壓縮。我們無法估算這個參數對圖片最終大小的影響,所以我們只能采用循環(huán)的方式來處理我們的圖片。


    /**
     * @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 以下的圖片不進行壓縮,發(fā)現小圖的的壓縮效率不高而且質量損毀的十分嚴重。
        while (baos.toByteArray().length > limitSize&&baos.toByteArray().length> ignoreSize) {
            baos.reset();
            image.compress(Bitmap.CompressFormat.JPEG, options, baos);
            //每次減少的量,可以進行調整。由于compress這個函數占用時間很長所以我們應當盡量減少循環(huán)次數
            options -= 15;
            Log.i("lzc","currentSize"+(baos.toByteArray().length/1024));
        }
        image.compress(Bitmap.CompressFormat.JPEG, options, outputStream);
        baos.close();
        outputStream.close();
    }

壓縮

編寫完成壓縮相關函數,接下來我們就要考慮這些函數的調用方式了。很明顯這些操作都是耗時操作,不能放在主線程中執(zhí)行。而且我們有壓縮多張圖片的需求,考慮到內存問題,我們應該使用service單獨開進程來對圖片壓縮。
另外,多張圖片的壓縮是順序,還是并發(fā)執(zhí)行的問題值得我們考慮。順序執(zhí)行可以減少內存占用而并發(fā)執(zhí)行可以減少壓縮時間
我選擇了并發(fā)執(zhí)行,畢竟壓縮之后還要緊接上傳,不宜讓用戶等待過久。我們來建立我們的service開啟線程池來處理壓縮圖片流程。

創(chuàng)建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;
//創(chuàng)建新文件
        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.由于是多線程并發(fā),所以我們需要對幾個關鍵模塊加上同步鎖。第一個是多張圖片完成壓縮記的計數

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

第二個地方是我們圖片讀取的地方,否則會產生多張圖拼接到一起的問題。

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

這樣我們細數圖片上傳功能用到的知識點的三篇文章就全部講完了。
撒花,完結!

細數圖片上傳功能用到的知識點(圖片選取&拍照篇)
細數圖片上傳功能用到的知識點(裁剪篇)
細數圖片上傳功能用到的知識點(圖片壓縮篇)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容