Glide源码分析和低内存优化

### Glide加载图片的应用场景
    App在使用的过程中,会经常需要加载图片。

        1.从网络中加载图片,一般都是从服务器端获取图片的文件流,在通过 BitmapFactory.decodeStream(inputStream)来加载 bitmap。

        2.压缩网络图片,网络加载少量的图片不会出现问题,但是若短时间内加载大量的图片,短时间内占用大量的内存,造成内存抖动,甚至稻城 OOM(内存溢出)

        3.变换图片,剪切切割圆角,旋转,高斯模糊等。

        4.缓存图片,主要是防止在移动流量的情况下,用户每次进入都会进行网络加载,浪费数据流量,所以要对图片资源进行缓存处理,这样可以解决用户的数据流量,还能加快图片加载的速度。

        5.异步加载,虽然可以利用缓存来加快图片加载的速度,但是需要加载很多图片的时候,如:瀑布流,还需要使用多线程来加载图片,使用多线程会涉及到图片的同步加载和异步加载的问题。

### 处理图片的主要逻辑
    现在比较常用的是 Glide ,Picasso,Fresco 等三方框架。

    从网路或者本地等路径加载图片,接着解码图片,延后进行压缩,然后可能会有对图片进行圆角,剪切,高斯模糊的处理,然后 三级缓存 加载的图片,加载图片的过程 涉及到同步异步加载图片,最后 刷新到 view 上。

