写在前面:众所周知,在Java层进行图片的处理,因为bitmap对象占用的大内存,很容易产生OOM(Out Of Memory)的错误。为了解决此类问题,Android也在不断完善bitmap的处理机制,方便程序员使用,提升应用性能。本文将从图片处理机制出发,简述在爱奇艺头条1.10.20版本引入的,视频长拼图功能技术原理,和一些注意的地方。
-
Bitmap对象 在Android2.3.3之前的版本中,Bitmap对象与其像素数据是分开存储的,Bitmap对象存储在Dalvik heap中,而Bitmap对象的像素数据则存储在Native Memory中,这使得存储在Native Memory中的像素数据的释放是不可预知的,我们可以调用recycle()方法来对Native Memory中的像素数据进行释放,前提是你可以清楚的确定Bitmap已不再使用了,如果你调用了Bitmap对象recycle()之后再将Bitmap绘制出来,就会出现"Canvas: trying to use a recycled bitmap"错误,而在Android3.0之后,Bitmap的像素数据和Bitmap对象一起存储在Dalvik heap中,所以我们不用手动调用recycle()来释放Bitmap对象,内存的释放都交给垃圾回收器来做。 都说Bitmap占用内存很严重,那么到底占用了多少内存呢?特地写了一个demo测试了一下。 Bitmap bm = BitmapFactory.decodeFile(path); 通过这样的方式decode一个大小为2.64M的jpg图片,得到的Bitmap大小是31.6M,这样我们解析不了几张图片就会OOM。所以一般我们在解析的时候都会通过压缩图片尺寸,来获得减小Bitmap的内存占用。 例如,同样一张图片,我们通过下面的方式decode: BitmapFactory.Options options = new BitmapFactory.Options(); options.inSampleSize = 4; Bitmap bm = BitmapFactory.decodeFile(path, options); 得到Bitmap的大小为1.98M,缩小到不做任何压缩的1/16. 很容易理解,这里的inSampleSize是将原图的宽高,分别除以4,得到的大小将是原来的1/16。 这是一种降低内存使用的最初级,也是最有效的方法。
- 管理Bitmap的内存 虽然通过简单的压缩,单张Bitmap的内存占用比较低了。但是在我们拼长图的使用场景中,我们可以会重复调用decode得到多个Bitmap对象,然后将其绘制在Canvas上。重复进行这个操作,最终得到一个长图。 在2.3.3及以前,我们没有只能通过Bitmap的recycle()方法来回收native的内存,为了避免OOM,可以在设置BitmapFactory.Options.inPurgeable字段为true,标识当系统内存不足的时候,该Bitmap可以被回收。 Android 3.0以后,虽然我们不用手动管理Bitmap对象,可以让其同其他java对象一样,通过GC回收,但是如果频繁decode出新的Bitmap对象,在这个过程中,系统内存会遇到下面这个问题,如图:
可以看出,由于频繁创建新的bitmap,又频繁频繁触发GC,这肯定不是一个好的现象。 那么如何规避这种情况呢?在android3.0引入了BitmapFactory.Options.inBitmap参数。这个参数是Bitmap类型,如果设置了该参数,那么在decode图片是,会优先使用inBitmap参数所在的内存,以此来实现内存的复用。
上图是不设置inBitmap,那么三张图片会占用不同的内存区域
上图是设置了inBitmap参数,那么如果该内存区域可用,会复用这块内存区域。 下图是设置了inBitmap参数之后的内存使用情况,很明显内存非常平滑。
使用这个参数,需要注意的是:
- 该参数要和BitmapFactory.Options.inMutable一同使用,只有当inMutable设置为true时,inBitmap参数才会生效。
- 该参数在android 4.4才完善,在4.4及以后的版本中,只要新图片的大小,小于inBitmap的大小,即可使用。在4.4之前的版本中,只有新图片的大小,正好等于inBitmap的大小,并且inSampleSize为1时,inBitmap才会被使用。
-
长拼图功能的实现 通过上面的描述,基本可以了解长拼图的基本功能原理了。其基本实现流程如下:
这是我们基本的实现逻辑。但是有没有更好的方法呢?
-
优化实现 我们一般在拼长图之前,会把图片展示出来。这个过程,我们一般会使用Fresco等第三方库,这样的第三方库一般已经帮我们做了decode出bitmap,并且设置到View中去。我们能否直接使用其生成的Bitmap,而不用再从文件decode一遍呢? 为此我们查看了一下Fresco 用来Decode图片的decoder,我们找到了下面一段代码。
public
static
PlatformDecoder buildPlatformDecoder(
PoolFactory poolFactory,
boolean
directWebpDirectDecodingEnabled) {
if
(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
int
maxNumThreads = poolFactory.getFlexByteArrayPoolMaxNumThreads();
return
new
ArtDecoder(
poolFactory.getBitmapPool(),
maxNumThreads,
new
Pools.SynchronizedPool(maxNumThreads));
}
else
{
if
(directWebpDirectDecodingEnabled
&& Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
return
new
GingerbreadPurgeableDecoder();
}
else
{
return
new
KitKatPurgeableDecoder(poolFactory.getFlexByteArrayPool());
}
}
}
可以看出,Fresco在不同的Android版本上使用了不同的decodor,我们来看一下最新的ArtDecoder的内部实现。
protected
CloseableReference decodeStaticImageFromStream(
InputStream inputStream,
BitmapFactory.Options options) {
Preconditions.checkNotNull(inputStream);
int
sizeInBytes = BitmapUtil.getSizeInByteForBitmap(
options.outWidth,
options.outHeight,
options.inPreferredConfig);
final
Bitmap bitmapToReuse = mBitmapPool.get(sizeInBytes);
if
(bitmapToReuse ==
null
) {
throw
new
NullPointerException(
"BitmapPool.get returned null"
);
}
options.inBitmap = bitmapToReuse;
Bitmap decodedBitmap;
ByteBuffer byteBuffer = mDecodeBuffers.acquire();
if
(byteBuffer ==
null
) {
byteBuffer = ByteBuffer.allocate(DECODE_BUFFER_SIZE);
}
try
{
options.inTempStorage = byteBuffer.array();
decodedBitmap = BitmapFactory.decodeStream(inputStream,
null
, options);
}
catch
(RuntimeException re) {
mBitmapPool.release(bitmapToReuse);
throw
re;
}
finally
{
mDecodeBuffers.release(byteBuffer);
}
if
(bitmapToReuse != decodedBitmap) {
mBitmapPool.release(bitmapToReuse);
decodedBitmap.recycle();
throw
new
IllegalStateException();
}
return
CloseableReference.of(decodedBitmap, mBitmapPool);
}
可以看到, 其实Fresco在Decode图片的时候,也是利用了inBitmap属性,来实现Bitmap空间的复用。当然他的使用更加专业,通过一个bitmap的pool,来管理各个用来复用的Bitmap对象。既然Fresco已经帮助我们做了这样的操作,那我们其实可以利用Fresco的方法,来实现我们的目标。
这里附上我们的实现方法。
展开源码
上面是在Android 5.0以后的实现方案,我们直接利用了Fresco为我们生成的decoder,来decode本地图片,返回一个bitmap对象,绘制到我们的长拼图中。 下面是拼长图功能生成的一张长图,这张图的宽高为720 × 5019,占用的空间大小2MB,图片仅经过尺寸的压缩,没有压缩图片质量。可以看到该图非常的清晰。
生成这张图片时的内存占用情况如图。可以看到内存占用非常平滑,内存占用大概为20Mb。
- 其他可以帮助的点 无论如何优化,因为Bitmap占用内存的特性,如果需要拼更大的图,那么需要给应用分配更大的内存。可以使用android已经提供给我们的方法,来增加应用可申请的最大内存数。 ....... 当标识largeHeap时,可以大大增加应用可申请的内存量,从而有效避免OOM。