Android多媒体开发实战:从MediaPlayer到ExoPlayer的架构选型与性能优化

1. 项目概述:为什么说多媒体是Android开发的“硬骨头”?

做Android开发有些年头了,从早期的2.x版本一路跟到现在,要说哪个模块最能体现移动设备的特性、最能直接提升用户体验,同时又最容易让开发者头疼,那非多媒体莫属。这个“day8:Android多媒体使用”的标题,乍一看像是某个学习计划中的一环,但它背后涵盖的内容,远不止调用几个API那么简单。它涉及音频、视频、图像的处理、播放、录制、编解码,以及与系统硬件(如摄像头、麦克风、扬声器)的深度交互。对于新手来说,这里面的坑一个接一个:为什么我的应用播放视频没声音?为什么录制的视频在其他设备上无法播放?为什么图片加载多了应用就卡顿甚至崩溃?

这些问题,本质上都是因为Android多媒体框架的复杂性和设备碎片化导致的。今天,我们就抛开那些速成教程里简单的“Hello World”式调用,深入聊聊在Android中实际使用多媒体功能时,你需要掌握的核心思路、必须绕开的深坑,以及如何构建一个健壮、高效的多媒体功能模块。无论你是想实现一个音乐播放器、一个短视频拍摄应用,还是仅仅需要在应用里展示图片和播放提示音,这篇文章里的经验都能帮你省下大量调试时间。

2. 核心架构与方案选型:不是所有场景都用 MediaPlayer

一提到Android多媒体,很多人第一个想到的就是 MediaPlayer 。没错,它是Android SDK中用于播放音频和视频的核心类,但把它当作万能钥匙就大错特错了。方案选型直接决定了你后续开发的复杂度、应用的性能以及用户的体验。

2.1 音频播放: MediaPlayer SoundPool 还是 AudioTrack

  • MediaPlayer :这是最通用、功能最全的音频播放方案。它支持多种格式(MP3, AAC, WAV等),能处理流媒体(如网络音频),并且集成了音量控制、循环播放、监听播放状态等高级功能。 适合场景 :播放背景音乐、长音频(如播客)、网络流媒体。它的缺点是延迟较高(通常超过100毫秒),不适合需要精准触发的音效。
  • SoundPool :专为播放短促、需要快速响应、可能同时播放多个的音频片段而设计。它先将音频文件加载到内存中,播放时直接从内存解码,延迟极低(可控制在10毫秒内)。 适合场景 :游戏音效、UI交互反馈音(如按钮点击)。但要注意,它占用内存,不适合长音频,且支持的音频格式和编码参数有限制。
  • AudioTrack :这是最底层的音频播放API,提供了对音频数据的字节流进行写入和播放的能力。它给你最大的控制权,比如你可以自己生成PCM数据或进行音频处理。 适合场景 :需要实时生成或处理音频的应用(如音频合成器、变声器、专业的音频处理工具)。它的使用也最复杂,你需要自己管理音频流、处理线程同步。

实操心得 :在项目中,我通常混合使用。对于背景音乐和长内容,用 MediaPlayer ;对于所有UI音效和游戏短音效,统一用 SoundPool 管理,并严格控制加载到 SoundPool 中的音频文件大小和数量,避免OOM(内存溢出)。曾经在一个游戏项目中,因为把所有音效都用 MediaPlayer 播放,导致快速点击时音效堆积、延迟严重,后来切换到 SoundPool 后体验立刻流畅了。

