核心概念
- 下载: 从网络 URL 获取 MP3 文件数据,Android 提供了多种方式,如
HttpURLConnection(传统)、OkHttp(现代推荐)。 - 播放: 将下载到的 MP3 数据流或文件传递给 Android 的媒体播放器进行播放。
MediaPlayer是最核心的类。 - 缓存: 为了提升用户体验,避免重复下载,我们需要将下载的 MP3 文件缓存到本地存储中,可以使用
File或专门的缓存库如DiskLruCache。 - 后台播放: 即使 App 进入后台,也要能继续播放音乐,这需要使用
MediaSession和Notification来创建媒体通知,并与系统媒体中心交互。
完整实现步骤
我们将使用 OkHttp 进行网络请求,MediaPlayer 进行播放,并实现一个简单的本地缓存。

第 1 步:添加网络权限和依赖
在 app/build.gradle 文件中添加 OkHttp 依赖:
dependencies {
implementation 'com.squareup.okhttp3:okhttp:4.12.0' // 使用最新稳定版本
}
在 AndroidManifest.xml 中添加网络权限:
<uses-permission android:name="android.permission.INTERNET" /> <!-- 如果需要下载到外部存储,还需要这个权限 --> <!-- <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> --> <!-- Android 13 (API 33+) 及以上版本需要申请媒体权限 --> <!-- <uses-permission android:name="android.permission.READ_MEDIA_AUDIO" /> -->
注意: 从 Android 10 (API 29) 开始,应用默认不能在外部存储(
Environment.getExternalStorageDirectory())创建自己的文件,推荐使用应用专属的内部缓存目录getExternalFilesDir(null)。
第 2 步:创建缓存工具类
这个类负责管理 MP3 文件的下载和缓存。

