王学岗性能优化7——Bitmap内存管理

我们在布局中创建一个ListView,listView的item只有一张图片
第一次优化
我们创建一个ListView,当我们上下滑动item的时候发现native内存一直在增加,只要是新出现的图片,都会开辟新的内存地址。所以我们需要复用这些内存块。Android8.0后这些内存是放在native内存而不是Java堆内存

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
   >
    <ListView
        android:id="@+id/listView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
         />
</FrameLayout>

这是item的布局

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
   >
   <ImageView
       android:id="@+id/iv"
       android:layout_width="80dp"
       android:layout_height="80dp" />
</FrameLayout>

这是MainActivity的代码

  ListView listView = findViewById(R.id.listView);
        listView.setAdapter(new MyAdapter(this));

这是上一节课的ImageResize,我们让它具备异变功能。

package com.example.administrator.lsn7_demo;

import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;


public class ImageResize {

    /**
     *  缩放bitmap
     * @param context
     * @param id
     * @param maxW
     * @param maxH
     * @param reusable增加异变功能
     * @return
     */
    public static Bitmap resizeBitmap(Context context,int id,int maxW,int maxH,boolean hasAlpha,Bitmap reusable){
        Resources resources = context.getResources();
        BitmapFactory.Options options = new BitmapFactory.Options();
        // 只解码出 outxxx参数 比如 宽、高
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(resources,id,options);
        //根据宽、高进行缩放
        int w = options.outWidth;
        int h = options.outHeight;
        //设置缩放系数
        options.inSampleSize = calcuteInSampleSize(w,h,maxW,maxH);
        if (!hasAlpha){
            options.inPreferredConfig = Bitmap.Config.RGB_565;
        }
        options.inJustDecodeBounds = false;
        //设置成能复用
        options.inMutable=true;
        options.inBitmap=reusable;
        return BitmapFactory.decodeResource(resources,id,options);
    }

    /**
     * 计算缩放系数
     * @param w
     * @param h
     * @param maxW
     * @param maxH
     * @return 缩放的系数
     */
    private static int calcuteInSampleSize(int w,int h,int maxW,int maxH) {
        int inSampleSize = 1;
        if (w > maxW && h > maxH){
            inSampleSize = 2;
            //循环 使宽、高小于 最大的宽、高
            while (w /inSampleSize > maxW && h / inSampleSize > maxH){
                inSampleSize *= 2;
            }
        }
        return inSampleSize;
    }
}

我们在Adapter中设置图片

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        ViewHolder holder;
        if (null == convertView) {
            convertView = LayoutInflater.from(context).inflate(R.layout.item, parent, false);
            holder = new ViewHolder(convertView);
            convertView.setTag(holder);
        } else {
            holder = (ViewHolder) convertView.getTag();
        }

    
         Bitmap bitmap = ImageResize.resizeBitmap(context, R.mipmap.wyz_p,
                 80, 80, false);
        holder.iv.setImageBitmap(bitmap);
        return convertView;
    }

第一次优化详解
我们现在向下滑动ListView,会发现它的Native层内存一直在增长,但是Java内存却没有增长,Android 8.0之前,bitmap的管理是放在Java堆的。现在的话这些内存是直接开在native层的。现在我们写内存管理框架就要处理native内存的问题。比如内存释放就要自己去处理。我们现在向上滑动ListView,内存还是在一直增加,并没有释放掉。因为不管你是上滑还是下滑,只要是新出现的图片,它都会开辟新的内存地址。也许你会说ListView的convertView会做优化,没错,但是我们使用图片并不全是在ListView中。比如其它的地方我们需要更新Bitmap,如果我们不做任何处理的话,这些更新会在内存中不断的开辟内存块。
怎么解决呢?我们要使用inBitmap属性,假如我们现在需要加载三张bitmap,第一张bitmap加载后我们在删除第一张Bitmap,加载第二张图片,这时候如果第二张图片不比第一张图片大,我们就可以复用第一张图片的内存块。第三张图片也可以这么加载。这就是内存复用
下面我们看下在代码中如何实现内存复用,这与Glide原理相同。我们可以把这些代码加入到ImageResize 中。

 Bitmap bitmap=BitmapFactory.decodeResource(getResources());
        for(int i=0;i<100;i++){
         bitmap=BitmapFactory.decodeResource(getResources());
        }