2.2 视频播放: MediaPlayer + SurfaceView / TextureView 还是 ExoPlayer

  • MediaPlayer + SurfaceView / TextureView :这是Android原生的标准方案。 MediaPlayer 负责解码和播放控制, SurfaceView TextureView 负责渲染视频画面。 SurfaceView 性能更好,因为它拥有独立的绘图表面,但无法进行动画或叠加视图。 TextureView 则是一个普通的View,可以执行动画、叠加其他元素,但性能开销稍大。
  • ExoPlayer :这是Google官方推出的一个开源、基于 MediaPlayer API但更强大、更灵活的媒体播放库。它支持更多的格式(如DASH, SmoothStreaming, HLS等流媒体协议),更容易定制和扩展(例如,自定义数据源、渲染器),并且更新活跃。 对于任何需要高级播放功能(如自适应码流、自定义UI、复杂播放列表)或需要支持多种流媒体协议的项目, ExoPlayer 几乎是目前的不二之选。

为什么选 ExoPlayer 原生 MediaPlayer 在不同厂商、不同系统版本的设备上行为不一致的问题(碎片化)是开发者的噩梦。 ExoPlayer 通过软件解码和统一的实现,提供了更一致的行为。此外,它的模块化架构让你可以轻松替换其中任何一个组件(比如,使用FFmpeg进行软解码来支持更多格式)。

2.3 图像加载: ImageView 直接设置? BitmapFactory ?还是用库?

直接通过 BitmapFactory.decodeFile() 加载图片然后设置给 ImageView ,这是最原始的方式,也是内存崩溃的根源。一张几兆的相机图片,解码成 Bitmap 后,在内存中的占用会急剧膨胀(宽度 * 高度 * 每个像素的字节数)。

成熟的方案是使用图片加载库:

  • Glide :Google推荐,API极其简洁,专注于流畅的滚动体验。它默认做了大量的优化:自动缓存(内存和磁盘)、自动处理 Bitmap 生命周期、支持GIF等。对于绝大多数应用场景,Glide是首选。
  • Picasso :Square公司出品,API同样简单,但功能比Glide稍少(例如早期不支持GIF)。
  • Fresco :Facebook出品,性能最强,尤其是在处理大量图片时。它使用了一个特殊的存储区域(Ashmem)来存储图片,可以大幅减少Java堆内存的压力。但它的库体积较大,API相对复杂。

避坑指南 :如果你没有特殊需求(比如需要极致的内存优化来处理海量图片),直接上 Glide 。它的学习成本最低,社区活跃,能解决95%的图片加载问题。记住一个原则: 永远不要在主线程中解码大图 ,也 永远要对待加载的图片进行采样压缩 ,根据显示的ImageView大小来加载合适尺寸的图片。

3. 核心细节解析与避坑要点

知道了用什么工具,接下来就是怎么用好。多媒体开发中,细节决定成败,很多崩溃和异常都源于对细节的忽视。

3.1 生命周期管理:资源泄露的罪魁祸首

这是Android多媒体开发中最重要,也最容易出错的一点。 MediaPlayer Camera AudioRecord 等对象都持有宝贵的系统资源(如编解码器、硬件设备句柄)。如果不在合适的时机释放,就会导致资源泄露,表现为应用卡顿、耗电剧增,甚至其他应用无法使用多媒体功能。

标准模式

  1. 初始化 :通常在 onCreate() 或用户触发时初始化。
  2. 准备 :调用 prepareAsync() (网络流)或 prepare() (本地文件),并在监听器中开始播放。
  3. 释放 :在 onPause() onStop() 中,必须调用 release() 方法释放所有资源。对于 MediaPlayer ,释放后该对象就不可再用了。
  4. 重置 :如果你只是想切换播放源,可以先调用 reset() 回到空闲状态,再设置新的数据源并准备。
