设为首页收藏本站
网站公告 | 这是第一条公告
     

 找回密码
 立即注册
缓存时间16 现在时间16 缓存数据 “我不是一个含着金汤匙生的小孩, 我的家人,一直用他们自己的方式, 又普通又隆重地爱我” —张艺兴 ​​

“我不是一个含着金汤匙生的小孩, 我的家人,一直用他们自己的方式, 又普通又隆重地爱我” —张艺兴 ​​ -- 外婆

查看: 1505|回复: 3

Android监控和阻断InputDispatching ANR的方法

[复制链接]

  离线 

TA的专栏

  • 打卡等级:即来则安
  • 打卡总天数:24
  • 打卡月天数:0
  • 打卡总奖励:329
  • 最近打卡:2025-04-03 01:43:46
等级头衔

等級:晓枫资讯-上等兵

在线时间
0 小时

积分成就
威望
0
贡献
376
主题
336
精华
0
金钱
1418
积分
762
注册时间
2023-2-10
最后登录
2025-4-3

发表于 2024-4-9 13:41:11 | 显示全部楼层 |阅读模式
目录


  • 前言
  • 本篇前奏

    • 失败的Socket FD 监听方案
    • 失败InputEventReceiver中间件方案

  • ANR Monitor Dialog方案

    • ViewRootImpl 与 WindowSession关系
    • Window 层级
    • ViewRootImpl异步渲染
    • 核心原理

      • 阻断ANR 产生
      • 延长ANR 阈值
      • 监控ANR


  • AnrMonitorDialog 实现逻辑

    • 用法
    • 测试效果
    • 评价

  • InputEventCompatProcessor方案

    • 反隐藏类
    • InputEventCompatProcessor 事件异步转发实现
    • 注入新的InputEventReceiver
    • 用法
    • 测试效果
    • 评价

  • 总结

前言

如何在Java层实现异步监控和阻断InputDispatching ANR?我相信这是很多开发者都想要的功能。
  1. 本篇,我们会通过“探索”两种方案来实现在Java层监控&阻断的方法
复制代码
Android版本发展已经趋于稳定,各种AMP工具都已经很成熟了,甚至很多人都能背出来具体实现。但是,仍然有一些东西我们要回过头去看,过去我们认为不能或者很难实现的东西,或许是因为我们很少去质疑。
任何时候都要重新审视一下过去的方法。
有时候解决问题的方法并不只有一种,我们要质疑为什么选的是不是最好用的一种。一些人的代码,提前引入现有需求不需要的逻辑是否合理?还有就是,为了解决一个小问题,比如解决相似图片的问题,结果完整引入了opencv,引入这样一个很大的框架是否合理?这些都需要去质疑。

本篇前奏

这里,我们简单了解下事件传递和一些尝试方案,如果不看本节,其实影响不大,可直接跳至下一节。
我们回到本篇主题,我们如何才能使用Java代码实现InputEvent ANR 监控和阻断呢,我们先来看这样一张图。我为什么选择这一张图呢,因为它很经典,虽然我在上面稍微改造了一下。
1.webp

当然,上图缺少WindowSesssion的角色,实际上,ViewRootImpl和WindowManagerService通信少不了WindowSession,那么WindowSession是如何通信的呢,我们继续往下看。
  1. public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
  2.     synchronized (this) {
  3.       ...
  4.       if ((mWindowAttributes.inputFeatures
  5.           & WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) {
  6.           mInputChannel = new InputChannel(); //创建InputChannel对象
  7.       }
  8.       //通过Binder调用,进入system进程的Session[见小节2.4]
  9.       res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
  10.                   getHostVisibility(), mDisplay.getDisplayId(),
  11.                   mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
  12.                   mAttachInfo.mOutsets, mInputChannel);
  13.       ...
  14.       if (mInputChannel != null) {
  15.           if (mInputQueueCallback != null) {
  16.               mInputQueue = new InputQueue();
  17.               mInputQueueCallback.onInputQueueCreated(mInputQueue);
  18.           }
  19.           //创建WindowInputEventReceiver对象[见3.1]
  20.           mInputEventReceiver = new WindowInputEventReceiver(mInputChannel,
  21.                   Looper.myLooper());
  22.       }
  23.     }
  24. }
复制代码
在这里我们可以看到,事件传递是通过InputChannel实现,而InputChannel负责事件发送、事件应答两部分,因此,肯定能双向通信,那么是不是Binder呢?
实际上,InputChannel在底层是Socket实现
  1. status_t InputChannel::openInputChannelPair(const String8& name,
  2.         sp<InputChannel>& outServerChannel, sp<InputChannel>& outClientChannel) {
  3.     int sockets[2];
  4.     //真正创建socket对的地方【核心】
  5.     if (socketpair(AF_UNIX, SOCK_SEQPACKET, 0, sockets)) {
  6.         ...
  7.         return result;
  8.     }

  9.     int bufferSize = SOCKET_BUFFER_SIZE; //32k
  10.     setsockopt(sockets[0], SOL_SOCKET, SO_SNDBUF, &bufferSize, sizeof(bufferSize));
  11.     setsockopt(sockets[0], SOL_SOCKET, SO_RCVBUF, &bufferSize, sizeof(bufferSize));
  12.     setsockopt(sockets[1], SOL_SOCKET, SO_SNDBUF, &bufferSize, sizeof(bufferSize));
  13.     setsockopt(sockets[1], SOL_SOCKET, SO_RCVBUF, &bufferSize, sizeof(bufferSize));

  14.     String8 serverChannelName = name;
  15.     serverChannelName.append(" (server)");
  16.     //创建InputChannel对象
  17.     outServerChannel = new InputChannel(serverChannelName, sockets[0]);

  18.     String8 clientChannelName = name;
  19.     clientChannelName.append(" (client)");
  20.     //创建InputChannel对象
  21.     outClientChannel = new InputChannel(clientChannelName, sockets[1]);
  22.     return OK;
  23. }
