Android接入HEIC和AVIF解码说明

Android接入HEIC和AVIF解码说明

本文为您介绍如何配置 Android 客户端的相关依赖,以便支持 AVIF HEIC 图片解码。

Android 接入 HEIC 解码

HEIC 是一种高效的压缩图片格式,在相同质量的前提下,HEIC 文件大小约为 JPEG 40%。

阿里云高性能 HEIC 解码库在开源的 libheif libde265 基础上进行二次开发,针对 ARM 平台做了大量优化,参考了其它同类开源库优点,在解码链路上的各个环节进行优化,显著地提升了运行速度。相比原始的开源版本,解码的效率有数倍的提升。阿里云高性能 HEIC 解码库代码链接,请参见 Github

Android 客户端接入

通过 Android 客户端接入 HEIC 解码步骤如下:

  1. build.gradle repositories 节点中添加阿里云 Maven 仓库配置。

    maven { url 'https://maven.aliyun.com/repository/public' }
  2. build.gradle dependencies 节点添加依赖。

    implementation 'com.aliyun:libheif:1.0.0'

混淆配置

如果开启混淆需配置,添加如下代码以保证功能正常使用。

-keep class com.aliyun.libheif.**

解码 HEIC 图片

  • 场景一:直接使用 API 接口解码

        /**
         * decode HEIC file to RGBA format
         * @param length  the length of effective file memory
         * @param filebuf file pointer in memory
         * @return rgba byte, convenient to create a {@link android.graphics.Bitmap}
        public static native boolean toRgba(long length, byte[] fileBuf, Bitmap bitmap);
         * judge file is heic format or not
         * @param length  the length of effective file memory
         * @param filebuf file pointer in memory
         * @return bool, if true, the format is heif;
         *               if false, the fromat is not heif
        public static native boolean isHeic(long length, byte[] fileBuf);
         * get info from heic picture
         * @param HeifInfo info
         * @param length  the length of effective file memory
         * @param filebuf file pointer in memory
         * @return bool, if true, outSize is valid;
         *               if false, outSize is not valid
        public static native boolean getInfo(HeifInfo info, long length, byte[] fileBuf);                

    以下示例展示了如何将 asset 目录下的 test.heic 解码成 Bitmap

    val image = findViewById<ImageView>(R.id.image);
    val inputStream  = assets.open("test.heic")
    val buffer = ByteArray(8192)
    var bytesRead: Int
    val output = ByteArrayOutputStream()
    while (inputStream.read(buffer).also { bytesRead = it } != -1) {
                output.write(buffer, 0, bytesRead)
    val heifInfo = HeifInfo()
    val fileBuffer: ByteArray = output.toByteArray()
    HeifNative.getInfo(heifInfo, fileBuffer.size.toLong(), fileBuffer)
    // 单帧图片,获取frameList的第一个元素即图片高宽。
    val heifSize = heifInfo.frameList.first()
    // 根据高宽创建好图片。
    val bitmap = Bitmap.createBitmap(
                heifSize.width,
                heifSize.height,
                Bitmap.Config.ARGB_8888)
    // 解码。
    val heifBuffer = HeifNative.toRgba(fileBuffer.size.toLong(), fileBuffer, bitmap)
    image.setImageBitmap(bitmap)
  • 场景二:使用 Glide 集成解码库解码

    Glide 是一个知名开源的图片缓存库。更多信息,请参见 Glide 官方文档

    1. build.gradle 中接入 Glide。

      implementation 'com.aliyun:libheif:1.0.0'
      implementation 'com.github.bumptech.glide:glide:4.13.2'
      implementation 'com.github.bumptech.glide:compiler:4.13.2'
    2. 自定义 HEIF 解码器,在 decode 方法中接入阿里云高性能 HEIC 解码库,具体可以参考 Glide 官方文档 中自定义组件模块。

      class HeifByteBufferBitmapDecoder(bitmapPool: BitmapPool): ResourceDecoder<ByteBuffer, Bitmap> {
          private val bitmapPool: BitmapPool
          init {
              this.bitmapPool = Preconditions.checkNotNull(bitmapPool)
          override fun handles(source: ByteBuffer, options: Options): Boolean {
              val buffer = ByteBufferUtil.toBytes(source)
              return HeifNative.isHeic(buffer.size.toLong(), buffer);
          override fun decode(
              source: ByteBuffer,
              width: Int,
              height: Int,
              options: Options
          ): Resource<Bitmap>? {
              val buffer = ByteBufferUtil.toBytes(source)
              var heifInfo = HeifInfo()
              HeifNative.getInfo(heifInfo, buffer.size.toLong(), buffer)
              val heifSize = heifInfo.frameList[0]
              val bitmap = Bitmap.createBitmap(heifSize.width, heifSize.height, Bitmap.Config.ARGB_8888)
              HeifNative.toRgba(buffer.size.toLong(), buffer, bitmap)
              return BitmapResource.obtain(bitmap, bitmapPool)
      }
    3. 注册解码器 Module。

      @GlideModule(glideName = "HeifGlide")
      open class HeifGlideModule : LibraryGlideModule() {
          override fun registerComponents(
              context: Context, glide: Glide, registry: Registry
              val byteBufferBitmapDecoder = HeifByteBufferBitmapDecoder(glide.bitmapPool)
              registry.prepend(
                  ByteBuffer::class.java,
                  Bitmap::class.java, byteBufferBitmapDecoder
      }
    4. 加载显示 HEIF 图片。

      fun loadHeif(context: Context, imageView: ImageView, file: File) {
           Glide.with(context).asBitmap().load(file).into(imageView)
       }
  • 场景三:使用 Fresco 集成解码库解码

    Fresco Facebook 开源的一个知名 Android 图片缓存库。更多信息,请参见 Fresco 官方文档

    1. build.gradle 中接入 Fresco。

      implementation 'com.aliyun:libheif:1.0.0'
      implementation 'com.facebook.fresco:fresco:2.6.0'
    2. 创建自定义 HEIF 解码器,在 decode 方法中接入阿里云高性能 HEIC 解码库。

      class FrescoHeifDecoder: ImageDecoder {
          private var mFile: File? = null
          private var mUri: Uri? = null
          constructor(file: File) {
              mFile = file
          constructor(uri: Uri) {
              mUri = uri
          override fun decode(
              encodedImage: EncodedImage,
              length: Int,
              qualityInfo: QualityInfo,
              options: ImageDecodeOptions
          ): CloseableImage? {
              var imageRequest: ImageRequest? = null
              if (mFile != null) {
                  imageRequest = ImageRequest.fromFile(mFile)
              if (mUri != null) {
                  imageRequest = ImageRequest.fromUri(mUri)
              try {
                  val cacheKey = DefaultCacheKeyFactory.getInstance()
                      .getEncodedCacheKey(imageRequest, null)
                  val fileCache = ImagePipelineFactory.getInstance()
                      .mainFileCache
                  val resource = fileCache.getResource(cacheKey)
                  val file: File = if (resource == null) {
                      mFile!!
                  } else {
                      (resource as FileBinaryResource).file
                  val bufferFromFile = ByteBufferUtil.fromFile(file)
                  val bytes = ByteBufferUtil.toBytes(bufferFromFile)
                  val heifInfo = HeifInfo()
                  HeifNative.getInfo(heifInfo, file.length(), bytes)
                  val heifSize = heifInfo.frameList[0]
                  val bitmap =
                  Bitmap.createBitmap(heifSize.width, heifSize.height, Bitmap.Config.ARGB_8888)
                  HeifNative.toRgba(file.length(), bytes, bitmap)
                  return CloseableStaticBitmap(
                      pinBitmap(bitmap),
                      qualityInfo,
                      encodedImage.rotationAngle,
                      encodedImage.exifOrientation
              } catch (e: Exception) {
              return null
          private fun pinBitmap(bitmap: Bitmap?): CloseableReference<Bitmap>? {
              return CloseableReference.of(Preconditions.checkNotNull(bitmap), BitmapCounterProvider.get().releaser)
      }

      接入阿里云高性能 HEIC 解码库时,调用了 ByteBufferUtil 参考类,具体如下:

      object ByteBufferUtil {
          @Throws(IOException::class)
          fun fromFile(file: File): ByteBuffer {
              var raf: RandomAccessFile? = null
              var channel: FileChannel? = null
              return try {
                  val fileLength = file.length()
                  if (fileLength > Int.MAX_VALUE) {
                      throw IOException("File too large to map into memory")
                  if (fileLength == 0L) {
                      throw IOException("File unsuitable for memory mapping")
                  raf = RandomAccessFile(file, "r")
                  channel = raf.channel
                  channel.map(FileChannel.MapMode.READ_ONLY, 0, fileLength).load()
              } finally {
                  if (channel != null) {
                      try {
                          channel.close()
                      } catch (e: IOException) {
                  if (raf != null) {
                      try {
                          raf.close()
                      } catch (e: IOException) {
          fun toBytes(byteBuffer: ByteBuffer): ByteArray {
              val result: ByteArray
              val safeArray = getSafeArray(byteBuffer)
              if (safeArray != null && safeArray.offset == 0 && safeArray.limit == safeArray.data.size) {
                  result = byteBuffer.array()
              } else {
                  val toCopy = byteBuffer.asReadOnlyBuffer()
                  result = ByteArray(toCopy.limit())
                  rewind(toCopy)
                  toCopy[result]
              return result
          private fun rewind(buffer: ByteBuffer): ByteBuffer {
              return buffer.position(0) as ByteBuffer
          private fun getSafeArray(byteBuffer: ByteBuffer): SafeArray? {
              return if (!byteBuffer.isReadOnly && byteBuffer.hasArray()) {
                  SafeArray(byteBuffer.array(), byteBuffer.arrayOffset(), byteBuffer.limit())
              } else null
          internal class SafeArray
              (val data: ByteArray, val offset: Int, val limit: Int)
      }
    3. 加载 HEIF 图片。

      fun loadHeif(simpleDraweeView: SimpleDraweeView, file: File) {
              val request =
              ImageRequestBuilder.newBuilderWithSource(Uri.fromFile(file)).setImageDecodeOptions(
                      ImageDecodeOptions.newBuilder().setCustomImageDecoder(FrescoHeifDecoder(file))
                          .build()
                  ).build()
              val controller = Fresco.newDraweeControllerBuilder()
                  .setImageRequest(request).build()
              simpleDraweeView.controller = controller
          }

Android 接入 AVIF 解码

AVIF 是一种基于 AV1 视频编码的新图像格式,相比 JPEG WebP,压缩率更高,画面细节更好。AVIF 文件大小约为同质量 JPEG 文件的 35%。

AVIF 解码可采用官方解码库 libavif,已封装 Android 接口,代码在 android_jni 目录,更多信息请参见 Github

AVIF 集成解码库

build.gradle 中添加以下依赖:

implementation'org.aomedia.avif.android:avif:0.11.1.3c786d2'

直接解码

调用以下方法解码 ByteBuffer:

/**
   * Decodes the AVIF image into the bitmap.
   * @param encoded The encoded AVIF image. encoded.position() must be 0.
   * @param length Length of the encoded buffer.
   * @param bitmap The decoded pixels will be copied into the bitmap.
   * @param threads Number of threads to be used for the AVIF decode. Zero means use number of CPU
   *     cores as the thread count. Negative values are invalid. When this value is > 0, it is
   *     simply mapped to the maxThreads parameter in libavif. For more details, see the
   *     documentation for maxThreads variable in avif.h.
   * @return true on success and false on failure. A few possible reasons for failure are: 1) Input
   *     was not valid AVIF. 2) Bitmap was not large enough to store the decoded image. 3) Negative
   *     value was passed for the threads parameter.
  public static native boolean decode(ByteBuffer encoded, int length, Bitmap bitmap, int threads);

建议将 threads 参数设为 1(单线程解码)。若图片编码使用了 AV1 tiles 特性,开启多线程可提升效率。更多信息请参见 Github

Glide 集成解码库

Glide 自定义 Module 已支持 AVIF,源码请参见 Github

4.13.0 版本开始支持,添加以下依赖即可:

说明

请确保版本号为 4.13.0 及以上版本。

implementation "com.github.bumptech.glide:avif-integration:4.13.2"       

Fresco 集成解码库

Fresco 集成解码库请参见 自定义解码器相关文档