// 一个非常简化的生命周期管理示例
public class MusicPlayerActivity extends AppCompatActivity {
    private MediaPlayer mediaPlayer;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mediaPlayer = new MediaPlayer();
        // ... 设置数据源,设置监听器等
        try {
            mediaPlayer.setDataSource(filePath);
            mediaPlayer.prepareAsync();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    protected void onPause() {
        super.onPause();
        if (mediaPlayer != null && mediaPlayer.isPlaying()) {
            mediaPlayer.pause();
        }
    }

    @Override
    protected void onStop() {
        super.onStop();
        // 关键:在onStop中释放,确保页面不可见时资源被回收
        releaseMediaPlayer();
    }

    private void releaseMediaPlayer() {
        if (mediaPlayer != null) {
            mediaPlayer.stop();
            mediaPlayer.release();
            mediaPlayer = null; // 置空是个好习惯
        }
    }
}

血泪教训 :我曾经遇到一个Bug,用户从播放页面切到后台,一段时间后回来,音乐播放卡顿,最后整个系统音频都出问题。排查了很久才发现,是另一个全局单例持有的 MediaPlayer 在Activity销毁时没有正确释放,导致音频焦点和硬件解码器一直被占用。所以, 一定要在组件的生命周期结束时(如Activity的onDestroy,Service的onDestroy)确保释放资源 。对于在 Fragment ViewModel 中使用的播放器,也需要找到对应的生命周期节点进行释放。

3.2 权限与运行时请求

从Android 6.0 (API 23)开始,危险权限需要在运行时动态申请。多媒体相关的主要权限包括:

  • RECORD_AUDIO :录制音频。
  • CAMERA :使用摄像头。
  • READ_EXTERNAL_STORAGE / WRITE_EXTERNAL_STORAGE :读写外部存储(访问相册、保存录制文件)。注意,从Android 10 (API 29)开始,作用域存储(Scoped Storage)政策改变了文件访问方式,推荐使用 MediaStore API。

操作流程

  1. AndroidManifest.xml 中声明权限。
  2. 在需要权限的代码处(如点击录制按钮时),检查是否已授权 ( ContextCompat.checkSelfPermission )。
  3. 如果未授权,使用 ActivityCompat.requestPermissions 弹出系统对话框申请。
  4. onRequestPermissionsResult 回调中处理授权结果。

注意事项 :用户可能点击“拒绝且不再询问”。对于必须的权限,你需要优雅地处理这种情况:弹出一个自定义对话框,解释为什么需要这个权限,并引导用户去应用设置页手动开启。代码会变得冗长,但这正是健壮性的一部分。

3.3 音频焦点管理:做一个“有礼貌”的应用

当你的应用开始播放音频时,应该请求音频焦点 ( AudioManager.requestAudioFocus )。如果请求成功,你就可以播放。当有另一个应用(比如来电或另一个音乐App)请求音频焦点时,系统会通知你,你应该根据焦点丢失的类型 ( AUDIOFOCUS_LOSS 永久丢失, AUDIOFOCUS_LOSS_TRANSIENT 暂时丢失) 来暂停播放或降低音量。当焦点重新获得时,再恢复播放。

不管理音频焦度的应用 会被用户讨厌:比如正在听音乐,打开你的应用,一声巨大的提示音把音乐打断了,并且还不恢复。实现音频焦点管理,是提升应用品质的重要一环。

// 简化的音频焦点请求示例
AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
int result = audioManager.requestAudioFocus(
    focusChangeListener, // 这是一个 AudioManager.OnAudioFocusChangeListener
    AudioManager.STREAM_MUSIC, // 使用音乐流
    AudioManager.AUDIOFOCUS_GAIN // 请求永久焦点
);

if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
    // 可以开始播放了
    mediaPlayer.start();
}

4. 实战演练:构建一个简单的音乐播放器

让我们把上面的理论付诸实践,一步步构建一个具备基本功能的本地音乐播放器。这个例子将涵盖从扫描本地音乐文件到播放控制的完整流程。

4.1 扫描本地存储获取音乐文件

在Android中,我们使用 ContentResolver 查询 MediaStore 来获取媒体文件信息,这是官方推荐且兼容性最好的方式,特别是适应了Android 10+的作用域存储。

