WPF多进程UI探索(Like Chrome)

Chrome客户端每个Tab都是一个进程,这样每个Tab就形成了一个沙盒,任何一个Tab出现问题都不会影响其他Tab。同时,每个Tab是独立进程可以完全利用一个进程的资源,当多个Tab合并到一起后,看起来像一个进程,实际上利用了多个进程的资源。下面将探索在WPF框架下实现类Chrome的多进程UI客户端。

Chrome是怎么做的

以下是Chrome的架构预览图:


image27.png

可以看到,它有一个主进程和多个子进程,主进程和子进程之间用IPC通信,此IPC的底层走的是命名管道,效率较高。它的每个子进程都会使用一个渲染进程负责Web页面的渲染(此渲染进程可以被多个子进程共享使用),渲染完成之后的RenderView会传递到主进程中显示(RenderViewHost),关于Chrome的更多细节可以参见 Chrome Architecture

进程通信

用WPF做一个类似Chrome的程序,首先要解决的是进程通信问题,当Client出现问题,比如崩溃,Host要能收到消息,Client从Host中分离出来,也需要在Host和Client之间通信。目前在Widnows上实现IPC的方案很多,选择了.NET Remoting的IPCChannel,这里有一些介绍。这都比较简单,比较麻烦一些的是UI的传递,在.NET中,能够跨越程序边界是有条件的,要么从MarshalByRefObject类继承,要么实现ISerializable接口,要么标记了SerializableAttribute特性,显然WPF的UI是不具备这样的特点的。

跨进程传递UI

思路一:使用窗口模拟

假想每个进程都有一个窗口,通过控制窗口的位置和大小来模拟Chrome的效果。很快就放弃了,感觉坑会比较多,电脑性能稍微差点,窗口中承载的内容稍微复杂点,基本就无法正常工作了,而且,要求每个进程都有窗口,那如果后续要做带UI的第三方插件支持,这些插件都必须有窗口才行了。

思路二:UI序列化和反序列化

在子进程渲染完成后,把UI进行序列化,传递到主进程,主进程再反序列化回来进行显示。这个方案也很多坑,首先就是序列化和反序列化的效率,然后所有操作都是在主进程接收的,传递到子进程后,子进程处理完再次更新UI,又会走序列化和反序列化。

继续探索,在现有的WPF技术中找找是否有能够让UI跨越程序边界的方案。发现MAF支持外接程序为UI或者外接程序返回UI,看看MAF是怎么做的。

MAF

MAF是微软提供的一个方便管理和隔离程序的扩展部分的框架,通过隔离宿主和插件,使得插件的崩溃不影响宿主的运行。以下是MAF开发中插件和宿主之间通信的方式:


image28.png

在实际开发中,以上几个部分都不可缺少,并且,MAF的契约依赖文件夹,管道中的每个部分都需要输出到指定的文件夹中才能工作,最终要求的文件夹结构是这样的:


image29.png

显然,这样的依赖对于实际项目的开发极不友好。

还是回归正题,“它是怎么处理不同程序域UI的传递的?”

在外界程序返回UI的代码中有两段看起来有点意思的代码。

AddInSideAdapter中:

INativeHandleContract handle = FrameworkElementAdapters.ViewToContractAdapter(frameworkElement);

HostSideAdapter中:

INativeHandleContract handle = GetHandle();
FrameworkElement frameworkElement = FrameworkElementAdapters.ContractToViewAdapter(handle);

这里MAF利用了WPF和Win32的交互,在插件端,WPF获得要传递的UI的窗口句柄,包装在一个继承自HwndSource(用于Win32中承载WPF内容)并实现了INativeHandleContract接口的类中,而此类是可以跨越程序边界的。在宿主端,收到此类后通过ContractToViewAdapter转换成一个从HwndHost(用于WPF承载Win32窗口)继承的类,而HwndHost的基类是FrameworkElement。这就好办了,WPF中熟悉的FrameworkElement,直接在WPF宿主中使用即可。

继续跨进程传递UI

有了MAF的经验,似乎可以利用INativeHandleContract实现跨进程传递UI,马上在Demo中验证,果然,没那么顺利,弹出这样的错误:


image30.png

难道内部有使用非公共或静态方法?只有看源码找答案,用dotPeek打开程序集(.NET源码 中没有这部分的源码),没跟几步就找到一个internal的方法:

image31.png

方法虽然找到了,但是改不了。

留意到AddInHost的构造函数中,如果contract转换AddInHwndSourceWrapper失败就不会执行那个导致抛出异常的方法,而这个contract是可以自己实现的,把FrameworkElementAdapters.ViewToContractAdapter(frameworkElement)的结果包装到自己实现的INativeHandleContract中就可以了,代码如下:

public class CustomNativeHandleContract : MarshalByRefObject, INativeHandleContract
{
    
    private readonly INativeHandleContract _contract;
    public CustomNativeHandleContract(INativeHandleContract contract)
    {
        _contract = contract;
    }

    public IContract QueryContract(string contractIdentifier)
    {
        return _contract.QueryContract(contractIdentifier);
    }

    //省略若干INativeHandleContract的方法

    // ...
}

在子进程中

INativeHandleContract Contract = new CustomNativeHandleContract(FrameworkElementAdapters.ViewToContractAdapter(element));

完成UI到HwndSource的转换。

等等,我们刚刚好像少调用了一个方法:RegisterKeyboardInputSite,在MAF中,这个方法会管理HwndSource中的键盘焦点,但是我们这里HwndSource仅仅是作为一个中转,我们不会依赖HwndSource的行为,所以不会有问题。

结论

按照上面的思路,利用MAF的特性通过Remoting的IPCChannel实现了多进程UI的客户端程序,需要注意的是,在WPF内容转到HwndSource时会执行WPF的内容的布局过程,这会带来一定的性能损耗。