返回

解决Android ViewRootImpl.performTraversals空指针异常

Android

Android ViewRootImpl.performTraversals 空指针异常深度解析与解决方案

你是不是也被用户反馈的 android.view.ViewRootImpl.performTraversals 空指针异常搞得焦头烂额?别担心,这个问题很常见,尤其是在涉及 MediaPlayer 进行视频播放的时候。这篇文章将带你深入了解这个问题,并提供多种解决方案。

一、 问题:ViewRootImpl.performTraversals 崩溃

用户反馈应用在播放视频几秒后崩溃,错误日志如下:

java.lang.NullPointerException
at android.os.Parcel.readException(Parcel.java:1333)
at android.os.Parcel.readException(Parcel.java:1281)
at android.view.IWindowSession$Stub$Proxy.relayout(IWindowSession.java:634)
at android.view.ViewRootImpl.relayoutWindow(ViewRootImpl.java:3751)
at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:1321)
at android.view.ViewRootImpl.handleMessage(ViewRootImpl.java:2587)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:137)
at android.app.ActivityThread.main(ActivityThread.java:4424)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:511)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:784)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:551)
at dalvik.system.NativeStart.main(Native Method)

更诡异的是,即使弹出了错误报告对话框,视频播放似乎还能继续!在自己的设备上却无法复现,这更让人头疼。

二、 原因分析:跨进程通信与窗口布局

从错误堆栈来看,问题出在 android.view.ViewRootImpl.performTraversals 方法。 这个方法负责视图树的测量、布局和绘制。更具体地说,崩溃发生在 relayoutWindow 方法中,它通过 IWindowSession 接口与 WindowManagerService 进行跨进程通信 (IPC)。

问题就出在这个跨进程通信上。 Parcel.readException() 抛出空指针,表明从 WindowManagerService 返回的应答数据有问题。 这通常意味着 WindowManagerService 在处理请求时发生了错误,或者在数据传输过程中出现了问题。

至于为什么视频播放还能继续,可能是因为崩溃只影响了 UI 更新的某个环节,而 MediaPlayer 的解码和播放是在另一个线程或进程中进行的,所以不受影响。

深层原因分析:

  1. 窗口状态异常: 视频播放通常会改变窗口的属性,比如大小、位置、可见性等。在某些情况下,比如快速切换横竖屏、进入/退出全屏、与其他应用分屏等,窗口的状态可能会变得不稳定,导致 WindowManagerService 无法正确处理布局请求。

  2. 资源竞争: MediaPlayer 播放视频需要消耗大量的系统资源,包括 CPU、内存、GPU 等。如果设备资源不足,或者同时运行的其他应用也在大量消耗资源,可能会导致 WindowManagerService 处理请求超时或失败。

  3. 系统 bug 或兼容性问题: 某些设备或 Android 版本可能存在 bug,导致在特定情况下 ViewRootImpl 与 WindowManagerService 的通信出现问题。 不同厂商的ROM也有自己实现的差异性。

  4. Activity/Window 生命周期问题 : 如果 Activity/Window 处于不正确的生命周期状态时进行relayout, 有可能导致错误.

三、 解决方案: 逐个击破

既然问题可能出在多个方面,我们需要多种手段来解决。下面列出一些可行的方案,你可以根据自己的实际情况选择合适的方案。

1. 延迟 MediaPlayer 初始化

原理: 避免在 ActivityonCreateonResume 方法中立即初始化 MediaPlayer。 将初始化操作延迟到 UI 完全稳定后进行。 这样可以确保窗口已经准备好,减少与 WindowManagerService 的冲突。

代码示例:

public class MyActivity extends Activity {