import android.content.Context;
import android.util.Log;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
public class Mp3CacheManager {
private static final String TAG = "Mp3CacheManager";
private static final String CACHE_DIR_NAME = "mp3_cache";
private final Context context;
private final OkHttpClient okHttpClient;
public Mp3CacheManager(Context context) {
this.context = context.getApplicationContext();
this.okHttpClient = new OkHttpClient();
}
/**
* 获取或下载MP3文件
* @param mp3Url MP3文件的URL
* @return 文件对象,如果下载失败则返回null
*/
public File getCachedOrDownloadFile(String mp3Url) {
// 1. 检查缓存中是否已存在
File cacheFile = new File(getCacheDir(), getFileNameFromUrl(mp3Url));
if (cacheFile.exists()) {
Log.d(TAG, "File found in cache: " + cacheFile.getAbsolutePath());
return cacheFile;
}
// 2. 如果不存在,则下载
Log.d(TAG, "Downloading file from: " + mp3Url);
try (InputStream inputStream = downloadFile(mp3Url);
FileOutputStream outputStream = new FileOutputStream(cacheFile)) {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
Log.d(TAG, "File downloaded successfully: " + cacheFile.getAbsolutePath());
return cacheFile;
} catch (IOException e) {
Log.e(TAG, "Failed to download file", e);
if (cacheFile.exists()) {
cacheFile.delete(); // 下载失败,删除不完整的文件
}
return null;
}
}
/**
* 使用OkHttp下载文件流
*/
private InputStream downloadFile(String fileUrl) throws IOException {
Request request = new Request.Builder()
.url(fileUrl)
.build();
Response response = okHttpClient.newCall(request).execute();
if (!response.isSuccessful()) {
throw new IOException("Unexpected code " + response);
}
return response.body().byteStream();
}
/**
* 从URL中提取文件名
*/
private String getFileNameFromUrl(String url) {
return url.substring(url.lastIndexOf('/') + 1);
}
/**
* 获取缓存目录
*/
private File getCacheDir() {
File cacheDir = new File(context.getExternalFilesDir(null), CACHE_DIR_NAME);
if (!cacheDir.exists()) {
cacheDir.mkdirs();
}
return cacheDir;
}
}
第 3 步:创建 MediaPlayer 管理器
这个类负责管理 MediaPlayer 的生命周期、播放、暂停等操作。
import android.content.Context;
import android.media.MediaPlayer;
import android.net.Uri;
import android.os.PowerManager;
import android.util.Log;
import java.io.IOException;
public class Mp3PlayerManager implements MediaPlayer.OnPreparedListener, MediaPlayer.OnErrorListener, MediaPlayer.OnCompletionListener {
private static final String TAG = "Mp3PlayerManager";
private MediaPlayer mediaPlayer;
private Context context;
private PlaybackListener listener;
public interface PlaybackListener {
void onPrepared();
void onError(String error);
void onCompletion();
void onProgressUpdate(int progress, int duration);
}
public Mp3PlayerManager(Context context) {
this.context = context;
}
public void setPlaybackListener(PlaybackListener listener) {
this.listener = listener;
}
public void play(String mp3Url) {
// 如果正在播放,先停止并释放
if (mediaPlayer != null) {
mediaPlayer.stop();
mediaPlayer.release();
mediaPlayer = null;
}
mediaPlayer = new MediaPlayer();
mediaPlayer.setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK); // 防止屏幕关闭时播放停止
mediaPlayer.setOnPreparedListener(this);
mediaPlayer.setOnErrorListener(this);
mediaPlayer.setOnCompletionListener(this);
try {
// 使用缓存管理器获取文件
Mp3CacheManager cacheManager = new Mp3CacheManager(context);
File mp3File = cacheManager.getCachedOrDownloadFile(mp3Url);
if (mp3File != null) {
// 从本地文件播放
mediaPlayer.setDataSource(context, Uri.fromFile(mp3File));
mediaPlayer.prepareAsync(); // 异步准备,避免阻塞UI线程
} else {
if (listener != null) {
listener.onError("Failed to download or find the MP3 file.");
}
}
} catch (IOException e) {
Log.e(TAG, "Error setting data source", e);
if (listener != null) {
listener.onError("IO Error: " + e.getMessage());
}
}
}
public void pause() {
if (mediaPlayer != null && mediaPlayer.isPlaying()) {
mediaPlayer.pause();
}
}
public void resume() {
if (mediaPlayer != null && !mediaPlayer.isPlaying()) {
mediaPlayer.start();
}
}
public void stop() {
if (mediaPlayer != null) {
mediaPlayer.stop();
mediaPlayer.release();
mediaPlayer = null;
}
}
public int getDuration() {
return mediaPlayer != null ? mediaPlayer.getDuration() : 0;
}
public int getCurrentPosition() {
return mediaPlayer != null ? mediaPlayer.getCurrentPosition() : 0;
}
public void seekTo(int position) {
if (mediaPlayer != null) {
mediaPlayer.seekTo(position);
}
}
@Override
public void onPrepared(MediaPlayer mp) {
Log.d(TAG, "MediaPlayer prepared");
mp.start();
if (listener != null) {
listener.onPrepared();
}
}
@Override
public boolean onError(MediaPlayer mp, int what, int extra) {
Log.e(TAG, "MediaPlayer error. What: " + what + ", Extra: " + extra);
if (listener != null) {
listener.onError("MediaPlayer Error");
}
return true; // 表示我们已经处理了错误
}
@Override
public void onCompletion(MediaPlayer mp) {
Log.d(TAG, "Playback completed");
if (listener != null) {
listener.onCompletion();
}
}
}
第 4 步:在 Activity 或 Fragment 中使用
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.view.View;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
public class MainActivity extends AppCompatActivity implements Mp3PlayerManager.PlaybackListener {
private static final String MP3_URL = "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3"; // 示例MP3链接
private Mp3PlayerManager playerManager;
private Button playPauseButton;
private Button stopButton;
private TextView statusText;
private ProgressBar progressBar;
private Handler progressHandler = new Handler(Looper.getMainLooper());
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
playPauseButton = findViewById(R.id.btn_play_pause);
stopButton = findViewById(R.id.btn_stop);
statusText = findViewById(R.id.tv_status);
progressBar = findViewById(R.id.progress_bar);
playerManager = new Mp3PlayerManager(this);
playerManager.setPlaybackListener(this);
playPauseButton.setOnClickListener(v -> {
if (playerManager.getDuration() > 0 && !playerManager.isPlaying()) {
playerManager.resume();
playPauseButton.setText("Pause");
} else {
playerManager.pause();
playPauseButton.setText("Play");
}
});
stopButton.setOnClickListener(v -> {
playerManager.stop();
playPauseButton.setText("Play");
progressBar.setProgress(0);
statusText.setText("Stopped");
});
}
@Override
public void onPrepared() {
runOnUiThread(() -> {
playPauseButton.setText("Pause");
statusText.setText("Playing...");
updateProgress();
});
}
@Override
public void onError(String error) {
runOnUiThread(() -> {
Toast.makeText(this, "Error: " + error, Toast.LENGTH_SHORT).show();
statusText.setText("Error");
});
}
@Override
public void onCompletion() {
runOnUiThread(() -> {
playPauseButton.setText("Play");
statusText.setText("Completed");
progressHandler.removeCallbacksAndMessages(null); // 停止进度更新
});
}
private void updateProgress() {
if (playerManager == null) return;
int duration = playerManager.getDuration();
int currentPos = playerManager.getCurrentPosition();
if (duration > 0) {
int progress = (int) (1000 * (float) currentPos / duration); // 假设进度条最大为1000
progressBar.setProgress(progress);
statusText.setText(String.format("Playing... %d/%d ms", currentPos, duration));
}
// 每秒更新一次
progressHandler.postDelayed(this::updateProgress, 1000);
}
@Override
protected void onDestroy() {
super.onDestroy();
if (playerManager != null) {
playerManager.stop();
}
progressHandler.removeCallbacksAndMessages(null); // 防止内存泄漏
}
}
对应的 activity_main.xml 布局文件:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/tv_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ready to play"
android:textSize="18sp" />
<ProgressBar
android:id="@+id/progress_bar"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:max="1000" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:orientation="horizontal">
<Button
android:id="@+id/btn_play_pause"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Play" />
<Button
android:id="@+id/btn_stop"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Stop" />
</LinearLayout>
</LinearLayout>
进阶与最佳实践
后台播放与媒体通知
为了让音乐在后台播放,你需要:
- 创建一个 Service: 创建一个继承自
Service的类,MusicService,在这个 Service 中初始化并管理MediaPlayer。 - 使用 MediaSession: 创建一个
MediaSession来与系统媒体中心交互,这允许锁屏控制、通知控制以及与其他音乐 App 的协调。 - 创建通知: 使用
MediaStyle创建一个通知,显示播放/暂停按钮和歌曲信息,当通知被点击时,能将 App 拉到前台。
这是一个比较复杂的主题,Android 官方提供了 MediaSession 和 MediaController 的详细指南。