### 从网络中加载图片
    在加载图片的时候,可以先从内存缓存中获取,当内存缓存中不存在缓存对象时,就去磁盘缓存中尝试缓存,如果此时磁盘缓存中依然不存在时,就需要进行网络请求获取图片的文件流,在这将采用最原生的网络请求方式HttpURLConnection方式进行图片获取。
    怎么去加载一个图片呢?

    /**
     * 请求网络图片转化成bitmap
     * @param url                       url
     * @return                          将url图片转化成bitmap对象
     */
    private static long time = 0;
    public static Bitmap returnBitMap(String url) {
        long l1 = System.currentTimeMillis();
        URL myFileUrl = null;
        Bitmap bitmap = null;
        HttpURLConnection conn = null;
        InputStream is = null;
        try {
            myFileUrl = new URL(url);
        } catch (MalformedURLException e) {
            e.printStackTrace();
        }
        try {
            conn = (HttpURLConnection) myFileUrl.openConnection();
            conn.setRequestMethod("GET");
            conn.setConnectTimeout(10000);
            conn.setReadTimeout(5000);
            conn.setDoInput(true);
            conn.connect();
            is = conn.getInputStream();
            bitmap = BitmapFactory.decodeStream(is);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (is != null) {
                    is.close();
                    conn.disconnect();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            long l2 = System.currentTimeMillis();
            time = (l2-l1) + time;
            LogUtils.e("毫秒值"+time);
        }
        return bitmap;
    }
### 图片压缩Api
    常用的压缩方法 1.Bitmap.compress() 质量压缩。2.BitmapFactory.options.inSampleSize 内存压缩。

    Bitmap.compress():质量压缩,不会对内存产生影响, 它是在保持像素的前提下改变图片的位深及透明度等,来达到压缩图片的目的,不会减少图片的像素。进过它压缩的图片文件大小会变小,但是解码成bitmap 后占得内存是不变的。
    BitmapFactory.Options.inSampleSize:内存压缩,节码图片的时候没设置 BitmapFactory.Options类的inJustDecodeBounds属性为true,可以在bitmap不加载到内存的前提下,获得Bitmap的原始宽高,而设置

    BitmapFactory.options.inSampleSize属性,可以真的压缩 Bitmap 占用的内存,设置inSampleSize之后,Bitmap的宽、高都会缩小inSampleSize倍。如:一张宽高为2048x1536的图片,设置inSampleSize为4之后,实际加载到内存中的图片宽高是512x384。占有的内存就是0.75M而不是12M。需要根据实际情况来设置
inSampleSize比1小的话会被当做1,任何inSampleSize的值会被取接近2的幂值。
### 图片压缩方式
    1.质量压缩:在保持像素的前提下改变图片的位深及透明度等,来达到压缩图片的目的,这样适合去传递二进制的图片数据,比如分享图片,要传入二进制数据过去,限制500kb之内。
            a:bitmap图片的大小不会改变

            b:bytes.length是随着quality变小而变小的。
    2.采样率压缩:设置inSampleSize的值(int类型)后,假如设为n,则宽和高都为原来的1/n,宽高都减少,内存降低。上面的代码没用过options.inJustDecodeBounds = true;
        为什么叫采样率压缩?

                因为配合inJustDecodeBounds,先获取图片的宽、高(这个过程就是取样)。然后通过获取的宽高,动态的设置inSampleSize的值,当inJustDecodeBounds设置为true的时候, BitmapFactory通过decodeResource或者decodeFile解码图片时,将会返回空(null)的Bitmap对象,这样可以避免Bitmap的内存分配, 但是它可以返回Bitmap的宽度、高度以及MimeType。
    3.缩放法压缩:Android中使用Matrix对图像进行缩放、旋转、平移、斜切等变换的。 Matrix提供了一些方法来控制图片变换:Matrix调用一系列set,pre,post方法时, 可视为将这些方法插入到一个队列。当然,按照队列中从头至尾的顺序调用执行。其中pre表示在队头插入一个方法,post表示在队尾插入一个方法。而set表示把当前队列清空,并且总是位于队列的最中间位置。当执行了一次set后:pre方法总是插入到set前部的队列的最面,post方法总是插入到set后部的队列的最后面。
### 本地磁盘缓存
    DiskLruCache适用于本地磁盘缓存,

    File diskCacheDir = getDiskCahceDir(context, "bitmap");
    if (!diskCacheDir.exists()) {
        diskCacheDir.mkdirs();
    }
    if (getUsableSpace(diskCacheDir) > DISK_CACHE_SIZE) {
        try {
            diskLruCache = DiskLruCache.open(diskCacheDir, 1, 1, DISK_CACHE_SIZE);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    构造方法有四个参数,第一个参数指的是磁盘缓存的路径,传参前需要确保该路径下的文件夹存在,没有就创建一个;第二个参数是版本号,通常设为1即可;第三个参数是指单个节点所对应的数据的个数,通常也设为1即可; 第四个参数是指该DiskLruCache的容量大小。
### 内存缓存
    LruCache是Android提供的一个缓存类,用于内存缓存,LruCache是一个泛型类,底层是用一个LinkedHashMap以强引用的方式存储外界的缓存对象来实现的。
    为什么使用LinkedHashMap作为LruCache的存储?

    因 LinkedHashMap 有两种 排序方式,一种是插入方式,另一种是访问排序方式,默认情况下是以访问方式来存储缓存对象的;LruCache提供了get和put方法来完成缓存的获取和添加,当缓存满时,会将最近最少使用的对象移除掉,然后再添加新的缓存对象。如下源码所示,底层是LinkedHashMap。    
    public LruCache(int maxSize) {
        if (maxSize <= 0) {
            throw new IllegalArgumentException("maxSize <= 0");
        }
        this.maxSize = maxSize;
        this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
    }
    在使用LruCache的时候,首先需要获取当前设备的内存容量,通常情况下会将总容量的八分之一作为LruCache的容量,然后重写LruCache的sizeof方法,sizeof方法用于计算缓存对象的大小,单位需要与分配的容量的单位一致;
    // 获取系统最大缓存
    int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
    // set LruCache size;
    int cacheSize = maxMemory / 8;
    LruCache<String, Bitmap> memoryCache = new LruCache<String, Bitmap>(cacheSize) {
        @Override
        protected int sizeOf(@NonNull String uri, @NonNull Bitmap bitmap) {
            return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
        }
    };
    //插入对象
    memoryCache.put(key, bitmap);
    //取出对象
    memoryCache.get(key);
    LinkedHashMap 构造函数的第三个参数:accessOrder,传入true时, 元素会按访问顺序排列,最后访问的在遍历器最后端。 在进行淘汰时,移除遍历器前端的元素,直至缓存总大小降低到指定大小以下。
###  Gilde源码
   /*
    * @param context Any context, will not be retained.
    * @return A RequestManager for the top level application that can be used to start a load.
    * @see #with(android.app.Activity)
    * @see #with(android.app.Fragment)
    * @see #with(android.support.v4.app.Fragment)
    * @see #with(android.support.v4.app.FragmentActivity)
    */
    @NonNull
    public static RequestManager with(@NonNull Context context) {
        return getRetriever(context).get(context);
    }
    
    //然后去看看get(context)方法源码
    @NonNull
    public RequestManager get(@NonNull Context context) {
        if (context == null) {
          throw new IllegalArgumentException("You cannot start a load on a null Context");
        } else if (Util.isOnMainThread() && !(context instanceof Application)) {
          if (context instanceof FragmentActivity) {
            return get((FragmentActivity) context);
          } else if (context instanceof Activity) {
            return get((Activity) context);
          } else if (context instanceof ContextWrapper) {
            return get(((ContextWrapper) context).getBaseContext());
          }
        }
        
        return getApplicationManager(context);
    }
     RequestManager 实现了  LifecycleListener ,将request与生命周期绑定,这样就可以通过context的生命周期去操作网络请求的开始,暂停。
    @NonNull
    public RequestManager get(@NonNull FragmentActivity activity) {
        if (Util.isOnBackgroundThread()) {
          return get(activity.getApplicationContext());
        } else {
          assertNotDestroyed(activity);
          FragmentManager fm = activity.getSupportFragmentManager();
          return supportFragmentGet(
              activity, fm, /*parentHint=*/ null, isActivityVisible(activity));
        }
    }   
    如果Glide在子线程中使用或者传入的context是ApplicationContext,那么就与全局的ApplicationContext绑定,如果不是那么创建一个无界面的fragment,即SupportRequestManagerFragment,让请求和你的activity的生命周期同步。
    在with方法中,RequestManager的类,这个是网络请求的管理类,主要负责将所有的类的生命周期与调用其的组件的生命周期相绑定,这也是Glide与其他图片加载框架不一样的地方,它是和组件的生命周期相绑定的。
### Glide加载问题
    1.1 在Recyclerview使用GlideAPP加载大量图片导致内存溢出

    //在RecyclerView中重写这个方法,当item被隐藏的时候,
    //调用 Glide.with(fragment).clear(imageView);
    @Override
    public void onViewRecycled(@NonNull MyViewHolder holder) {
        super.onViewRecycled(holder);
        ImageView imageView = holder.photoView;
        if (imageView!=null){
            Glide.with(mContext).clear(imageView);
        }
    }
    1.2 尽量减少图片资源的大小,这种方法是可行的。但是会导致刷新闪烁的问题,导致原因是glide设置了跳过内存缓存 skipMemoryCache(true) 导致的。
    GlideApp.with(mContext)
            .asDrawable()               //相对而言,asDrawable()比asBitmap要省(具体相关可以去百度)
            .thumbnail(0.2f)
            .load(imageUrl)
            .skipMemoryCache(true)              //跳过内存缓存
            .diskCacheStrategy(DiskCacheStrategy.ALL)//全部使用磁盘缓存
            .error(R.mipmap.pic_load_fail)
            .addListener(new RequestListener<Drawable>() {
                @Override
                public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<Drawable> target, boolean isFirstResource) {
                    holder.loading.setVisibility(View.GONE);
                    if (e!=null){
                        LogUtils.e("加载图片"+"加载失败"+e.getMessage());
                    }
                    //显示默认图片
                    holder.photoView.setImageResource(R.mipmap.pic_load_fail);
                    return false;
                }
    
                @Override
                public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target, DataSource dataSource, boolean isFirstResource) {
                    holder.loading.setVisibility(View.GONE);
                    return false;
                }
            })
            .into(holder.photoView);
    1.3 Recyclerview使用Glide加载图片滑动卡顿

    //RecyclerView.SCROLL_STATE_IDLE //空闲状态
    //RecyclerView.SCROLL_STATE_FLING //滚动状态
    //RecyclerView.SCROLL_STATE_TOUCH_SCROLL //触摸后状态
    //滑动的过程中不加载图片
    recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
        @Override
        public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
            super.onScrollStateChanged(recyclerView, newState);
            if (newState == RecyclerView.SCROLL_STATE_IDLE) {
                LoggerUtils.e("initRecyclerView"+ "恢复Glide加载图片");
                Glide.with(ImageBrowseActivity.this).resumeRequests();
            }else {
                LoggerUtils.e("initRecyclerView"+"禁止Glide加载图片");
                Glide.with(ImageBrowseActivity.this).pauseRequests();
            }
        }
    });
      1.4 适当避免使用圆角的自定义ImageView控件

            在实际项目内,经常会用到一些带圆角的图片,或者直接就是圆形的图片。圆形的图片,多数用于一些用户的头像之类的显示效果。android中有大量的类似 XXXImageView 的开源控件,操作 Bitmap 达到圆角图片的效果,大部分的原理,是接收到你传递的 Bitmap ,然后再输出一个与原来 Bitmap 等大的新 Bitmap ,在此基础之上,进行圆角的一些处理,这就导致了内存中会多个bitmap,占用的内存会增加一倍。

    推荐使用glide-transformations开源库:利用 Glide 的 bitmapTransfrom() 接口,实现对加载的 Bitmap 的进行一些变换操作。提供一系类对加载的图片的变换操作,从形状变换到色彩变换,全部支持,基本上满足大部分开发需要,并且它会复用 Glide 的 BitmapPool ,来达到节约内存的目的。