// 在后台线程中执行此查询
private List<MusicItem> scanLocalMusic(Context context) {
    List<MusicItem> musicList = new ArrayList<>();
    ContentResolver resolver = context.getContentResolver();

    // 定义要查询的列
    String[] projection = {
            MediaStore.Audio.Media._ID,
            MediaStore.Audio.Media.TITLE,
            MediaStore.Audio.Media.ARTIST,
            MediaStore.Audio.Media.ALBUM,
            MediaStore.Audio.Media.DURATION,
            MediaStore.Audio.Media.DATA // 文件路径,注意Android Q后可能需要使用Uri而非路径
    };

    // 查询条件:是音乐文件,且时长大于一定值(过滤短提示音)
    String selection = MediaStore.Audio.Media.IS_MUSIC + " != 0 AND " +
                       MediaStore.Audio.Media.DURATION + " >= 60000"; // 大于1分钟
    String sortOrder = MediaStore.Audio.Media.TITLE + " ASC";

    Cursor cursor = null;
    try {
        cursor = resolver.query(
                MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
                projection,
                selection,
                null,
                sortOrder
        );

        if (cursor != null && cursor.moveToFirst()) {
            do {
                long id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID));
                String title = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE));
                String artist = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST));
                long duration = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DURATION));
                String path = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DATA));

                // 使用Uri构建内容Uri,更安全(尤其针对Android Q+)
                Uri contentUri = ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id);

                MusicItem item = new MusicItem(id, title, artist, duration, path, contentUri.toString());
                musicList.add(item);
            } while (cursor.moveToNext());
        }
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        if (cursor != null) {
            cursor.close();
        }
    }
    return musicList;
}

