您当前的位置: 首页 > 
  • 0浏览

    0关注

    674博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

长拼图技术分享

沙漠一只雕得儿得儿 发布时间:2021-12-01 11:53:32 ,浏览量:0

写在前面:众所周知,在Java层进行图片的处理,因为bitmap对象占用的大内存,很容易产生OOM(Out Of Memory)的错误。为了解决此类问题,Android也在不断完善bitmap的处理机制,方便程序员使用,提升应用性能。本文将从图片处理机制出发,简述在爱奇艺头条1.10.20版本引入的,视频长拼图功能技术原理,和一些注意的地方。

  1. 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。 这是一种降低内存使用的最初级,也是最有效的方法。  

  2. 管理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参数之后的内存使用情况,很明显内存非常平滑。 使用这个参数,需要注意的是:
    1. 该参数要和BitmapFactory.Options.inMutable一同使用,只有当inMutable设置为true时,inBitmap参数才会生效。
    2. 该参数在android 4.4才完善,在4.4及以后的版本中,只要新图片的大小,小于inBitmap的大小,即可使用。在4.4之前的版本中,只有新图片的大小,正好等于inBitmap的大小,并且inSampleSize为1时,inBitmap才会被使用。
    在其他应用的场景中,inBitmap参数可以和cache机制相结合,优化多图情境下的内存使用和系统效率。详情可以见android官方文档https://developer.android.com/topic/performance/graphics/manage-memory.html#recycle。
  3. 长拼图功能的实现 通过上面的描述,基本可以了解长拼图的基本功能原理了。其基本实现流程如下: 这是我们基本的实现逻辑。但是有没有更好的方法呢?

  4. 优化实现 我们一般在拼长图之前,会把图片展示出来。这个过程,我们一般会使用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。

     

  5. 其他可以帮助的点 无论如何优化,因为Bitmap占用内存的特性,如果需要拼更大的图,那么需要给应用分配更大的内存。可以使用android已经提供给我们的方法,来增加应用可申请的最大内存数。     ....... 当标识largeHeap时,可以大大增加应用可申请的内存量,从而有效避免OOM。

关注
打赏
1657159701
查看更多评论
立即登录/注册

微信扫码登录

0.0422s