复制代码
InputChannel既创建Server又创建Client,看着是很奇怪的行为,事实上,在Linux中通信是通过Fd就能实现,而InputChannel是Parcelable的子类,可以把FD发送至WMS.

失败的Socket FD 监听方案

其实上面的这些代码和本篇关系不大,为什么要贴出代码呢,主要原因是我之前尝试过监听Socket的FD,可问题是InputChannel的FD拿不到,除非ChannelName为空,但是上面两个都有ChannelName,然后我就去找有没有让Name为空的方法,很遗憾也没有。
因此,这种实现只能借助Native Hook暴露接口,难度也有些大,因此,只能放弃这种方案了。

失败InputEventReceiver中间件方案

于是我找到另一种方案,在ViewRootImple#WindowInputEventReceiver 和 InputChannel之间插入一个MiddleWareInputEventReceiver,经过大量推断,将ViewRootImple#WindowInputEventReceiver dispose了,然后会发现,事件消费问题无法处理,因为ViewRootImple#WindowInputEventReceiver 调用finishInputEvent的方法无法调用到MiddleWareInputEventReceiver。
为什么做这种尝试呢,主要还是下面一段代码,我们可以看到Looper,这个类是可以传入Looper的,InputChannel之间插入一个MiddleWareInputEventReceiver异步监听,然后转发给dispose后的WindowInputEventReceiver。
  1. public InputEventReceiver(InputChannel inputChannel, Looper looper) {
  2.     if (inputChannel == null) {
  3.         throw new IllegalArgumentException("inputChannel must not be null");
  4.     }
  5.     if (looper == null) {
  6.         throw new IllegalArgumentException("looper must not be null");
  7.     }

  8.     mInputChannel = inputChannel;
  9.     mMessageQueue = looper.getQueue();
  10.     mReceiverPtr = nativeInit(new WeakReference<InputEventReceiver>(this),
  11.             inputChannel, mMessageQueue);

  12.     mCloseGuard.open("dispose");
  13. }
复制代码
ANR Monitor Dialog方案

上面的方案中,实现复杂且稳定性很差,或许只有通过HOOK手段或者替换一些方法地址(ArtMethod)才能解决一些问题。
我们本篇利用一种比较新颖的方案,纯java实现 具体怎么实现的呢?
我们要先来确定以下几种关系。

ViewRootImpl 与 WindowSession关系

先来看一张图
2.webp

在这张图中,我们可以清楚的看到,ViewRootImpl和WindowManagerService是多对一的关系,但是我们也要知道,他们之间的IWindow和IWindowSesssion和ViewRootImpl也是一对一的关系,也就是说,一个ViewRootImpl对应一个IWindow和IWindowSession。
因此,我们要明白,Activity中的PhoneWindow和WindowManagerService是没有任何关系的,Activity中PhoneWindow也不负责管理如Dialog、PopWindow这样的组件,最终是WindowManager负责管理的。
好了,我们再看下一个知识点

Window 层级

在Android中,Window是有层级关系的,当然这种关系被google改来改去,如果要使用的话需要处理一些兼容性问题。
3.webp

目前来说,除了OVERLAY类型外,其他的都需要window Token来与Activity强行绑定,但这不是本篇的重点,重点是,我们要知道为什么Dialog作为Activity的组件,会展示在Activity的上面。
主要原因是Activity的WindowType一般小于等于Dialog的WindowType (dialog的为TYPE_APPLICATION_ATTACHED_DIALOG),因此他能展示Activity上面。
注意: WindowType如果相等,那么后面加入的ViewRootImpl层级也是高于前面的。
  1. public int subWindowTypeToLayerLw(int type) {
  2.        switch (type) {
  3.        case TYPE_APPLICATION_PANEL:
  4.        case TYPE_APPLICATION_ATTACHED_DIALOG:
  5.            return APPLICATION_PANEL_SUBLAYER;//返回值是1
  6.        case TYPE_APPLICATION_MEDIA:
  7.            return APPLICATION_MEDIA_SUBLAYER;//返回值是-2  
  8.        case TYPE_APPLICATION_MEDIA_OVERLAY:
  9.            return APPLICATION_MEDIA_OVERLAY_SUBLAYER;//返回值是-1  
  10.        case TYPE_APPLICATION_SUB_PANEL:
  11.            return APPLICATION_SUB_PANEL_SUBLAYER;//返回值是2
  12.        case TYPE_APPLICATION_ABOVE_SUB_PANEL:
  13.            return APPLICATION_ABOVE_SUB_PANEL_SUBLAYER;//返回值是3  
  14.        }
  15.        Log.e(TAG, "Unknown sub-window type: " + type);
  16.        return 0;
  17.    }
