大家在做音乐类App的时候,应该都遇到过类似的困境:播放页面自己实现了,通知栏也想搞个自定义控制器,耳机线控要单独接按键事件,锁屏封面又是一套逻辑,恨不得每个系统入口都写一遍对接代码。等真把这一堆全做完,你会发现不同入口之间的状态还经常对不上——通知栏显示播放中,App里其实已经暂停了。
这其实就是缺了AVSession造成的局面。AVSession Kit在HarmonyOS上扮演的是媒体会话中枢的角色,App只需要维护一份播放状态,系统拿到后会自动同步到控制中心、状态栏、锁屏和耳机按键这些系统入口,不需要开发者逐个适配。HarmonyOS 6把这套能力推进到了API 20,新增了一批针对媒体应用痛点的接口。这篇文章我顺着仿某云音乐这个练手项目的完整实现,把AVSession Kit新特性的用法、控制命令分发链路、后台播放合规路径以及实测中踩过的坑都过一遍,适合正在做音频类应用的HarmonyOS开发者参考。
1. 先理解AVSession在设计上解决了什么问题
1.1 没有媒体会话之前,媒体应用的状态是散落的
很多开发者第一次接触AVSession时容易想偏,觉得这不过是个"状态上报接口"。但真正理解它的价值,得先看看没有它的时候App要面对什么。
假设你正在实现一个播放器,最简单的需求是:用户在锁屏上能看到歌名,按一下耳机按键能暂停。在没有统一媒体会话的框架里,这两个需求分别要走完全不同的技术路线。锁屏封面需要往系统UI里推信息,耳机按键要注册音频焦点并监听按键事件,通知栏的播放控制器又是一套独立的RemoteView。三套方案互相之间没有任何状态关联,播放状态一变,你得同时手工更新三处。
更麻烦的是,系统的控制中心、语音助手这些入口要看媒体信息时,没有一个标准的数据结构供它们读取。每个App各写各的,系统的统一呈现就无从谈起。这就是AVSession存在的根本原因:给媒体应用定义一套标准的数据模型和控制通道,App把状态往上一交,所有系统入口自动生效。
1.2 AVSession在系统里的位置像一个"交换中心"
我习惯把AVSession理解成媒体信息的集散中心。App这边是会话的创建方,通过AVSession对象上报元数据和播放状态;系统那边是会话的消费方,通过AVSessionController对象读取信息、下发控制指令。App和系统UI之间不直接通信,全部经由AVSession中转。
这样一来,数据结构就是固定的那一套。通知栏要显示什么,控制中心要展示什么,锁屏要渲染什么,都从同一份数据源读取;用户在任何入口按下播放、暂停、切歌,最终都以标准控制指令的形式回到App的回调里。开发者的对接对象从"系统里的各个UI模块"简化成了"AVSession这一个点"。
1.3 一个直接的好处:状态一致性由系统保证
我印象很深的一点是,AVSession让"UI显示的播放状态"和"App实际的播放状态"保持一致这件事变成了默认行为。以前我自己维护通知栏状态时,最怕的就是播放器在某个异常分支里崩溃,通知栏还停留在"播放中"。用AVSession之后,App每次调用setAVPlaybackState都会同步到所有系统UI,App崩溃、会话销毁时系统UI也会自动清空对应状态。少了一大类状态同步的bug。
2. API 20的新特性:媒体会话能力的一次补全
2.1 元数据字段更贴近真实音乐类App
API 20之前,AVMetadata虽然能设置标题、作者这些基本字段,但遇到音乐类App的典型需求时往往不够用。比如某云音乐那种"同一首歌可能存在多个版本,专辑封面、歌词、音质标识都要展示"的场景,旧版接口要么自己想办法拼字符串,要么干脆不支持。
API 20在这一块做了明显补强,新增的字段包括但不限于:
| 字段 | 作用 | 典型使用场景 |
|---|---|---|
assetId | 媒体资源唯一标识 | 多端同步播放进度时定位具体音频 |
lyric | 歌词文本 | 锁屏和控制中心展示滚动歌词 |
mediaImage | 媒体封面图 | 通知栏大图、锁屏封面 |
mediaSource | 媒体来源标识 | 区分在线、缓存或本地文件 |
previousAssetId/nextAssetId | 上一首/下一首的资源ID | 快速预加载相邻曲目 |
totalTrackCount | 播放列表总曲目数 | 控制中心显示"第3首/共20首" |
这里我建议assetId一定不要省。仿某云音乐时,我一开始只填了title和artist,结果后续想根据当前歌曲查缓存状态、做播放记录埋点时都没有稳定索引,又回头补了这一字段。而totalTrackCount这种看着不起眼的字段,很多人在测试时忽略,实际上控制中心的"曲目序号"展示完全依赖它。
2.2 播放状态控制新增了循环和随机粒度
API 20对AVPlaybackState的控制粒度也细化了。旧版对loopMode和shuffleMode的支持比较弱,甚至出现过"设置了循环状态但系统控制中心不更新"的情况。新版本把这两个模式提升为标准属性,并且允许通过控制指令由系统UI反向修改。
什么意思呢?用户在控制中心点了一下"循环模式"按钮,系统会下发对应的handleLoopModeChange指令,App在回调里处理之后,再把最新的loopMode状态通过setAVPlaybackState上报回去。这个闭环跑通之后,App内部播放器模式和系统UI展示的模式就永远是一致的。
shuffleMode同理。实测下来,实现这两个指令的收益非常大,因为用户已经养成了在系统控制中心直接切换播放模式的习惯。
2.3 音频焦点冲突的处理从"通知"变成了"联动"
HarmonyOS里音频焦点一直是音频类App必须处理的点。API 20之前,音频焦点变化走的是音频管理那边的事件回调,由App自己决定暂停还是压低音量,AVSession不参与。新版本里,音频焦点事件和控制指令做了联动,来电、语音助手抢占焦点时,系统会自动把播放指令传达给App。
实际联动逻辑:
- 焦点被临时抢占(比如来电响铃):系统下发
pause指令,App收到后暂停播放并上报暂停状态 - 焦点被永久抢占(比如另一个App开始播歌):系统下发
pause指令,同时标记当前会话失去焦点 - 焦点恢复(来电结束):系统下发
play指令,App可以选择恢复或维持暂停
这对应的实际代码改动很小——只需在on('play')、on('pause')这些回调里处理,不需要再单独监听音频焦点回调。代码量少了,焦点的处理逻辑反而更完整。
3. 仿某云音乐第1步:创建会话、上报元数据和播放状态
3.1 环境准备和信息配置
先说开发环境。API 20对应的是HarmonyOS 6,使用DevEco Studio时,在build-profile.json5里确保compileSdkVersion和targetSdkVersion都指向API 20及以上:
{ "app": { "signingConfigs": [], "products": [ { "name": "default", "compileSdkVersion": "6.0.0(20)", "targetSdkVersion": "6.0.0(20)", "runtimeOS": "HarmonyOS" } ] } }然后有一个很容易忽略的点:AVSession本身不需要在module.json5里声明特定权限,但后台播放需要。后面讲后台播放管理时会细说,这里先留个印象。
创建会话之前,先想清楚会话类型。AVSession的SessionType是创建时必须确定的,告诉系统这个会话是干嘛的:
| 类型 | 适用场景 |
|---|---|
AUDIO | 普通音频播放,含音乐、播客、有声书 |
VIDEO | 视频播放 |
VOICE_ASSISTANT | 语音助手类 |
CALL | 通话类(语音/视频) |
仿照某云音乐的场景,用AUDIO类别即可。
3.2 创建AVSession实例的完整写法
import { avSession } from '@kit.AVSessionKit'; import { common } from '@kit.AbilityKit'; async function createMediaSession(context: common.UIAbilityContext): Promise<avSession.AVSession> { // 参数依次为:context、会话类型、会话标签(用于调试区分) const session = await avSession.createAVSession( context, avSession.SessionType.AUDIO, 'CloudMusicPlayer' ); return session; }createAVSession返回的是一个AVSession实例,后续所有媒体状态的上报都通过它完成。注意第三个参数tag,最好设置成当前页面的功能标识。我在调试多挂载会话问题时,就靠这个tag区分到底是哪个页面创建的会话。
3.3setAVMetadata:把歌曲信息交给系统
拿到session实例后,第一步就是把当前要播放的音频元数据上报:
async function updateMediaMetadata(session: avSession.AVSession) { const metadata: avSession.AVMetadata = { assetId: 'song_20240607_001', // 唯一资源ID,建议用服务端返回的歌曲ID title: '晴天', artist: '周杰伦', album: '叶惠美', duration: 269000, // 单位是毫秒 lyric: '[00:12.00]故事的小黄花...', mediaImage: { // 封面图有多种传法,推荐传ArrayBuffer或PixelMap mimeType: 'image/jpeg', data: coverImageBuffer // 注意控制大小,建议先压缩再传 }, previousAssetId: 'song_20240607_000', nextAssetId: 'song_20240607_002', totalTrackCount: 15, mediaSource: 'online' }; await session.setAVMetadata(metadata); }关于封面图我要专门提醒一下。mediaImage支持PixelMap和ArrayBuffer两种形态,真机上往系统UI传图时数据量太大会引起明显的卡顿。我个人实践下来的做法是:把封面压缩到最大边长不超过512像素,JPEG格式,转成ArrayBuffer再传。这样既能保证控制中心和锁屏的清晰度,又不会拖垮性能。
3.4setAVPlaybackState:保持系统UI显示与播放器同步
元数据描述的是"这首歌是什么",播放状态描述的是"现在播得怎么样":
async function updatePlaybackState(session: avSession.AVSession, isPlaying: boolean, position: number) { const playbackState: avSession.AVPlaybackState = { state: isPlaying ? avSession.PlaybackState.PLAYBACK_STATE_PLAYING : avSession.PlaybackState.PLAYBACK_STATE_PAUSED, position: position, // 单位毫秒 speed: isPlaying ? 1.0 : 0.0, loopMode: avSession.LoopMode.LOOP_MODE_ALL, shuffleMode: avSession.ShuffleMode.SHUFFLE_MODE_OFF }; await session.setAVPlaybackState(playbackState); }这里有两个细节值得展开说。第一个是position的含义,它表示当前播放位置。让播放器在播放中定期回调updatePlaybackState是一个好习惯,比如每5秒同步一次,或者每一首切歌完成时同步一次。如果长时间不上报,控制中心的进度条会失去准头。第二个是state的取值集合,API 20的PlaybackState枚举比旧版丰富,至少要保证在PLAYING、PAUSED、STOPPED、BUFFERING这几个状态间正确切换。
实测中BUFFERING这个状态特别重要。仿某云音乐在线播放时,如果网络慢导致播放器反复等待,不上报BUFFERING状态的话,控制中心会长时间显示"播放中",直到用户手动操作出现异常才发现问题。
3.5 新增getAVPlaybackState读取完整状态
API 20之前,查询当前播放状态时拿到的对象字段不全,尤其是自定义扩展信息。新版getAVPlaybackState返回完整的AVPlaybackState,包含position、speed、loopMode、shuffleMode等全部字段。
我用这个接口做过一个很实用的功能:App冷启动后,通过它查上次会话的播放状态,再决定UI是显示播放页还是列表页。应用被杀掉再回来,播放位置、播放模式直接恢复,体验上和某云音乐的"续播"功能非常接近。
async function restorePlaybackState(session: avSession.AVSession) { const state = await session.getAVPlaybackState(); // 判断state.state,恢复播放器状态 if (state.state === avSession.PlaybackState.PLAYBACK_STATE_PLAYING) { // 恢复播放进度 } }4. 控制链路全拆解:从耳机键到App回调
4.1 控制器的获取与会话监听
AVSession这边创建会话后,控制指令并不是直接发给session,而是发给"控制器"。系统UI、耳机按键、控制中心,本质都是通过AVSessionController来发指令的。App作为会话的创建方,需要通过监听从控制器下发的事件来响应操作。
先获取当前系统中所有媒体会话对应的控制器:
async function getControllers(context: common.UIAbilityContext) { // 返回当前所有媒体会话的控制器列表 const controllers = await avSession.getAllSessionControllers(context); controllers.forEach(controller => { console.info(`当前会话 tag: ${controller.ownerSessionTag}`); // 每个controller可以绑定一个session }); }同时监听新会话的创建:
// 系统中有一个新会话被创建时触发 avSession.on('sessionCreate', (session: avSession.AVSession) => { console.info(`新会话创建: ${session.sessionTag}`); }); // 会话销毁时触发 avSession.on('sessionDestroy', (sessionId: string) => { console.info(`会话销毁: ${sessionId}`); });我比较推荐的做法是在EntryAbility的onCreate阶段就注册监听,这样App启动后无论有多少个会话,都能第一时间追踪到,避免"当前会话已经被销毁但监听器还挂着旧控制器"的问题。
4.2 核心控制指令的监听与处理
控制指令的监听直接在AVSessionController上注册即可。常用的指令有这些:
controller.on('play', () => { // 用户点击播放/耳机播放键 startPlayback(); }); controller.on('pause', () => { // 用户点击暂停/耳机暂停键 pausePlayback(); }); controller.on('next', () => { // 用户点击下一首 playNext(); }); controller.on('previous', () => { // 用户点击上一首 playPrevious(); }); controller.on('seek', (position: number) => { // 用户拖拽进度条,position单位毫秒 seekTo(position); });除了上面这些基础指令,还有几个进阶指令需要处理。如果控制中心支持完整控制(比如某云音乐的页面里就有单曲循环、随机播放的入口),就要监听handleLoopModeChange和handleShuffleModeChange:
controller.on('handleLoopModeChange', (loopMode: avSession.LoopMode) => { // 系统UI下发切换循环模式指令 player.setLoopMode(loopMode); }); controller.on('handleShuffleModeChange', (shuffleMode: avSession.ShuffleMode) => { player.setShuffleMode(shuffleMode); });这里要注意:收到指令后,在App内部完成对应操作后,要同步通过setAVPlaybackState把最新状态上报回去,形成闭环。只处理指令而不回传状态,系统UI上显示的循环模式就不会更新。
4.3 播放进度的“主动推送”机制
AVSession的状态同步机制里,有一部分事件是"系统主动拉取",有一部分需要"App主动推送"。播放进度属于后者。
API 20推荐的做法是周期性地调用setAVPlaybackState更新位置。周期多长合适?我实测下来,5秒更新一次,控制中心进度条基本看不出卡顿;10秒更新一次的话,用户在锁屏上看进度时会明显觉得"跳了一段"。当然,如果App正在执行seek操作,建议立刻上报一次position,否则要等最多5秒控制中心才跳到位。
// 定时器,每5秒上报一次位置 setInterval(() => { if (isPlaying) { updatePlaybackState(session, true, player.getCurrentPosition()); } }, 5000);一个我在实机上发现的坑:定时器一定要在App退到后台后继续运行。HarmonyOS的JS定时器在后台有节流机制,长时间不触发会导致进度上报断掉。需要配合后面讲的后台任务机制,保证这个定时器在后台能持续执行。
4.4 利用dispatchSessionEvent实现自定义事件下发
AVSession除了标准控制指令,还提供了dispatchSessionEvent用于传递自定义事件。比如某云音乐里"歌词已加载"、"音质切换为无损"这类可能与系统UI无关、但App内其他模块需要感知的消息,可以走这个通道。
// 会话方向:App内部主动发事件 session.dispatchSessionEvent('qualityChanged', { quality: 'lossless' }); // 控制器方向:订阅该事件 controller.on('event', (event) => { const eventName = event.eventName; const eventData = event.eventData; // 处理自定义事件 });不过说实话,dispatchSessionEvent在系统UI上几乎是不会展示的,主要用于App内部模块解耦或者未来智能手表、车机生态的多端联动。如果单纯想在App内部通信,直接用全局状态管理就行,不建议为了"炫技"把这个通道用得太频繁。
5. 后台播放的合规姿势:确保你的App不会被杀
5.1 后台播放属于哪类长时任务
AVSession解决的是"媒体信息怎么同步到系统",后台播放解决的是"App在后台时播放进程怎么不被回收"。这两个能力是配合使用的,缺一不可。很多人在开发时先把AVSession打通了,结果一锁屏音频就断——问题往往出在后台任务没有正确申请。
HarmonyOS的后台任务分类中,音频播放属于"长时任务"里的audioPlayback类型。要让系统允许App在后台持续运行,需要在module.json5中声明对应的backgroundModes,并且运行时向后台任务管理器发起申请。
先看配置文件。在module.json5的module.abilities里给入口Ability配置后台模式:
{ "module": { "abilities": [ { "name": "EntryAbility", "srcEntry": "./ets/entryability/EntryAbility.ets", "backgroundModes": ["audioPlayback"], "startWindowIcon": "$media:startIcon", "startWindowBackground": "$color:start_window_background" } ], "requestPermissions": [ { "name": "ohos.permission.KEEP_BACKGROUND_RUNNING", "reason": "用于后台继续播放音频", "usedScene": { "ability": ["EntryAbility"], "when": "always" } } ] } }ohos.permission.KEEP_BACKGROUND_RUNNING是一个系统级受限权限,普通应用可以直接声明,但上架实名认证后(调试模式不需要)审核时会检查用途。音乐类App用途明确,一般都能过。
5.2 运行时申请后台任务
配置只是第一步,真正让App在后台运行时不被回收,需要在播放开始的时候调用后台任务API。
import { backgroundTaskManager } from '@kit.BackgroundTasksKit'; import { common } from '@kit.AbilityKit'; let delayInfo: backgroundTaskManager.DelaySuspendInfo | null = null; async function startBackgroundRunning(context: common.UIAbilityContext) { try { // 参数:任务类型,这里必须是音频播放 // 第二个参数是一个回调,系统准备挂起App时会触发 delayInfo = await backgroundTaskManager.requestSuspendDelay( 'audio playback', () => { // 系统即将挂起,这里保存状态,为下次恢复做准备 savePlaybackStateToCache(); // 注意:此处不能直接注销后台任务,要在真正释放资源时才release } ); console.info(`后台任务申请成功,可继续运行时间:${delayInfo.actualDelayTime}ms`); } catch (err) { console.error(`后台任务申请失败:${JSON.stringify(err)}`); } } // 暂停播放时,释放后台任务 async function stopBackgroundRunning() { if (delayInfo) { backgroundTaskManager.cancelSuspendDelay(delayInfo.requestId); delayInfo = null; } }注意requestSuspendDelay回调的含义:系统会在需要挂起App之前调用它,给App一个"再救一下"的机会,但要真正让App持续存活,还得靠别的机制——比如在回调里重启一个前台服务。这块逻辑容易理解偏,我单独说一下。
5.3 AVSession、后台任务、实际播放器三者如何配合
我整理了一下API 20场景下媒体类App后台播放的完整配合链路:
| 环节 | 组件 | 职责 |
|---|---|---|
| 媒体状态同步 | AVSession | 上报元数据和播放状态,接收控制指令 |
| 后台存活保障 | backgroundTaskManager | 申请长时任务,避免进程被回收 |
| 实际播放能力 | AVPlayer/音频框架 | 真正解码、渲染音频 |
| 生命周期保护 | 延续任务/Fiber | 在系统要挂起时延续后台运行 |
启动一个完整的后台播放流程是这样的:
- 用户点击播放按钮
- 播放器开始播放,同时调用
requestSuspendDelay申请后台任务 - 创建AVSession并上报当前歌曲元数据和播放状态
- App退到后台,由于长时任务申请成功,系统不会立刻回收
- 用户按下耳机暂停键,控制器下发
pause指令,App回调里暂停播放器 - 暂停完成后调用
cancelSuspendDelay释放后台任务
暂停时不释放后台任务,是新手最容易犯的错误。后台任务一直挂着,等于告诉系统"我要一直在后台跑",对系统功耗、对其他App公平性都不好。正确做法是:播放暂停就释放,恢复播放再申请。
5.4 进程可能被重启:不用怕,但要兜住关键状态
即使正确申请了后台任务,系统仍然有可能因为内存压力在极端情况下回收App。但系统会记住这个会话的存在,重启App后可以重建会话并恢复之前的状态。
处理方式:在onDestroy等生命周期回调里保存当前播放曲目、播放位置、播放模式,重启后在onCreate里读取并通过createAVSession重新建立会话,再通过setAVPlaybackState恢复状态。这就是"续播"能力的底层逻辑。API 20的getAVPlaybackState在这里非常有用——如果App是被系统杀掉重建的,旧会话信息还在系统侧存活时,直接查询新会话状态就能恢复。
6. 实测中遇到的坑:这些细节常规文档不会写
6.1 创建会话时Context的作用域问题
createAVSession对Context有要求。使用页面UIAbility的context传入没有问题,但从某个子模块拿到的context如果类型不对,会出现能力配置错误。我遇到过传ApplicationContext创建的会话,结果控制中心能收到元数据但无法下发控制指令的情况。
h经验是:创建会话时用UIAbilityContext,这是最稳的。如果模块里只有Context,可以通过context as common.UIAbilityContext强转,但前提是它本质确实是UIAbilityContext。在纯服务模块里创建会话时尤其要注意这个类型问题。
6.2 position和duration的单位一致性
这个看起来低级,但真的很常见。AVMetadata.duration、AVPlaybackState.position、seek指令的参数,统一都是毫秒。但AVPlayer那边有些API返回的时间单位是微秒(比如部分历史版本的时长接口),如果不做转换直接传给AVSession,控制中心的进度条会出现"比例完全不对"的现象——歌显示播了半个小时,其实才过了3分钟。
建议在整个播放器模块里做一个时间戳工具方法,统一把所有时间换算成毫秒后再和AVSession交互:
function toMillis(microsOrMillis: number, unit: 'us' | 'ms'): number { return unit === 'us' ? Math.round(microsOrMillis / 1000) : microsOrMillis; }6.3 多个会话并存时,系统UI会显示哪个
一个App如果创建了多个AVSession(比如每个页面创建一个),控制中心会展示多个媒体卡片吗?实测下来,系统UI通常会优先展示"最近一次活跃"的会话。但问题是,如果你签名相同、包名相同的两个页面各自管理一个会话,切换页面时容易出现控制中心卡片切换不及时、指令发给旧会话的情况。
处理建议:App全局尽量保持单一AVSession。仿某云音乐时我就是把会话对象放在一个全局单例里,播放页和列表页共用这个会话,所有状态更新都走同一份逻辑。如果是极复杂场景需要创建新会话,记得在退出页面前销毁旧会话,避免会话数量堆积。
6.4 销毁会话的时机,控制中心的卡片残留
AVSession在页面退出时不会自动销毁。很多开发者发现退出播放页后控制中心的卡片还在,甚至还能点击,就是这个原因——会话还活着,没有显式销毁。需要在页面销毁或播放器释放时主动清理:
async function deactivateSession(session: avSession.AVSession | null) { if (session) { await session.destroy(); } }注意销毁的前提是确实不再需要后台播放了。如果用户退到桌面但歌还在播,会话不能销毁,否则控制中心直接变空白。正确的时机是:播放暂停、且确认不需要恢复播放时,才销毁。
6.5 模拟器与真机的表现差异
AVSession的很多特性在模拟器上表现不完整。比如锁屏封面的显示、音频焦点联动的触发,模拟器往往没有对应的系统UI入口或音频外设支持,我在模拟器上就遇到过"锁屏看不到控制卡片"的情况。这不是代码问题,是模拟器能力缺失。
所以建议涉及AVSession的调试尽量用真机,尤其测试这些场景:
- 控制中心卡片展示和操作
- 锁屏媒体卡片
- 耳机线控指令
- 音频焦点抢占(比如来电打断)
模拟器只用来验证创建会话和状态上报这种逻辑性操作。
6.6 在ArkTS中的import路径,经常编译不过
这个问题在团队协作里踩得最多。AVSession Kit在API 20的ArkTS中统一通过@kit.AVSessionKit导入,但有些博客和旧版本代码用的是@ohos.multimedia.avsession,在API 20里这会直接编译报错。
// 正确姿势,API 20推荐 import { avSession } from '@kit.AVSessionKit'; import { backgroundTaskManager } from '@kit.BackgroundTasksKit';新版DevEco Studio对新工程默认就是@kit前缀,但如果你是从旧项目升上来的,别忘了把import路径全部改掉。这也是一个"看着编译失败莫名其妙"的常见原因。
7. 从仿某云音乐到生产级App,最后差在这几步
AVSession的接入只是媒体类App的起点。API 20把会话标准、控制指令、焦点联动这些基础能力补齐了,但要真正达到某云音乐那种体验,还需要在会话之上叠加更多自己的工程细节。
我做完这个仿某云音乐项目后,最明显的体会有三点。第一,媒体应用的稳定性关键在于状态机,播放状态到哪里上报、什么时机上报,要当成核心逻辑来设计,而不是作为UI的附属品。第二,后台播放不是"申请完任务就万事大吉",用户重复进出后台、频繁播放暂停,后台任务的申请和释放要严格配对,否则系统资源很快被消耗,最后照样被杀。第三,一定要在实机上把控制中心的每一个按钮都点一遍。很多人只在模拟器上跑通就提交代码,到了真机上发现按钮没反应、状态不同步,返工成本反而更高。
AVSession这套框架,真正用熟练之后,你会发现它帮开发者挡掉的脏活比想象中多得多。做一次全链路接入,后续每次新增媒体功能需要写的系统对接代码基本为零,只需要维护好那一份状态数据源。从这个角度看,投入这些时间去理解会话机制是完全值得的。