Android監(jiān)控和阻斷InputDispatching ANR的方法
前言
如何在Java層實現異步監(jiān)控和阻斷InputDispatching ANR?我相信這是很多開發(fā)者都想要的功能。
本篇,我們會通過“探索”兩種方案來實現在Java層監(jiān)控&阻斷的方法
Android版本發(fā)展已經趨于穩(wěn)定,各種AMP工具都已經很成熟了,甚至很多人都能背出來具體實現。但是,仍然有一些東西我們要回過頭去看,過去我們認為不能或者很難實現的東西,或許是因為我們很少去質疑。
任何時候都要重新審視一下過去的方法。
有時候解決問題的方法并不只有一種,我們要質疑為什么選的是不是最好用的一種。一些人的代碼,提前引入現有需求不需要的邏輯是否合理?還有就是,為了解決一個小問題,比如解決相似圖片的問題,結果完整引入了opencv,引入這樣一個很大的框架是否合理?這些都需要去質疑。
本篇前奏
這里,我們簡單了解下事件傳遞和一些嘗試方案,如果不看本節(jié),其實影響不大,可直接跳至下一節(jié)。
我們回到本篇主題,我們如何才能使用Java代碼實現InputEvent ANR 監(jiān)控和阻斷呢,我們先來看這樣一張圖。我為什么選擇這一張圖呢,因為它很經典,雖然我在上面稍微改造了一下。
當然,上圖缺少WindowSesssion的角色,實際上,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對象 } //通過Binder調用,進入system進程的Session[見小節(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對象[見3.1] mInputEventReceiver = new WindowInputEventReceiver(mInputChannel, Looper.myLooper()); } } }
在這里我們可以看到,事件傳遞是通過InputChannel實現,而InputChannel負責事件發(fā)送、事件應答兩部分,因此,肯定能雙向通信,那么是不是Binder呢?
實際上,InputChannel在底層是Socket實現
status_t InputChannel::openInputChannelPair(const String8& name, sp<InputChannel>& outServerChannel, sp<InputChannel>& outClientChannel) { int sockets[2]; //真正創(chuàng)建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)"); //創(chuàng)建InputChannel對象 outServerChannel = new InputChannel(serverChannelName, sockets[0]); String8 clientChannelName = name; clientChannelName.append(" (client)"); //創(chuàng)建InputChannel對象 outClientChannel = new InputChannel(clientChannelName, sockets[1]); return OK; }
InputChannel既創(chuàng)建Server又創(chuàng)建Client,看著是很奇怪的行為,事實上,在Linux中通信是通過Fd就能實現,而InputChannel是Parcelable的子類,可以把FD發(fā)送至WMS.
失敗的Socket FD 監(jiān)聽方案
其實上面的這些代碼和本篇關系不大,為什么要貼出代碼呢,主要原因是我之前嘗試過監(jiān)聽Socket的FD,可問題是InputChannel的FD拿不到,除非ChannelName為空,但是上面兩個都有ChannelName,然后我就去找有沒有讓Name為空的方法,很遺憾也沒有。
因此,這種實現只能借助Native Hook暴露接口,難度也有些大,因此,只能放棄這種方案了。
失敗InputEventReceiver中間件方案
于是我找到另一種方案,在ViewRootImple#WindowInputEventReceiver 和 InputChannel之間插入一個MiddleWareInputEventReceiver,經過大量推斷,將ViewRootImple#WindowInputEventReceiver dispose了,然后會發(fā)現,事件消費問題無法處理,因為ViewRootImple#WindowInputEventReceiver 調用finishInputEvent的方法無法調用到MiddleWareInputEventReceiver。
為什么做這種嘗試呢,主要還是下面一段代碼,我們可以看到Looper,這個類是可以傳入Looper的,InputChannel之間插入一個MiddleWareInputEventReceiver異步監(jiā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方案
上面的方案中,實現復雜且穩(wěn)定性很差,或許只有通過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系統(tǒng)中,Window層級越高,意味著權限越大,假設你的彈窗能展示在系統(tǒng)彈窗(如指紋識別彈窗)的上面,那么你就可以做一些看不見的事。當然google是不會讓你這么做的,Google大費周折關聯Window Token,就是為了修復此類風險。
那么,還意味著什么?
我們還知道,層級越高,SurfsceFlinger中展示順序的優(yōu)先級越高,主線程和RenderThread線程優(yōu)先級越高,同時線程調度的優(yōu)先級越高,當然,和本篇有關的是,接收【事件】順序的優(yōu)先級越高。
ViewRootImpl異步渲染
實際上,很多時候容易被忽略的一件事是,ViewRootImpl其實是支持異步渲染的,同樣Choreographer也是支持異步的。為什么這樣說呢?
因為現成的例子:android.app.Dialog
在Android系統(tǒng)中,Dialog是支持異步彈出的,這也就是為什么其內部的Handler是沒有綁定主線程Looper的原因。
核心原理
通過上面3個知識點,我們就可以做到一件事
在Activity ViewRootImpl上面加一個異步創(chuàng)建的Dialog,然后將Dialog接收的事件通過主線程Handler轉發(fā)給Activity。
很顯然,上面的方法是可行的。
那么,我們是不是可以做更多的事情呢?
答案是:是的。
阻斷ANR 產生
我們可以為了避免InputEventDispatcher ANR,在Dialog異步線程中,提前讓InputEventReceiver的finishInputEvent方法調用,這樣就能避免ANR。
延長ANR 閾值
我們知道,InputEventDispatcher Timeout時間為5s,我們可以主線程第4s的還沒完成的時候,提前finishInputEvent,然后我們自行啟動異步監(jiān)控,比如我們決定在第6s ANR,如果主線程的任務在第6s沒有結束,我們就下面的方法,來觸發(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信號,也有通過Looper.Printer進行檢測到異常后,輪詢AMS的的相關接口。
但是這里都可以做到對ANR的控制了,角色由消費者變成生產者,這種情況下自身就不需要監(jiān)控了,只需要通知是否產生ANR。
AnrMonitorDialog 實現邏輯
首先,我們我們來定義一個Dialog,實際上,Dialog會影響狀態(tài)欄和底部導航欄的樣式,因此,對于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,顯然系統(tǒng)類中沒有這個方法,但是如果你要實現的話無法通過super關鍵字調用,解決辦法也是有的,就是利用Java 7中的MethodHandle動態(tài)invoke,這里我們暫不實現了,畢竟這個KeyEvent一般APP也用不到。
/** * fixed Lenovo/Sharp Device * */ @Keep public boolean dispatchFnKeyEvent(KeyEvent event) { //可以利用MethodHandle調用父類的方法 return false; }
這里我們復寫Dialog的一些方法,我們以TouchEvent的傳遞為例子,當我們拿到MotionEvent的時候,我們就能將event轉發(fā)給主線程。其實這里最穩(wěn)妥的方法是對事件復制,因為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 負責觸發(fā)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發(fā)生并不可取,但是我們可以借助這些時間段收集一些線程狀態(tài)和內存信息,以及業(yè)務信息,提高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跳轉也是正常的。
評價
通過上面的實現,我們將異步線程創(chuàng)建的全屏Dialog覆蓋到Activity上面,然后通過Dialog轉發(fā)事件到Activity,從而實現了在Java層就能監(jiān)控和阻斷InputDispatching ANR。
不過,這里也有些可能的問題,具體我們有測試,但可能會存在。
- 焦點問題:由于ViewRootImpl 內部有焦點處理邏輯,如果把事件直接給Window.Callback可能還不合適,因此,如果是TV版本開發(fā),還可能需要從DecorView層面進一步兼容一下,不過測試過程中發(fā)現大部分走焦邏輯是正常的,暫沒有發(fā)現特別嚴重的問題。
- 一些低級別WindowType的彈窗無法攔截事件:實際上,在Android中,WindowType一樣的話,后面的彈窗會覆蓋到上面,但是對于一些魔改的系統(tǒng),可能存在問題,但是解決辦法就是調整WindowType,其次,AnrMonitorDialog要盡可能早一些彈出
- 僅限于對Activity的事件監(jiān)控: 本篇方案僅限于對Activity的的監(jiān)控,但如果是想支持其他Dialog,那么要保證AnrMonitorDialog 有更高的層級,同時要能支持其他Dialog的Window.Callback獲取,當然,最好的方式就是從WindowManagerGlobal中獲取次一級的ViewRootImpl,然后想辦法獲取DecorView
- 輸入法問題:由于部分系統(tǒng)輸入法在Dialog下面,按道理輸入法層級更高才是,且輸入法不屬于app自身的UI,因此無法點擊。我們要做2件事才能實現兼容: ①監(jiān)聽全局焦點,如果移動到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 進行處理,便能實現事件監(jiān)控和阻斷。
當然,為了避免重復處理,我們要返回的processedEvents 為EmptyList即可。
反隱藏類
顯然我們需要反隱藏,我們需要反射一些方法,這里推薦使用《FreeFlection》開源項目去開啟反射。
不過,為了能繼承InputEventCompatProcessor,我們就需要一些新的手段
我們要Hook被@hide標記的類實際上是不行的,因此我們可以在Android Studio中創(chuàng)建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 事件異步轉發(fā)實現
下面是核心實現,當然,ANR 監(jiān)控部分和Dialog類似了,這里的監(jiān)控和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方法中進行監(jiān)控
override fun onCreate(savedInstanceState: Bundle?) { enableEdgeToEdge(statusBarStyle = SystemBarStyle.dark(Color.TRANSPARENT)) super.onCreate(savedInstanceState) AnrInterceptor.monitor(this) }
測試效果
可以完美兼容焦點模式和觸屏兩種模式,效果相對ANR Monitor Dialog更好,不需要處理鍵盤、窗口層級,同時也避免了很多復雜的事件轉發(fā)。
評價
相比ANR Monitor Dialog而言,這種方法的穩(wěn)定性相對差一些,同時需要大量反射,最重要的一點是無法兼容到Android 10之前的版本。
總結
本篇實現了2種ANR 監(jiān)控方案 ANR Monior Dialog 和InputEventCompatProcessor 各自都有優(yōu)點和缺點,總體上,如果是Android 10+版本的系統(tǒng),建議使用后者。
目前來說,這兩種方法在特定場景下還是比較實用的,比如調試環(huán)境,我們遇到一類問題,就是DEBUG時間太長,一些系統(tǒng)中AMS直接將APP進程殺死;
還有就是一些系統(tǒng),如果出現ANR,連Native層SIGQUIT信號可能都來不及接收就直接force-stop進程的情況。
總之,這屬于一種Java層監(jiān)控ANR的方案,目前來說還有很多不足,但是至少來說,解決調試時ANR進程被殺問題還是可以的,當然,能否線上使用,目前還有一些事情要處理。
以上就是Android監(jiān)控和阻斷InputDispatching ANR的方法的詳細內容,更多關于Android InputDispatching ANR的資料請關注腳本之家其它相關文章!
相關文章
舉例講解Android中ViewPager中的PagerTitleStrip子控件
這篇文章主要介紹了Android中ViewPager中的PagerTitleStrip子控件使用例子,講解了PagerTitleStrip子控件的嵌入與設置標題的用法,需要的朋友可以參考下2016-03-03Android顯示系統(tǒng)SurfaceFlinger詳解
本文詳細講解了Android顯示系統(tǒng)SurfaceFlinger,文中通過示例代碼介紹的非常詳細。對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-12-12Android 模擬器(emulator-5554...)出現錯誤解決辦法
這篇文章主要介紹了Android 模擬器出現錯誤解決辦法的相關資料,如:Unable to get view server version from device,Failed to install helloworld.apk on device 'emulator-5554': timeout,這種常見錯誤,解決辦法,需要的朋友可以參考下2016-11-11Android 調用notifyDataSetChanged方法失敗解決辦法
這篇文章主要介紹了Android 調用notifyDataSetChanged方法失敗解決辦法的相關資料,需要的朋友可以參考下2017-07-07