Android large image loading optimization solution to avoid program OOM

We often use many pictures when writing Android programs. Different pictures always have different shapes and sizes, but in most cases, these pictures will be larger than the size required by our program. For example, Weibo long pictures, posters, etc. So we have to partially display the image.

Analysis of basic requirements and principles of large image loading

Insert image description here
Basic requirements : When we have a large green-sized picture, we need to display it in the size of the blue part. Generally, during our sliding process, we can only see the blue part of the picture, and the part below the blue part. You can see it by swiping down.
Principle analysis : This involves regional loading . Since our human eyes can only see pictures that fill the screen size of the mobile phone, the part below the blue part cannot be seen. This means that every time we load a picture, we only need to load it into The area we can see is sufficient, and the area we cannot see will not be loaded.
Insert image description here

Assuming that the height of our picture is 5 times that of the mobile phone, then the first time we load it is actually 1/5 of the picture, and no matter we continue to scroll down, we will load 1/5 of the picture every time, then we can save 4/5 of the memory. .
So here’s the problem? How do we achieve region loading and memory reuse ?
Insert image description here

For example, if we say that the picture is divided into 5 parts, we load 1/5 of the memory every time. In order to ensure that 1/5 of the memory is loaded every time, if we slide to the second area, we will still load the first area. area, otherwise it would be equivalent to loading all 5 copies of memory into it, which may cause OOM.

Large image loading basic api analysis

        //设置一个矩形区域(可以理解为矩形区域框定)
      Rect  mRect = new Rect();
        //用于内存复用(Google提供的对内存复用设置一些参数,比如设置编码格式)
      BitmapFactory.Options  mOptions = new BitmapFactory.Options();
        //手势支持
      GestureDetector  mGestureDetector = new GestureDetector(context, this);
        //滚动类
      Scroller  mScroller = new Scroller(context);
       //触摸时触发事件,比如触碰就停止屏幕滚动
      setOnTouchListener(this);

Insert image description here
If we want to convert the green-sized original image into a mobile phone screen-sized blueprint, we need to scale the image, and we need to obtain relevant information such as the image size. But a problem arises. When we obtain the image width and height information, we cannot load the entire image, otherwise our memory reuse will be meaningless. This is when mOptions is used.

//inJustDecodeBounds方法,只加载边缘区域来获取图片宽高
        mOptions.inJustDecodeBounds=true;
            //将is传进去解码就能获取到图片的宽和高
        BitmapFactory.decodeStream(is,null,mOptions);
          //拿到宽和高
        mImageWidth = mOptions.outWidth;
        mImageHeight = mOptions.outHeight;
          //开启内存复用
        mOptions.inMutable=true;
        //设置图片格式:rgb565
        mOptions.inPreferredConfig= Bitmap.Config.RGB_565;
         //用完需要关闭
        mOptions.inJustDecodeBounds=false;

In this way, the width and height of the image can be obtained without loading the entire image into memory.

The relationship between image encoding format and memory usage

Insert image description here

For example, Glide uses RGB_565, and Picasso uses ARGB_8888.
When we infinitely zoom in on a picture, you will find that it is composed of n pixels, and each pixel has its own color. For example, the picture below is There are black, yellow and light yellow.
Insert image description here
When you retract it, you will find a normal picture. You can find that the picture is composed of pixels. The pixels are composed of RGB,
Insert image description here
three primary colors (red, green and blue)
and ARGB_8888 represents the pixels in the picture. Pixels have four color channels: A, R, G, and B. Each channel occupies 8 bits of memory, a total of 32 bits, which is equivalent to 4 bytes. That is to say, each pixel occupies 4 bytes.
Compared with ARGB_8888, RGB_565 lacks the A transparent channel, which means that the R channel occupies 5 bits, the G channel occupies 6 bits, and the B channel occupies 5 bits, a total of 16 bits, which is equivalent to 2 bytes, that is, each pixel Occupies 2 bytes. In this case, the memory can be reduced by half compared to the above.

Image initialization display implementation of large image loading

public class MainActivity extends Activity {
    
    

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    
    
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        BigView bigView= findViewById(R.id.bigView);
        InputStream is=null;
        try {
    
    
            is= getResources().getAssets().open("test.jpg");
            bigView.setImage(is);
        } catch (IOException e) {
    
    
            e.printStackTrace();
        }
    }
}
public class BigView extends View implements GestureDetector.OnGestureListener, View.OnTouchListener {
    
    

    private Rect mRect;
    private BitmapFactory.Options mOptions;
    private GestureDetector mGestureDetector;
    private Scroller mScroller;
    private int mImageWidth;
    private int mImageHeight;
    private BitmapRegionDecoder mDecoder;
    private int mViewWidth;
    private int mViewHeight;
    private Bitmap mBitmap;
    private float mScaleX;
    private float mScaleY;

    public BigView(Context context) {
    
    
        this(context,null);
    }

    public BigView(Context context, @Nullable AttributeSet attrs) {
    
    
        this(context, attrs,0);
    }

    public BigView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    
    
