当前位置: 代码网 > it编程>App开发>Android > Android监控和阻断InputDispatching ANR的方法

Android监控和阻断InputDispatching ANR的方法

2024年05月28日 Android 我要评论
前言如何在java层实现异步监控和阻断inputdispatching anr?我相信这是很多开发者都想要的功能。本篇,我们会通过“探索”两种方案来实现在java层监控&

前言

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

本篇,我们会通过“探索”两种方案来实现在java层监控&阻断的方法

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

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

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

本篇前奏

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

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

当然,上图缺少windowsesssion的角色,实际上,viewrootimpl和windowmanagerservice通信少不了windowsession,那么windowsession是如何通信的呢,我们继续往下看。

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实现

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。

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");
}

anr monitor dialog方案

上面的方案中,实现复杂且稳定性很差,或许只有通过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上面。

注意: windowtype如果相等,那么后面加入的viewrootimpl层级也是高于前面的。

 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

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。

anrmonitordialog 实现逻辑

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

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也用不到。

/**
 *  fixed lenovo/sharp device 
 *
 */
@keep
public boolean dispatchfnkeyevent(keyevent event) {
    //可以利用methodhandle调用父类的方法
    return false;
}

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

@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的定时逻辑
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的子类,我们简单封装一下,来等待事件完成。

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中加入即可

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泄漏

@override
protected void ondestroy() {
    anrmonitordialog.hidedialog(anrmonitordialog);
    super.ondestroy();
}

测试效果

经过测试,在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的源码部分

      @override
        public void oninputevent(inputevent event) {
            trace.tracebegin(trace.trace_tag_view, "processinputeventforcompatibility");
            list<inputevent> processedevents;
            try {
                processedevents =
                    minputcompatprocessor.processinputeventforcompatibility(event);
            } finally {
                trace.traceend(trace.trace_tag_view);
            }
            if (processedevents != null) {
                if (processedevents.isempty()) {
                    // inputevent consumed by minputcompatprocessor
                    finishinputevent(event, true);
                } else {
                    for (int i = 0; i < processedevents.size(); i++) {
                        enqueueinputevent(
                                processedevents.get(i), this,
                                queuedinputevent.flag_modified_for_compatibility, true);
                    }
                }
            } else {
                enqueueinputevent(event, this, 0, true);
            }
        }

上面的代码中,如果我们将windowinputeventreceiver的looper设置为异步的,然后,我们直接将后面的逻辑移动到processinputeventforcompatibility 进行处理,便能实现事件监控和阻断。

当然,为了避免重复处理,我们要返回的processedevents 为emptylist即可。

反隐藏类

显然我们需要反隐藏,我们需要反射一些方法,这里推荐使用《freeflection》开源项目去开启反射。

不过,为了能继承inputeventcompatprocessor,我们就需要一些新的手段

我们要hook被@hide标记的类实际上是不行的,因此我们可以在android studio中创建moudle,将这些被@hide类标记的空实现加入到 android.view包名下,然后通过compileonly方式引入项目中 比如viewrootimpl 的空实现

package android.view;
public class viewrootimpl {  
}

那么inputeventcompatprocessor也是同理

package android.view;  
  
import android.content.context;  
import java.util.list;  
  
public class inputeventcompatprocessor {  
    protected context mcontext;  
    public inputeventcompatprocessor(context context) {  
        mcontext = context;  
    }  

    public list<inputevent> processinputeventforcompatibility(inputevent e) {  
        return null;  
    }  
    public inputevent processinputeventbeforefinish(inputevent e) {  
        // no changes needed  
        return e;  
    }  
}

其他类如inputchannel,inputeventreceiver也是如此

inputeventcompatprocessor 事件异步转发实现

下面是核心实现,当然,anr 监控部分和dialog类似了,这里的监控和anr阻断方式和anr monitor dialog类似,就不再重复了。

public class windowinputeventcompatprocessor extends inputeventcompatprocessor {

