亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

Android監(jiān)控和阻斷InputDispatching ANR的方法

 更新時(shí)間:2024年04月09日 11:51:16   作者:時(shí)光少年  
如何在Java層實(shí)現(xiàn)異步監(jiān)控和阻斷InputDispatching ANR?我相信這是很多開(kāi)發(fā)者都想要的功能,本篇,我們會(huì)通過(guò)“探索”兩種方案來(lái)實(shí)現(xiàn)在Java層監(jiān)控&阻斷的方法,需要的朋友可以參考下

前言

如何在Java層實(shí)現(xiàn)異步監(jiān)控和阻斷InputDispatching ANR?我相信這是很多開(kāi)發(fā)者都想要的功能。

本篇,我們會(huì)通過(guò)“探索”兩種方案來(lái)實(shí)現(xiàn)在Java層監(jiān)控&阻斷的方法

Android版本發(fā)展已經(jīng)趨于穩(wěn)定,各種AMP工具都已經(jīng)很成熟了,甚至很多人都能背出來(lái)具體實(shí)現(xiàn)。但是,仍然有一些東西我們要回過(guò)頭去看,過(guò)去我們認(rèn)為不能或者很難實(shí)現(xiàn)的東西,或許是因?yàn)槲覀兒苌偃ベ|(zhì)疑。

任何時(shí)候都要重新審視一下過(guò)去的方法。

有時(shí)候解決問(wèn)題的方法并不只有一種,我們要質(zhì)疑為什么選的是不是最好用的一種。一些人的代碼,提前引入現(xiàn)有需求不需要的邏輯是否合理?還有就是,為了解決一個(gè)小問(wèn)題,比如解決相似圖片的問(wèn)題,結(jié)果完整引入了opencv,引入這樣一個(gè)很大的框架是否合理?這些都需要去質(zhì)疑。

本篇前奏

這里,我們簡(jiǎn)單了解下事件傳遞和一些嘗試方案,如果不看本節(jié),其實(shí)影響不大,可直接跳至下一節(jié)。

我們回到本篇主題,我們?nèi)绾尾拍苁褂肑ava代碼實(shí)現(xiàn)InputEvent ANR 監(jiān)控和阻斷呢,我們先來(lái)看這樣一張圖。我為什么選擇這一張圖呢,因?yàn)樗芙?jīng)典,雖然我在上面稍微改造了一下。

當(dāng)然,上圖缺少WindowSesssion的角色,實(shí)際上,ViewRootImpl和WindowManagerService通信少不了WindowSession,那么WindowSession是如何通信的呢,我們繼續(xù)往下看。

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(); //創(chuàng)建InputChannel對(duì)象
      }
      //通過(guò)Binder調(diào)用,進(jìn)入system進(jìn)程的Session[見(jiàn)小節(jié)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);
          }
          //創(chuàng)建WindowInputEventReceiver對(duì)象[見(jiàn)3.1]
          mInputEventReceiver = new WindowInputEventReceiver(mInputChannel,
                  Looper.myLooper());
      }
    }
}

在這里我們可以看到,事件傳遞是通過(guò)InputChannel實(shí)現(xiàn),而InputChannel負(fù)責(zé)事件發(fā)送、事件應(yīng)答兩部分,因此,肯定能雙向通信,那么是不是Binder呢?

實(shí)際上,InputChannel在底層是Socket實(shí)現(xiàn)

status_t InputChannel::openInputChannelPair(const String8& name,
        sp<InputChannel>& outServerChannel, sp<InputChannel>& outClientChannel) {
    int sockets[2];
    //真正創(chuàng)建socket對(duì)的地方【核心】
    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)");
    //創(chuàng)建InputChannel對(duì)象
    outServerChannel = new InputChannel(serverChannelName, sockets[0]);

    String8 clientChannelName = name;
    clientChannelName.append(" (client)");
    //創(chuàng)建InputChannel對(duì)象
    outClientChannel = new InputChannel(clientChannelName, sockets[1]);
    return OK;
}

InputChannel既創(chuàng)建Server又創(chuàng)建Client,看著是很奇怪的行為,事實(shí)上,在Linux中通信是通過(guò)Fd就能實(shí)現(xiàn),而InputChannel是Parcelable的子類,可以把FD發(fā)送至WMS.

