1、问题背景
使用RecyclerView管理item,由于异步数据获取,在做页面编辑的时候,由于手机和内容本身差异,总会有延迟或者卡顿,不可避免的会出现数据变化了没有及时更新的问题,导致两个崩溃出现。
Fatal Exception: java.lang.IndexOutOfBoundsException
Inconsistency detected. Invalid item position 4(offset:4).state:44
androidx.recyclerview.widget.RecyclerView$Recycler.tryGetViewHolderForPositionByDeadline (RecyclerView.java:6364)
androidx.recyclerview.widget.GapWorker.prefetchPositionWithDeadline (GapWorker.java:288)
androidx.recyclerview.widget.GapWorker.flushTaskWithDeadline (GapWorker.java:345)
androidx.recyclerview.widget.GapWorker.flushTasksWithDeadline (GapWorker.java:361)
androidx.recyclerview.widget.GapWorker.prefetch (GapWorker.java:368)
Fatal Exception: java.lang.IndexOutOfBoundsException
Inconsistency detected. Invalid view holder adapter positionContentViewHolder{355881d position=29 id=-1, oldPos=-1, pLpos:-1 no parent}
androidx.recyclerview.widget.RecyclerView$Recycler.validateViewHolderForOffsetPosition (RecyclerView.java:6156)
androidx.recyclerview.widget.RecyclerView$Recycler.tryGetViewHolderForPositionByDeadline (RecyclerView.java:6339)
androidx.recyclerview.widget.GapWorker.prefetchPositionWithDeadline (GapWorker.java:288)
androidx.recyclerview.widget.GapWorker.flushTaskWithDeadline (GapWorker.java:345)
androidx.recyclerview.widget.GapWorker.flushTasksWithDeadline (GapWorker.java:361)
androidx.recyclerview.widget.GapWorker.prefetch (GapWorker.java:368)
androidx.recyclerview.widget.GapWorker.run (GapWorker.java:399)
android.os.Handler.handleCallback (Handler.java:938)
intcut("execution(* androidx.recyclerview.widget.GapWorker.prefetchPositionWithDeadline(..))")
我们尝试了线程里编辑、编辑后延迟操作等方案,都未能杜绝,只是概率降低了,测试很难复现。但崩溃量确长期排第一。
2、方案探索
数据侧没办法改变同步,只能考虑改变RecyclerView源码逻辑,阻止异常发生或者抛出。
2.1、修改源码逻辑,防止异常发生。
通过查看逻辑,是因为状态错误,抛出异常没有捕获。而这些处理流程都是RecyclerView的private方法,重写父类方法不可行。
复制RecyclerView源码修改,代码量太大,以后升级维护都有风险。
方案不可行。
2.2、反射注入修改
寻找反射点,GapWorker是final,不能继承,反射注入方式也不行。
2.3、编译时技术
RecyclerView是AAR包里代码,要修改就得借助编译时技术,需要考虑自定义插件。技术有javassist和ASM,
在尝试开发javassist插件过程中,我们XAOP框架也有编译修改插件,于是借助AspectJ方案,修改最简单。
于是有了下面方案,直接上代码:
@Aspect
public class CatchRecyclerviewAspectJ {
@Pointcut("execution(* androidx.recyclerview.widget.GapWorker.prefetchPositionWithDeadline(..))")
public void prefetchPositionWithDeadlineAspectJ() {
// Not used
} //方法切入点
@Pointcut("execution(* androidx.recyclerview.widget.RecyclerView.removeAnimatingView(..))")
public void removeAnimatingViewAspectJ() {
// Not used
} //方法切入点
@SuppressLint("LongLogTag")
@Around("prefetchPositionWithDeadlineAspectJ()|| removeAnimatingViewAspectJ()")//在连接点进行方法替换
public Object aroundJoinPoint(final ProceedingJoinPoint joinPoint) throws Throwable {
Object result = null;
try {
result = joinPoint.proceed();
} catch (Throwable e) {
Log.e("CatchRecyclerviewAspectJ catch --- ", e.getMessage());
CrashReport.postCatchedException(e); // bugly会将这个throwable上报
}
return result;
}
}
但遇到一个坑,反编译apk代码发现注解没有生效,反复排查后发现是因为aspectjx插件没有配置目标类的包名支持。
aspectjx {
include 'androidx.recyclerview.widget'
}
加入后断点调试正常:
firebase自定义消息弹窗崩溃,也一样拿下:
Fatal Exception: android.view.WindowManager$BadTokenException
Unable to add window -- token null is not valid; is your activity running?
android.view.ViewRootImpl.setView (ViewRootImpl.java:1351)
android.view.WindowManagerImpl.addView (WindowManagerImpl.java:148)
com.google.firebase.inappmessaging.display.internal.FiamWindowManager.show (FiamWindowManager.java:67)
com.google.firebase.inappmessaging.display.FirebaseInAppMessagingDisplay$4$4.run (FirebaseInAppMessagingDisplay.java:416)
android.app.Activity.runOnUiThread (Activity.java:7412)
com.google.firebase.inappmessaging.display.FirebaseInAppMessagingDisplay$4.onSuccess (FirebaseInAppMessagingDisplay.java:412)
com.google.firebase.inappmessaging.display.internal.FiamImageLoader$Callback.onResourceReady (FiamImageLoader.java:157)
com.google.firebase.inappmessaging.display.internal.FiamImageLoader$Callback.onResourceReady (FiamImageLoader.java:135)
com.bumptech.glide.request.SingleRequest.onResourceReady (SingleRequest.java:667)
com.bumptech.glide.request.SingleRequest.onResourceReady (SingleRequest.java:596)
com.bumptech.glide.load.engine.Engine.load (Engine.java:220)
@Pointcut("execution(* com.google.firebase.inappmessaging.display.internal.FiamWindowManager.show(..))")
public void fiamWindowManagerShowAspectJ() {
// Not used
} //方法切入点