    private final inputeventcompatprocessor processor;
    private final inputeventreceiver eventreceiver;
    private viewrootimpl viewrootimpl;
    final handler mainhandler;
    private static final atomicinteger mnextseq = new atomicinteger();
    private sparseintarray eventmaps = new sparseintarray();
    private method enqueueinputevent;
    public static final int flag_modified_for_compatibility = 1 << 6;
    private handler anrhandler;

    private string tag = "windowinputeventcompatprocessor";
    private method finishinputevent;

    public windowinputeventcompatprocessor(context context, inputeventcompatprocessor processor, viewrootimpl viewrootimpl, inputeventreceiver eventreceiver) {
        super(context);
        this.processor = processor;
        this.mainhandler = new handler(looper.getmainlooper());
        this.viewrootimpl = viewrootimpl;
        this.eventreceiver = eventreceiver;
    }

    @override
    public list<inputevent> processinputeventforcompatibility(inputevent e) {

        if (anrhandler == null) {
            anrhandler = new handler(looper.mylooper());
        }

        inputevent copyevent = null;
        if(e instanceof keyevent){
            copyevent =  keyevent.changeflags((keyevent) e,((keyevent) e).getflags());
        }else if( e instanceof motionevent){
            copyevent = motionevent.obtain((motionevent) e);
        }

        if(copyevent == null){
            return collections.emptylist();
        }

        final inputevent event = copyevent;

        if(looper.mylooper() == looper.getmainlooper()){
            anrhandler.post(new runnable() {
              @override
              public void run() {
                  finishinputevent(e,true);
              }
          });
          anrhandler.postattime(manrtimeouttask,event, systemclock.uptimemillis() + 6000l);
          mainhandler.post(new runnable() {
              @override
              public void run() {
                  anrhandler.removecallbacks(manrtimeouttask,event);
              }
          });
          list<inputevent> processedevents = processor.processinputeventforcompatibility(event);
            if(processedevents == null){
                processedevents = new arraylist<>();
            }
            if(processedevents.isempty()){
                processedevents.add(event);
            }
      
          return processedevents;
        }

        eventmaps.append(event.hashcode(), mnextseq.getandincrement());
        anrhandler.postattime(manrtimeouttask,event, systemclock.uptimemillis() + 6000l);
        mainhandler.post(new runnable() {
            @override
            public void run() {

                anrhandler.removecallbacks(manrtimeouttask,event);

                list<inputevent> processedevents = processor.processinputeventforcompatibility(event);

                if (processedevents != null) {
                    if (processedevents.isempty()) {
                        // inputevent consumed by minputcompatprocessor
                        // finishinputevent(event, true);
                        //这里一定不要调用哦,防止外部重复调用
                    } else {
                        for (int i = 0; i < processedevents.size(); i++) {
                            enqueueinputevent(
                                    processedevents.get(i), eventreceiver,
                                    flag_modified_for_compatibility, true);
                        }
                    }
                } else {
                    //修改事件flag
                    enqueueinputevent(event, eventreceiver, flag_modified_for_compatibility, true);
                }

            }
        });

        return collections.emptylist();
    }

    private void finishinputevent(inputevent event, boolean ishandled) {
        try {
            if (finishinputevent == null) {
                finishinputevent = class.forname(inputeventreceiver.class.getname()).getdeclaredmethod("finishinputevent", inputevent.class, boolean.class);
                finishinputevent.setaccessible(true);
            }
            finishinputevent.invoke(eventreceiver, event,ishandled);
        } catch (exception e) {
            e.printstacktrace();
        }
    }


    private void enqueueinputevent(inputevent event, inputeventreceiver eventreceiver, int flags, boolean processimmediately) {
        try {
            if (enqueueinputevent == null) {
                enqueueinputevent = viewrootimpl.class.getdeclaredmethod("enqueueinputevent", inputevent.class, inputeventreceiver.class, int.class, boolean.class);
                enqueueinputevent.setaccessible(true);
            }
            enqueueinputevent.invoke(viewrootimpl, event, eventreceiver, flags, processimmediately);
        } catch (exception e) {
            e.printstacktrace();
        }
    }

