الغوص العميق: أفضل ممارسات MediaPlayer

الصورة مارسيلا Laskoski على Unsplash

يبدو أن MediaPlayer سهل الاستخدام بشكل خادع ، لكن التعقيد يعيش تحت السطح مباشرة. على سبيل المثال ، قد يكون من المغري كتابة شيء مثل هذا:

MediaPlayer.create (سياق ، R.raw.cowbell) .start ()

هذا يعمل بشكل جيد في المرة الأولى وربما الثانية أو الثالثة أو حتى أكثر من مرة. ومع ذلك ، يستهلك كل MediaPlayer جديد موارد النظام ، مثل الذاكرة وبرامج الترميز. يمكن أن يؤدي ذلك إلى انخفاض أداء التطبيق ، وربما الجهاز بأكمله.

لحسن الحظ ، من الممكن استخدام MediaPlayer بطريقة بسيطة وآمنة من خلال اتباع بعض القواعد البسيطة.

القضية البسيطة

الحالة الأساسية هي أن لدينا ملفًا صوتيًا ، وربما موردًا خامًا ، نود فقط تشغيله. في هذه الحالة ، سنقوم بإنشاء مشغل واحد يعيد استخدامه في كل مرة نحتاج فيها لتشغيل صوت. يجب إنشاء المشغل باستخدام شيء مثل هذا:

private val mediaPlayer = MediaPlayer (). Apply {
    setOnPreparedListener {start ()}
    setOnCompletionListener {reset ()}
}

تم إنشاء المشغل مع اثنين من المستمعين:

  • OnPreparedListener ، والذي سيبدأ التشغيل تلقائيًا بعد إعداد اللاعب.
  • يقوم OnCompletionListener بتنظيف الموارد تلقائيًا عند انتهاء التشغيل.

مع إنشاء المشغل ، تكون الخطوة التالية هي إنشاء دالة تأخذ معرف مورد وتستخدم MediaPlayer لتشغيله:

تجاوز متعة playSound (RawRes rawResId: Int) {
    val الأصولFileDescriptor = context.resources.openRawResourceFd (rawResId)؟: return
    mediaPlayer.run {
        إعادة تعيين()
        setDataSource (originFileDescriptor.fileDescriptor، stocksFileDescriptor.startOffset، stocksFileDescriptor.declaredLength)
        prepareAsync ()
    }
}

هناك بعض الشيء يحدث في هذه الطريقة القصيرة:

  • يجب تحويل معرف المورد إلى AssetFileDescriptor لأن هذا هو ما يستخدمه MediaPlayer لتشغيل الموارد الأولية. يضمن التحقق من وجود المورد.
  • استدعاء إعادة التعيين () يضمن أن اللاعب في حالة التهيئة. هذا يعمل بغض النظر عن حالة اللاعب.
  • اضبط مصدر البيانات للاعب.
  • preparAsync يعد اللاعب للعب والعودة على الفور ، مما يجعل واجهة المستخدم سريعة الاستجابة. هذا يعمل لأن OnPreparedListener المرفق يبدأ اللعب بعد إعداد المصدر.

من المهم أن نلاحظ أننا لا ندعو إطلاق () على لاعب لدينا أو تعيينه فارغ. نريد إعادة استخدامها! لذلك بدلاً من ذلك ، ندعو reset () ، الذي يحرر الذاكرة وبرامج الترميز التي كان يستخدمها.

تشغيل الصوت بسيط مثل الاتصال:

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 (السياق: السياق) {
    سياق فال الخاص: السياق = سياق.التطبيق
    لاعبين خاصين في الاستخدام = mutableListOf  ()

    fun fun buildPlayer () = MediaPlayer (). Apply {
        setOnPreparedListener {start ()}
        setOnCompletionListener {
            it.release ()
            playerInUse - = ذلك
        }
    }

    تجاوز متعة playSound (RawRes rawResId: Int) {
        val الأصولFileDescriptor = context.resources.openRawResourceFd (rawResId)؟: return
        فال ميديا ​​بلاير = buildPlayer ()

        mediaPlayer.run {
            playerInUse + = ذلك
            setDataSource (originFileDescriptor.fileDescriptor، stocksFileDescriptor.startOffset،
                    assetFileDescriptor.declaredLength)
            prepareAsync ()
        }
    }
}

