☰
Android MediaPlayer初始化全解析:从Java层到Native跨进程创建链路
2026/10/5 9:01:51 网站建设 项目流程

Android的MediaPlayer从API Level 1就存在了,可以说是Android多媒体框架里最老牌的播放器接口。但这个类的复杂度,远远超过它的API表象。Java层一句MediaPlayer mp = new MediaPlayer(),背后从JNI到Native再到Binder、从进程内对象到跨进程的MediaPlayerService,牵扯到好几百行的初始化逻辑。如果你只会调用setDataSource、prepare、start,而不知道这些调用到底经过了哪些层、哪些对象、哪些跨进程通道,那一旦遇到疑难问题——比如播放卡顿、回调丢失、资源冲突、偶现ANR——排查起来基本就是抓瞎。

这篇文章我会基于AOSP源码,从new MediaPlayer()开始,把初始化和创建阶段完整的调用链拆开给你看:Java层做了什么、JNI层怎么转接、Native层如何与MediaPlayerService建立跨进程连接、各个关键对象的职责和生命周期是什么。搞清楚这些,你不仅对MediaPlayer本身会有全面认识,对Android整个C/S架构(Client-Server架构)的理解也会上一个台阶。


1. 整体架构定位:MediaPlayer在Android多媒体体系中处在什么位置

1.1 三层结构与C/S架构的基本盘

先不急着看代码,我们得先把MediaPlayer在整个Android多媒体体系里的位置摆清楚。MediaPlayer不是一个孤立的类,它只是客户端视角的“门面”。

从整体架构上看,MediaPlayer涉及三层:

  • Java层:android.media.MediaPlayer,我们平时写业务代码直接调用的就是这个。它通过JNI调用Native层。
  • Native框架层:libmedia.so中的MediaPlayer类(frameworks/av/media/libmedia/MediaPlayer.cpp),负责与MediaPlayerService通信,同时实现Player状态机的控制逻辑。
  • 服务层:MediaPlayerService(frameworks/av/media/mediaserver/目录下),运行在独立的mediaserver进程中,通过Binder接收客户端的IPC请求,实际创建并管理真正干活的播放引擎——Player。

这本质上是典型的C/S架构。MediaPlayer(客户端)通过Binder与远端的MediaPlayerService(服务端)建立连接,由服务端进程内的Player实例真正执行音频解码、渲染输出等重活。为什么要这么设计?原因很直接:保活策略和稳定性隔离。

比如Android中音频焦点、音频路由、音量等策略,都是系统级的,不可能让每个App进程自己管。把播放的核心逻辑放到独立的mediaserver进程,App进程崩溃了,服务端的资源还有机会回收;播放引擎出问题,也不至于把App进程拖死(当然有些底层问题还是会Crash,但至少架构上是隔离的)。早期的Android版本中mediaserver还承担了音频、视频的录制,后面升级到Android 8.0后拆成了audioserver和mediaserver,也是同样的设计思想——按功能域隔离系统进程,避免一个模块的故障影响全局。

1.2 初始化阶段在整个生命周期中的分量

MediaPlayer是有严格状态机约束的。它的状态包括Idle、Initialized、Preparing、Prepared、Started、Paused、Stopped、PlaybackCompleted、Error、End等。官方文档给过一张状态图,我相信搞过多媒体开发的人多少都见过。

我要强调的一点是:初始化阶段是状态机能否顺利运转的前提,也是很多偶发问题的根源。

比如官方状态机文档规定了:new MediaPlayer()创建出来的实例处于Idle状态。但很多人不知道,在调用reset()之后,对象也回到Idle状态;而在Idle状态下如果调用了release(),那么对象进入End状态,这个实例就废了,不能再做任何操作。

初始化和创建的源码,就是在为整个状态机建立“地基”:

  • 注册JNI方法,让Java层的调用能找到Native入口;
  • 构造Native层的MediaPlayer对象,建立监听器回调的通道;
  • 通过Binder获取MediaPlayerService,并创建服务端的Client实例;
  • 初始化音频属性、唤醒锁、事件回调等基础配置。

只有把这些都搞明白,你才能理解为什么某些调用必须在prepare之前做,某些调用必须在prepare之后做,以及各种“莫名奇妙”的报错其实是哪里抛出来的。


2. Java层的出生:new MediaPlayer()到底做了什么