    private MediaPlayer mMediaPlayer;
    private SurfaceView mSurfaceView;
    private Handler mHandler = new Handler(Looper.getMainLooper());

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_my);
        mSurfaceView = findViewById(R.id.surface_view);

         // 不要立即初始化 MediaPlayer
         //mMediaPlayer = new MediaPlayer();

        // 延迟初始化
         mHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                initMediaPlayer();
            }
        }, 500); // 延迟 500 毫秒,根据实际情况调整
    }

    private void initMediaPlayer() {
          try {
              mMediaPlayer = new MediaPlayer();
              // ... 其他 MediaPlayer 设置 ...
              mMediaPlayer.setDataSource(/* ... */);
              mMediaPlayer.setDisplay(mSurfaceView.getHolder());
              mMediaPlayer.prepareAsync();

           } catch (Exception e){
              e.printStackTrace();
          }
    }

    @Override
    protected void onPause() {
        super.onPause();
        if (mMediaPlayer != null) {
            mMediaPlayer.pause(); // onPause 暂停
        }
    }

      @Override
     protected void onDestroy(){
         super.onDestroy();
         if (mMediaPlayer != null) {
             mMediaPlayer.release();
             mMediaPlayer = null; // 避免内存泄漏
         }
         mHandler.removeCallbacksAndMessages(null);
     }

    // ... 其他方法 ...
}

2. 使用 TextureView 替代 SurfaceView

原理:SurfaceView 使用独立的 Surface,它的渲染不受应用主线程的影响,但与 WindowManagerService 的交互更复杂。TextureView 则将视频渲染到应用的 UI 线程中,与 View 体系集成更紧密,可以减少与 WindowManagerService 的冲突。

代码示例 (关键部分):

  //xml中用 TextureView替换 SurfaceView
   <TextureView
        android:id="@+id/texture_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
public class MyActivity extends Activity implements TextureView.SurfaceTextureListener {

     private MediaPlayer mMediaPlayer;
    private TextureView mTextureView;
     //... 其他代码 ...
       @Override
    protected void onCreate(Bundle savedInstanceState) {
           super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_my); //假设这个布局包含了上面的xml定义.
            mTextureView = findViewById(R.id.texture_view);
           mTextureView.setSurfaceTextureListener(this); //设置监听
        }

      @Override
      public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) {
           //当SurfaceTexture 可用时 初始化MediaPlayer
          initMediaPlayer(surfaceTexture);
      }

      private void initMediaPlayer(SurfaceTexture surfaceTexture) {
            try{
                mMediaPlayer = new MediaPlayer();
                 //...
                  mMediaPlayer.setSurface(new Surface(surfaceTexture));
                   // ...
            }catch (Exception e){
                e.printStackTrace();
            }
        }
     @Override
     public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {}
     @Override
    public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
          if(mMediaPlayer!=null){
                mMediaPlayer.stop();
               mMediaPlayer.release();
              mMediaPlayer = null;
           }
        return true; // 返回 true,表示我们自己处理 SurfaceTexture 的销毁
      }

      @Override
      public void onSurfaceTextureUpdated(SurfaceTexture surface) {
        //每当有新的帧可用时调用,通常用于自定义绘制或者做视频的特效.
     }

      // ...
}

额外建议: 切换到TextureView可能会稍微影响性能,特别是对低端设备, 需要仔细权衡。

3. 优化视频播放逻辑

原理:减少不必要的 UI 操作和 MediaPlayer 相关操作,降低系统负载。

*   避免在视频播放过程中频繁改变 UI 元素(如动画、布局更新等)。
*   使用 `MediaPlayer.setScreenOnWhilePlaying(true)` 保持屏幕常亮,避免频繁的屏幕开关。
*  在后台播放时(如果需要),将 `MediaPlayer` 的音频输出切换到后台服务,而不是 UI 线程。
*  确保正确地释放`MediaPlayer` 资源. 确保在`Activity` 或 `Fragment`的生命周期结束时(`onDestroy`), 释放MediaPlayer。
* 尽量不要在循环中频繁调用`seekTo()`。

