前言
最近自己做了一個app,列表中有大量圖片需要加載,毫無任何處理的情況下佔用的內存可達250M之上:
所以需要對所有的圖片進行優化處理,那麼優化主要有以下兩個方面:
- 圖片加載時優化
- 圖片的緩存
圖片內存
首先需要了解啥圖片的內存是如何計算出來的;我們一半所說的圖片寬高就是鼠標右鍵圖片查看詳細信息那裏的像素
圖片是由一個個像素點構成的,圖片的像素點有以下四種格式:
- ALPHA_8 //每個像素佔用1byte內存
- RGB_565 //每個像素佔用2byte內存
- ARGB_4444 //每個像素佔用2byte內存
- ARGB_8888 //每個像素佔用4byte內存
圖片佔用的內存 = 寬 * 高 * 像素點格式
這裏要注意:圖片的內存佔用大小,只和它的像素點格式有關,和它的文件格式無關,.png、.jpg、.webp等同樣的圖片不同格式佔用內存是相同的。
但是,在Android項目中,同樣圖片放在不同的文件目錄,所佔用的內存大小是不同的。在BitmapFactory中的Options有一個屬性, inDensity,表示bitmap的像素密度,它是根據不同的文件目錄去賦值的
drawable-ldpi 120
drawable-mdpi 160
drawable-hdpi 240
drawable-xhdpi 320
drawable-xxhdpi 480
優化場景
什麼時候需要對圖片優化?比如,一個高清大圖在app中只需要顯示在一個比較小的控件上;又或者比如一些特別長的圖,那麼就需要分段加載,用戶滑動到哪裏就加載那一部分;前面兩種都是加載單個圖片的場景,當有圖片列表時就需要緩存,同樣的圖片不要重複創建bitmap對象或是重複從網絡獲取。針對以上場景逐個分析。
大圖片顯示在小控件
以這張圖片爲例,讓他加載到app中
//XML
<ImageView
android:layout_marginTop="50dp"
android:id="@+id/ivCover"
android:layout_width="300dp"
android:layout_height="200dp"/>
//Java
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.big);
ivCover.setImageBitmap(bitmap);
Log.e("圖片佔用的內存", bitmap.getByteCount() + " byte");
可以看到,當界面只加載這一張圖片,內存也會飆升
從log中可以看出,這個圖片佔用了50M的內存(log中的byte單位),這種情況下,圖片的寬高遠大於View的寬高,我們就可以對他進行縮放;
新建 ImgUtils,寫入以下代碼:
public class ImgUtils {
/**
* 對圖片縮放、降低質量
* @param context
* @param resId
* @param showWidth
* @param showHeight
* @return
*/
public static Bitmap resizeBitmap(Context context, int resId, int showWidth, int showHeight) {
BitmapFactory.Options mOptions = new BitmapFactory.Options();
mOptions.inMutable = true;
mOptions.inJustDecodeBounds = true;
BitmapFactory.decodeResource(context.getResources(), resId, mOptions);
int width = mOptions.outWidth;
int height = mOptions.outHeight;
mOptions.inSampleSize = calcuteInSampleSize(width, height, showWidth, showHeight);
mOptions.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(context.getResources(), resId, mOptions);
}
/**
* 計算最大縮放比例
* @param relWidth 真實寬高
* @param relHeight 真是寬高
* @param showWidth 顯示在view中的寬高
* @param showHeight 顯示在view中的寬高
* @return
*/
public static int calcuteInSampleSize(int relWidth, int relHeight, int showWidth, int showHeight) {
Log.d("ImgUtils", "relWidth : " + relWidth + " relHeight : " + relHeight + " showWidth : " + showWidth + " showHeight : " + showHeight );
int inSampleSize = 1;
if (relWidth > showWidth && relHeight > showHeight) {
inSampleSize = 2;
while ((relWidth /= 2) > showWidth && (relHeight /= 2) > showHeight) {
inSampleSize *= 2;
}
}
Log.d("ImgUtils", "calcuteInSampleSize : " + inSampleSize);
return inSampleSize;
}
}
修改activity中的代碼:
Bitmap resizeBitmap = ImgUtils.resizeBitmap(this, R.drawable.big, 300, 200);
Log.e("圖片內存_優化一", resizeBitmap.getByteCount() + " byte");
ivCover.setImageBitmap(resizeBitmap);
運行後會發現native大幅度下降
看一下log
優化了將近二十倍;上面的代碼中,最核心的就是給 Bitmap 設置了 inSampleSize 屬性;inSampleSize 就是取圖片寬高的幾分之一,如果一個圖片的寬高都是100像素,inSampleSize 等於2 的情況下,bitmap分別取圖片寬高的 二分之一,那麼也就意味着圖片佔用的內存是原來的 四分之一;當然,這樣縮放是會造成圖片失真,所有一定要注意 inSampleSize 的大小;
除了設置 inSampleSize,之前還說了圖片的像素格式,RGB_565 佔用 2字節,是ARGB_8888 的一半,也可以修改圖片的像素格式達到減少內存佔用的目的;
在ImgUtils類中的resizeBitmap方法加入以下代碼測試:
public class ImgCacheUtils {
public static Bitmap resizeBitmap(Context context, int resId, int showWidth, int showHeight) {
BitmapFactory.Options mOptions = new BitmapFactory.Options();
mOptions.inMutable = true;
// 修改圖片像素格式
mOptions.inPreferredConfig = Bitmap.Config.RGB_565;
mOptions.inJustDecodeBounds = true;
BitmapFactory.decodeResource(context.getResources(), resId, mOptions);
int width = mOptions.outWidth;
int height = mOptions.outHeight;
mOptions.inSampleSize = calcuteInSampleSize(width, height, showWidth, showHeight);
mOptions.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(context.getResources(), resId, mOptions);
}
}
運行後,看一下log
和沒加入修改格式代碼時相比,內存佔用又縮小了一半;同樣,這樣也會讓圖片失真,在處理時一定要考慮圖片的效果問題;
超長圖片處理
遇到特別長的圖片,我們就需要讓他局部加載,就需要我們自定義View;主要實現的功能:只加載屏幕上可見的部分,用戶滑動時改變可見區域,既然滑動,那麼也要實現滑動的邏輯。圖片壓縮處理,依舊採用上面工具類中的方法,對 inSimpleSize 進行修改,和修改圖片的像素格式
新建LongImageView:
public class LongImageView extends View implements GestureDetector.OnGestureListener, View.OnTouchListener {
//View 滑動相關
private GestureDetector mGestureDetector;
private Scroller mScroller;
//可見的矩形區域
Rect mRect;
BitmapFactory.Options mOptions;
BitmapRegionDecoder mBitmapRegionDecoder;
//圖片寬高
int mImageWidth;
int mImageHeight;
//View的寬高
int mViewHeight;
int mViewWidth;
//縮放比例
float mZoom;
Bitmap bitmap = null;
public LongImageView(Context context) {
this(context, null, 0);
}
public LongImageView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public LongImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mGestureDetector = new GestureDetector(context, this);
setOnTouchListener(this);
mScroller = new Scroller(context);
mRect = new Rect();
mOptions = new BitmapFactory.Options();
}
public void setImage(InputStream in) {
mOptions.inJustDecodeBounds = true;
BitmapFactory.decodeStream(in, null, mOptions);
//獲取圖片寬高
mImageWidth = mOptions.outWidth;
mImageHeight = mOptions.outHeight;
//設置圖片格式
mOptions.inMutable = true;
//mOptions.inPreferredConfig = Bitmap.Config.RGB_565;
mOptions.inJustDecodeBounds = false;
try {
// 第二個參數爲 false 表示 輸入流關閉時 不受影響
mBitmapRegionDecoder = BitmapRegionDecoder.newInstance(in, false);
} catch (IOException e) {
e.printStackTrace();
}
requestLayout();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mBitmapRegionDecoder == null){
return;
}
mOptions.inBitmap = bitmap;
bitmap = mBitmapRegionDecoder.decodeRegion(mRect, mOptions);
Matrix matrix = new Matrix();
matrix.setScale(mZoom * mOptions.inSampleSize, mZoom * mOptions.inSampleSize);
Log.e("佔用的內存", bitmap.getByteCount() + " byte");
canvas.drawBitmap(bitmap, matrix, null);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
mViewWidth = getMeasuredWidth();
mViewHeight = getMeasuredHeight();
if (mBitmapRegionDecoder == null){
return;
}
//設置矩形區域
mRect.left = 0;
mRect.top = 0;
mRect.right = mImageWidth;
//根據圖片縮放程度 計算出圖片顯示的高度
mZoom = (float)mViewWidth / (float)mImageWidth;
mRect.bottom = (int) (mViewHeight / mZoom);
mOptions.inMutable = true;
mOptions.inSampleSize = ImgUtils.calcuteInSampleSize(mImageWidth, mImageHeight, mViewWidth, mViewHeight);
}
/**
* 用戶 觸摸 屏幕
* @param e
* @return
*/
@Override
public boolean onDown(MotionEvent e) {
if (!mScroller.isFinished()){
mScroller.forceFinished(true);
}
return true;
}
/**
* 用戶 觸摸 屏幕 但是 沒有鬆開 拖動
* @param e
*/
@Override
public void onShowPress(MotionEvent e) {
}
/**
* 用戶 觸摸 屏幕 鬆開後
*/
@Override
public boolean onSingleTapUp(MotionEvent e) {
return false;
}
/**
* 用戶 滑動 屏幕
*/
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
mRect.offset(0, (int) distanceY);
if(mRect.bottom > mImageHeight){
mRect.bottom = mImageHeight;
mRect.top = mImageHeight - (int) (mViewHeight / mZoom);
}
if (mRect.top < 0) {
mRect.top = 0;
mRect.bottom = (int) (mViewHeight / mZoom);
}
invalidate();
return false;
}
@Override
public void onLongPress(MotionEvent e) {
}
/**
* 用戶 觸摸 屏幕 快速滑動後鬆開
*/
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
mScroller.fling(0, mRect.top, 0, (int) - velocityY, 0, 0,
0, mImageHeight - (int) (mViewHeight / mZoom));
return false;
}
@Override
public boolean onTouch(View v, MotionEvent event) {
return mGestureDetector.onTouchEvent(event);
}
@Override
public void computeScroll() {
if (mScroller.isFinished()) {
return;
}
//返回 true 表示 在滑動
if (mScroller.computeScrollOffset()) {
mRect.top = mScroller.getCurrY();
mRect.bottom = mRect.top + (int) (mViewHeight / mZoom);
invalidate();
}
}
}
在這個自定義view中,我們藉助了GestureDetector 手勢檢測 和 Scoller,處理用戶觸摸,滑動等事件;圖像局部顯示利用的是BitmapRegionDecoder,看以下他的api:
當用戶滑動時,不斷通過 computeScroll 方法去計算顯示的位置,並且重繪界面,這裏要注意 滑到頂部 和 滑到底部 時的判斷、邏輯處理。