☰
Android原生MediaPlayer真机适配避坑指南
2026/10/15 1:28:16 网站建设 项目流程

简介:本资源是一个面向Android初学者与移动开发入门者的简易音乐播放器完整项目,聚焦音频播放核心功能实现与典型UI交互设计,帮助开发者系统掌握MediaPlayer控制、本地音乐扫描、列表展示、后台服务及通知栏控制等关键技能。压缩包共68个文件,包含20个编译后class文件、14个XML布局与配置文件(如Activity界面、Manifest声明)、13个PNG图标资源、8个Java源码文件(含主逻辑与适配器),以及APK安装包、支持库jar、ProGuard混淆配置等,整体3.22MB,结构清晰,便于逐层理解工程组织与运行机制。已有263人学习下载,项目可直接导入Android Studio编译运行,附带完整目录结构与基础权限配置,涵盖从文件读取、元数据解析到SeekBar进度同步、播放状态持久化的全流程实践,是Android多媒体开发的典型入门范例。

1. 为什么一个“简易”Android播放器,反而最容易在真机上集体翻车?

你写了个只有三四个Activity、调用MediaPlayer几行代码的Android简易播放器,本地模拟器跑得飞起,一上真机——音画不同步、横竖屏切崩、后台被杀、耳机拔插无声、甚至点一次播放按钮触发两次回调……这不是玄学,是Android多媒体生命周期和硬件抽象层(HAL)在真实设备上的「条件反射」。这个标题不是教你怎么堆功能,而是直击一线开发者最常忽略的「最小可行播放器」落地红线:它必须扛住小米/华为/OPPO等厂商定制ROM对AudioFlinger的调度干预、绕过MediaCodec硬解码的初始化黑匣子、兼容Android 8.0到14的Surface生命周期变更。适合刚从Java/Kotlin基础转进多媒体开发的新人,也适合被客户一句“就播个MP3”拖进坑里、结果发现连状态同步都写不稳的熟手。本文不讲ExoPlayer源码,只用原生MediaPlayer+SurfaceView+Handler组合,在200行核心代码内,跑通从文件路径输入到全屏播放、后台保活、异常恢复的完整链路。


2. 用 MediaPlayer + SurfaceView 搭建最小可运行播放器:从初始化到首帧渲染

2.1 创建播放器Activity并声明必要权限与配置

Android 9(API 28)起,默认禁止明文HTTP请求;Android 10(API 29)起,外部存储访问需适配Scoped Storage;而播放器最基础的音频焦点、前台服务、后台执行限制,全靠Manifest精准控制。以下是最小但不可删减的配置:

<!-- AndroidManifest.xml --> <application android:usesCleartextTraffic="true" android:requestLegacyExternalStorage="true"> <activity android:name=".PlayerActivity" android:exported="true" android:configChanges="orientation|screenSize|keyboardHidden" android:launchMode="singleTop" android:screenOrientation="sensor" android:theme="@style/Theme.AppCompat.NoActionBar" /> <!-- 后台播放需前台服务 --> <service android:name=".PlaybackService" android:enabled="true" android:exported="false" /> </application> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.WAKE_LOCK" />

提示:android:configChanges="orientation|screenSize|keyboardHidden"是横竖屏不重启Activity的关键,否则Surface重建会直接中断播放;singleTop避免重复启动Activity导致MediaPlayer实例冲突。

2.2 初始化SurfaceView并绑定MediaPlayer生命周期

SurfaceView不是普通View,它的Surface由独立线程管理,必须在Surface创建后才可设置MediaPlayer的显示目标。常见错误是setContentView()后立刻setDataSource()——此时Surface尚未就绪,必抛IllegalStateException。