上面代码我们加载一百张图片,我们不使用内存池,即我们不复用内存块,我们可以发现native内存里显示占用了20MB
下面的代码我们使用复用池技术

  BitmapFactory.Options options=new BitmapFactory.Options();
        //如果要复用,需要设计成异变
        options.inMutable=true;
        //这样设置图片就具备了异变功能,可以使用inBitmap功能了
        Bitmap bitmap=BitmapFactory.decodeResource(getResources(),R.mipmap.wyz_p,options);
        for(int i=0;i<100;i++){
        //以后使用的时候它会使用相同的一块内存
       // bitmap所指向的内存块是可以复用的。我在加载新图片到这个内存的时候,
       //有了这个选项它就可以用了
            options.inBitmap=bitmap;
                  bitmap=BitmapFactory.decodeResource(getResources(),R.mipmap.wyz_p,options);
        }

运行结果显示 现在加载100张图片需要的内存要少于20MB

第二次优化
申请图片第一次去内存缓存,第二次去磁盘缓存,第三次去网络获取。内存缓存和磁盘缓存可以使用LRU算法,在LRU算法中,所有的图片都是一个双向链表。我们使用哪一张图片,哪一张图片就放到链表的最前面,当有新的图片放入的时候,会把新的图片放到链表最前面,同时删除链表最后一张图片。
内存缓存我们是有这样的API可以使用,做磁盘缓存则需要自己下载库这是下载地址注意下载发布版本。
我们解压这个包,在src中找到这三个文件。放到我们的项目中,如下图
在这里插入图片描述
现在我们可以使用磁盘缓存了,我们自己做一个缓存机制;
我们的缓存包括内存缓存和磁盘缓存,我画一张图来解释下内存缓存
在这里插入图片描述
新建一个ImageCache,来管理缓存

package com.example.administrator.lsn7_demo;

import android.app.ActivityManager;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Build;
import android.util.LruCache;

import com.example.administrator.lsn7_demo.disk.DiskLruCache;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

/**
 * 管理内存中的图片
 */
public class ImageCache {

    private static ImageCache instance;
    private Context context;
    //内存缓存
    private LruCache<String,Bitmap> memoryCache;
    //磁盘缓存
    private DiskLruCache diskLruCache;
    //用于内存复用
    BitmapFactory.Options options=new BitmapFactory.Options();

    /**
     * 定义一个复用沲
     */
    public static Set<WeakReference<Bitmap>> reuseablePool;

//单例模式
    public static ImageCache getInstance(){
        if(null==instance){
            synchronized (ImageCache.class){
                if(null==instance){
                    instance=new ImageCache();
                }
            }
        }
        return instance;
    }

    //引用队列,在复用池中被释放的东西拉到这个队列里面,在引用队列俩面
    //开辟一个死循环的线程,把这些图片一一删除
    ReferenceQueue referenceQueue;
    Thread clearReferenceQueue;
    //线程开关
    boolean shutDown;