代码示例 (关键部分):

   @Override
    protected void onStop() {
        super.onStop();
       if (!isChangingConfigurations() && mMediaPlayer!=null && mMediaPlayer.isPlaying()) {  // 判断是否因为ConfigurationChange而销毁Activity
               //考虑: 把MediaPlayer 移到 Service 中去播放。
       }
   }

   //循环 seekTo 的优化:
    private  boolean mIsSeeking = false;
    private  long  mPendingSeekPosition = -1;
   private Handler mSeekHandler = new Handler(Looper.getMainLooper());

    public void smoothSeekTo(long position){
        if(!mIsSeeking){
            mIsSeeking = true;
           internalSeekTo(position);
         }else {
            mPendingSeekPosition = position; //有正在进行的seek,记录
          }
     }

     private void internalSeekTo(long position){
         if(mMediaPlayer != null) {
            mMediaPlayer.seekTo((int) position);

              mSeekHandler.removeCallbacksAndMessages(null); //防止过于频繁.

              mSeekHandler.postDelayed(new Runnable() {
                @Override
              public void run() {
                 mIsSeeking = false;
                  if(mPendingSeekPosition != -1){  //有等待执行的seek.
                     internalSeekTo(mPendingSeekPosition); //递归处理。
                      mPendingSeekPosition = -1;  //reset
                  }
             }
             }, 200); //设置延迟
        }
    }

4. 捕获并处理异常

原理: 尽管不能完全避免异常,但可以通过捕获异常,防止应用崩溃,并记录更详细的日志信息,以便进一步分析。

代码示例:

     private void initMediaPlayer(){
          try{
            //...  MediaPlayer 设置 ....
          } catch(Exception e){
                Log.e("MyActivity", "MediaPlayer 初始化失败", e);
                // 记录更详细的设备信息、网络状态等
                logDetailedDeviceInfo();
              //给用户提示
                Toast.makeText(this, "视频播放出错,请稍后重试", Toast.LENGTH_SHORT).show();
           }
     }
      private void logDetailedDeviceInfo() {
         // 获取设备型号、Android 版本、CPU 信息、内存信息等
           String deviceInfo = "Device Model: " + Build.MODEL + "\n" +
                           "Android Version: " + Build.VERSION.RELEASE + "\n" +
                            "Manufacturer:" + Build.MANUFACTURER + "\n"+
                           // ... 其他信息 ...
                             "Available Memory:" + getAvailableMemory(); // 获取可用内存
           Log.d("DeviceInfo", deviceInfo);

        // 可以考虑上传到服务器进行分析.

      }
    private String getAvailableMemory() {
           ActivityManager.MemoryInfo mi = new ActivityManager.MemoryInfo();
        ActivityManager activityManager = (ActivityManager) getSystemService(ACTIVITY_SERVICE);
          activityManager.getMemoryInfo(mi);
         return Formatter.formatFileSize(this, mi.availMem);
    }

5. 监听 Configuration Changes

原理: 当屏幕方向,语言设置改变时,Activity会被销毁并重新创建. 在这种情况下不正确地处理MediaPlayer, 可能引发错误。

  @Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);

         // 处理 configuration changes. 例如根据新的配置 调整 TextureView 或 SurfaceView的尺寸.
       if(mTextureView != null){ //用TextureView作为例子.
          adjustAspectRatio(mTextureView.getWidth(), mTextureView.getHeight()); //例子函数
       }
  }

    /**
     * 调整TextureView以保持正确的宽高比. (根据你视频的具体宽高比来调整).
     * @param viewWidth  The width of the TextureView
      * @param viewHeight The height of the TextureView
       */
     private void adjustAspectRatio(int viewWidth, int viewHeight) {
        //TODO  获取你视频的宽高 然后计算正确的比例,调整view的LayoutParams
     }

6. 进阶使用技巧: 监控 WindowManager 相关指标(需要root权限/或可访问系统级信息的方式)

  • 使用 dumpsys window 命令监控 WindowManagerService 的状态,查找异常日志或可疑行为。
  • 可以自己 hook IWindowSession 相关的函数,来查看调用细节.

注意, 这部分需要对Android系统底层有深入的了解,并且操作需要谨慎,防止破坏系统稳定性。 一般情况下,无需这样做,除非以上方法都不管用.

总结

android.view.ViewRootImpl.performTraversals 空指针异常是一个复杂的问题,可能涉及多个层面。 解决这个问题需要耐心和细致的排查。 上面提供的多种方案, 可以组合使用。 遇到这类问题, 做好详细日志, 从用户反馈中获得更多的信息.