尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Android MediaPlayer初始化全链路剖析:JNI、Binder到NuPlayer创建

Android MediaPlayer初始化全链路剖析:JNI、Binder到NuPlayer创建 做Android多媒体开发这几年MediaPlayer是绕不过去的一个大块头。你大概率在项目里反复用过它new 一个出来setDataSource 指定资源prepare 之后 start看起来也就这几板斧。但真到了线上出现 ANR、崩溃、播放失败的时候你对它“初始化阶段到底发生了什么”的理解程度直接决定了排查效率。这篇开始我打算用几篇文章把 Android MediaPlayer 整体架构源码分析完第一篇先盯死最容易被忽略的初始化和创建阶段。本文会从 Java 层的 new MediaPlayer() 一路往下追穿过 JNI、C 客户端、Binder IPC最后到 MediaPlayerService 里的 NuPlayerDriver 和 NuPlayer 被创建出来的全过程。适合已经能熟练写 MediaPlayer Demo、准备深入 Android Framework 的开发者阅读。我这次分析的源码基线是 Android 13 的 AOSP主要涉及 framework/av 下的 media 模块代码片段会做必要裁剪但关键调用链和状态变化会尽量贴近真实源码逻辑。1. 先看全景初始化阶段在整套架构中的位置1.1 为什么第一篇先啃创建与初始化很多朋友学 MediaPlayer 源码习惯一上来就翻 onPrepared、onSeekComplete 这些回调或者直接冲到 NuPlayer 的播放状态机里看音视频同步。我的建议恰恰相反先把创建和初始化这一条链路吃透。原因很简单。MediaPlayer 是一个跨进程播放器框架从你 new 出 Java 对象开始到 Native 层真正具备播放能力中间要经过 JNI 封装、Binder 获取服务、服务端创建播放器实例、注册回调监听、拉起事件循环线程等一系列动作。这一串动作里任何一环出问题后面所有播放逻辑都无从谈起。我在实际项目中遇到过的“诡异 bug”比如 setDataSource 之后回调偶发丢失、频繁创建播放器导致 ANR、某些 ROM 上 mediaserver 被杀后 App 卡死根因全都埋在这一阶段。换句话说这一篇讲的是 MediaPlayer 的“出生过程”。只有先把出生过程看明白了后面看播放流程、渲染管线、音视频同步才会有脚踏实地的感觉。1.2 MediaPlayer 整体分层与源码目录速查MediaPlayer 的整体架构可以用六层来理解。最上层是 Java 应用层对应 android.media.MediaPlayer我们平时写业务代码接触的就是它。这层下面是通过 JNI 注册的 Native 桥接层核心文件是 android_media_MediaPlayer.cpp负责把 Java 调用翻译成 C 调用、把 Native 事件翻译回 Java 回调。JNI 层之下是 Native 层的客户端 MediaPlayer代码在 media/libmedia/MediaPlayer.cpp。这个类的作用有点像“遥控器”它本身不干活但负责通过 Binder 拿到 media.player 系统服务的代理然后向服务端发指令。再往下就是关键的 Binder IPC 层涉及 IMediaPlayerService、IMediaPlayer、IMediaPlayerClient 这几个接口App 进程和服务进程就是靠它们完成跨进程通信的。服务端一侧MediaPlayerService 收到创建请求后会生成 NuPlayerDriver 和 NuPlayer这两个类的代码在 media/libmediaplayerservice/nuplayer/ 目录下。NuPlayer 是真正的播放引擎基于 ALooper/AHandler 消息驱动机制工作内部再拆分出数据源、解码器、渲染器等模块。从 Android 8.0 开始NuPlayer 已经是唯一的 Native 播放引擎取代了更早的 AwesomePlayer所以我们源码分析的终点就是它。1.3 初始化阶段的核心类与关键目录我把这一篇会涉及的类和对应源码路径整理一下方便你本地对照阅读。角色类 / 文件源码路径AOSP 13Java 层 APIMediaPlayer.javaframeworks/base/media/java/android/media/MediaPlayer.javaJNI 桥接android_media_MediaPlayer.cppframeworks/base/media/jni/android_media_MediaPlayer.cppNative 客户端MediaPlayer.cppframeworks/av/media/libmedia/MediaPlayer.cppBinder 接口IMediaPlayerService / IMediaPlayer / IMediaPlayerClientframeworks/av/media/libmedia/IMediaPlayer*.cpp服务端管理MediaPlayerService.cppframeworks/av/media/libmediaplayerservice/MediaPlayerService.cpp播放器驱动NuPlayerDriver.cppframeworks/av/media/libmediaplayerservice/nuplayer/NuPlayerDriver.cpp播放引擎NuPlayer.cppframeworks/av/media/libmediaplayerservice/nuplayer/NuPlayer.cpp消息循环ALooper / AHandlerframeworks/av/media/libstagefright/foundation/ALooper.cpp如果你本地还没把源码拉下来建议直接用 Android Studio 的 C 工程方式打开 framework/av 目录配合官方在线代码搜索服务跳转符号即可。纯看日志调试的话记住三个核心 TAGMediaPlayer、MediaPlayerService、NuPlayer后面排查问题全靠它们。2. 从 new MediaPlayer() 开始的几个关键跃迁2.1 Java 层构造函数不只是 new 一个对象那么简单先看我们最熟悉的入口——Java 层构造函数。MediaPlayer.java 的构造函数核心逻辑可以简化成两步准备事件分发用的 Handler然后调用 native_setup 创建 Native 层对象。这里有一点非常值得注意构造函数会优先拿当前线程的 Looper拿不到就退到主线程 Looper再不行就暂时不绑定 Handler。public MediaPlayer() { Looper looper; if ((looper Looper.myLooper()) ! null) { mEventHandler new EventHandler(this, looper); } else if ((looper Looper.getMainLooper()) ! null) { mEventHandler new EventHandler(this, looper); } else { mEventHandler null; } native_setup(new WeakReferenceMediaPlayer(this)); }这个 Looper 的选择逻辑很关键。MediaPlayer 的所有异步回调比如 onPrepared、onCompletion、onError最终都是通过这个 mEventHandler 发到指定线程的 Looper 队列里执行的。如果你在一个没有任何 Looper 的工作线程里直接 new MediaPlayer又没有主线程 Looper 兜底后面会回调不到任何事件表现就是“播放器好像死了”。这个坑我在实际开发里踩过后面常见问题部分会展开讲。native_setup 里传的是一个 WeakReference 而不是 this这也是有讲究的。JNI 层持有一个指向 Java 对象的弱引用是为了在回调 Java 层方法时能判断对象是否已经被 GC避免 Native 层反向引用导致 Java 对象无法回收。2.2 JNI 桥接native_setup 在 C 层做了什么native_setup 是 native 方法真正的实现在 android_media_MediaPlayer.cpp 里。流程大致是创建一个 C 层的 MediaPlayer 对象、给它设置一个 JNI 事件监听器、然后把 Native 对象指针保存到 Java 对象的 Native 字段中。static void android_media_MediaPlayer_native_setup(JNIEnv *env, jobject thiz, jobject weak_this) { spMediaPlayer mp new MediaPlayer(); mp-setListener(new JNIMediaPlayerListener(env, thiz, weak_this)); setMediaPlayer(env, thiz, mp); }这个 JNIMediaPlayerListener 是整个回调链的起点。它实现了 C 层的 MediaPlayerListener 接口一旦 Native 播放器有事件发生就会调用它的 notify 方法。notify 内部再通过 JNI 调用 Java 层的 postEventFromNative 静态方法把事件编码成 Message 后发给前面说的 mEventHandler。至于 setMediaPlayer 保存指针则是典型 JNI 字段操作把一个 long 类型的 Native 指针塞进 Java 对象的 address 字段里后续所有 native 方法都能从该字段取回同一个 C 对象。这一层值得留意的是 spMediaPlayer。它不是裸指针而是强引用智能指针。setMediaPlayer 时指针先被 sp 管理再通过 jobject 保存JNI 层和 Java 层同时持有时引用计数不乱这依赖 Android 一套完善的 RefBase 引用计数机制。如果你后续要自研播放器 SDK建议直接沿用这种 sp 管理模式别在 JNI 层手动 delete很容易 double free。2.3 C 层 MediaPlayer 与 MediaPlayerService 的第一次握手JNI 层 new 出来的MediaPlayer是我们说的 Native 客户端。它的构造函数里有一个非常核心的调用获取 MediaPlayerService 的 Binder 代理。getMediaPlayerService 的逻辑可以简化成下面这样。spMediaPlayerService MediaPlayer::getMediaPlayerService() { spIServiceManager sm defaultServiceManager(); spIBinder binder sm-getService(String16(media.player)); return interface_castMediaPlayerService(binder); }getService 是阻塞式 Binder 调用如果 mediaserver 进程没有启动或者系统服务注册没完成会一直等。平时我们遇到 App 冷启动时立刻创建 MediaPlayer偶尔会卡住几秒很多时候就是这里的服务获取超时。如果一个进程内连续创建多个 MediaPlayer这个服务代理不会重复获取MediaPlayer 内部有静态缓存一旦拿到就尽量复用。拿到服务代理后客户端会调用 service-create(this, sessionId, uid)把自身作为 IMediaPlayerClient 的回调接口传进服务端。这里的 this 实际上是 MediaPlayer 自身它继承自 BnMediaPlayerClient能够接收服务端的 notify 回调。也就是说在创建阶段App 进程和服务进程之间就建立起了一条双向通道App 通过 IMediaPlayer 接口发指令服务端通过 IMediaPlayerClient 接口回事件。2.4 NuPlayerDriver 登场Native 播放器的状态机雏形MediaPlayerService::create 收到请求后会做一个很重要的分流根据客户端进程的 uid、pid、音频 session 等信息创建一个 NuPlayerDriver 实例。NuPlayerDriver 这个名字里的 Driver 不是指硬件驱动而是“播放器驱动/门面”的意思它对外实现 IMediaPlayer 接口对内管理 NuPlayer 引擎和状态机。NuPlayerDriver 构造函数里发生的三件事基本决定了整个 Native 播放器的存活形态。NuPlayerDriver::NuPlayerDriver(pid_t pid, uid_t uid, const spMediaPlayerClient client, audio_session_t audioSessionId) { mLooper new ALooper; mLooper-setName(nuplayer); mLooper-start(); mPlayer new NuPlayer(pid, uid, client); mLooper-registerHandler(mPlayer); // ... 初始化状态、设置音频属性等 }看到没有真正的 NuPlayer 对象在这里才被创建而且它不是一个独立线程而是挂在一个叫 ALooper 的轻量级消息循环上。这个 Looper 线程的名字叫 nuplayer所有播放控制命令、解码器回调、渲染任务都会被封装成消息丢进这个队列由 NuPlayer 的 onMessageReceived 逐个处理。相比直接在调用线程里执行这种异步消息模型能避免耗时操作阻塞 Binder 调用线程是播放器稳定运行的基础。另外 NuPlayerDriver 在构造阶段还会初始化状态机初始状态置为 STATE_UNPREPARED。后面每次 setDataSource、prepare、start本质上都是一次状态迁移。明白了这一点你对 MediaPlayer 抛出的 IllegalStateException 会有更直观的理解不是系统乱抛错而是当前状态不允许执行这个操作。3. 创建阶段最容易忽略的三个机制3.1 ALooper/AHandler 消息循环Native 播放器的“主心骨”NuPlayer 之所以高效核心在于它的消息模型。ALooper 和 AHandler 是 Stagefright 框架里的一套极简消息循环设计思路跟 Java Handler 非常像。AHandler 负责定义消息处理逻辑子类重写 onMessageReceivedALooper 则维护一个线程不断从消息队列里取出 AMessage 分发给目标 AHandler。void NuPlayer::setDataSourceAsync(const spIMediaHTTPService httpService, const char *url) { spAMessage msg new AMessage(kWhatSetDataSource, this); msg-setString(url, url); msg-post(); }你可能会问为什么不直接在 Binder 线程里执行 setDataSource因为 setDataSource 往往要解析协议、探测媒体格式、创建解码器等这些操作耗时不可控。如果全在 Binder 线程里做服务端的 Binder 线程池会被占满其他 App 的音视频请求也会被拖垮。使用 ALooper 后Binder 线程只负责快速入队真正的脏活累活在 nuplayer 线程里排队执行这是一种非常务实的隔离策略。3.2 notify 回调链状态是怎么一步一步传回 Java 的创建阶段埋下的回调链在 prepare、播放阶段会频繁触发。我先把这条链完整走一遍。NuPlayerDriver 内部有事件产生时会调用 notify传入事件类型和数据。void NuPlayerDriver::notifyListener(int msg, int ext1, int ext2, const Parcel *obj) { mClient-notify(msg, ext1, ext2, obj); }mClient 是 App 进程传进来的 IMediaPlayerClient 代理这个调用会跨进程跑到 App 端的 Binder 接收线程。App 端 MediaPlayer::notify 收到后把它转交给 JNI 层设置的 listener。再往后是 JNIMediaPlayerListener::notify它通过 JNIEnv 调用 Java 的 postEventFromNative紧接着 Java 层 EventHandler 的 handleMessage 被调度最终触发你注册的 OnPreparedListener、OnErrorListener 等回调。这条链路上每个环节都是异步的所以项目里出现回调比预期晚几十毫秒甚至偶发丢失不要惊讶是消息排队和跨进程传输的正常现象。排查时别只在 Java 层打日志还要看 NuPlayerDriver 有没有发出事件、mClient 有没有收到逐段确认才能定位断点。3.3 mediaserver 死亡监听初始化时的隐性依赖还有一个很隐蔽的机制在创建阶段建立客户端对 mediaserver 进程的死亡监听。Android 多媒体服务集中在 mediaserver 进程里一旦它崩溃或被系统杀掉所有 MediaPlayer 实例都会失去服务端支撑。如果没有死亡通知App 会一直等一个永远不来的回调表现就是播放卡住、界面无响应。Android 的解法是客户端在获取 MediaPlayerService 代理后会给它注册 Binder 死亡回调同时 MediaPlayerService 侧也会维护一个客户端列表。服务端异常死亡时客户端通过死亡回调触发 handle_media_server_death使用 MediaPlayer 的 App 会收到 onError而不是进入死等状态。在低端机或定制 ROM 上这类场景并不少见如果你发现播放器偶发“无任何回调就卡死”先怀疑这条链路是否被 ROM 改动破坏。4. setDataSource 与 prepare初始化的延续还是新阶段4.1 setDataSource 的三种路径与参数选择初始化创建完播放器骨架之后紧接着就是 setDataSource这一步虽然严格说不算“创建”但它是让播放器从空壳变成有实体数据源的关键动作。Java 层有三种最常见的传参方式路径字符串、Context Uri、FileDescriptor。setDataSource(String path) 是最直接的一条路底层把字符串传到 Native 层后走 DataSourceFactory::createFromURL。setDataSource(Context, Uri) 会先判断 Uri 的 scheme。如果是 content:// 协议会通过 ContentResolver.openFileDescriptor 把 Uri 解析成文件描述符再走 FileDescriptor 那条路如果是 file:// 协议则提取路径后走字符串路径。setDataSource(FileDescriptor) 则最底层直接把句柄传给 Native内部会做 dup 复制一份描述符由播放器内部管理生命周期。这里要提醒一下跨 App 传文件的场景。Android 7.0 之后强制要求使用 FileProvider 生成 content:// URI所以你会看到工微信、百度网盘这类 App 在 Logcat 里打印 content://com.tencent.wework.fileprovider/... 这样的路径。MediaPlayer 拿到这种 URI 后最终也会落到文件描述符读取。如果你在自定义 FileProvider 时配置错了 paths或者对方传了带 file:// 协议的 Uri就会在 setDataSource 阶段直接抛出 FileUriExposedException。4.2 prepare 与 prepareAsync阻塞与非阻塞的本质区别数据源设置完播放器处于“有料但没准备”的状态下一步就是 prepare。这里有两个 API很多人只记得“网络视频要用 prepareAsync”但对背后的状态机差异不太清楚。prepare() 是一个阻塞式调用。在 NuPlayerDriver 里prepare 会调用 prepareAsync 发起消息然后用条件变量挂起当前线程等待播放器状态迁移到 PREPARED。status_t NuPlayerDriver::prepare() { status_t err prepareAsync(); if (err ! OK) return err; while (mState ! STATE_PREPARED) { mPreparedCondition.wait(mLock); } return OK; }prepareAsync() 则只负责发出 kWhatPrepare 消息到 NuPlayer 的消息队列函数立刻返回。真正准备数据源、探测格式、创建解码器的工作全在 nuplayer 线程异步完成。主线程可以继续做 UI 刷新等 onPrepared 回调到了再调用 start。理解了这套状态机你就明白为什么在主线程直接 prepare 一个网络资源会 ANR那是在等待跨进程 网络 IO 解码器初始化全部完成不卡才怪。4.3 一条 onPrepared 消息的完整旅行我们来完整走一遍从 prepareAsync 到 onPrepared 的事件链。这个案例非常有代表性能帮你把所有阶段的知识串起来。App 调用 prepareAsyncJava 层通过 JNI 调 Native MediaPlayer::prepareAsync再经 Binder 传到服务端的 NuPlayerDriver。NuPlayerDriver 把状态置为 STATE_PREPARING然后给 NuPlayer 发一条准备消息。NuPlayer 收到消息后在 nuplayer 线程里创建数据源、分辨媒体格式如果是本地文件就创建 MediaExtractor如果是网络流就走 HTTP Live 等协议逻辑然后把数据源交给内部模块做解码准备。所有准备工作完成后NuPlayer 给 NuPlayerDriver 发一个 kWhatSourcePrepared 的消息NuPlayerDriver 将状态切换为 STATE_PREPARED并通过 notifyListener 把 EVENT_PREPARED 发给 App 进程。事件回到 App 端的流程就是我们 3.2 节那条回调链IMediaPlayerClient Binder 回调到 Native MediaPlayer::notify再转给 JNIMediaPlayerListenerJNI 调用 Java 的 postEventFromNative最后 Java 层 EventHandler 分发到你注册的 onPrepared。从用户手指点击播放按钮到 onPrepared 响铃中间经历了两三个进程、四五层消息队列任何一环变慢感知就是“转圈慢、起播慢”。5. 常见问题与排查实录5.1 创建阶段最常踩的五个坑第一个坑FileUriExposedException。应用 targetSdk 大于等于 24 之后直接给 MediaPlayer 传 file:// 协议路径多半会在 setDataSource 崩掉。正确做法是用 FileProvider 转成 content:// 协议或者直接打开 FileInputStream 后调用 setDataSource(FileDescriptor)。第二个坑无 Looper 线程创建 MediaPlayer 导致回调丢失。如果你在后台线程 new MediaPlayer而该线程没有 Looper同时主线程 Looper 又拿不到mEventHandler 会是空后面所有回调都进不来。规避方式是保证创建 MediaPlayer 的线程有 Looper或者自行封装 Handler 接收事件。第三个坑未 prepare 就 start。很多人拿 MediaPlayer 当秒播工具setDataSource 之后立刻 start结果抛 IllegalStateException。前面讲了状态机新播放器实例的状态是 STATE_UNPREPARED此时 start 属于非法操作必须先 prepare 或 prepareAsync。第四个坑创建播放器不复用。频繁 new MediaPlayer 再 release每个实例都要创建 Native 对象和一条 nuplayer 线程对象销毁还要等 Binder 释放服务端资源高频场景极易触发 ANR 或 native 内存抖动。我见过一个直播 App 每收到一帧就 new 一个播放器最后直接把 mediaserver 干崩了。第五个坑忽略 mediaserver 重启。在系统级 App 或车机平台mediaserver 可能会被看门狗杀掉重启。你创建在手里的 MediaPlayer 对象已经失去服务端支撑如果不监听死亡回调或及时重建播放会一直挂在准备阶段。针对这种情况建议对播放器做生命周期兜底检测到服务端重建时统一恢复现场。5.2 从 log 和 dumpsys 快速定位问题初始化问题通常靠 Logcat 就能定位。我建议抓日志时同时抓 MediaPlayer、MediaPlayerService、NuPlayer 三个 TAG这三个足够覆盖创建到准备的全过程。如果看到 E/MediaPlayerService: create failed 或者 Binder 获取失败优先怀疑 mediaserver 进程没有正常启动。执行 adb shell ps -A | grep media 确认进程存在。如果存在再通过 adb shell dumpsys media.player 查看当前已经创建的播放器实例数量和状态这个命令能列出每个播放器的创建时间、状态、数据源排查泄漏特别好用。如果遇到 Native 崩溃backtrace 里能直接看到 NuPlayerDriver::setDataSource、NuPlayer::setDataSourceAsync 之类的符号说明问题在服务端解析数据源阶段这时候要重点检查文件是否存在、权限是否够、格式是否被系统解析器支持。5.3 多实例场景与内存开销提醒最后说一个创建阶段容易被忽视的性能问题每个 MediaPlayer 实例在 Native 层不是“一个对象”这么简单。它有独立的 ALooper 线程、状态机、事件分发通道再加上解码器、渲染器、音视频缓冲一个播放器的内存占用轻松到几 MB。如果项目需要同时播放多个音轨比如视频里的背景音乐、语音消息、提示音同时响建议评估 AudioTrack 直接播放短音频而不是风格统一地全部交给 MediaPlayer。另外在调用 release 时也要规范。Java 层 release 会触发 native_release最终释放 Native 播放器资源。如果只把 Java 引用置空而不调用 releaseNative 层播放器会继续存活直到 GC 触发 finalize这个时机完全不可控。做播放器资源管理时一定要用 try/finally 或生命周期回调确保 release 执行。我在实际调试中体会最深的一点是MediaPlayer 虽然用起来像普通 Java 对象但它从 new 的那一刻起就牵动着跨进程服务、线程调度、事件分发和状态机迁移。很多人遇到的播放问题根源根本不在播放环节而在创建和初始化阶段埋下的隐患。把这个阶段彻底啃透后面的播放路线、音视频同步、音效处理这些内容学起来会顺畅很多。如果这篇对你有点帮助后面我会继续写 NuPlayer 的播放流程和解码器协作欢迎持续关注。
返回列表