JavaFx卡顿监控思路

思路

其实基本上可视化应用都有个单独的UI线程处理UI更新事件,JavaFX也不例外。而单线程模型的特点利用事件循环机制,不断从消息队列里取消息,然后执行消息。所以,我们只要在取消息前和取消息后加入hook操作,就可以得出当前主线程的一个任务的执行时间。

当需要更新UI元素值时,如果不在UI线程,会抛出Not on FX application thread的异常。所以我们在子线程获取完数据后,需要用如下方式更新数据:Platform.runLater { /*update ui*/ }

步骤

  1. 找到UI线程的消息队列

  2. 找到消息执行点

  3. 在消息执行前后插入hook函数

  4. 统计消息执行时长

方案

其实思路和步骤很简单,主要是消息队列的获取,以及如何插入hook函数。

因为UI线程消息都是经过Platform.runLater { /*update ui*/ }抛进消息队列的(其他方式也一样,最终将排队进入消息队列),那么就从这个方法入手。执行如下:

Platform.runLater(runnable)->Toolkit.getToolkit().defer(runnable)->Application.invokeLater(runnable)->Application._invokeLater(runnable)->invokeLaterDispatcher.invokeLater(runnable)->deque.addLast(runnable)

那么,这个deque就是UI线程要获取执行的消息队列了。

接下来就是找到执行点,并查找到可以hook的地方。

查找hook函数

有些开发框架有自带的hook点(Android),开发者可以方便得在消息执行前后插入hook回调,进行统计计算。但很遗憾,在JavaFX中暂时没找到

字节码插装

编译时,在目标代码前后插入自定义逻辑。实现难度大。且Java1.8中,JavaFX是在JDK的源码中,编译时是不会编译进项目源码里的。

替换JDK源码

查找InvokeLaterDispatcher.class(分发器类)所在的jar包,并替换此class文件。理论感觉可行,但尚未尝试!!!

反射替换

查找查找InvokeLaterDispatcher实例。并替换实例中的deque成员变量,重写addLast方法,插入自定义的Runnable包装器,在自定义包装器中保存原执行的Runnable,并且在run方法中,执行原Runnable的run方法,使其能正常执行,那么此时就可以在run方法前后就可以插入hook函数,进行统计并计算了。

  1. 在方案中我们发现,Application.invokeLater(runnable)是个静态方法,静态方法只能调用静态方法和使用静态实例,那么可以从这入手。

  2. 进一步,在Application中有一个静态单例对象application,而invokeLaterDispatcher实例就是Application的成员对象。

  3. 找到invokeLaterDispatcher后,就可以找到deque成员变量,之后进行替换就可以了。

kotlin 复制代码
fun inject() {

//各个平台Application抽象类

//其中Mac平台实现类为MacApplication;win平台实现类为WinApplication。此外还有iOS平台,SWT平台等。

val appClass = Application::class.java

//Application抽象类中有个application的静态实例(),代表当前的全局Application

val macAppField = appClass.getDeclaredField("application")

macAppField.isAccessible = true

//获取静态application实例

val application = macAppField.get(null)

//InvokeLaterDispatcher是用于将延迟的可运行对象逐个提交到本机系统调度程序

//在Application中有个invokeLaterDispatcher实例对象

val invokeLaterDispatcherField = application.javaClass.getDeclaredField("invokeLaterDispatcher")

invokeLaterDispatcherField.isAccessible = true

val invokeLaterDispatcher = invokeLaterDispatcherField.get(application)

//InvokeLaterDispatcher调度器中执行的消息队列

//即所有将要在主线程(JavaFxThread)中执行的任务队列

val dequeField = invokeLaterDispatcher.javaClass.getDeclaredField("deque")

dequeField.isAccessible = true

// val deque = dequeField.get(invokeLaterDispatcher) as LinkedBlockingDeque<Runnable>

//自定义队列,用于hook操作。并替换系统中的消息队列实例。

val myDeque = MyDeque()

dequeField.set(invokeLaterDispatcher, myDeque)

fxUserThread = appClass.getDeclaredField("eventThread").run {

isAccessible = true

get(null) as Thread

}

}

