• 记一次 kotlin 在 MutableList 中使用 remove 引发的问题


    背景

    我开发的APP,隐云图解制作 中有一个功能是拼接多个 gif 动图到一张动图上。

    这段代码是使用 FFmpeg 来实现。

    测试时发现直接水平拼接和直接横向拼接都没有问题,唯独方形拼接(类似九宫格)在开启按比例缩放后总是出现问题。

    而这段代码的核心逻辑是先遍历输入文件列表,读取每个 gif 的尺寸信息并将其保存下来,同时按照特定拼接规则计算出缩放尺寸并保存。

    然后将计算得到的尺寸生成 FFmpeg 命令。

    排查过程

    使用四张分辨率分别为:
    640x368, 1080x1920, 640x368, 800x1280
    的动图经过缩放计算后得到如下 MutableList:

    [[640, 368], [640, 1137], [640, 368], [640, 1024]]

    生成的 FFmpeg 命令为:

    -y -i jointBg.png -i 2022-07-20-13-44-41-by_EL.gif -i 2022-07-20-13-44-18-by_EL.gif -i 2022-07-13-15-51-07-by_EL.gif -i 2022-07-14-14-28-57-by_EL.gif -filter_complex [0:v]pad=1280:2161[bg];[1:v]scale=640:1137[gif0];[2:v]scale=640:368[gif1];[3:v]scale=640:1024[gif2];[4:v]scale=640:368[gif3];[bg][gif0] overlay=0:0[over0];[over0][gif1] overlay=640:0[over1];[over1][gif2] overlay=0:1137[over2];[over2][gif3] overlay=640:368 2022-07-20-14-03-17-by_EL.gif
    
    • 1

    把上述命令中的其他参数移除,仅保留缩放和拼接命令,并适当的加点缩进:

    # 缩放
    [0:v]pad=1280:2161[bg];
    [1:v]scale=640:1137[gif0];
    [2:v]scale=640:368[gif1];
    [3:v]scale=640:1024[gif2];
    [4:v]scale=640:368[gif3];
    
    # 拼接
    [bg][gif0] overlay=0:0[over0];
    [over0][gif1] overlay=640:0[over1];
    [over1][gif2] overlay=0:1137[over2];
    [over2][gif3] overlay=640:368
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    稍微解释一下上述命令:

    上述缩放中的代码表示把输入的第 n 个文件按照 x:y 分辨率缩放后取别名为 xxx,如:

    [0:v]pad=1280:2161[bg] 表示把输入的第 0 个文件当成画板,并扩展分辨率为 1280x2161,然后取别名为 bg 以供后续使用。

    [1:v]scale=640:1137[gif0] 表示把第 1 个文件缩放为 640x1137 并取别名为 gif0.

    而拼接中的代码也很好理解,如:

    [bg][gif0] overlay=0:0[over0] 表示将别名为 gif0 的文件覆盖到 别名为 bg 的文件上,并且起点坐标为 0:0 ,最后将处理后的文件取别名为 over0 以供后续使用。

    解释完,各位有没有发现问题?没有?哈哈,没有就对了,等我画个图就理解了。

    按照预想情况,拼接后的动图应该是这样排列的:

    请添加图片描述

    (哈哈哈,因为这里只是用极端个例来说明这个现象,所以拼出来的是这么一个奇怪的形状)

    但是实际情况确实这样的:

    请添加图片描述

    哈哈,发现问题了吧。

    拼接的方向居然反了,如果只是反了到也还能看,关键是连同缩放尺寸一起反了,导致 16:9 的动图被强行拉伸成了 9:16,而原本 9:16 的动图又被强行压缩成了 16:9 ,那观感,简直不要太惨不忍睹。

    那么造成这种情况的原因是什么呢?

    先看代码:

    val gifResolution = getJointGifResolution(context, jointMode, gifUris)
    Log.i(TAG, "jointGif: gifResolution = $gifResolution")
    
    
    val totalResolution = gifResolution[gifResolution.size - 2]
    val minResolution = gifResolution[gifResolution.size - 1]
    
    gifResolution.remove(totalResolution)
    gifResolution.remove(minResolution)
    
    val cmdBuilder = FFMpegArgumentsBuilder.Builder()
    cmdBuilder.setOverride(true)
            .setInput(jointBg.absolutePath)  //输入背景
    
    for (uri in gifUris) {  //输入GIF
        cmdBuilder.setInput(FileUtils.getMediaAbsolutePath(context, uri))
    }
    
    cmdBuilder.setArg("-filter_complex")
    
    
    var cmdFilter = ""
    //设置背景并扩展分辨率到 total
    cmdFilter += "[0:v]pad=${totalResolution[0]}:${totalResolution[1]}[bg];"
    
    //将输入文件缩放并取别名为 gifX (X为索引)
    gifResolution.forEachIndexed { index, mutableList ->
        cmdFilter += "[${index+1}:v]scale=${mutableList[0]}:${mutableList[1]}[gif$index];"
    }
    cmdFilter += "[bg][gif0] overlay=0:0[over0];"
    
    //开始叠加动图
    cmdFilter += getCmdFilterOverlaySquare(gifUris, gifResolution)
    
    ....
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    上述代码中,val gifResolution = getJointGifResolution(context, jointMode, gifUris) 表示计算输入文件的分辨率信息,计算完毕后打印日志输出的分辨率顺序还是正确的。

    但是此处,为了方便返回数据,我会在原有 list 末尾再添加两条数据,并且在遍历前将这两条数据通过 remove 移除。

    此时再去遍历这个 list 就出现了上述所说的顺序错乱。

    所以合理猜测是由于调用了 remove 导致顺序被重新排列了?

    我们写一段 demo 来尝试一下:

    val list: MutableList<MutableList<Int>> = mutableListOf()
    
    // 添加 ”正常数据“
    list.add(mutableListOf(640, 368))
    list.add(mutableListOf(640, 1137))
    list.add(mutableListOf(640, 368))
    list.add(mutableListOf(640, 1024))
    
    // 添加 ”额外数据“
    list.add(mutableListOf(1280, 2161))
    list.add(mutableListOf(640, 368))
    
    // 先输出处理前的 list
    println("处理前:$list")
    
    // 模拟处理数据
    val value1 = list[list.size - 2]
    val value2 = list[list.size - 1]
    
    list.remove(value1)
    list.remove(value2)
    
    // 输出处理后数据
    println("处理后:$list")
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    不出意外,输出结果为:

    处理前:[[640, 368], [640, 1137], [640, 368], [640, 1024], [1280, 2161], [640, 368]]
    处理后:[[640, 1137], [640, 368], [640, 1024], [640, 368]]
    
    • 1
    • 2

    果然 list 被重新排序了。

    那么问题来了,为什么呢?又该怎么解决呢?

    错误原因及解决方法

    既然已经知道了问题出自于 remove 方法,那么自然是从它下手。

    先来看看 remove 的源码:

        /**
         * Removes a single instance of the specified element from this
         * collection, if it is present.
         *
         * @return `true` if the element has been successfully removed; `false` if it was not present in the collection.
         */
        public fun remove(element: E): Boolean
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    嗯,是个接口,问题不大,找到它的实现:

        override fun remove(element: E): Boolean {
            checkIsMutable()
            val i = indexOf(element)
            if (i >= 0) removeAt(i)
            return i >= 0
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    代码很简单,核心就在第3-4行,使用 indexOf 查找到元素位置后调用 removeAt 删除。

    哈哈,看出问题没有?

    其实去我在刚写完背景时就突然发现了问题所在,但是想着写都写了,还是假装不知道继续写下去吧。

    什么?还是没看出来问题所在?

    那么我们来看看 indexOf 的源码……算了,源码都不用看了,直接看注释:

        /**
         * {@inheritDoc}
         *
         * @implSpec
         * This implementation first gets a list iterator (with
         * {@code listIterator()}).  Then, it iterates over the list until the
         * specified element is found or the end of the list is reached.
         *
         * @throws ClassCastException   {@inheritDoc}
         * @throws NullPointerException {@inheritDoc}
         */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    看到了吗?这个方法返回的是“指定元素第一次出现的索引位置”

    也就是说,调用 remove 后删除的是第一个该元素。

    我们再来看看上述代码返回的删除前后的 list 变化:

    处理前:[[640, 368], [640, 1137], [640, 368], [640, 1024], [1280, 2161], [640, 368]]
    处理后:[[640, 1137], [640, 368], [640, 1024], [640, 368]]
    
    • 1
    • 2

    发现了吗?并不是删除元素后 list 被重排了,只是好巧不巧的,我想删除最后一个 [640, 368] 但是,前面也有一个一样的 list 导致删除的是前面的第一个元素,而非我想要删除的最后一个元素。

    那么问题来了,它凭什么就说这两个 [640, 368] 是同一个对象?

    再看一段代码:

    val a = listOf(100, 100)
    val b = listOf(100, 100)
    
    println(a==b)
    
    // 输出
    true
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    这又是为什么???

    哈哈哈,这个问题留给各位自己想了。

    总之,最后的解决方法也很简单,因为我需要删除的元素索引是固定的,所以只需要把代码中的:

    gifResolution.remove(totalResolution)
    gifResolution.remove(minResolution)
    
    • 1
    • 2

    改为:

    // 别看了,没写错,就是两个 size-1 ,为啥?你猜
    gifResolution.removeAt(gifResolution.size - 1)
    gifResolution.removeAt(gifResolution.size - 1)
    
    • 1
    • 2
    • 3

    参考资料

    1. Equality
    2. 揭秘 Kotlin 中的 == 和 ===

    原文发表于我的博客:Likehide.com

  • 相关阅读:
    Oracle常用命令
    vueRouter 重定向 高亮 传参 嵌套 简单示例
    Python基础(6-1)函数
    【PTE-day05 宽字节注入】
    【Python】PySpark 数据输入 ① ( RDD 简介 | RDD 中的数据存储与计算 | Python 容器数据转 RDD 对象 | 文件文件转 RDD 对象 )
    [附源码]Python计算机毕业设计Django宁财二手物品交易网站
    竞赛选题 深度学习的水果识别 opencv python
    九度 1463 招聘会(任务调度, 贪心算法)
    [附源码]java毕业设计基于servlet技术实现游戏娱乐平台
    如何挑选猫主食罐头?宠物店自用的5款猫主食罐头推荐!
  • 原文地址:https://blog.csdn.net/sinat_17133389/article/details/125895011