失敗的Socket FD 監(jiān)聽(tīng)方案

其實(shí)上面的這些代碼和本篇關(guān)系不大,為什么要貼出代碼呢,主要原因是我之前嘗試過(guò)監(jiān)聽(tīng)Socket的FD,可問(wèn)題是InputChannel的FD拿不到,除非ChannelName為空,但是上面兩個(gè)都有ChannelName,然后我就去找有沒(méi)有讓Name為空的方法,很遺憾也沒(méi)有。

因此,這種實(shí)現(xiàn)只能借助Native Hook暴露接口,難度也有些大,因此,只能放棄這種方案了。

失敗InputEventReceiver中間件方案

于是我找到另一種方案,在ViewRootImple#WindowInputEventReceiver 和 InputChannel之間插入一個(gè)MiddleWareInputEventReceiver,經(jīng)過(guò)大量推斷,將ViewRootImple#WindowInputEventReceiver dispose了,然后會(huì)發(fā)現(xiàn),事件消費(fèi)問(wèn)題無(wú)法處理,因?yàn)閂iewRootImple#WindowInputEventReceiver 調(diào)用finishInputEvent的方法無(wú)法調(diào)用到MiddleWareInputEventReceiver。

為什么做這種嘗試呢,主要還是下面一段代碼,我們可以看到Looper,這個(gè)類是可以傳入Looper的,InputChannel之間插入一個(gè)MiddleWareInputEventReceiver異步監(jiān)聽(tīng),然后轉(zhuǎn)發(fā)給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方案

上面的方案中,實(shí)現(xiàn)復(fù)雜且穩(wěn)定性很差,或許只有通過(guò)HOOK手段或者替換一些方法地址(ArtMethod)才能解決一些問(wèn)題。

我們本篇利用一種比較新穎的方案,純java實(shí)現(xiàn) 具體怎么實(shí)現(xiàn)的呢?

我們要先來(lái)確定以下幾種關(guān)系。

ViewRootImpl 與 WindowSession關(guān)系

先來(lái)看一張圖

在這張圖中,我們可以清楚的看到,ViewRootImpl和WindowManagerService是多對(duì)一的關(guān)系,但是我們也要知道,他們之間的IWindow和IWindowSesssion和ViewRootImpl也是一對(duì)一的關(guān)系,也就是說(shuō),一個(gè)ViewRootImpl對(duì)應(yīng)一個(gè)IWindow和IWindowSession。

因此,我們要明白,Activity中的PhoneWindow和WindowManagerService是沒(méi)有任何關(guān)系的,Activity中PhoneWindow也不負(fù)責(zé)管理如Dialog、PopWindow這樣的組件,最終是WindowManager負(fù)責(zé)管理的。

好了,我們?cè)倏聪乱粋€(gè)知識(shí)點(diǎn)

Window 層級(jí)

在Android中,Window是有層級(jí)關(guān)系的,當(dāng)然這種關(guān)系被google改來(lái)改去,如果要使用的話需要處理一些兼容性問(wèn)題。

目前來(lái)說(shuō),除了OVERLAY類型外,其他的都需要window Token來(lái)與Activity強(qiáng)行綁定,但這不是本篇的重點(diǎn),重點(diǎn)是,我們要知道為什么Dialog作為Activity的組件,會(huì)展示在Activity的上面。

主要原因是Activity的WindowType一般小于等于Dialog的WindowType (dialog的為T(mén)YPE_APPLICATION_ATTACHED_DIALOG),因此他能展示Activity上面。

注意: WindowType如果相等,那么后面加入的ViewRootImpl層級(jí)也是高于前面的。

 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系統(tǒng)中,Window層級(jí)越高,意味著權(quán)限越大,假設(shè)你的彈窗能展示在系統(tǒng)彈窗(如指紋識(shí)別彈窗)的上面,那么你就可以做一些看不見(jiàn)的事。當(dāng)然google是不會(huì)讓你這么做的,Google大費(fèi)周折關(guān)聯(lián)Window Token,就是為了修復(fù)此類風(fēng)險(xiǎn)。

那么,還意味著什么?

