目录:网上冲浪指南

记一次 Silverlight 问题排查经历

2020/05/19

刚入职,被安排上了维护一个旧项目 为什么每次入职都是维护旧项目,如标题所写,这是一个使用 Silverlight 开发的旧项目。当这个项目的第一行代码写下的时候,我还在念小学。最近,这个项目中的一个历史遗留问题终于不能被拖延了,需要被马上修复。

问题背景

这个项目有自己的一套业务开发框架,大多数情况下,后续接手的开发人员不需要对 Silerlight 有太多的了解就可以快速的开发新需求。例如,业务框架提供了一个 SendRequestAsync: (reqPath: string, callback: action<T>) → void 的方法,用来异步的调用远程 HTTP 服务,在 callback 中则可以处理请求的响应。再比如,业务框架还提供了一个 IsCompleted: () → bool 的虚方法,用来封装一个表单提交完成后的固定操作。通过子类对 IsCompleted 的重写,来实现控制是否提交表单并触发这个固定操作。

那么现在问题来了,业务要求子类的 IsCompleted 的实现中,必须要先通过 SendRequestAsync 调用一个接口,然后根据接口的返回值来决定 IsCompleted 的返回值。如你所见,IsCompleted 是个同步函数,他的返回值只是一个单纯的 bool 类型而已,所以这个问题本质上是需要在同步函数中等待异步返回结果。

先看看业务框架文档

在接手这个项目的时候,我收到了一份开发文档,里面提到了一个跟 SendRequestAsync 相呼应的 SendRequest: (reqPath: string, seconds: int, callback: action<T>) → void,从命名上可以看到这应该是用来同步发送请求,文档里面的一句话注释也是这么记录的

不过如果真的这么简单,这个问题也就不值得我写一篇文章来记录了。把发送请求的方法替换成 SendRequest 后,我发现执行请求的逻辑仍然是异步的,问了交接项目的同事,他也说这个方法一定是同步的,只有 SendRequestAsync 才是异步调用。这样,我只好求助于 dotPeek 将业务框架反编译,对比了 SendRequest 以及 SendRequestAsync 的实现后,我发现他们之间的差异竟然是 SendRequest 仅仅比 SendRequestAsync 多了一个定时器做超时控制。气得我浑身发抖,大热天的全身冷汗手脚冰凉,这算哪门子的同步调用啊!

要不手动实现同步?

既然文档里写的是骗人的,我只好试着来把这个异步调用改造成同步方法了。我首先想到的就是 TaskCompletionSource,我可以在 IsCompleted 中等待对应的 TaskCompletionSource.Task 执行完成,然后在 SendRequestAsync 的回调中使用 TaskCompletionSource.SetResult 来让等待的线程继续执行。跟交接的同事讨论了这样的想法,他也觉得可行,不过在实现了这个逻辑之后,Silverlight 狠狠地给我们甩了一个脸色。

Silverlight 的 UI 状态只能在 UI 线程中访问,也就是说通过按钮事件触发的 IsCompleted 方法将会在 UI 线程执行。而业务框架为了方便开发,SendRequestAsync 的 callback 也是在 UI 线程执行的。这样一来,如果我们阻塞了 IsCompleted 的调用,那么 SendRequestAsync 的回调将永远不会执行。

将思路逆转过来

上面提到了,业务框架会在一个子类的无法访问的按钮的点击事件回调中调用 IsCompleted 来判断是否需要执行通用的表单提交后的固定操作。将我们的思维逆转过来,不要去纠结如何在同步函数中等待异步结果返回,而是将问题转化为“如何在异步回调中执行通用的固定操作”。因为基类访问性的限制,这个问题最终就变成了“如何调用基类的私有方法”。

遇到这种绕过访问控制的问题,我第一个想到的就是反射,一顿操作就把反射调用基类私有方法的函数(CallBaseCommit)完成了。不过 Silverlight 又一次将我的方案驳回,并告诉我“方法尝试访问失败”。经过一番面向爆栈搜索后发现,Silverlight 4 是不支持反射访问实例成员的,Silverlight 5 中对反射的使用也有很多限制,这就会导致“尝试访问失败”。

反射调用私有方法不能直接使用,我们还有表达式树。首先通过反射获取到私有方法的 MethodInfo,然后使用 Expression.Call 构造调用实例方法的表达式,接着用这个表达式构建一个 Lambda 表达式,最终将其编译成一个委托调用。终于,这个阻塞了我接近 6 小时的“同步函数阻塞调用异步函数”的问题,通过表达式树解决了!

最后

  1. 基础库的开发应当注重命名,比如上面提到的 SendRequest 应为 BeginSendRequestWithTimeoutAsync

  2. 异步流程的模板方法应该提供异步版本,如果业务框架提供了 IsCompletedAsync 就不会有这篇文章了

  3. dotPeek 反编译真得劲