//自定义队列,重写addLast方法,插入自定义的Runnable包装器,用于hook操作

class MyDeque : LinkedBlockingDeque<Runnable>() {

override fun addLast(e: Runnable) {

super.addLast(MyRunnableWrap(e))

}

}

class MyRunnableWrap(private val origin: Runnable) : Runnable {

private val TAG = "MyRunnableWrap"

private companion object {

var arg1: Field? = null

}

override fun run() {

val start = System.currentTimeMillis()

origin.run()

println("after run,cost=${System.currentTimeMillis() - start}ms")

}

private fun getOriginName() = try {

(arg1 ?: origin.javaClass.getDeclaredField("arg$1").also {

it.isAccessible = true

arg1 = it

}).get(origin).toString()

} catch (_: Exception) {

null

}

}

这个方法,可以在我们自定义的Application(和上文的Application不是同一个)中的start方法,或者直接在构造函数中调用都可以(亲测可用)。

点击事件hook

经测试,鼠标的点击事件到来时,runnable里的执行不会抛给UI线程的消息队列,而是由native直接抛给UI线程执行。那么针对点击事件我们也需要进行hook。

  1. 从点击事件开始debug,查找点击事件来源堆栈的根节点

  2. Scene.TKScene(1.8中是impl_peer,17中是peer)字段,类型是ViewScene

  3. 所有事件来源于ViewScene.platformView.eventHandler

  4. 替换eventHandler为自己的eventHandler,并在事件前后加上hook函数,即统计到当前点击事件执行用时

关键代码如下

kotlin 复制代码
fun injectViewEventHandler(scene: Scene) {

//java1.8中字段名是impl_peer,JavaFX17中(单独的库了),字段名是peer

val impl_peerField = scene.javaClass.getDeclaredField("peer").also { it.isAccessible = true }

val impl_peer = impl_peerField.get(scene)

val platformViewField = impl_peer.javaClass.getDeclaredField("platformView").also { it.isAccessible = true }

val platformView = platformViewField.get(impl_peer) as View

val originHandler = platformView.eventHandler

platformView.eventHandler = MyEventHandlerProxy(originHandler)

}

class MyEventHandlerProxy(val origin: View.EventHandler) : View.EventHandler() {}

监控

既然可以得到消息执行前后的回调,那么我们可以启动一个监控线程:

  1. 当主线程消息开始执行时,往监控线程里发送一个延迟消息,延迟时间由自己定义,比如设置延迟5秒,表示5秒后执行此消息。

  2. 当主线程消息结束执行时,移除这个延迟消息。

  3. 如果这条延迟消息没有被移除,说明主线程的这个任务5秒了还没执行完,那么可以判断当前发生了ANR,提取主线程JavaFxThread此时的调用堆栈即可得到当前执行线程,并且也可以知道此时的主线程消息,进而做进一步的判断和排查。

相关推荐
WHabcwu4 分钟前
统⼀异常处理
java·开发语言
zaim15 分钟前
计算机的错误计算(一百六十三)
java·c++·python·matlab·错数·等价算式
枫叶丹45 分钟前
【在Linux世界中追寻伟大的One Piece】多线程(一)
java·linux·运维
2401_854391086 分钟前
Spring Boot OA:企业数字化转型的利器
java·spring boot·后端
山山而川粤13 分钟前
废品买卖回收管理系统|Java|SSM|Vue| 前后端分离
java·开发语言·后端·学习·mysql
栗豆包16 分钟前
w053基于web的宠物咖啡馆平台的设计与实现
java·struts·spring·tomcat·maven·intellij-idea
weixin_4467077444 分钟前
IDEA2024 maven构建跳过测试
java·maven
开朗觉觉1 小时前
RabbitMQ高可用&&延迟消息&&惰性队列
java·rabbitmq·java-rabbitmq
zmd-zk1 小时前
flink学习(3)——方法的使用—对流的处理(map,flatMap,filter)
java·大数据·开发语言·学习·flink·tensorflow
昵称20211 小时前
flink1.16+连接Elasticsearch7官方例子报错解决方案
java·flink·es7