我們還知道,層級(jí)越高,SurfsceFlinger中展示順序的優(yōu)先級(jí)越高,主線程和RenderThread線程優(yōu)先級(jí)越高,同時(shí)線程調(diào)度的優(yōu)先級(jí)越高,當(dāng)然,和本篇有關(guān)的是,接收【事件】順序的優(yōu)先級(jí)越高。

ViewRootImpl異步渲染

實(shí)際上,很多時(shí)候容易被忽略的一件事是,ViewRootImpl其實(shí)是支持異步渲染的,同樣Choreographer也是支持異步的。為什么這樣說(shuō)呢?

因?yàn)楝F(xiàn)成的例子:android.app.Dialog

在Android系統(tǒng)中,Dialog是支持異步彈出的,這也就是為什么其內(nèi)部的Handler是沒(méi)有綁定主線程Looper的原因。

核心原理

通過(guò)上面3個(gè)知識(shí)點(diǎn),我們就可以做到一件事

在Activity ViewRootImpl上面加一個(gè)異步創(chuàng)建的Dialog,然后將Dialog接收的事件通過(guò)主線程Handler轉(zhuǎn)發(fā)給Activity。

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

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

答案是:是的。

阻斷ANR 產(chǎn)生

我們可以為了避免InputEventDispatcher ANR,在Dialog異步線程中,提前讓InputEventReceiver的finishInputEvent方法調(diào)用,這樣就能避免ANR。

延長(zhǎng)ANR 閾值

我們知道,InputEventDispatcher Timeout時(shí)間為5s,我們可以主線程第4s的還沒(méi)完成的時(shí)候,提前finishInputEvent,然后我們自行啟動(dòng)異步監(jiān)控,比如我們決定在第6s ANR,如果主線程的任務(wù)在第6s沒(méi)有結(jié)束,我們就下面的方法,來(lái)觸發(fā)ANR。

android.app.ActivityManager#appNotResponding

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

監(jiān)控ANR

很多ANR的監(jiān)控都在Native 層監(jiān)控Sig_Quit信號(hào),也有通過(guò)Looper.Printer進(jìn)行檢測(cè)到異常后,輪詢AMS的的相關(guān)接口。

但是這里都可以做到對(duì)ANR的控制了,角色由消費(fèi)者變成生產(chǎn)者,這種情況下自身就不需要監(jiān)控了,只需要通知是否產(chǎn)生ANR。

AnrMonitorDialog 實(shí)現(xiàn)邏輯

首先,我們我們來(lái)定義一個(gè)Dialog,實(shí)際上,Dialog會(huì)影響狀態(tài)欄和底部導(dǎo)航欄的樣式,因此,對(duì)于Activity而言,為了避免Dialog和Activity的點(diǎn)擊位置沒(méi)法對(duì)齊,我們需要將Activity的一些樣式同步到dialog上,下面是同步了全屏和非全屏兩種,實(shí)際過(guò)程可能還需要同步其他幾種。

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();
            }
        });
    }
    
   // 省略一堆關(guān)鍵代碼
}

在實(shí)現(xiàn)的過(guò)程中,我們可以復(fù)寫(xiě)Dialog的一些方法,當(dāng)然你還可以給Dialog的Window設(shè)置Window.Callback。這里要說(shuō)的一點(diǎn)是,一些設(shè)備自定義了特殊的實(shí)現(xiàn),如dispatchFnKeyEvent,顯然系統(tǒng)類中沒(méi)有這個(gè)方法,但是如果你要實(shí)現(xiàn)的話無(wú)法通過(guò)super關(guān)鍵字調(diào)用,解決辦法也是有的,就是利用Java 7中的MethodHandle動(dòng)態(tài)invoke,這里我們暫不實(shí)現(xiàn)了,畢竟這個(gè)KeyEvent一般APP也用不到。

/**
 *  fixed Lenovo/Sharp Device 
 *
 */
@Keep
public boolean dispatchFnKeyEvent(KeyEvent event) {
    //可以利用MethodHandle調(diào)用父類的方法
    return false;
}

這里我們復(fù)寫(xiě)Dialog的一些方法,我們以TouchEvent的傳遞為例子,當(dāng)我們拿到MotionEvent的時(shí)候,我們就能將event轉(zhuǎn)發(fā)給主線程。其實(shí)這里最穩(wěn)妥的方法是對(duì)事件復(fù)制,因?yàn)镸otionEvent是可以被recycle的,如果不復(fù)制就會(huì)被異步修改。

