在平常開發中咱們常常遇到加載圖片報出oom的錯誤,咱們要解決這個問題,首先要明白oom表明out of memory 內存溢出,由於手機內存有限,分給每一個應用的內存有限,因此要解決這個問題就是要解決圖片佔用內存問題 android 中圖片是以bitmap的形式存在的,那麼bitmap中所佔的內存,直接影響到了是否oom,咱們瞭解一下bitmap的佔用內存的計算方法java
從本地加載或者從網絡加載能夠用下面的公式計算android
圖片的長度 * 圖片的寬度 * 一個像素點佔用的字節數
複製代碼
若是從資源文件夾加載,會怎麼樣?canvas
首先把同一張圖片放進不一樣的資源文件夾會發生什麼?bash
看下源碼網絡
if (env->GetBooleanField(options, gOptions_scaledFieldID)) {
const int density = env->GetIntField(options, gOptions_densityFieldID);
const int targetDensity = env->GetIntField(options, gOptions_targetDensityFieldID);
const int screenDensity = env->GetIntField(options, gOptions_screenDensityFieldID);
if (density != 0 && targetDensity != 0 && density != screenDensity) {
scale = (float) targetDensity / density;
}
}
...
int scaledWidth = decoded->width();
int scaledHeight = decoded->height();
if (willScale && mode != SkImageDecoder::kDecodeBounds_Mode) {
scaledWidth = int(scaledWidth * scale + 0.5f);
scaledHeight = int(scaledHeight * scale + 0.5f);
}
...
if (willScale) {
const float sx = scaledWidth / float(decoded->width());
const float sy = scaledHeight / float(decoded->height());
bitmap->setConfig(decoded->getConfig(), scaledWidth, scaledHeight);
bitmap->allocPixels(&javaAllocator, NULL);
bitmap->eraseColor(0);
SkPaint paint;
paint.setFilterBitmap(true);
SkCanvas canvas(*bitmap);
canvas.scale(sx, sy);
canvas.drawBitmap(*decoded, 0.0f, 0.0f, &paint);
}
複製代碼
咱們能夠看到壓縮比例是由下面的公式得出app
scale = (float) targetDensity / density;
複製代碼
及縮放的比例和targetDensity,density有關,那麼這個倆個變量又表明着什麼呢?ide
其中density和Bitmap存放的資源目錄有關,不一樣的資源目錄有不一樣的值測試
density | 0.75 | 1 | 1.5 | 2 | 3 3.5 | 4 |
---|---|---|---|---|---|---|
densityDpi | 120 | 160 | 240 | 320 | 480 | 560 |
DpiFolder | ldpi | mdpi | hdpi | xhdpi | xxhdpi | xxxhdpi |
能夠得出如下結論優化
因此Bitmap在資源目錄中的計算方式爲編碼
Bitmap內存佔用 ≈ 像素數據總大小 = 圖片寬 × 圖片高× (當前設備密度dpi/圖片所在文件夾對應的密度dpi)^2 × 每一個像素的字節大小
複製代碼
下面咱們一個個的來說這些優化
Android 中提供一下幾種編碼
也便是說咱們能夠經過改變圖片格式,來改變每一個像素佔用字節數,來改變佔用的內存,看下面代碼
BitmapFactory.Options options = new BitmapFactory.Options();
//不獲取圖片,不加載到內存中,只返回圖片屬性
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(photoPath, options);
//圖片的寬高
int outHeight = options.outHeight;
int outWidth = options.outWidth;
Log.d("mmm", "圖片寬=" + outWidth + "圖片高=" + outHeight);
//圖片格式壓縮
options.inPreferredConfig = Bitmap.Config.RGB_565;
options.inJustDecodeBounds = false;
Bitmap bitmap = BitmapFactory.decodeFile(photoPath, options);
float bitmapsize = getBitmapsize(bitmap);
Log.d("mmm","壓縮後:圖片佔內存大小" + bitmapsize + "MB / 寬度=" + bitmap.getWidth() + "高度=" + bitmap.getHeight());
複製代碼
看下log
07-09 11:10:46.042 15312-15312/com.example.jh.rxhapp D/mmm: 原圖:圖片佔內存大小=45.776367MB / 寬度=4000高度=3000
07-09 11:10:46.043 15312-15312/com.example.jh.rxhapp D/mmm: 圖片寬=4000圖片高=3000
07-09 11:10:46.367 15312-15312/com.example.jh.rxhapp D/mmm: 壓縮後:圖片佔內存大小22.887695MB / 寬度=4000高度=3000
複製代碼
寬高沒變,咱們改變了圖片的格式,從ARGB_8888 變成了RGB_565 ,像素佔用字節數減小了通常,根據log 內存也減小了一半,這種方式可行
注意:因爲ARGB_4444的畫質慘不忍睹,通常假如對圖片沒有透明度要求的話,能夠改爲RGB_565,相比ARGB_8888將節省一半的內存開銷。
咱們瞭解到了計算bitmap的佔用內存的方法 ,是以bitmap的寬高和每一個像素佔用的字節數決定的,下面咱們分別講一下倆個 的概念
1 bitmap的寬高
顧名思義,圖片的大小就是bitmap的寬高,按公式咱們能夠縮減bitmap的寬高來達到壓縮圖片佔用內存的目的,看下面代碼,以縮減寬高來達到壓縮的目的
BitmapFactory.Options options = new BitmapFactory.Options();
//不獲取圖片,不加載到內存中,只返回圖片屬性
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(photoPath, options);
//圖片的寬高
int outHeight = options.outHeight;
int outWidth = options.outWidth;
Log.d("mmm", "圖片寬=" + outWidth + "圖片高=" + outHeight);
//計算採樣率
int i = utils.computeSampleSize(options, -1, 1000 * 1000);
//設置採樣率,不能小於1 假如是2 則寬爲以前的1/2,高爲以前的1/2,一共縮小1/4 一次類推
options.inSampleSize = i;
Log.d("mmm", "採樣率爲=" + i);
//圖片格式壓縮
//options.inPreferredConfig = Bitmap.Config.RGB_565;
options.inJustDecodeBounds = false;
Bitmap bitmap = BitmapFactory.decodeFile(photoPath, options);
float bitmapsize = getBitmapsize(bitmap);
Log.d("mmm","壓縮後:圖片佔內存大小" + bitmapsize + "MB / 寬度=" + bitmap.getWidth() + "高度=" + bitmap.getHeight());
複製代碼
看下打印信息
07-09 11:02:11.714 8010-8010/com.example.jh.rxhapp D/mmm: 原圖:圖片佔內存大小=45.776367MB / 寬度=4000高度=3000
07-09 11:02:11.715 8010-8010/com.example.jh.rxhapp D/mmm: 圖片寬=4000圖片高=3000
07-09 11:02:11.715 8010-8010/com.example.jh.rxhapp D/mmm: 採樣率爲=4
07-09 11:02:11.944 8010-8010/com.example.jh.rxhapp D/mmm: 壓縮後:圖片佔內存大小1.4296875MB / 寬度=1000高度=750
複製代碼
這種咱們根據BitmapFactory 的採樣率進行壓縮 設置採樣率,不能小於1 假如是2 則寬爲以前的1/2,高爲以前的1/2,一共縮小1/4 一次類推,咱們看到log ,確實起到了壓縮的目的
圖片複用指的是inBitmap這個屬性
這個屬性又什麼做用?
不使用這個屬性,你加載三張圖片,系統會給你分配三分內存空間,用於分別儲存這三張圖片
若是用了inBitmap這個屬性,加載三張圖片,這三張圖片會指向同一塊內存,而不用開闢三塊內存空間
inBitmap的限制
Android 系統爲了進程間共享數據開闢的一塊內存區域,因爲這塊區域不受應用的Head的大小限制,至關於能夠繞開oom,FaceBook的Fresco首次應用到實際中
限制:5.0之後就限制了匿名共享內存的使用
2.3- | 3.0-4.4 | 5.0-7.1 | 8.0 |
---|---|---|---|
Bitmap對象 | java Heap | java Heap | java Heap |
像素數據 | Native Heap | java Heap | Native Heap |
遷移緣由 | - | 解決Native Bitmap內存泄露 | 共享整個系統的內存減小OOM |
8.0Bitmap的像素數據存儲在Native,爲何又改成Native存儲呢?
由於8.0共享了整個系統的內存,測試8.0手機若是一直建立Bitmap,若是手機內存有1G,那麼你的應用加載1G也不會oom
咱們能夠利用LRU開管理Bitmap,給他設置內存最大值,及時回收
圖片的壓縮通常有倆種
bitmap.compress(Bitmap.CompressFormat.JPEG, 20,
new FileOutputStream("sdcard/result.jpg"));
複製代碼
這個你們用該都用過,這個壓縮是保持像素的前提下改變圖片的位深及透明度,來達到壓縮的目的,不過這種壓縮不會改變圖片在內存中的帶下,並且這種壓縮會致使圖片的失真,可是有沒有壓縮到100k左右,還不失真的方法?
推薦看下這個博客 www.jianshu.com/p/06a1cae9c…
若是有需求,要求咱們既不能壓縮圖片,又不能發生oom怎麼辦,這種狀況咱們須要加載圖片的一部分區域來顯示,下面咱們來了解一下BitmapRegionDecoder這個類,加載圖片的一部分區域,他的用法很簡單
//支持傳入圖片的路徑,流和圖片修飾符等
BitmapRegionDecoder mDecoder = BitmapRegionDecoder.newInstance(path, false);
//須要顯示的區域就有由rect控制,options來控制圖片的屬性
Bitmap bitmap = mDecoder.decodeRegion(mRect, options);
複製代碼
因爲要顯示一部分區域,因此要有手勢的控制,方便上下的滑動,須要自定義控件,而自定義控件的思路也很簡單 1 提供圖片的入口 2 重寫onTouchEvent, 根據手勢的移動更新顯示區域的參數 3 更新區域參數後,刷新控件從新繪製
下面是完整代碼
public class BigImageView extends View {
private BitmapRegionDecoder mDecoder;
private int mImageWidth;
private int mImageHeight;
//圖片繪製的區域
private Rect mRect = new Rect();
private static final BitmapFactory.Options options = new BitmapFactory.Options();
static {
options.inPreferredConfig = Bitmap.Config.RGB_565;
}
public BigImageView(Context context) {
super(context);
init();
}
public BigImageView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public BigImageView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
}
/** * 自定義view的入口,設置圖片流 * * @param path 圖片路徑 */
public void setFilePath(String path) {
try {
//初始化BitmapRegionDecoder
mDecoder = BitmapRegionDecoder.newInstance(path, false);
BitmapFactory.Options options = new BitmapFactory.Options();
//即是隻加載圖片屬性,不加載bitmap進入內存
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(path, options);
//圖片的寬高
mImageWidth = options.outWidth;
mImageHeight = options.outHeight;
Log.d("mmm", "圖片寬=" + mImageWidth + "圖片高=" + mImageHeight);
requestLayout();
invalidate();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//獲取本view的寬高
int measuredHeight = getMeasuredHeight();
int measuredWidth = getMeasuredWidth();
//默認顯示圖片左上方
mRect.left = 0;
mRect.top = 0;
mRect.right = mRect.left + measuredWidth;
mRect.bottom = mRect.top + measuredHeight;
}
//第一次按下的位置
private float mDownX;
private float mDownY;
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mDownX = event.getX();
mDownY = event.getY();
break;
case MotionEvent.ACTION_MOVE:
float moveX = event.getX();
float moveY = event.getY();
//移動的距離
int xDistance = (int) (moveX - mDownX);
int yDistance = (int) (moveY - mDownY);
Log.d("mmm", "mDownX=" + mDownX + "mDownY=" + mDownY);
Log.d("mmm", "movex=" + moveX + "movey=" + moveY);
Log.d("mmm", "xDistance=" + xDistance + "yDistance=" + yDistance);
Log.d("mmm", "mImageWidth=" + mImageWidth + "mImageHeight=" + mImageHeight);
Log.d("mmm", "getWidth=" + getWidth() + "getHeight=" + getHeight());
if (mImageWidth > getWidth()) {
mRect.offset(-xDistance, 0);
checkWidth();
//刷新頁面
invalidate();
Log.d("mmm", "刷新寬度");
}
if (mImageHeight > getHeight()) {
mRect.offset(0, -yDistance);
checkHeight();
invalidate();
Log.d("mmm", "刷新高度");
}
break;
case MotionEvent.ACTION_UP:
break;
default:
}
return true;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Bitmap bitmap = mDecoder.decodeRegion(mRect, options);
canvas.drawBitmap(bitmap, 0, 0, null);
}
/** * 確保圖不劃出屏幕 */
private void checkWidth() {
Rect rect = mRect;
int imageWidth = mImageWidth;
int imageHeight = mImageHeight;
if (rect.right > imageWidth) {
rect.right = imageWidth;
rect.left = imageWidth - getWidth();
}
if (rect.left < 0) {
rect.left = 0;
rect.right = getWidth();
}
}
/** * 確保圖不劃出屏幕 */
private void checkHeight() {
Rect rect = mRect;
int imageWidth = mImageWidth;
int imageHeight = mImageHeight;
if (rect.bottom > imageHeight) {
rect.bottom = imageHeight;
rect.top = imageHeight - getHeight();
}
if (rect.top < 0) {
rect.top = 0;
rect.bottom = getHeight();
}
}
}
複製代碼
代碼行有註釋,應該很好理解了
參考https://www.jianshu.com/p/3f6f6e4f1c88 www.jianshu.com/p/e49ec7d05…