复制代码
那么展示在上面意味着什么?
  1. 我们要知道,在Android系统中,Window层级越高,意味着权限越大,假设你的弹窗能展示在系统弹窗(如指纹识别弹窗)的上面,那么你就可以做一些看不见的事。当然google是不会让你这么做的,Google大费周折关联Window Token,就是为了修复此类风险。
复制代码
那么,还意味着什么?
  1. 我们还知道,层级越高,SurfsceFlinger中展示顺序的优先级越高,主线程和RenderThread线程优先级越高,同时线程调度的优先级越高,当然,和本篇有关的是,接收【事件】顺序的优先级越高。
复制代码
ViewRootImpl异步渲染

实际上,很多时候容易被忽略的一件事是,ViewRootImpl其实是支持异步渲染的,同样Choreographer也是支持异步的。为什么这样说呢?
因为现成的例子:android.app.Dialog
在Android系统中,Dialog是支持异步弹出的,这也就是为什么其内部的Handler是没有绑定主线程Looper的原因。

核心原理

通过上面3个知识点,我们就可以做到一件事
  1. 在Activity ViewRootImpl上面加一个异步创建的Dialog,然后将Dialog接收的事件通过主线程Handler转发给Activity。
复制代码
很显然,上面的方法是可行的。
那么,我们是不是可以做更多的事情呢?
  1. 答案是:是的。
复制代码
阻断ANR 产生

我们可以为了避免InputEventDispatcher ANR,在Dialog异步线程中,提前让InputEventReceiver的finishInputEvent方法调用,这样就能避免ANR。

延长ANR 阈值

我们知道,InputEventDispatcher Timeout时间为5s,我们可以主线程第4s的还没完成的时候,提前finishInputEvent,然后我们自行启动异步监控,比如我们决定在第6s ANR,如果主线程的任务在第6s没有结束,我们就下面的方法,来触发ANR。
  1. android.app.ActivityManager#appNotResponding
复制代码
  1. public void appNotResponding(@NonNull final String reason) {
  2.     try {
  3.         getService().appNotResponding(reason);
  4.     } catch (RemoteException e) {
  5.         throw e.rethrowFromSystemServer();
  6.     }
  7. }
复制代码
监控ANR

很多ANR的监控都在Native 层监控Sig_Quit信号,也有通过Looper.Printer进行检测到异常后,轮询AMS的的相关接口。
但是这里都可以做到对ANR的控制了,角色由消费者变成生产者,这种情况下自身就不需要监控了,只需要通知是否产生ANR。

AnrMonitorDialog 实现逻辑

首先,我们我们来定义一个Dialog,实际上,Dialog会影响状态栏和底部导航栏的样式,因此,对于Activity而言,为了避免Dialog和Activity的点击位置没法对齐,我们需要将Activity的一些样式同步到dialog上,下面是同步了全屏和非全屏两种,实际过程可能还需要同步其他几种。
  1. public class AnrMonitorDialog extends Dialog {

  2.     private static HandlerThread AnrMonitorThread = new HandlerThread("ANR-Monitor");

  3.     static {
  4.         AnrMonitorThread.start();
  5.     }

  6.     private static Handler sAnrMonitorHandler = new Handler(AnrMonitorThread.getLooper());
  7.     private final Window.Callback mHost;
  8.     private final Handler mainHandler;
  9.     private boolean isFullScreen = false;

  10.     AnrMonitorDialog(Context context, Window hostWindow) {
  11.         super(context);
  12.         this.mainHandler = new Handler(Looper.getMainLooper());
  13.         this.mHost = hostWindow.getCallback();
  14.         this.isFullScreen = (WindowManager.LayoutParams.FLAG_FULLSCREEN & hostWindow.getAttributes().flags) != 0;
  15.     }


  16.   
  17.     @Override
  18.     protected void onCreate(Bundle savedInstanceState) {
  19.         super.onCreate(savedInstanceState);
  20.         Window window = getWindow();
  21.         window.requestFeature(Window.FEATURE_NO_TITLE);
  22.         View view = new View(getContext());
  23.         view.setFocusableInTouchMode(false);
  24.         view.setFocusable(false);
  25.         setContentView(view);
  26.         if (isFullScreen) {
  27.             window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
  28.         } else {
  29.             window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
  30.         }
  31.         WindowManager.LayoutParams attributes = window.getAttributes();
  32.         attributes.format = PixelFormat.TRANSPARENT;
  33.         attributes.dimAmount = 0f;
  34.         attributes.flags |= FLAG_NOT_FOCUSABLE;
  35.         window.setBackgroundDrawable(new ColorDrawable(0x00000000));
  36.         window.setLayout(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.MATCH_PARENT);

  37.         setCancelable(false);
  38.         setCanceledOnTouchOutside(false);

  39.     }


  40.     public static void hideDialog(DialogInterface dialog) {
  41.         if (dialog == null) return;
  42.         sAnrMonitorHandler.post(new Runnable() {
  43.             @Override
  44.             public void run() {
  45.                 dialog.dismiss();
  46.             }
  47.         });
  48.     }


  49.     public static void showDialog(final Activity activity, final Window window, OnShowListener onShowListener) {
  50.         sAnrMonitorHandler.post(new Runnable() {
  51.             @Override
  52.             public void run() {
  53.          
  54.                 if(activity.isFinishing()){
  55.                     return;
  56.                 }
  57.                 AnrMonitorDialog anrMonitorDialog = new AnrMonitorDialog(activity, window);
  58.                 anrMonitorDialog.setOnShowListener(onShowListener);
  59.                 anrMonitorDialog.show();
  60.             }
  61.         });
  62.     }
  63.    
  64.    // 省略一堆关键代码
  65. }