@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();  //自己拷貝的事件,需要主動(dòng)回收
            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 負(fù)責(zé)觸發(fā)ActivityManager#appNotResponding
  • mCancelAnrTimeoutTask 用于取消sAnrMonitorHandler的定時(shí)邏輯
private Runnable mAnrTimeoutTask = new Runnable() {
    @Override
    public void run() {
        sendAppNotResponding("Dispatching Timeout");
    }
};

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

原理是,如果在指定的時(shí)間沒(méi)有取消,說(shuō)明主線程是卡住了,我們可以不拋ANR,但是點(diǎn)擊之后卡住不動(dòng),任何人的心情都會(huì)很難受,抑制ANR發(fā)生并不可取,但是我們可以借助這些時(shí)間段收集一些線程狀態(tài)和內(nèi)存信息,以及業(yè)務(wù)信息,提高ANR上報(bào)率和場(chǎng)景覆蓋。

那么Waiter是什么呢,其實(shí)是CountDownLatch的子類,我們簡(jiǎn)單封裝一下,來(lái)等待事件完成。

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

用法

很簡(jiǎn)單,我們?cè)贐aseActivity的onCreate中加入即可

AnrMonitorDialog.showDialog(this, getWindow(), new DialogInterface.OnShowListener() {
    @Override
    public void onShow(DialogInterface dialog) {
        anrMonitorDialog = dialog;  
         //由于是異步返回的的dialog,這里要做二次檢測(cè),防止InputChannel泄漏
        postToMain(new Runnable(){
          if(actvitiyIsFinish()){ 
              AnrMonitorDialog.hideDialog(anrMonitorDialog);
              anrMonitorDialog = null;
           
          }
        });
    }
});

不過(guò),我們一定要在onDestoryed中關(guān)閉Dialog,避免InputChannel泄漏

@Override
protected void onDestroy() {
    AnrMonitorDialog.hideDialog(anrMonitorDialog);
    super.onDestroy();
}

測(cè)試效果

經(jīng)過(guò)測(cè)試,在Touch Event模式下,基本沒(méi)有出現(xiàn)問(wèn)題,滑動(dòng)和點(diǎn)擊都難正常,也不會(huì)出現(xiàn)遮擋,包括Activity跳轉(zhuǎn)也是正常的。

評(píng)價(jià)

通過(guò)上面的實(shí)現(xiàn),我們將異步線程創(chuàng)建的全屏Dialog覆蓋到Activity上面,然后通過(guò)Dialog轉(zhuǎn)發(fā)事件到Activity,從而實(shí)現(xiàn)了在Java層就能監(jiān)控和阻斷InputDispatching ANR。

不過(guò),這里也有些可能的問(wèn)題,具體我們有測(cè)試,但可能會(huì)存在。

  • 焦點(diǎn)問(wèn)題:由于ViewRootImpl 內(nèi)部有焦點(diǎn)處理邏輯,如果把事件直接給Window.Callback可能還不合適,因此,如果是TV版本開(kāi)發(fā),還可能需要從DecorView層面進(jìn)一步兼容一下,不過(guò)測(cè)試過(guò)程中發(fā)現(xiàn)大部分走焦邏輯是正常的,暫沒(méi)有發(fā)現(xiàn)特別嚴(yán)重的問(wèn)題。
  • 一些低級(jí)別WindowType的彈窗無(wú)法攔截事件:實(shí)際上,在Android中,WindowType一樣的話,后面的彈窗會(huì)覆蓋到上面,但是對(duì)于一些魔改的系統(tǒng),可能存在問(wèn)題,但是解決辦法就是調(diào)整WindowType,其次,AnrMonitorDialog要盡可能早一些彈出
  • 僅限于對(duì)Activity的事件監(jiān)控: 本篇方案僅限于對(duì)Activity的的監(jiān)控,但如果是想支持其他Dialog,那么要保證AnrMonitorDialog 有更高的層級(jí),同時(shí)要能支持其他Dialog的Window.Callback獲取,當(dāng)然,最好的方式就是從WindowManagerGlobal中獲取次一級(jí)的ViewRootImpl,然后想辦法獲取DecorView
  • 輸入法問(wèn)題:由于部分系統(tǒng)輸入法在Dialog下面,按道理輸入法層級(jí)更高才是,且輸入法不屬于app自身的UI,因此無(wú)法點(diǎn)擊。我們要做2件事才能實(shí)現(xiàn)兼容: ①監(jiān)聽(tīng)全局焦點(diǎn),如果移動(dòng)到TextView或EditText上,那么需要關(guān)閉AnrMonitorDialog彈窗 ② Hook windowManager來(lái)判斷是否有其他Dialog彈出,等到其他Dialog關(guān)閉后且焦點(diǎn)不在EidtText和TextView上之后,同時(shí)判斷鍵盤(pán)已經(jīng)收起之后,再恢復(fù)AnrMonitorDialog 。

