### 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