Android 簡單代碼實現圖片極致壓縮不在oom

最近遇到一個前人留下坑,如下報錯。

 java.lang.RuntimeException: Canvas: trying to draw too large(268435456bytes) bitmap.
09-20 17:06:39.298 26126 26126 E AndroidRuntime:        at android.view.DisplayListCanvas.throwIfCannotDraw(DisplayListCanvas.java:229)
09-20 17:06:39.298 26126 26126 E AndroidRuntime:        at android.view.RecordingCanvas.drawBitmap(RecordingCanvas.java:98)
09-20 17:06:39.298 26126 26126 E AndroidRuntime:        at android.graphics.drawable.BitmapDrawable.draw(BitmapDrawable.java:545)
09-20 17:06:39.298 26126 26126 E AndroidRuntime:        at android.widget.ImageView.onDraw(ImageView.java:1360)
09-20 17:06:39.298 26126 26126 E AndroidRuntime:        at android.view.View.draw(View.java:20211)
09-20 17:06:39.298 26126 26126 E AndroidRuntime:        at android.view.View.updateDisplayListIfDirty(View.java:19086)
09-20 17:06:39.298 26126 26126 E AndroidRuntime:        at android.view.View.draw(View.java:19939)
09-20 17:06:39.298 26126 26126 E AndroidRuntime:        at android.view.ViewGroup.drawChild(ViewGroup.java:4333)
09-20 17:06:39.298 26126 26126 E AndroidRuntime:        at android.view.ViewGroup.dispatchDraw(ViewGroup.java:4112)
09-20 17:06:39.298 26126 26126 E AndroidRuntime:        at android.view.View.draw(View.java:20214)
09-20 17:06:39.298 26126 26126 E AndroidRuntime:        at android.view.View.updateDisplayListIfDirty(View.java:19086)

問題原因:

ListView刷圖片時每次都取本地圖片並做一系列壓縮等操作,但壓縮沒有做好導致壓縮完成後圖片還是很大,發生了上面的Crash.

下面我貼一下問題代碼:

該代碼有如下幾個問題:

1.圖片沒有緩存每次加載都要壓縮計算

2.直接開啓線程,沒有使用線程池

3.壓縮方法寫死了,防縮比例計算有問題

4.防縮後沒有對圖片進行大小壓縮

private void loadBitmapsTask(String path,ImageView imageView){
        new Thread(new Runnable() {
            @Override
            public void run() {
                Bitmap bitmap = getImage(path);
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        if(imageView != null && bitmap !=null){
                            ImageView imageViewImage = imageView;
                            if(imageViewImage != null){
                                imageViewImage.setImageBitmap(bitmap);
                            }
                        }
                    }
                });
            }
        }).start();
    }


private static Bitmap getImage(String srcPath){
            BitmapFactory.Options newopts = new BitmapFactory.Options();
            //返回bitmap尺寸
            newopts.inJustDecodeBounds = true;
            BitmapFactory.decodeFile(srcPath,newopts);
            //獲取bitmap寬高
            int w = newopts.outWidth;
            int h = newopts.outHeight;
            float resolutionH = 80f;
            float resolutionW = 80f;
            int be = 1;
            if(w > h && w>resolutionW){
                be = (int)(w/resolutionW);
            }else if(w < h && h > resolutionH){
                be = (int)(h/resolutionH);
            }
            if(be <= 0 ){
                be = 1;
            }
            newopts.inJustDecodeBounds = false;
            newopts.inSampleSize = be;
            Bitmap bitmap = BitmapFactory.decodeFile(srcPath, newopts);
            return bitmap;
        }

面對以上問題,結合網上資料我做了一個工具類:

針對上面的問題我們需要以下:

1.緩存

2.本地網絡圖片加載

3.圖片極致壓縮

4.線程池多任務管理

5.線程切換異步刷新View

一,內存緩存

public class ImageCache extends LruCache<String, Object> {

    private Map<String, SoftReference<Object>> cacheMap;

    public ImageCache(Map<String, SoftReference<Object>> cacheMap) {
        super((int) (Runtime.getRuntime().maxMemory() / 8));
        this.cacheMap = cacheMap;
    }

    @Override
    protected int sizeOf(String key, Object value) {
        if(value instanceof Bitmap){
            return ((Bitmap)value).getRowBytes() * ((Bitmap)value).getHeight();
        }else{
            return super.sizeOf( key,  value);
        }
    }

    @Override
    protected void entryRemoved(boolean evicted, String key, Object oldValue, Object newValue) {
        if (oldValue != null) {
            SoftReference<Object> softReference = new SoftReference<>(oldValue);
            cacheMap.put(key, softReference);
        }
    }

    public Map<String, SoftReference<Object>> getCacheMap() {
        return cacheMap;
    }
}

二,圖片壓縮工具

public class ImageCompressUtils {
    //圖片最後壓縮大小訪問小於50kb
    private static final int IMAGE_COMPRESS_MAXS_IZE = 1024 * 50;

