Kotlin的random()总是生成相同的“随机”数字。

6
我创建了一个应用程序,应该从一组图像中随机选择一张图片。在我的模拟器 Nexus 5X Android 5.1 上,一切正常。但是,在我的真实设备 Galaxy Note 10 Lite 上尝试相同操作时,我总是以相同顺序获取相同的“随机”数字。我需要先重新启动手机才能生成新的“随机”数字列表,然后这些数字一直都是相同的。例如,我的数组包含200个元素,我在Galaxy上打开应用程序,它为图像ID选择以下随机数字:43、12、176、33、2、78。然后我关闭应用程序并再次打开应用程序,现在它又有完全相同的“随机”数字:43、12、176、33、2、78。我需要重新启动手机才能获得新的随机数字,这些数字会一直保持不变,直到我再次重启手机。在模拟器上一切正常,每次重新启动应用程序时,我都能如预期地获得新的随机数。
以下是我的应用程序的完整代码,不包括图像列表:
MainActivity.kt
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

 val imageList = arrayOf(Image(R.drawable.image1, false),
            Image(R.drawable.image2, false),
            Image(R.drawable.image3, false))

val imageViewMain = findViewById<ImageView>(R.id.imageViewMain)
    loadNextImage(imageViewMain, imageList)

    imageViewMain.setOnClickListener {
        val dialogClickListener =
            DialogInterface.OnClickListener { _, which ->
                when (which) {
                    DialogInterface.BUTTON_POSITIVE -> {
                        loadNextImage(imageViewMain, imageList)
                    }
                    DialogInterface.BUTTON_NEGATIVE -> { }
                }
            }
        val builder: AlertDialog.Builder = AlertDialog.Builder(this)
        builder.setMessage("Nächstes Bild?").setPositiveButton("Ja", dialogClickListener)
            .setNegativeButton("Nein", dialogClickListener).show()
    }
}

private fun getNextChoice(): Int {
    return (0..1).random()
}

private fun getNextImage(imageList: Array<Image>): Int {
    val listSize = imageList.size
    var imageId: Int
    do {
        imageId = (0 until listSize).random()
    } while (imageList[imageId].played)

    imageList[imageId].played = true
    return imageList[imageId].image
}

private fun loadNextImage(imageViewMain: ImageView, imageList: Array<Image>) {
    val imageQuestionmark = R.drawable.questionmark
    val nextChoice = getNextChoice()
    if (nextChoice == 0) {
        imageViewMain.load(imageQuestionmark)
    } else if (nextChoice == 1) {
        imageViewMain.load(getNextImage(imageList))
    }
    Toast.makeText(this, "Bild hat geladen", Toast.LENGTH_SHORT).show()
}
}

图片:

data class Image(
    val image: Int,
    var played: Boolean
)

编辑: 我尝试了cactustictacs在评论中提出的建议,并创建了一个简单的应用程序,一次使用kotlin随机函数,一次使用java随机函数。以下是我使用的代码:

Kotlin:

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Button
import android.widget.Toast

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val buttonTest = findViewById<Button>(R.id.buttonTest)

        buttonTest.setOnClickListener {
            val getRandomNumber = (0..999).random()
            Toast.makeText(this, getRandomNumber.toString(), Toast.LENGTH_SHORT).show()
        }
    }
}

Java:

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.widget.Button;
import android.widget.Toast;

import java.util.Random;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Button buttonTest = (Button) findViewById(R.id.buttonTest);

        buttonTest.setOnClickListener(v -> {
            int randomNumber = new Random().nextInt(999);
            Toast.makeText(this, "" + randomNumber, Toast.LENGTH_SHORT).show();
        });
    }
}

在 Kotlin 上,我遇到了与我的初始问题相同的行为,无论我对应用程序做什么(即使是卸载和重新安装),我始终得到相同的数字集合。在 Java 上,它按预期工作,一旦我关闭应用程序,就会得到一个新的数字集合。因此,错误明确地出现在 Kotlin 中。

也许这有所帮助,我的 Android 版本是12,我的手机是 Galaxy Note 10 Lite。


