问:几种主流的图片加载框架的了解有哪些?
答:目前较为主流的图片加载框架有Glide、Picasso、Fresco以及目前不再更新的ImageLoader
- Picasso:是 Square 开源的项目,Picasso将网络请求的缓存部分交给了okhttp实现,也因此Picasso的缓存只能缓存原图
- Glide: Google 的亲儿子,模仿了Picasso的API,在其基础上添加了扩展(比如gif支持、视频解析等)。Glide默认的Bitmap格式是RGB_565,比Picasso默认的ARGB_8888格式的内存开销要小一半;Picasso缓存的是全尺寸的图像(只缓存一种),而Glide缓存的是跟ImageView尺寸相同的图像。
- Fresco: 是 Facebook 上开源的图片缓存框架。在5.0以下系统,Fresco将图片放到一个Ashmem区(匿名共享内存区域),这部分内存类似于Native内存区,不占用Java堆内存,这样Bitmap对象的创建和释放将不会引发GC,更少的GC会使你的App运行得更加流畅,同时在图片不显示的时候,占用的内存会自动被释放,减少因图片内存占用而引发的OOM。在5.0以后系统默认就是存储在Ashmem区了。
如果是大量图片应用可以选择Fresco(体积较大),图片少的且对图片质量要求不需要很高的可以选择使用Glide(体积较小),毕竟Glide是google的亲儿子,又学习了Picasso的优点~
问:如果要你自己设计一个图片加载框架,你要怎么做?
其实,没有哪个公司是真的要你自己设计一个图片加载框架,这只是想看看你对第三方框架的了解程度吧。所以,别慌!
答:如果自己设计一个图片加载框架,要考虑的问题就比较多,例如:
- 图片加载肯定是耗时任务,所以需要用线程去加载,那就需要设计线程池。
- 既然用到线程,在Android中更新页面需要在主线程中执行,所以就需要考虑切换线程的问题,不管是RxJava还是EventBus等等,都需要用到Handler来切换线程。
- Android中对图片Bitmap的操作极易引发OOM,所以需要考虑到如何防止OOM的问题,一般就使用软引用、缓存策略、图片压缩等等。
- 考虑到资源加载效率的问题,就需要考虑缓存策略了,例如LruCache、DiskLruCache以及缓存合适图片大小等
- 页面关闭时还需要考虑内存泄漏的问题,框架要显示图片,肯定需要引用页面的View,这里就要考虑到生命周期同步管理的方式了,例如使用无页面的Fragment等,让这个框架有能力感知页面生命周期。
接下来就按照这几个思路去分析一下Glide使用及源码设计
Glide的简单使用:
// 加载本地图片
val file = File(externalCacheDir.toString() + "/image.jpg")
Glide.with(this).load(file).into(imageView)
// 加载应用资源
val resource: Int = R.drawable.image
Glide.with(this).load(resource).into(imageView)
// 加载二进制流
val image: ByteArray = getImageBytes()
Glide.with(this).load(image).into(imageView)
// 加载Uri对象
val imageUri: Uri = getImageUri()
Glide.with(this).load(imageUri).into(imageView)
问:Glide的线程池有哪些,如何设计的?
答:Glide线程池有三个:
1、sourceExecutor——加载源文件的线程池,包括网络加载
2、diskCacheExecutor——加载硬盘缓存的线程池
3、animationExecutor——动画线程池,不重要,不看了
从Glide.with(this)源码一路跟踪到GlideBuilder类中的Glide build()方法中:
//GlideBuilder.java
Glide build(@NonNull Context context) {
if (sourceExecutor == null) {
sourceExecutor = GlideExecutor.newSourceExecutor(); //加载源文件的线程池,包括网络加载
}
if (diskCacheExecutor == null) {
diskCacheExecutor = GlideExecutor.newDiskCacheExecutor(); //加载硬盘缓存的线程池
}
if (animationExecutor == null) {
animationExecutor = GlideExecutor.newAnimationExecutor(); //动画线程池,不重要
}
//省略一些其他初始化代码,先看线程池
}
//newSourceExecutor()方法,设置线程池线程数量及名称
public static GlideExecutor.Builder newSourceBuilder() {
return new GlideExecutor.Builder(/*preventNetworkOperations=*/ false)
.setThreadCount(calculateBestThreadCount())
.setName(DEFAULT_SOURCE_EXECUTOR_NAME);
}
//calculateBestThreadCount()获取最佳线程数量,最多为4个线程(MAXIMUM_AUTOMATIC_THREAD_COUNT)
public static int calculateBestThreadCount() {
if (bestThreadCount == 0) {
bestThreadCount =
Math.min(MAXIMUM_AUTOMATIC_THREAD_COUNT, RuntimeCompat.availableProcessors());
}
return bestThreadCount;
}
//设置核心线程数及线程总数
public Builder setThreadCount(@IntRange(from = 1) int threadCount) {
corePoolSize = threadCount;
maximumPoolSize = threadCount;
return this;
}
//磁盘缓存线程池,核心线程及线程总数为1
public static GlideExecutor.Builder newDiskCacheBuilder() {
return new GlideExecutor.Builder(/*preventNetworkOperations=*/ true)
.setThreadCount(DEFAULT_DISK_CACHE_EXECUTOR_THREADS)
.setName(DEFAULT_DISK_CACHE_EXECUTOR_NAME);
}
通过上面源码可见:Glide线程池有三个
sourceExecutor——资源加载线程池最大线程数量为4个或系统CPU支持的最大线程数量,如果低于4则取CPU最大支持线程数。
diskCacheExecutor——加载磁盘缓存线程池核心线程数为1且最大线程数量为1的线程池。
同时,线程空闲时间为0(任务执行完成即可立即回收),使用的是PriorityBlockingQueue工作队列,PriorityBlockingQueue是一个支持优先级的无界阻塞队列,直到系统资源耗尽
问:Glide是如何进行线程切换的
答:Glide线程切换还是利用Handler来进行的,在GlideBuilder类中的Glide build()方法中初始化了RequestManagerRetriever请求管理器,在RequestManagerRetriever类中初始化了一个hanlder,用于最终请求完成的callback中进行线程切换。
Glide build(@NonNull Context context) {
//省略其他代码...
RequestManagerRetriever requestManagerRetriever =
new RequestManagerRetriever(requestManagerFactory);
//省略其他代码...
}
public RequestManagerRetriever(@Nullable RequestManagerFactory factory) {
this.factory = factory != null ? factory : DEFAULT_FACTORY;
//在请求执行器管理器中初始化一个handler,关联主线程Looper
handler = new Handler(Looper.getMainLooper(), this /* Callback */);
}
这里明确了Glide最终的线程切换还是使用的Handler。
问:Glide是如何防止OOM以及Glide的缓存的实现?
答:Glide使用 BitmapPool 的池化概念,使得bitmap得以复用。Glide默认加载RGB_565,比ARGB_8888内存减少一半,并且Glide引入 内存缓存、磁盘缓存 两种缓存方式,内存缓存中又分为 活动内存缓存 和 非活动内存缓存。用bitmap池、压缩及缓存的方式来降低内存的使用,减少OOM的发生。
Glide加载引擎是Engine来实现的,看看Engine的load方法:
public <R> LoadStatus load(
//省略一堆参数...
) {
//生成缓存key
EngineKey key =
keyFactory.buildKey(
//省略一堆参数
);
EngineResource<?> memoryResource;
synchronized (this) {
//从内存缓存中获取缓存
memoryResource = loadFromMemory(key, isMemoryCacheable, startTime);
//这里先不管
if (memoryResource == null) {
return waitForExistingOrStartNewJob(
//省略一堆参数...
);
}
}
//获取到缓存直接返回
cb.onResourceReady(memoryResource, DataSource.MEMORY_CACHE);
return null;
}
看看Glide是如何获取内存缓存的:
private EngineResource<?> loadFromMemory( EngineKey key, boolean isMemoryCacheable, long startTime) {
if (!isMemoryCacheable) {
return null;
}
//获取活动内存缓存(弱引用缓存)
EngineResource<?> active = loadFromActiveResources(key);
if (active != null) {
if (VERBOSE_IS_LOGGABLE) {
logWithTimeAndKey("Loaded resource from active resources", startTime, key);
}
return active;
}
//获取非活动内存缓存
EngineResource<?> cached = loadFromCache(key);
if (cached != null) {
if (VERBOSE_IS_LOGGABLE) {
logWithTimeAndKey("Loaded resource from cache", startTime, key);
}
return cached;
}
return null;
}
//获取活动缓存
private EngineResource<?> loadFromActiveResources(Key key) {
EngineResource<?> active = activeResources.get(key);
if (active != null) {
active.acquire();
}
return active;
}
//获取非活动缓存
private EngineResource<?> loadFromCache(Key key) {
EngineResource<?> cached = getEngineResourceFromCache(key);
if (cached != null) {
cached.acquire();
//将非活动缓存存入活动缓存
activeResources.activate(key, cached);
}
return cached;
}
private EngineResource<?> getEngineResourceFromCache(Key key) {
//cache实际上就是LruResourceCache
//这里执行remove在LruCache算法中只是将链表指针修改为最前端,表示最近最新使用
Resource<?> cached = cache.remove(key);
final EngineResource<?> result;
if (cached == null) {
result = null;
} else if (cached instanceof EngineResource) {
// Save an object allocation if we've cached an EngineResource (the typical case).
result = (EngineResource<?>) cached;
} else {
result =
new EngineResource<>(
cached, /*isMemoryCacheable=*/ true, /*isRecyclable=*/ true, key, /*listener=*/ this);
}
return result;
}
从上面可以看出Glide的内存缓存分为两种,活动内存缓存(实际上是弱引用缓存)、非活动内存缓存(LruResourceCache)。当我们从LruResourceCache中获取到缓存图片之后会将它从缓存中移除,然后在将这个缓存图片存储到activeResources当中。activeResources就是一个弱引用的HashMap,用来缓存正在使用中的图片,我们可以看到,loadFromActiveResources()方法就是从activeResources这个HashMap当中取值的。使用activeResources来缓存正在使用中的图片,可以保护这些图片不会被LruCache算法回收掉。
总结一下内存缓存的过程就是:先从活动缓存中取,如果取不到则从非活动缓存中取(LruResourceCache),将非活动缓存中要取出的缓存去除,然后加入到活动缓存中。
这里要说一下LruCache算法的实现:
LruCache 采用最近最少使用算法,设定一个缓存大小,当缓存达到这个大小之后,会将最老的数据移除,避免图片占用内存过大导致OOM。
public class LruCache<K, V> {
// 数据最终存在 LinkedHashMap 中
private final LinkedHashMap<K, V> map;
//...
public LruCache(int maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
this.maxSize = maxSize;
// 创建一个LinkedHashMap,accessOrder 传true 表示按照访问顺序排序
this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
}
//...
}
LinkedHashMap 继承 HashMap,在 HashMap 的基础上进行扩展,put 方法并没有重写,说明LinkedHashMap遵循HashMap的数组加链表的结构,LinkedHashMap重写了 createEntry 方法。
void createEntry(int hash, K key, V value, int bucketIndex) {
HashMapEntry<K,V> old = table[bucketIndex];
LinkedHashMapEntry<K,V> e = new LinkedHashMapEntry<>(hash, key, value, old);
table[bucketIndex] = e; //数组的添加
e.addBefore(header); //处理链表
size++;
}
HashMap中存储的是 HashMapEntry,LinkedHashMap中存储的是LinkedHashMapEntry
private static class LinkedHashMapEntry<K,V> extends HashMapEntry<K,V> {
// These fields comprise the doubly linked list used for iteration.
LinkedHashMapEntry<K,V> before, after; //双向链表
private void remove() {
before.after = after;
after.before = before;
}
private void addBefore(LinkedHashMapEntry<K,V> existingEntry) {
after = existingEntry;
before = existingEntry.before;
before.after = this;
after.before = this;
}
}
LinkedHashMapEntry继承 HashMapEntry,添加before和after变量,所以是一个双向链表结构,还添加了addBefore和remove 方法,用于新增和删除链表节点。
existingEntry 传的都是链表头header,将一个节点添加到header节点前面,只需要移动链表指针即可,添加新数据都是放在链表头header 的before位置,链表头节点header的before是最新访问的数据,header的after则是最旧的数据。链表节点的移除比较简单,改变指针指向即可。
LinkedHashMap的put方法:
public final V put(K key, V value) {
V previous;
synchronized (this) {
putCount++;
//size增加
size += safeSizeOf(key, value);
// 1、linkHashMap的put方法
previous = map.put(key, value);
if (previous != null) {
//如果有旧的值,会覆盖,所以大小要减掉
size -= safeSizeOf(key, previous);
}
}
trimToSize(maxSize);
return previous;
}
public void trimToSize(int maxSize) {
while (true) {
K key;
V value;
synchronized (this) {
//大小没有超出,不处理
if (size <= maxSize) {
break;
}
//超出大小,移除最老的数据
Map.Entry<K, V> toEvict = map.eldest();
if (toEvict == null) {
break;
}
key = toEvict.getKey();
value = toEvict.getValue();
map.remove(key);
//这个大小的计算,safeSizeOf 默认返回1;
size -= safeSizeOf(key, value);
evictionCount++;
}
entryRemoved(true, key, value, null);
}
}
LruCache算法总结一下就是:
- 基于LinkHashMap双向链表实现,在 HashMap的基础上,新增了双向链表结构,每次访问数据的时候,会更新被访问的数据的链表指针,具体就是先在链表中删除该节点,然后添加到链表头header之前,这样就保证了链表头header节点之前的数据都是最近访问的(从链表中删除并不是真的删除数据,只是移动链表指针,数据本身在map中的位置是不变的)。
- LruCache 内部用LinkHashMap存取数据,在双向链表保证数据新旧顺序的前提下,设置一个最大内存,往里面put数据的时候,当数据达到最大内存的时候,将最老的数据移除掉,保证内存不超过设定的最大值。
以上是有内存缓存的情况下的,没有内存缓存的情况下就要执行
if (memoryResource == null) {
return waitForExistingOrStartNewJob(
//省略一堆参数...
engineJob.start(decodeJob);
);
}
//创建了 EngineJob 和 DecodeJob 并调用了 EngineJob 的 start(decodeJob) 方法:
public synchronized void start(DecodeJob<R> decodeJob) {
this.decodeJob = decodeJob;
GlideExecutor executor =
decodeJob.willDecodeFromCache() ? diskCacheExecutor : getActiveSourceExecutor();
executor.execute(decodeJob);
}
//之后执行run方法,再执行到:
private void runWrapped() {
switch (runReason) {
case INITIALIZE:
stage = getNextStage(Stage.INITIALIZE);
currentGenerator = getNextGenerator();
runGenerators();
break;
case SWITCH_TO_SOURCE_SERVICE:
runGenerators();
break;
case DECODE_DATA:
decodeFromRetrievedData();
break;
default:
throw new IllegalStateException("Unrecognized run reason: " + runReason);
}
}
getNextGenerator() 得到的是 DataCacheGenerator,并执行了 startNext 方法:
public boolean startNext() {
while (modelLoaders == null || !hasNextModelLoader()) {
...
Key originalKey = new DataCacheKey(sourceId, helper.getSignature());
cacheFile = helper.getDiskCache().get(originalKey);
if (cacheFile != null) {
this.sourceKey = sourceId;
modelLoaders = helper.getModelLoaders(cacheFile);
modelLoaderIndex = 0;
}
}
loadData = null;
boolean started = false;
while (!started && hasNextModelLoader()) {
ModelLoader<File, ?> modelLoader = modelLoaders.get(modelLoaderIndex++);
loadData =
modelLoader.buildLoadData(
cacheFile, helper.getWidth(), helper.getHeight(), helper.getOptions());
if (loadData != null && helper.hasLoadPath(loadData.fetcher.getDataClass())) {
started = true;
loadData.fetcher.loadData(helper.getPriority(), this);
}
}
return started;
}
调用了 cacheFile = helper.getDiskCache().get(originalKey) 获取 cacheFile 对象,此处的 helper 是 DecodeHelper,getDiskCache() 方法:
DiskCache getDiskCache() {
return diskCacheProvider.getDiskCache();
}
获取到的是 DiskCache,是从 diskCacheProvider 中获取,而 diskCacheProvider 的实现在 Engine 类中:
private static class LazyDiskCacheProvider implements DecodeJob.DiskCacheProvider {
private final DiskCache.Factory factory;
private volatile DiskCache diskCache;
LazyDiskCacheProvider(DiskCache.Factory factory) {
this.factory = factory;
}
@VisibleForTesting
synchronized void clearDiskCacheIfCreated() {
if (diskCache == null) {
return;
}
diskCache.clear();
}
@Override
public DiskCache getDiskCache() {
if (diskCache == null) {
synchronized (this) {
if (diskCache == null) {
diskCache = factory.build();
}
if (diskCache == null) {
diskCache = new DiskCacheAdapter();
}
}
}
return diskCache;
}
}
而 DiskCache 是从 DiskCache.Factory 中获取,DiskCache.Factory 的实现类是 DiskLruCacheFactory,通过调用 factory.build() 方法:
public DiskCache build() {
File cacheDir = cacheDirectoryGetter.getCacheDirectory();
if (cacheDir == null) {
return null;
}
if (!cacheDir.mkdirs() && (!cacheDir.exists() || !cacheDir.isDirectory())) {
return null;
}
return DiskLruCacheWrapper.create(cacheDir, diskCacheSize);
}
}
通过 DiskLruCacheWrapper 的 create 方法去创建 DiskLruCacheWrapper,而上面的 getDiskCache 的实现就是在此:
private synchronized DiskLruCache getDiskCache() throws IOException {
if (diskLruCache == null) {
diskLruCache = DiskLruCache.open(directory, APP_VERSION, VALUE_COUNT, maxSize);
}
return diskLruCache;
}
这里可以看到,最终Glide的磁盘缓存通过DiskLruCache来实现。
问:Glide是如果管理内存泄漏问题的?
答:Glide在使用时调用了into方法,传入一个View(ImageView),在Activity/Fragment关闭时,页面已经不存在,但Glide的加载线程任然可能持有ImageView,就可能造成内存泄漏。Glide在with方法传入一个上下文,通过上下文来获取或生成一个无页面的Fragment来实现Glide与使用者的生命周期的同步。
//在Activity中使用
public RequestManager get(@NonNull FragmentActivity activity) {
if (Util.isOnBackgroundThread()) { //判断是否是在主线程,如果不是主线程中使用,则传入的是application的Context
return get(activity.getApplicationContext());
} else {
assertNotDestroyed(activity);
FragmentManager fm = activity.getSupportFragmentManager();
return supportFragmentGet(activity, fm, /*parentHint=*/ null, isActivityVisible(activity));
}
}
//后面就详细去看了,最终是将生命周期的状态监听交给RequestManager来管理的
网友评论