    /**
     *
     * @param srcPath
     * @param width
     * @param height
     * @return
     */
    public static Bitmap getImageFromLocal(String srcPath, int width, int height) {
        BitmapFactory.Options newopts = new BitmapFactory.Options();
        //返回bitmap尺寸
        newopts.inJustDecodeBounds = true;
        BitmapFactory.decodeFile(srcPath, newopts);
        newopts.inJustDecodeBounds = false;
        newopts.inSampleSize = calculateInSampleSize(newopts, width, height);
        Bitmap bitmap = BitmapFactory.decodeFile(srcPath, newopts);
        return compressImage(bitmap);
    }

    /**
     * 計算圖片放縮到目標大小
     * @param options
     * @param reqWidth
     * @param reqHeight
     * @return
     */
    public static int calculateInSampleSize(
            BitmapFactory.Options options, int reqWidth, int reqHeight) {
        // Raw height and width of image
        final int height = options.outHeight;
        final int width = options.outWidth;
        int inSampleSize = 1;

        if (height > reqHeight || width > reqWidth) {

            final int halfHeight = height / 2;
            final int halfWidth = width / 2;

            // Calculate the largest inSampleSize value that is a power of 2 and keeps both
            // height and width larger than the requested height and width.
            while ((halfHeight / inSampleSize) >= reqHeight
                    && (halfWidth / inSampleSize) >= reqWidth) {
                inSampleSize *= 2;
            }
        }

        return inSampleSize;
    }
    
    /**
     * 質量壓縮方法
     * @param image
     * @return
     */
    public static Bitmap compressImage(Bitmap image) {
        if (image == null) {
            return null;
        }
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        // 質量壓縮方法,這裏100表示不壓縮,把壓縮後的數據存放到baos中
        image.compress(Bitmap.CompressFormat.JPEG, 100, baos);
        int options = 80;
        // 循環判斷如果壓縮後圖片是否大於100kb,大於繼續壓縮
        while (baos.toByteArray().length > IMAGE_COMPRESS_MAXS_IZE && options >= 0) {
            // 重置baos即清空baos
            baos.reset();
            // 這裏壓縮options%,把壓縮後的數據存放到baos中
            image.compress(Bitmap.CompressFormat.JPEG, options, baos);
            // 每次都減少5
            options -= 5;
        }
        // 把壓縮後的數據baos存放到ByteArrayInputStream中
        ByteArrayInputStream isBm = new ByteArrayInputStream(baos.toByteArray());
        // 把ByteArrayInputStream數據生成圖片
        Bitmap bitmap = BitmapFactory.decodeStream(isBm, null, null);
        return bitmap;
    }
}

三,圖片加載工具類

public class ImageUtils {
    //網絡下載緩存路徑
    private static final String CACHE_PATH = Environment.getExternalStorageDirectory().getAbsolutePath() + "/Picture";
    //圖片緩存
    private static ImageCache mCaches;
    private static ImageUtils mInstance;

    private static Handler mHandler;

    //聲明線程池,全局只有一個線程池,所有訪問網絡圖片,只有這個池子去訪問。
    private static ExecutorService mPool;

    private Map<ImageView, String> mTags = new LinkedHashMap<>();

    private ImageUtils() {
        Map<String, SoftReference<Object>> cacheMap = new HashMap<>();
        mCaches = new ImageCache(cacheMap);
    }

    public static ImageUtils getInstance() {
        if (mInstance == null) {
            synchronized (ImageUtils.class) {
                if (mInstance == null) {
                    mInstance = new ImageUtils();
                    if (mHandler == null) {
                        //實例化Handler
                        mHandler = new Handler(Looper.getMainLooper());
                    }

                    if (mPool == null) {
                        mPool = Executors.newCachedThreadPool();
                    }
                }
            }
        }
        return mInstance;
    }

    /**
     * 給imageView加載url對應的圖片
     * @param iv
     * @param url
     */
    public void display(ImageView iv, String url) {
        //1.從內存中獲取
        Bitmap bitmap = getBitmapFromMemory(url);
        if (bitmap != null) {
            //內存中有,顯示圖片
            iv.setImageBitmap(bitmap);
            return;
        }
        //2.內存中沒有,從本地獲取
        bitmap = loadFromLocal(url);
        if (bitmap != null) {
            //本地有,顯示
            iv.setImageBitmap(bitmap);
            return;
        }

        //從網絡中獲取
        loadFromNet(iv, url);
    }

    /**
     * 加載圖片
     * @param iv
     * @param url
     */
    public void loadFromLocal(ImageView iv, String url) {
        mTags.put(iv, url);//url是ImageView最新的地址
        //1.從內存中獲取
        Bitmap bitmap = getBitmapFromMemory(url);
        if (bitmap != null) {
            //內存中有,顯示圖片
            iv.setImageBitmap(bitmap);
            return;
        }
        //用線程池去管理
        mPool.execute(new LocalImageTask(iv, url));
    }