复制代码
在实现的过程中,我们可以复写Dialog的一些方法,当然你还可以给Dialog的Window设置Window.Callback。这里要说的一点是,一些设备自定义了特殊的实现,如dispatchFnKeyEvent,显然系统类中没有这个方法,但是如果你要实现的话无法通过super关键字调用,解决办法也是有的,就是利用Java 7中的MethodHandle动态invoke,这里我们暂不实现了,毕竟这个KeyEvent一般APP也用不到。
  1. /**
  2. *  fixed Lenovo/Sharp Device
  3. *
  4. */
  5. @Keep
  6. public boolean dispatchFnKeyEvent(KeyEvent event) {
  7.     //可以利用MethodHandle调用父类的方法
  8.     return false;
  9. }
复制代码
这里我们复写Dialog的一些方法,我们以TouchEvent的传递为例子,当我们拿到MotionEvent的时候,我们就能将event转发给主线程。其实这里最稳妥的方法是对事件复制,因为MotionEvent是可以被recycle的,如果不复制就会被异步修改。
  1. @Override
  2. public boolean dispatchTouchEvent(final MotionEvent event) {
  3.     final Waiter waiter = new Waiter();

  4.     final MotionEvent targetEvent = copyMotionEvent(event);
  5.     mainHandler.post(new Runnable() {
  6.         @Override
  7.         public void run() {
  8.             switch (event.getAction()) {
  9.                 case MotionEvent.ACTION_DOWN:
  10.                     break;
  11.                 case MotionEvent.ACTION_UP:
  12.                 case MotionEvent.ACTION_CANCEL:
  13.                     break;
  14.             }
  15.             boolean isHandled = mHost.dispatchTouchEvent(targetEvent);
  16.             targetEvent.recycle();  //自己拷贝的事件,需要主动回收
  17.             waiter.countDown(isHandled);
  18.         }
  19.     });
  20.     try {
  21.         if(!waiter.await(4000, TimeUnit.MILLISECONDS)){
  22.             sAnrMonitorHandler.postAtTime(mAnrTimeoutTask, SystemClock.uptimeMillis() + 2000L);
  23.             mainHandler.postAtFrontOfQueue(mCancelAnrTimeoutTask);
  24.         }
  25.     } catch (InterruptedException e) {
  26.         e.printStackTrace();

  27.     }

  28.     return waiter.isHandled;
  29. }
复制代码

  • mAnrTimeoutTask 负责触发ActivityManager#appNotResponding
  • mCancelAnrTimeoutTask 用于取消sAnrMonitorHandler的定时逻辑
  1. private Runnable mAnrTimeoutTask = new Runnable() {
  2.     @Override
  3.     public void run() {
  4.         sendAppNotResponding("Dispatching Timeout");
  5.     }
  6. };

  7. private Runnable mCancelAnrTimeoutTask = new Runnable() {
  8.     @Override
  9.     public void run() {
  10.         sAnrMonitorHandler.removeCallbacks(mAnrTimeoutTask);
  11.     }
  12. };
复制代码
原理是,如果在指定的时间没有取消,说明主线程是卡住了,我们可以不抛ANR,但是点击之后卡住不动,任何人的心情都会很难受,抑制ANR发生并不可取,但是我们可以借助这些时间段收集一些线程状态和内存信息,以及业务信息,提高ANR上报率和场景覆盖。
那么Waiter是什么呢,其实是CountDownLatch的子类,我们简单封装一下,来等待事件完成。
  1. static class Waiter extends CountDownLatch {
  2.     boolean isHandled = false;
  3.     public Waiter() {
  4.         super(1);
  5.     }

  6.     public void countDown(boolean isHandled){
  7.         this.isHandled = isHandled;
  8.         super.countDown();
  9.     }
  10.     @Override
  11.     public void countDown() {
  12.          throw new Exception("I like along, don't call me");
  13.     }
  14. }
复制代码

用法

