Android Spotify克隆应用MusicService创建失败:IllegalArgumentException排查
Android Spotify克隆应用MusicService崩溃问题排查与解决
开发Android平台Spotify克隆应用时,实现MusicService组件后运行崩溃,抛出java.lang.RuntimeException: Unable to create service com.example.spotifyclone.exoplayer.MusicService: java.lang.IllegalArgumentException,异常栈指向PlayerNotificationManager.Builder初始化环节,已尝试重建项目、清理缓存重启等操作无效,以下是完整异常信息、相关代码及解决方案。
异常信息
java.lang.RuntimeException: Unable to create service com.example.spotifyclone.exoplayer.MusicService: java.lang.IllegalArgumentException at android.app.ActivityThread.handleCreateService(ActivityThread.java:3610) at android.app.ActivityThread.access$1500(ActivityThread.java:206) at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1716) at android.os.Handler.dispatchMessage(Handler.java:106) at android.os.Looper.loop(Looper.java:201) at android.app.ActivityThread.main(ActivityThread.java:6820) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:547) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:922) Caused by: java.lang.IllegalArgumentException at com.google.android.exoplayer2.util.Assertions.checkArgument(Assertions.java:39) at com.google.android.exoplayer2.ui.PlayerNotificationManager$Builder.<init>(PlayerNotificationManager.java:353) at com.example.spotifyclone.exoplayer.MusicNotificationManager.<init>(MusicNotificationManager.kt:33) at com.example.spotifyclone.exoplayer.MusicService.onCreate(MusicService.kt:71) at android.app.ActivityThread.handleCreateService(ActivityThread.java:3598)
相关代码
MusicService.kt代码
import android.app.PendingIntent import android.content.Intent import android.os.Bundle import android.support.v4.media.MediaBrowserCompat import android.support.v4.media.MediaDescriptionCompat import android.support.v4.media.MediaMetadataCompat import android.support.v4.media.session.MediaSessionCompat import androidx.media.MediaBrowserServiceCompat import com.example.spotifyclone.exoplayer.callbacks.MusicPlaybackPreparer import com.example.spotifyclone.exoplayer.callbacks.MusicPlayerEventListener import com.example.spotifyclone.exoplayer.callbacks.MusicPlayerNotificationListener import com.example.spotifyclone.other.Constants.MEDIA_ROOT_ID import com.example.spotifyclone.other.Constants.NETWORK_ERROR import com.google.android.exoplayer2.ExoPlayer import com.google.android.exoplayer2.Player import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector import com.google.android.exoplayer2.ext.mediasession.TimelineQueueNavigator import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.* import javax.inject.Inject private const val SERVICE_TAG = "MusicService" @AndroidEntryPoint class MusicService : MediaBrowserServiceCompat() { @Inject lateinit var dataSourceFactory: DefaultDataSourceFactory @Inject lateinit var exoPlayer: ExoPlayer @Inject lateinit var firebaseMusicSource: FirebaseMusicSource private val serviceJob = Job() private val serviceScope = CoroutineScope(Dispatchers.Main + serviceJob) private lateinit var mediaSession: MediaSessionCompat private lateinit var mediaSessionConnector: MediaSessionConnector var isForegroundService = false private var curPlayingSong: MediaMetadataCompat? = null private lateinit var musicNotificationManager: MusicNotificationManager private var isPlayerInitialized = false private lateinit var musicPlayerEventListener: MusicPlayerEventListener companion object { var curSongDuration = 0L private set } override fun onCreate() { super.onCreate() serviceScope.launch { firebaseMusicSource.fetchMediaData() } val activityIntent = packageManager?.getLaunchIntentForPackage(packageName)?.let { PendingIntent.getActivity(this, 0, it, 0) } mediaSession = MediaSessionCompat(this, SERVICE_TAG).apply { setSessionActivity(activityIntent) isActive = true } sessionToken = mediaSession.sessionToken musicNotificationManager = MusicNotificationManager( this, mediaSession.sessionToken, MusicPlayerNotificationListener(this) ) { curSongDuration = exoPlayer.duration } val musicPlaybackPreparer = MusicPlaybackPreparer(firebaseMusicSource) { curPlayingSong = it preparePlayer( firebaseMusicSource.songs, it, true ) } mediaSessionConnector = MediaSessionConnector(mediaSession) mediaSessionConnector.setPlaybackPreparer(musicPlaybackPreparer) mediaSessionConnector.setQueueNavigator(MusicQueueNavigator()) mediaSessionConnector.setPlayer(exoPlayer) musicPlayerEventListener = MusicPlayerEventListener(this) exoPlayer.addListener(musicPlayerEventListener) musicNotificationManager.showNotification(exoPlayer) } private inner class MusicQueueNavigator : TimelineQueueNavigator(mediaSession) { override fun getMediaDescription(player: Player, windowIndex: Int): MediaDescriptionCompat { return firebaseMusicSource.songs[windowIndex].description } } private fun preparePlayer( songs: List<MediaMetadataCompat>, itemToPlay: MediaMetadataCompat?, playNow: Boolean ) { val curSongIndex = if (curPlayingSong == null) 0 else songs.indexOf(itemToPlay) exoPlayer.setMediaSource(firebaseMusicSource.asMediaSource(dataSourceFactory)) exoPlayer.prepare() exoPlayer.seekTo(curSongIndex, 0L) exoPlayer.playWhenReady = playNow } override fun onTaskRemoved(rootIntent: Intent?) { super.onTaskRemoved(rootIntent) exoPlayer.stop() } override fun onDestroy() { super.onDestroy() serviceScope.cancel() exoPlayer.removeListener(musicPlayerEventListener) exoPlayer.release() } override fun onGetRoot( clientPackageName: String, clientUid: Int, rootHints: Bundle? ): BrowserRoot? { return BrowserRoot(MEDIA_ROOT_ID, null) } override fun onLoadChildren( parentId: String, result: Result<MutableList<MediaBrowserCompat.MediaItem>> ) { when (parentId) { MEDIA_ROOT_ID -> { val resultsSent = firebaseMusicSource.whenReady { isInitialized -> if (isPlayerInitialized) { result.sendResult(firebaseMusicSource.asMediaItems()) if (!isInitialized && firebaseMusicSource.songs.isNotEmpty()) { preparePlayer( firebaseMusicSource.songs, firebaseMusicSource.songs[0], false ) isPlayerInitialized = true } } else { mediaSession.sendSessionEvent(NETWORK_ERROR, null) result.sendResult(null) } } if (!resultsSent) { result.detach() } } } } }
AndroidManifest.xml代码
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" package="com.example.spotifyclone" <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <application android:name=".SpotifyApplication" android:allowBackup="true" android:dataExtractionRules="@xml/data_extraction_rules" android:fullBackupContent="@xml/backup_rules" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.SpotifyClone" tools:targetApi="31"> <service android:name="com.example.spotifyclone.exoplayer.MusicService" android:exported="false"> <intent-filter> <action android:name="android.media.browse.MediaBrowserService" /> </intent-filter> </service> <activity android:name=".ui.MainActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
MusicNotificationManager.kt代码
@SuppressLint("Range") class MusicNotificationManager( private val context:Context, sessionToken:MediaSessionCompat.Token, notificationListener:PlayerNotificationManager.NotificationListener, private val newSongCallback:() -> Unit ) { private val notificationManager: PlayerNotificationManager init { val mediaController = MediaControllerCompat(context, sessionToken) notificationManager = PlayerNotificationManager.Builder( context, NOTIFICATION_ID, NOTIFICATION_CHANNEL_ID ) .setChannelNameResourceId(R.string.notification_channel_name) .setChannelDescriptionResourceId(R.string.notification_channel_description) .setMediaDescriptionAdapter(DescriptionAdapter(mediaController)) .setNotificationListener(notificationListener) .setSmallIconResourceId(R.drawable.ic_music) .build() } fun showNotification(player: Player) { notificationManager.setPlayer(player) } private inner class DescriptionAdapter( private val mediaController: MediaControllerCompat ) : PlayerNotificationManager.MediaDescriptionAdapter { override fun getCurrentContentTitle(player: Player): CharSequence { return mediaController.metadata.description.title.toString() } override fun createCurrentContentIntent(player: Player): PendingIntent? { return mediaController.sessionActivity } override fun getCurrentContentText(player: Player): CharSequence? { return mediaController.metadata.description.subtitle.toString() } override fun getCurrentLargeIcon( player: Player, callback: PlayerNotificationManager.BitmapCallback ): Bitmap? { Glide.with(context).asBitmap() .load(mediaController.metadata.description.iconUri) .into(object : CustomTarget<Bitmap>() { override fun onResourceReady( resource: Bitmap, transition: Transition<in Bitmap>? ) { callback.onBitmap(resource) } override fun onLoadCleared(placeholder: Drawable?) = Unit }) return null } } }
解决方案
问题根源
异常栈显示PlayerNotificationManager.Builder初始化时触发参数校验失败,核心原因是MusicNotificationManager.kt中使用的NOTIFICATION_ID和NOTIFICATION_CHANNEL_ID未定义,导致Builder接收到非法参数(默认值为0或空字符串),违反ExoPlayer的断言规则(通知ID必须大于0,渠道ID不能为空)。
修复步骤
- 定义合法的通知常量
在MusicNotificationManager.kt顶部添加常量定义:
private const val NOTIFICATION_ID = 101 // 必须大于0的整数 private const val NOTIFICATION_CHANNEL_ID = "music_playback_channel" // 非空字符串
- 确保通知渠道资源存在
在res/values/strings.xml中添加通知渠道的名称和描述字符串:
<string name="notification_channel_name">音乐播放</string> <string name="notification_channel_description">后台音乐播放专用通知渠道</string>
- 适配高版本PendingIntent Flag
在MusicService.kt中创建Activity PendingIntent时,替换flag为适配Android 12+的常量:
val activityIntent = packageManager?.getLaunchIntentForPackage(packageName)?.let { PendingIntent.getActivity(this, 0, it, PendingIntent.FLAG_IMMUTABLE) }
修复说明
- 未定义的常量会导致Builder接收到非法参数,触发ExoPlayer的
Assertions.checkArgument校验失败,直接抛出异常。 - 提前添加通知渠道资源,避免后续创建通知渠道时因资源缺失导致崩溃。
- 适配PendingIntent Flag是Android 12及以上系统的强制要求,避免潜在的兼容性问题。
内容的提问来源于stack exchange,提问作者Edgar
相关产品推荐
相关产品推荐