// PlayerActivity.java public class PlayerActivity extends AppCompatActivity implements SurfaceHolder.Callback { private MediaPlayer mediaPlayer; private SurfaceView surfaceView; private SurfaceHolder surfaceHolder; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_player); surfaceView = findViewById(R.id.surface_view); surfaceHolder = surfaceView.getHolder(); surfaceHolder.addCallback(this); // 关键:监听Surface创建/销毁 initMediaPlayer(); } private void initMediaPlayer() { mediaPlayer = new MediaPlayer(); // 设置音频流类型,影响音量控制与焦点策略 mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); // 错误监听必须设,否则prepare失败静默崩溃 mediaPlayer.setOnErrorListener((mp, what, extra) -> { Log.e("Player", "MediaPlayer error: " + what + ", extra: " + extra); handlePlaybackError(); return true; }); } @Override public void surfaceCreated(SurfaceHolder holder) { // ✅ 此时Surface已就绪,可安全设置display if (mediaPlayer != null && holder.getSurface().isValid()) { try { mediaPlayer.setDisplay(holder.getSurface()); } catch (Exception e) { Log.w("Player", "setDisplay failed", e); } } } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { // Surface尺寸变化,如横竖屏切换,无需重设display } @Override public void surfaceDestroyed(SurfaceHolder holder) { // ✅ Surface销毁,必须释放MediaPlayer资源 if (mediaPlayer != null) { mediaPlayer.reset(); // 重置状态,避免下次prepare失败 } } }

逻辑说明:

  • SurfaceHolder.Callback是Surface生命周期钩子,surfaceCreated()是唯一安全调用setDisplay()的时机;
  • setAudioStreamType()必须在setDataSource()前调用,否则部分设备(如华为EMUI)会静音;
  • reset()在surfaceDestroyed()中调用,是防止Surface重建时MediaPlayer残留状态导致后续prepare失败的后悔药。

2.3 加载媒体源并触发首帧渲染:prepareAsync() 的正确打开方式

同步prepare()会阻塞主线程,卡死UI;但prepareAsync()引入异步回调竞争——若用户快速点击“播放”再点“暂停”,onPrepared()可能仍在执行,造成状态错乱。必须用状态机兜底:

private enum PlaybackState { IDLE, PREPARING, PREPARED, PLAYING, PAUSED, STOPPED, ERROR } private PlaybackState currentState = PlaybackState.IDLE; public void loadAndPlay(String filePath) { if (mediaPlayer == null) return; try { mediaPlayer.reset(); // 清理上一次状态 mediaPlayer.setDataSource(filePath); currentState = PlaybackState.PREPARING; // 异步准备,避免ANR mediaPlayer.prepareAsync(); mediaPlayer.setOnPreparedListener(mp -> { if (currentState == PlaybackState.PREPARING) { currentState = PlaybackState.PREPARED; startPlayback(); // 准备就绪后自动播放 } }); } catch (IOException e) { Log.e("Player", "Failed to set data source", e); currentState = PlaybackState.ERROR; handlePlaybackError(); } } private void startPlayback() { if (currentState == PlaybackState.PREPARED || currentState == PlaybackState.PAUSED) { mediaPlayer.start(); currentState = PlaybackState.PLAYING; updateUIForPlaying(); } }

参数说明:

  • prepareAsync()是硬性要求,尤其对网络流或大文件,否则主线程卡顿超5秒触发ANR;
  • onPreparedListener内部加currentState == PREPARING判断,是防用户快速操作导致回调“迟到”的关键守门员;
  • startPlayback()不直接调mediaPlayer.start(),而是先校验状态,避免IllegalStateException: Unable to start service。

3. 横竖屏切换与Surface重建:如何让画面不闪、不黑、不卡顿

3.1 理解SurfaceView在configChanges下的真实行为

当声明android:configChanges="orientation|screenSize"后,系统不再销毁重建Activity,但SurfaceView内部的Surface仍会被销毁并重建——这是Android图形栈底层机制,与Activity生命周期无关。很多开发者误以为“不重建Activity=Surface不重建”,结果横屏时画面一闪变黑,或出现绿屏残影。

根本原因:SurfaceView的Surface由SurfaceHolder管理,surfaceDestroyed()和surfaceCreated()仍会触发,只是Activity实例没换。因此,MediaPlayer必须在Surface重建后重新绑定display,且不能中断播放。

@Override public void onConfigurationChanged(@NonNull Configuration newConfig) { super.onConfigurationChanged(newConfig); // ✅ 不要在这里做任何MediaPlayer操作! // Surface重建由Callback自动处理,此处仅更新UI布局逻辑 updateLayoutForOrientation(newConfig.orientation); } private void updateLayoutForOrientation(int orientation) { if (orientation == Configuration.ORIENTATION_LANDSCAPE) { getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN); } else { getWindow().addFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN); getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); } }

注意:onConfigurationChanged()里绝对不要调用mediaPlayer.start()或pause()——此时Surface可能尚未创建,setDisplay()会失败,且播放状态应由Surface回调驱动,而非配置变更驱动。

3.2 Surface重建期间保持音频持续播放的实操方案

Surface重建(即surfaceDestroyed()→surfaceCreated())耗时约50~200ms,若不做处理,视频画面会黑屏,但音频通常不受影响(因AudioTrack未中断)。要实现“画面无缝续播”,需在Surface重建后立即恢复视频渲染:

@Override public void surfaceCreated(SurfaceHolder holder) { if (mediaPlayer == null) return; // 1. 若正在播放,记录当前位置(毫秒) long currentPosition = 0; if (currentState == PlaybackState.PLAYING || currentState == PlaybackState.PAUSED) { currentPosition = mediaPlayer.getCurrentPosition(); } // 2. 重设Surface显示目标 try { mediaPlayer.setDisplay(holder.getSurface()); } catch (Exception e) { Log.w("Player", "setDisplay failed on surfaceCreated", e); return; } // 3. 若之前在播放,seek回原位置并继续(避免跳帧) if (currentState == PlaybackState.PLAYING) { try { mediaPlayer.seekTo((int) currentPosition); mediaPlayer.start(); // resume playback } catch (Exception e) { Log.w("Player", "seekTo/start failed after surface recreate", e); } } else if (currentState == PlaybackState.PAUSED) { try { mediaPlayer.seekTo((int) currentPosition); } catch (Exception e) { Log.w("Player", "seekTo failed after surface recreate", e); } } }

关键点:

  • seekTo()必须在setDisplay()成功后调用,否则无效;
  • 即使只seek到当前毫秒数,也能强制MediaPlayer刷新视频帧缓冲区,消除黑屏;
  • 不要pause()再start(),直接seekTo()+start()更可靠,避免状态机二次扰动。

3.3 全屏模式下适配刘海屏与挖孔屏的Safe Area处理

Android 9+ 的WindowInsetsAPI 可获取屏幕安全区域,但SurfaceView默认铺满整个窗口,导致视频内容被刘海遮挡。需动态裁剪SurfaceView的显示区域:

// 在surfaceCreated()之后,或onCreate()中调用 private void applySafeAreaInsets() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { Window window = getWindow(); window.getInsetsController().setSystemBarsBehavior( WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE); } View decorView = getWindow().getDecorView(); decorView.setOnApplyWindowInsetsListener((v, insets) -> { // 获取状态栏、导航栏高度 int statusBarHeight = insets.getInsets(WindowInsets.Type.statusBars()).top; int navigationBarHeight = insets.getInsets(WindowInsets.Type.navigationBars()).bottom; // SurfaceView外层FrameLayout的padding用于避开刘海 FrameLayout container = findViewById(R.id.player_container); container.setPadding(0, statusBarHeight, 0, navigationBarHeight); return insets; }); }

提示:此方案不修改Surface本身,而是通过外层容器padding留出安全边距,确保UI控件不被遮挡;视频画面仍全屏渲染,仅UI层避让——这才是“全屏播放器”的合理定义。


4. 后台播放与系统资源抢占:如何让音乐在锁屏/切应用后继续响

4.1 为什么MediaPlayer在后台会被静音或杀死?

Android 8.0(Oreo)引入后台执行限制:应用进入后台后,10秒内若未启动前台服务,系统将终止其所有后台服务与线程。而MediaPlayer依赖的AudioTrack、MediaCodec等组件,一旦所属进程被冻结,音频输出立即中断。更隐蔽的是音频焦点(AudioFocus)机制:当微信语音、系统闹钟等申请焦点时,你的播放器若未正确监听并响应,会被系统强制降低音量甚至静音。

解决方案是双轨制:

  • 前台服务(Foreground Service):向系统声明“我正在做重要事”,换取后台运行白名单;
  • 音频焦点监听(AudioFocusRequest):主动让出/抢回焦点,避免被静音。

4.2 启动前台服务并绑定MediaPlayer实例

不能把MediaPlayer直接放Service里——它必须运行在有Looper的线程(通常是主线程),否则setDataSource()等方法抛IllegalStateException。正确做法是:Service仅作保活壳,通过Messenger或BroadcastReceiver与Activity通信,MediaPlayer仍驻留在Activity中。

// PlaybackService.java public class PlaybackService extends Service { private static final int NOTIFICATION_ID = 1001; private Notification notification; @Override public void onCreate() { super.onCreate(); createNotificationChannel(); buildNotification(); } @Override public int onStartCommand(Intent intent, int flags, int startId) { // ✅ 必须调用startForeground,否则Android 8+立即杀掉 startForeground(NOTIFICATION_ID, notification); return START_STICKY; // 进程被杀后尝试重启 } private void buildNotification() { Intent notificationIntent = new Intent(this, PlayerActivity.class); notificationIntent.setAction(Intent.ACTION_MAIN); notificationIntent.addCategory(Intent.CATEGORY_LAUNCHER); notificationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); PendingIntent pendingIntent = PendingIntent.getActivity( this, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE); notification = new NotificationCompat.Builder(this, "player_channel") .setContentTitle("播放中...") .setContentText("点击返回播放器") .setSmallIcon(R.drawable.ic_play) .setContentIntent(pendingIntent) .setOngoing(true) .build(); } @Override public IBinder onBind(Intent intent) { return null; // 本例不提供Binder,仅保活 } }

启动服务代码(在Activity中):

// PlayerActivity.java private void startForegroundService() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { startForegroundService(new Intent(this, PlaybackService.class)); } else { startService(new Intent(this, PlaybackService.class)); } }

注意:startForegroundService()必须在5秒内调用startForeground(),否则抛ForegroundServiceStartNotAllowedException;因此onCreate()中立即构建Notification是刚需。

4.3 主动申请与响应音频焦点:避免被微信/钉钉静音

private AudioFocusRequest audioFocusRequest; private AudioManager audioManager; private void requestAudioFocus() { audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { audioFocusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN) .setOnAudioFocusChangeListener(this::onAudioFocusChange) .setAcceptsDelayedFocusGain(true) .setWillPauseWhenDucked(false) // 被压低音量时不暂停 .build(); int result = audioManager.requestAudioFocus(audioFocusRequest); if (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { Log.w("Player", "Audio focus request denied"); } } else { // 旧版API兼容 audioManager.requestAudioFocus( this::onAudioFocusChange, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN); } } private void onAudioFocusChange(int focusChange) { switch (focusChange) { case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: // 短暂失去焦点(如来电提示音),暂停播放 if (currentState == PlaybackState.PLAYING) { mediaPlayer.pause(); currentState = PlaybackState.PAUSED; } break; case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: // 可被压低(如导航播报),降低音量而非暂停 mediaPlayer.setVolume(0.2f, 0.2f); break; case AudioManager.AUDIOFOCUS_LOSS: // 永久失去焦点(如其他App开始播放),停止并释放 stopPlayback(); break; case AudioManager.AUDIOFOCUS_GAIN: // 重新获得焦点,恢复音量与播放 mediaPlayer.setVolume(1.0f, 1.0f); if (currentState == PlaybackState.PAUSED) { mediaPlayer.start(); currentState = PlaybackState.PLAYING; } break; } }

血泪经验:

  • AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK下绝不调pause(),否则导航音结束用户还得手动点播放;
  • AUDIOFOCUS_LOSS时必须stopPlayback(),否则下次request焦点可能被拒绝;
  • setWillPauseWhenDucked(false)是Android 8+新API,明确告诉系统“我不愿被暂停”,提升用户体验。

5. 播放器避坑指南:5个真机必现、文档不写、StackOverflow高票问题

5.1 现象:小米/Redmi手机播放MP3时无声音,Logcat显示“W/MediaPlayer: info/warning (3, 0)”

  • 原因:MIUI深度定制AudioFlinger策略,对STREAM_MUSIC流类型做额外权限校验。若未在AndroidManifest.xml中声明<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />,部分机型静音。
  • 解决:补全权限声明,并在initMediaPlayer()中显式调用audioManager.setStreamVolume()预设音量:
    AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE); am.setStreamVolume(AudioManager.STREAM_MUSIC, am.getStreamMaxVolume(AudioManager.STREAM_MUSIC) / 2, 0);

5.2 现象:华为Mate系列横屏后画面拉伸变形,纵宽比严重失真

  • 原因:华为EMUI对SurfaceView的setFixedSize()有特殊处理,且SurfaceHolder.setFormat(PixelFormat.RGBA_8888)在部分固件下失效,导致GPU采样时按错误宽高比缩放。
  • 解决:放弃setFixedSize(),改用TextureView替代SurfaceView(仅限需要精确宽高比的场景);或强制在surfaceChanged()中重设Surface尺寸:
    @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { // 华为特供:强制重设为原始分辨率 if (Build.MANUFACTURER.equalsIgnoreCase("HUAWEI")) { holder.setFixedSize(width, height); // 重置为实际尺寸 } }

5.3 现象:Android 12+设备锁屏后播放中断,Log显示“E/AudioTrack: AudioFlinger could not create track, status: -12”

  • 原因:Android 12引入AudioAttributes强制要求,旧版setAudioStreamType()已被标记为deprecated,新系统拒绝创建AudioTrack。
  • 解决:升级MediaPlayer初始化逻辑,使用AudioAttributes:
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { AudioAttributes attrs = new AudioAttributes.Builder() .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) .setUsage(AudioAttributes.USAGE_MEDIA) .build(); mediaPlayer.setAudioAttributes(attrs); } else { mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); }

5.4 现象:OPPO/Realme手机快速连续点击“播放/暂停”,MediaPlayer抛IllegalStateException: start() called on uninitialized MediaPlayer

  • 原因:reset()后未等待setDataSource()完成,用户又点播放,start()在prepareAsync()前调用。
  • 解决:引入原子状态锁,startPlayback()前校验isPlayable():
    private boolean isPlayable() { return mediaPlayer != null && (currentState == PlaybackState.PREPARED || currentState == PlaybackState.PAUSED) && mediaPlayer.isPlaying() == false; }

5.5 现象:播放网络URL时,Android 9+报java.io.FileNotFoundException: Cleartext HTTP traffic to xxx not permitted

  • 原因:Android 9默认禁用明文HTTP,但部分内网测试服务器仍用HTTP。
  • 解决:两种方案二选一:
    (1)服务端升级HTTPS(推荐);
    (2)在res/xml/network_security_config.xml中白名单:
    <domain-config> <domain includeSubdomains="true">your-test-server.com</domain> <cleartextTrafficPermitted="true" /> </domain-config>
    并在Manifest中引用:android:networkSecurityConfig="@xml/network_security_config"

6. 验证播放器健壮性的5个必测场景与自动化脚本思路

6.1 构建可复现的真机验证清单(非模拟器)

别信模拟器——它不模拟AudioFlinger调度、不触发MIUI/EMUI省电策略、不模拟Surface重建延迟。以下5个场景,必须在至少3台不同品牌真机(覆盖华为、小米、OPPO)上逐条验证:

场景操作步骤期望结果失败典型表现
S1:冷启动播放安装APK → 点击图标 → 立即选择本地MP32秒内出声,无黑屏首帧延迟>5秒、播放按钮无响应、闪退
S2:横竖屏暴力切换播放中连续点击旋转按钮10次画面始终连续,无绿屏/黑屏每次切换必黑屏1秒、横屏后画面挤压
S3:锁屏保活播放中按电源键锁屏 → 等待30秒 → 解锁解锁后仍在播放,通知栏显示“播放中”锁屏瞬间静音、通知栏无图标、解锁后需手动点播放
S4:多任务抢占播放中切到微信 → 发一条语音 → 返回播放器语音结束后自动恢复播放微信语音期间被静音、返回后仍暂停、需手动点
S5:异常断网恢复播放网络流 → 关WiFi → 等10秒 → 开WiFi自动重连并续播,不卡死持续“缓冲中”、报错后无法再播放、ANR

提示:每台真机首次测试前,关闭“省电模式”、“后台冻结”、“自启管理”,否则所有测试均无效。

6.2 用ADB命令批量触发关键事件,替代手工点按

手工测试效率低且易漏。以下ADB命令可脚本化,集成进CI流程:

# 1. 触发横竖屏切换(循环10次) for i in {1..10}; do adb shell content insert --uri content://settings/system --bind name:s:accelerometer_rotation --bind value:i:0 sleep 0.5 adb shell content insert --uri content://settings/system --bind name:s:accelerometer_rotation --bind value:i:1 sleep 0.5 done # 2. 模拟锁屏/解锁 adb shell input keyevent KEYCODE_POWER sleep 2 adb shell input keyevent KEYCODE_POWER # 3. 强制停止并重启服务(验证前台服务保活) adb shell am force-stop com.example.player adb shell am startservice -n com.example.player/.PlaybackService

将上述命令存为test_scenario.sh,配合adb logcat | grep -i "player\|mediaplayer\|audiofocus"实时抓日志,可快速定位S3/S4类问题。

6.3 一个血泪换来的状态同步技巧:用HandlerThread隔离MediaPlayer回调

MediaPlayer的onPrepared()、onCompletion()等回调默认在主线程,但若你在回调里做耗时操作(如读取下一首元数据),会卡UI。而直接开子线程调mediaPlayer.start()又会抛IllegalStateException(因MediaPlayer非线程安全)。

我的解法:用HandlerThread创建专属消息循环,所有MediaPlayer操作(prepare、start、pause)都在该线程执行,但UI更新仍切回主线程:

private HandlerThread playerThread; private Handler playerHandler; private void initPlayerThread() { playerThread = new HandlerThread("MediaPlayerThread"); playerThread.start(); playerHandler = new Handler(playerThread.getLooper()); } // 所有MediaPlayer操作走此Handler private void postToPlayerThread(Runnable action) { playerHandler.post(action); } // 示例:安全prepare public void safePrepareAsync(String path) { postToPlayerThread(() -> { try { mediaPlayer.reset(); mediaPlayer.setDataSource(path); mediaPlayer.prepareAsync(); } catch (Exception e) { // post回主线程报错 runOnUiThread(() -> handlePlaybackError()); } }); }

这招让我躲过了80%的“回调里更新UI卡顿”和“子线程调MediaPlayer崩溃”问题。它不改变MediaPlayer线程模型,只是把危险操作封装进可控线程,既保安全又保响应。

最后说句实在话:所谓“简易播放器”,从来不是代码行数少,而是你敢不敢把它扔进华为Mate 60、小米14、OPPO Find X7的真实用户手里,扛住他们一边刷抖音一边切后台、一边充电一边横竖屏、一边接电话一边听播客的混合压力。我见过太多人花三天写完播放逻辑,却用三周调MIUI的音频焦点bug。希望这篇笔记里每一个surfaceCreated()的判断、每一行adb命令、每一个AudioFocusRequest的参数,都是你少踩的一个坑。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询