    private void loadFromNet(ImageView iv, String url) {
        mTags.put(iv, url);//url是ImageView最新的地址
        //用線程池去管理
        mPool.execute(new NetImageTask(iv, url));
    }

    /**
     * 本地加載圖片任務,包括加載防縮,壓縮等過程
     */
    private class LocalImageTask implements Runnable {
        private ImageView iv;
        private String url;

        public LocalImageTask(ImageView iv, String url) {
            this.iv = iv;
            this.url = url;
        }

        @Override
        public void run() {
            //2.內存中沒有,從本地獲取
            Bitmap bitmap = loadFromLocal(url);
            //存儲到內存
            mCaches.put(url, bitmap);

            //在顯示UI之前,拿到最新的url地址
            String recentlyUrl = mTags.get(iv);

            //把這個url和最新的url地址做一個比對,如果相同,就顯示ui
            if (url.equals(recentlyUrl)) {
                //顯示到UI,當前是子線程,需要使用Handler。其中post方法是執行在主線程的
                mHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        display(iv, url);
                    }
                });
            }
        }
    }

    /**
     * 網絡加載任務
     */
    private class NetImageTask implements Runnable {
        private ImageView iv;
        private String url;

        public NetImageTask(ImageView iv, String url) {
            this.iv = iv;
            this.url = url;
        }

        @Override
        public void run() {
            try {
                HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();

                //連接服務器超時時間
                conn.setConnectTimeout(5000);
                conn.setReadTimeout(5000);

                //連接服務器(可寫可不寫)
                conn.connect();

                //獲取流
                InputStream is = conn.getInputStream();

                //將流變成bitmap
                Bitmap bitmap = BitmapFactory.decodeStream(is);

                //存儲到本地
                save2Local(bitmap, url);

                //存儲到內存
                mCaches.put(url, bitmap);

                //在顯示UI之前,拿到最新的url地址
                String recentlyUrl = mTags.get(iv);

                //把這個url和最新的url地址做一個比對,如果相同,就顯示ui
                if (url.equals(recentlyUrl)) {
                    //顯示到UI,當前是子線程,需要使用Handler。其中post方法是執行在主線程的
                    mHandler.post(new Runnable() {
                        @Override
                        public void run() {
                            display(iv, url);
                        }
                    });
                }


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

    }

    /**
     * 存儲到本地
     *
     * @param bitmap
     * @param url
     */
    public void save2Local(Bitmap bitmap, String url) throws FileNotFoundException {
        File file = getCacheFile(url);
        FileOutputStream fos = new FileOutputStream(file);
        /**
         * 用來壓縮圖片大小
         * Bitmap.CompressFormat format 圖像的壓縮格式;
         * int quality 圖像壓縮率,0-100。 0 壓縮100%,100意味着不壓縮;
         * OutputStream stream 寫入壓縮數據的輸出流;
         * 返回值:如果成功地把壓縮數據寫入輸出流,則返回true。
         */
        bitmap.compress(Bitmap.CompressFormat.JPEG, 80, fos);
    }

    /**
     * 從本地獲取圖片
     *
     * @param url
     * @return bitmap
     */
    private Bitmap loadFromLocal(String url) {
        //本地需要存儲路徑
        File file = new File(url);

        if (file.exists()) {
            //本地有
            //把文件解析成Bitmap
            Bitmap bitmap = ImageCompressUtils.getImageFromLocal(url, 80, 80);
            //存儲到內存
            setBitmapToMemory(url, bitmap);
            mCaches.put(url, bitmap);

            return bitmap;
        }

        return null;
    }


    /**
     * 獲取緩存文件路徑(緩存目錄)
     *
     * @return 緩存的文件
     */
    private File getCacheFile(String url) {
        String name = url;

        File dir = new File(CACHE_PATH);
        if (!dir.exists()) {
            //文件不存在,就創建
            dir.mkdirs();
        }
        //此處的url可能會很長,一般會使用md5加密
        return new File(dir, name);
    }

    /**
     * 從內存中讀圖片
     * @param url
     */
    public Bitmap getBitmapFromMemory(String url) {
        Bitmap bitmap = (Bitmap) mCaches.get(url);
        // 如果圖片不存在強引用中,則去軟引用(SoftReference)中查找
        if (bitmap == null) {
            Map<String, SoftReference<Object>> cacheMap = mCaches.getCacheMap();
            SoftReference<Object> softReference = cacheMap.get(url);
            if (softReference != null) {
                bitmap = (Bitmap) softReference.get();
                //重新放入強引用緩存中
                mCaches.put(url, bitmap);
            }
        }
        return bitmap;

    }

    /**
     * 往內存中寫圖片
     * @param url
     * @param bitmap
     */
    public void setBitmapToMemory(String url, Bitmap bitmap) {
        mCaches.put(url, bitmap);
    }

    public void clear() {
        mCaches.getCacheMap().clear();
    }

}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章