背景
做一下图片优化的总结吧。主要记录下大概的实现思路和代码,说得不对的地方,还请大佬指出来。
图片内存占用方式计算
计算公式
图片占用内存 = 图片质量 * 宽 * 高
- 图片质量:默认情况下,系统默认使用
ARGB_8888
作为像素点的数据格式,这样每个像素点就是4 Byte
的大小。 - 图片宽高:指的是实际加载到内存中的Bitmap宽高。
注意:系统在加载res目录下的资源图片的时候,会根据图片存放的不同目录做一次分辨率的转换
,而转换的规则是:
新图的高度 = 原图高度 * (设备的 dpi / 目录对应的 dpi ) 新图的宽度 = 原图宽度 * (设备的 dpi / 目录对应的 dpi )
设备dpi
- 进入adb shell
- 查看当前的dpi:vm density
- 自定义修改dpi:vm density XXX
- reset操作:vm density reset
例子
举个例子,我们一般会把切图文件放在xhdpi
下,有张100x100的切图。
- 在dpi为320的设备上,占用内存为:100 x 100 x 4=40000B(约39KB)
- 在dpi为480的设备上,占用内存为:100 x
(480/320)
x 100 x(480/320)
x 4=90,000B(约87KB)
所以对于切图资源,需要放在适当的dpi目录下
。
不同res目录下,占用内存是不一样的。
可以参考这篇文章:Android中一张图片占据的内存大小是如何计算
分析内存中的Bitmap对象
工具
Memory Profiler+Bitmap Preview
- 前提:
手机设备是8.0以下
的,这个预览功能还是比较方便的。 - 原因:Android 3.0-7.0,将Bitmap对象和像素数据统一放到Java堆中,Android 8.0之后将Bitmap放到Native中。
- 操作:通过AS的Memory Profiler,dump出当前app运行时的内存快照.hprof文件。选中内存中的Bitmap对象,右侧会出现一个Bitmap Preview功能,可以用来预览当前是哪张图片。
- 分析:一般我们主要针对分析内存占用Top N的Bitmap,看看能否进行优化。配合Bitmap Preview使用起来,还是比较方便的。
MAT+GIMP
- 用 MAT(Memory Analyzer Tool) 分析Android内存时,将 Bitmap 的 mBuffer 原始数据导出,使用 GIMP 软件打开,恢复原始图像。
- 使用教程可参考: Android中MAT、GIMP查看内存占用
单张图片的内存优化
本地图片
场景:当前测试设备是xxxhdpi,主工程xhdpi目录下放一张300x300的切图,在xml文件中的ImageView中设置src为这张图。
结果:
图片内存占用过大
:300x2x300x2x4=1440000(约1.37MB)对应ImageView和整个布局xml的infalte时间都会变长
,因为这个过程中获取图片资源(Resource#getDrawable),这个过程是在主线程进行的。
优化:
- 切图,需要存放在正确的dpi目录下。
- 自己实现异步加载,在后台线程加载图片资源,得到Bitmap对象后,再post到主线程给ImageView进行设置。
- 或者直接
使用fresco进行加载
,Fresco加载本地图片资源,默认是在个IO线程中进行解析后,再post到主线程进行显示。
图片源文件尺寸应该与目标ImageVIew相近
场景:Bitmap的宽高比View实际宽高大很多。有点浪费内存资源
。
优化:
- 注意设置适当大小的View
- 本地图的话,注意不要放错dpi目录。(比如把切图放在hdpi目录下,那在xxhdpi设备上加载出来的bitmap是会乘以相应系数的宽高的)
- 网络图的话,可以获取指定适当宽高的图片(需要服务端支持),
更快地下载到图片资源
,也可以节省带宽
- 进行
大图检测
,当Bitmap比View大太多的话,提示开发者进行优化。
减少像素点的大小
场景:系统默认以ARGB_8888
格式进行处理,那么每个像素点就是4 Byte的大小。改变这个格式,就可以改变每个像素点占用的内存大小。
优化:比如可以替换成RGB_565
,ARGB_4444等。减少每个像素点的大小,从而降低整张图片占据的内存大小。
降低分辨率
- inSampleSize
在加载图片的时候,设置BitmapFactory.Options.inSampleSize
之后,Bitmap的宽高都会缩小inSampleSize倍,这样实际加载的图片占用内存,将缩小为inSampleSize* inSampleSize分之一。
- Fresco#ResizeOptions
Resize 并不改变原始图片,它只在解码前修改内存中的图片大小。
在创建ImageRequest时,提供一个ResizeOptions
,指定对应的宽高即可。
Uri uri = "file:///mnt/sdcard/MyApp/myfile.jpg";
int width = 50, height = 50;
ImageRequest request = ImageRequestBuilder.newBuilderWithSource(uri)
.setResizeOptions(new ResizeOptions(width, height)) //设置ResizeOptions
.build();
PipelineDraweeController controller = Fresco.newDraweeControllerBuilder()
.setOldController(mDraweeView.getController())
.setImageRequest(request)
.build();
mSimpleDraweeView.setController(controller);
大图检测
Epic
Epic 是一个在虚拟机层面、以 Java Method 为粒度的 运行时 AOP Hook 框架
。它可以拦截本进程内部几乎任意的 Java 方法调用
,可用于实现 AOP 编程、运行时插桩、性能分析、安全审计等。
ImageView大图检测
Hook点:ImageView的setImageDrawable(drawable)
实现:自定义BigSizeImageHook类,hook ImageView的setImageDrawable方法,解析参数,可以得到bitmap宽高和view宽高,对比两者的大小关系。当Bitmap大小是view大小的n倍,就输出日志信息进行提示。
class BigSizeImageHook : XC_MethodHook() {
private val TAG = "ImageHook"
private val max_threshold = 2 //自定义一个阀值,当Bitmap宽高比view宽高大n倍,就输出对应的日志
override fun afterHookedMethod(param: MethodHookParam?) {
super.afterHookedMethod(param)
param ?: return
//拿到imageView和drawable对象
val imageView = param.thisObject as ImageView
val drawable = imageView.drawable
checkBitmap(imageView, drawable)
}
private fun checkBitmap(view: View, drawable: Drawable?) {
if (drawable is BitmapDrawable) {
val bitmap = drawable.bitmap
val viewWidth = imageView.width
val viewHeight = imageView.height
if (viewWidth > 0 && viewHeight > 0) {
//如果图片的宽高都大于view的2倍以上,则警告
if (bitmap.width >= viewWidth * max_threshold && bitmap.height >= viewHeight * max_threshold) {
wran(imageView, bitmap)
}
} else {
//当宽高等于0时,说明ImageView还没有进行绘制,使用ViewTreeObserver进行监听获取到宽高信息
imageView.viewTreeObserver.addOnPreDrawListener(object :
ViewTreeObserver.OnPreDrawListener {
override fun onPreDraw(): Boolean {
if (imageView.width > 0 && imageView.height > 0) {
if (bitmap.width >= viewWidth * max_threshold && bitmap.height >= viewHeight * max_threshold) {
wran(imageView, bitmap)
}
imageView.viewTreeObserver.removeOnPreDrawListener(this)
}
return true
}
})
}
}
}
//输出相关日志信息
private fun wran(imageView: ImageView, bitmap: Bitmap) {
val warnInfo = "Bitmap size too large, " +
"view size : (${imageView.width},${imageView.height}), " +
"bitmap size:(${bitmap.width},${bitmap.height}), " +
"view id:${getId(imageView)}, " +
"bitmap id:${bitmap.density}"
Log.d(TAG, "$warnInfo")
}
//获取view的id
private fun getId(view: View?): String? {
view ?: return "no-id"
return if (view.id == View.NO_ID) "no-id" else view.resources.getResourceName(view.id)
}
}
hook住ImageView的setImageDrawable方法,这样每个ImageView调用setImageDrawable的时候,都会走到上面BigSizeImageHook的检测逻辑中。
DexposedBridge.hookAllConstructors(ImageView::class.java, object : XC_MethodHook() {
override fun afterHookedMethod(param: MethodHookParam?) {
super.afterHookedMethod(param)
DexposedBridge.findAndHookMethod(
ImageView::class.java,
"setImageDrawable",
Drawable::class.java,
ImageHook()
)
}
})
Fresco大图检测
上面的检测逻辑不太适用于SimpleDraweeView,所以需要自己重新实现一下。其实就是找到一个比较合理的hook点,既能够拿到bitmap,又能拿到view。
Hook点:GenericDraweeHierarchy的setImage(Drawable drawable, float progress, boolean immediate)
,最终显示的Bitmap会通过这个方法进行设置到SimpleDraweeView上。通过drawable参数可以拿到bitmap,通过drawable的callback字段,可以拿到对应的simpleDraweeView。
//GenericDraweeHierarchy类
setImage(Drawable drawable, float progress, boolean immediate)
实现:自定义BigSizeFrescoImageHook类,hook GenericDraweeHierarchy的setImage方法,解析参数,得到bitmap宽高和view宽高,对比两者的大小关系。当Bitmap大小是view大小的n倍,就输出日志信息进行提示。
class BigSizeFrescoImageHook : XC_MethodHook() {
private val TAG = "BigSizeFrescoImageHook"
private val max_threshold = 2 //自定义一个阀值,当Bitmap宽高比view宽高大n倍,就输出对应的日志
override fun afterHookedMethod(param: MethodHookParam?) {
super.afterHookedMethod(param)
Log.d(TAG, "afterHookedMethod: ")
param ?: return
//拿到simpleDraweeView和drawable对象
if (param.thisObject is GenericDraweeHierarchy){
val hierarchy=param.thisObject as GenericDraweeHierarchy
val bitmapDrawable =param.args[0] as BitmapDrawable
val simpleDraweeView =hierarchy.topLevelDrawable.callback as SimpleDraweeView
//检测逻辑是上面是一样的
checkBitmap(simpleDraweeView,bitmapDrawable)
}
}
}
//使用
DexposedBridge.findAndHookMethod(GenericDraweeHierarchy::class.java,"setImage",Drawable::class.java,Float::class.java,Boolean::class.java,BigSizeFrescoImageHook())
重复图片检测
解析hprof文件
通过dump出当前app运行时的内存快照.hprof文件,然后基于com.squareup.haha:haha
这个开源框架对内存快照进行内存分析,得到内存中所有的Bitmap对象。
代码实现可以参考:hprof_bitmap_dump
实现思路
- 读取指定目录下的hprof文件
- 通过类名android.graphics.Bitmap获取到获取到ClassObj对象
- 获取Bitmap的实例列表
- for循环遍历,读取每个bitmap中的信息,比如宽度,高度,像素数据。
- 根据width,height,像素数据,将对应的图片输出到本地磁盘上
具体代码
public static void main(String[] args) {
//可以改成自定义的本地文件地址,方便自己测试
final File hprofFile = new File(args[0]);
//用于判断是否重复,key是对应图片的md5值,value是对应图片的buffer数据。
HashMap<String, String> hashMap = new HashMap<>();
try {
//读取解析hprof文件
final HprofBuffer buffer = new MemoryMappedFileBuffer(hprofFile);
final HprofParser parser = new HprofParser(buffer);
final Snapshot snapshot = parser.parse();
//通过类名获取到获取到ClassObj对象
final ClassObj bitmapClass = snapshot.findClass("android.graphics.Bitmap");
//获取Bitmap的实例个数
final int bitmapCount = bitmapClass.getInstanceCount();
System.out.println("Found bitmap instances: " + bitmapCount);
//获取Bitmap的实例列表
final List<Instance> bitmapInstances = bitmapClass.getInstancesList();
//for循环遍历,读取每个bitmap中的信息
int n = 0;
for (Instance bitmapInstance : bitmapInstances) {
if (bitmapInstance instanceof ClassInstance) {
int width = 0;
int height = 0;
byte[] data = null;
String md5="";
String id="";
final ClassInstance bitmapObj = (ClassInstance) bitmapInstance;
//获取Bitmap中的字段列表
final List<ClassInstance.FieldValue> values = bitmapObj.getValues();
//for循环,读取filedd列表中的每个field
for (ClassInstance.FieldValue fieldValue : values) {
if ("mWidth".equals(fieldValue.getField().getName())) {
//图片宽度
width = (Integer) fieldValue.getValue();
} else if ("mHeight".equals(fieldValue.getField().getName())) {
//图片高度
height = (Integer) fieldValue.getValue();
} else if ("mBuffer".equals(fieldValue.getField().getName())) {
//图片像素数据,是个byte数组
ArrayInstance arrayInstance = (ArrayInstance) fieldValue.getValue();
Object[] boxedBytes = arrayInstance.getValues();
data = new byte[boxedBytes.length];
for (int i = 0; i < data.length; i++) {
data[i] = (Byte) boxedBytes[i];
}
//计算图片的md5值
md5=Md5Util.getMd5(data);
//对象Id
id=Integer.toHexString((int) bitmapObj.getId());
}
}
//输出每个Bitmap的信息
System.out.println("Bitmap #" + n + ": " + width + "x" + height+" id :"+id);
//判断图片是否重复
if (!hashMap.containsKey(md5)) {
hashMap.put(md5,id);
} else {
System.out.println("纯在重复图片,当前id:"+id+"重复id:"+hashMap.get(md5));
}
//根据width,height,像素数据,将图片输出到本地磁盘上
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_4BYTE_ABGR);
for (int row = 0; row < height; row++) {
for (int col = 0; col < width; col++) {
int offset = 4 * (row * width + col);
int byte3 = 0xff & data[offset++];
int byte2 = 0xff & data[offset++];
int byte1 = 0xff & data[offset++];
int byte0 = 0xff & data[offset++];
int alpha = byte0;
int red = byte1;
int green = byte2;
int blue = byte3;
int pixel = (alpha << 24) | (blue << 16) | (green << 8) | red;
image.setRGB(col, row, pixel);
}
}
final OutputStream inb = new FileOutputStream("bitmap-0x" + Integer.toHexString((int) bitmapObj.getId()) + ".png");
final ImageWriter wrt = ImageIO.getImageWritersByFormatName("png").next();
final ImageOutputStream imageOutput = ImageIO.createImageOutputStream(inb);
wrt.setOutput(imageOutput);
wrt.write(image);
inb.close();
n++;
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
比较两个Bitmap是否一样
比较字段:Bitmap中的mBuffer,这两个byte[]数据的md5值
一样,则可以认为两个Bitmap图片是一样的。
算法实现:搞了个HashMap来判断是否重复,key是对应图片的md5值,value是对应图片的buffer数据。
//用于判断是否重复,key是对应图片的md5值,value是对应图片的buffer数据。
HashMap<String, String> hashMap = new HashMap<>();
//md5工具类
static class Md5Util {
private static MessageDigest md5;
static {
try {
md5 = MessageDigest.getInstance("MD5");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static String getMd5(byte[] bs) {
StringBuilder sb = new StringBuilder(40);
for (byte x : bs) {
if ((x & 0xff) >> 4 == 0) {
sb.append("0").append(Integer.toHexString(x & 0xff));
} else {
sb.append(Integer.toHexString(x & 0xff));
}
}
return sb.toString();
}
}
图片缓存管理
默认情况下,Fresco会有三级缓存:Bitmap缓存+未解码图片缓存+硬盘缓存。
- BitmapCache:存储
Bitmap
对象 - EncodeCache
未解码图片缓存
:存储的是原始压缩格式的图片。从该缓存取到的图片在使用之前,需要先进行解码。 - DiskCache
磁盘缓存
:存储的是未解码的原始压缩格式的图片,在使用之前同样需要经过解码等处理。 - Fresco初始化的时候,可配置的选项挺多,我们可以根据需要,修改各种配置。
ImagePipelineConfig config = ImagePipelineConfig.newBuilder(context)
.setBitmapMemoryCacheParamsSupplier(bitmapCacheParamsSupplier)//自定义内存缓存的配置参数
.setDownsampleEnabled(true)//是否开启图片向下采样
.setEncodedMemoryCacheParamsSupplier(encodedCacheParamsSupplier)//自定义未解码图片缓存配置
.setExecutorSupplier(executorSupplier)//自定义线程池提供者
.setImageCacheStatsTracker(imageCacheStatsTracker)//可以用来统计的图片缓存事件
.setMainDiskCacheConfig(mainDiskCacheConfig)//自定义磁盘缓存的配置参数
.setMemoryTrimmableRegistry(memoryTrimmableRegistry)//内存变化监听注册表,那些需要监听系统内存变化的对象需要添加到这个表中类
.setRequestListeners(requestListeners)//监听请求过程中的各种事件
.setSmallImageDiskCacheConfig(smallImageDiskCacheConfig)//磁盘缓存配置
.build();
Fresco.initialize(context, config);
配置两个磁盘缓存
默认情况下,fresco的磁盘缓存只有一个,使用的是MainDiskCache。那根据LRU Cache的原则,当缓存满的时候,就需要删除访问时间最早的那条数据。
优化:配置两个磁盘缓存,一个用于缓存大图,一个用于缓存小图
。
这样的话,小文件就不会因大文件的频繁变动而被从缓存中移除。
val IMAGE_PIPELINE_MAIN_CACHE_DIR = "fresco_cache_big"
val IMAGE_PIPELINE_SMALL_CACHE_DIR = "fresco_cache_small"
/**
* 配置磁盘缓存
*/
fun configDiskCache(context: Context, builder: ImagePipelineConfig.Builder) {
builder.setMainDiskCacheConfig(
//大图缓存
DiskCacheConfig.newBuilder(context)
.setBaseDirectoryPath(context.externalCacheDir)
.setBaseDirectoryName(IMAGE_PIPELINE_MAIN_CACHE_DIR)
.build()
)
.setSmallImageDiskCacheConfig(
//小图缓存
DiskCacheConfig.newBuilder(context)
.setBaseDirectoryPath(context.externalCacheDir)
.setBaseDirectoryName(IMAGE_PIPELINE_SMALL_CACHE_DIR)
.build()
)
}
在构造图片请求的时候,通过setCacheChoice
,指定缓存的类型就可以了。
val request = ImageRequestBuilder.newBuilderWithSource(Uri.parse("https://img95.699pic.com/photo/40011/0709.jpg_wh860.jpg"))
.setCacheChoice(ImageRequest.CacheChoice.SMALL)
.build()
mBinding.draweeView.setImageRequest(request)
根据设备设置不同的缓存大小
磁盘缓存
在DiskCacheConfig.Builder类中定义一系列的默认值。
如果手机磁盘空间充足的话,默认情况下,MainCache最大是40MB。 磁盘空间比较低的话,MainCache最大是10MB。 磁盘控件非常低的话,MainCache最大是2MB。
private long mMaxCacheSize = 40 * ByteConstants.MB;
private long mMaxCacheSizeOnLowDiskSpace = 10 * ByteConstants.MB;
private long mMaxCacheSizeOnVeryLowDiskSpace = 2 * ByteConstants.MB;
内存缓存
MemoryCacheParams,内存缓存配置类。
Fresco有两层内存缓存,所以对应的默认配置有两个实现(DefaultBitmapMemoryCacheParamsSupplier和 DefaultEncodedMemoryCacheParamsSupplier
)。
Fresco的默认缓存大小是根据当前应用的运行内存来决定的
,对于应用运行内存达到64MB以上的手机(现在的手机普遍已经大于这个值了),Fresco
的默认缓存大小是maxMemory / 4
public class DefaultBitmapMemoryCacheParamsSupplier implements Supplier<MemoryCacheParams> {
@Override
public MemoryCacheParams get() {
return new MemoryCacheParams(
getMaxCacheSize(),
MAX_CACHE_ENTRIES,
MAX_EVICTION_QUEUE_SIZE,
MAX_EVICTION_QUEUE_ENTRIES,
MAX_CACHE_ENTRY_SIZE,
PARAMS_CHECK_INTERVAL_MS);
}
private int getMaxCacheSize() {
final int maxMemory =
Math.min(mActivityManager.getMemoryClass() * ByteConstants.MB, Integer.MAX_VALUE);
if (maxMemory < 32 * ByteConstants.MB) {
return 4 * ByteConstants.MB;
} else if (maxMemory < 64 * ByteConstants.MB) {
return 6 * ByteConstants.MB;
} else {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
return 8 * ByteConstants.MB;
} else {
return maxMemory / 4;
}
}
}
}
通过setBitmapMemoryCacheParamsSupplier
和setEncodedMemoryCacheParamsSupplier
去自定义两个内存缓存的配置。
/**
* 配置内存缓存
*/
private fun configMemoryCache(builder: ImagePipelineConfig.Builder) {
builder.setBitmapMemoryCacheParamsSupplier(object : Supplier<MemoryCacheParams> {
override fun get(): MemoryCacheParams {
val cacheSize = 50 * ByteConstants.MB
return MemoryCacheParams(
cacheSize,//内存缓存中总图片的最大大小,以字节为单位
Int.MAX_VALUE,//内存缓存中图片的最大数量
Runtime.getRuntime().maxMemory().toInt()/8,//内存缓存中准备清除但尚未被删除的总图片的最大大小,以字节为单位
Int.MAX_VALUE,//内存缓存中准备清除的总图片的最大数量
Int.MAX_VALUE //内存缓存中单个图片的最大大小
)
}
})
优化点:可以根据手机设备的情况,适当增大磁盘缓存和内存缓存的大小
。缓存空间变大,可以存放更多的元素,理论上缓存命中率就有可能会提高,从而提高图片加载速度。
根据系统状态去释放相应的内存
onTrimMemory
onTrimMemory 是系统提供的一个API,主要作用是提示开发者在内存不足的时候,可以通过处理部分资源来释放内存,从而避免被Android系统杀死
。
在onTrimMemory内存吃紧的时候,我们可以清理掉Fresco的图片内存缓存,释放部分内存
。等再次回来的时候,虽然内存缓存被移除掉了,但是我们还可以从磁盘缓存或者网络请求重新加载图片。
MemoryTrimmableRegistry
MemoryTrimmableRegistry,Fresco提供的内存调节器,可以调用registerMemoryTrimmable注册各种实现MemoryTrimmable接口的类。在需要调整内存使用量的时候,MemoryTrimmableRegistry可以去通知已注册的MemoryTrimmable,进行相应的内存调整操作
。
MemoryTrimmable,默认的Fresco里面的缓存管理类,都已经实现了这个接口,可以根据MemoryTrimType,自动调整缓存的大小
public interface MemoryTrimmableRegistry {
void registerMemoryTrimmable(MemoryTrimmable trimmable);
void unregisterMemoryTrimmable(MemoryTrimmable trimmable);
}
自定义MemoryTrimmableRegistry
默认:默认的实现类NoOpMemoryTrimmableRegistry,是没有进行任何处理的。
优化:我们可以自己实现MemoryTrimmableRegistry接口,使用一个list来保存已注册的MemoryTrimmable对象
。当内存不足的时候,遍历list通知已注册的MemoryTrimmable对象,调用对应的trim方法。
- 后台进程,并且当前手机内存吃紧,trimType设置为
MemoryTrimType.OnAppBackgrounded
,这样的话,fresco会回收所有的内存缓存 - 前台进程,但是目前手机内存吃紧,trimType设置为
MemoryTrimType.OnSystemLowMemoryWhileAppInForeground
,这样的话,fresco会回收一半的内存缓存,释放内存压力。
/**
* 自定义内存调节器
*/
object CustomMemoryTrimmableRegistry :
MemoryTrimmableRegistry {
private val TAG = "CustomMemoryTrimmableRegistry"
val list = CopyOnWriteArrayList<MemoryTrimmable>()
fun init(application: Application) {
//注册回调,监听onTrimMemory
application.registerComponentCallbacks(object : ComponentCallbacks2 {
override fun onConfigurationChanged(newConfig: Configuration) {
}
override fun onLowMemory() {
}
override fun onTrimMemory(level: Int) {
dispatchTrim(level)
}
})
}
override fun registerMemoryTrimmable(trimmable: MemoryTrimmable?) {
list.add(trimmable)
}
override fun unregisterMemoryTrimmable(trimmable: MemoryTrimmable?) {
list.remove(trimmable)
}
private fun dispatchTrim(level: Int) {
var trimType: MemoryTrimType? = null
if (level > ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) {
//后台进程
trimType = MemoryTrimType.OnAppBackgrounded
} else if (level > ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW) {
//前台进程,是目前手机比较吃紧
trimType = MemoryTrimType.OnSystemLowMemoryWhileAppInForeground
}
trimType ?: return
Log.d(TAG, "onTrimMemory trimType:$trimType")
list.forEach {
it.trim(trimType)
}
}
}
手动管理缓存
默认情况下,Fresco会有三级缓存:Bitmap缓存+未解码图片缓存+硬盘缓存。
其中有两级是内存缓存
,BitmapCache和EncodingCache,会占用一定的内存。直到 onTrimMemory 通知内存不足的时候,才会释放部分内存。
- 优化:
在适当的时机,手动添加缓存或者删除缓存
。
图片预加载
- 部分场景可以这样搞,比如图片列表流的场景。一般先通过网络请求获取到列表数据,然后再设置数据源给RecyclerView,ViewHolder再将图片URL设置给对应的SimpleDraweeView进行显示,SimpleDraweeView这个时候才去加载网络图片。
- 优化:网络数据回来之后,可以
尝试先预加载前几张部分图片到BitmapCache内存缓存
中。这样加载的时候,可以直接从Bitmap缓存中获取到Bitmap,用户可以更快速的看到图片。
Fresco.getImagePipeline().prefetchToBitmapCache()
移除指定的图片缓存(内存+磁盘)
- 比如,类似探探滑卡这种场景,只能不断向后面滑。所以,当我们切换到下一个卡片的时候,上一个卡片的图片对应的BitmapCache是可以进行移除掉的了,及时释放内存资源。
Fresco.getImagePipeline().evictFromMemoryCache(imageUrl)
移除过期的图片缓存(磁盘)
- 比如启动后,可以清除掉24小时之前的磁盘缓存,减少存储空间的占用。
- 可以使用 WorkManager 来实现这块逻辑。
WorkManager 是 Android 平台上推荐的任务调度程序,用于处理可延迟的工作,同时可保证其得到执行
。
/**
* Fresco磁盘缓存,清理任务。
* 定义一个新的Worker,调用FileCache.clearOldEntries(long cacheExpirationMs) 清除过期时间的图片磁盘缓存。
* 清除时机:启动后,清除掉24之前的图片缓存
*/
class FrescoCacheCleanWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
private val TAG="FrescoCacheCleanWorker"
//定义一个阀值,24小时
private val cacheExpirationMs = TimeUnit.MILLISECONDS.convert(24, TimeUnit.HOURS)
override fun doWork(): Result {
Fresco.getImagePipelineFactory().mainFileCache.clearOldEntries(cacheExpirationMs)
return Result.success()
}
}
自定义缓存算法
缓存命中率,跟缓存使用的淘汰算法还是有点关系的。
Fresco缓存算法默认用的是LRU。当缓存空间被用满时,会去清除那些最近最久没有被访问过的对象。
自定义实现CountingMemoryCache
接口,替换默认的LRUCache,使用其他缓存算法,自己去管理缓存相关的逻辑。(这里提供思路,因为我自己还没具体实践过,理论上可行)
指标监控
缓存命中率
- 缓存命中率是缓存中的一个挺重要的问题,它是衡量缓存有效性的重要指标。命中率越高,表明缓存的使用率越高。能够直接从缓存中读取到数据,提高性能。
- 具体对应:内存缓存命中率,磁盘缓存命中率
- 计算方法:命中数/(命中数+未命中数)
- 实现方式:Fresco提供了一个
ImageCacheStatsTracker
的接口,可以实现ImageCacheStatsTracker。在这个类中,每个缓存事件都有回调通知,基于这些事件,可以实现缓存的计数和统计。
object FrescoImageCacheTracker : ImageCacheStatsTracker {
private val TAG = "FrescoImageCacheTracker"
/**
* memoryCacheHitCount:命中数
* memoryCacheMissCount:没命中数
* 缓存命中率计算=memoryCacheHitCount/(memoryCacheHitCount+memoryCacheMissCount)
*/
private var memoryCacheHitCount = AtomicInteger()
private var memoryCacheMissCount = AtomicInteger()
//命中 Bitmap缓存
override fun onBitmapCacheHit(cacheKey: CacheKey?) {
memoryCacheHitCount.incrementAndGet()
Log.d(TAG, "onBitmapCacheHit , cacheKey:$cacheKey")
}
//没命中 Bitmap缓存
override fun onBitmapCacheMiss(cacheKey: CacheKey?) {
memoryCacheMissCount.incrementAndGet()
Log.d(TAG, "onBitmapCacheMiss , cacheKey:$cacheKey")
}
//命中 磁盘缓存
override fun onDiskCacheHit(cacheKey: CacheKey?) {
Log.d(TAG, "onDiskCacheHit , cacheKey:$cacheKey")
}
//没命中 磁盘缓存
override fun onDiskCacheMiss(cacheKey: CacheKey?) {
Log.d(TAG, "onDiskCacheMiss , cacheKey:$cacheKey")
}
}
统计Fresco占用的内存大小
一般我们都会对App进行整体的内存监控,这个时候也可以补充上报Fresco占用内存的相关数据,帮助我们排查问题。
/**
* 获取Fresco的内存和磁盘大小
*/
object FrescoMemoryStat {
//占用内存大小:bitmap+encode
val memorySize: String
get() {
return (
ImagePipelineFactory.getInstance().bitmapMemoryCache.sizeInBytes + ImagePipelineFactory.getInstance().encodedMemoryCache.sizeInBytes
).byteToString()
}
//占用磁盘大小:mainCache+smallCache
val diskCache: String
get() {
return (
ImagePipelineFactory.getInstance().mainFileCache.size + ImagePipelineFactory.getInstance().smallImageFileCache.size
).byteToString()
}
}
图片加载成功率统计
Fresco 提供了全局的监听器来监听整个图片加载过程中的每一步,我们在配置Fresco时就可以设置多个自定义的RequestListener
。
Producer
在Fresco中代表这个图片加载流程中的某一步,比如网络加载、解码等等。每一个Producer
都有一个特定的名字,因此我们只需要在回调中解析我们感兴趣的Producer
的事件即可。
- 成功率=成功数/(成功数+失败数)
- 实现:继承
BaseRequestListener
,通过onRequestSuccess
和onRequestFailure
进行计数,最后算出结果即可。 - 在onRequestFailure即图片加载失败的时候,可以拿到对应加载失败的throwable,上报到服务器,方便后面针对具体的错误原因进行修复。
/**
* 图片加载成功率统计
* 成功率=成功数/(成功数+失败数)
*/
object FrescoImageSuccessStatListener: BaseRequestListener() {
private val TAG="FrescoRequestListener"
private val successCount = AtomicInteger()
private val failCount = AtomicInteger()
val succssRatio: Float
get() {
return successCount.get().toFloat() / (successCount.get() + failCount.get()).apply {
Log.d(TAG, "successCount:$successCount,failCount:$failCount ,ratio:$this")
}
}
override fun requiresExtraMap(requestId: String?): Boolean {
return true
}
override fun onRequestSuccess(request: ImageRequest?, requestId: String?, isPrefetch: Boolean) {
Log.d(TAG, "onRequestSuccess,requestId:$requestId,isPrefetch:$isPrefetch ")
successCount.incrementAndGet()
}
override fun onRequestFailure(
request: ImageRequest?,
requestId: String?,
throwable: Throwable?,
isPrefetch: Boolean
) {
Log.d(TAG, "onRequestFailure,requestId:$requestId,isPrefetch:$isPrefetch,throwable:$throwable,request:$request")
failCount.incrementAndGet()
}
override fun onRequestCancellation(requestId: String?) {
Log.d(TAG, "onRequestCancellation,requestId:$requestId")
}
}
网络图片加载速度统计
默认情况下,从网络层获取图片流是HttpUrlConnectionNetworkFetcher
来实现的。在这个过程中,会自动标记一些时间点,相应的时间戳记录在HttpUrlConnectionNetworkFetchState,数据会填充到ExtraMap
中。
(使用Okhttp作为fresco的网络层的话,对应的类应该是OkHttpNetworkFetcher和OkHttpNetworkFetchState
)
通过简单的日志打印,可以看到,NetworkFetchProducer
触发onProducerFinishWithSuccess回调的时候,可以填充extraMap数据返回。
extraMap:{
queue_time=193,//请求丢入请求线程池到最后请求成功响应的时间
total_time=358, //从response读完IO流的时间
image_size=201925, //图片大小
fetch_time=165 //请求丢入请求线程池到最后读完IO流的时机
}
public class HttpUrlConnectionNetworkFetcher
@Override
public Map<String, String> getExtraMap(
HttpUrlConnectionNetworkFetchState fetchState, int byteSize) {
Map<String, String> extraMap = new HashMap<>(4);
extraMap.put(QUEUE_TIME, Long.toString(fetchState.responseTime - fetchState.submitTime));
extraMap.put(FETCH_TIME, Long.toString(fetchState.fetchCompleteTime - fetchState.responseTime));
extraMap.put(TOTAL_TIME, Long.toString(fetchState.fetchCompleteTime - fetchState.submitTime));
extraMap.put(IMAGE_SIZE, Integer.toString(byteSize));
return extraMap;
}
}
public static class HttpUrlConnectionNetworkFetchState extends FetchState {
private long submitTime;
private long responseTime;
private long fetchCompleteTime;
public HttpUrlConnectionNetworkFetchState(
Consumer<EncodedImage> consumer, ProducerContext producerContext) {
super(consumer, producerContext);
}
}
既然数据能够获取到了,那就自定义FrescoNetWorkImageListener去实现获取需要的数据,然后可以将这些字段上报到服务器进行统计。简单的实现,可以参考下面。
/**
* 网络图片监控
*/
object FrescoNetWorkImageListener: BaseRequestListener() {
private val TAG = "FrescoNetWorkImageListener"
override fun onProducerFinishWithSuccess(
requestId: String?,
producerName: String?,
extraMap: MutableMap<String, String>?
) {
Log.d(
TAG,
"onProducerFinishWithSuccess, requestId:$requestId,producerName:$producerName,extraMap:$extraMap"
)
if (producerName == NetworkFetchProducer.PRODUCER_NAME && extraMap != null) {
//NetworkFetchProducer,读取到相应的字段
val queue_time = extraMap["queue_time"]
val fetch_time = extraMap["fetch_time"]
val total_time = extraMap["total_time"]
val image_size = extraMap["image_size"]
//可以将这些字段,上报都服务器进行统计
}
}
//需要重写为true,extraMap才会填充额外的一些字段,方便分析
override fun requiresExtraMap(requestId: String?): Boolean {
return true
}
}