上图是线上集成Facebook的Fresco图片框架而导致的一个异常,bugly异常统计历史累计1409次。
在Fresco的gitHub上issue的提问比比皆是,但有效的解决方案却是少之又少,基本上都是不靠谱的,Fresco官方甚至每次回答这个问题时都是新版本已解决等等,但是问题依旧。
跟同事讨论提到,可以尝试通过分析源码捕获这个崩溃异常再做操作,即使so文件找不到图片不显示也行,不要让应用崩溃!
按照这个思路下载了Fresco的源码,准备啃一口它的源码了,当然我肯定想要以最小的代价解决这个问题,所以百度大法搞起,瞧一瞧看有没有人解决这个问题,很遗憾百度上找出来的也是没啥用,那只能祭出绝招Google大法了。
在Google搜索中找到了这么一条信息,跳过发现还是Fresco的github上issue,本来都打算退出了,但瞄了瞄,这一瞄找到了解决办法。
这位大佬说他有一个解决方案,并且给出了代码(虽然是Kotlin写的,但是我会告诉你我学过kotlin?)而且还点出了造成这个错误的原因是libimagepipeline.so文件,如果我们捕获到这个异常并尝试处理它就能解决这个问题,这和讨论的思路相同了,而且看到下面的点赞量,我立马想试试了。
代码量很少总共就这么多,核心的其实就几行,添加后打包测试,通过云测提供的云真机,对修改后的方案进行验证,效果很好,出现崩溃的机子在安装新包后运行正常且图片正常加载,效果棒棒哒!
解决了问题,我们来研究一下导致整个问题的根本原因是什么,不能只拿结果不管过程。
根据异常分析报错的原因是so库加载失败,CPU架构找不到对应的so文件,通过bugly异常分析中看到:so文件加载失败后,Fresco调用了libimagepipeline.so文件下的SoLoader native方法,这个时候问题就来了,so文件已经加载失败了,它怎么去找内部的方法呢?自然就报错了。
而上面通过简单的几行代码就解决了这个问题,是怎么做到呢?这几行代码的作用是什么呢!我们来分析一下。
1.ImagePipelineNativeLoader.load();
这个方法我们从名字都能看出来是native方法初始化,进入方法里面
public class ImagePipelineNativeLoader {
public static final String DSO_NAME = "imagepipeline";
public static final List DEPENDENCIES;
static {
List dependencies = new ArrayList();
DEPENDENCIES = Collections.unmodifiableList(dependencies);
}
public static void load() {
SoLoader.loadLibrary("imagepipeline");
}
}
我们可以看到它就是加载so文件中的方法,新的解决方案是把它放到初始化时调用,如果此时so文件加载失败就会报UnsatisfiedLinkError 异常,这个时候我们可以通过捕获这个异常来进行一些自己的操作!
那么在修改前发生异常的fresco源码中此方法在哪调用呢 ?通过追踪源码我们发现
// 获取ImagePipeline对象 Fresco的一些配置包括缓存策略及编码等等的配置就在这里面加载
ImagePipeline mImagePipeline = Fresco.getImagePipeline();
=================进入源码==================================
/** Gets the image pipeline instance. */
public static ImagePipeline getImagePipeline() {
return getImagePipelineFactory().getImagePipeline();
}
//这一段都是初始化配置信息等等一些操作
public ImagePipeline getImagePipeline() {
if (mImagePipeline == null) {
mImagePipeline =
new ImagePipeline(
getProducerSequenceFactory(),
mConfig.getRequestListeners(),
mConfig.getIsPrefetchEnabledSupplier(),
getBitmapMemoryCache(),
getEncodedMemoryCache(),
getMainBufferedDiskCache(),
getSmallImageBufferedDiskCache(),
mConfig.getCacheKeyFactory(),
mThreadHandoffProducerQueue,
Suppliers.of(false),
mConfig.getExperiments().isLazyDataSource());
}
return mImagePipeline;
}
此处省略若干方法,经过一系列的追踪
private MemoryChunkPool getMemoryChunkPool(@MemoryChunkType int memoryChunkType) {
switch (memoryChunkType) {
case NATIVE_MEMORY:
return getNativeMemoryChunkPool();
case BUFFER_MEMORY:
return getBufferMemoryChunkPool();
default:
throw new IllegalArgumentException("Invalid MemoryChunkType");
}
}
可以看到 这地方了Fresco的缓存模式被分为了两种,一种是native层缓存 一种是磁盘缓存,我们进入它的native缓存调用的方法里面看看发生了什么
public NativeMemoryChunkPool getNativeMemoryChunkPool() {
if (mNativeMemoryChunkPool == null) {
mNativeMemoryChunkPool =
new NativeMemoryChunkPool(
mConfig.getMemoryTrimmableRegistry(),
mConfig.getMemoryChunkPoolParams(),
mConfig.getMemoryChunkPoolStatsTracker());
}
return mNativeMemoryChunkPool;
}
这个方法返回了一个NativeMemoryChunkPool这东西,进去后发现
public class NativeMemoryChunkPool extends MemoryChunkPool {
public NativeMemoryChunkPool(
MemoryTrimmableRegistry memoryTrimmableRegistry,
PoolParams poolParams,
PoolStatsTracker nativeMemoryChunkPoolStatsTracker) {
super(memoryTrimmableRegistry, poolParams, nativeMemoryChunkPoolStatsTracker);
}
@Override
protected NativeMemoryChunk alloc(int bucketedSize) {
return new NativeMemoryChunk(bucketedSize);
}
}
它的alloc方法调用了nativeMemoryChunk 看着名字我们就怀疑它就是初始化ImagePipelineNativeLoader.load()的地方,进去后果不其然
/**
* Wrapper around chunk of native memory.
*
*
This class uses JNI to obtain pointer to native memory and read/write data from/to it.
*
*
Native code used by this class is shipped as part of libimagepipeline.so @ThreadSafe
*/
@DoNotStrip
public class NativeMemoryChunk implements MemoryChunk, Closeable {
private static final String TAG = "NativeMemoryChunk";
static {
ImagePipelineNativeLoader.load();
}
/**
* Address of memory chunk wrapped by this NativeMemoryChunk
*/
private final long mNativePtr;
}
到此我们可以确定当Fresco加载图片时会加载它里面的配置信息,而最终调用它的native方法,来设置缓存策略,我们也是因为so文件加载失败后,依然让它选择这种缓存策略,这样就会导致它崩溃,因为它压根找不到so文件下的方法。
接着往下看捕获异常后我们先是调用了Fresco.shutDown();方法它只不过是把Fresco的配置全部重置,以便于后面重新初始化,看后面。
2.ImagePipelineConfig.experiment().setNativeCodeDisabled(true);
看一看它干了些什么
/**
* If true, the pipeline will use alternative implementations without native code.
*
* @param nativeCodeDisabled set true for disabling native implementation.
* @return The Builder itself for chaining
*/
public ImagePipelineConfig.Builder setNativeCodeDisabled(boolean nativeCodeDisabled) {
mNativeCodeDisabled = nativeCodeDisabled;
return mConfigBuilder;
}
通过注释可以看到,这个方法可以设置让fresco禁止以native缓存策略加载。
我们刚才看到Fresco分了两种缓存策略如果我们禁止了它,那它会选择另外一种方式吗?继续追踪源码探索。
private static int getMemoryChunkType(
final Builder builder, final ImagePipelineExperiments imagePipelineExperiments) {
if (builder.mMemoryChunkType != null) {
return builder.mMemoryChunkType;
} else if (imagePipelineExperiments.isNativeCodeDisabled()) {
return MemoryChunkType.BUFFER_MEMORY;
} else {
return MemoryChunkType.NATIVE_MEMORY;
}
}
我们可以看到isNativeCodeDisabled
变量默认是false,而它默认选择了MemoryChunkType.NATIVE_MEMORY
缓存策略,我们变量改成true后它就选择了MemoryChunkType.BUFFER_MEMORY
缓存,再回到之前的
private MemoryChunkPool getMemoryChunkPool(@MemoryChunkType int memoryChunkType) {
switch (memoryChunkType) {
case NATIVE_MEMORY:
return getNativeMemoryChunkPool();
case BUFFER_MEMORY:
return getBufferMemoryChunkPool();
default:
throw new IllegalArgumentException("Invalid MemoryChunkType");
}
}
这个地方另外一种缓存策略
/**
* Wrapper around chunk using a direct ByteBuffer in native memory. A direct ByteBuffer is composed
* of a Java {@link ByteBuffer} object allocated on the Java heap and the underlying buffer which is
* in native memory.
*
*
The buffer in native memory will be released when the Java object gets garbage collected.
*/
public class BufferMemoryChunk implements MemoryChunk, Closeable {
private static final String TAG = "BufferMemoryChunk";
/** Internal representation of the chunk */
private ByteBuffer mBuffer;
/** Size of the ByteBuffer */
private final int mSize;
/** Unique identifier of the chunk */
private final long mId;
public BufferMemoryChunk(final int size) {}
}
通过这个类的注释我们可以看到它说这个是一个在java内存中创建的一个缓冲区,存在于本机内存中。
到这里基本上就能明白为什么捕获异常后修改这个参数为true就能修复这个崩溃,它直接修改了 Fresco的缓存策略让它从native缓存替换到了本机磁盘缓存,这样即使你找不到so文件在加载失败的情况下依旧可以正常的使用它而不是直接崩溃。
===========================================分割线=================================
两周过去了,线上出现了新的错误,发现是Fresco的gif图的加载机制出现问题,根据异常定位到GifImage类中,发现犹如之前一样它去加载了gifImage.so文件,报错的原因也是它
一开始我像之前一样,查了查源码想要以代码的方式控制gif的加载策略,来达到修复问题,最后发现并没有提供对应的API
所以这次来了个折中方案,我们在出现异常的机型上不能控制去加载gif的so文件,但是可以让程序在异常的机型上不去加载gif图,转而言之我们用一张静态图占位,这样没有gif图的存在那么也不会去加载gif.so文件,就不会出现崩溃
上期也说到出现这个错误的原因是CPU架构不对应造成的,也可以看到这两起的错误类型都是so文件加载失败引起的,那么fresco源码下那么多的so文件都会报错吗?
不确定!它也有加载webp格式图片的so文件,我在崩溃机型上加载了webp图片并没有引起崩溃,所以并不是所有的so文件都会引起崩溃
目前先按照这种方式对线上问题进行了处理,后续如果有新的问题,继续追踪解决。。。