Android监控和阻断InputDispatching ANR的方法
目录
- 前言
- 本篇前奏
- 失败的Socket FD 监听方案
- 失败InputEventReceiver中间件方案
- ANR Monitor Dialog方案
- ViewRootImpl 与 Windowsession关系
- Window 层级
- ViewRootImpl异步渲染
- 核心原理
- 阻断ANR 产生
- 延长ANR 阈值
- 监控ANR
- AnrMonitorDialog 实现逻辑
- 用法
- 测试效果
- 评价
- InputEventCompatProcessor方案
- 反隐藏类
- InputEventCompatProcessor 事件异步转发实现
- 注入新的InputEventReceiver
- 用法
- 测试效果
- 评价
- 总结
前言
如何在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] php 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 IllegalArHMyPQpbOgumentException("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,这里要做二次检测,防止InputChaphpnnel泄漏 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方式引入项目中 比如ViewRootImplHMyPQpbO 的空实现
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)www.devze.com; } }; 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的资料请关注编程客栈(www.devze.com)其它相关文章!
精彩评论