如果您制作一个基本的应用程序,其中包含一个调用(0..999).random()Random.nextInt(999)并显示它的按钮,那么您会得到相同的行为吗?它应该使用设备的默认随机源,所以我首先要检查的是您的设备是否实际上正在这样做。也许可以检查一下Java类(例如https://docs.oracle.com/javase/8/docs/api/java/util/Random.html)是否有任何不同的行为。 - cactustictacs
我会尝试一下并在这里回复结果。 - rm -rf
@cactustictacs,我更新了我的帖子,并添加了两个测试应用程序。 - rm -rf
3
这个问题出现在Kotlin 1.7.20-Beta的补丁说明中 KT-52618 ThreadLocalRandom 在 Android SDK 34 之前不是一个好的随机数源, 因此不要在 Kotlin Random 中使用它 - 这与你所提到的问题有关吗? - aSemy
Random.nextInt(999) 在 Kotlin 中能够正常工作吗?还是一样的?Random.Default 呢?您可以在集合、范围等中使用 Random 来生成随机数,还有一个 toKotlinRandom() 扩展可以用于 java.util.Random()(您可以从 Kotlin 调用该扩展),但您不需要提供它。如果您始终在默认的随机生成器中看到此行为,则可能值得在 Kotlin 网站上报告此问题。他们会更了解您在此情况下应该做什么。 - cactustictacs
显示剩余2条评论
3个回答

3

我曾经也遇到过这个问题。我的解决方案是使用一个有种子的随机对象,而不是调用函数Random.nextSomething()。然后我以currentTimeInMillis作为种子来初始化该对象。

var randomGenerator = Random(System.currentTimeMillis())
var result = randomGenerator.nextInt(30, 50)

这是正确的,因为这样你就不会使用Kotlin的Random对象,而是Java的Random对象。不幸的是,在KMM项目中这是不可能的。 - pauminku

3
这是一个相当糟糕的Kotlin默认Random类实现。Java的Random类会尽力在每个新实例上总是使用不同的种子,而Kotlin硬编码了整个设备上相同的种子。这怎么可能成为Random实现的默认行为呢?我花费了很长时间才理解它。
参见Java实现:
 public Random() {
    this(seedUniquifier() ^ System.nanoTime());
}

private static long seedUniquifier() {
    // L'Ecuyer, "Tables of Linear Congruential Generators of
    // Different Sizes and Good Lattice Structure", 1999
    for (;;) {
        long current = seedUniquifier.get();
        long next = current * 181783497276652981L;
        if (seedUniquifier.compareAndSet(current, next))
            return next;
    }
}

private static final AtomicLong seedUniquifier
    = new AtomicLong(8682522807148012L);

现在让我们来介绍 Kotlin:

companion object Default : Random(), Serializable {
    private val defaultRandom: Random = defaultPlatformRandom()

    private object Serialized : Serializable {
        private const val serialVersionUID = 0L

        private fun readResolve(): Any = Random
    }

    private fun writeReplace(): Any = Serialized

    override fun nextBits(bitCount: Int): Int = defaultRandom.nextBits(bitCount)
    override fun nextInt(): Int = defaultRandom.nextInt()
    override fun nextInt(until: Int): Int = defaultRandom.nextInt(until)
    override fun nextInt(from: Int, until: Int): Int = defaultRandom.nextInt(from, until)

defaultRandom是一个单例,始终使用相同的种子进行初始化...

(我在Android Studio源代码中找到了这段代码...)

注意: 这是一个关于kotlin版本1.7.10和Android API低于33-34版本的bug。已经在1.7.20版本中得到修复...


defaultPlatformRandom() 最初是在调用 Java 的 ThreadLocalRandom。这个 Bug 发生在 Android 系统上,而不是 Kotlin 语言本身。因此,在 Android SDK 34 之前,不要使用 ThreadLocalRandom 作为 Kotlin Random 的随机源。更多详细信息请参考 https://youtrack.jetbrains.com/issue/KT-52618/ - Tenfour04
我遇到了同样的问题,据我所知我正在使用kotlin 1.7.20。我在gradle中有这个:implementation("org.jetbrains.kotlin:kotlin-stdlib:1.7.20")。这不够吗?如果有帮助的话,我有compileSdk = 33 - pauminku
1.7.20 应该足够了。 - Mertcan Çüçen
我检查了KotlinVersion.CURRENT,它是1.7.20,但仍然失败。我从头开始创建了一个新项目,以查看我的测试代码是否正确,结果每次重新启动都会重复相同的序列,对于1.7.10来说工作正常,而对于1.7.20则没有问题。也许我有一些依赖关系使其无论Kotlin版本如何都无法正常工作...我不知道还能尝试什么。 - pauminku

1
看起来您正在从活动的 onCreate() 中调用 loadNextImage()。这意味着,除非活动被销毁,否则它将永远不会重新生成您想要的随机 ID。如果您强制停止活动,然后重新启动它,会发生什么?我希望您能获得一组新的 ID。
如果我的猜测是正确的,并且您想每次打开活动时都获得一组新的 ID,则解决方案是从 onResume() 中调用 loadNextImage()(如果您不想每次恢复活动时都生成它们,您需要包含一些逻辑来决定何时重新生成这些 ID)。

那个函数每次被调用时都会生成一个新的序列,因此对于每次点击它应该是一张随机图片 - 它不会缓存一组结果或其他任何东西。而且OP说它在模拟器上运行良好,可能是OEM问题(尽管对于像随机数生成这样的东西,这似乎是一个巨大的问题)。 - cactustictacs
我想说的是,如果该活动未被销毁,则每次他打开应用程序时都不会生成任何内容。它已经生成了所有的ID。没有理由缓存任何东西--该活动从未被销毁,因此它只是显示在第一次运行期间生成的内容。在模拟器上,只要他按下“返回”按钮,就可能会杀死该活动。但在真实设备上,这可能只是将其放在后台,直到操作系统必须杀死它以释放资源。 - user496854
我不明白你的意思 - 它在哪里存储生成的ID?有一个固定的“Image”对象列表,但它通过调用“getNextImage”随机循环遍历这些对象,直到找到一个仍然将“played”设置为false的对象。调用“random()”应该(没有使用相同值种子的Random源)产生唯一的序列。OP每次重新启动应用程序时都会看到相同的序列。 - cactustictacs
我已经尝试过强制关闭应用程序,也通过手机设置删除了应用程序缓存,出于某种原因还重新安装了应用程序,但除非我重启手机,否则我始终会在我的真实设备上获得相同的“随机”ID。正如我所说,在模拟器上一切都按预期工作。只要我关闭应用程序,就会获得一组新的ID。 - rm -rf
@user496854 请查看编辑 - rm -rf
好的,那么你能否在build.gradle中发布你的kotlinOptions部分?另外,请提供androidx.core:core-ktxorg.jetbrains.kotlin:kotlin-stdlib-jdk7的版本号。 - user496854

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接