很简单,我们在BaseActivity的onCreate中加入即可
  1. AnrMonitorDialog.showDialog(this, getWindow(), new DialogInterface.OnShowListener() {
  2.     @Override
  3.     public void onShow(DialogInterface dialog) {
  4.         anrMonitorDialog = dialog;  
  5.          //由于是异步返回的的dialog,这里要做二次检测,防止InputChannel泄漏
  6.         postToMain(new Runnable(){
  7.           if(actvitiyIsFinish()){
  8.               AnrMonitorDialog.hideDialog(anrMonitorDialog);
  9.               anrMonitorDialog = null;
  10.            
  11.           }
  12.         });
  13.     }
  14. });
复制代码
不过,我们一定要在onDestoryed中关闭Dialog,避免InputChannel泄漏
  1. @Override
  2. protected void onDestroy() {
  3.     AnrMonitorDialog.hideDialog(anrMonitorDialog);
  4.     super.onDestroy();
  5. }
复制代码

测试效果

经过测试,在Touch Event模式下,基本没有出现问题,滑动和点击都难正常,也不会出现遮挡,包括Activity跳转也是正常的。


评价

通过上面的实现,我们将异步线程创建的全屏Dialog覆盖到Activity上面,然后通过Dialog转发事件到Activity,从而实现了在Java层就能监控和阻断InputDispatching ANR。
不过,这里也有些可能的问题,具体我们有测试,但可能会存在。

  • 焦点问题:由于ViewRootImpl 内部有焦点处理逻辑,如果把事件直接给Window.Callback可能还不合适,因此,如果是TV版本开发,还可能需要从DecorView层面进一步兼容一下,不过测试过程中发现大部分走焦逻辑是正常的,暂没有发现特别严重的问题。
  • 一些低级别WindowType的弹窗无法拦截事件:实际上,在Android中,WindowType一样的话,后面的弹窗会覆盖到上面,但是对于一些魔改的系统,可能存在问题,但是解决办法就是调整WindowType,其次,AnrMonitorDialog要尽可能早一些弹出
  • 仅限于对Activity的事件监控: 本篇方案仅限于对Activity的的监控,但如果是想支持其他Dialog,那么要保证AnrMonitorDialog 有更高的层级,同时要能支持其他Dialog的Window.Callback获取,当然,最好的方式就是从WindowManagerGlobal中获取次一级的ViewRootImpl,然后想办法获取DecorView
  • 输入法问题:由于部分系统输入法在Dialog下面,按道理输入法层级更高才是,且输入法不属于app自身的UI,因此无法点击。我们要做2件事才能实现兼容: ①监听全局焦点,如果移动到TextView或EditText上,那么需要关闭AnrMonitorDialog弹窗 ② Hook windowManager来判断是否有其他Dialog弹出,等到其他Dialog关闭后且焦点不在EidtText和TextView上之后,同时判断键盘已经收起之后,再恢复AnrMonitorDialog 。

InputEventCompatProcessor方案

在Android 10中,新增了InputEventCompatProcessor用來兼容事件,正因为如此,我们便可使用其在java层挂载hook,来绕过WindowInputEventReceiver无法被复写的问题,下面是WindowInputEventReceiver的源码部分
  1.       @Override
  2.         public void onInputEvent(InputEvent event) {
  3.             Trace.traceBegin(Trace.TRACE_TAG_VIEW, "processInputEventForCompatibility");
  4.             List<InputEvent> processedEvents;
  5.             try {
  6.                 processedEvents =
  7.                     mInputCompatProcessor.processInputEventForCompatibility(event);
  8.             } finally {
  9.                 Trace.traceEnd(Trace.TRACE_TAG_VIEW);
  10.             }
  11.             if (processedEvents != null) {
  12.                 if (processedEvents.isEmpty()) {
  13.                     // InputEvent consumed by mInputCompatProcessor
  14.                     finishInputEvent(event, true);
  15.                 } else {
  16.                     for (int i = 0; i < processedEvents.size(); i++) {
  17.                         enqueueInputEvent(
  18.                                 processedEvents.get(i), this,
  19.                                 QueuedInputEvent.FLAG_MODIFIED_FOR_COMPATIBILITY, true);
  20.                     }
  21.                 }
  22.             } else {
  23.                 enqueueInputEvent(event, this, 0, true);
  24.             }
  25.         }
复制代码
上面的代码中,如果我们将WindowInputEventReceiver的Looper设置为异步的,然后,我们直接将后面的逻辑移动到processInputEventForCompatibility 进行处理,便能实现事件监控和阻断。
当然,为了避免重复处理,我们要返回的processedEvents 为EmptyList即可。

反隐藏类

显然我们需要反隐藏,我们需要反射一些方法,这里推荐使用《FreeFlection》开源项目去开启反射。
不过,为了能继承InputEventCompatProcessor,我们就需要一些新的手段
我们要Hook被@hide标记的类实际上是不行的,因此我们可以在Android Studio中创建Moudle,将这些被@hide类标记的空实现加入到 android.view包名下,然后通过compileOnly方式引入项目中 比如ViewRootImpl 的空实现
  1. package android.view;
  2. public class ViewRootImpl {  
  3. }