### TrimMemory和LowMemory方法作用
    在App中内存不足的情况下,会调用一些onTrimMemory()或者onLowMemory()等方法,这些都是在提醒开发者,当前设备的内存环境已经发生了变化,避免被系统清理掉或者出现 OOM 。 

    那么什么是LowMemoryKiller的策略?

    在 Android 系统中,当运行的 App 被移动到后台的之后,Android 为了保证下次启动的速度,会将它移入 Cached 的状态,这时该App 的进程依然存在,但是当中的组件是否存在就不一定了 ,该 App 的进程还存活着,下次启动的速度就会很快,这就是热启动。

    但是退到后台中,也不是一定不会被清理掉的,它们只是不占用CPU的资源,但是依然会占用内存空间,当系统的内存不足时,就会按照优先级清理掉一些优先级不那么高的进程,来回收一些内存空间,供新启动的程序使用,这就是 LowMemoryKiller 的策略。

    那么为了让我们的进程活的久一点,我们就必须的 合理优化进程的内存,进入后台时可以将 图片的缓存清理掉,因为图片是 占用内存的大户,再次切回到前台,可以从网络或者本地重新加载,这样降低了被杀的概率。

    1.1 TrimMemory 和  onLowMemory源码
        @CallSuper
        public void onTrimMemory(int level) {
            Object[] callbacks = collectComponentCallbacks();
            if (callbacks != null) {
                for (int i=0; i<callbacks.length; i++) {
                    Object c = callbacks[i];
                    if (c instanceof ComponentCallbacks2) {
                        ((ComponentCallbacks2)c).onTrimMemory(level);
                    }
                }
            }
        }

    @CallSuper
    public void onLowMemory() {
        Object[] callbacks = collectComponentCallbacks();
        if (callbacks != null) {
            for (int i=0; i<callbacks.length; i++) {
                ((ComponentCallbacks)callbacks[i]).onLowMemory();
            }
        }
    }
     onTrimMemory() 传递一个 level 参数。

     TRIM_MEMORY_UI_HIDDEN:App 的所有 UI 界面被隐藏,最常见的就是 App 被 home 键或者 back 键,置换到后台了。
     TRIM_MEMORY_RUNNING_MODERATE:表示 App 正常运行,并且不会被杀掉,但是目前手机内存已经有点低了,系统可能会根据 LRU List 来开始杀进程。
     TRIM_MEMORY_RUNNING_LOW:表示 App正常运行,并且不会被杀掉。但是目前手机内存已经非常低了。
     TRIM_MEMORY_RUNNING_CRITICAL:表示 App 正在正常运行,但是系统已经开始根据 LRU List 的缓存规则杀掉了一部分缓存的进程。这个时候应该尽可能的释放掉不需要的内存资源,否者系统可能会继续杀掉其他缓存中的进程。
     TRIM_MEMORY_BACKGROUND:表示 App 退出到后台,并且已经处于 LRU List 比较靠后的位置,暂时前面还有一些其他的 App 进程,暂时不用担心被杀掉。
     TRIM_MENORY_MODERATE:表示 App 退出到后台,并且已经处于 LRU List 中间的位置,如果手机内存仍然不够的话,还是有被杀掉的风险的。
     TRIM_MEMORY_COMPLETE:表示 App 退出到后台,并且已经处于 LRU List 比较考靠前的位置,并且手机内存已经极低,随时都有可能被系统杀掉。
    大致分为 三类

        1.UI 置于后台:TRIM_MEMORY_UI_HIDDEN 。
        2.App 正在前台运行时候的状态:TRIM_MEMORY_RUNNING_Xxx
        3.App 被置于后台,在 Cached 状态下的回调:TRIM_MEMORY_Xxx
    这三种我们只需关注被置于Cache状态下的情况,因为系统是不会杀掉正在前台执行的程序的,若是在后台正在运行就会有被杀的风险,当收到 TRIM_MEMORY_Xxx 的回调,就需要注意了,这些只是标记了当前 App 处于 LRU List 的位置,若回收了靠前的App进程之后,依然达不到内存使用的要求,可能会进一步去杀进程,

    极端情况下,可能从 TRIM_MEMORY_BACKGROUND 到 TRIM_MEMORY_COMPLETE 是瞬间完成的事情,所以我们需要慎重处理它们,尽量对这三个状态都进行判断,然后做统一的回收内存资源的处理。
      OnLowMemory是Android提供的API,在系统内存不足,所有后台程序(优先级为background的进程,不是指后台运行的进程)都被杀死时,系统会调用OnLowMemory。
     OnTrimMemory是Android 4.0之后提供的API,系统会根据不同的内存状态来回调。