        this(context, attrs, defStyleAttr,0);
    }

    public BigView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    
    
        super(context, attrs, defStyleAttr, defStyleRes);
        //第一步 设置BigView需要的成员变量
        //设置一个矩形区域(矩形区域框定)
        mRect = new Rect();
        //用于内存复用(设置编码格式)
        mOptions = new BitmapFactory.Options();
        //手势支持
        mGestureDetector = new GestureDetector(context, this);
        //滚动类
        mScroller = new Scroller(context);
        //触摸时触发事件
        setOnTouchListener(this);
    }


    //第二步设置图片
    public void setImage(InputStream is){
    
    
        //获取图片的宽和高
        //此时不能将整张图片加载进来,这样内存复用无意义,需要使用inJustDecodeBounds方法,只加载部分区域来获取图片宽高
        mOptions.inJustDecodeBounds=true;
        //将is传进去解码就能获取到图片的宽和高
        BitmapFactory.decodeStream(is,null,mOptions);

        mImageWidth = mOptions.outWidth;
        mImageHeight = mOptions.outHeight;

        //开启内存复用
        mOptions.inMutable=true;

        //设置图片格式:rgb565
        mOptions.inPreferredConfig= Bitmap.Config.RGB_565;

        //用完需要关闭
        mOptions.inJustDecodeBounds=false;

        //区域解码器
        try {
    
    
            mDecoder = BitmapRegionDecoder.newInstance(is, false);
        } catch (IOException e) {
    
    
            e.printStackTrace();
        }
        //去调用onMeasure方法
        requestLayout();
    }

    //第三步 加载图片
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    
    
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        mViewWidth = getMeasuredWidth();
        mViewHeight = getMeasuredHeight();
        //绑定图片加载区域
        //上边界为0
        mRect.top=0;
        //左边界为0
        mRect.left=0;
        //右边界为图片的宽度
        mRect.right=mImageWidth;
        //下边界为view的高度,在这里相当于手机的高度
        mRect.bottom=mViewHeight;
    }

    //第四步 画图
    @Override
    protected void onDraw(Canvas canvas) {
    
    
        super.onDraw(canvas);
        if(mDecoder==null){
    
    
            return;
        }
        //内存复用
        //复用inBitmap这块的内存(每次滚动重新绘制都会复用这块内存,达到内存复用)
        mOptions.inBitmap=mBitmap;
       
        mBitmap=mDecoder.decodeRegion(mRect,mOptions);
     
        //计算缩放因子
        mScaleX = mViewWidth / (float) mImageWidth;
   
        mScaleY = mViewHeight / (float) mImageHeight;
      
        //得到矩阵缩放
        Matrix matrix = new Matrix();
        matrix.setScale(mScaleX, mScaleX);//如果matrix.setScale(mScaleX, mScaleY)则图片会在充满在当前的view的x和y轴
        canvas.drawBitmap(mBitmap,matrix,null);
    }

    //第五步 处理点击事件
    @Override
    public boolean onTouch(View v, MotionEvent event) {
    
    
        //将Touch事件传递给手势
        return true;
    }
  
    @Override
    public void onShowPress(MotionEvent e) {
    
    

    }

    @Override
    public boolean onSingleTapUp(MotionEvent e) {
    
    
        return false;
    }

    @Override
    public void onLongPress(MotionEvent e) {
    
    

    }
}

Implementation of image scrolling function for large image loading

  //第五步 处理点击事件
    @Override
    public boolean onTouch(View v, MotionEvent event) {
    
    
        //将Touch事件传递给手势
        return mGestureDetector.onTouchEvent(event);
    }

    //第六步 处理手势按下事件

    @Override
    public boolean onDown(MotionEvent e) {
    
    
        //如果滑动没有停止就 强制停止
        if(!mScroller.isFinished()){
    
    
            mScroller.forceFinished(true);
        }
        //将事件进行传递,接收后续事件
        //因为在GestureDetector中,onDown方法是用于监听手指按下事件的,如果不返回true消费该事件,
        // GestureDetector就不会将后续的事件传递给其他的方法进行处理,
        // 包括滑动事件。因此,如果要实现按下手指后进行滑动图片的效果,需要在onDown方法中返回true进行消费。
        return true;
    }

    //第七步 处理滑动事件(手势)指手势的拖动
    //e1 开始事件
    //e2 即时事件也就是滑动时
    @Override
    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
    
    
        //上下滑动时,直接改变Rect的显示区域
        mRect.offset(0,(int) distanceY);//上下滑动只需要改变Y轴
        //判断到顶和到底的情况
        if(mRect.bottom>mImageHeight){
    
    //滑到最底
            mRect.bottom=mImageHeight;
            mRect.top=mImageHeight-mViewHeight;
        }
        if(mRect.top<0){
    
    //滑到最顶
            mRect.top=0;
            mRect.bottom=mViewHeight;
        }
        invalidate();
        return true;
    }

Implementation of image inertial scrolling function for large image loading

    //第八步 处理惯性问题(手势)指手势的滑动
    @Override
    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
    
    
        //velocityY表示Y轴的惯性值,startX和startY为滑动的开始位置,minY和maxY为滑动距离的最小值和最大值
        mScroller.fling(0,mRect.top,0,(int) -velocityY,0,0,0,mImageHeight-mViewHeight);
        return false;
    }

    //该方法可以获取当前的滚动值
    @Override
    public void computeScroll() {
    
    
        super.computeScroll();
        //如果没有滚动,直接返回即可
        if(mScroller.isFinished()){
    
    
            return;
        }
        //如果已经滚动到新位置返回true
        if(mScroller.computeScrollOffset()){
    
    
            mRect.top=mScroller.getCurrY();
            mRect.bottom=mRect.top+mViewHeight;//底部边框等于更新的top位置加上
        }
        invalidate();
    }

Please add image description

project address

github click to view

Guess you like

Origin blog.csdn.net/ChenYiRan123456/article/details/131310826