Android 一种异步监控和阻断InputDispatching ANR的方法

前言

如何在Java层实现异步监控和阻断InputDispatching ANR?我相信这是很多开发者都想要的功能。

Android版本发展已经趋于稳定,各种AMP工具都已经很成熟了,甚至很多人都能背出来具体实现。但是,仍然有一些东西我们要回过头去看,过去我们认为不能或者很难实现的东西,或许是因为我们很少去质疑。

任何时候都要重新审视一下过去的方法。

有时候解决问题的方法并不只有一种,我们要质疑为什么选的是不是最好用的一种。一些人的代码,提前引入现有需求不需要的逻辑是否合理?还有就是,为了解决一个小问题,比如解决相似图片的问题,结果完整引入了opencv,引入这样一个很大的框架是否合理?这些都需要去质疑。

本篇前奏

这里,我们简单了解下事件传递和一些尝试方案,如果不看本节,其实影响不大,可直接跳至下一节。

我们回到本篇主题,我们如何才能使用Java代码实现InputEvent ANR 监控和阻断呢,我们先来看这样一张图。我为什么选择这一张图呢,因为它很经典,虽然我在上面稍微改造了一下。

当然,上图缺少WindowSesssion的角色,实际上,ViewRootImpl和WindowManagerService通信少不了WindowSession,那么WindowSession是如何通信的呢,我们继续往下看。

java 复制代码
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
    synchronized (this) {
      ...
      if ((mWindowAttributes.inputFeatures
          & WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) {
          mInputChannel = new InputChannel(); //创建InputChannel对象
      }
      //通过Binder调用,进入system进程的Session[见小节2.4]
      res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                  getHostVisibility(), mDisplay.getDisplayId(),
                  mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                  mAttachInfo.mOutsets, mInputChannel);
      ...
      if (mInputChannel != null) {
          if (mInputQueueCallback != null) {
              mInputQueue = new InputQueue();
              mInputQueueCallback.onInputQueueCreated(mInputQueue);
          }
          //创建WindowInputEventReceiver对象[见3.1]
          mInputEventReceiver = new WindowInputEventReceiver(mInputChannel,
                  Looper.myLooper());
      }
    }
}

在这里我们可以看到,事件传递是通过InputChannel实现,而InputChannel负责事件发送、事件应答两部分,因此,肯定能双向通信,那么是不是Binder呢?

实际上,InputChannel在底层是Socket实现

java 复制代码
status_t InputChannel::openInputChannelPair(const String8& name,
        sp<InputChannel>& outServerChannel, sp<InputChannel>& outClientChannel) {
    int sockets[2];
    //真正创建socket对的地方【核心】
    if (socketpair(AF_UNIX, SOCK_SEQPACKET, 0, sockets)) {
        ...
        return result;
    }

    int bufferSize = SOCKET_BUFFER_SIZE; //32k
    setsockopt(sockets[0], SOL_SOCKET, SO_SNDBUF, &bufferSize, sizeof(bufferSize));
    setsockopt(sockets[0], SOL_SOCKET, SO_RCVBUF, &bufferSize, sizeof(bufferSize));
    setsockopt(sockets[1], SOL_SOCKET, SO_SNDBUF, &bufferSize, sizeof(bufferSize));
    setsockopt(sockets[1], SOL_SOCKET, SO_RCVBUF, &bufferSize, sizeof(bufferSize));

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

    String8 clientChannelName = name;
    clientChannelName.append(" (client)");
    //创建InputChannel对象
    outClientChannel = new InputChannel(clientChannelName, sockets[1]);
    return OK;
}

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。

java 复制代码
public InputEventReceiver(InputChannel inputChannel, Looper looper) {
    if (inputChannel == null) {
        throw new IllegalArgumentException("inputChannel must not be null");
    }
    if (looper == null) {
        throw new IllegalArgumentException("looper must not be null");
    }

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

    mCloseGuard.open("dispose");
}

本篇实现

上面的方案中,实现复杂且稳定性很差,或许只有通过HOOK手段或者替换一些方法地址(ArtMethod)才能解决一些问题。

我们本篇利用一种比较新颖的方案,纯java实现 具体怎么实现的呢?

我们要先来确定以下几种关系。

