• Android MediaPlayer IllegalStateException源码分析定位


    1.源码分析,定位报错点

    最近发现Bugly上存在一个长期的bug:

    java.lang.IllegalStateExceptionandroid.media.MediaPlayer._start(Native Method)
    android.media.MediaPlayer.startImpl(MediaPlayer.java:1373)
    android.media.MediaPlayer.start(MediaPlayer.java:1345)
    org.appplay.lib.SoundPlayer$1.onPrepared(SoundPlayer.java:46)
    android.media.MediaPlayer$EventHandler.handleMessage(MediaPlayer.java:3453)
    android.os.Handler.dispatchMessage(Handler.java:106)
    android.os.Looper.loop(Looper.java:224)
    android.app.ActivityThread.main(ActivityThread.java:7087)
    java.lang.reflect.Method.invoke(Native Method)
    com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:604)
    com.android.internal.os.ZygoteInit.main(ZygoteInit.java:928)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    从上面的报错看,只能根据源码走向,推断报错点了。

    MediaPlayer#startImpl()方法开始追踪:

    frameworks/base/media/java/android/media/MediaPlayer.java:

        private void startImpl() {
            //...
            _start();// 通过jni 调用native 层的开始方法
        }
    
    • 1
    • 2
    • 3
    • 4

    接下来找到java 中_start() 对应的native层方法 ,media 对应的jni 类一般都是放在frameworks/base/media/jni中。

    frameworks/base/media/jni/android_media_MediaPlayer.cpp:

    static const JNINativeMethod gMethods[] = {
          
          {"_start",              "()V",                              (void *)android_media_MediaPlayer_start}
    }
    
    • 1
    • 2
    • 3
    • 4

    在该类中全局检索_start , 找对对应的jni 方法android_media_MediaPlayer_start()

    接下来看下该方法:

    static void
    android_media_MediaPlayer_start(JNIEnv *env, jobject thiz)
    {
        ALOGV("start");
        sp mp = getMediaPlayer(env, thiz);
        if (mp == NULL ) {
            //关键点:mp 为空,则抛出异常
            jniThrowException(env, "java/lang/IllegalStateException", NULL);
            return;
        }
        //.......
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    从上面代码来看,获取MediaPlayer* 指针对象为空,导致抛出IllegalStateException异常。

    接下来看下该方法:

    static sp getMediaPlayer(JNIEnv* env, jobject thiz)
    {
        Mutex::Autolock l(sLock);
        MediaPlayer* const p = (MediaPlayer*)env->GetLongField(thiz, fields.context);
        return sp(p);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    jobject 对象是java层中MediaPlayer类对象,env->GetLongField(thiz, fields.context)是反射获取MediaPlayer 类中某个属性,也就是该属性为空导致异常的。

    全局检索下field.context 相关信息:

    struct fields_t {
        jfieldID    context;
        //......
    };
    
    
    • 1
    • 2
    • 3
    • 4
    • 5

    fields_t 是c++中结构体,类似java 中实体类,专门存储不同数据类型的数据。context 又是哪个属性的field呢?

    全局检索下,找到该方法:

    static void
    android_media_MediaPlayer_native_init(JNIEnv *env)
    {
        jclass clazz;
        clazz = env->FindClass("android/media/MediaPlayer");
        if (clazz == NULL) {
            return;
        }
        //反射获取MediaPlayer中mNativeContext
        fields.context = env->GetFieldID(clazz, "mNativeContext", "J");
        if (fields.context == NULL) {
            return;
        }    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    filelds.context 是mNativeContext的field ,即反射获取MediaPlayer中mNativeContext属性为空,导致抛出异常。

    在Java 层MediaPlayer 中检索mNativeContext的赋值操作并没有找到。

    在native 层检索相关信息,找到该方法:

    static sp setMediaPlayer(JNIEnv* env, jobject thiz, const sp& player)
    {
        Mutex::Autolock l(sLock);
        sp old = (MediaPlayer*)env->GetLongField(thiz, fields.context);
        if (player.get()) {
            player->incStrong((void*)setMediaPlayer);
        }
        if (old != 0) {
            old->decStrong((void*)setMediaPlayer);
        }
        // context 进行赋值操作
        env->SetLongField(thiz, fields.context, (jlong)player.get());
        return old;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    进一步检索发现有两处调用setMediaPlayer()

    第一处是:

    static void
    android_media_MediaPlayer_native_setup(JNIEnv *env, jobject thiz, jobject weak_this,
                                           jobject jAttributionSource)
    {
        ALOGV("native_setup");
       //.....
        //构建出MediaPlayer*
        sp mp = sp::make(attributionSource);
        if (mp == NULL) {
            jniThrowException(env, "java/lang/RuntimeException", "Out of memory");
            return;
        }
        //...........
        // 将MediaPlayer* 设置给mNativeContext
        setMediaPlayer(env, thiz, mp);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    上面代码是java 层MediaPlayer 初始化调用的native 层方法,会对mNativeContext进行赋值操作。该上述逻辑,并不是关键。

    第二处是:

    static void
    android_media_MediaPlayer_release(JNIEnv *env, jobject thiz)
    {
        ALOGV("release");
        decVideoSurfaceRef(env, thiz);
        // 进行释放操作
        sp mp = setMediaPlayer(env, thiz, 0);
        //....
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    在上面的代码中会对mNativeContext释放为空的操作,因此判断,问题是调用了该方法导致的。

    接下来,查看下该方法被哪里调用。

    static const JNINativeMethod gMethods[] = {
     {"_release",            "()V",                              (void *)android_media_MediaPlayer_release}
    }
    
    • 1
    • 2
    • 3

    在java 层的MediaPlayer中release()中会调用到native层的_release()

    基本上可以推断出,导致该问题是原因是调用release()释放资源后,又调用start()导致异常。

    2.解决方式

    查看下项目中MediaPlayer相关逻辑,会首先调用stop有关的操作(释放old MediaPlayer 资源),接着new MediaPlayer,调用prepareAsync()异步加载,在OnPreparedListener回调器中又调用start()。这里可能是短时间多次播放,存在覆盖问题,导致错误调用已经释放的MediaPlayer,又调用start()。

    解决办法如下:

        public void playByUrl(String url, boolean isLoop)
        {
            // 如果在播放,先停止
            stop();
            mMediaPlayer = new MediaPlayer();
            try {
                //.......
                mMediaPlayer.prepareAsync();
                mMediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener(){
                    @Override
                    public void onPrepared(MediaPlayer mp) {
    
                        if (mp!=mMediaPlayer){
                            // 因异加载的回调,可能导致短时间多个MediaPlayer覆盖问题
                            return ;
                        }
                        // 装载完毕 开始播放流媒体
                        if (mMediaPlayer != null) {
                            mMediaPlayer.start();
                            if (!isLoadOk) {
                                isLoadOk = true;
                            }
                        }
                    }
                });
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
  • 相关阅读:
    微信小程序开发---自定义底部tabBar
    SpringBoot启动顺序
    【微信小程序】常用组件基本使用(viewscroll-viewswiper、textrich-text、buttonimage)
    mindspore训练一段时间后,报内存不够错误
    Windows 10 数据恢复与预防数据丢失指南
    16. 最接近的三数之和(javascript)16. 3Sum Closest
    逍遥自在学C语言 | 变量、常量与数据类型
    2.3_7生产者-消费者问题
    巧家蒙姑文笔社区:和谐社区育新风 用心铺就团结路
    buildadmin+tp8表格操作(4) Table组件,baTable类和 elementplus中的属性关系
  • 原文地址:https://blog.csdn.net/hexingen/article/details/126405410