    @override
    public inputevent processinputeventbeforefinish(final inputevent e) {
        final int hashcode = e.hashcode();
        runnable runnable = new runnable() {
            @override
            public void run() {
                int keyindex = eventmaps.indexofkey(hashcode);
                if (keyindex >= 0) {
                    eventmaps.removeat(keyindex);
                }
                processor.processinputeventbeforefinish(e);
            }
        };
        if(looper.mylooper() == anrhandler.getlooper()){
            runnable.run();
        }else {
            anrhandler.post(runnable);
        }
        return null;
    }


    private runnable manrtimeouttask = new runnable() {
        @override
        public void run() {
            appmanager.sendappnotresponding("dispatching timeout");
        }
    };

}

注入新的inputeventreceiver

我们需要在activity的oncreate方法中进行注入,当然,这里有大量反射,我们不仅仅需要重新注入windowinputeventreceiver,还需要注入新的inputeventcompatprocessor

public class anrinterceptor {

    static final handlerthread handlerthread = new handlerthread("anr-looper");
    static {
        if(build.version.sdk_int > build.version_codes.p){
            handlerthread.start();
        }

    }
    public static void monitor(final activity activity){

        if(build.version.sdk_int <= build.version_codes.p){
            return;
        }

        final view decorview = activity.getwindow().getdecorview();
        decorview.addonattachstatechangelistener(new view.onattachstatechangelistener() {
            @override
            public void onviewattachedtowindow(@nonnull view decorview) {
                viewrootimpl viewrootimpl = (viewrootimpl) decorview.getparent();
                try {
                    class viewrootimplklass = viewrootimpl.getclass();
                    field minputcompatprocessorfield = viewrootimplklass.getdeclaredfield("minputcompatprocessor");
                    minputcompatprocessorfield.setaccessible(true);
                    inputeventcompatprocessor inputeventcompatprocessor = (inputeventcompatprocessor) minputcompatprocessorfield.get(viewrootimpl);

                    if(inputeventcompatprocessor instanceof windowinputeventcompatprocessor){
                        return;
                    }

                    field minputeventreceiverfield = viewrootimplklass.getdeclaredfield("minputeventreceiver");
                    minputeventreceiverfield.setaccessible(true);
                    inputeventreceiver receiver = (inputeventreceiver) minputeventreceiverfield.get(viewrootimpl);

                    class<?> windowinputeventreceiverclass = receiver.getclass();
                    field inputchannelfield = class.forname(inputeventreceiver.class.getname()).getdeclaredfield("minputchannel");
                    inputchannelfield.setaccessible(true);
                    inputchannel inputchannel = (inputchannel) inputchannelfield.get(receiver);

                    constructor windowinputeventreceiverconstructor = windowinputeventreceiverclass.getdeclaredconstructor(viewrootimpl.class,inputchannel.class, looper.class);
                    windowinputeventreceiverconstructor.setaccessible(true);
                    inputeventreceiver inputeventreceiver = (inputeventreceiver) windowinputeventreceiverconstructor.newinstance(viewrootimpl,inputchannel,handlerthread.getlooper());

                    inputeventcompatprocessor windowinputeventcompatprocessor =  new windowinputeventcompatprocessor(activity,inputeventcompatprocessor,viewrootimpl,inputeventreceiver);
                    minputeventreceiverfield.set(viewrootimpl,inputeventreceiver);
                    minputcompatprocessorfield.set(viewrootimpl,windowinputeventcompatprocessor);

                    receiver.dispose();
                } catch (throwable e) {
                    e.printstacktrace();
                }
            }

            @override
            public void onviewdetachedfromwindow(@nonnull view v) {

            }
        });
    }
}

用法

在activity的oncreate方法中进行监控

override fun oncreate(savedinstancestate: bundle?) {  
    enableedgetoedge(statusbarstyle = systembarstyle.dark(color.transparent))  
    super.oncreate(savedinstancestate)  

    anrinterceptor.monitor(this)
}

测试效果

可以完美兼容焦点模式和触屏两种模式,效果相对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的资料请关注代码网其它相关文章!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2025  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com