ViewRootImpl 与 WindowSession关系

先来看一张图

在这张图中,我们可以清楚的看到,ViewRootImpl和WindowManagerService是多对一的关系,但是我们也要知道,他们之间的IWindow和IWindowSesssion和ViewRootImpl也是一对一的关系,也就是说,一个ViewRootImpl对应一个IWindow和IWindowSession。

因此,我们要明白,Activity中的PhoneWindow和WindowManagerService是没有任何关系的,Activity中PhoneWindow也不负责管理如Dialog、PopWindow这样的组件,最终是WindowManager负责管理的。

好了,我们再看下一个知识点

Window 层级

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

目前来说,除了OVERlAY类型外,其他的都需要window Token来与Activity强行绑定,但这不是本篇的重点,重点是,我们要知道为什么Dialog作为Activity的组件,会展示在Activity的上面。

主要原因是Activity的WindowType小于Dialog的WindowType (dialog的为TYPE_APPLICATION_ATTACHED_DIALOG),因此他能展示Activity上面。

java 复制代码
 public int subWindowTypeToLayerLw(int type) {
       switch (type) {
       case TYPE_APPLICATION_PANEL:
       case TYPE_APPLICATION_ATTACHED_DIALOG:
           return APPLICATION_PANEL_SUBLAYER;//返回值是1
       case TYPE_APPLICATION_MEDIA:
           return APPLICATION_MEDIA_SUBLAYER;//返回值是-2  
       case TYPE_APPLICATION_MEDIA_OVERLAY:
           return APPLICATION_MEDIA_OVERLAY_SUBLAYER;//返回值是-1  
       case TYPE_APPLICATION_SUB_PANEL:
           return APPLICATION_SUB_PANEL_SUBLAYER;//返回值是2 
       case TYPE_APPLICATION_ABOVE_SUB_PANEL:
           return APPLICATION_ABOVE_SUB_PANEL_SUBLAYER;//返回值是3  
       }
       Log.e(TAG, "Unknown sub-window type: " + type);
       return 0;
   }

那么展示在上面意味着什么?

我们要知道,在Android系统中,Window层级越高,意味着权限越大,假设你的弹窗能展示在系统弹窗(如指纹识别弹窗)的上面,那么你就可以做一些看不见的事。当然google是不会让你这么做的,Google大费周折关联Window Token,就是为了修复此类风险。

那么,还意味着什么?

我们还知道,层级越高,SurfsceFlinger中展示顺序的优先级越高,主线程和RenderThread线程优先级越高,同时线程调度的优先级越高,当然,和本篇有关的是,接收【事件】顺序的优先级越高。

ViewRootImpl异步渲染

实际上,很多时候容易被忽略的一件事是,ViewRootImpl其实是支持异步渲染的,同样Choreographer也是支持异步的。为什么这样说呢?

因为现成的例子:android.app.Dialog

在Android系统中,Dialog是支持异步弹出的,这也就是为什么其内部的Handler是没有绑定主线程Looper的原因。

核心原理

通过上面3个知识点,我们就可以做到一件事

在Activity ViewRootImpl上面加一个异步创建的Dialog,然后将Dialog接收的事件通过主线程Handler转发给Activity。

很显然,上面的方法是可行的。

那么,我们是不是可以做更多的事情呢?

答案是:是的。

阻断ANR 产生

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

延长ANR 阈值

我们知道,InputEventDispatcher Timeout时间为5s,我们可以主线程第4s的还没完成的时候,提前finishInputEvent,然后我们自行启动异步监控,比如我们决定在第6s ANR,如果主线程的任务在第6s没有结束,我们就下面的方法,来触发ANR。

android.app.ActivityManager#appNotResponding

java 复制代码
public void appNotResponding(@NonNull final String reason) {
    try {
        getService().appNotResponding(reason);
    } catch (RemoteException e) {
        throw e.rethrowFromSystemServer();
    }
}

监控ANR

很多ANR的监控都在Native 层监控Sig_Quit信号,也有通过Looper.Printer进行检测到异常后,轮询AMS的的相关接口。

但是这里都可以做到对ANR的控制了,角色由消费者变成生产者,这种情况下自身就不需要监控了,只需要通知是否产生ANR。

实现逻辑

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