### Glide内部低内存处理
    Glide 也为我们提供了类似方法的接口,开发者只需要调用即可,它在内部会随着不同的内存情况,帮我们对缓存的图片进行优化。主要用到Glide的trimMemory()和cleanMemroy()方法,它们一个用来裁剪Glide缓存的图片内存空间,一个用来清理 Glide 缓存的内存空间。
     1.在Application  通过registerComponentCallbacks()方法进行注册
registerComponentCallbacks(new ComponentCallbacks2() {
            @Override
            public void onTrimMemory(int level) {
                //HOME键退出应用程序
                //程序在内存清理的时候执行
            }
        
            @Override
            public void onConfigurationChanged(Configuration newConfig) {
        
            }
        
            @Override
            public void onLowMemory() {
                //低内存的时候执行
            }
        });
   2.直接在Application中,重写对应的方法

        /**
         * 低内存的时候执行
         */
        @Override
        public void onLowMemory() {
            Log.d("Application", "onLowMemory");
            super.onLowMemory();
        }
        
        
        /**
         * HOME键退出应用程序
         * 程序在内存清理的时候执行
         */
        @Override
        public void onTrimMemory(int level) {
            Log.d("Application", "onTrimMemory");
            super.onTrimMemory(level);
        }
        
        
        /**
         * onConfigurationChanged
         */
        @Override
        public void onConfigurationChanged(Configuration newConfig) {
            Log.d("Application", "onConfigurationChanged");
            super.onConfigurationChanged(newConfig);
        }
    大概的思路:在低内存,Application 被切换到后台,调用Glide.clearMemory清理所有的内存缓存。onTrimMemroy()回调中,直接调用Glide.trimMemory()方法来交给Glide处理内存情况。
        /**
         * 低内存的时候执行
         */
        @Override
        public void onLowMemory() {
            super.onLowMemory();
            Log.e("Application----------", "onLowMemory");
            //在低内存的情况下,可以清除glide缓存
            Glide.get(this).clearMemory();
        }
        
        
        /**
         * HOME键退出应用程序
         * 程序在内存清理的时候执行
         */
        @Override
        public void onTrimMemory(int level) {
            super.onTrimMemory(level);
            Log.e("Application----------", "onTrimMemory"+level);
            if (level == TRIM_MEMORY_UI_HIDDEN){
                Glide.get(this).clearMemory();
            }
            Glide.get(this).trimMemory(level);
        }
