此次来面试的是一个有着5年工做经验的小伙,截取了一段对话以下:java
面试官:我看你写到Glide,为何用Glide,而不选择其它图片加载框架?
小伙:Glide 使用简单,链式调用,很方便,一直用这个。
面试官:有看过它的源码吗?跟其它图片框架相比有哪些优点?
小伙:没有,只是在项目中使用而已~
面试官:假如如今不让你用开源库,须要你本身写一个图片加载框架,你会考虑哪些方面的问题,说说大概的思路。
小伙:额~,压缩吧。
面试官:还有吗?
小伙:额~,这个没写过。android
说到图片加载框架,你们最熟悉的莫过于Glide了,但我却不推荐简历上写熟悉Glide,除非你熟读它的源码,或者参与Glide的开发和维护。c++
在通常面试中,遇到图片加载问题的频率通常不会过低,只是问法会有一些差别,例如:git
带着问题进入正文~github
Glide因为其口碑好,不少开发者直接在项目中使用,使用方法至关简单面试
一、添加依赖:数组
implementation 'com.github.bumptech.glide:glide:4.10.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.10.0'
复制代码
二、添加网络权限缓存
<uses-permission android:name="android.permission.INTERNET" />
复制代码
三、一句代码加载图片到ImageViewbash
Glide.with(this).load(imgUrl).into(mIv1);
复制代码
进阶一点的用法,参数设置
RequestOptions options = new RequestOptions()
.placeholder(R.drawable.ic_launcher_background)
.error(R.mipmap.ic_launcher)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.override(200, 100);
Glide.with(this)
.load(imgUrl)
.apply(options)
.into(mIv2);
复制代码
使用Glide加载图片如此简单,这让不少开发者省下本身处理图片的时间,图片加载工做所有交给Glide来就完事,同时,很容易就把图片处理的相关知识点忘掉。
从前段时间面试的状况,我发现了这个现象:简历上写熟悉Glide的,基本都是熟悉使用方法,不少3年-6年工做经验,除了说Glide使用方便,不清楚Glide跟其余图片框架如Fresco的对比有哪些优缺点。
首先,当下流行的图片加载框架有那么几个,能够拿 Glide 跟Fresco对比,例如这些:
Glide:
Fresco:
对于通常App来讲,Glide彻底够用,而对于图片需求比较大的App,为了防止加载大量图片致使OOM,Fresco 会更合适一些。并非说用Glide会致使OOM,Glide默认用的内存缓存是LruCache,内存不会一直往上涨。
首先,梳理一下必要的图片加载框架的需求:
固然,还有一些不是必要的需求,例如加载动画等。
线程池,多少个?
缓存通常有三级,内存缓存、硬盘、网络。
因为网络会阻塞,因此读内存和硬盘能够放在一个线程池,网络须要另一个线程池,网络也能够采用Okhttp内置的线程池。
读硬盘和读网络须要放在不一样的线程池中处理,因此用两个线程池比较合适。
Glide 必然也须要多个线程池,看下源码是否是这样
public final class GlideBuilder {
...
private GlideExecutor sourceExecutor; //加载源文件的线程池,包括网络加载
private GlideExecutor diskCacheExecutor; //加载硬盘缓存的线程池
...
private GlideExecutor animationExecutor; //动画线程池
复制代码
Glide使用了三个线程池,不考虑动画的话就是两个。
图片异步加载成功,须要在主线程去更新ImageView,
不管是RxJava、EventBus,仍是Glide,只要是想从子线程切换到Android主线程,都离不开Handler。
看下Glide 相关源码:
class EngineJob<R> implements DecodeJob.Callback<R>,Poolable {
private static final EngineResourceFactory DEFAULT_FACTORY = new EngineResourceFactory();
//建立Handler
private static final Handler MAIN_THREAD_HANDLER =
new Handler(Looper.getMainLooper(), new MainThreadCallback());
复制代码
问RxJava是彻底用Java语言写的,那怎么实现从子线程切换到Android主线程的? 依然有不少3-6年的开发答不上来这个很基础的问题,并且只要是这个问题回答不出来的,接下来有关于原理的问题,基本都答不上来。
有很多工做了不少年的Android开发不知道鸿洋、郭霖、玉刚说,不知道掘金是个啥玩意,心里估计会想是否是还有叫掘银掘铁的(我不知道有没有)。
我想表达的是,干这一行,真的是须要有对技术的热情,不断学习,不怕别人比你优秀,就怕比你优秀的人比你还努力,而你殊不知道。
咱们常说的图片三级缓存:内存缓存、硬盘缓存、网络。
通常都是用LruCache
Glide 默认内存缓存用的也是LruCache,只不过并无用Android SDK中的LruCache,不过内部一样是基于LinkHashMap,因此原理是同样的。
// -> GlideBuilder#build
if (memoryCache == null) {
memoryCache = new LruResourceCache(memorySizeCalculator.getMemoryCacheSize());
}
复制代码
既然说到LruCache ,必需要了解一下LruCache的特色和源码:
为何用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);
}
...
复制代码
LruCache 构造方法里建立一个LinkedHashMap,accessOrder 参数传true,表示按照访问顺序排序,数据存储基于LinkedHashMap。
先看看LinkedHashMap 的原理吧
LinkedHashMap 继承 HashMap,在 HashMap 的基础上进行扩展,put 方法并无重写,说明LinkedHashMap遵循HashMap的数组加链表的结构,
LinkedHashMap重写了 createEntry 方法。
看下HashMap 的 createEntry 方法
void createEntry(int hash, K key, V value, int bucketIndex) {
HashMapEntry<K,V> e = table[bucketIndex];
table[bucketIndex] = new HashMapEntry<>(hash, key, value, e);
size++;
}
复制代码
HashMap的数组里面放的是HashMapEntry
对象
看下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++;
}
复制代码
LinkedHashMap的数组里面放的是LinkedHashMapEntry
对象
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
方法,用于新增和删除链表节点。
LinkedHashMapEntry#addBefore
将一个数据添加到Header的前面
private void addBefore(LinkedHashMapEntry<K,V> existingEntry) {
after = existingEntry;
before = existingEntry.before;
before.after = this;
after.before = this;
}
复制代码
existingEntry 传的都是链表头header,将一个节点添加到header节点前面,只须要移动链表指针便可,添加新数据都是放在链表头header 的before位置,链表头节点header的before是最新访问的数据,header的after则是最旧的数据。
再看下LinkedHashMapEntry#remove
private void remove() {
before.after = after;
after.before = before;
}
复制代码
链表节点的移除比较简单,改变指针指向便可。
再看下LinkHashMap的put 方法
public final V put(K key, V value) {
V previous;
synchronized (this) {
putCount++;
//size增长
size += safeSizeOf(key, value);
// 一、linkHashMap的put方法
previous = map.put(key, value);
if (previous != null) {
//若是有旧的值,会覆盖,因此大小要减掉
size -= safeSizeOf(key, previous);
}
}
trimToSize(maxSize);
return previous;
}
复制代码
LinkedHashMap 结构能够用这种图表示
LinkHashMap 的 put方法和get方法最后会调用trimToSize
方法,LruCache 重写trimToSize
方法,判断内存若是超过必定大小,则移除最老的数据
LruCache#trimToSize,移除最老的数据
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);
}
}
复制代码
对LinkHashMap 还不是很理解的话能够参考:
图解LinkedHashMap原理
LruCache小结:
依赖:
implementation 'com.jakewharton:disklrucache:2.0.2'
DiskLruCache 跟 LruCache 实现思路是差很少的,同样是设置一个总大小,每次往硬盘写文件,总大小超过阈值,就会将旧的文件删除。简单看下remove操做:
// DiskLruCache 内部也是用LinkedHashMap
private final LinkedHashMap<String, Entry> lruEntries =
new LinkedHashMap<String, Entry>(0, 0.75f, true);
...
public synchronized boolean remove(String key) throws IOException {
checkNotClosed();
validateKey(key);
Entry entry = lruEntries.get(key);
if (entry == null || entry.currentEditor != null) {
return false;
}
//一个key可能对应多个value,hash冲突的状况
for (int i = 0; i < valueCount; i++) {
File file = entry.getCleanFile(i);
//经过 file.delete() 删除缓存文件,删除失败则抛异常
if (file.exists() && !file.delete()) {
throw new IOException("failed to delete " + file);
}
size -= entry.lengths[i];
entry.lengths[i] = 0;
}
...
return true;
}
复制代码
能够看到 DiskLruCache 一样是利用LinkHashMap的特色,只不过数组里面存的 Entry 有点变化,Editor 用于操做文件。
private final class Entry {
private final String key;
private final long[] lengths;
private boolean readable;
private Editor currentEditor;
private long sequenceNumber;
...
}
复制代码
加载图片很是重要的一点是须要防止OOM,上面的LruCache缓存大小设置,能够有效防止OOM,可是当图片需求比较大,可能须要设置一个比较大的缓存,这样的话发生OOM的几率就提升了,那应该探索其它防止OOM的方法。
回顾一下Java的四大引用:
private Context context;
怎么理解强引用:
强引用对象的回收时机依赖垃圾回收算法,咱们常说的可达性分析算法,当Activity销毁的时候,Activity会跟GCRoot断开,至于GCRoot是谁?这里能够大胆猜测,Activity对象的建立是在ActivityThread中,ActivityThread要回调Activity的各个生命周期,确定是持有Activity引用的,那么这个GCRoot能够认为就是ActivityThread,当Activity 执行onDestroy的时候,ActivityThread 就会断开跟这个Activity的联系,Activity到GCRoot不可达,因此会被垃圾回收器标记为可回收对象。
软引用的设计就是应用于会发生OOM的场景,大内存对象如Bitmap,能够经过 SoftReference 修饰,防止大对象形成OOM,看下这段代码
private static LruCache<String, SoftReference<Bitmap>> mLruCache = new LruCache<String, SoftReference<Bitmap>>(10 * 1024){
@Override
protected int sizeOf(String key, SoftReference<Bitmap> value) {
//默认返回1,这里应该返回Bitmap占用的内存大小,单位:K
//Bitmap被回收了,大小是0
if (value.get() == null){
return 0;
}
return value.get().getByteCount() /1024;
}
};
复制代码
LruCache里存的是软引用对象,那么当内存不足的时候,Bitmap会被回收,也就是说经过SoftReference修饰的Bitmap就不会致使OOM。
固然,这段代码存在一些问题,Bitmap被回收的时候,LruCache剩余的大小应该从新计算,能够写个方法,当Bitmap取出来是空的时候,LruCache清理一下,从新计算剩余内存;
还有另外一个问题,就是内存不足时软引用中的Bitmap被回收的时候,这个LruCache就形同虚设,至关于内存缓存失效了,必然出现效率问题。
当内存不足的时候,Activity、Fragment会调用onLowMemory
方法,能够在这个方法里去清除缓存,Glide使用的就是这一种方式来防止OOM。
//Glide
public void onLowMemory() {
clearMemory();
}
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();
}
复制代码
咱们知道,系统为每一个进程,也就是每一个虚拟机分配的内存是有限的,早期的16M、32M,如今100+M,
虚拟机的内存划分主要有5部分:
而对象的分配通常都是在堆中,堆是JVM中最大的一块内存,OOM通常都是发生在堆中。
Bitmap 之因此占内存大不是由于对象自己大,而是由于Bitmap的像素数据, Bitmap的像素数据大小 = 宽 * 高 * 1像素占用的内存。
1像素占用的内存是多少?不一样格式的Bitmap对应的像素占用内存是不一样的,具体是多少呢?
在Fresco中看到以下定义代码
/**
* Bytes per pixel definitions
*/
public static final int ALPHA_8_BYTES_PER_PIXEL = 1;
public static final int ARGB_4444_BYTES_PER_PIXEL = 2;
public static final int ARGB_8888_BYTES_PER_PIXEL = 4;
public static final int RGB_565_BYTES_PER_PIXEL = 2;
public static final int RGBA_F16_BYTES_PER_PIXEL = 8;
复制代码
若是Bitmap使用 RGB_565
格式,则1像素占用 2 byte,ARGB_8888
格式则占4 byte。
在选择图片加载框架的时候,能够将内存占用这一方面考虑进去,更少的内存占用意味着发生OOM的几率越低。 Glide内存开销是Picasso的一半,就是由于默认Bitmap格式不一样。
至于宽高,是指Bitmap的宽高,怎么计算的呢?看BitmapFactory.Options
的 outWidth
/**
* The resulting width of the bitmap. If {@link #inJustDecodeBounds} is
* set to false, this will be width of the output bitmap after any
* scaling is applied. If true, it will be the width of the input image
* without any accounting for scaling.
*
* <p>outWidth will be set to -1 if there is an error trying to decode.</p>
*/
public int outWidth;
复制代码
看注释的意思,若是 BitmapFactory.Options
中指定 inJustDecodeBounds
为true,则为原图宽高,若是是false,则是缩放后的宽高。因此咱们通常能够经过压缩来减少Bitmap像素占用内存。
扯远了,上面分析了Bitmap像素数据大小的计算,只是说明Bitmap像素数据为何那么大。那是否可让像素数据不放在java堆中,而是放在native堆中呢?听说Android 3.0到8.0 之间Bitmap像素数据存在Java堆,而8.0以后像素数据存到native堆中,是否是真的?看下源码就知道了~
java层建立Bitmap方法
public static Bitmap createBitmap(@Nullable DisplayMetrics display, int width, int height,
@NonNull Config config, boolean hasAlpha, @NonNull ColorSpace colorSpace) {
...
Bitmap bm;
...
if (config != Config.ARGB_8888 || colorSpace == ColorSpace.get(ColorSpace.Named.SRGB)) {
//最终都是经过native方法建立
bm = nativeCreate(null, 0, width, width, height, config.nativeInt, true, null, null);
} else {
bm = nativeCreate(null, 0, width, width, height, config.nativeInt, true,
d50.getTransform(), parameters);
}
...
return bm;
}
复制代码
Bitmap 的建立是经过native方法 nativeCreate
对应源码 8.0.0_r4/xref/frameworks/base/core/jni/android/graphics/Bitmap.cpp
//Bitmap.cpp
static const JNINativeMethod gBitmapMethods[] = {
{ "nativeCreate", "([IIIIIIZ[FLandroid/graphics/ColorSpace$Rgb$TransferParameters;)Landroid/graphics/Bitmap;",
(void*)Bitmap_creator },
...
复制代码
JNI动态注册,nativeCreate 方法 对应 Bitmap_creator
;
//Bitmap.cpp
static jobject Bitmap_creator(JNIEnv* env, jobject, jintArray jColors,
jint offset, jint stride, jint width, jint height,
jint configHandle, jboolean isMutable,
jfloatArray xyzD50, jobject transferParameters) {
...
//1. 申请堆内存,建立native层Bitmap
sk_sp<Bitmap> nativeBitmap = Bitmap::allocateHeapBitmap(&bitmap, NULL);
if (!nativeBitmap) {
return NULL;
}
...
//2.建立java层Bitmap
return createBitmap(env, nativeBitmap.release(), getPremulBitmapCreateFlags(isMutable));
}
复制代码
主要两个步骤:
allocateHeapBitmap
方法//
static sk_sp<Bitmap> allocateHeapBitmap(size_t size, const SkImageInfo& info, size_t rowBytes,
SkColorTable* ctable) {
// calloc 是c++ 的申请内存函数
void* addr = calloc(size, 1);
if (!addr) {
return nullptr;
}
return sk_sp<Bitmap>(new Bitmap(addr, size, info, rowBytes, ctable));
}
复制代码
能够看到经过c++的 calloc
函数申请了一块内存空间,而后建立native层Bitmap对象,把内存地址传过去,也就是native层的Bitmap数据(像素数据)是存在native堆中。
//Bitmap.cpp
jobject createBitmap(JNIEnv* env, Bitmap* bitmap,
int bitmapCreateFlags, jbyteArray ninePatchChunk, jobject ninePatchInsets,
int density) {
...
BitmapWrapper* bitmapWrapper = new BitmapWrapper(bitmap);
//经过JNI回调Java层,调用java层的Bitmap构造方法
jobject obj = env->NewObject(gBitmap_class, gBitmap_constructorMethodID,
reinterpret_cast<jlong>(bitmapWrapper), bitmap->width(), bitmap->height(), density,
isMutable, isPremultiplied, ninePatchChunk, ninePatchInsets);
...
return obj;
}
复制代码
env->NewObject,经过JNI建立Java层Bitmap对象,gBitmap_class,gBitmap_constructorMethodID
这些变量是什么意思,看下面这个方法,对应java层的Bitmap的类名和构造方法。
//Bitmap.cpp
int register_android_graphics_Bitmap(JNIEnv* env)
{
gBitmap_class = MakeGlobalRefOrDie(env, FindClassOrDie(env, "android/graphics/Bitmap"));
gBitmap_nativePtr = GetFieldIDOrDie(env, gBitmap_class, "mNativePtr", "J");
gBitmap_constructorMethodID = GetMethodIDOrDie(env, gBitmap_class, "<init>", "(JIIIZZ[BLandroid/graphics/NinePatch$InsetStruct;)V");
gBitmap_reinitMethodID = GetMethodIDOrDie(env, gBitmap_class, "reinit", "(IIZ)V");
gBitmap_getAllocationByteCountMethodID = GetMethodIDOrDie(env, gBitmap_class, "getAllocationByteCount", "()I");
return android::RegisterMethodsOrDie(env, "android/graphics/Bitmap", gBitmapMethods,
NELEM(gBitmapMethods));
}
复制代码
8.0 的Bitmap建立就两个点:
像素数据是存在native层Bitmap,也就是证实8.0的Bitmap像素数据存在native堆中。
直接看native层的方法,
/7.0.0_r31/xref/frameworks/base/core/jni/android/graphics/Bitmap.cpp
//JNI动态注册
static const JNINativeMethod gBitmapMethods[] = {
{ "nativeCreate", "([IIIIIIZ)Landroid/graphics/Bitmap;",
(void*)Bitmap_creator },
...
static jobject Bitmap_creator(JNIEnv* env, jobject, jintArray jColors,
jint offset, jint stride, jint width, jint height,
jint configHandle, jboolean isMutable) {
...
//1.经过这个方法来建立native层Bitmap
Bitmap* nativeBitmap = GraphicsJNI::allocateJavaPixelRef(env, &bitmap, NULL);
...
return GraphicsJNI::createBitmap(env, nativeBitmap,
getPremulBitmapCreateFlags(isMutable));
}
复制代码
native层Bitmap 建立是经过GraphicsJNI::allocateJavaPixelRef
,看看里面是怎么分配的, GraphicsJNI 的实现类是Graphics.cpp
android::Bitmap* GraphicsJNI::allocateJavaPixelRef(JNIEnv* env, SkBitmap* bitmap,
SkColorTable* ctable) {
const SkImageInfo& info = bitmap->info();
size_t size;
//计算须要的空间大小
if (!computeAllocationSize(*bitmap, &size)) {
return NULL;
}
// we must respect the rowBytes value already set on the bitmap instead of
// attempting to compute our own.
const size_t rowBytes = bitmap->rowBytes();
// 1. 建立一个数组,经过JNI在java层建立的
jbyteArray arrayObj = (jbyteArray) env->CallObjectMethod(gVMRuntime,
gVMRuntime_newNonMovableArray,
gByte_class, size);
...
// 2. 获取建立的数组的地址
jbyte* addr = (jbyte*) env->CallLongMethod(gVMRuntime, gVMRuntime_addressOf, arrayObj);
...
//3. 建立Bitmap,传这个地址
android::Bitmap* wrapper = new android::Bitmap(env, arrayObj, (void*) addr,
info, rowBytes, ctable);
wrapper->getSkBitmap(bitmap);
// since we're already allocated, we lockPixels right away // HeapAllocator behaves this way too bitmap->lockPixels(); return wrapper; } 复制代码
能够看到,7.0 像素内存的分配是这样的:
由此说明,7.0 的Bitmap像素数据是放在java堆的。
固然,3.0 如下Bitmap像素内存听说也是放在native堆的,可是须要手动释放native层的Bitmap,也就是须要手动调用recycle方法,native层内存才会被回收。这个你们能够本身去看源码验证。
Java层的Bitmap对象由垃圾回收器自动回收,而native层Bitmap印象中咱们是不须要手动回收的,源码中如何处理的呢?
记得有个面试题是这样的:
说说final、finally、finalize 的关系
三者除了长得像,其实没有半毛钱关系,final、finally你们都用的比较多,而 finalize
用的少,或者没用过,finalize
是 Object 类的一个方法,注释是这样的:
/**
* Called by the garbage collector on an object when garbage collection
* determines that there are no more references to the object.
* A subclass overrides the {@code finalize} method to dispose of
* system resources or to perform other cleanup.
* <p>
...**/
protected void finalize() throws Throwable { }
复制代码
意思是说,垃圾回收器确认这个对象没有其它地方引用到它的时候,会调用这个对象的finalize
方法,子类能够重写这个方法,作一些释放资源的操做。
在6.0之前,Bitmap 就是经过这个finalize 方法来释放native层对象的。 6.0 Bitmap.java
Bitmap(long nativeBitmap, byte[] buffer, int width, int height, int density,
boolean isMutable, boolean requestPremultiplied,
byte[] ninePatchChunk, NinePatch.InsetStruct ninePatchInsets) {
...
mNativePtr = nativeBitmap;
//1.建立 BitmapFinalizer
mFinalizer = new BitmapFinalizer(nativeBitmap);
int nativeAllocationByteCount = (buffer == null ? getByteCount() : 0);
mFinalizer.setNativeAllocationByteCount(nativeAllocationByteCount);
}
private static class BitmapFinalizer {
private long mNativeBitmap;
// Native memory allocated for the duration of the Bitmap,
// if pixel data allocated into native memory, instead of java byte[]
private int mNativeAllocationByteCount;
BitmapFinalizer(long nativeBitmap) {
mNativeBitmap = nativeBitmap;
}
public void setNativeAllocationByteCount(int nativeByteCount) {
if (mNativeAllocationByteCount != 0) {
VMRuntime.getRuntime().registerNativeFree(mNativeAllocationByteCount);
}
mNativeAllocationByteCount = nativeByteCount;
if (mNativeAllocationByteCount != 0) {
VMRuntime.getRuntime().registerNativeAllocation(mNativeAllocationByteCount);
}
}
@Override
public void finalize() {
try {
super.finalize();
} catch (Throwable t) {
// Ignore
} finally {
//2.就是这里了,
setNativeAllocationByteCount(0);
nativeDestructor(mNativeBitmap);
mNativeBitmap = 0;
}
}
}
复制代码
在Bitmap构造方法建立了一个 BitmapFinalizer
类,重写finalize 方法,在java层Bitmap被回收的时候,BitmapFinalizer 对象也会被回收,finalize 方法确定会被调用,在里面释放native层Bitmap对象。
6.0 以后作了一些变化,BitmapFinalizer 没有了,被NativeAllocationRegistry取代。
例如 8.0 Bitmap构造方法
Bitmap(long nativeBitmap, int width, int height, int density,
boolean isMutable, boolean requestPremultiplied,
byte[] ninePatchChunk, NinePatch.InsetStruct ninePatchInsets) {
...
mNativePtr = nativeBitmap;
long nativeSize = NATIVE_ALLOCATION_SIZE + getAllocationByteCount();
// 建立NativeAllocationRegistry这个类,调用registerNativeAllocation 方法
NativeAllocationRegistry registry = new NativeAllocationRegistry(
Bitmap.class.getClassLoader(), nativeGetNativeFinalizer(), nativeSize);
registry.registerNativeAllocation(this, nativeBitmap);
}
复制代码
NativeAllocationRegistry 就不分析了, 无论是BitmapFinalizer 仍是NativeAllocationRegistry,目的都是在java层Bitmap被回收的时候,将native层Bitmap对象也回收掉。 通常状况下咱们无需手动调用recycle方法,由GC去盘它便可。
上面分析了Bitmap像素存储位置,咱们知道,Android 8.0 以后Bitmap像素内存放在native堆,Bitmap致使OOM的问题基本不会在8.0以上设备出现了(没有内存泄漏的状况下),那8.0 如下设备怎么办?赶忙升级或换手机吧~
咱们换手机固然没问题,可是并非全部人都能跟上Android系统更新的步伐,因此,问题仍是要解决~
Fresco 之因此能跟Glide 正面交锋,必然有其独特之处,文中开头列出 Fresco 的优势是:“在5.0如下(最低2.3)系统,Fresco将图片放到一个特别的内存区域(Ashmem区)” 这个Ashmem区是一块匿名共享内存,Fresco 将Bitmap像素放到共享内存去了,共享内存是属于native堆内存。
Fresco 关键源码在 PlatformDecoderFactory
这个类
public class PlatformDecoderFactory {
/**
* Provide the implementation of the PlatformDecoder for the current platform using the provided
* PoolFactory
*
* @param poolFactory The PoolFactory
* @return The PlatformDecoder implementation
*/
public static PlatformDecoder buildPlatformDecoder(
PoolFactory poolFactory, boolean gingerbreadDecoderEnabled) {
//8.0 以上用 OreoDecoder 这个解码器
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
int maxNumThreads = poolFactory.getFlexByteArrayPoolMaxNumThreads();
return new OreoDecoder(
poolFactory.getBitmapPool(), maxNumThreads, new Pools.SynchronizedPool<>(maxNumThreads));
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
//大于5.0小于8.0用 ArtDecoder 解码器
int maxNumThreads = poolFactory.getFlexByteArrayPoolMaxNumThreads();
return new ArtDecoder(
poolFactory.getBitmapPool(), maxNumThreads, new Pools.SynchronizedPool<>(maxNumThreads));
} else {
if (gingerbreadDecoderEnabled && Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
//小于4.4 用 GingerbreadPurgeableDecoder 解码器
return new GingerbreadPurgeableDecoder();
} else {
//这个就是4.4到5.0 用的解码器了
return new KitKatPurgeableDecoder(poolFactory.getFlexByteArrayPool());
}
}
}
}
复制代码
8.0 先不看了,看一下 4.4 如下是怎么获得Bitmap的,看下GingerbreadPurgeableDecoder
这个类有个获取Bitmap的方法
//GingerbreadPurgeableDecoder
private Bitmap decodeFileDescriptorAsPurgeable(
CloseableReference<PooledByteBuffer> bytesRef,
int inputLength,
byte[] suffix,
BitmapFactory.Options options) {
// MemoryFile :匿名共享内存
MemoryFile memoryFile = null;
try {
//将图片数据拷贝到匿名共享内存
memoryFile = copyToMemoryFile(bytesRef, inputLength, suffix);
FileDescriptor fd = getMemoryFileDescriptor(memoryFile);
if (mWebpBitmapFactory != null) {
// 建立Bitmap,Fresco本身写了一套建立Bitmap方法
Bitmap bitmap = mWebpBitmapFactory.decodeFileDescriptor(fd, null, options);
return Preconditions.checkNotNull(bitmap, "BitmapFactory returned null");
} else {
throw new IllegalStateException("WebpBitmapFactory is null");
}
}
}
复制代码
捋一捋,4.4如下,Fresco 使用匿名共享内存来保存Bitmap数据,首先将图片数据拷贝到匿名共享内存中,而后使用Fresco本身写的加载Bitmap的方法。
Fresco对不一样Android版本使用不一样的方式去加载Bitmap,至于4.4-5.0,5.0-8.0,8.0 以上,对应另外三个解码器,你们能够从PlatformDecoderFactory
这个类入手,本身去分析,思考为何不一样平台要分这么多个解码器,8.0 如下都用匿名共享内存很差吗?期待你在评论区跟你们分享~
曾经在Vivo驻场开发,带有头像功能的页面被测出内存泄漏,缘由是SDK中有个加载网络头像的方法,持有ImageView引用致使的。
固然,修改也比较简单粗暴,将ImageView用WeakReference修饰就完事了。
事实上,这种方式虽然解决了内存泄露问题,可是并不完美,例如在界面退出的时候,咱们除了但愿ImageView被回收,同时但愿加载图片的任务能够取消,队未执行的任务能够移除。
Glide的作法是监听生命周期回调,看 RequestManager
这个类
public void onDestroy() {
targetTracker.onDestroy();
for (Target<?> target : targetTracker.getAll()) {
//清理任务
clear(target);
}
targetTracker.clear();
requestTracker.clearRequests();
lifecycle.removeListener(this);
lifecycle.removeListener(connectivityMonitor);
mainHandler.removeCallbacks(addSelfToLifecycle);
glide.unregisterRequestManager(this);
}
复制代码
在Activity/fragment 销毁的时候,取消图片加载任务,细节你们能够本身去看源码。
因为RecyclerView或者LIstView的复用机制,网络加载图片开始的时候ImageView是第一个item的,加载成功以后ImageView因为复用可能跑到第10个item去了,在第10个item显示第一个item的图片确定是错的。
常规的作法是给ImageView设置tag,tag通常是图片地址,更新ImageView以前判断tag是否跟url一致。
固然,能够在item从列表消失的时候,取消对应的图片加载任务。要考虑放在图片加载框架作仍是放在UI作比较合适。
列表滑动,会有不少图片请求,若是是第一次进入,没有缓存,那么队列会有不少任务在等待。因此在请求网络图片以前,须要判断队列中是否已经存在该任务,存在则不加到队列去。
本文经过Glide开题,分析一个图片加载框架必要的需求,以及各个需求涉及到哪些技术和原理。
文中也遗留一些问题,例如:
Fresco为何要在不一样Android版本上使用不一样解码器去获取Bitmap,8.0如下都用匿名共享内存不能够吗?期待你主动学习而且在评论区跟你们分享~
就这样,欢迎评论区留言~
相关参考文章:
图解LinkedHashMap原理
谈谈fresco的bitmap内存分配
我在掘金发布的其它文章:
总结UI原理和高级的UI优化方式
面试官:说说多线程并发问题
面试官又来了:你的app卡顿过吗?
面试官:今日头条启动很快,你以为多是作了哪些优化?