java 复制代码
public class AnrMonitorDialog extends Dialog {

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

    static {
        AnrMonitorThread.start();
    }

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

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


  
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Window window = getWindow();
        window.requestFeature(Window.FEATURE_NO_TITLE);
        View view = new View(getContext());
        view.setFocusableInTouchMode(false);
        view.setFocusable(false);
        setContentView(view);
        if (isFullScreen) {
            window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
        } else {
            window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
        }
        WindowManager.LayoutParams attributes = window.getAttributes();
        attributes.format = PixelFormat.TRANSPARENT;
        attributes.dimAmount = 0f;
        attributes.flags |= FLAG_NOT_FOCUSABLE;
        window.setBackgroundDrawable(new ColorDrawable(0x00000000));
        window.setLayout(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.MATCH_PARENT);

        setCancelable(false);
        setCanceledOnTouchOutside(false);

    }


    public static void hideDialog(DialogInterface dialog) {
        if (dialog == null) return;
        sAnrMonitorHandler.post(new Runnable() {
            @Override
            public void run() {
                dialog.dismiss();
            }
        });
    }


    public static void showDialog(final Activity activity, final Window window, OnShowListener onShowListener) {
        sAnrMonitorHandler.post(new Runnable() {
            @Override
            public void run() {
          
                if(activity.isFinishing()){
                    return;
                }
                AnrMonitorDialog anrMonitorDialog = new AnrMonitorDialog(activity, window);
                anrMonitorDialog.setOnShowListener(onShowListener);
                anrMonitorDialog.show();
            }
        });
    }
    
   // 省略一堆关键代码
}

在实现的过程中,我们可以复写Dialog的一些方法,当然你还可以给Dialog的Window设置Window.Callback。这里要说的一点是,一些设备自定义了特殊的实现,如dispatchFnKeyEvent,显然系统类中没有这个方法,但是如果你要实现的话无法通过super关键字调用,解决办法也是有的,就是利用Java 7中的MethodHandle动态invoke,这里我们暂不实现了,毕竟这个KeyEvent一般APP也用不到。

java 复制代码
/**
 *  fixed Lenovo/Sharp Android 4.4.3 dispatchFnKeyEvent
 *  不要删除 dispatchFnKeyEvent 方法
 */
@Keep
public boolean dispatchFnKeyEvent(KeyEvent event) {
    //可以利用MethodHandle调用父类的方法
    return false;
}

这里我们复写Dialog的一些方法,我们以TouchEvent的传递为例子,当我们拿到MotionEvent的时候,我们就能将event转发给主线程。其实这里最稳妥的方法是对事件复制,因为MotionEvent是可以被recycle的,如果不复制就会被异步修改。

java 复制代码
@Override
public boolean dispatchTouchEvent(final MotionEvent event) {
    final Waiter waiter = new Waiter();

    final MotionEvent targetEvent = copyMotionEvent(event);
    mainHandler.post(new Runnable() {
        @Override
        public void run() {
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    break;
                case MotionEvent.ACTION_UP:
                case MotionEvent.ACTION_CANCEL:
                    break;
            }
            boolean isHandled = mHost.dispatchTouchEvent(targetEvent);
            targetEvent.recycle();  //自己拷贝的事件,需要主动回收
            waiter.countDown(isHandled);
        }
    });
    try {
        if(!waiter.await(4000, TimeUnit.MILLISECONDS)){
            sAnrMonitorHandler.postAtTime(mAnrTimeoutTask, SystemClock.uptimeMillis() + 2000L);
            mainHandler.postAtFrontOfQueue(mCancelAnrTimeoutTask);
        }
    } catch (InterruptedException e) {
        e.printStackTrace();

    }

    return waiter.isHandled;
}
  • mAnrTimeoutTask 负责触发ActivityManager#appNotResponding
  • mCancelAnrTimeoutTask 用于取消sAnrMonitorHandler的定时逻辑
java 复制代码
private Runnable mAnrTimeoutTask = new Runnable() {
    @Override
    public void run() {
        sendAppNotResponding("Dispatching Timeout");
    }
};

private Runnable mCancelAnrTimeoutTask = new Runnable() {
    @Override
    public void run() {
        sAnrMonitorHandler.removeCallbacks(mAnrTimeoutTask);
    }
};

