Lottie框架Native Heap内存爆炸问题解决
Lottie 是在 Android 和 iOS上 原生渲染 的After Effects(AE)动画,Lottie是 Airbnb 开源 的支持Android 和 iOS 的动画库,它可以解析 AE 动画中用Bodymovin 导出的json文件,并在移动设备上利用原生库进行渲染 !项目地址:https://github.com/airbnb/lottie-android
遇到的问题
Lottie框架帮我们解决很多过场动画和图标点击动画这些应用场景,可能是因为太好用了,最近一个版本的UI大量使用到了lottie动画,这个时候问题就出现了,使用Profiler进行内存检测的时候,发现了Native Heap 内存可以飙升到1GB,Android 8.0之后Bitmap 像素占用的内存分配到了 Native Heap中(目前主流用户大部分都在8.0以上的版本了),基本上就可以确定就是lottie框架加载动效太多造成的,那么我们就来清除Bitmap的缓存吧
源码解析
根据阅读源码可以
LottieAnimationView 是主要进行属性设置以及状态管理的,我们来看invalidateDrawable方法
@Override
public void invalidateDrawable(@NonNull Drawable dr) {
if (getDrawable() == lottieDrawable) {
// We always want to invalidate the root drawable so it redraws the whole drawable.
// Eventually it would be great to be able to invalidate just the changed region.
// 绘制lottie动画
super.invalidateDrawable(lottieDrawable);
} else {
// Otherwise work as regular ImageView
super.invalidateDrawable(dr);
}
}
由于后续的源码调用的链路是这样子的LottieDrawbale.draw(canvas)–》CompositionLayer.draw - 》 BaseLayer.draw -》 ImageLayer.draw ,我们来看看ImageLayer的源码
@Override public void drawLayer(@NonNull Canvas canvas, Matrix parentMatrix, int parentAlpha) {
Bitmap bitmap = getBitmap();
if (bitmap == null || bitmap.isRecycled()) {
return;
}
float density = Utils.dpScale();
paint.setAlpha(parentAlpha);
if (colorFilterAnimation != null) {
paint.setColorFilter(colorFilterAnimation.getValue());
}
canvas.save();
canvas.concat(parentMatrix);
src.set(0, 0, bitmap.getWidth(), bitmap.getHeight());
dst.set(0, 0, (int) (bitmap.getWidth() * density), (int) (bitmap.getHeight() * density));
canvas.drawBitmap(bitmap, src, dst , paint);
canvas.restore();
}
那么getBitmap()方法是调用的ImageAssetManager.bitmapForId
@Nullable
public Bitmap bitmapForId(String id) {
LottieImageAsset asset = imageAssets.get(id);
if (asset == null) {
return null;
}
Bitmap bitmap = asset.getBitmap();
// 检测bitmap是否已经回收
if (bitmap != null && !bitmap.isRecycled()) {
return bitmap;
}
if (delegate != null) {
bitmap = delegate.fetchBitmap(asset);
if (bitmap != null) {
putBitmap(id, bitmap);
}
return bitmap;
}
String filename = asset.getFileName();
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inScaled = true;
opts.inDensity = 160;
if (filename.startsWith("data:") && filename.indexOf("base64,") > 0) {
// Contents look like a base64 data URI, with the format data:image/png;base64,<data>.
byte[] data;
try {
data = Base64.decode(filename.substring(filename.indexOf(',') + 1), Base64.DEFAULT);
} catch (IllegalArgumentException e) {
Logger.warning("data URL did not have correct base64 format.", e);
return null;
}
bitmap = BitmapFactory.decodeByteArray(data, 0, data.length, opts);
return putBitmap(id, bitmap);
}
InputStream is;
try {
if (TextUtils.isEmpty(imagesFolder)) {
throw new IllegalStateException("You must set an images folder before loading an image." +
" Set it with LottieComposition#setImagesFolder or LottieDrawable#setImagesFolder");
}
is = context.getAssets().open(imagesFolder + filename);
} catch (IOException e) {
Logger.warning("Unable to open asset.", e);
return null;
}
bitmap = BitmapFactory.decodeStream(is, null, opts);
bitmap = Utils.resizeBitmapIfNeeded(bitmap, asset.getWidth(), asset.getHeight());
return putBitmap(id, bitmap);
}
解决思路
- ImageLayer drawLayer 方法中最后一行 添加bitmap.recycle()每绘制一张图片 就回收一张
- ImageAssetManager 增加clearCache方法 在动画加载完毕之后将bitmap集合进行全部回
- ImageAssetManager bitmapForId 修改为bitmap不会null并且没有回收的情况下 才直接使用,否则就要重新进行加载资源