资讯动态

Android MediaPlayer初始化全解析:从Java层到Native跨进程创建链路

发布时间:2026/10/5 9:01:58 来源:尧图企业网站定制
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状态机的控制逻辑。服务层MediaPlayerServiceframeworks/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, MapString, String headers, ListHttpCookie cookies) throws IOException, IllegalArgumentException, SecurityException, IllegalStateException { // ...权限检查 native_setup(new WeakReferenceMediaPlayer(this), context.getPackageName(), context.getOpPackageName(), context.getApplicationInfo().targetSdkVersion, attributes); // ...设置数据源 }看着这行代码你可能已经发现了关键信息native_setup传了一个new WeakReferenceMediaPlayer(this)进去。为什么要用WeakReference而不是直接传this因为JNI层如果持有了Java对象的强引用就会导致Java对象无法被GC回收造成内存泄漏。使用弱引用可以保证Java层的MediaPlayer实例在没有强引用时能被正常回收JNI层只是在需要回调时临时提升这个弱引用为强引用。2.2 setDataSource的多种重载与内部逻辑setDataSource有多种重载形式它们是MediaPlayer从“壳”走向“实”的入口重载形式使用场景内部流程说明setDataSource(String path)本地文件路径最终调用setDataSource(path, null, null)→ native层通过路径创建FileSourcesetDataSource(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状态强行调用setDataSourceNative层会报状态错误。这是因为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传入的其实是WeakReferenceMediaPlayer这个签名为什么写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对象 spMediaPlayer 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引用而不调用releaseNative层的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 spAudioAttributes 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 spMediaPlayerListener 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 获取MediaPlayerServiceBinder与defaultServiceManager真正创建服务端连接的时刻是在MediaPlayer::setDataSource首次调用时。我们先看这个函数status_t MediaPlayer::setDataSource( const spIMediaHTTPService httpService, const char *url, const KeyedVectorString8, String8 *headers) { ALOGV(setDataSource(%s), url); status_t err UNKNOWN_ERROR; const spIMediaPlayerService service(getMediaPlayerService()); if (service ! 0) { // 跨进程调用MediaPlayerService::create spIMediaPlayer 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 spIMediaPlayerService MediaPlayer::getMediaPlayerService() { Mutex::Autolock _l(sServiceLock); if (sMediaPlayerService 0) { // 获取系统默认的ServiceManager spIServiceManager sm defaultServiceManager(); spIBinder 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_castIMediaPlayerService(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.cppspIMediaPlayer MediaPlayerService::create( const spIMediaPlayerClient client, int audioSessionId, const spAudioAttributes audioAttributes, int pid, int uid) { pid_t callerPid (pid -1) ? IPCThreadState::self()-getCallingPid() : pid; uid_t callerUid (uid -1) ? IPCThreadState::self()-getCallingUid() : uid; spClient c new Client(this, client, callerPid, callerUid, audioSessionId, audioAttributes); ALOGV(create player for uid %d, pid %d, callerUid, callerPid); // 根据数据源类型创建真正的player实体 spMediaPlayerBase p c-createPlayer(); if (p NULL) { return NULL; } c-setPlayer(p); return c; }这个create函数最关键的产出物是两样东西Client对象它是BnMediaPlayer的实现类也实现了IMediaPlayerClient接口作为回调接收端。Client代表一个播放器会话的“活页夹”负责分发控制命令到真正的Player同时把Player产生的事件通过Binder回调给客户端进程。MediaPlayerBase对象这是真正的播放引擎抽象基类实际干活的可能是NuPlayerDriver现代Android默认、StagefrightPlayer老版本、TestPlayer等。createPlayer()内部通过MediaPlayerFactory根据数据源类型和MIME选择合适的Player实现。这里值得说一下MediaPlayerFactory。它本质上是一个注册制工厂typedef MediaPlayerBase* (*PlayerFactory)(const spMediaPlayerBase::Listener listener, const spIMediaPlayerClient client, const spMediaPlayerService service, pid_t pid, uid_t uid, const spAudioAttributes audioAttributes);系统里可以注册多个工厂按优先级匹配。比如视频播放走NuPlayer某些特殊音频格式可能注册了对应的高优先级解码器。MediaPlayerFactory::createPlayer会遍历所有已注册的工厂返回第一个能成功创建Player实例的工厂创建出来的对象。从Android 5.0Lollipop开始默认的Player实现就是NuPlayerNuPlayerDriver。NuPlayer基于ALooperAHandler的异步消息模型构建底层再挂载MediaCodec、AudioSink、Extractor等模块。如果之后你想深入分析播放流程比如prepare怎么异步完成的、数据管道的建立顺序就要看NuPlayerDriver的实现了。Client构造函数里还有一段内容值得关注——音频会话IDaudioSessionId的处理MediaPlayerService::Client::Client(const spMediaPlayerService service, const spIMediaPlayerClient client, pid_t pid, uid_t uid, int audioSessionId, const spAudioAttributes 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_setup4JNI层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代理即Client7服务端MediaPlayerService::create()new Client通过MediaPlayerFactory创建PlayerClient MediaPlayerBase8Native层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又是在什么时候建立的。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