关键点 MediaStore.Audio.Media.DATA 字段在Android Q及以上版本可能无法直接获取到可用的文件路径,尤其是对于应用没有权限访问的目录。更健壮的做法是使用 _ID 列构造一个 contentUri (如 content://media/external/audio/media/123 ),然后通过 ContentResolver.openFileDescriptor 来获取文件描述符进行播放。上面的代码为了清晰展示了两种方式。

4.2 实现播放控制核心逻辑

我们将播放控制封装在一个单例或绑定Service中,以便在多个Activity间共享和控制。

public class MusicPlayerManager implements MediaPlayer.OnPreparedListener,
        MediaPlayer.OnCompletionListener,
        MediaPlayer.OnErrorListener {

    private static MusicPlayerManager instance;
    private MediaPlayer mediaPlayer;
    private List<MusicItem> playList;
    private int currentPosition = 0; // 当前播放歌曲在列表中的索引
    private PlaybackState state = PlaybackState.IDLE;
    private WeakReference<MusicPlayerCallback> callbackRef;

    public enum PlaybackState { IDLE, PREPARING, PLAYING, PAUSED }

    private MusicPlayerManager() {
        mediaPlayer = new MediaPlayer();
        mediaPlayer.setOnPreparedListener(this);
        mediaPlayer.setOnCompletionListener(this);
        mediaPlayer.setOnErrorListener(this);
        // 设置音频流类型为音乐,以便受音量键和音频焦点控制
        mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
    }

    public static synchronized MusicPlayerManager getInstance() {
        if (instance == null) {
            instance = new MusicPlayerManager();
        }
        return instance;
    }

    public void setPlayList(List<MusicItem> list) {
        this.playList = list;
    }

    public void play(int position) {
        if (playList == null || playList.isEmpty() || position < 0 || position >= playList.size()) {
            return;
        }
        currentPosition = position;
        MusicItem item = playList.get(position);
        try {
            // 重置播放器到初始状态
            mediaPlayer.reset();
            // 设置数据源,这里使用文件路径,实际生产环境建议使用Uri
            mediaPlayer.setDataSource(item.getPath());
            // 异步准备,避免阻塞主线程
            mediaPlayer.prepareAsync();
            state = PlaybackState.PREPARING;
            if (callbackRef != null && callbackRef.get() != null) {
                callbackRef.get().onPlaybackStateChanged(state);
                callbackRef.get().onTrackChanged(item);
            }
        } catch (IOException e) {
            e.printStackTrace();
            state = PlaybackState.IDLE;
        }
    }

    public void pause() {
        if (state == PlaybackState.PLAYING && mediaPlayer.isPlaying()) {
            mediaPlayer.pause();
            state = PlaybackState.PAUSED;
            notifyStateChanged();
        }
    }

    public void resume() {
        if (state == PlaybackState.PAUSED) {
            mediaPlayer.start();
            state = PlaybackState.PLAYING;
            notifyStateChanged();
        }
    }

    public void stop() {
        if (state == PlaybackState.PLAYING || state == PlaybackState.PAUSED) {
            mediaPlayer.stop();
            state = PlaybackState.IDLE;
            notifyStateChanged();
        }
    }

    public void seekTo(int msec) {
        // 只有在准备完成或播放/暂停状态下才能跳转
        if (state == PlaybackState.PLAYING || state == PlaybackState.PAUSED) {
            mediaPlayer.seekTo(msec);
        }
    }

    // 实现 MediaPlayer.OnPreparedListener
    @Override
    public void onPrepared(MediaPlayer mp) {
        mp.start();
        state = PlaybackState.PLAYING;
        notifyStateChanged();
        // 这里可以开始更新播放进度条
    }

    // 实现 MediaPlayer.OnCompletionListener
    @Override
    public void onCompletion(MediaPlayer mp) {
        // 播放完成,自动播放下一首
        playNext();
    }

    // 实现 MediaPlayer.OnErrorListener
    @Override
    public boolean onError(MediaPlayer mp, int what, int extra) {
        Log.e("MusicPlayer", "Playback error: what=" + what + ", extra=" + extra);
        state = PlaybackState.IDLE;
        notifyStateChanged();
        // 返回true表示错误已处理,不会触发onCompletion
        return true;
    }

    public void playNext() {
        int nextPos = (currentPosition + 1) % playList.size();
        play(nextPos);
    }

    public void playPrevious() {
        int prevPos = (currentPosition - 1 + playList.size()) % playList.size();
        play(prevPos);
    }

    // ... 其他getter方法,如getCurrentPosition, getDuration等

    private void notifyStateChanged() {
        if (callbackRef != null && callbackRef.get() != null) {
            callbackRef.get().onPlaybackStateChanged(state);
        }
    }

    public void setCallback(MusicPlayerCallback callback) {
        this.callbackRef = new WeakReference<>(callback);
    }

    public interface MusicPlayerCallback {
        void onPlaybackStateChanged(PlaybackState state);
        void onTrackChanged(MusicItem item);
    }

    // 在应用退出或合适的时候调用
    public void release() {
        if (mediaPlayer != null) {
            mediaPlayer.release();
            mediaPlayer = null;
        }
        instance = null;
    }
}

这个管理器封装了基本的播放逻辑,并通过回调接口将状态变化通知给UI。在实际项目中,你还需要为其添加 音频焦点管理 前台服务通知 (用于后台播放)、 播放进度同步 等功能。

4.3 前台服务与通知:实现后台播放

用户希望切到其他应用或锁屏时音乐还能继续播放。这需要在 Service 中运行播放器,并将其设置为 前台服务 ,并显示一个持续的通知。

  1. 创建播放Service :继承 Service ,在 onCreate 中初始化 MusicPlayerManager ,在 onStartCommand 中处理播放控制命令(如播放、暂停)。
  2. 启动前台服务 :在开始播放时,调用 startForeground(notificationId, notification) 。这需要提供一个 Notification ,并申请 FOREGROUND_SERVICE 权限(Android 9.0+)。
  3. 构建通知 :通知中应包含歌曲信息、播放/暂停按钮(通过 PendingIntent 和广播或服务交互)。从Android 12开始,播放控制按钮有更严格的模板要求。
  4. 管理Service生命周期 :当播放停止且不需要后台运行时,调用 stopForeground(false) (保留通知)或 stopForeground(true) (移除通知),然后 stopSelf()

注意事项 :从Android 8.0 (API 26) 开始,后台服务限制变严,长时间后台任务必须使用前台服务并显示通知。务必处理好通知渠道(Notification Channel)的创建,并为通知中的操作按钮设计好响应逻辑,通常是通过 BroadcastReceiver 来接收点击事件,再传递给Service。

5. 进阶话题与性能优化

当基础功能实现后,要打造一个优秀的应用,还需要关注以下方面。

5.1 使用 ExoPlayer 替代 MediaPlayer

对于更复杂的播放需求,迁移到 ExoPlayer 是值得的。它的基本使用并不复杂:

  1. 添加依赖 :在 build.gradle 中添加 implementation 'com.google.android.exoplayer:exoplayer:2.X.X'
  2. 创建播放器实例
    SimpleExoPlayer player = new SimpleExoPlayer.Builder(context).build();
    // 将播放器绑定到用于渲染视频的SurfaceView或TextureView
    playerView.setPlayer(player);
    
  3. 准备媒体源
    // 播放本地文件
    MediaItem mediaItem = MediaItem.fromUri(Uri.parse(fileUriString));
    // 播放网络流(HLS, DASH等)
    // MediaItem mediaItem = MediaItem.fromUri("https://example.com/stream.m3u8");
    player.setMediaItem(mediaItem);
    player.prepare();
    player.play();
    
  4. 控制播放 player.play() , player.pause() , player.seekTo() 等。

ExoPlayer 的强大在于其可扩展性。你可以自定义 DataSource Renderers ,监听各种事件,并轻松实现如预加载下一个视频、自适应码流切换等高级功能。

5.2 图片加载优化与内存管理

即使使用了Glide,不当的操作仍会导致问题。

  • 指定加载尺寸 :这是最重要的优化。永远不要加载一张2048x1536的图片到一个只有200x150的 ImageView 里。
    Glide.with(context)
         .load(imageUrl)
         .override(200, 150) // 指定目标宽高
         .into(imageView);
    
    或者,Glide通常能自动根据 ImageView 的尺寸进行采样,但前提是 ImageView 的布局尺寸必须确定(不是 wrap_content 且父布局已完成测量)。在 RecyclerView 中,如果item高度固定,最好在 override 中指定。
  • 使用合适的缓存策略 :Glide默认的缓存策略( AUTOMATIC )对大多数情况都很好。对于频繁变化但文件名不变的网络图片,可以考虑 .diskCacheStrategy(DiskCacheStrategy.NONE) 跳过磁盘缓存。
  • 注意上下文 :使用 Glide.with() 时传入的 Context 类型很重要。如果传入 Activity ,图片加载会在Activity销毁时自动暂停和清理,这是最安全的。避免在全局单例或 Application Context中启动长时间加载任务。
  • 监控内存 :在开发者选项中开启“不保留活动”和“后台进程限制”来测试你的应用。使用Android Profiler工具定期检查内存中的 Bitmap 对象,确保没有异常累积。

5.3 视频处理与压缩

如果应用涉及视频录制或编辑,你会面临视频压缩的问题。Android原生的 MediaRecorder 录制的视频体积可能很大。使用 MediaCodec MediaMuxer API进行自定义编码和复用是高性能的选择,但极其复杂。

对于大多数应用,一个折中的方案是使用开源库如** FFmpeg (通过 FFmpegKit mobile-ffmpeg 等封装库)或 MediaCodec 的封装库(如 Transcoder )**。这些库可以让你相对简单地执行视频转码、压缩、裁剪、添加水印等操作。

性能提示 :视频处理是CPU和IO密集型操作, 务必在后台线程执行 ,并使用进度回调通知用户。处理大视频文件时,注意手机发热和电量消耗。

6. 常见问题排查与调试技巧

开发过程中,你肯定会遇到各种奇怪的问题。这里记录一些常见坑点和排查方法。

问题现象 可能原因 排查与解决思路
播放没声音 1. 媒体文件本身无声或损坏。
2. 音频焦点被其他应用占用。
3. 播放器音频流类型设置错误。
4. 设备音量被静音或调至最低。
5. MediaPlayer 未调用 prepare() prepareAsync() 就调用了 start()
1. 用系统播放器测试文件。
2. 检查音频焦点请求逻辑,监听焦点变化。
3. 确保 setAudioStreamType(AudioManager.STREAM_MUSIC)
4. 检查系统音量,并考虑引导用户调整。
5. 确保在 OnPreparedListener 回调中才开始播放。
播放卡顿、延迟高(网络流) 1. 网络状况差。
2. 缓冲区设置过小。
3. 服务器响应慢。
1. 监听 onBufferingUpdate 更新UI,提示用户。
2. 对于 ExoPlayer ,可以调整 LoadControl 中的缓冲区参数。
3. 考虑提供多种清晰度选择(自适应码流)。
录制视频在其他设备无法播放 1. 编码格式或参数不兼容。
2. 录制的视频缺少关键元数据。
1. 使用广泛支持的编码组合,如H.264视频 + AAC音频,MP4容器。
2. 使用 MediaRecorder 时,设置标准的输出格式和编码器。
图片加载导致列表滑动卡顿 1. 未对图片进行尺寸优化,加载了过大的Bitmap。
2. 在 RecyclerView 滑动时未暂停加载。
3. 内存缓存不足,频繁GC。
1. 使用Glide/Picasso并指定 override 尺寸。
2. Glide会自动管理 RecyclerView 的加载,但需确保 RecyclerView 正确使用了 RecyclerView.OnScrollListener 或在Adapter的 onViewRecycled 中清理请求。
3. 检查是否有内存泄露,确保 ImageView 在复用时旧的加载请求被取消。
MediaPlayer 抛出 prepareAsync failed (status=0x80000000) 1. 数据源路径错误或文件无法访问。
2. 文件格式不支持。
3. 权限问题(Android 6.0+未申请存储权限)。
1. 打印并检查数据源URI或路径是否正确。
2. 尝试播放一个标准的MP3文件测试。
3. 动态申请 READ_EXTERNAL_STORAGE 权限,并检查Android 10+的作用域存储限制。
后台播放被系统杀死 1. 未使用前台服务。
2. 应用在后台时系统为节省内存清理了进程。
3. 用户手动清理了后台任务。
1. 确保播放时启动了前台服务并显示通知。
2. 这是正常系统行为。可以在Service的 onStartCommand 中返回 START_STICKY ,让系统在内存允许时重启服务(但播放状态会丢失)。更可靠的做法是将播放状态持久化,并在重启后恢复。

调试技巧

  • 使用 Log :在 MediaPlayer 的各种监听器( OnPreparedListener , OnErrorListener , OnCompletionListener )中打印日志,这是追踪播放状态最直接的方法。
  • 检查MediaPlayer状态机 MediaPlayer 有一个严格的状态机。在不正确的状态下调用方法(如在 Idle 状态下调用 start() )会抛出异常。务必理清 reset() , prepare() , start() , pause() , stop() , release() 之间的状态转换。
  • 利用Android Profiler :监控应用的内存和CPU使用情况。播放视频时,观察 Graphics Memory 标签页,检查是否有 Bitmap 泄露或解码器资源未释放。
  • 多设备测试 :音频和视频的编解码器支持因设备厂商和系统版本差异很大。务必在你能找到的不同品牌、不同Android版本的设备上进行测试。

多媒体开发是一个充满挑战但也极具成就感的领域。它要求开发者不仅理解API的调用,更要深入理解音视频的基础原理、Android系统的运行机制以及用户体验的细微之处。从处理好一个按钮点击音效开始,到构建一个完整的流媒体播放应用,每一步的扎实积累都会让你对移动开发有更深的认识。记住,耐心调试、关注细节、善用成熟的第三方库,是攻克多媒体这座堡垒的关键。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值