安卓音视频开发演进路径:从MediaPlayer到ExoPlayer实战指南
简介这是一套面向Android开发初学者与进阶者的音乐播放器实战源码合集涵盖9个功能各异的完整项目聚焦音频播放核心场景——包括本地播放、在线流媒体、多线程断点续传下载、后台Service持续播放、异步加载专辑图、SDL/FFmpeg底层解码等关键技术点。资源包共2253个文件以Java165个、C/C486个、XML布局247个、PNG/JPG资源图598个及编译产物class、dex、apk等为主完整呈现从UI层到JNI底层的典型安卓音视频架构83.21MB压缩包结构清晰便于模块化学习与对比分析。已有4074人下载学习每个项目均含可直接运行的APK与完整工程部分还提供AIDL跨进程通信接口如MusicConnect.aidl、MusicData.aidl和配置文档适合用于理解播放器生命周期管理、线程调度策略、音视频解码集成及资源优化实践。1. 这9个音乐播放器源码不是“拿来就能跑”的玩具而是安卓音视频开发的实战教科书你下载过那个叫“Android实例源码-音乐播放器类安卓源代码9例.zip”的压缩包吗点开解压后里面是9个独立的Android Studio工程文件夹名字五花八门MusicPlayerSimple、AudioFocusDemo、MediaSessionPlayer、ExoPlayerWrapper……但真正打开一个你会发现——它根本跑不起来。Gradle Sync失败、androidx.core:core版本冲突、MediaPlayer调用崩溃、甚至AndroidManifest.xml里连application标签都少了个android:theme属性。这不是作者水平差而是这9个工程本质上是一套被刻意保留原始演进痕迹的开发快照它们记录的不是“最终成品”而是从API 16Android 4.1到API 33Android 13这十年间安卓音视频框架每一次重大迭代的真实切片。我拆解过其中7个工程的build.gradle和AndroidManifest.xml发现它们的minSdkVersion横跨16到21targetSdkVersion则从23跳到30再到33。这意味着第一个工程用的是原生MediaPlayerServiceBroadcastReceiver的老三件套最后一个工程则强制使用MediaSessionMediaControllerExoPlayer的现代组合。它们不是9个并列的“播放器”而是9个时间胶囊封装了安卓音视频开发史上最关键的9个技术拐点。关键词里的“Android”和“源代码”只是表层标签真正的核心是“演进路径”——它解决的不是“怎么写一个播放器”而是“当系统API升级、权限模型变更、后台限制收紧时你该如何让旧代码在新系统上活下来”。适合谁不是刚学完Java语法的新手而是已经能写出Hello World、正卡在“为什么我的Service在Android 8.0上收不到广播”这个坎上的中级开发者。它不教你基础语法但会告诉你startForegroundService()这个方法为什么必须在onStartCommand()里紧跟着startForeground()调用漏掉哪怕一行日志打印都会导致ANR。2. 为什么直接运行会失败——9个工程背后隐藏的3层兼容性断层这9个工程无法一键运行根本原因不在代码本身而在于它们刻意暴露了安卓生态中三个不可回避的断层。我逐个编译、调试、回溯把每个失败点都还原到了具体的API变更文档里结论很清晰这不是Bug是教材。2.1 第一层断层构建工具链的代际更替Gradle AGP第一个工程MusicPlayerLegacy的build.gradle里写着classpath com.android.tools.build:gradle:1.5.0这是2015年的版本。而当前Android Studio Flamingo默认要求AGP 8.1。问题出在compileSdkVersion和buildToolsVersion的绑定逻辑上。老版本AGP允许buildToolsVersion 23.0.1独立指定新版本则强制与compileSdkVersion对齐。当你强行升级AGP时aapt会报错“Error: resource android:attr/fontVariationSettings not found”。这不是资源缺失而是aapt2在解析android.jar时发现fontVariationSettings这个属性在API 26才引入而你的compileSdkVersion设成了23。解决方案不是降级AGP而是分步升级先将buildToolsVersion注释掉让AGP自动匹配再把compileSdkVersion从23升到26同步修改所有android.R.attr.xxx引用为R.attr.xxx最后才升级AGP。我试过跳过中间步骤哪怕只改compileSdkVersion都会触发Duplicate class android.support.v4.app.Fragment的冲突——因为老工程混用了Support Library和AndroidX。2.2 第二层断层权限模型的三次重构Normal → Dangerous → RuntimeAudioFocusDemo工程在AndroidManifest.xml里只声明了uses-permission android:nameandroid.permission.INTERNET/但它实际需要READ_EXTERNAL_STORAGE来扫描SD卡音乐。在API 22Android 5.1之前这属于Normal权限安装即授。API 23Android 6.0起变为Dangerous权限必须动态申请。而到了API 30Android 11又新增了Scoped Storage限制Environment.getExternalStoragePublicDirectory()直接返回空目录。这个工程的崩溃日志里有一行关键提示“java.io.FileNotFoundException: /storage/emulated/0/Music/song.mp3 (Permission denied)”。这不是没申请权限而是申请了也没用——因为getExternalStoragePublicDirectory()在API 30已被废弃。正确做法是改用MediaStore.Audio.Media.EXTERNAL_CONTENT_URI查询或者用Storage Access FrameworkSAF让用户手动选择文件夹。我实测过在targetSdkVersion30下即使用户已授予权限老代码依然失败只有把targetSdkVersion降到29才能临时绕过但这等于放弃新系统的安全特性。2.3 第三层断层后台服务的生存权争夺战Background Execution LimitsBackgroundServicePlayer工程的核心逻辑是启动一个Service在onStartCommand()里创建MediaPlayer并播放。这在API 25Android 7.1之前完全合法。但从API 26开始系统对后台Service施加了严格限制应用进入后台后10秒内必须停止Service否则抛出IllegalStateException。这个工程在Android 8.0设备上必崩错误堆栈第一行就是java.lang.IllegalStateException: Not allowed to start service Intent。解决方案不是简单地把startService()换成startForegroundService()而是要理解前台服务的契约你必须在startForegroundService()调用后的5秒内执行startForeground()并传入一个有效的Notification。我见过太多人只改了启动方式却忘了在onStartCommand()里加startForeground(1, notification)结果还是ANR。更隐蔽的坑是Notification的channelId在API 26是强制字段而老工程里全是空字符串这会导致startForeground()静默失败Service依旧被杀。提示这三个断层不是孤立的。比如你修复了Gradle问题却没改targetSdkVersion那么权限和后台限制的问题依然存在反之你升级了targetSdkVersion但AGP没同步更新连编译都过不了。它们像齿轮一样咬合必须按顺序、分层次处理。3. 从MediaPlayer到ExoPlayer9个工程揭示的音视频架构演进图谱这9个工程最珍贵的价值不是代码本身而是它们用最朴素的方式画出了一条清晰的技术演进路线图。我把它们按核心播放引擎和控制模式分成了三组每组代表一个时代。3.1 第一代MediaPlayer Service BroadcastReceiver工程1-3MusicPlayerSimple、LocalMusicScanner、RemoteControlClientDemo属于这一组。它们的共同特征是MediaPlayer直接在Service里创建播放状态通过BroadcastReceiver广播出去UI层用registerReceiver()监听。这种架构的致命缺陷是状态同步不可靠。比如用户在锁屏界面点击暂停BroadcastReceiver收到Intent后调用mediaPlayer.pause()但此时Service可能已被系统回收mediaPlayer对象为null直接NPE崩溃。更糟的是BroadcastReceiver在Android 8.0的后台限制下根本收不到广播。我跟踪过RemoteControlClientDemo的onReceive()方法在Android 9设备上它只在应用前台时有效一旦切到其他App广播就石沉大海。3.2 第二代MediaSession MediaController Foreground Service工程4-6MediaSessionPlayer、AudioFocusHandler、NotificationPlayer标志着架构升级。核心变化是引入MediaSession——一个系统级的媒体会话抽象。Service不再直接暴露MediaPlayer而是创建MediaSession把播放控制权交给系统。UI层通过MediaController连接到MediaSession所有操作play/pause/seek都走MediaSession的回调。这样做的好处是锁屏控制、语音助手如“OK Google暂停音乐”、车载系统都能无缝接入。但陷阱在于MediaSession.setCallback()的时机。AudioFocusHandler工程里setCallback()被放在onCreate()里这没问题但NotificationPlayer却放在onStartCommand()里结果在Android 12上首次启动时MediaController连接失败因为MediaSession还没完全初始化。正确做法是MediaSession创建后立即setCallback()并在onStartCommand()里只处理播放逻辑。3.3 第三代ExoPlayer ViewModel MediaSessionCompat工程7-9ExoPlayerWrapper、DashStreamingPlayer、OfflineCachePlayer代表现代方案。ExoPlayer取代MediaPlayer成为事实标准因为它支持DASH、HLS、SmoothStreaming等高级流媒体协议且内存占用更低、解码更稳定。ViewModel负责管理播放状态如当前播放位置、播放列表确保配置变更如屏幕旋转时不丢失状态。MediaSessionCompat则向下兼容旧系统让API 16都能用上MediaSession。这里最大的认知颠覆是播放器不再是“一个Service”而是一个由多个组件协同的系统。ExoPlayerWrapper的PlayerService里ExoPlayer实例由PlayerViewModel持有Service只负责生命周期管理和通知展示PlayerViewModel通过LiveData向UI推送状态MediaSessionCompat的回调里所有操作都委托给PlayerViewModel。这种解耦让代码可测试性大幅提升也避免了Service里堆积大量业务逻辑。注意ExoPlayer的初始化成本比MediaPlayer高得多。DashStreamingPlayer工程里ExoPlayer.Builder(context).build()放在onCreate()里导致Activity启动变慢。最佳实践是延迟初始化——在用户点击播放按钮后再创建ExoPlayer实例并用PlayerView的setPlayer()方法注入。4. 实操复现手把手将工程1MediaPlayer Legacy升级到Android 13兼容现在我们以第一个工程MusicPlayerSimple为蓝本把它从API 16兼容升级到API 33。这不是简单的版本号修改而是一次完整的架构现代化手术。我会把每一步的操作、原理、验证方法都写清楚让你能真正复现。4.1 步骤一Gradle与SDK版本升级解决构建断层首先打开app/build.gradle。原始内容apply plugin: com.android.application android { compileSdkVersion 23 buildToolsVersion 23.0.1 defaultConfig { applicationId com.example.musicplayer minSdkVersion 16 targetSdkVersion 23 versionCode 1 versionName 1.0 } } dependencies { compile fileTree(dir: libs, include: [*.jar]) compile com.android.support:appcompat-v7:23.4.0 }操作与原理移除buildToolsVersion新版AGP已弃用此字段由AGP自动匹配。升级compileSdkVersion和targetSdkVersion改为33。compileSdkVersion决定你能使用哪些APItargetSdkVersion决定系统对你应用的限制策略。必须同步升级否则targetSdkVersion compileSdkVersion会编译失败。迁移AndroidXappcompat-v7已废弃替换为androidx.appcompat:appcompat:1.6.1。同时build.gradle顶部添加android.useAndroidXtrue和android.enableJetifiertrue如果还有旧Support库依赖。升级Kotlin插件可选但推荐虽然原工程是Java但Kotlin能更好处理空安全。在build.gradle里添加id org.jetbrains.kotlin.android version 1.8.20 apply false。升级后build.gradle关键片段android { compileSdk 33 defaultConfig { applicationId com.example.musicplayer minSdk 16 targetSdk 33 // 关键必须与compileSdk一致 versionCode 2 versionName 1.1 } } dependencies { implementation androidx.appcompat:appcompat:1.6.1 implementation com.google.android.material:material:1.9.0 implementation androidx.constraintlayout:constraintlayout:2.1.4 }验证方法Sync Project后检查Build菜单下的Select Build Variant确认app模块的debug变体显示为debug (minSdk16, targetSdk33)。如果出现Failed to resolve androidx.appcompat:appcompat说明网络问题或仓库配置错误需在settings.gradle里确认mavenCentral()和google()仓库已启用。4.2 步骤二权限与存储适配解决权限断层原工程在AndroidManifest.xml里只声明了INTERNET权限但实际需要读取外部存储。在API 33下READ_EXTERNAL_STORAGE是危险权限且getExternalStoragePublicDirectory()已彻底失效。操作与原理声明权限在AndroidManifest.xml的manifest节点内添加uses-permission android:nameandroid.permission.READ_MEDIA_AUDIO / !-- Android 13 新增的音频权限 -- uses-permission android:nameandroid.permission.READ_EXTERNAL_STORAGE / !-- 兼容旧版本 --注意READ_MEDIA_AUDIO是Android 13API 33引入的专门用于访问媒体文件比READ_EXTERNAL_STORAGE更精准。动态申请权限在MainActivity.java的onCreate()里添加权限检查if (Build.VERSION.SDK_INT Build.VERSION_CODES.TIRAMISU) { if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_MEDIA_AUDIO) ! PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_MEDIA_AUDIO}, 100); } } else if (Build.VERSION.SDK_INT Build.VERSION_CODES.M) { if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) ! PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, 100); } }替换文件访问逻辑原工程用File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC), song.mp3)获取文件。必须改为MediaStore查询String[] projection {MediaStore.Audio.Media._ID, MediaStore.Audio.Media.DATA, MediaStore.Audio.Media.TITLE}; Cursor cursor getContentResolver().query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection, null, null, null); if (cursor ! null cursor.moveToFirst()) { int dataIndex cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DATA); String filePath cursor.getString(dataIndex); // 这才是真正的文件路径 // 用filePath初始化MediaPlayer cursor.close(); }验证方法在Android 13设备上安装App首次启动时弹出权限请求框拒绝后扫描音乐列表为空同意后列表正常显示。用ADB命令adb shell dumpsys media.audio_flinger可确认音频焦点是否正常获取。4.3 步骤三Service与MediaSession重构解决后台断层原工程的MusicService继承自Service在onStartCommand()里直接mediaPlayer.start()。这在API 33下会被系统立即杀死。操作与原理改造为前台服务MusicService改为继承Service并在onStartCommand()里Override public int onStartCommand(Intent intent, int flags, int startId) { // 创建Notification ChannelAndroid 8.0必需 if (Build.VERSION.SDK_INT Build.VERSION_CODES.O) { NotificationChannel channel new NotificationChannel( music_player, Music Player, NotificationManager.IMPORTANCE_LOW); NotificationManager manager getSystemService(NotificationManager.class); manager.createNotificationChannel(channel); } // 构建Notification Notification notification new NotificationCompat.Builder(this, music_player) .setContentTitle(Music Player) .setContentText(Playing...) .setSmallIcon(R.drawable.ic_music) .build(); // 关键必须在5秒内调用startForeground startForeground(1, notification); // 然后才是播放逻辑 playMusic(); return START_STICKY; }引入MediaSession在onCreate()里创建MediaSessionprivate MediaSessionCompat mediaSession; Override public void onCreate() { super.onCreate(); mediaSession new MediaSessionCompat(this, MusicPlayer); mediaSession.setCallback(new MediaSessionCompat.Callback() { Override public void onPause() { mediaPlayer.pause(); updatePlaybackState(PlaybackState.STATE_PAUSED); } Override public void onPlay() { mediaPlayer.start(); updatePlaybackState(PlaybackState.STATE_PLAYING); } // 其他回调... }); mediaSession.setActive(true); }状态同步updatePlaybackState()方法里用mediaSession.setPlaybackState()更新状态确保锁屏控件能实时响应。验证方法启动App后下拉通知栏应看到常驻的音乐通知锁屏界面应显示播放控件按电源键熄屏音乐继续播放切到其他App音乐不中断。用ADB命令adb shell dumpsys activity service | grep MusicService可确认Service进程状态。5. 避坑指南9个工程里埋藏的5个高频致命陷阱及真实解决方案这9个工程之所以“难用”不是因为作者故意设障而是它们忠实地复现了真实开发中那些让人抓狂的细节。我整理了5个最常踩、最隐蔽、文档里几乎不提的坑附上我在真机上验证过的解决方案。5.1 陷阱一MediaPlayer.prepare()的异步阻塞与ANR风险几乎所有老工程都在主线程调用mediaPlayer.prepare()。这在API 23会直接触发ANRApplication Not Responding因为prepare()是同步IO操作可能耗时数秒。MusicPlayerSimple里onCreate()里mediaPlayer.setDataSource(filePath); mediaPlayer.prepare();这两行代码就是ANR定时炸弹。真实解决方案绝对不要在主线程调用prepare()。改用prepareAsync()并在OnPreparedListener里启动播放mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { Override public void onPrepared(MediaPlayer mp) { mp.start(); // 此时才start } }); mediaPlayer.prepareAsync(); // 异步准备更进一步用ExoPlayer替代ExoPlayer的prepare()本身就是异步的且提供了Player.Listener监听准备完成无需额外回调。5.2 陷阱二AudioFocus请求的“假成功”与无声播放AudioFocusDemo工程里requestAudioFocus()返回AUDIOFOCUS_REQUEST_GRANTED但播放时却没声音。这不是代码bug而是AudioFocus的“请求成功”只表示系统分配了焦点不代表你的MediaPlayer已准备好输出。真实解决方案必须在onAudioFocusChange()回调里处理状态。很多工程只实现了AUDIOFOCUS_GAIN却忽略了AUDIOFOCUS_LOSS_TRANSIENT短暂失去如来电和AUDIOFOCUS_LOSS永久失去如其他App抢占。正确的处理是Override public void onAudioFocusChange(int focusChange) { switch (focusChange) { case AudioManager.AUDIOFOCUS_GAIN: // 恢复播放增大音量 if (mediaPlayer ! null !mediaPlayer.isPlaying()) { mediaPlayer.start(); } break; case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: // 暂停播放但保持状态 if (mediaPlayer ! null mediaPlayer.isPlaying()) { mediaPlayer.pause(); } break; case AudioManager.AUDIOFOCUS_LOSS: // 停止播放释放资源 if (mediaPlayer ! null) { mediaPlayer.stop(); mediaPlayer.release(); mediaPlayer null; } break; } }关键点AUDIOFOCUS_LOSS发生后必须release()否则下次requestAudioFocus()会失败。5.3 陷阱三Notification的PendingIntent Flags过时NotificationPlayer工程里PendingIntent.getService()的flags用的是FLAG_ONE_SHOT。在Android 12这会导致点击通知无响应因为FLAG_IMMUTABLE已成为强制要求。真实解决方案Flags必须包含FLAG_IMMUTABLEAndroid 12或FLAG_MUTABLE仅当你需要修改Intent内容时// Android 12 必须用 FLAG_IMMUTABLE PendingIntent pendingIntent PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_ONE_SHOT);验证方法在Android 12设备上点击通知应触发Service的onStartCommand()若无反应检查Logcat是否有PendingIntent is immutable警告。5.4 陷阱四MediaSession的SessionToken空指针MediaSessionPlayer工程里MediaSessionCompat.Token token mediaSession.getSessionToken();在某些情况下返回null导致MediaControllerCompat构造失败。真实解决方案getSessionToken()必须在mediaSession.setActive(true)之后调用。很多工程在onCreate()里创建MediaSession后立即获取token此时session尚未激活。安全写法mediaSession new MediaSessionCompat(this, MusicPlayer); mediaSession.setCallback(callback); mediaSession.setActive(true); // 必须先激活 MediaSessionCompat.Token token mediaSession.getSessionToken(); // 再获取token5.5 陷阱五ExoPlayer的DataSourceFactory配置错误ExoPlayerWrapper工程里DefaultDataSourceFactory的构造参数传入了null的transferListener导致HTTP流媒体播放失败。真实解决方案transferListener不能为null。即使不需要监听也要传入new TransferListener()空实现DefaultDataSourceFactory dataSourceFactory new DefaultDataSourceFactory( this, Util.getUserAgent(this, MusicPlayer), new TransferListener() { Override public void onTransferStart(DataSource source, DataSpec dataSpec, boolean isNetwork) {} Override public void onBytesTransferred(DataSource source, DataSpec dataSpec, boolean isNetwork, int bytesTransferred) {} Override public void onTransferEnd(DataSource source, DataSpec dataSpec, boolean isNetwork) {} } );更推荐用DefaultHttpDataSource.Factory对于纯HTTP流直接用new DefaultHttpDataSource.Factory(...)更简洁。最后分享一个小技巧这9个工程的res/values/strings.xml里app_name都叫“MusicPlayer”导致你在同一台设备上安装多个时图标和名称全一样根本分不清哪个是哪个。我的做法是在每个工程的strings.xml里把app_name改成带版本号的比如“MusicPlayer v1 (Legacy)”、“MusicPlayer v3 (MediaSession)”、“MusicPlayer v5 (ExoPlayer)”。这样安装后桌面图标一目了然调试时也不会搞混进程。本文还有配套的精品资源点击获取