InputEventCompatProcessor方案

在Android 10中,新增了InputEventCompatProcessor用來(lái)兼容事件,正因?yàn)槿绱耍覀儽憧墒褂闷湓趈ava層掛載hook,來(lái)繞過(guò)WindowInputEventReceiver無(wú)法被復(fù)寫(xiě)的問(wèn)題,下面是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設(shè)置為異步的,然后,我們直接將后面的邏輯移動(dòng)到processInputEventForCompatibility 進(jìn)行處理,便能實(shí)現(xiàn)事件監(jiān)控和阻斷。

當(dāng)然,為了避免重復(fù)處理,我們要返回的processedEvents 為EmptyList即可。

反隱藏類

顯然我們需要反隱藏,我們需要反射一些方法,這里推薦使用《FreeFlection》開(kāi)源項(xiàng)目去開(kāi)啟反射。

不過(guò),為了能繼承InputEventCompatProcessor,我們就需要一些新的手段

我們要Hook被@hide標(biāo)記的類實(shí)際上是不行的,因此我們可以在Android Studio中創(chuàng)建Moudle,將這些被@hide類標(biāo)記的空實(shí)現(xiàn)加入到 android.view包名下,然后通過(guò)compileOnly方式引入項(xiàng)目中 比如ViewRootImpl 的空實(shí)現(xiàn)

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 事件異步轉(zhuǎn)發(fā)實(shí)現(xiàn)

下面是核心實(shí)現(xiàn),當(dāng)然,ANR 監(jiān)控部分和Dialog類似了,這里的監(jiān)控和ANR阻斷方式和ANR Monitor Dialog類似,就不再重復(fù)了。

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);
                        //這里一定不要調(diào)用哦,防止外部重復(fù)調(diào)用
                    } 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方法中進(jìn)行注入,當(dāng)然,這里有大量反射,我們不僅僅需要重新注入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方法中進(jìn)行監(jiān)控

override fun onCreate(savedInstanceState: Bundle?) {  
    enableEdgeToEdge(statusBarStyle = SystemBarStyle.dark(Color.TRANSPARENT))  
    super.onCreate(savedInstanceState)  

    AnrInterceptor.monitor(this)
}

測(cè)試效果

可以完美兼容焦點(diǎn)模式和觸屏兩種模式,效果相對(duì)ANR Monitor Dialog更好,不需要處理鍵盤(pán)、窗口層級(jí),同時(shí)也避免了很多復(fù)雜的事件轉(zhuǎn)發(fā)。

評(píng)價(jià)

相比ANR Monitor Dialog而言,這種方法的穩(wěn)定性相對(duì)差一些,同時(shí)需要大量反射,最重要的一點(diǎn)是無(wú)法兼容到Android 10之前的版本。

總結(jié)

本篇實(shí)現(xiàn)了2種ANR 監(jiān)控方案 ANR Monior Dialog 和InputEventCompatProcessor 各自都有優(yōu)點(diǎn)和缺點(diǎn),總體上,如果是Android 10+版本的系統(tǒng),建議使用后者。

目前來(lái)說(shuō),這兩種方法在特定場(chǎng)景下還是比較實(shí)用的,比如調(diào)試環(huán)境,我們遇到一類問(wèn)題,就是DEBUG時(shí)間太長(zhǎng),一些系統(tǒng)中AMS直接將APP進(jìn)程殺死;

還有就是一些系統(tǒng),如果出現(xiàn)ANR,連Native層SIGQUIT信號(hào)可能都來(lái)不及接收就直接force-stop進(jìn)程的情況。

