Deep Dive: MediaPlayer Best Practices

ภาพถ่ายโดย Marcela Laskoski บน Unsplash

MediaPlayer ดูเหมือนว่าจะใช้งานง่ายหลอกลวง แต่ความซับซ้อนมีชีวิตอยู่ใต้พื้นผิว ตัวอย่างเช่นอาจดึงดูดให้เขียนสิ่งนี้:

MediaPlayer.create (บริบท, R.raw.cowbell) .start ()

วิธีนี้ใช้งานได้ดีในครั้งแรกและอาจเป็นครั้งที่สองที่สามหรือมากกว่านั้น อย่างไรก็ตาม MediaPlayer ใหม่แต่ละตัวใช้ทรัพยากรระบบเช่นหน่วยความจำและตัวแปลงสัญญาณ สิ่งนี้สามารถลดประสิทธิภาพของแอพของคุณและอาจเป็นได้ทั้งอุปกรณ์

โชคดีที่เป็นไปได้ที่จะใช้ MediaPlayer ในวิธีที่ง่ายและปลอดภัยโดยปฏิบัติตามกฎง่ายๆ

กรณีง่าย

กรณีพื้นฐานที่สุดคือเรามีไฟล์เสียงซึ่งอาจเป็นทรัพยากรดิบที่เราต้องการเล่น ในกรณีนี้เราจะสร้างผู้เล่นเดี่ยวนำมาใช้ใหม่ทุกครั้งที่เราต้องการเล่นเสียง ผู้เล่นควรสร้างด้วยสิ่งนี้:

private val mediaPlayer = MediaPlayer (). ใช้ {
    setOnPreparedListener {start ()}
    setOnCompletionListener {รีเซ็ต ()}
}

เครื่องเล่นถูกสร้างขึ้นพร้อมผู้ฟังสองคน:

  • OnPreparedListener ซึ่งจะเริ่มเล่นโดยอัตโนมัติหลังจากที่ผู้เล่นเตรียม
  • OnCompletionListener ซึ่งล้างทรัพยากรโดยอัตโนมัติเมื่อเล่นจบ

เมื่อผู้เล่นสร้างขึ้นขั้นตอนต่อไปคือการสร้างฟังก์ชั่นที่ใช้ ID ทรัพยากรและใช้ MediaPlayer เพื่อเล่น:

แทนที่ความสนุก playSound (@RawRes rawResId: Int) {
    val assetFileDescriptor = context.resources.openRawResourceFd (rawResId) หรือไม่: ส่งคืน
    mediaPlayer.run {
        รีเซ็ต ()
        setDataSource (assetFileDescriptor.fileDescriptor, assetFileDescriptor.startOffset, assetFileDescriptor.declaredLength)
        prepareAsync ()
    }
}

มีวิธีการสั้น ๆ นี้เกิดขึ้นบ้าง:

  • รหัสทรัพยากรต้องถูกแปลงเป็น AssetFileDescriptor เพราะนี่คือสิ่งที่ MediaPlayer ใช้ในการเล่นทรัพยากรดิบ การตรวจสอบค่า null ทำให้แน่ใจว่ามีทรัพยากรอยู่
  • การรีเซ็ตการโทร () ทำให้แน่ใจว่าผู้เล่นอยู่ในสถานะเริ่มต้น สิ่งนี้ใช้งานได้ไม่ว่าผู้เล่นจะอยู่ในสถานะใด
  • ตั้งค่าแหล่งข้อมูลสำหรับเครื่องเล่น
  • prepareAsync เตรียมผู้เล่นให้เล่นและกลับมาทันทีโดยรักษา UI ตอบสนอง ใช้งานได้เนื่องจาก OnPreparedListener ที่แนบมาเริ่มเล่นหลังจากที่ได้รับการเตรียมแหล่งที่มา

เป็นสิ่งสำคัญที่จะต้องทราบว่าเราไม่ได้โทรออก () ในเครื่องเล่นของเราหรือตั้งค่าเป็นโมฆะ เราต้องการนำมาใช้ซ้ำ! ดังนั้นเราจึงเรียกการรีเซ็ต () ซึ่งเพิ่มหน่วยความจำและตัวแปลงสัญญาณที่ใช้อยู่

การเล่นเสียงนั้นง่ายเหมือนการโทร:

PlaySound (R.raw.cowbell)

! ง่าย

Cowbells เพิ่มเติม

การเล่นทีละหนึ่งเสียงเป็นเรื่องง่าย แต่ถ้าหากคุณต้องการเริ่มเสียงอื่นในขณะที่เสียงแรกยังคงเล่นอยู่ การโทร playSound () หลายครั้งเช่นนี้จะไม่ทำงาน:

PlaySound (R.raw.big_cowbell)
PlaySound (R.raw.small_cowbell)

ในกรณีนี้ R.raw.big_cowbell เริ่มเตรียมพร้อม แต่การโทรครั้งที่สองจะรีเซ็ตผู้เล่นก่อนที่จะมีอะไรเกิดขึ้นดังนั้นคุณจะได้ยินเฉพาะ R.raw.small_cowbell

และถ้าเราต้องการเล่นหลายเสียงพร้อมกันในเวลาเดียวกัน เราต้องสร้าง MediaPlayer สำหรับแต่ละรายการ วิธีที่ง่ายที่สุดในการทำเช่นนี้คือมีรายการผู้เล่นที่ใช้งานอยู่ บางทีสิ่งนี้:

