最近遇到一個前人留下坑,如下報錯。
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();
}
}