الآن بعد أن أصبح لكل صوت لاعب خاص به ، يمكن تشغيل كل من R.raw.big_cowbell و R.raw.small_cowbell معًا! في احسن الاحوال!

... حسنا ، ما يقرب من الكمال. لا يوجد أي شيء في التعليمات البرمجية الخاصة بنا يحد من عدد الأصوات التي يمكن تشغيلها في آن واحد ، وما زال MediaPlayer بحاجة إلى ذاكرة وبرامج ترميز للعمل معها. عندما ينفد ، فشل MediaPlayer بصمت ، مع ملاحظة "E / MediaPlayer: Error (1 ، -19)" في logcat.

أدخل MediaPlayerPool

نريد دعم تشغيل أصوات متعددة في وقت واحد ، لكننا لا نريد نفاد الذاكرة أو برامج الترميز. أفضل طريقة لإدارة هذه الأشياء هي الحصول على مجموعة من اللاعبين ثم اختيار واحد لاستخدامه عندما نريد تشغيل صوت. يمكننا تحديث الكود الخاص بنا ليكون هكذا:

فئة MediaPlayerPool (السياق: السياق ، maxStreams: Int) {
    سياق فال الخاص: السياق = سياق.التطبيق

    private val mediaPlayerPool = mutableListOf  () .also {
        من أجل (i في 0..maxStreams) + + buildPlayer ()
    }
    لاعبين خاصين في الاستخدام = mutableListOf  ()

    fun fun buildPlayer () = MediaPlayer (). Apply {
        setOnPreparedListener {start ()}
        setOnCompletionListener {recyclePlayer (it)}
    }

    / **
     * إرجاع [MediaPlayer] في حالة توفرها ،
     * لاغى خلاف ذلك.
     * /
    requestPlayer () متعة خاصة: MediaPlayer؟ {
        إرجاع إذا (! mediaPlayerPool.isEmpty ()) {
            mediaPlayerPool.removeAt (0). أيضا
                playerInUse + = ذلك
            }
        } لاغٍ آخر
    }

    متعة خاصة إعادة التدوير (mediaPlayer: MediaPlayer) {
        mediaPlayer.reset ()
        playersInUse - = mediaPlayer
        mediaPlayerPool + = mediaPlayer
    }

    متعة playSound (RawRes rawResId: Int) {
        val الأصولFileDescriptor = context.resources.openRawResourceFd (rawResId)؟: return
        val mediaPlayer = requestPlayer ()؟: return

        mediaPlayer.run {
            setDataSource (originFileDescriptor.fileDescriptor، stocksFileDescriptor.startOffset،
                    assetFileDescriptor.declaredLength)
            prepareAsync ()
        }
    }
}

يمكن الآن تشغيل أصوات متعددة في آن واحد ، ويمكننا التحكم في الحد الأقصى لعدد المشغلات المتزامنة لتجنب استخدام الكثير من الذاكرة أو العديد من برامج الترميز. ونظرًا لأننا نعيد تدوير الحالات ، فلن يتعين على جامع القمامة أن يعمل على تنظيف جميع الحالات القديمة التي انتهت من اللعب.

هناك بعض الجوانب السلبية لهذا النهج:

  • بعد تشغيل أصوات maxStreams ، يتم تجاهل أي مكالمات إضافية إلى playSound حتى يتم تحرير اللاعب. يمكنك التغلب على هذا من خلال "سرقة" لاعب مستخدم بالفعل لتشغيل صوت جديد.
  • يمكن أن يكون هناك تأخير كبير بين استدعاء playSound وتشغيل الصوت فعليًا. على الرغم من إعادة استخدام MediaPlayer ، إلا أنه في الواقع عبارة عن غلاف رفيع يتحكم في كائن C ++ أساسي عبر JNI. يتم إتلاف المشغل الأصلي في كل مرة تتصل فيها بـ MediaPlayer.reset () ، ويجب إعادة إنشائه كلما تم إعداد MediaPlayer.

إن تحسين زمن الانتقال مع الحفاظ على القدرة على إعادة استخدام اللاعبين أمر صعب. لحسن الحظ ، بالنسبة لأنواع معينة من الأصوات والتطبيقات التي تتطلب زمن انتقال منخفض ، هناك خيار آخر سنبحثه في المرة القادمة: SoundPool.