第二阶段:Android音视频基础

文章目录

    • [🎯 本阶段学习目标](#🎯 本阶段学习目标)
    • [📅 1. MediaPlayer基础](#📅 1. MediaPlayer基础)
      • [🎵 什么是MediaPlayer?](#🎵 什么是MediaPlayer?)
        • [🔍 MediaPlayer的状态机](#🔍 MediaPlayer的状态机)
        • [💻 基础音频播放实现](#💻 基础音频播放实现)
    • [📅 2. VideoView视频播放](#📅 2. VideoView视频播放)
      • [📺 什么是VideoView?](#📺 什么是VideoView?)
        • [💻 基础视频播放实现](#💻 基础视频播放实现)
        • [📱 布局文件](#📱 布局文件)
    • [📅 3. 音频焦点管理](#📅 3. 音频焦点管理)
      • [🔊 什么是音频焦点?](#🔊 什么是音频焦点?)
        • [💻 音频焦点管理实现](#💻 音频焦点管理实现)
    • [📅 4. 生命周期整合](#📅 4. 生命周期整合)
      • [🔄 为什么要管理生命周期?](#🔄 为什么要管理生命周期?)
        • [💻 完整的生命周期管理](#💻 完整的生命周期管理)
    • [🎓 第二阶段总结](#🎓 第二阶段总结)
      • [✅ 你已经掌握的技能](#✅ 你已经掌握的技能)

从理论走向实践,开始编写真正的代码 📱💻

掌握Android平台的基础音视频API

🎯 本阶段学习目标

学完这个阶段,你将能够:

  • 🎵 使用MediaPlayer播放音频:掌握基础音频播放
  • 📺 使用VideoView播放视频:实现简单视频播放
  • 🔊 管理音频焦点:处理多应用音频冲突
  • 🔄 正确管理生命周期:避免内存泄漏和崩溃

📅 1. MediaPlayer基础

🎵 什么是MediaPlayer?

生活比喻 :MediaPlayer就像是一台简单的CD播放机

text 复制代码
💿 音频文件 → 🎮 MediaPlayer → 🔊 扬声器播放
🔍 MediaPlayer的状态机

MediaPlayer有一个复杂的状态机,理解它很重要:

kotlin 复制代码
/**
 * 🎮 MediaPlayer状态机详解
 */
class MediaPlayerStates {
    
    /**
     * MediaPlayer就像一台有很多按钮的机器
     * 每个按钮只能在特定状态下按,否则会出错
     */
    enum class State(val description: String, val allowedOperations: List<String>) {
        IDLE("空闲状态", listOf("setDataSource")),
        INITIALIZED("已初始化", listOf("prepare", "prepareAsync")),
        PREPARING("准备中", listOf("等待准备完成")),
        PREPARED("准备完成", listOf("start", "seekTo", "setLooping")),
        STARTED("播放中", listOf("pause", "stop", "seekTo")),
        PAUSED("暂停中", listOf("start", "stop", "seekTo")),
        STOPPED("已停止", listOf("prepare", "prepareAsync")),
        PLAYBACK_COMPLETED("播放完成", listOf("start", "stop", "seekTo")),
        ERROR("错误状态", listOf("reset")),
        END("已释放", listOf("无操作"))
    }
    
    /**
     * 🚨 常见错误:在错误的状态调用方法
     */
    fun showCommonMistakes() {
        println("❌ 常见错误示例:")
        println("1. 在IDLE状态调用start() → 崩溃")
        println("2. 在PREPARING状态调用pause() → 崩溃") 
        println("3. 忘记调用prepare() → 崩溃")
        println("4. 忘记调用release() → 内存泄漏")
    }
}
💻 基础音频播放实现
kotlin 复制代码
/**
 * 🎵 简单音频播放器
 */
class SimpleAudioPlayer {
    
    private var mediaPlayer: MediaPlayer? = null
    
    /**
     * 播放本地音频文件
     * 
     * @param context 上下文对象,用于访问应用资源
     * @param rawResourceId 音频资源ID,位于res/raw目录下
     * 
     * 使用场景:播放应用内置的音效、背景音乐等
     * 优点:不需要网络,加载速度快
     * 缺点:会增加APK大小
     */
    fun playLocalAudio(context: Context, rawResourceId: Int) {
        try {
            // 第一步:创建MediaPlayer实例
            // MediaPlayer.create()是便捷方法,会自动完成setDataSource()和prepare()
            mediaPlayer = MediaPlayer.create(context, rawResourceId)
            
            // 第二步:设置播放完成监听器
            // 当音频播放到结尾时会触发此回调
            mediaPlayer?.setOnCompletionListener { mp ->
                println("🎵 播放完成")
                releasePlayer() // 播放完成后释放资源,避免内存泄漏
            }
            
            // 设置错误监听器
            // 当播放过程中发生错误时会触发此回调
            // what: 错误类型,extra: 额外错误信息
            mediaPlayer?.setOnErrorListener { mp, what, extra ->
                println("❌ 播放错误: what=$what, extra=$extra")
                releasePlayer() // 发生错误时也要释放资源
                true // 返回true表示已处理错误,不会再抛出异常
            }
            
            // 第三步:开始播放
            // 由于使用了MediaPlayer.create(),此时已经是PREPARED状态,可以直接start()
            mediaPlayer?.start()
            println("▶️ 开始播放音频")
            
        } catch (e: Exception) {
            println("❌ 播放失败: ${e.message}")
            releasePlayer()
        }
    }
    
    /**
     * 播放网络音频文件
     * 
     * @param url 网络音频URL,支持HTTP/HTTPS协议
     * 
     * 使用场景:播放在线音乐、播客、网络电台等
     * 注意事项:
     * 1. 需要网络权限:<uses-permission android:name="android.permission.INTERNET" />
     * 2. 需要处理网络异常情况
     * 3. 建议使用异步准备,避免阻塞UI线程
     */
    fun playNetworkAudio(url: String) {
        try {
            mediaPlayer = MediaPlayer().apply {
                // 设置网络音频数据源
                // 这会将MediaPlayer从IDLE状态转换到INITIALIZED状态
                setDataSource(url)
                
                // 异步准备音频数据(网络音频强烈推荐使用异步)
                // 同步prepare()会阻塞UI线程,可能导致ANR
                // 异步prepareAsync()不会阻塞,准备完成后会回调OnPreparedListener
                prepareAsync()
                
                // 设置准备完成监听器
                // 只有在此回调中才能安全地调用start()方法
                setOnPreparedListener { mp ->
                    println("✅ 准备完成,开始播放")
                    mp.start() // 从PREPARED状态转换到STARTED状态
                }
                
                // 设置播放完成监听器
                setOnCompletionListener { mp ->
                    println("🎵 播放完成")
                    releasePlayer() // 自动释放资源
                }
                
                // 设置错误处理监听器
                // 网络音频更容易出错:网络中断、URL无效、格式不支持等
                setOnErrorListener { mp, what, extra ->
                    println("❌ 播放错误: what=$what, extra=$extra")
                    releasePlayer()
                    true // 返回true表示错误已被处理
                }
            }
            
        } catch (e: Exception) {
            println("❌ 播放失败: ${e.message}")
            releasePlayer()
        }
    }
    
    /**
     * 暂停音频播放
     * 
     * 只有在STARTED状态下才能调用pause()
     * 暂停后MediaPlayer进入PAUSED状态,可以通过start()恢复播放
     */
    fun pauseAudio() {
        // 使用takeIf确保只有在播放状态下才暂停,避免IllegalStateException
        mediaPlayer?.takeIf { it.isPlaying }?.pause()
        println("⏸️ 暂停播放")
    }
    
    /**
     * 恢复音频播放
     * 
     * 从PAUSED状态恢复到STARTED状态
     * 会从暂停的位置继续播放,而不是从头开始
     */
    fun resumeAudio() {
        // 确保MediaPlayer存在且当前不在播放状态
        mediaPlayer?.takeIf { !it.isPlaying }?.start()
        println("▶️ 继续播放")
    }
    
    /**
     * 停止音频播放
     * 
     * 停止后MediaPlayer进入STOPPED状态
     * 如果要重新播放,需要重新调用prepare()或prepareAsync()
     * 与pause()不同,stop()后无法直接start()恢复
     */
    fun stopAudio() {
        mediaPlayer?.stop()
        println("⏹️ 停止播放")
    }
    
    /**
     * 🧹 释放MediaPlayer资源(非常重要!)
     * 
     * 为什么必须调用release():
     * 1. MediaPlayer占用系统底层资源(音频解码器、缓冲区等)
     * 2. 这些资源数量有限,不释放会导致其他应用无法播放音频
     * 3. 可能导致内存泄漏和性能问题
     * 
     * 调用时机:
     * - Activity/Fragment的onDestroy()中
     * - 播放完成后
     * - 发生错误时
     * - 不再需要播放器时
     */
    fun releasePlayer() {
        mediaPlayer?.release() // 释放底层资源,MediaPlayer进入END状态
        mediaPlayer = null     // 清空引用,帮助GC回收
        println("🧹 播放器资源已释放")
    }
}

📅 2. VideoView视频播放

📺 什么是VideoView?

生活比喻 :VideoView就像是一台简单的电视机

text 复制代码
📹 视频文件 → 📺 VideoView → 🖥️ 屏幕显示
💻 基础视频播放实现
kotlin 复制代码
/**
 * 📺 简单视频播放器Activity
 */
class SimpleVideoActivity : AppCompatActivity() {
    
    private lateinit var videoView: VideoView
    private lateinit var mediaController: MediaController
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_simple_video)
        
        initVideoView()
        playVideo()
    }
    
    /**
     * 初始化VideoView组件和相关设置
     * 
     * VideoView是基于MediaPlayer和SurfaceView的封装,提供了简单的视频播放功能
     */
    private fun initVideoView() {
        videoView = findViewById(R.id.video_view)
        
        // 创建媒体控制器(提供播放、暂停、进度条等UI控件)
        // MediaController会在用户点击屏幕时显示,几秒后自动隐藏
        mediaController = MediaController(this)
        mediaController.setAnchorView(videoView) // 设置控制器的附着视图
        videoView.setMediaController(mediaController) // 将控制器绑定到VideoView
        
        // 设置视频准备完成监听器
        // 当视频数据准备完成后会触发此回调
        videoView.setOnPreparedListener { mediaPlayer ->
            println("✅ 视频准备完成")
            
            // 设置视频缩放模式
            // SCALE_TO_FIT: 保持宽高比,缩放到适合尺寸
            // SCALE_TO_FIT_WITH_CROPPING: 填满屏幕,可能裁剪部分内容
            mediaPlayer.setVideoScalingMode(MediaPlayer.VIDEO_SCALING_MODE_SCALE_TO_FIT)
        }
        
        // 设置视频播放完成监听器
        videoView.setOnCompletionListener { mediaPlayer ->
            println("🎬 视频播放完成")
            // 可以在这里处理播放完成后的逻辑,比如跳转到下一个视频
        }
        
        // 设置视频播放错误监听器
        videoView.setOnErrorListener { mediaPlayer, what, extra ->
            println("❌ 视频播放错误: what=$what, extra=$extra")
            // 常见错误:网络问题、文件格式不支持、文件损坏等
            true // 返回true表示错误已被处理
        }
    }
    
    /**
     * 播放视频文件
     * 
     * VideoView支持多种视频源:本地文件、网络文件、内容提供者等
     */
    private fun playVideo() {
        // 方式1:播放应用内置的本地视频文件
        // 视频文件需要放在res/raw目录下
        // android.resource://协议用于访问应用资源
        val uri = Uri.parse("android.resource://$packageName/${R.raw.sample_video}")
        videoView.setVideoURI(uri)
        
        // 方式2:播放网络视频文件
        // 需要网络权限和处理网络异常
        // val networkUri = Uri.parse("https://example.com/video.mp4")
        // videoView.setVideoURI(networkUri)
        
        // 方式3:播放本地存储的视频文件
        // val localFile = File(Environment.getExternalStorageDirectory(), "video.mp4")
        // val fileUri = Uri.fromFile(localFile)
        // videoView.setVideoURI(fileUri)
        
        // 开始播放视频
        // VideoView会自动调用prepare()和start()
        videoView.start()
    }
    
    /**
     * Activity暂停时的处理
     * 
     * 当Activity进入后台或被其他Activity遮挡时会调用
     * 需要暂停视频播放以节省资源和电量
     */
    override fun onPause() {
        super.onPause()
        // 检查视频是否正在播放,如果是则暂停
        // 这样可以避免在后台继续播放视频,浪费电量和流量
        if (videoView.isPlaying) {
            videoView.pause()
        }
    }
    
    /**
     * Activity销毁时的处理
     * 
     * 当Activity被销毁时必须释放视频播放器资源
     * 避免内存泄漏和资源泄漏
     */
    override fun onDestroy() {
        super.onDestroy()
        // 停止视频播放并释放相关资源
        // stopPlayback()会停止播放并释放底层MediaPlayer资源
        videoView.stopPlayback()
    }
}
📱 布局文件
xml 复制代码
<!-- activity_simple_video.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">
    
    <VideoView
        android:id="@+id/video_view"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" />
        
</LinearLayout>

📅 3. 音频焦点管理

🔊 什么是音频焦点?

生活比喻 :音频焦点就像是话语权

text 复制代码
🎵 音乐APP正在播放
📞 电话来了 → 抢夺话语权 → 音乐暂停
📞 电话结束 → 归还话语权 → 音乐继续
💻 音频焦点管理实现
kotlin 复制代码
/**
 * 🔊 音频焦点管理器
 */
class AudioFocusManager(private val context: Context) {
    
    private val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
    private var mediaPlayer: MediaPlayer? = null
    
    /**
     * 音频焦点变化监听器
     * 
     * 当系统音频焦点发生变化时会回调此监听器
     * 应用需要根据不同的焦点变化类型做出相应的处理
     */
    private val audioFocusChangeListener = AudioManager.OnAudioFocusChangeListener { focusChange ->
        when (focusChange) {
            AudioManager.AUDIOFOCUS_GAIN -> {
                // 获得音频焦点(完全获得或重新获得)
                // 场景:应用启动播放、其他应用释放焦点后
                println("🔊 获得音频焦点,恢复播放")
                mediaPlayer?.start()                    // 开始或恢复播放
                mediaPlayer?.setVolume(1.0f, 1.0f)     // 恢复正常音量
            }
            
            AudioManager.AUDIOFOCUS_LOSS -> {
                // 永久失去音频焦点
                // 场景:用户打开了其他音乐应用、接听电话等
                // 应该停止播放并释放资源
                println("🔇 永久失去音频焦点,停止播放")
                mediaPlayer?.stop()                     // 停止播放
                releaseAudioFocus()                     // 释放音频焦点
            }
            
            AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
                // 暂时失去音频焦点
                // 场景:通知声音、短信提示音、导航语音等
                // 应该暂停播放,等待重新获得焦点
                println("⏸️ 暂时失去音频焦点,暂停播放")
                mediaPlayer?.pause()                    // 暂停播放,保持播放位置
            }
            
            AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
                // 暂时失去焦点,但可以降低音量继续播放("ducking")
                // 场景:导航提示、语音助手等短暂的音频
                // 可以降低音量继续播放,不需要完全停止
                println("🔉 降低音量继续播放")
                mediaPlayer?.setVolume(0.3f, 0.3f)     // 降低到30%音量
            }
        }
    }
    
    /**
     * 请求音频焦点
     * 
     * @return Boolean 是否成功获得音频焦点
     * 
     * 在开始播放音频前必须先请求音频焦点
     * 这是Android音频系统的礼貌约定,确保不同应用间的音频不会冲突
     */
    fun requestAudioFocus(): Boolean {
        val result = audioManager.requestAudioFocus(
            audioFocusChangeListener,              // 焦点变化监听器
            AudioManager.STREAM_MUSIC,             // 音频流类型(音乐流)
            AudioManager.AUDIOFOCUS_GAIN           // 请求的焦点类型(完全获得)
        )
        
        // 检查是否成功获得音频焦点
        // AUDIOFOCUS_REQUEST_GRANTED: 成功获得
        // AUDIOFOCUS_REQUEST_FAILED: 获取失败
        // AUDIOFOCUS_REQUEST_DELAYED: 延迟获得(API 26+)
        return result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED
    }
    
    /**
     * 释放音频焦点
     * 
     * 当不再需要播放音频时应该主动释放音频焦点
     * 这样其他应用就可以获得音频焦点进行播放
     * 
     * 调用时机:
     * - 播放完成时
     * - 用户主动停止播放时
     * - Activity销毁时
     * - 发生播放错误时
     */
    fun releaseAudioFocus() {
        // 放弃音频焦点,传入之前注册的监听器
        audioManager.abandonAudioFocus(audioFocusChangeListener)
    }
    
    /**
     * 带音频焦点管理的播放器
     * 
     * @param audioUrl 音频文件URL
     * 
     * 这是一个完整的音频播放流程示例:
     * 1. 先请求音频焦点
     * 2. 获得焦点后才开始播放
     * 3. 播放过程中响应焦点变化
     * 4. 出错时释放焦点
     */
    fun playWithAudioFocus(audioUrl: String) {
        // 第一步:请求音频焦点
        // 只有获得焦点后才能开始播放,这是良好的用户体验
        if (requestAudioFocus()) {
            try {
                // 第二步:创建并配置MediaPlayer
                mediaPlayer = MediaPlayer().apply {
                    setDataSource(audioUrl)
                    prepareAsync()                      // 异步准备
                    
                    // 准备完成后开始播放
                    setOnPreparedListener { mp ->
                        mp.start()
                        println("▶️ 开始播放(已获得音频焦点)")
                    }
                    
                    // 播放完成后释放焦点
                    setOnCompletionListener { mp ->
                        releaseAudioFocus()
                    }
                    
                    // 出错时也要释放焦点
                    setOnErrorListener { mp, what, extra ->
                        releaseAudioFocus()
                        true
                    }
                }
            } catch (e: Exception) {
                println("❌ 播放失败: ${e.message}")
                releaseAudioFocus()                 // 出错时释放焦点
            }
        } else {
            println("❌ 无法获得音频焦点,播放失败")
            // 可能的原因:其他应用正在播放重要音频(如通话)
        }
    }
}

📅 4. 生命周期整合

🔄 为什么要管理生命周期?

生活比喻 :就像离开房间要关灯一样

text 复制代码
🏠 进入房间 → 💡 开灯 → 🚶‍♂️ 离开房间 → 💡 关灯
📱 进入页面 → ▶️ 开始播放 → 🔄 离开页面 → ⏹️ 停止播放
💻 完整的生命周期管理
kotlin 复制代码
/**
 * 🔄 生命周期感知的音视频播放器
 */
class LifecycleAwarePlayer : AppCompatActivity() {
    
    private var mediaPlayer: MediaPlayer? = null
    private var videoView: VideoView? = null
    private lateinit var audioFocusManager: AudioFocusManager
    
    // 记录播放状态
    private var isPlaying = false
    private var currentPosition = 0
    
    /**
     * Activity创建时的初始化工作
     * 
     * 这是Activity的第一个生命周期方法,只会调用一次
     * 在这里进行一次性的初始化工作
     */
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_player)
        
        // 初始化音频焦点管理器
        audioFocusManager = AudioFocusManager(this)
        
        // 初始化播放器组件(但不开始播放)
        initPlayer()
        
        println("🔄 onCreate: 初始化播放器")
    }
    
    /**
     * Activity变为可见状态
     * 
     * 当Activity从不可见变为可见时调用
     * 可以在这里做一些准备工作,但不要开始播放
     */
    override fun onStart() {
        super.onStart()
        println("🔄 onStart: 页面可见")
        // 可以在这里做一些准备工作,比如检查网络状态
    }
    
    /**
     * Activity获得焦点,变为活跃状态
     * 
     * 这是用户可以与Activity交互的状态
     * 在这里恢复播放是最合适的
     */
    override fun onResume() {
        super.onResume()
        println("🔄 onResume: 页面获得焦点")
        
        // 恢复之前的播放状态
        // 只有在之前正在播放的情况下才恢复
        if (isPlaying) {
            resumePlayback()
        }
    }
    
    /**
     * Activity失去焦点,进入暂停状态
     * 
     * 当其他Activity覆盖当前Activity或用户按下主页键时调用
     * 必须在这里暂停播放,节省资源和电量
     */
    override fun onPause() {
        super.onPause()
        println("🔄 onPause: 页面失去焦点")
        
        // 第一步:保存当前播放状态和位置
        // 这样在onResume()时可以恢复到相同的状态
        savePlaybackState()
        
        // 第二步:暂停播放
        // 避免在后台继续播放,浪费电量和流量
        pausePlayback()
    }
    
    /**
     * Activity变为不可见状态
     * 
     * 当Activity完全被其他Activity遮挡时调用
     * 可以在这里停止一些不必要的后台任务
     */
    override fun onStop() {
        super.onStop()
        println("🔄 onStop: 页面不可见")
        // 可以在这里停止一些耗资源的操作,比如停止位置更新
    }
    
    /**
     * Activity被销毁
     * 
     * 这是Activity的最后一个生命周期方法
     * 必须在这里释放所有资源,避免内存泄漏
     */
    override fun onDestroy() {
        super.onDestroy()
        println("🔄 onDestroy: 页面销毁")
        
        // 释放所有资源:MediaPlayer、VideoView、音频焦点等
        // 这是防止内存泄漏的关键步骤
        releaseAllResources()
    }
    
    /**
     * 保存当前播放状态和位置
     * 
     * 在onPause()中调用,保存播放状态以便在onResume()中恢复
     * 这样可以提供更好的用户体验,用户回到应用时可以继续之前的播放
     */
    private fun savePlaybackState() {
        // 保存MediaPlayer的状态
        mediaPlayer?.let { player ->
            isPlaying = player.isPlaying           // 是否正在播放
            currentPosition = player.currentPosition // 当前播放位置(毫秒)
        }
        
        // 保存VideoView的状态
        videoView?.let { video ->
            if (video.isPlaying) {
                isPlaying = true
                currentPosition = video.currentPosition
            }
        }
    }
    
    /**
     * 暂停当前播放
     * 
     * 在onPause()中调用,暂停所有正在播放的媒体
     * 暂停后可以通过resumePlayback()恢复播放
     */
    private fun pausePlayback() {
        // 暂停音频播放器
        mediaPlayer?.pause()
        
        // 暂停视频播放器
        videoView?.pause()
    }
    
    /**
     * 恢复之前的播放状态
     * 
     * 在onResume()中调用,恢复到之前保存的播放位置和状态
     * 只有在之前正在播放的情况下才会恢复播放
     */
    private fun resumePlayback() {
        // 恢复音频播放器
        mediaPlayer?.let { player ->
            player.seekTo(currentPosition)     // 跳转到之前的播放位置
            player.start()                     // 开始播放
        }
        
        // 恢复视频播放器
        videoView?.let { video ->
            video.seekTo(currentPosition)      // 跳转到之前的播放位置
            video.start()                      // 开始播放
        }
    }
    
    /**
     * 释放所有媒体播放相关资源
     * 
     * 在onDestroy()中调用,确保所有资源都被正确释放
     * 这是防止内存泄漏的关键步骤
     */
    private fun releaseAllResources() {
        // 释放MediaPlayer资源
        mediaPlayer?.release()              // 释放底层音频解码器资源
        mediaPlayer = null                  // 清空引用
        
        // 释放VideoView资源
        videoView?.stopPlayback()           // 停止播放并释放资源
        videoView = null                    // 清空引用
        
        // 释放音频焦点
        audioFocusManager.releaseAudioFocus() // 让其他应用可以获得音频焦点
        
        println("🧹 所有资源已释放")
    }
}

🎓 第二阶段总结

✅ 你已经掌握的技能

🎵 MediaPlayer使用

  • 理解了MediaPlayer的状态机
  • 能够播放本地和网络音频
  • 掌握了基础的播放控制

📺 VideoView使用

  • 能够播放本地和网络视频
  • 了解了MediaController的使用
  • 掌握了基础的视频播放

🔊 音频焦点管理

  • 理解了音频焦点的概念
  • 能够正确处理音频冲突
  • 实现了礼貌的音频播放

🔄 生命周期管理

  • 理解了Android生命周期
  • 能够正确管理播放器资源
  • 避免了内存泄漏问题

💡 学习建议

一定要动手实践!理论再多,不如写一行代码。

每个示例都要运行一遍,遇到问题就是学习的机会! 🛠️

相关推荐
度熊君3 小时前
深入理解 Kotlin 协程结构化并发
android·程序员
吴Wu涛涛涛涛涛Tao3 小时前
用 Flutter + BLoC 写一个顺手的涂鸦画板(支持撤销 / 重做 / 橡皮擦 / 保存相册)
android·flutter·ios
bqliang3 小时前
从喝水到学会 Android ASM 插桩
android·kotlin·android studio
HAPPY酷4 小时前
Flutter 开发环境搭建全流程
android·python·flutter·adb·pip
圆肖4 小时前
File Inclusion
android·ide·android studio
青旬4 小时前
我的AI搭档:从“年久失修”的AGP 3.4到平稳着陆AGP 8.0时代
android·ai编程
加油20195 小时前
音视频处理(四):一文讲清楚VoIP语音通话SIP和RTP协议
音视频·pcm·voip·sip·rtp·g.711·语音通话
FrameNotWork5 小时前
#RK3588 Android 14 虚拟相机 HAL 开发踩坑实录:从 Mali Gralloc 报错到成功显示画面
android·车载系统
恋猫de小郭6 小时前
回顾 Flutter Flight Plans ,关于 Flutter 的现状和官方热门问题解答
android·前端·flutter