复制代码
那么InputEventCompatProcessor也是同理
  1. package android.view;  
  2.   
  3. import android.content.Context;  
  4. import java.util.List;  
  5.   
  6. public class InputEventCompatProcessor {  
  7.     protected Context mContext;  
  8.     public InputEventCompatProcessor(Context context) {  
  9.         mContext = context;  
  10.     }  

  11.     public List<InputEvent> processInputEventForCompatibility(InputEvent e) {  
  12.         return null;  
  13.     }  
  14.     public InputEvent processInputEventBeforeFinish(InputEvent e) {  
  15.         // No changes needed  
  16.         return e;  
  17.     }  
  18. }
复制代码
其他类如InputChannel,InputEventReceiver也是如此

InputEventCompatProcessor 事件异步转发实现

下面是核心实现,当然,ANR 监控部分和Dialog类似了,这里的监控和ANR阻断方式和ANR Monitor Dialog类似,就不再重复了。
  1. public class WindowInputEventCompatProcessor extends InputEventCompatProcessor {

  2.     private final InputEventCompatProcessor processor;
  3.     private final InputEventReceiver eventReceiver;
  4.     private ViewRootImpl viewRootImpl;
  5.     final Handler mainHandler;
  6.     private static final AtomicInteger mNextSeq = new AtomicInteger();
  7.     private SparseIntArray eventMaps = new SparseIntArray();
  8.     private Method enqueueInputEvent;
  9.     public static final int FLAG_MODIFIED_FOR_COMPATIBILITY = 1 << 6;
  10.     private Handler anrHandler;

  11.     private String TAG = "WindowInputEventCompatProcessor";
  12.     private Method finishInputEvent;

  13.     public WindowInputEventCompatProcessor(Context context, InputEventCompatProcessor processor, ViewRootImpl viewRootImpl, InputEventReceiver eventReceiver) {
  14.         super(context);
  15.         this.processor = processor;
  16.         this.mainHandler = new Handler(Looper.getMainLooper());
  17.         this.viewRootImpl = viewRootImpl;
  18.         this.eventReceiver = eventReceiver;
  19.     }

  20.     @Override
  21.     public List<InputEvent> processInputEventForCompatibility(InputEvent e) {

  22.         if (anrHandler == null) {
  23.             anrHandler = new Handler(Looper.myLooper());
  24.         }

  25.         InputEvent copyEvent = null;
  26.         if(e instanceof KeyEvent){
  27.             copyEvent =  KeyEvent.changeFlags((KeyEvent) e,((KeyEvent) e).getFlags());
  28.         }else if( e instanceof MotionEvent){
  29.             copyEvent = MotionEvent.obtain((MotionEvent) e);
  30.         }

  31.         if(copyEvent == null){
  32.             return Collections.emptyList();
  33.         }

  34.         final InputEvent event = copyEvent;

  35.         if(Looper.myLooper() == Looper.getMainLooper()){
  36.             anrHandler.post(new Runnable() {
  37.               @Override
  38.               public void run() {
  39.                   finishInputEvent(e,true);
  40.               }
  41.           });
  42.           anrHandler.postAtTime(mAnrTimeoutTask,event, SystemClock.uptimeMillis() + 6000L);
  43.           mainHandler.post(new Runnable() {
  44.               @Override
  45.               public void run() {
  46.                   anrHandler.removeCallbacks(mAnrTimeoutTask,event);
  47.               }
  48.           });
  49.           List<InputEvent> processedEvents = processor.processInputEventForCompatibility(event);
  50.             if(processedEvents == null){
  51.                 processedEvents = new ArrayList<>();
  52.             }
  53.             if(processedEvents.isEmpty()){
  54.                 processedEvents.add(event);
  55.             }
  56.       
  57.           return processedEvents;
  58.         }

  59.         eventMaps.append(event.hashCode(), mNextSeq.getAndIncrement());
  60.         anrHandler.postAtTime(mAnrTimeoutTask,event, SystemClock.uptimeMillis() + 6000L);
  61.         mainHandler.post(new Runnable() {
  62.             @Override
  63.             public void run() {

  64.                 anrHandler.removeCallbacks(mAnrTimeoutTask,event);

  65.                 List<InputEvent> processedEvents = processor.processInputEventForCompatibility(event);

  66.                 if (processedEvents != null) {
  67.                     if (processedEvents.isEmpty()) {
  68.                         // InputEvent consumed by mInputCompatProcessor
  69.                         // finishInputEvent(event, true);
  70.                         //这里一定不要调用哦,防止外部重复调用
  71.                     } else {
  72.                         for (int i = 0; i < processedEvents.size(); i++) {
  73.                             enqueueInputEvent(
  74.                                     processedEvents.get(i), eventReceiver,
  75.                                     FLAG_MODIFIED_FOR_COMPATIBILITY, true);
  76.                         }
  77.                     }
  78.                 } else {
  79.                     //修改事件flag
  80.                     enqueueInputEvent(event, eventReceiver, FLAG_MODIFIED_FOR_COMPATIBILITY, true);
  81.                 }

  82.             }
  83.         });

  84.         return Collections.emptyList();
  85.     }

  86.     private void finishInputEvent(InputEvent event, boolean isHandled) {
  87.         try {
  88.             if (finishInputEvent == null) {
  89.                 finishInputEvent = Class.forName(InputEventReceiver.class.getName()).getDeclaredMethod("finishInputEvent", InputEvent.class, boolean.class);
  90.                 finishInputEvent.setAccessible(true);
  91.             }
  92.             finishInputEvent.invoke(eventReceiver, event,isHandled);
  93.         } catch (Exception e) {
  94.             e.printStackTrace();
  95.         }
  96.     }


  97.     private void enqueueInputEvent(InputEvent event, InputEventReceiver eventReceiver, int flags, boolean processImmediately) {
  98.         try {
  99.             if (enqueueInputEvent == null) {
  100.                 enqueueInputEvent = ViewRootImpl.class.getDeclaredMethod("enqueueInputEvent", InputEvent.class, InputEventReceiver.class, int.class, boolean.class);
  101.                 enqueueInputEvent.setAccessible(true);
  102.             }
  103.             enqueueInputEvent.invoke(viewRootImpl, event, eventReceiver, flags, processImmediately);
  104.         } catch (Exception e) {
  105.             e.printStackTrace();
  106.         }
  107.     }

  108.     @Override
  109.     public InputEvent processInputEventBeforeFinish(final InputEvent e) {
  110.         final int hashCode = e.hashCode();
  111.         Runnable runnable = new Runnable() {
  112.             @Override
  113.             public void run() {
  114.                 int keyIndex = eventMaps.indexOfKey(hashCode);
  115.                 if (keyIndex >= 0) {
  116.                     eventMaps.removeAt(keyIndex);
  117.                 }
  118.                 processor.processInputEventBeforeFinish(e);
  119.             }
  120.         };
  121.         if(Looper.myLooper() == anrHandler.getLooper()){
  122.             runnable.run();
  123.         }else {
  124.             anrHandler.post(runnable);
  125.         }
  126.         return null;
  127.     }


  128.     private Runnable mAnrTimeoutTask = new Runnable() {
  129.         @Override
  130.         public void run() {
  131.             AppManager.sendAppNotResponding("Dispatching Timeout");
  132.         }
  133.     };

  134. }