    private ReferenceQueue<Bitmap> getReferenceQueue(){
        if(null==referenceQueue){
            //当弱用引需要被回收的时候,会进到这个队列中
            referenceQueue=new ReferenceQueue<Bitmap>();
            //单开一个线程,去扫描引用队列中GC扫到的内容,交到native层去释放,我自己调用
            //recycle()方法释放,这比GC要快
            clearReferenceQueue=new Thread(new Runnable() {
                @Override
                public void run() {
                    while(!shutDown){
                        try {
                            //remove是阻塞式的,如果这个队列是空的,代码会一直停留在这一行
                            Reference<Bitmap> reference=referenceQueue.remove();
                            Bitmap bitmap=reference.get();
                            if(null!=bitmap && !bitmap.isRecycled()){
                                bitmap.recycle();
                            }
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            });
            clearReferenceQueue.start();
        }
        return referenceQueue;
    }

    //dir是用来存放图片文件的路径,磁盘缓存要存放的目录
    public void init(Context context,String dir){
        this.context=context.getApplicationContext();

        //复用池,带锁的set集合
        reuseablePool=Collections.synchronizedSet(new HashSet<WeakReference<Bitmap>>());

        ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
        //获取程序最大可用内存 单位是M
        int memoryClass=am.getMemoryClass();
        //这是Android提供好的API,参数表示能够缓存的内存最大值  单位是byte,一般情况下是能使用的内存的1/8
        memoryCache=new LruCache<String,Bitmap>(memoryClass/8*1024*1024){
            /**
             * @return value占用的内存大小
             */
            @Override
            protected int sizeOf(String key, Bitmap value) {
                //1,19之前   复用是必需同等大小,才能复用  inSampleSize=1
                //2,19之后不同,比如第一次使用图片显示的时候,他是a大小,下一次复用内存的时候,只
                //复用了a-10;value.getByteCount()只能得到a-10的内存而不是a.
                //android 19后有一个API可以得到整块内存空间。我们应该返回的内存a,而不是a-10
                if(Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT){
                    //bitmaap在复用的时候19以后与以前不同
                    return value.getAllocationByteCount();
                }
                //取到的是图片大小而不是内存大小
                return value.getByteCount();
            }
            /**
             * 1,当lru满了,bitmap从lru中移除对象时(就是上文提到的链表),会回调
             * 2,假设内存大小是10,并且已经放满,我们现在放入第十一张图片,原来的十张图片就会删除链表的最后一位
             * 删除掉的这张图片就可以使用oldValue进行接收.
             * 3,这些被删除的图片是在native层进行回收的。我们这里可以设计一个复用池,在内存中找不到的可以再
             * native内存中寻找。相当于做了两级缓存。我们可以使用弱引用缓存native层里的东西
             */
            @Override
            protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
                //
                if(oldValue.isMutable()){//如果是设置成能复用的内存块,拉到java层来管理
                    //3.0以下   Bitmap   native
                    //3.0以后---8.0之前  java
                    //8。0开始      native
                    //把这些图片放到一个复用沲中,复用池里面保存被删除的东东
                    reuseablePool.add(new WeakReference<Bitmap>(oldValue,referenceQueue));
                }else{
                    //释放bitmap,图片就找不到啦
                    oldValue.recycle();
                }


            }
        };
          //做磁盘缓存
        //参数:1,缓存的目录,2,app的版本,3,valueCount:表示一个key对应valueCount个文件,4,磁盘缓存的大小
       try {
           diskLruCache = DiskLruCache.open(new File(dir), BuildConfig.VERSION_CODE, 1, 10 * 1024 * 1024);
       }catch(Exception e){
           e.printStackTrace();
       }

       getReferenceQueue();
    }

    /**
     * 往内存中存放图片
     * @param key
     * @param bitmap
     */
    public void putBitmapToMemeory(String key,Bitmap bitmap){
        memoryCache.put(key,bitmap);
    }
    //从内存中获取图片
    public Bitmap getBitmapFromMemory(String key){
        return memoryCache.get(key);
    }
    //清空内存
    public void clearMemoryCache(){
        memoryCache.evictAll();
    }

      /**
     * 获取复用池中的内容
     * @param w 图片的宽高,从复用池取东西,宽高只能比原图小
     * @param h
     * @param inSampleSize 必须大于等于1
     * @return
     */
    public Bitmap getReuseable(int w,int h,int inSampleSize){
        //版本在3.0以前
        if(Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB){
            return null;
        }
        Bitmap reuseable=null;
        //从复用池里面迭代
        Iterator<WeakReference<Bitmap>> iterator = reuseablePool.iterator();
        while(iterator.hasNext()){
            //这里是弱引用,所以用get方法
            Bitmap bitmap=iterator.next().get();
            //我们每从复用池拿一个就把它从复用池删除掉,删掉的那个加入到LRU内存中
            if(null!=bitmap){
                //不为空表示可以复用
                //检测是否能用
                if(checkInBitmap(bitmap,w,h,inSampleSize)){
                    reuseable=bitmap;
                    iterator.remove();
                    break;
                }else{
                    iterator.remove();
                }
            }
        }
        return reuseable;

    }

    /**
     * 检测版本是否能用
     * @param bitmap
     * @param w
     * @param h
     * @param inSampleSize
     * @return
     */
    private boolean checkInBitmap(Bitmap bitmap, int w, int h, int inSampleSize) {
        //版本在19以前
        if(Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT){
            return bitmap.getWidth()==w && bitmap.getHeight()==h && inSampleSize==1;
        }
        //19版本以后,进行缩放
        if(inSampleSize>=1){
            w/=inSampleSize;
            h/=inSampleSize;
        }
        //宽X高X像素的字节个数
        int byteCount=w*h*getPixelsCount(bitmap.getConfig());
        //小于bitmap的存储空间就返回为真
        //只有我现在申请的内存块比我图片像素占得更多,才可以复用,才能返回为真
        return byteCount<=bitmap.getAllocationByteCount();
    }

    private int getPixelsCount(Bitmap.Config config) {
        if(config==Bitmap.Config.ARGB_8888){
            return 4;
        }
        return 2;
    }


    //磁盘缓存的处理
    /**
     * 加入磁盘缓存
     */
    public void putBitMapToDisk(String key,Bitmap bitmap){
        //snapshot进行编辑提交
        DiskLruCache.Snapshot snapshot=null;
        OutputStream os=null;
        try {

            snapshot=diskLruCache.get(key);
            //如果缓存中已经有这个文件  不理他
            if(null==snapshot){
                //为空表示这个key不存在,也就是没有这个文件,那就生成这个文件
                DiskLruCache.Editor editor=diskLruCache.edit(key);
                if(null!=editor){
                    //0表示文件从头开始
                    os=editor.newOutputStream(0);
                    //压缩bitmap
                    bitmap.compress(Bitmap.CompressFormat.JPEG,50,os);
                    //压缩的图片存放到磁盘里
                    editor.commit();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if(null!=snapshot){
                snapshot.close();
            }
            if(null!=os){
                try {
                    os.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    /**
     * 从磁盘缓存中取
     */
    public Bitmap getBitmapFromDisk(String key,Bitmap reuseable){
        DiskLruCache.Snapshot snapshot=null;
        Bitmap bitmap=null;
        try {
            snapshot=diskLruCache.get(key);
            if(null==snapshot){
                //如果为空,说明缓存中没有图片,我们就从网络中加载
                return null;
            }
            //获取文件输入流,读取bitmap
            InputStream is=snapshot.getInputStream(0);
            //解码个图片,写入
            //可以异变的
            options.inMutable=true;
            options.inBitmap=reuseable;
            bitmap=BitmapFactory.decodeStream(is,null,options);
            if(null!=bitmap){
                //之所以从磁盘中获取,是因为内存中读取不到,所以内存中也存一份
                memoryCache.put(key,bitmap);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally{
            if(null!=snapshot){
                snapshot.close();
            }
        }
        return bitmap;
    }

}

Adapter中使用

package com.example.administrator.lsn7_demo;

import android.content.Context;
import android.graphics.Bitmap;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;


public class MyAdapter extends BaseAdapter {
    private Context context;

    public MyAdapter(Context context) {
        this.context = context;
    }

    @Override
    public int getCount() {
        return 999;
    }

    @Override
    public Object getItem(int position) {
        return null;
    }

    @Override
    public long getItemId(int position) {
        return 0;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        ViewHolder holder;
        if (null == convertView) {
            convertView = LayoutInflater.from(context).inflate(R.layout.item, parent, false);
            holder = new ViewHolder(convertView);
            convertView.setTag(holder);
        } else {
            holder = (ViewHolder) convertView.getTag();
        }

        //第一次优化
//        Bitmap bitmap = ImageResize.resizeBitmap(context, R.mipmap.wyz_p,
//                80, 80, false);
        //先从内存中读取,我们就用index作键
        Bitmap bitmap=ImageCache.getInstance().getBitmapFromMemory(String.valueOf(position));
        if(null==bitmap){
            //如果内存没数据,就去复用池找
            //大小一定小于等于原来的,我们原来是80X80,insampleSize大于等于一
            //reuseable能复用的内存快,不是图片
            //复用池里不是图片,是能复用的内存块
            Bitmap reuseable=ImageCache.getInstance().getReuseable(60,60,1);

            //从磁盘找
            bitmap = ImageCache.getInstance().getBitmapFromDisk(String.valueOf(position),reuseable);
            //如果磁盘中也没缓存,就从网络下载
            if(null==bitmap){
                //网络中下载图片
                bitmap=ImageResize.resizeBitmap(context,R.mipmap.wyz_p,80,80,false,reuseable);
                //加载到内存
                ImageCache.getInstance().putBitmapToMemeory(String.valueOf(position),bitmap);
                //加载到磁盘
                ImageCache.getInstance().putBitMapToDisk(String.valueOf(position),bitmap);
                Log.i("jett","从网络加载了数据");
            }else{
                Log.i("jett","从磁盘中加载了数据");
            }

        }else{
            Log.i("jett","从内存中加载了数据");
        }



        holder.iv.setImageBitmap(bitmap);
        return convertView;
    }

    class ViewHolder {
        ImageView iv;

        ViewHolder(View view) {
            iv = view.findViewById(R.id.iv);
        }
    }
}

在MainActivity中初始化

  ImageCache.getInstance().init(this,Environment.getExternalStorageDirectory()+"/dn");

我们第一次运行向下滑动直到底部,log打印输出从网络中获取,
我们再向上滑动,log打印输出从内存中获取,native内存也没有增长。、
我们关闭app后再打开,向下滑动,log打印输出从磁盘中获取
附源码本章源码

猜你喜欢

转载自blog.csdn.net/qczg_wxg/article/details/90110930