有限的内存加载无限大的图片,导致OOM:
1、图片库里展示的图片(大部分由手机摄像头拍出来)分辨率比手机屏幕分辨率高得多,应该将图片压缩和用来展示的控件大小相近,否则会占用内存
2、Android对图片解码:图片大小=图片总像素数*图片每个像素大小
首先获得屏幕的大小,然后获得图片的大小,最后通过计算屏幕与图片的缩放比,按照缩放比来解析位图。
//1.获取手机的分辨率 获取windowmanager 实例
WindowManager wm = (WindowManager) getSystemService(WINDOW_SERVICE); screenWidth = wm.getDefaultDisplay().getWidth();
screenHeight = wm.getDefaultDisplay().getHeight();
//2.把xxxx.jpg 转换成bitmap
//创建bitmap工厂的配置参数
BitmapFactory.Options options = new Options();
//=true返回一个null 没有bitmap:不去为bitmap分配内存 但是能返回图片的一些信息(宽和高) options.inJustDecodeBounds = true; BitmapFactory.decodeFile("/mnt/sdcard/xxxx.jpg",options);
//3.获取图片的宽和高
int imgWidth = options.outWidth;
int imgHeight = options.outHeight;
//4.计算缩放比
int scalex = imgWidth/screenWidth;
int scaley = imgHeight /screenHeight;
scale =min(scalex, scaley,scale);
//5.按照缩放比显示图片,inSampleSize给图片赋予缩放比,其值大于1时,会按缩放比返回一个小图片用来节省内存
options.inSampleSize = scale;
//6.=false开始真正的解析位图,可显示位图
options.inJustDecodeBounds = false;
Bitmap bitmap = BitmapFactory.decodeFile("/mnt/sdcard/dog.jpg",options);
//7.把bitmap显示到控件上
iv.setImageBitmap(bitmap); } }
图片的分块加载在地图绘制的情况上最为明显,当想获取一张尺寸很大的图片的某一小块区域时,就用到了图片的分块加载,在Android中BitmapRegionDecoder类的功能就是加载一张图片的指定区域。
图片的三级缓存机制一般是指应用加载图片的时候,分别去访问内存,文件和网络而获取图片数据的一种行为。
LruCache是android提供的一个缓存工具类,其算法是最近最少使用算法。它把最近使用的对象用“强引用”存储在LinkedHashMap中,并且把最近最少使用的对象在缓存值达到预设定值之前就从内存中移除。
LinkedHashMap:HashMap和双向链表合二为一即是LinkedHashMap。它通过维护一个额外的双向链表保证了迭代顺序。特别地,该迭代顺序可以是插入顺序,也可以是访问顺序。
/**
* 它的主要算法原理是把最近使用的对象用强引用存储在 LinkedHashMap 中,并且把最近最少使用的对象在缓存值达到预设定值之前从内存中移除。(最近使用的数据在尾部,老数据在头部)
*/
public class LruCache {
private final LinkedHashMap map;
public LruCache(int maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
this.maxSize = maxSize;
// 注意第三个参数,是accessOrder=true,表示linkedHashMap为访问顺序,当对已存在LinkedHashMap的Entry进行get和put操作时,会把Entry移动到双向链表的表头
this.map = new LinkedHashMap(0, 0.75f, true);
}
LruCache的put方法
public final V put(K key, V value) {
V previous;
// 对map进行操作之前,先进行同步操作(HashMap是线程不安全的,应该先进行同步操作)
//synchronized加锁,表示一次只能有一个方法进入该线程
synchronized (this) {
putCount++;
size += safeSizeOf(key, value);
//向map中加入缓存对象,若缓存中已存在,返回已有的值,否则执行插入新的数据,并返回null,并将缓存恢复为之前的值
previous = map.put(key, value)
if (previous != null) {
size -= safeSizeOf(key, previous);
}
}
if (previous != null) {
entryRemoved(false, key, previous, value);
}
// 根据缓存大小整理内存,看是否需要移除LinkedHashMap中的元素
trimToSize(maxSize);
return previous; }
trimToSize(maxSize)
public void trimToSize(int maxSize) {
while (true) {
// while循环,不断移除LinkedHashMap中双向链表表头表头元素(近期最少使用的数据),直到满足当前缓存大小小于或等于最大可缓存大小
//如果当前缓存大小已小于等于最大可缓存大小,则直接返回,不需要再移除LinkedHashMap数据
if (size <= maxSize || map.isEmpty()) {
break; }
//得到双向链表表头header的下一个Entry(近期最少使用的数据=表头数据)
Map.Entry toEvict = map.entrySet().iterator().next();
key = toEvict.getKey();
value = toEvict.getValue();
//移除当前取出的Entry并重新计算当前缓存大小
map.remove(key);
size -= safeSizeOf(key, value);
evictionCount++;
}
entryRemoved(true, key, value, null);
}
}
LruCache的get方法
public final V get(K key) {
//如果该值在缓存中存在或可被创建便返回,当调用LruCache的get()方法获取集合中的缓存对象时,就代表访问了一次该元素,将会更新队列,移动到表尾,这个更新过程就是在LinkedHashMap中的get()方法中完成的。
V mapValue;
synchronized (this) {
mapValue = map.get(key);
if (mapValue != null) {
return mapValue;
}
}
}
由重排序可知,对LinkedHashMap的put和get操作,都会让被操作的Entry移动到双向链表的尾部。而移除是从map.entrySet().iterator().next()开始的,也就是双向链表的表头的header的after开始的,这也就符合了LRU算法的需求。
/* 内存缓存 */
private LruCache mMemoryCache = 4*1024*1024;
//初始化这个cache前需要设定这个cache的大小,这里的大小官方推荐是用当前app可用内存的八分之一
mMemoryCache = new LruCache(memoryCache) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
// 重写此方法来衡量每张图片的大小,默认返回图片数量。
return bitmap.getByteCount() / 1024;
}
};
//将bitmap添加到内存中去
public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
if (getBitmapFromMemCache(key) == null) {
mMemoryCache.put(key, bitmap);
}
}
// 通过key来从内存缓存中获得bitmap对象
private Bitmap getBitmapFromMemCache(String key) {
return mMemoryCache.get(key);
}
private void loadBitmapToImageView(int resId, ImageView imageView) {
final String imageKey = String.valueOf(resId);
final Bitmap bitmap = getBitmapFromMemCache(imageKey); // 先看这个资源在不在内存中,如果在直接读取为bitmap,否则返回null
if (bitmap != null) {
imageView.setImageBitmap(bitmap);
} else {
imageView.setImageResource(R.drawable.ic_launcher); // 如果没有在内存中,先显示默认的图片,然后启动线程去下载图片
BitmapWorkerTask task = new BitmapWorkerTask(imageView);
task.execute(resId); // 启动线程,模拟从网络下载图片,下载后加入缓存
}
}
缓存如果没有,则继续
存储的路径首先要考虑SD卡的缓存目录,当SD卡不存在时,就只能存到内部存储的缓存目录了。
private File getCacheDir() {
// 获取缓存路径目录
File file;
if (Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED) {
file = mContext.getExternalCacheDir(); // 有SD卡就保存到sd卡
} else {
file = mContext.getCacheDir(); // 没有就保存到内部储存
}
return file;
}
private Bitmap getBitmapFromFile() {
// 根据url中获取文件名字,存储文件的文件名截取URL中的名字,并且文件名用md5加密
String fileName = url.substring(url.lastIndexOf("/") + 1);
File file = new File(getCacheDir(),MD5Util.encodeMd5(fileName));
if (file.exists() && file.length() > 0) {
diskBitmap = BitmapFactory.decodeFile(file.getAbsolutePath());
mImageCache.put(url, new SoftReference(diskBitmap)); // 保存到内存中去
return diskBitmap;
} else {
return null;
}
}
//构建出5条线程的线程池
private ExecutorService mExecutorService = Executors.newFixedThreadPool(5);
//用HttpUrlConnection加载网络图片
URL loadUrl = new URL(url);
HttpURLConnection conn = (HttpURLConnection) loadUrl.openConnection();
mImageCache.put(url, new SoftReference<>(bm)); //保存到内存
String fileName = url.substring(url.lastIndexOf("/") + 1);//从Url中获取文件名字,保存到磁盘
File file = new File(getCacheDir(), MD5Util.encodeMd5(fileName));//获取存储路径
FileOutputStream os = new FileOutputStream(file);
bm.compress(Bitmap.CompressFormat.JPEG, 100, os);//将图片转为文件存储
//with(Context/Activity/Fragment)决定Glide加载图片的生命周期
//load(url)url包括网络图片、本地图片、应用资源、二进制流、Uri对象等等(重载)
//into(imageView)
Glide.with(this).load(url).into(imageView);
//扩展功能
.placeholder(R.drawable.loading)//加载图片过程占位符,加载完成会替换占位符
.error(R.drawable.error)//加载图片错误占位符
.asGif()/.asBitmap()只显示动态图/只显示静态图(不设置时,Glide会自动判断图片格式)
.diskCacheStrategy(DiskCacheStrategy.NONE)//禁用Glide缓存机制
.override(100, 100)//指定图片大小(Glide会自动判断ImageView的大小,然后将对应的图片像素加载本地,节省内存开支)
得到一个RequestManager对象(实现request和Activity/Fragment生命周期的关联),Glide再根据传入的with()方法的参数确定图片加载的生命周期:
1、Application类型参数——应用程序生命周期
2、非Application类型参数——Activity/Fragment生命周期
得到一个DrawableTypeRequest对象(extends DrawableRequestBuilder)
return into(glide.buildImageViewTarget(view, transcodeClass));
其中glide.buildImageViewTarget(view,transcodeClass):
public interface Target extends LifecycleListener {
//The typical lifecycle is onLoadStarted -> onResourceReady or onLoadFailed -> onLoadCleared.
request的载体,各种资源对应的加载类,含有生命周期的回调方法,方便开发人员在图片加载过程进行相应的准备以及资源回收工作。
在buildTarget()方法中会根据传入的class参数来构建不同的Target对象,如果你在使用Glide加载图片的时候调用了asBitmap()方法,那么这里就会构建出BitmapImageViewTarget对象,否则的话构建的都是GlideDrawableImageViewTarget对象。
通过glide.buildImageViewTarget()方法,我们构建出了一个GlideDrawableImageViewTarget对象。即
into(GlideDrawableImageViewTarget)
public > Y into(Y target) {
Request request = buildRequest(target);
target.setRequest(request);
lifecycle.addListener(target);
requestTracker.runRequest(request);
return target;
}
(1) buildRequest(buildRequestRecursive)
//创建请求,如果配置了thumbnail(缩略图)请求,则构建一个ThumbnailRequestCoordinator(包含了FullRequest和ThumbnailRequest)请求,
否则调用了obtainRequest()方法来获取一个Request对象,并传入相应的参数(target,sizeMultiplier,priority,requestCoordinator...)
return GenericRequest.obtain(target, sizeMultiplier, priority, parentCoordinator…);
(2) requestTracker.runRequest
public void runRequest(Request request) {
//处理请求
requests.add(request);
if (!isPaused) {
request.begin();//调用GenericRequest的begin方法
} else {
pendingRequests.add(request);
}
}
GenericRequest.begin()
public void begin() {
if (model == null) { //model为Url,图片路径,Uri等等图片资源模型
onException(null); //调用setErrorPlaceholder(),将error占位符填入imageView上
return;
}
if (Util.isValidDimensions(overrideWidth, overrideHeight)) {
onSizeReady(overrideWidth, overrideHeight);
} else {//未指定宽高,则glide根据ImageView的layout_width和layout_height值做一系列的计算,来算出图片应该的宽高(防止OOM),再调用onSizeReady(width,height)
target.getSize(this);
}
if (!isComplete() && !isFailed() && canNotifyStatusChanged()) {
target.onLoadStarted(getPlaceholderDrawable());
}
}
public void onSizeReady(int width, int height) {
width = Math.round(sizeMultiplier * width);
height = Math.round(sizeMultiplier * height);
//ModelLoader:各种资源model的ModelLoader,主要功能有:1、将任意复杂的model转换为可以被DataFetcher所decode的数据类型2、允许model结合View的尺寸获取特定大小的资源
ModelLoader modelLoader = loadProvider.getModelLoader();
//每次通过ModelLoader加载资源都会创建的实例
final DataFetcher dataFetcher = modelLoader.getResourceFetcher(model, width, height);
// ResourceTranscoder : 资源转换器,将给定的资源类型,转换为另一种资源类型,比如将Bitmap转换为Drawable,Bitmap转换为Bytes
ResourceTranscoder transcoder = loadProvider.getTranscoder();
loadedFromMemoryCache = true;
loadStatus = engine.load(signature, width, height, dataFetcher, loadProvider, transformation, transcoder, priority, isMemoryCacheable, diskCacheStrategy, this); //真正开始加载资源
loadedFromMemoryCache = resource != null;
}
Engine.load(…)
真正的开始加载资源,load调用处理流程图:
public LoadStatus load(Key signature, int width, int height, DataFetcher fetcher,
DataLoadProvider loadProvider, Transformation transformation, ResourceTranscoder transcoder,
Priority priority, boolean isMemoryCacheable, DiskCacheStrategy diskCacheStrategy, ResourceCallback cb) {
//创建一个EngineJob:调度DecodeJob,添加,移除资源回调,并notify回调
EngineJob engineJob = engineJobFactory.build(key, isMemoryCacheable);
//创建一个DecodeJob:调度任务的核心类,整个请求的繁重工作都在这里完成:处理来自缓存或者原始的资源,应用转换动画以及transcode。
负责根据缓存类型获取不同的Generator加载数据,数据加载成功后回调DecodeJob的onDataFetcherReady方法对资源进行处理
DecodeJob decodeJob = new DecodeJob(key, width, height, fetcher, loadProvider, transformation, transcoder, diskCacheProvider, diskCacheStrategy, priority);
EngineJob engineJob = engineJobFactory.build(key, isMemoryCacheable);
//创建一个EngineRunnable implements Runnable
EngineRunnable runnable = new EngineRunnable(engineJob, decodeJob, priority);
jobs.put(key, engineJob);
engineJob.addCallback(cb);
engineJob.start(runnable); //*
return new LoadStatus(cb, engineJob);
}
*在子线程执行EngineRunnable的run()方法=> DecodeJob.decodeFromSource()/decodeFromCache()
(1)decodeSource()//获取一个Resource对象
1、fetcher.loadData()进行网络通讯获得InputStream,并包装成ImageVideoWrapper对象
private InputStream loadDataWithRedirects(URL url, int redirects, URL lastUrl, Map headers)
throws IOException {
urlConnection = connectionFactory.build(url);
urlConnection.connect();
return getStreamForSuccessfulRequest(urlConnection);
}
2、decoded = decodeFromSourceData(data)对ImageVideoWrapper对象(流的封装类)进行解码;包括对图片的压缩,旋转,圆角等逻辑处理,得到加载好的图片对象Bitmap,并层层包装BitmapResource(Resource)->GifBitmapWrapper->GifBitmapWrapperResource(Resource)使加载的图片能够以Resource接口形式返回,并同时能够处理Bitmap和gif图片
public Bitmap decode(InputStream is, BitmapPool pool, int outWidth, int outHeight, DecodeFormat decodeFormat) {
final Bitmap downsampled =downsampleWithSize(invalidatingStream, bufferedStream, options, pool, inWidth, inHeight, sampleSize, decodeFormat);
}
处理这个Resource对象,转化为可在ImageView显示的Resource对象
public void onResourceReady(final Resource> resource) {
this.resource = resource;
//使用Handler发出了一条MSG_COMPLETE消息
MAIN_THREAD_HANDLER.obtainMessage(MSG_COMPLETE, this).sendToTarget();
}
private static class MainThreadCallback implements Handler.Callback {
//在MainThreadCallback的handleMessage()方法中就会收到MSG_COMPLETE消息。从这里开始,所有的逻辑又回到主线程当中进行了,因为很快就需要更新UI了
@Override
public boolean handleMessage(Message message) {
if (MSG_COMPLETE == message.what || MSG_EXCEPTION == message.what) {
EngineJob job = (EngineJob) message.obj;
if (MSG_COMPLETE == message.what) {
job.handleResultOnMainThread();
} else {
job.handleExceptionOnMainThread();
}
return true;
}
return false;
}
}
最后在ImageViewTarget的onResourceReady()方法当中调用了setResource()方法,即调用ImageView.setImageDrawable()方法,图片最终显示出来
1、设置内存缓存开关
skipMemoryCache(true)
2、 设置磁盘缓存模式
diskCacheStrategy(DiskCacheStrategy.NONE)
可以设置4种模式:
DiskCacheStrategy.NONE:表示不缓存任何内容。
DiskCacheStrategy.SOURCE:表示只缓存原始图片。
DiskCacheStrategy.RESULT:表示只缓存转换过后的图片(默认选项)。
DiskCacheStrategy.ALL :表示既缓存原始图片,也缓存转换过后的图片。
内存缓存的操作应该是在异步处理之前,磁盘缓存是耗时操作应该是在异步处理中完成。Glide的内存存缓存的读存都在Engine类中完成。
内存缓存使用弱引用(ActiveCache)和LruCache(Cache)结合完成的,弱引用来缓存的是正在使用中的图片。图片封装类Resources内部有个计数器判断是该图片否正在使用。
流程:
读:是先从lruCache取,取不到再从弱引用中取;
存:
内存缓存取不到,从网络拉取回来先放在弱引用里,渲染图片,图片对象Resources使用计数加一,计数>0,正在使用;
渲染完图片,图片对象Resources使用计数减一,如果计数为0,图片缓存从弱引用中删除,放入lruCache缓存。
1、从内存缓存读取图片
Engine在加载流程的中的入口方法是load方法
public class Engine implements EngineJobListener,
MemoryCache.ResourceRemovedListener,
EngineResource.ResourceListener {
…
public LoadStatus load(Key signature, int width, int height, DataFetcher fetcher,
DataLoadProvider loadProvider, Transformation transformation, ResourceTranscoder transcoder,
Priority priority, boolean isMemoryCacheable, DiskCacheStrategy diskCacheStrategy, ResourceCallback cb) {
Util.assertMainThread();
long startTime = LogTime.getLogTime();
final String id = fetcher.getId();
//生成缓存的key
EngineKey key = keyFactory.buildKey(id, signature, width, height, loadProvider.getCacheDecoder(),
loadProvider.getSourceDecoder(), transformation, loadProvider.getEncoder(),
transcoder, loadProvider.getSourceEncoder());
//从LruCache获取缓存图片
EngineResource> cached = loadFromCache(key, isMemoryCacheable);
if (cached != null) {
cb.onResourceReady(cached);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Loaded resource from cache", startTime, key);
}
return null;
}
//从弱引用获取缓存图片
EngineResource> active = loadFromActiveResources(key, isMemoryCacheable);
if (active != null) {
cb.onResourceReady(active);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Loaded resource from active resources", startTime, key);
}
return null;
}
…//进行异步处理
}
从内存缓存中读取图片的主流程:
生成缓存的key->从LruCache获取缓存图片->LruCache没取到,从弱引用获取图片->内存缓存取不到,进入异步处理。
从内存混存取图片的两个方法loadFromCache()和loadFromActiveResources()。loadFromCache使用的就是LruCache算法,loadFromActiveResources使用的就是弱引用。
public class Engine implements EngineJobListener,
MemoryCache.ResourceRemovedListener,
EngineResource.ResourceListener {
private final MemoryCache cache;
private final Map>> activeResources;
private EngineResource> loadFromCache(Key key, boolean isMemoryCacheable) {
if (!isMemoryCacheable) {//skipMemoryCache()方法设置是否内存缓存被禁用
return null;
}
EngineResource> cached = getEngineResourceFromCache(key); //从LruCache获取图片缓存
if (cached != null) {
//从LruResourceCache中获取到缓存图片之后会将它从缓存中移除,将缓存图片存储到activeResources当中。activeResources就是弱引用的HashMap,用来缓存正在使用中的图片。
cached.acquire();
activeResources.put(key, new ResourceWeakReference(key, cached, getReferenceQueue()));
}
return cached;
}
private EngineResource> loadFromActiveResources(Key key, boolean isMemoryCacheable) {
if (!isMemoryCacheable) {
return null;
}
EngineResource> active = null;
WeakReference> activeRef = activeResources.get(key);
if (activeRef != null) {
active = activeRef.get();
if (active != null) {
active.acquire();
} else {
activeResources.remove(key);
}
}
return active;
}
2、将图片存入内存缓存
EngineJob 获取到图片后 会回调Engine的onEngineJobComplete()将正在加载的图片放到弱引用缓存
public void onEngineJobComplete(Key key, EngineResource> resource) {
if (resource != null) {
resource.setResourceListener(key, this);
if (resource.isCacheable()) {
activeResources.put(key, new ResourceWeakReference(key, resource, getReferenceQueue()));//将正在加载的图片放到弱引用缓存
}
}
jobs.remove(key);
}
EngineResource是用一个acquired变量用来记录图片被引用的次数,调用acquire()方法会让变量加1,调用release()方法会让变量减1。当引用计数acquired变量为0,即表示图片使用完,应该放入LruCache中。这里调用了listener.onResourceReleased(key, this);这个listener就是Engine对象。
public void onResourceReleased(Key cacheKey, EngineResource resource) {
activeResources.remove(cacheKey); //从弱引用中删除图片缓存
if (resource.isCacheable()) {
cache.put(cacheKey, resource); //如果支持缓存,则缓存到LruCache
} else {
resourceRecycler.recycle(resource); //不支持缓存直接调用垃圾回收,回收图片
}
}
读:先找处理后(result)的图片,没有的话再找原图。
存:先存原图,再存处理后的图。
1、从磁盘缓存读取图片
EngineRunnable的run()方法->decode()方法
private Resource> decode() throws Exception {
if (isDecodingFromCache()) {//从磁盘缓存读取图片
return decodeFromCache();
} else {//从原始位置读取图片
return decodeFromSource();
}
}
2、将图片存入磁盘缓存
获取图片后存入图片
public Resource decodeFromSource() throws Exception {
Resource decoded = decodeSource();
return transformEncodeAndTranscode(decoded);
}
(1) 存入原图
private Resource decodeSource() throws Exception {
decoded = decodeFromSourceData(data);
}
private Resource decodeFromSourceData(A data) throws IOException {
final Resource decoded;
if (diskCacheStrategy.cacheSource()) {//设置是否缓存原图
decoded = cacheAndDecodeSourceData(data);
} else {
decoded = loadProvider.getSourceDecoder().decode(data, width, height);
}
return decoded;
}
private Resource cacheAndDecodeSourceData(A data) throws IOException {
SourceWriter writer = new SourceWriter(loadProvider.getSourceEncoder(), data);
//获取DiskCache工具类并写入缓存
diskCacheProvider.getDiskCache().put(resultKey.getOriginalKey(), writer);
Resource result = loadFromCache(resultKey.getOriginalKey());
return result;
}
(2) 存入处理后的图
在图片处理之后,在transformEncodeAndTranscode()方法中存入处理后的图
private Resource transformEncodeAndTranscode(Resource decoded) {
Resource transformed = transform(decoded);
writeTransformedToCache(transformed);
Resource result = transcode(transformed);
return result;
}
private void writeTransformedToCache(Resource transformed) {
SourceWriter> writer = new SourceWriter>(loadProvider.getEncoder(), transformed);
diskCacheProvider.getDiskCache().put(resultKey, writer); //获取DiskCache实例并写入缓存
}
(1.1)构建GenericRequest对象——Glide配置
(1.2)构建decodeJob对象——异步处理核心对象
(1)发起网络请求,拿到数据流
(2)将数据流解码成bitmap对象
(3)将bitmap转码成drawable对象