總之,這屬于一種Java層監(jiān)控ANR的方案,目前來(lái)說(shuō)還有很多不足,但是至少來(lái)說(shuō),解決調(diào)試時(shí)ANR進(jìn)程被殺問(wèn)題還是可以的,當(dāng)然,能否線上使用,目前還有一些事情要處理。

以上就是Android監(jiān)控和阻斷InputDispatching ANR的方法的詳細(xì)內(nèi)容,更多關(guān)于Android InputDispatching ANR的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • Android簡(jiǎn)單實(shí)現(xiàn)引導(dǎo)頁(yè)

    Android簡(jiǎn)單實(shí)現(xiàn)引導(dǎo)頁(yè)

    這篇文章主要為大家詳細(xì)介紹了Android簡(jiǎn)單實(shí)現(xiàn)引導(dǎo)頁(yè),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2021-04-04
  • Android下Activity全屏顯示實(shí)現(xiàn)方法

    Android下Activity全屏顯示實(shí)現(xiàn)方法

    這篇文章主要介紹了Android下Activity全屏顯示實(shí)現(xiàn)方法,以兩種不同的方法來(lái)實(shí)現(xiàn)這一技巧,非常具有實(shí)用性,需要的朋友可以參考下
    2014-10-10
  • Android View 布局流程(Layout)全面解析

    Android View 布局流程(Layout)全面解析

    這篇文章主要為大家全面解析了Android View 布局流程Layout,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2017-02-02
  • 舉例講解Android中ViewPager中的PagerTitleStrip子控件

    舉例講解Android中ViewPager中的PagerTitleStrip子控件

    這篇文章主要介紹了Android中ViewPager中的PagerTitleStrip子控件使用例子,講解了PagerTitleStrip子控件的嵌入與設(shè)置標(biāo)題的用法,需要的朋友可以參考下
    2016-03-03
  • Android顯示系統(tǒng)SurfaceFlinger詳解

    Android顯示系統(tǒng)SurfaceFlinger詳解

    本文詳細(xì)講解了Android顯示系統(tǒng)SurfaceFlinger,文中通過(guò)示例代碼介紹的非常詳細(xì)。對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2021-12-12
  • Android時(shí)間設(shè)置的3個(gè)小彩蛋分享

    Android時(shí)間設(shè)置的3個(gè)小彩蛋分享

    這篇文章主要給大家介紹了關(guān)于Android時(shí)間設(shè)置的3個(gè)小彩蛋,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),對(duì)各位Android開(kāi)發(fā)者們具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下
    2023-03-03
  • Android實(shí)現(xiàn)驗(yàn)證碼登錄

    Android實(shí)現(xiàn)驗(yàn)證碼登錄

    這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)驗(yàn)證碼登錄,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2021-03-03
  • Android 模擬器(emulator-5554...)出現(xiàn)錯(cuò)誤解決辦法

    Android 模擬器(emulator-5554...)出現(xiàn)錯(cuò)誤解決辦法

    這篇文章主要介紹了Android 模擬器出現(xiàn)錯(cuò)誤解決辦法的相關(guān)資料,如:Unable to get view server version from device,F(xiàn)ailed to install helloworld.apk on device 'emulator-5554': timeout,這種常見(jiàn)錯(cuò)誤,解決辦法,需要的朋友可以參考下
    2016-11-11
  • Android實(shí)現(xiàn)通過(guò)手勢(shì)控制圖片大小縮放的方法

    Android實(shí)現(xiàn)通過(guò)手勢(shì)控制圖片大小縮放的方法

    這篇文章主要介紹了Android實(shí)現(xiàn)通過(guò)手勢(shì)控制圖片大小縮放的方法,結(jié)合實(shí)例形式分析了Android控制圖片縮放的原理、實(shí)現(xiàn)步驟與相關(guān)操作技巧,需要的朋友可以參考下
    2016-10-10
  • Android 調(diào)用notifyDataSetChanged方法失敗解決辦法

    Android 調(diào)用notifyDataSetChanged方法失敗解決辦法

    這篇文章主要介紹了Android 調(diào)用notifyDataSetChanged方法失敗解決辦法的相關(guān)資料,需要的朋友可以參考下
    2017-07-07

最新評(píng)論