复制代码
注入新的InputEventReceiver

我们需要在Activity的onCreate方法中进行注入,当然,这里有大量反射,我们不仅仅需要重新注入WindowInputEventReceiver,还需要注入新的InputEventCompatProcessor
  1. public class AnrInterceptor {

  2.     static final HandlerThread handlerThread = new HandlerThread("ANR-Looper");
  3.     static {
  4.         if(Build.VERSION.SDK_INT > Build.VERSION_CODES.P){
  5.             handlerThread.start();
  6.         }

  7.     }
  8.     public static void monitor(final Activity activity){

  9.         if(Build.VERSION.SDK_INT <= Build.VERSION_CODES.P){
  10.             return;
  11.         }

  12.         final View decorView = activity.getWindow().getDecorView();
  13.         decorView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
  14.             @Override
  15.             public void onViewAttachedToWindow(@NonNull View decorView) {
  16.                 ViewRootImpl viewRootImpl = (ViewRootImpl) decorView.getParent();
  17.                 try {
  18.                     Class ViewRootImplKlass = viewRootImpl.getClass();
  19.                     Field mInputCompatProcessorField = ViewRootImplKlass.getDeclaredField("mInputCompatProcessor");
  20.                     mInputCompatProcessorField.setAccessible(true);
  21.                     InputEventCompatProcessor inputEventCompatProcessor = (InputEventCompatProcessor) mInputCompatProcessorField.get(viewRootImpl);

  22.                     if(inputEventCompatProcessor instanceof WindowInputEventCompatProcessor){
  23.                         return;
  24.                     }

  25.                     Field mInputEventReceiverField = ViewRootImplKlass.getDeclaredField("mInputEventReceiver");
  26.                     mInputEventReceiverField.setAccessible(true);
  27.                     InputEventReceiver receiver = (InputEventReceiver) mInputEventReceiverField.get(viewRootImpl);

  28.                     Class<?> WindowInputEventReceiverClass = receiver.getClass();
  29.                     Field inputChannelField = Class.forName(InputEventReceiver.class.getName()).getDeclaredField("mInputChannel");
  30.                     inputChannelField.setAccessible(true);
  31.                     InputChannel inputChannel = (InputChannel) inputChannelField.get(receiver);

  32.                     Constructor WindowInputEventReceiverConstructor = WindowInputEventReceiverClass.getDeclaredConstructor(ViewRootImpl.class,InputChannel.class, Looper.class);
  33.                     WindowInputEventReceiverConstructor.setAccessible(true);
  34.                     InputEventReceiver inputEventReceiver = (InputEventReceiver) WindowInputEventReceiverConstructor.newInstance(viewRootImpl,inputChannel,handlerThread.getLooper());

  35.                     InputEventCompatProcessor WindowInputEventCompatProcessor =  new WindowInputEventCompatProcessor(activity,inputEventCompatProcessor,viewRootImpl,inputEventReceiver);
  36.                     mInputEventReceiverField.set(viewRootImpl,inputEventReceiver);
  37.                     mInputCompatProcessorField.set(viewRootImpl,WindowInputEventCompatProcessor);

  38.                     receiver.dispose();
  39.                 } catch (Throwable e) {
  40.                     e.printStackTrace();
  41.                 }
  42.             }

  43.             @Override
  44.             public void onViewDetachedFromWindow(@NonNull View v) {

  45.             }
  46.         });
  47.     }
  48. }