原理是,如果在指定的时间没有取消,说明主线程是卡住了,我们可以不抛ANR,但是点击之后卡住不动,任何人的心情都会很难受,抑制ANR发生并不可取,但是我们可以借助这些时间段收集一些线程状态和内存信息,以及业务信息,提高ANR上报率和场景覆盖。

那么Waiter是什么呢,其实是CountDownLatch的子类,我们简单封装一下,来等待事件完成。

java 复制代码
static class Waiter extends CountDownLatch {
    boolean isHandled = false;
    public Waiter() {
        super(1);
    }

    public void countDown(boolean isHandled){
        this.isHandled = isHandled;
        super.countDown();
    }
    @Override
    public void countDown() {
         throw new Exception("I like along, don't call me");
    }
}

用法

很简单,我们在BaseActivity的onCreate中加入即可

java 复制代码
AnrMonitorDialog.showDialog(this, getWindow(), new DialogInterface.OnShowListener() {
    @Override
    public void onShow(DialogInterface dialog) {
        anrMonitorDialog = dialog;  
         //由于是异步返回的的dialog,这里要做二次检测,防止InputChannel泄漏
        postToMain(new Runnable(){
          if(actvitiyIsFinish()){ 
              AnrMonitorDialog.hideDialog(anrMonitorDialog);
              anrMonitorDialog = null;
           
          }
        });
    }
});

不过,我们一定要在onDestoryed中关闭Dialog,避免InputChannel泄漏

java 复制代码
@Override
protected void onDestroy() {
    AnrMonitorDialog.hideDialog(anrMonitorDialog);
    super.onDestroy();
}

测试效果

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

另一种可行方案: HOOK MessageQueue

然后在Native层监控MessageQueue的事件输入也是一种可行的方案,动态转发事件到主线程,我们要Hook MessageQueue中的Native Looper,这种有一定的可行性,我这里没有试过,有时间可以试一下。

总结

通过上面的实现,我们将异步线程创建的全屏Dialog覆盖到Activity上面,然后通过Dialog转发事件到Activity,从而实现了在Java层就能监控和阻断InputDispatching ANR。当然,如果是双屏异显的逻辑也可以实现,不过就得使用之前我一篇文章中实现的Dialog《Android 双屏异显自适应Dialog

不过,这里也有些可能的问题,具体我们有测试,但可能会存在。

  • 焦点问题:由于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 。

另一个要考虑的点是实用性,目前来说,这种方法在特定场景下还是比较实用的,比如调试环境,我们遇到一类问题,就是DEBUG时间太长,一些系统中AMS直接将APP进程杀死;

还有就是一些系统,如果出现ANR,连Native层SIGQUIT信号可能都来不及接收就直接force-stop进程的情况。

总之,这属于一种Java层监控ANR的方案,目前来说还有很多不足,但是至少来说,解决调试时ANR进程被杀问题还是可以的。

引申思考

这种方法不是系统漏洞,但是我们这里要思考的一个问题是,是不是还有更加优秀的方案,我们时常能看到各种系统源码分析的博客,是不是还有其他捷径可以走呢?

相关推荐
黄林晴2 小时前
如何判断手机是否是纯血鸿蒙系统
android
火柴就是我2 小时前
flutter 之真手势冲突处理
android·flutter
法的空间2 小时前
Flutter JsonToDart 支持 JsonSchema
android·flutter·ios
循环不息优化不止3 小时前
深入解析安卓 Handle 机制
android
恋猫de小郭3 小时前
Android 将强制应用使用主题图标,你怎么看?
android·前端·flutter
jctech3 小时前
这才是2025年的插件化!ComboLite 2.0:为Compose开发者带来极致“爽”感
android·开源
用户2018792831673 小时前
为何Handler的postDelayed不适合精准定时任务?
android
叽哥3 小时前
Kotlin学习第 8 课:Kotlin 进阶特性:简化代码与提升效率
android·java·kotlin
Cui晨3 小时前
Android RecyclerView展示List<View> Adapter的数据源使用View
android
氦客3 小时前
Android Doze低电耗休眠模式 与 WorkManager
android·suspend·休眠模式·workmanager·doze·低功耗模式·state_doze