คลาส MediaPlayers (บริบท: บริบท) {
    บริบท val ส่วนตัว: Context = context.applicationContext
    private val playersInUse = mutableListOf  ()

    private fun buildPlayer () = MediaPlayer (). ใช้ {
        setOnPreparedListener {start ()}
        setOnCompletionListener {
            it.release ()
            playersInUse - = มัน
        }
    }

    แทนที่ความสนุก playSound (@RawRes rawResId: Int) {
        val assetFileDescriptor = context.resources.openRawResourceFd (rawResId) หรือไม่: ส่งคืน
        val mediaPlayer = buildPlayer ()

        mediaPlayer.run {
            ผู้เล่นใช้ + = มัน
            setDataSource (assetFileDescriptor.fileDescriptor, assetFileDescriptor.startOffset
                    assetFileDescriptor.declaredLength)
            prepareAsync ()
        }
    }
}

ตอนนี้ทุก ๆ เสียงมีผู้เล่นเป็นของตัวเองมันเป็นไปได้ที่จะเล่นทั้ง R.raw.big_cowbell และ R.raw.small_cowbell ด้วยกัน! ที่สมบูรณ์แบบ!

…ดีเกือบสมบูรณ์แบบ ไม่มีอะไรในรหัสของเราที่ จำกัด จำนวนของเสียงที่สามารถเล่นได้ในเวลาเดียวและ MediaPlayer ยังคงต้องมีหน่วยความจำและตัวแปลงสัญญาณเพื่อใช้งาน เมื่อพวกเขาหมด MediaPlayer ล้มเหลวอย่างเงียบ ๆ เพียงสังเกต“ E / MediaPlayer: ข้อผิดพลาด (1, -19)” ใน logcat

ป้อน MediaPlayerPool

เราต้องการสนับสนุนการเล่นหลายเสียงในครั้งเดียว แต่เราไม่ต้องการให้หน่วยความจำหรือโคเดกหมด วิธีที่ดีที่สุดในการจัดการสิ่งเหล่านี้คือการมีกลุ่มผู้เล่นแล้วเลือกหนึ่งกลุ่มเพื่อใช้เมื่อเราต้องการเล่นเสียง เราสามารถอัปเดตโค้ดของเราให้เป็นดังนี้:

คลาส MediaPlayerPool (บริบท: บริบท, maxStreams: Int) {
    บริบท val ส่วนตัว: Context = context.applicationContext

    private val mediaPlayerPool = mutableListOf  () .also {
        สำหรับ (ฉันเป็น 0..maxStreams) มัน + = buildPlayer ()
    }
    private val playersInUse = mutableListOf  ()

    private fun buildPlayer () = MediaPlayer (). ใช้ {
        setOnPreparedListener {start ()}
        setOnCompletionListener {recyclePlayer (it)}
    }

    / **
     * ส่งคืน [MediaPlayer] ถ้ามี
     * เป็นโมฆะ
     * /
    คำร้องขอความสนุกส่วนตัว (): MediaPlayer? {
        ส่งคืนถ้า (! mediaPlayerPool.isEmpty ()) {
            mediaPlayerPool.removeAt (0) .also {
                ผู้เล่นใช้ + = มัน
            }
        } else เป็นโมฆะ
    }

    ความสนุกส่วนตัว recyclePlayer (mediaPlayer: MediaPlayer) {
        mediaPlayer.reset ()
        playersInUse - = mediaPlayer
        mediaPlayerPool + = mediaPlayer
    }

    fun playSound (@RawRes rawResId: Int) {
        val assetFileDescriptor = context.resources.openRawResourceFd (rawResId) หรือไม่: ส่งคืน
        val mediaPlayer = requestPlayer ()?: return

        mediaPlayer.run {
            setDataSource (assetFileDescriptor.fileDescriptor, assetFileDescriptor.startOffset
                    assetFileDescriptor.declaredLength)
            prepareAsync ()
        }
    }
}

ตอนนี้เสียงหลายเสียงสามารถเล่นได้ในคราวเดียวและเราสามารถควบคุมจำนวนผู้เล่นพร้อมกันสูงสุดเพื่อหลีกเลี่ยงการใช้หน่วยความจำมากเกินไปหรือตัวแปลงสัญญาณมากเกินไป และเนื่องจากเรากำลังรีไซเคิลอินสแตนซ์ตัวเก็บรวบรวมขยะจะไม่ต้องเรียกใช้เพื่อล้างข้อมูลอินสแตนซ์เก่าทั้งหมดที่เล่นจนจบ

วิธีการนี้มีข้อเสียเล็กน้อย:

  • หลังจากเสียง maxStreams กำลังเล่นการโทรไปที่ playSound ใด ๆ จะถูกข้ามจนกว่าผู้เล่นจะได้รับอิสระ คุณสามารถแก้ไขได้ด้วยการ“ ขโมย” ผู้เล่นที่ใช้เล่นเสียงใหม่อยู่แล้ว
  • อาจมีความล่าช้าอย่างมากระหว่างการโทร playSound และการเล่นเสียง แม้ว่า MediaPlayer จะถูกนำมาใช้ซ้ำจริง ๆ แล้วมันเป็นเสื้อคลุมบาง ๆ ที่ควบคุมวัตถุพื้นเมือง C ++ พื้นฐานผ่าน JNI โปรแกรมเล่นเนทีฟจะถูกทำลายทุกครั้งที่คุณเรียก MediaPlayer.reset () และต้องสร้างใหม่ทุกครั้งที่เตรียม MediaPlayer

การปรับปรุงความล่าช้าในขณะที่การรักษาความสามารถในการใช้ซ้ำผู้เล่นทำได้ยากขึ้น โชคดีสำหรับเสียงและแอปบางประเภทที่ต้องการเวลาในการตอบสนองต่ำมีตัวเลือกอื่นที่เราจะพิจารณาในครั้งต่อไป: SoundPool