复制代码
用法

在Activity的onCreate方法中进行监控
  1. override fun onCreate(savedInstanceState: Bundle?) {  
  2.     enableEdgeToEdge(statusBarStyle = SystemBarStyle.dark(Color.TRANSPARENT))  
  3.     super.onCreate(savedInstanceState)  

  4.     AnrInterceptor.monitor(this)
  5. }
复制代码
测试效果

可以完美兼容焦点模式和触屏两种模式,效果相对ANR Monitor Dialog更好,不需要处理键盘、窗口层级,同时也避免了很多复杂的事件转发。
评价

相比ANR Monitor Dialog而言,这种方法的稳定性相對差一些,同時需要大量反射,最重要的一点是无法兼容到Android 10之前的版本。

总结

本篇实现了2种ANR 监控方案 ANR Monior Dialog 和InputEventCompatProcessor 各自都有优点和缺点,总体上,如果是Android 10+版本的系统,建议使用后者。
目前来说,这两种方法在特定场景下还是比较实用的,比如调试环境,我们遇到一类问题,就是DEBUG时间太长,一些系统中AMS直接将APP进程杀死;
还有就是一些系统,如果出现ANR,连Native层SIGQUIT信号可能都来不及接收就直接force-stop进程的情况。
总之,这属于一种Java层监控ANR的方案,目前来说还有很多不足,但是至少来说,解决调试时ANR进程被杀问题还是可以的,当然,能否线上使用,目前还有一些事情要处理。
以上就是Android监控和阻断InputDispatching ANR的方法的详细内容,更多关于Android InputDispatching ANR的资料请关注晓枫资讯其它相关文章!

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
晓枫资讯-科技资讯社区-免责声明
免责声明:以上内容为本网站转自其它媒体,相关信息仅为传递更多信息之目的,不代表本网观点,亦不代表本网站赞同其观点或证实其内容的真实性。
      1、注册用户在本社区发表、转载的任何作品仅代表其个人观点,不代表本社区认同其观点。
      2、管理员及版主有权在不事先通知或不经作者准许的情况下删除其在本社区所发表的文章。
      3、本社区的文章部分内容可能来源于网络,仅供大家学习与参考,如有侵权,举报反馈:点击这里给我发消息进行删除处理。
      4、本社区一切资源不代表本站立场,并不代表本站赞同其观点和对其真实性负责。
      5、以上声明内容的最终解释权归《晓枫资讯-科技资讯社区》所有。
http://bbs.yzwlo.com 晓枫资讯--游戏IT新闻资讯~~~

  离线 

TA的专栏

等级头衔

等級:晓枫资讯-列兵

在线时间
0 小时

积分成就
威望
0
贡献
0
主题
0
精华
0
金钱
16
积分
12
注册时间
2022-12-25
最后登录
2022-12-25

发表于 2024-4-12 23:53:22 | 显示全部楼层
路过,支持一下
http://bbs.yzwlo.com 晓枫资讯--游戏IT新闻资讯~~~

  离线 

TA的专栏

等级头衔

等級:晓枫资讯-列兵

在线时间
0 小时

积分成就
威望
0
贡献
0
主题
1
精华
0
金钱
49
积分
15
注册时间
2022-12-25
最后登录
2023-8-12

发表于 2024-7-1 17:54:59 | 显示全部楼层
感谢楼主分享。
http://bbs.yzwlo.com 晓枫资讯--游戏IT新闻资讯~~~

  离线 

TA的专栏

等级头衔

等級:晓枫资讯-列兵

在线时间
0 小时

积分成就
威望
0
贡献
0
主题
1
精华
0
金钱
22
积分
25
注册时间
2022-12-21
最后登录
2023-8-13

发表于 2024-12-17 15:20:07 | 显示全部楼层
顶顶更健康!!!
http://bbs.yzwlo.com 晓枫资讯--游戏IT新闻资讯~~~
严禁发布广告,淫秽、色情、赌博、暴力、凶杀、恐怖、间谍及其他违反国家法律法规的内容。!晓枫资讯-社区
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

1楼
2楼
3楼
4楼

手机版|晓枫资讯--科技资讯社区 本站已运行

CopyRight © 2022-2025 晓枫资讯--科技资讯社区 ( BBS.yzwlo.com ) . All Rights Reserved .

晓枫资讯--科技资讯社区

本站内容由用户自主分享和转载自互联网,转载目的在于传递更多信息,并不代表本网赞同其观点和对其真实性负责。

如有侵权、违反国家法律政策行为,请联系我们,我们会第一时间及时清除和处理! 举报反馈邮箱:点击这里给我发消息

Powered by Discuz! X3.5

快速回复 返回顶部 返回列表