###  glide中clearMemory()和trimMemory(level)源码
  /**
   * Clears as much memory as possible.
   *
   * @see android.content.ComponentCallbacks#onLowMemory()
   * @see android.content.ComponentCallbacks2#onLowMemory()
   */
  public void clearMemory() {
    // Engine asserts this anyway when removing resources, fail faster and consistently
    Util.assertMainThread();
    // memory cache needs to be cleared before bitmap pool to clear re-pooled Bitmaps too. See #687.
    memoryCache.clearMemory();
    bitmapPool.clearMemory();
    arrayPool.clearMemory();
  }

  /**
   * Clears some memory with the exact amount depending on the given level.
   *
   * @see android.content.ComponentCallbacks2#onTrimMemory(int)
   */
  public void trimMemory(int level) {
    // Engine asserts this anyway when removing resources, fail faster and consistently
    Util.assertMainThread();
    // memory cache needs to be trimmed before bitmap pool to trim re-pooled Bitmaps too. See #687.
    memoryCache.trimMemory(level);
    bitmapPool.trimMemory(level);
    arrayPool.trimMemory(level);
  }
    这两个方法都会去操作 MemoryCache 和 BitmapPool这两个对象,Glide对他们的默认实现LruResourceCache 和 LruBitmapPool 。它们都是遵循 LRU 算法的.
    MemoryCache 是 Glide 用来在内存中缓存图片资源,使其在需要使用的时候立刻就可以使用,而不必执行磁盘的 I/O 操作.

    BitmapPool 则是 Glide 维护了一个图片复用池,LruBitmapPool 使用 Lru 算法保留最近使用的尺寸的 Bitmap

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值