IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> 记一次 kotlin 在 MutableList 中使用 remove 引发的问题 -> 正文阅读

[移动开发]记一次 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

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

# 缩放
[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

稍微解释一下上述命令:

上述缩放中的代码表示把输入的第 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)

....

上述代码中,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")

不出意外,输出结果为:

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

果然 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

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

    override fun remove(element: E): Boolean {
        checkIsMutable()
        val i = indexOf(element)
        if (i >= 0) removeAt(i)
        return i >= 0
    }

代码很简单,核心就在第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}
     */

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

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

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

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

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

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

再看一段代码:

val a = listOf(100, 100)
val b = listOf(100, 100)

println(a==b)

// 输出
true

这又是为什么???

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

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

gifResolution.remove(totalResolution)
gifResolution.remove(minResolution)

改为:

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

参考资料

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

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

  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2022-07-21 21:40:09  更:2022-07-21 21:40:26 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/25 3:35:43-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码