上文已经说了,DiskCacheStrategy.RESOURCE
缓存的是变换后的资源,DiskCacheStrategy.DATA
缓存的是变换前的资源
举个例子,同一张图片,我们先在100*100
的View
是展示,再在200*200
的View
上展示
如果不缓存变换后的类型相当于每次都要进行一次变换操作,如果不缓存原始数据则每次都要去重新下载数据
如下可以看出,两种缓存的key
不一样
DiskCacheStrategy.RESOURCE
currentKey = new ResourceCacheKey(helper.getArrayPool(),sourceId,helper.getSignature(),helper.getWidth(),helper.getHeight(),transformation,resourceClass,helper.getOptions());
DiskCacheStrategy.DATA
DataCacheKey newOriginalKey = new DataCacheKey(loadData.sourceKey, helper.getSignature());
2.2.2 小结
本节主要介绍了Glide
磁盘缓存的几种策略并介绍了为什么需要两种磁盘缓存的原因
这里也没有贴什么源码,如果想要看源码的同学可参考:从源码的角度分析 Glide 缓存策略
3.Glide
做了哪些内存优化?
Glide
的内存优化主要也是对Bitmap
的优化,在回答这个问题前,我们可以想想有哪些常见的Bitmap
优化手段
1.当图片大小与View
大小不一致时,可以用inSampleSize
进行尺寸优化
2.图片所占内存即宽_高_每像素所占内存大小,不同的模式每个像素所占的内存大小不同,我们可以利用inpreferredconfig
配置
3.Bitmpa
所占内存比较大,如果频繁创建回收Bitmap
内存可能造成内存抖动,我们可以利用inBitmap
利用Bitmap
内存
4.内存缓存,上文我们已经介绍了Glide
的弱引用缓存与LRU
缓存
其实常见的Bitmap
内存优化也就这么几种了,不过我们在工作中比较少直接使用他们。
下面我们就介绍下Glide
中具体是怎么使用他们的.
3.1 尺寸优化
当装载图片的容器例如ImageView只有100*100
,而图片的分辨率为800 * 800
,这个时候将图片直接放置在容器上,很容易OOM
,同时也是对图片和内存资源的一种浪费。当容器的宽高都很小于图片的宽高,其实就需要对图片进行尺寸上的压缩,将图片的分辨率调整为ImageView
宽高的大小,一方面不会对图片的质量有影响,同时也可以很大程度上减少内存的占用
我们通常使用inSampleSize
对Bitmap
进行尺寸缩放
如果
inSampleSize
设置的值大于1,则请求解码器对原始的bitmap
进行子采样图像,然后返回较小的图片来减少内存的占用,例如inSampleSize
== 4,则采样后的图像宽高为原图像的1/4,而像素值为原图的1/16,也就是说采样后的图像所占内存也为原图所占内存的1/16;当inSampleSize
<=1时,就当作1来处理也就是和原图一样大小。另外最后一句还注明,inSampleSize
的值一直为2的幂,如1,2,4,8。任何其他的值也都是四舍五入到最接近2的幂。
//1
int widthScaleFactor = orientedSourceWidth / outWidth;
int heightScaleFactor = orientedSourceHeight / outHeight;
//2
int scaleFactor =
rounding == SampleSizeRounding.MEMORY
? Math.max(widthScaleFactor, heightScaleFactor)
: Math.min(widthScaleFactor, heightScaleFactor);
int powerOfTwoSampleSize;
//3
if (Build.VERSION.SDK_INT <= 23
&& NO_DOWNSAMPLE_PRE_N_MIME_TYPES.contains(options.outMimeType)) {
powerOfTwoSampleSize = 1;
} else {
//4
powerOfTwoSampleSize = Math.max(1, Integer.highestOneBit(scaleFactor));
//5
if (rounding == SampleSizeRounding.MEMORY
// exactScaleFactor由各个裁剪策略如CenterCrop重写得到,详情可见代码
&& powerOfTwoSampleSize < (1.f / exactScaleFactor)) {
powerOfTwoSampleSize = powerOfTwoSampleSize << 1;
}
}
options.inSampleSize = powerOfTwoSampleSize;
如上就是Glide
图片进行尺寸缩放相关的代码
1.首先计算出图片与View
的宽高比
2.根据缩放策略是省内存还是高品质,决定取宽高比的最大值还是最小值
3.当Build.VERSION.SDK_INT<=23
时,一些格式的图片不能缩放
4.highestOneBit
的功能是把我们计算的比例四舍五入到最接近2的幂
5.如果缩放策略为省内存,并且我们计算的SampleSize<exactScaleFactor
,将inSampleSize*2
如上就是Glide
图片加载时做尺寸优化的大概逻辑
3.2 图片格式优化
我们知道,Bitmap
所占内存大小,由宽*高*每像素所占内存
决定
上面的尺寸优化决定宽高,图片格式优化决定每像素所占内存
在API29
中,将Bitmap
分为ALPHA_8
, RGB_565
, ARGB_4444
, ARGB_8888
, RGBA_F16
, HARDWARE
六个等级。
ALPHA_8
:不存储颜色信息,每个像素占1个字节;RGB_565
:仅存储RGB
通道,每个像素占2个字节,对Bitmap
色彩没有高要求,可以使用该模式;ARGB_4444
:已弃用,用ARGB_8888
代替;ARGB_8888
:每个像素占用4个字节,保持高质量的色彩保真度,默认使用该模式;RGBA_F16
:每个像素占用8个字节,适合宽色域和HDR
;HARDWARE
:一种特殊的配置,减少了内存占用同时也加快了Bitmap
的绘制。
每个等级每个像素所占用的字节也都不一样,所存储的色彩信息也不同。同一张100像素的图片,ARGB_8888
就占了400字节,RGB_565
才占200字节,RGB_565在内存上取得了优势,但是Bitmap
的色彩值以及清晰度却不如ARGB_8888
模式下的Bitmap
值得注意的是在Glide4.0
之前,Glide
默认使用RGB565
格式,比较省内存
但是Glide4.0
之后,默认格式已经变成了ARGB_8888
格式了,这一优势也就不存在了。
这本身也就是质量与内存之间的取舍,如果应用所需图片的质量要求不高,也可以修改默认格式
//默认格式修改为了ARGB_8888
public static final Option DECODE_FORMAT =
Option.memory(
“com.bumptech.glide.load.resource.bitmap.Downsampler.DecodeFormat”, DecodeFormat.DEFAULT);
3.3 内存复用优化
Bitmap
所占内存比较大,如果我们频繁创建与回收Bitmap
,那么很容易造成内存抖动,所以我们应该尽量复用Bitmap
内存
Glide
主要使用了inBitmap
与BitmapPool
来实现内存的复用
3.3.1 inBitmap
介绍
在 Android 3.0(API 级别 11)
开始,系统引入了 BitmapFactory.Options.inBitmap
字段。如果设置了此选项,那么采用 Options
对象的解码方法会在生成目标 Bitmap
时尝试复用 inBitmap
,这意味着 inBitmap
的内存得到了重复使用,从而提高了性能,同时移除了内存分配和取消分配。不过 inBitmap
的使用方式存在某些限制,在 Android 4.4(API 级别 19)
之前系统仅支持复用大小相同的位图,4.4 之后只要 inBitmap
的大小比目标 Bitmap
大即可
3.3.2 BitmapPool
介绍
通过上文我们知道了可以通过inBitmap
复用内存,但是还需要一个地方存储可复用的Bitmap
,这就是BitmapPool
JDK
中的 ThreadPoolExecutor
相信大多数开发者都很熟悉,我们一般将之称为“线程池”。池化是一个很常见的概念,其目的都是为了实现对象复用,例如 ThreadPoolExecutor
就实现了线程的复用机制
BitmapPool
即实现了Bitmap
的池化
3.3.3 Glide
的应用
private static void setInBitmap(
BitmapFactory.Options options, BitmapPool bitmapPool, int width, int height) {
@Nullable Bitmap.Config expectedConfig = null;
if (expectedConfig == null) {
expectedConfig = options.inPreferredConfig;
}
// BitmapFactory will clear out the Bitmap before writing to it, so getDirty is safe.
options.inBitmap = bitmapPool.getDirty(width, height, expectedConfig);
}
如上即是Glide
设置inBitmap
的代码,向BitmapPool
中传入宽高与格式,得到一个可复用的对象,这样就实现了Bitmap
的内存复用
由于篇幅原因,详细的源码这里没有贴出来,想要了解更多的读者可参考:Coil 和 Glide 的 Bitmap 缓存复用机制
4.Glide
如何管理生命周期?
当我们在做一个网络请示时,页面退出时应该中止请示,不然容易造成内存泄漏
对于图片加载也是如此,我们在页面退出时应该中止请示,销毁资源。
但是我们使用Glide
的时候却不需要在页面退出时做什么操作,说明Glide
可以做到在页面关闭时自动释放资源
下面我们一起看下Glide
是如何实现的
主要是两步:
1.调用时通过Glide.with
传入context
,利用context
构建一个Fragment
2.监听Fragment
生命周期,销毁时释放Glide
资源
4.1 传入context
构建Fragment
//通过Activity拿到RequestManager
public RequestManager get(@NonNull Activity activity) {
//拿到当前Activity的FragmentManager
android.app.FragmentManager fm = activity.getFragmentManager();
//生成一个Fragment去绑定一个请求管理RequestManager
return fragmentGet(
activity, fm, /parentHint=/ null, isActivityVisible(activity));
}
private RequestManager fragmentGet(@NonNull Context context,
@NonNull android.app.FragmentManager fm,
@Nullable android.app.Fragment parentHint,
boolean isParentVisible) {
//①在当前Activity添加一个Fragment用于管理请求的生命周期
RequestManagerFragment current = getRequestManagerFragment(fm, parentHint, isParentVisible);
//获取RequestManager
RequestManager requestManager = current.getRequestManager();
//如果不存在RequestManager,则创建
if (requestManager == null) {
Glide glide = Glide.get(context);
//②构建RequestManager
//current.getGlideLifecycle()就是ActivityFragmentLifecycle,也就是构建RequestManager时会传入fragment中的ActivityFragmentLifecycle
requestManager =
factory.build(
glide, current.getGlideLifecycle(), current.getRequestManagerTreeNode(), context);
//将构建出来的RequestManager绑定到fragment中
current.setRequestManager(requestManager);
}
//返回当前请求的管理者
return requestManager;
}
如上所示:
1.在当前Activity
添加一个透明Fragment
用于管理请示生命周期
2.构建RequestManager
并传入Fragment
生命周期
4.2 RequestManager
监听生命周期
public class RequestManager implements LifecycleListener,
ModelTypes<RequestBuilder> {
RequestManager(
Glide glide,
Lifecycle lifecycle,
RequestManagerTreeNode treeNode,
RequestTracker requestTracker,
ConnectivityMonitorFactory factory,
Context context) {
…
//将当前对象注册到ActivityFragmentLifecycle
lifecycle.addListener(this);
}
//…
//RequestManager实现了fragment生命周期回调
@Override
public synchronized void onStart() {
resumeRequests();
targetTracker.onStart();
}
@Override
public synchronized void onStop() {
pauseRequests();
targetTracker.onStop();
}
@Override
public synchronized void onDestroy() {
targetTracker.onDestroy();
}
}
public class RequestManagerFragment extends Fragment {
//生命周期的关键就在ActivityFragmentLifecycle
private final ActivityFragmentLifecycle lifecycle;
public RequestManagerFragment() {
this(new ActivityFragmentLifecycle());
}
RequestManagerFragment(@NonNull ActivityFragmentLifecycle lifecycle) {
this.lifecycle = lifecycle;
}
@Override
public void onStart() {
super.onStart();
lifecycle.onStart();
}
@Override
public void onStop() {
super.onStop();
lifecycle.onStop();
}
@Override
public void onDestroy() {
super.onDestroy();
lifecycle.onDestroy();
unregisterFragmentWithRoot();
}
//…
}
逻辑很简单:Fragment
生命周期变化会回调RequestManager
生命周期,然后在进行相关的资源释放工作
4.3 小结
Glide.with(this)
绑定了Activity
的生命周期。在Activity
内新建了一个无UI
的Fragment
,这个Fragment
持有一个Lifecycle
,通过Lifecycle
在Fragment
关键生命周期通知RequestManager进
行相关从操作。在生命周期onStart
时继续加载,onStop
时暂停加载,onDestory
时停止加载任务和清除操作。
由于篇幅有限,这里没有贴太多代码,更多细节可参考:Glide生命周期管理
5.Glide
怎么做大图加载
对于图片加载还有种情况,就是单个图片非常巨大,并且还不允许压缩。比如显示:世界地图、清明上河图、微博长图等
首先不压缩,按照原图尺寸加载,那么屏幕肯定是不够大的,并且考虑到内存的情况,不可能一次性整图加载到内存中
所以这种情况的优化思路一般是局部加载,通过BitmapRegionDecoder
来实现
这种情况下通常Glide
只负责将图片下载下来,图片的加载由我们自定义的ImageView
来实现
5.1 BitmapRegionDecoder
介绍
BitmapRegionDecoder
主要用于显示图片的某一块矩形区域,如果你需要显示某个图片的指定区域,那么这个类非常合适。
对于该类的用法,非常简单,既然是显示图片的某一块区域,那么至少只需要一个方法去设置图片;一个方法传入显示的区域即可
举个例子:
//设置显示图片的中心区域
BitmapRegionDecoder bitmapRegionDecoder = BitmapRegionDecoder.newInstance(inputStream, false);
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.RGB_565;
Bitmap bitmap = bitmapRegionDecoder.decodeRegion(new Rect(width / 2 - 100, height / 2 - 100, width / 2 + 100, height / 2 + 100), options);
mImageView.setImageBitmap(bitmap);
更详细的实现可见:Android 高清加载巨图方案 拒绝压缩图片
不过这种方法虽然也能加载大图,但做的还不够,滑动时内存抖动,卡顿现象比较明显,不能用于线上
下面介绍一种可以用于线上的大图加载方案
5.2 可用于线上的大图加载方案
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)
最后
其实Android开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。
下面分享的腾讯、头条、阿里、美团、字节跳动等公司2019-2021年的高频面试题全套解析,博主还把这些技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,下面只是以图片的形式给大家展示一部分。
知识不体系?这里还有整理出来的Android进阶学习的思维脑图,给大家参考一个方向。
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
,博主还把这些技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,下面只是以图片的形式给大家展示一部分。
[外链图片转存中…(img-DypMbKmy-1713438309326)]
知识不体系?这里还有整理出来的Android进阶学习的思维脑图,给大家参考一个方向。
[外链图片转存中…(img-HcnYbOzf-1713438309327)]
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!