2.1 构造方法、静态初始化与native_init

在Java层,new MediaPlayer()并不神秘。MediaPlayer的构造方法逻辑很简单,真正在做事情的是它的静态初始化和JNI层注册。

// frameworks/base/media/java/android/media/MediaPlayer.java (精简) public class MediaPlayer implements Player, VolumeAutomation, SubtitleController.Listener { static { System.loadLibrary("media_jni"); native_init(); } public MediaPlayer() { } private MediaPlayer(boolean fromXml) { // 供XML布局使用,内部会设置一些默认属性 } private native final void native_setup(Object mediaplayer_this, String packageName, String opPackageName, int targetSdkVersion, AudioAttributes attributes); }

注意看static{}块里的System.loadLibrary("media_jni")。这行代码加载的是libmedia_jni.so,里面包含了Java层MediaPlayer所有native方法的真实实现。紧接着的native_init()是一个native方法,在静态初始化阶段就被调用,作用是在JNI层做全局初始化——具体来说是register_android_media_MediaPlayer,把Java层的native方法跟JNI函数一一注册绑定。

这里有个值得记住的细节:static{}代码块只会在类被首次加载时执行一次。换句话说,native_init()在整个进程生命周期中最多执行一次。如果你在同一个进程里创建了100个MediaPlayer实例,JNI方法的注册和Native侧的全局初始化只需要做一遍。

至于构造函数,默认的public MediaPlayer()几乎什么都没干——没有调用任何native方法。真正驱动系统干活的反而是private的native_setup,而它并不是在构造函数里被调用的。这就引出下一个问题:什么时候调用native_setup?答案是:在setDataSource中调用。

等等,这里有个容易混淆的点。标题叫“初始化和创建”,但你可能会在源码里看到:new MediaPlayer()之后直接native_setup并没有立即执行。真正的初始化时序是——先创建好Java对象(搭好一个壳),然后等调用setDataSource时,系统才动态加载Native资源并建立与MediaPlayerService的连接。

源码如下:

public void setDataSource(@NonNull Context context, @NonNull Uri uri, Map<String, String> headers, List<HttpCookie> cookies) throws IOException, IllegalArgumentException, SecurityException, IllegalStateException { // ...权限检查 native_setup(new WeakReference<MediaPlayer>(this), context.getPackageName(), context.getOpPackageName(), context.getApplicationInfo().targetSdkVersion, attributes); // ...设置数据源 }

看着这行代码,你可能已经发现了关键信息:native_setup传了一个new WeakReference<MediaPlayer>(this)进去。为什么要用WeakReference而不是直接传this?因为JNI层如果持有了Java对象的强引用,就会导致Java对象无法被GC回收,造成内存泄漏。使用弱引用,可以保证Java层的MediaPlayer实例在没有强引用时能被正常回收,JNI层只是在需要回调时临时提升这个弱引用为强引用。

2.2 setDataSource的多种重载与内部逻辑

setDataSource有多种重载形式,它们是MediaPlayer从“壳”走向“实”的入口:

重载形式使用场景内部流程说明
setDataSource(String path)本地文件路径最终调用setDataSource(path, null, null)→ native层通过路径创建FileSource
setDataSource(Context, Uri, Map, List)ContentProvider或网络URI会解析URL、ContentResolver打开文件描述符,再传给Native层
setDataSource(FileDescriptor, long offset, long length)已打开的文件描述符底层直接使用dup()复制文件描述符
setDataSource(MediaDataSource)自定义数据源通过JNI把MediaDataSource包装成Native侧的DataSource

不管用哪种重载,最终都会在合适时机触发native_setup,然后调用NativeJNI对应的setDataSource方法,比如JNI层的android_media_MediaPlayer_setDataSourceFD。

这里还要注意一个状态机陷阱:如果你在Idle状态之外(比如第一次prepare之后)再次调用setDataSource,会抛IllegalStateException。源码里有一行注释写得很明白:

// Note that we do not check the state at this point. The native code will // handle the state change for us.

如果你在Prepared状态强行调用setDataSource,Native层会报状态错误。这是因为MediaPlayer的状态机是Native层强制管理的,Java层的注释其实是在告诉你:别担心,底层会给你返回错误。


3. JNI层的桥接:从Java方法到C++函数

3.1 注册过程与动态绑定

Java层的native方法声明之后,JNI层必须做注册。MediaPlayer的JNI实现位于frameworks/base/media/jni/android_media_MediaPlayer.cpp。

JNI注册有两种方式:

  • 静态注册:按照Java_android_media_MediaPlayer_native_setup这种命名规则,JVM直接通过函数名找到对应C++函数。
  • 动态注册:在JNI_OnLoad里通过RegisterNatives手动指定Java方法和C++函数的映射关系。

Android的MediaPlayer JNI层使用的是动态注册方式。它的注册代码大致长这样:

static const JNINativeMethod gMethods[] = { // Java方法名, 方法签名, C++实现函数指针 { "native_setup", "(Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String;ILandroid/media/AudioAttributes;)V", (void *)android_media_MediaPlayer_native_setup }, { "native_init", "()V", (void *)android_media_MediaPlayer_native_init }, // ... 还有很多 }; int register_android_media_MediaPlayer(JNIEnv *env) { return AndroidRuntime::registerNativeMethods(env, "android/media/MediaPlayer", gMethods, NELEM(gMethods)); }

动态注册的好处是灵活、性能更好(省去方法名解析),而且可以在注册时做更多参数校验。JNI方法签名里的Ljava/lang/Object;代表Java层的Object对象——但你回头看Java代码,native_setup传入的其实是WeakReference<MediaPlayer>,这个签名为什么写Object?其实在JNI层它会通过GetObjectClass检查实际类型。

3.2 new WeakReference在JNI层如何转成Native对象

这里最有意思的是JNI层的android_media_MediaPlayer_native_setup函数。它做的事情可以拆成四步:

static void android_media_MediaPlayer_native_setup(JNIEnv *env, jobject thiz, jobject weak_this, jstring packageName, jstring opPackageName, jint targetSdkVersion, jobject jAttributes) { // 1. 解析包名和操作包名 const char* packageNameStr = env->GetStringUTFChars(packageName, NULL); const char* opPackageNameStr = env->GetStringUTFChars(opPackageName, NULL); // 2. 创建Native层的MediaPlayer对象 sp<MediaPlayer> mp = new MediaPlayer(packageNameStr, opPackageNameStr, targetSdkVersion, attributes); // 3. 在Native层注册Java对象的弱引用,用于回调 mp->initAsMediaPlayer(env, thiz, weak_this, targetSdkVersion); // 4. 把Native对象的指针保存到Java层的mNativeContext字段中 setMediaPlayer(env, thiz, mp); }

重点看第4步。setMediaPlayer其实是在Java层的MediaPlayer对象里保存了一个long类型的mNativeContext字段,这个字段存的是Native层MediaPlayer对象的指针地址。以后Java层的每个native方法调用(比如native_start()、native_pause()),JNI层都会先从mNativeContext取回这个指针,再调用对应C++方法。

这是一个非常经典的模式:Java对象与Native对象通过一个long字段互相绑定。理解了这一点,你就知道为什么官方文档强调MediaPlayer实例必须通过release()释放——如果你只丢掉Java引用而不调用release,Native层的MediaPlayer对象和它持有的播放器资源根本不会被自动回收。

还有一个细节:JNI层在创建Native对象时,会把WeakReference本身保存到Java侧(通过setMediaPlayer),同时也会在Native侧保存一个Java层的WeakReference。这个WeakReference会在后续事件回调时被使用:当服务端有事件(如播放完成、错误、缓冲更新)需要通知Java层时,JNI层通过JNIEnv找到对应的Java对象,再回调Java的onPrepared、onCompletion等方法。


4. Native层创建与MediaPlayerService的跨进程连接

4.1 Native MediaPlayer构造与回调通道建立

Native层的MediaPlayer构造函数位于frameworks/av/media/libmedia/MediaPlayer.cpp。它的构造函数本身不复杂,核心职责是初始化状态变量:

MediaPlayer::MediaPlayer(const char *packageName, const char *opPackageName, int targetSdkVersion, const sp<AudioAttributes>& attributes) : mStatus(NO_INIT), mState(MEDIA_PLAYER_IDLE), mCurrentPosition(-1), mSeekPosition(-1), mCurrentSubtitleTrackIndex(-1), mAudioAttributes(attributes), mTargetSdkVersion(targetSdkVersion), mPackageName(packageName ? packageName : ""), mOpPackageName(opPackageName ? opPackageName : ""), mUid(-1), mTimeSource(NULL), mActiveAudioTrackId(-1) { ALOGV("constructor"); // setListener在这里先提供一个空的listener setListener(new MediaPlayerListener()); }

看到mState(MEDIA_PLAYER_IDLE)没有?Native层的状态机从一开始就是IDLE,这跟Java层的逻辑是对应的。构造函数最后创建的MediaPlayerListener是一个空实现,后续会在setDataSource流程中替换成真正的监听器。

关键点来了。MediaPlayer::setListener里有一段非常重要的逻辑:

status_t MediaPlayer::setListener(const sp<MediaPlayerListener>& listener) { // 会同时设置mListener并检查当前状态 { AutoMutex _l(mLock); mListener = listener; } // 如果已经连接了MediaPlayerService,则更新服务端的listener if (mPlayer != NULL) { mPlayer->setListener(listener); } return NO_ERROR; }

这里涉及到一个细微但关键的机制:Client端存在两处回调监听关系。一处是Java层和Native层之间的(通过WeakReference + JNI回调),另一处是Native层与MediaPlayerService服务端之间的(通过Binder回调接口IMediaPlayerClient)。mPlayer指向的是服务端返回的一个Binder代理对象(IMediaPlayer),通过它能控制播放动作、也能注册回调通道。

如果你想深入理解回调链路,可以这样记:

  • Java层MediaPlayer→ JNI回调 → Native层MediaPlayer(mListener)
  • Native层MediaPlayer→ Binder callback → 服务端MediaPlayerService::Client
  • 服务端事件源(解码器、渲染器)→Client::notify()→ Binder回调 → Native → JNI → Java 回调方法

这条链路非常长,但它是MediaPlayer一切事件通知(onPrepared、onCompletion、onError、onInfo、onBufferingUpdate)的主干道。

4.2 获取MediaPlayerService:Binder与defaultServiceManager

真正创建服务端连接的时刻,是在MediaPlayer::setDataSource首次调用时。我们先看这个函数:

status_t MediaPlayer::setDataSource( const sp<IMediaHTTPService> &httpService, const char *url, const KeyedVector<String8, String8> *headers) { ALOGV("setDataSource(%s)", url); status_t err = UNKNOWN_ERROR; const sp<IMediaPlayerService> service(getMediaPlayerService()); if (service != 0) { // 跨进程调用MediaPlayerService::create sp<IMediaPlayer> player(service->create(this, mAudioAttributes)); if (player != 0) { // 创建成功后再调用setDataSource err = player->setDataSource(httpService, url, headers); if (err == NO_ERROR) { mPlayer = player; } } } return err; }

第一行getMediaPlayerService()是整个初始化链路里最关键的环节。它内部逻辑大致如下:

/*static*/ const sp<IMediaPlayerService>& MediaPlayer::getMediaPlayerService() { Mutex::Autolock _l(sServiceLock); if (sMediaPlayerService == 0) { // 获取系统默认的ServiceManager sp<IServiceManager> sm = defaultServiceManager(); sp<IBinder> binder; do { // 尝试获取"media.player"服务 binder = sm->getService(String16("media.player")); if (binder != 0) { break; } ALOGW("MediaPlayerService not published, waiting..."); usleep(500000); // 等待0.5秒 } while (true); // 如果有新服务注册,需要做死亡通知处理 if (sDeathNotifier == NULL) { sDeathNotifier = new DeathNotifier(); } binder->linkToDeath(sDeathNotifier); // 把IBinder转为IMediaPlayerService接口 sMediaPlayerService = interface_cast<IMediaPlayerService>(binder); } return sMediaPlayerService; }

这里有个很容易被忽略的细节:getMediaPlayerService里有一个while循环等待机制。如果mediaserver进程还没来得及注册media.player服务,这里会每隔0.5秒重试一次,直到成功。这在系统启动早期或者mediaserver异常重启时会出现。你可能在日志里看到过MediaPlayerService not published, waiting...这种警告,就是这段代码打的。

这个等待循环在极端情况下会引起ANR。设想一个场景:mediaserver进程因为底层bug反复crash,那么App每次播放都会卡在这个循环里。遇到这种问题,你得去查socket通信、Binder驱动或者mediaserver崩溃的原因,单纯换一个setDataSource方案是解决不了问题的。

4.3 MediaPlayerService::create、Client与Player三件套

当Native层拿到了IMediaPlayerService这个Binder代理之后,下一步就是调用service->create(this, mAudioAttributes)。这个调用会发起一次Binder IPC,最终在mediaserver进程内执行BnMediaPlayerService::onCreate的对应实现。

服务端的create函数长这样(frameworks/av/media/mediaserver/MediaPlayerService.cpp):

sp<IMediaPlayer> MediaPlayerService::create( const sp<IMediaPlayerClient>& client, int audioSessionId, const sp<AudioAttributes>& audioAttributes, int pid, int uid) { pid_t callerPid = (pid == -1) ? IPCThreadState::self()->getCallingPid() : pid; uid_t callerUid = (uid == -1) ? IPCThreadState::self()->getCallingUid() : uid; sp<Client> c = new Client(this, client, callerPid, callerUid, audioSessionId, audioAttributes); ALOGV("create player for uid %d, pid %d", callerUid, callerPid); // 根据数据源类型创建真正的player实体 sp<MediaPlayerBase> p = c->createPlayer(); if (p == NULL) { return NULL; } c->setPlayer(p); return c; }

这个create函数最关键的产出物是两样东西:

  1. Client对象:它是BnMediaPlayer的实现类,也实现了IMediaPlayerClient接口(作为回调接收端)。Client代表一个播放器会话的“活页夹”,负责分发控制命令到真正的Player,同时把Player产生的事件通过Binder回调给客户端进程。
  2. MediaPlayerBase对象:这是真正的播放引擎抽象基类,实际干活的可能是NuPlayerDriver(现代Android默认)、StagefrightPlayer(老版本)、TestPlayer等。createPlayer()内部通过MediaPlayerFactory根据数据源类型和MIME选择合适的Player实现。

这里值得说一下MediaPlayerFactory。它本质上是一个注册制工厂:

typedef MediaPlayerBase* (*PlayerFactory)(const sp<MediaPlayerBase::Listener>& listener, const sp<IMediaPlayerClient>& client, const sp<MediaPlayerService>& service, pid_t pid, uid_t uid, const sp<AudioAttributes>& audioAttributes);

系统里可以注册多个工厂,按优先级匹配。比如视频播放走NuPlayer,某些特殊音频格式可能注册了对应的高优先级解码器。MediaPlayerFactory::createPlayer会遍历所有已注册的工厂,返回第一个能成功创建Player实例的工厂创建出来的对象。

从Android 5.0(Lollipop)开始,默认的Player实现就是NuPlayer(NuPlayerDriver)。NuPlayer基于ALooper+AHandler的异步消息模型构建,底层再挂载MediaCodec、AudioSink、Extractor等模块。如果之后你想深入分析播放流程(比如prepare怎么异步完成的、数据管道的建立顺序),就要看NuPlayerDriver的实现了。

Client构造函数里还有一段内容值得关注——音频会话ID(audioSessionId)的处理:

MediaPlayerService::Client::Client(const sp<MediaPlayerService>& service, const sp<IMediaPlayerClient>& client, pid_t pid, uid_t uid, int audioSessionId, const sp<AudioAttributes>& audioAttributes) : mAudioSessionId(audioSessionId) { // ... if (mAudioSessionId == AUDIO_SESSION_ALLOCATE) { mAudioSessionId = AudioSystem::newAudioUniqueId(AUDIO_UNIQUE_ID_USE_SESSION); } // ... }

如果调用方没有指定音频会话ID(传AUDIO_SESSION_ALLOCATE,通常是-2),系统会为这个播放器分配一个全局唯一的audio session ID。这个ID后面会被AudioTrack和AudioFlinger使用,用于音频效果处理、音量调节、焦点管理。理解了这一层,你就知道为什么MediaPlayer和AudioTrack可以通过audioSessionId关联起来了。


5. 初始化链路全景图与关键调用顺序梳理

5.1 一条完整的时间线

把前面几章的内容串起来,一次典型的MediaPlayer初始化创建过程大概是这样的:

阶段调用方被调用方核心动作产物
1Java代码new MediaPlayer()创建Java对象,无Native动作Java MediaPlayer实例(Idle态)
2JVMnative_initJNI方法注册/全局初始化JNI环境就绪
3Java代码setDataSource()检查状态、准备数据源触发native_setup
4JNI层android_media_MediaPlayer_native_setupnew Native MediaPlayer,绑定Java弱引用Native MediaPlayer对象
5Native层getMediaPlayerService()从ServiceManager获取media.player服务IMediaPlayerService Binder代理
6Native层service->create()Binder IPC,跨越App进程到mediaserver进程IMediaPlayer Binder代理(即Client)
7服务端MediaPlayerService::create()new Client,通过MediaPlayerFactory创建PlayerClient + MediaPlayerBase
8Native层player->setDataSource()通知服务端真正的Player设置数据源数据源与引擎绑定完成
9Java层prepare()/prepareAsync()异步准备,进入Preparing状态Native引擎开始准备

这9个步骤,就是一次“创建+初始化”的完整生命周期。别看new MediaPlayer()本身只有一行,但后面的6、7两步才是真正意义上的“创建”——它们在服务端创建了播放会话。

5.2 关键对象关系速记

我把初始化阶段涉及的核心对象和责任关系整理成了一个便于记忆的表格:

对象所在进程职责生命周期特点
android.media.MediaPlayerApp进程用户直接使用的API门面由Java GC管理,但必须手动release释放Native资源
android_media_MediaPlayer.cppJNI层App进程Java与Native之间的事件转发随media_jni库加载,全局注册一次
MediaPlayer(libmedia)App进程Native客户端;状态机控制;与服务端通信由sp<>智能指针管理,release后销毁
IMediaPlayerServiceApp进程(Binder代理)MediaPlayerService的客户端代理全局单例,与mediaserver绑定
MediaPlayerService::Clientmediaserver进程播放会话的持有者;控制命令分发;事件回传引用计数归零时自动清理
NuPlayerDrivermediaserver进程真正的播放引擎;调度解码、渲染由Client持有,reset/release时销毁

这样看下来,你会发现一个很有意思的点:App进程里的MediaPlayer更像一个“遥控器”,真正播放的核心逻辑都在mediaserver进程里。这种设计让多种播放器(多个MediaPlayer实例)可以并行工作,也方便系统统一管理资源。


6. 初始化与创建阶段的常见问题排查

6.1 那些年我们踩过的典型坑

翻了老半天源码,不聊几个实战问题,总觉得不过瘾。这里整理一些我在实际开发和上层反馈中遇到过的典型问题,都跟初始化和创建阶段直接相关。

问题一:setDataSource抛IllegalStateException

这个最常见的原因是在错误的时机调用了setDataSource。比如在prepare()之后想换一个视频源,直接再调一次setDataSource就会抛异常。正确的做法是先reset(),再重新setDataSource。但这里有个隐患:reset()会把Native层的播放器状态全部重置,如果你持有的是同一个实例,那reset()之后务必重新走完整的“setDataSource → prepare → start”流程。

还有一种隐蔽的情况:在OnPreparedListener回调里去调用setDataSource。从Java层看这似乎没毛病,但onPrepared回调时Native层可能还处于Preparing → Prepared的收尾阶段,状态没有完全稳定(底层锁还没释放完),此时立即reset()+setDataSource,有概率触发状态机校验失败。稳妥的做法是回调里先post到主线程下一个消息循环,延迟几个毫秒再操作。

问题二:native_setup报错或者无法初始化

如果System.loadLibrary("media_jni")加载失败,那所有MediaPlayer方法都会崩。这类问题多是系统裁剪导致的:某些定制ROM把libmedia_jni.so或libmedia.so从系统镜像里剔除或降级,导致找不到符号。

另一个可能是包名问题。native_setup会需要合法的packageName,如果调用方传入的Context是getApplicationContext(),一般没问题;但如果传入的是Activity,在极端情况下(比如Activity已销毁)包名解析可能异常。我建议在初始化MediaPlayer时,统一使用context.getApplicationContext()获取包名,避免生命周期问题。

问题三:回调迟迟不来,onPrepared一直不触发

这个问题的根源经常不在初始化代码上,而在回调通道的建立顺序上。前面我们分析过,回调链路是“服务端事件 → Binder回调 → Native → JNI → Java”。如果在setDataSource之后、prepareAsync之前,你把原来的OnPreparedListener设置覆盖掉了,就会导致回调丢失。因为prepareAsync发出的那一刻,setOnPreparedListener里会调用native层的setListener更新回调目标。

还有一类情况:设置监听器的时机太晚。假设你在prepareAsync之后才调用setOnPreparedListener,而服务端响应非常快,可能回调已经走完JNI层了,你的listener才设置上去,自然收不到。所以标准做法是:先setOnPreparedListener,再prepareAsync,这个顺序是必须遵守的。

问题四:getMediaPlayerService死循环导致ANR

有些定制的系统中mediaserver进程在开机阶段不稳定,或者被内核杀死后异常重启。那getMediaPlayerService里的while循环可能长时间无法退出,客户端线程会一直阻塞在Binder等待中。如果你在UI线程调了setDataSource,就会看到ANR。

解决办法有两个层面的:

  • 应用层:不要把setDataSource放在UI线程执行,使用子线程或AsyncTask包一层;
  • 系统层:排查mediaserver进程反复crash的原因,比如是否有第三方码率异常的视频流把解码器搞崩了。注意观察/data/tombstones里的crash日志,很多mediaserver崩溃是多媒体解码驱动导致的。

问题五:进程间对象引用导致的Native内存泄漏

MediaPlayer的release时序如果处理不好,很容易造成内存泄漏。比如你在播放过程中直接把Activity finish掉,但忘了在onPause或onStop里调用release(),那么Native层和服务端的Client会一直存活,直到整个App进程被回收。这对于长时间播放的场景(比如音乐播放),影响极其明显。

另外提醒一下:release()之后,这个MediaPlayer实例绝对不能再使用。源码里release()会使mNativeContext置0,如果之后你还调用其他方法,JNI层得到的指针是0或野指针,轻则抛异常,重则Native Crash。这在开发时可以用一个boolean字段记录是否已release,避免误用。

6.2 快速排查速查表

现象可能原因排查方向
setDataSource抛IllegalStateException状态机不在Idle/Initialized态检查是否在Prepared之后重复setDataSource;确认是否在onPrepared回调中立即操作
System.loadLibrary失败ROM裁剪或架构不匹配确认libmedia_jni.so是否存在;查看/system/lib(64)下符号
onPrepared不回调监听器设置顺序错误确认顺序:setOnPreparedListener → prepareAsync
初始化时线程阻塞/ANRmediaserver服务不可用`adb shell ps -A
内存持续增长release未调用使用LeakCanary或MAT分析MediaPlayer引用链
日志反复出现MediaPlayerService not publishedmediaserver启动异常或反复crash检查tombstones、logcat中mediaserver崩溃堆栈

7. 一些建议:阅读源码的思路和调试方法

如果你打算深入读MediaPlayer源码,我给几条个人建议:

第一,善用callback和state两个维度去理解代码。MediaPlayer源码本质上是“状态机 + 异步事件回调”的经典教材。你盯着某个方法看,永远看不出名堂;你得顺着“状态迁移”这条轴,把每个方法在哪个状态下可调用、调用后状态变成什么、会触发哪些回调,全部在脑子里过一遍。

第二,名称即注释。AOSP的源码命名非常规范。native_setup、initAsMediaPlayer、getMediaPlayerService、MediaPlayerFactory::createPlayer,每一步的名称都在告诉你它在干什么。我读源码的习惯是先把所有方法名、类名、变量名过一遍,画出调用关系图,再逐行细读。

第三,JNI层是很好的“缺口”。Java层和Native层的边界经常是问题的高发区。出问题时,先在JNI层加日志,看调用有没有到达Native;再到Native层加日志,看Binder有没有返回;最后到服务端加日志,看Player有没有收到。逐层缩小范围,比瞎猜有效得多。

第四,善用现有的调试工具。Android Studio的Profiler可以看进程CPU和内存,adb shell dumpsys media.player可以查看MediaPlayerService里的活跃会话和状态。很多时候,初始化问题通过dumpsys就能定位个大概。


这个系列的第一篇就到这里。通过这篇初始化与创建的分析,你应该已经能回答这些问题:Java层的MediaPlayer是不是真正的播放器?初始化阶段究竟建立了哪些通道?服务端的Client和Player什么时候创建?如果你能不看文章把这几个问题答清楚,说明架构脉络已经在你脑子里了。

接下来可以深入到prepare流程,看异步准备的完整调用链——那才是MediaPlayer真正开始运转的起点。到时候我们再看NuPlayer是怎么响应prepare的、Extractor是怎么打开的、AudioSink又是在什么时候建立的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询