引言
现在项目中所有的 sercive 文件都是在同一份 kotlin 文件中进行管理。因为 service 是纯业务层且不包含任何用户数据的,所以是可热更替换的,如下所示:
Kotlin 版本:
// 声明为 volatile 和 var ,以便更替
@Volatile
var testService = TestService()
Java 版本:
private static volatile TestService testService = new TestService();
public static final TestService getTestService() {
return testService;
}
public static final void setTestService(TestService newTestService) {
testService = newTestService;
}
代码热更
首先会继承原先 service 类和 Runnable。对于实现父类,相信大家都能理解。实现 Runnable 接口算是项目的约定吧!因为在启动主进程的时候,会单独启一个热加载的守护线程,用于类加载和实例化该加载类,再强转成 Runnable 后调用 run 方法。
class TestServiceBugFix: TestService(), Runnable {
override fun run() {
testService = this
}
// 省略修复的代码
}
潜在问题
这套流程是没有任何问题的。但是,如果 testService 在其它代码有引用,那就会有问题了。如下所示:
@Volatile
var routeMap:<RouteType, Service> = mapOf(
RouteType.TEST to testService,
...
RouteType.TEST_N to testServiceN
)
如果仅仅在 run 方法中执行 testService = this
是不够的,因为仅仅切换了 service 文件中的引用,routeMap
持有的还是旧的引用。正确代码如下:
- 方案一:testService 指向新的对象的同时,将所有持有 testService 引用的地方都做修改
class TestServiceBugFix: TestService(), Runnable {
override fun run() {
testService = this
// routeMap 指向新的对象
routeMap = mapOf(
RouteType.TEST to this,
...
RouteType.TEST_N to testServiceN
)
}
// 省略修复的代码
}
- 方案二:或者修改 routeMap 的值为函数(函数引用),即 getter, 每次路由的时候都是最新的对象。如下:
@Volatile
val routeMap:<RouteType, ()->Service> = mapOf(
RouteType.TEST to ::testService,
...
RouteType.TEST_N to ::testServiceN
)
- 方案三:操作原先对象的堆内存
Unsafe unsafe = Unsafe.getUnsafe();
long offset = unsafe.objectFieldOffset(Services.class, "testService");
unsafe.compareAndSetObject(Services.class, offset, oldService, newService);
总结
在做热更的时候,一定要排查清楚待热更的对象在其它代码处是否存在引用。 待补充。。。