使用 ExoPlayer (强烈推荐)
对于任何复杂的音频/视频播放需求,Google 的 ExoPlayer 是比 MediaPlayer 更现代、更强大的选择。
为什么推荐 ExoPlayer?
- 高度可定制: 可以自定义组件,如加载器、解码器、渲染器。
- 支持现代协议: 原生支持 DASH, HLS, SmoothStreaming 等流媒体协议,也能很好地处理普通 HTTP/HTTPS 下载。
- 更稳定的性能: 经过 Google 大量项目验证,性能和稳定性远超
MediaPlayer。 - 先进的特性: 支持自适应比特率、硬件加速、字幕等。
ExoPlayer 基本用法示例:
// build.gradle implementation "androidx.media3:media3-exoplayer:1.3.1" implementation "androidx.media3:media3-exoplayer-hls:1.3.1" // 如果需要播放HLS流 implementation "androidx.media3:media3-ui:1.3.1" // PlayerView
<!-- activity_main.xml -->
<androidx.media3.ui.PlayerView
android:id="@+id/player_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:use_controller="true" />
// MainActivity.java
import androidx.appcompat.app.AppCompatActivity;
import androidx.media3.common.MediaItem;
import androidx.media3.exoplayer.ExoPlayer;
public class MainActivity extends AppCompatActivity {
private ExoPlayer player;
private androidx.media3.ui.PlayerView playerView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
playerView = findViewById(R.id.player_view);
player = new ExoPlayer.Builder(this).build();
playerView.setPlayer(player);
// 准备要播放的媒体项目
MediaItem mediaItem = MediaItem.fromUri("https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3");
player.setMediaItem(mediaItem);
// 准备并播放
player.prepare();
player.play();
}
@Override
protected void onDestroy() {
super.onDestroy();
player.release(); // 释放播放器资源
}
}
权限处理
- INTERNET: 在
AndroidManifest.xml中声明即可。 - WRITE_EXTERNAL_STORAGE: 如果需要将文件下载到共享的外部存储,需要动态请求此权限(针对 Android 6.0+),但从 Android 10 开始,强烈建议使用
getExternalFilesDir(),它不需要任何权限。 - FOREGROUND_SERVICE: 如果你的后台 Service 需要显示前台通知,需要此权限(针对 Android 9+)。
- READ_MEDIA_AUDIO: 如果你的 App 需要读取用户的音频文件(在播放列表中显示),在 Android 13+ 上需要此权限。
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| MediaPlayer | 简单易用,Android 系统原生支持 | 功能有限,不支持现代流媒体协议,Bug 较多,难以定制 | 非常简单的音频播放,如提示音、短音效。 |
| ExoPlayer | 功能强大,性能优异,高度可定制,协议支持好 | 学习曲线稍陡,集成相对复杂 | 专业级音频/视频 App,流媒体 App,对播放体验要求高的 App。 |
对于网络 MP3 播放,强烈建议直接使用 ExoPlayer,虽然它比 MediaPlayer 复杂一些,但为你省去了大量处理边缘情况和兼容性问题的麻烦,提供了更好的用户体验和未来的可扩展性,上面的 MediaPlayer 示例可以帮助你理解基本流程,但新项目应优先考虑 ExoPlayer。
