CEFGlue之回忆篇

前言

刚毕业参加工作的时候,研究过一段时间CEF,解决了几个令人头疼的问题,转行之后就再没关注过。直到前两天,一个素不相识的童鞋突然不知道从哪里搞来我的QQ,加我好友询问CEF的一些问题,就在那时我突然意识到,也许这段经历应该被记录下来。———— 写给4年前的自己。

背景

在.NET平台的开发中,微软自带的WebBrowser控件存在以下问题:
(1)WebBrowser控件无法保证版本的一致,如果你在Xp下,那么你就有可能调用的IE6.0的版本,同时在Window7或者更高的系统版本下WebBrowser调用的版本也可能与本机版本不一致。
(2)HTML页面兼容性问题,不同的浏览器对同一个页面的解释上会有页面的兼容问题。虽然电脑上安装了IE8或者更高版本的IE浏览器,但Webbrowser控件默认总是使用IE7内核兼容模式来显示网页内容,导致很多网页样式无法正常显示,例如IE7不兼容HTML5,解决方法是在注册表中为你的进程指定引用IE的版本号。
更多细节可以参考这里

为了彻底解决这些问题,经过调研,决定采用第三方开源浏览器——嵌入式Chromium框架(简称CEF) 。CEF是一个由Marshall Greenblatt在2008建立的开源项目,它主要目的是开发一个基于Google Chromium的Webbrowser控件。CEF支持一系列的编程语言和操作系统,并且能很容易地整合到新的或已有的工程中去。
它的设计思想政治就是易用且兼顾性能。CEF基本的框架包含C/C++程序接口,通过本地库的接口来实现,而这个库则会隔离宿主程序和Chromium&Webkit的操作细节。它在浏览器控件和宿主程序之间提供紧密的整合,它支持用户插件,协议,javascript对象以及javascript扩展,宿主程序可以随意地控件资源下载,导航和打印等,并且可以跟Google Chrome浏览器一样,支持高性能和Html5技术.


CEFGlue的整体依赖如图所示,其中有两个棘手的问题亟待解决:

  • 1、让CEF支持常规音视频的播放;
  • 2、解决JS调用Native功能的问题;

CEF支持音视频

要想让CEF支持常规音视频的播放,需要对CEF内核重新编译

CEF内核编译流程

说起CEF内核的编译流程都是泪,相信编译过的同学一定深有体会,在此总结一下步骤和注意事项:

  • 1、操作系统必须是Win7旗舰版,而且必须64位,32位不支持!

  • 2、必须设置系统语言为英文,否则会报各种编码错误!(控制面板->区域->管理->语言)

  • 3、安装VS2013 Update 4,其他低版本不支持!

  • 4、准备稳定可靠的VPN,用于科学上网(VPN一定要稳定!!!)

  • 5、安装depot_tools(下载完成直接解压即可)
    下载地址:https://src.chromium.org/svn/trunk/tools/depot_tools.zip

  • 6、设置depot_tools环境变量:
    set GYP_GENERATORS=ninja,msvs-ninja
    set GYP_MSVS_VERSION=2013
    set DEPOT_TOOLS_WIN_TOOLCHAIN=0
    set USE_PROPRIETARY_CODECS=1

  • 7、下载chromium源码(CEF无法单独编译,必须依赖于chromium)

    • 设置文件夹,别出现特殊字符和中文:workspace/chromium/
    • 进入文件夹, fetch –nohooks chromium(会执行很久很久),如果中间失败, 请执行gclient sync
    • 确定CEF内核的版本,根据CEF内核的版本确定chrome的版本(例如 4d096306deb9e86953c81f08454dd6db07b406b5)
    • 获取所需chromium版本
      gclient sync –revision 4d096306deb9e86953c81f08454dd6db07b406b5 –jobs 16
      注:该步骤需要非常漫长的时间,需要耐心等待
  • 8、将对应版本的CEF源码文件夹拷贝到chromium/src
    修改CEF编译选项:
    打开cef.gypi文件,添加以下两行代码:
    ‘proprietary_codecs’: 1,
    ‘ffmpeg_branding’:’Chrome’,
    如果想仔细查看各种音频视频编码支持,请查看
    workspace\chromium\src\third_party\ffmpeg\chromium\scripts\build_ffmpeg.py

  • 9、编译CEF
    export GYP_GENERATORS=’ninja’
    cd workspace/chromium/src/cef
    cef_create_projects.bat
    cd workspace/chromium/src
    ninja -C out/Release cefclient cef_unittests

  • 10、发布CEF内核
    cd workspace/chromium/src/cef/tools
    make_distrib.bat –ninja-build

CEF内核的H5评估得分

  • Chromium版本号 : 43.0.2357.81
  • CEF版本号 : 3.2357.1280.geba024d
  • 具体依赖 : CEFGlue–>CEF–>Chromium

JS调用Native功能

Xilium CefGlue是一款基于Chromium Embedded Framework (CEF)的开源WebKit内核浏览器项目,用.NET进行了各种封装,本身有很多优势,内核也在不断更新,具有良好的前景。但是,它有一个致命的问题:CLR Object无法很好的与网页前端JavaScript进行交互,实现方法非常复杂,非常繁琐。

官方原生注册方法

官方提供的原生方法,类对象必须继承CefV8Handler,所有操作方法,都必须写在该类Execute ()里面;类对象的所有属性、方法,都需要在后台写出对应的JS脚本,进行注册绑定;如果想执行不同操作,就需要不断的写一大堆类对象,因为每个类只能做一件事。
具体步骤如下:
(1) 定义一个MyV8Handler,继承自CefV8Handler注意这里注册的对象必须继承于CefV8Handler,这意味着注册对象收到了限制

public class MyV8Handler : CefV8Handler
{
    public MyV8Handler()
    {
        this.name = "lingyun";
    }

    private string name;
    public string Name
    {
        set { this.name = value; }
        get { return this.name; }
    }

    public void MyFunction()
    {
        MessageBox.Show("JS调用C# : MyFunction");
    }

    private string email;
    public string GetEmail()
    {
        return this.email;
    }
    public void SetEmail(string email)
    {
        this.email = email;
    }
}

(2) 定义一个WpfRenderProcessHandler,继承自CefRenderProcessHandler,并重写OnWebKitInitialized()事件,在里面写要执行的操作代码,根据类的实际情况,人工手写JS脚本代码,通过官方提供的RegisterExtension方法进行注册。注意这里是OnWebKitInitialized中完成注册,也就意味着注册时机也受到了限制,一旦WebKit初始化完成,就已经失去了注册的机会

class WpfRenderProcessHandler:CefRenderProcessHandler
{
    private MyV8Handler myHandle;
    protected override void OnWebKitInitialized()
    {
        myHandle = new MyV8Handler();

        const string jsCode = @"function myHandle() {}

        if (!myHandle) myHandle = {};

        (function() {

            myHandle.__defineGetter__('name',
            function() {
                native function GetName();
                return GetName();
            });

            myHandle.__defineSetter__('name',
            function(arg0) {
                native function SetName(arg0);
                SetName(arg0);
            });

            myHandle.myFunction = function() {
                native function MyFunction();
                return MyFunction();
            };

            myHandle.getEmail = function() {
                native function GetEmail();
                return GetEmail();
            };

            myHandle.setEmail = function(arg0) {
                native function SetEmail(arg0);
                SetEmail(arg0);
            };

        })();";

        CefRuntime.RegisterExtension("myHandleName", jsCode, myHandle);

        base.OnWebKitInitialized();
    }
}

(3) 在MyV8Handler里的Execute()事件中,写要执行的操作代码

protected override bool Execute(string name, CefV8Value obj, CefV8Value[] arguments, out CefV8Value returnValue, out string exception)
    {
        string result = string.Empty;
        switch (name)
        {
            case "MyFunction":
                MyFunction();
                break;

            case "GetEmail":
                result = GetEmail();
                break;

            case "SetEmail":
                SetEmail(arguments[0].GetStringValue());
                break;

            default:
                break;
        }

        returnValue = CefV8Value.CreateString(result);
        exception = null;
        return true;
    }

(4) 写一个网页,去前台通过JS调用,例如网页那边调用:myHandle.myFunction ();

可以看到,为了注册一个Native方法,需要付出很高的代价,而且这种原生注册方法非常死板,如果想执行不同操作,就需要不断的写一大堆类对象,因为每个类只能做一件事,这样的方式没有任何通用性而言,也没有任何扩展性,写的代码近似乎于僵尸。

高级注册方法

注册核心原理洞悉

尽管官方的注册方式如此地繁冗,但是 核心原理 不难发现:所谓的注册本质上不过是让V8的上下文记了个方法名字和对应的参数个数和类型,告诉V8引擎,如果碰到了注册的名字,通过Excute方法把方法名和参数传出来,然后进行本地方法的匹配和调用。至于Excute方法是被谁调用的,怎么调用的,V8引擎会搞定,无需我们关心。明白了核心原理,并且.NET有良好的反射特性,我们不难推理:给定一个CLR对象,我们可以通过反射技术分析它的方法列表,并且能拿到每一个方法对应的参数个数和参数类型列表,这样就可以自动地进行注册,实现一个通用的注册方案。

多进程通信解决方案

CEF从3.0开始,就由原先的单进程模式变为多进程模式,例如:渲染模块是由专门的渲染进程来实现的,主进程(Browser进程)与渲染进程(Render进程)通过发送消息的机制实现进程之间的通信。因为注册对象只能在V8上下文中进行注册,而V8上下文只能被Render进程访问。如果想注册某一个Native对象,一般都会把Native对象在主进程里面进行序列化,然后通过发送消息的方式发送给渲染进程,渲染进程拿到消息,首先进行反序列化得到想要注册的对象,然后再进行注册。

然而以上思路在实践中碰到了比较严重的问题:.NET中并非每个对象都支持序列化,尤其是我们无法去修改一个已经封装好的DLL库中的对象,所以以上方法行不通。中间也曾经考虑过共享内存的解决方案,但是发现最后都无法逃脱序列化这一步骤。

经过进一步深入挖掘CEF注册内部机制,发现CEF注册的时候仅仅关心的是对象的方法名和属性名,并不在意该方法的具体实现,所以可以现在Browser进程内首先将对象保存到自己的对象池中,然后将对象的方法名(属性名)通过反射技术解析出来,并发送给Render进程。Render进程接收到对象的方法名(属性名)之后,会在Render进程的上下文中实现真正的注册。一旦JS中调用本地方法,在CefV8Handler的Execute触发,就会获取到对象的方法名和参数,此时Render进程将该对象的哈希值、方法名和参数一起发送给Browser进程。Browser进程拿到这个对象的方法名和参数,会在本地注册池内进行对相匹配,一旦找到对象,会在对象的方法中寻找最适合的方法进行匹配调用,相当于方法的执行完全跑到了Browser进程中,而在整个过程中Browser进程和Render进程之间通信的内容仅仅是对象的方法名,仅仅是一个字符串,这样就避免了对象的序列化。

关于CEF多进程通信的问题,收集到几个对我有启发的英文片段:

I have been struggling with this as well. I do not think there is a way to do this synchronously…or easily :) Perhaps what can be done is this:

From browser do sendProcessMessage with all JS information to renderer process. You can pass all kinds of parameters to this call in a structured way so encapsulating the JS method name and params in order should not be difficult to do. In renderer process (RenderProcessHandler onProcessMessageReceived method) do TryEval on the V8Context and get the return value via out parameters and sendProcessMessage back to the browser process with the JS return value (Note that this supports ordinary return semantics from your JS method).You get the browser instance reference in the onProcessMessageReceived so it is as easy as this (mixed pseudo code) browser.GetMainFrame().CefV8Context.tryEval(js-code,out retValue, out exception); process retValue; browser.sendProcessMessage(…); Browser will get a callback in the WebClient in onProcessMessageReceived. There is nothing special here in terms of setting up JS. I have for example a loaded html page with a js function in it. It takes a param as input and returns a string. in js-code parameter to TryEval I simply provide this value:

It is slightly convoluted but seems like a neat workable approach – better than doing ExecuteJavaScript and posting results via XHR on custom handler in my view. I tried this and it does work quite well indeed….and is not bad as it is all non-blocking. The wiring in the browser process needs to be done to process the response properly. This can be extended and built into a set of
classes to abstract this out for all kinds of calls.. Take a look at the Xilium demo app. Most of the necessary wiring is already there for onProcessMessage – do a global search. Look for DemoRendererProcessHandler.cs – renderer side this is where you will invoke tryEval DemoApp.cs – this is browser side, look for sendProcessMessage – this will initiate your JS invocation process. WebClient.cs – this is browser side. Here you receive messages from renderer with return value from your JS Cheers.

全自动反射注册

结合注册的核心原理和CEF的多进程通信方案,也通过啃一些源代码,给出最终方案的解决步骤:

(1) 定义一个MyV8Handler,但是 不再继承自CefV8Handler,摆脱了继承这一环,这意味着程序中的任何一个对象都可以被注册,灵活度已经大大提升,至于这里为什么可以不继承自CefV8Handler,是因为发现了CEF的底层注册原理,后面会看到。

public class MyV8Handler
{
    public MyV8Handler()
    {
        this.name = "lingyun";
    }

    private string name;
    public string Name
    {
        set { this.name = value; }
        get { return this.name; }
    }

    public void MyFunction()
    {
        MessageBox.Show("JS调用C# : MyFunction");
    }

    private string email;
    public string GetEmail()
    {
        return this.email;
    }
    public void SetEmail(string email)
    {
        this.email = email;
    }
}

(2)定义一个CefGlueRuntime,用来启动CEF内核,同时在Browser进程注册Native对象,但是这里的注册时一个假注册,它做的事就是简单地把对象保存下来。

public class CefGlueRuntime
{
    private static object locker = new object();
    public static bool Startup()
    {         
        // 装载libcef.dll
        try
        {
            CefRuntime.Load();
        }
        catch (Exception ex)
        {
            MessageBox.Show(ex.ToString(), "Error!", MessageBoxButton.OK, MessageBoxImage.Error);
            return false;
        }

        string[] args = { };

        var mainArgs = new CefMainArgs(args);
        var cefApp = new WpfCefApp();

        var exitCode = CefRuntime.ExecuteProcess(mainArgs, cefApp);
        if (exitCode != -1) { return false; }

        // CEF启动参数设定
        var cefSettings = new CefSettings
        {
            // BrowserSubprocessPath = browserSubprocessPath,
            SingleProcess = true,
            WindowlessRenderingEnabled = false,
            MultiThreadedMessageLoop = true,
            LogSeverity = CefLogSeverity.Disable,
            NoSandbox = true,                
            UserAgent = "Mozilla/5.0 (Windows NT 6.2; WOW64; Trident/6.0; rv:8.0) like Gecko Chromiumembedded/43.0.2357.81 CefGlue/3.2357.1280.geba024d (cxb,Founder)"
        };

        try
        {
            CefRuntime.Initialize(mainArgs, cefSettings, cefApp);
        }
        catch (CefRuntimeException ex)
        {
            MessageBox.Show(ex.ToString(), "Error!", MessageBoxButton.OK, MessageBoxImage.Error);
            return false;
        }
        return true;
    }

    public static void Shutdown()
    {
        lock (locker)
        {
            CefRuntime.Shutdown();
        }
    }

    // 全局对象列表,用来存放所有需要注册的对象,这里的注册并非真正的注册,只是暂时的寄存
    private static Dictionary<string, Dictionary<int, object>> ObjectsList = new Dictionary<string, Dictionary<int, object>>();

    // 实际情况下,一个exportName可能对应多个obj,因为对象经过序列化和反序列化的过程,其哈希值已经发生变化,所以需要
    // 一开始就将对象的哈希值传递过来(不能使用反序列化之后的哈希值),然后利用该哈希值来判定一个对象是否已经注册过,这样
    // 就彻底解决了对象的重复注册问题
    public static void RegisterJsObject(string exportName, Object obj,int hashCode)
    {
        // 如果外部调用接口不存在,则需要创建
        if (!ObjectsList.ContainsKey(exportName))
        {
            var objDic = new Dictionary<int,object>();
            objDic.Add(hashCode, obj);
            ObjectsList.Add(exportName,objDic);
        }
        else{ // 如果外部调用接口已经存在,则直接通过判断来添加
            if (!ObjectsList[exportName].ContainsKey(hashCode))
                ObjectsList[exportName].Add(hashCode, obj);
        }

        if (!ObjectList.ContainsKey(hashCode))
            ObjectList.Add(hashCode, obj);

    }

    public static Dictionary<string, Dictionary<int, object>> GetObjects()
    {
        return ObjectsList;
    }

    public static Dictionary<int, object> ObjectList = new Dictionary<int, object>();

}

(3) 定义一个WebView,在Browser进程中,方法RegisterJsObject通过反射技术提取对象的方法列表,以及每个方法对应的参数个数和参数类型,并将其打包发送给Render进程,同时调用CefGlueRuntimeRegisterJsObject方法将Native对象保存下来。


public class WebView : WpfCefBrowser
{

    // 执行JS脚本获取返回值需要的变量
    public object result = string.Empty;
    public string exceptionMessage = string.Empty;
    public AutoResetEvent signal = new AutoResetEvent(false); 

    /// <summary>
    /// Browser进程注册,实质上是向Render进程发送消息,由于序列化涉及到了太多的修改,甚至于无法前进,所以重新设计消息流程
    /// </summary>
    /// <param name="extentionName"></param>
    /// <param name="obj"></param>
    public void RegisterJsObject(string extentionName, object obj)
    {
        // 协议头以register来标识
        CefProcessMessage msg = CefProcessMessage.Create("Register");

        CefListValue objInfo = msg.Arguments;

        // 设置向JS暴露的对象名
        objInfo.SetString(0, extentionName);

        // 对象的哈希值
        int hashCode = obj.GetHashCode();
        objInfo.SetInt(1, hashCode);

        // 对象的方法名
        CefListValue methodList = CefListValue.Create();
        HashSet<string> methodNames = TypeUtil.GetMethodNames(obj.GetType());
        for (int i = 0; i < methodNames.Count; i++)
            methodList.SetString(i, methodNames.ElementAt<string>(i));
        objInfo.SetList(2, methodList);

        // 对象的属性名
        CefListValue propertyList = CefListValue.Create();
        var properties = TypeUtil.GetProperties(obj.GetType());
        for (int i = 0; i < properties.Keys.Count; i++)
        {
            string propertyName = properties.Keys.ElementAt<string>(i);
            CefV8PropertyAttribute propertyAttribute = CefV8PropertyAttribute.None;
            if (properties[propertyName].GetSetMethod() != null)
                propertyAttribute = CefV8PropertyAttribute.ReadOnly;
            CefListValue propertyInfo = CefListValue.Create();
            propertyInfo.SetString(0, propertyName);
            propertyInfo.SetInt(1, (int)propertyAttribute);
            propertyList.SetList(i, propertyInfo);
        }
        objInfo.SetList(3, propertyList);

        // 向Render进程发送消息
        this.SendProcessMessage(CefProcessId.Renderer, msg);

        // 将对象添加到本进程的注册池内
        CefGlueRuntime.RegisterJsObject(extentionName, obj, hashCode);
    }
}

(4)定义一个WpfRenderProcessHandler,继承自CefRenderProcessHandler,在OnProcessMessageReceived方法中接受Browser进程发送来的消息,逻辑处理在ProcessRegisterMessage方法中

protected override bool OnProcessMessageReceived(CefBrowser browser, CefProcessId sourceProcess, CefProcessMessage message)
{  
    // 处理注册消息
    if (message.Name == "Register") 
    {
        ProcessRegisterMessage(message);
    } 
    //执行JS获取返回值
    else if (message.Name == "EvaluteJavaScript")
    {
        if (CefRuntime.CurrentlyOn(CefThreadId.Renderer))
            ProcessEvaluateMessage(browser, message);
        else
            Wrapper.Helpers.PostTask(CefThreadId.Renderer, Wrapper.Helpers.Apply(ProcessEvaluateMessage, browser, message));                
    }
    else if (message.Name == "ExecuteMethodResult")
    {
        BindingHandler.exception = message.Arguments.GetString(0);
        CefBinaryValue binaryValue = message.Arguments.GetBinary(1);
        byte[] binaryData = binaryValue.ToArray();
        BindingHandler.result = SerializationUtil.DeserializeObject(binaryData);
        BindingHandler.signal.Set();
    }
    // 如果有别的处理消息,则添加逻辑处理
    var handled = MessageRouter.OnProcessMessageReceived(browser, sourceProcess, message);
    if (handled) return true;

    return true;
}
/// <summary>
/// 处理注册消息
/// </summary>
/// <param name="message"></param>
private void ProcessRegisterMessage(CefProcessMessage message)
{
    // Render进程接收到Browser进程的消息,然后把接收到的参数按照约定的方式进行反序列化,相当于得到Browser发送
    // 对象的一份拷贝,然后添加到Render进程里的注册池中
    var arguments = message.Arguments;

    // 第一个参数代表extensionName
    string extensionName = arguments.GetString(0);

    // 第二个参数代表对象的HashCode,唯一标识某一个对象
    int hashCode = arguments.GetInt(1);

    // 第三个参数存放对象的方法列表
    CefListValue methodList = arguments.GetList(2);
    List<string> ml = new List<string>();
    for (int i = 0; i < methodList.Count; i++)
        ml.Add(methodList.GetString(i));

    // 第四个参数代表对象的属性列表
    CefListValue propertyList = arguments.GetList(3);
    Dictionary<string, Dictionary<string, string>> pl = new Dictionary<string, Dictionary<string, string>>();
    for (int i = 0; i < propertyList.Count; i++)
    {
        CefListValue propertyInfo = propertyList.GetList(i);
        Dictionary<string, string> pi = new Dictionary<string, string>();
        pi.Add("propertyName", propertyInfo.GetString(0));
        pi.Add("propertyAttribute", propertyInfo.GetInt(1).ToString());
        pl.Add(propertyInfo.GetString(0),pi);
    }

    CefRenderRuntime.RegisterJsObject(extensionName, hashCode, ml, pl);

}

定义一个CefRenderRuntime,用来充当Render进程的对象池,接受从Browser进程传递过来的对象

class CefRenderRuntime
{
    // 全局对象列表,用来存放所有需要注册的对象的方法和属性,这里的注册并非真正的注册,只是暂时的寄存
    private static Dictionary<string, Dictionary<int, List<string>>> methodList = new Dictionary<string, Dictionary<int, List<string>>>();
    private static Dictionary<string, Dictionary<int, Dictionary<string, Dictionary<string, string>>>> propertyList = new Dictionary<string, Dictionary<int, Dictionary<string, Dictionary<string, string>>>>();
    private static Dictionary<string, HashSet<int>> keys = new Dictionary<string, HashSet<int>>();

    // 实际情况下,一个exportName可能对应多个obj,所以用哈希值来区分多个对象
    public static void RegisterJsObject(string exportName, int hashCode, List<string> ml, Dictionary<string,Dictionary<string, string>> pl)
    {
        // 方法部分:如果外部调用接口不存在,则需要创建
        if (!methodList.ContainsKey(exportName))
        {
            var methodDic = new Dictionary<int, List<string>>();
            methodDic.Add(hashCode, ml);
            methodList.Add(exportName, methodDic);
        }
        else
        { // 如果外部调用接口已经存在,则直接通过判断来添加
            if (!methodList[exportName].ContainsKey(hashCode))
                methodList[exportName].Add(hashCode, ml);
        }

        // 属性部分:如果外部调用接口不存在,则需要创建
        if (!propertyList.ContainsKey(exportName))
        {
            var propertyDic = new Dictionary<int, Dictionary<string,Dictionary<string, string>>>();
            propertyDic.Add(hashCode, pl);
            propertyList.Add(exportName, propertyDic);
        }
        else
        { // 如果外部调用接口已经存在,则直接通过判断来添加
            if (!propertyList[exportName].ContainsKey(hashCode))
                propertyList[exportName].Add(hashCode, pl);
        }

        // Key部分
         if (!keys.ContainsKey(exportName))
        {
            var keyDic = new HashSet<int>();
            keyDic.Add(hashCode);
            keys.Add(exportName, keyDic);
        }
        else
        { // 如果外部调用接口已经存在,则直接通过判断来添加               
            keys[exportName].Add(hashCode);
        }
    }

    public static List<string> GetMethods(string exportName, int hashcode)
    {
        return methodList[exportName][hashcode];
    }

    public static Dictionary<string,Dictionary<string, string>> GetProperties(string exportName, int hashcode)
    {
        return propertyList[exportName][hashcode];
    }

    public static Dictionary<string, HashSet<int>> GetKeys()
    {
        return keys;
    }
}

(5)在WpfRenderProcessHandlerOnContextCreated方法中使用BindingHandler进行注册,注意这里不再是 OnWebKitInitialized,而是OnContextCreated,这样意味着WebKit启动之后,我们仍然可以在每个上下文被创建的时候进行注册,注册时机也得到解放

protected override void OnContextCreated(CefBrowser browser, CefFrame frame, CefV8Context context)
{            
    // 当网页的上下文对象被创建的时候,则会在此重新触发Bind方法,然后在此真正的进行注册
    var keys = CefRenderRuntime.GetKeys();
    foreach (var key in keys.Keys)
    {
        var hashcodeList = keys[key];
        foreach (int hashcode in hashcodeList)
        {
            BindingHandler.Bind(key, hashcode, context.GetGlobal(), browser); 
        }
    }

    MessageRouter.OnContextCreated(browser, frame, context);
    base.OnContextCreated(browser, frame, context);
}

BindingHandler的定义如下:它继承自CefV8Handler这里的Bind函数是核心重点,可以看到最后的关键注册语句是window.SetValue,它注册的是一个CefV8Value对象,所以CLR对象在注册之前需要进行数据类型转换

class BindingHandler:CefV8Handler
{
    /// <summary>
    /// 绑定数据并注册对象
    /// 说明:已经过滤特殊名称,即不含系统自动生成的属性、方法
    /// </summary>
    /// <param name=”name”>对象名称</param>
    /// <param name=”obj”>需要绑定的对象</param>
    /// <param name=”window”>用于注册的V8 JS引擎对象,类似于整个程序的窗口句柄</param>
    public static void Bind(string name, int hashcode, CefV8Value window, CefBrowser browser)
    {
        // 把CLR对象用UnmanagedWrapper包装,方便参数传递,就好像CLR中的ref
        var unmanagedWrapper = new UnmanagedWrapper(hashcode);

        var propertyAccessor = new PropertyAccessor();
        CefV8Value javascriptWrapper = CefV8Value.CreateObject(propertyAccessor);

        // 将unmanagedWrapper作为javascriptWrapper的数据
        javascriptWrapper.SetUserData(unmanagedWrapper);

        // 定义一个V8Handler,真正注册的时候使用
        var handler = new BindingHandler();
        handler.OnSendExecuteMsgHandler -= browser.SendProcessMessage;
        handler.OnSendExecuteMsgHandler += browser.SendProcessMessage;

        // 提取CLR对象的属性,并打包到javascriptWrapper中
        unmanagedWrapper.Properties = CefRenderRuntime.GetProperties(name,hashcode);
        CreateJavascriptProperties(handler, javascriptWrapper, unmanagedWrapper.Properties);

        // 提取CLR对象的方法,并打包到javascriptWrapper中
        List<string> methodNames = CefRenderRuntime.GetMethods(name, hashcode);
        CreateJavascriptMethods(handler, javascriptWrapper, methodNames);

        // 最终注册的是一个javascriptWrapper的CefV8Value,其内容以键值对的方式存在
        window.SetValue(name, javascriptWrapper, CefV8PropertyAttribute.None);
    }

    /// <summary>
    /// 创建JavaScript方法
    /// </summary>
    /// <param name=”handler”>处理程序</param>
    /// <param name=”javascriptObject”>经过V8 JS引擎处理后的对象</param>
    /// <param name=”methodNames”>方法键值对集合</param>
    public static void CreateJavascriptMethods(CefV8Handler handler, CefV8Value javascriptObject, List<String> methodNames)
    {
        var unmanagedWrapper = (UnmanagedWrapper)(javascriptObject.GetUserData());

        foreach (string methodName in methodNames)
        {
            string jsMethodName = StringUtil.LowercaseFirst(methodName);
            unmanagedWrapper.AddMethodMapping(jsMethodName, methodName);
            string nameStr = jsMethodName;
            // 键值对:[方法名,方法,None]
            javascriptObject.SetValue(nameStr, CefV8Value.CreateFunction(nameStr, handler), CefV8PropertyAttribute.None);
        }
    }


    /// <summary>
    /// 根据属性设定javascriptObject
    /// </summary>
    /// <param name="handler"></param>
    /// <param name="javascriptObject"></param>
    /// <param name="properties"></param>
    public static void CreateJavascriptProperties(CefV8Handler handler, CefV8Value javascriptObject, Dictionary<string,Dictionary<string, string>> properties)
    {
        var unmanagedWrapper = (UnmanagedWrapper)(javascriptObject.GetUserData());

        foreach (var propertyInfo in properties.Values)
        {
            string propertyName = propertyInfo["propertyName"];
            string jsPropertyName = StringUtil.LowercaseFirst(propertyName);
            unmanagedWrapper.AddPropertyMapping(jsPropertyName, propertyName);
            string nameStr = jsPropertyName;
            CefV8PropertyAttribute propertyAttribute = (CefV8PropertyAttribute)(int.Parse(propertyInfo["propertyAttribute"]));
            // 键值对:[属性名,存取控制,None|Readonly]
            javascriptObject.SetValue(nameStr, CefV8AccessControl.Default, propertyAttribute);
        }
    }
}

(6)写一个网页,去前台通过JS调用,例如网页那边调用:myHandle.myFunction ()
首先在Render进程中触发BindingHandlerExecute方法,该方法解析JS传递进来的方法名,参数列表(TypeUtil将V8数据类型转换为CLR数据类型),之后将解析到的数据打包发送给Browser进程。

class BindingHandler:CefV8Handler
{
    protected override bool Execute(string name, CefV8Value obj, CefV8Value[] arguments, out CefV8Value returnValue, out string exception)
    {
        // 设置默认的返回值和异常值
        returnValue = CefV8Value.CreateNull();
        result = string.Empty ;
        exception = string.Empty;

        // 直接把注册进去的unmanagedWrapper传递到这里,属性和方法都可以从这里拿到
        UnmanagedWrapper unmanagedWrapper = obj.GetUserData() as UnmanagedWrapper;
        int hashcode = unmanagedWrapper.GetHashCode();
        if (hashcode == 0)
        {
            exception = "Binding's CLR object is null.";
            return true;
        }

        // 根据调用的方法名name直接拿到对应的CLR对象的真正的方法名
        string methodName = unmanagedWrapper.GetMethodMapping(name);

        // 获取从js中传递过来的参数,需要类型转换(TypeUtil.ConvertFromCef)
        var suppliedArguments = new object[arguments.Length];
        try
        {
            for (int i = 0; i < suppliedArguments.Length; i++)
                suppliedArguments[i] = TypeUtil.ConvertFromCefV8Value(arguments[i]);
        }
        catch (Exception err)
        {
            exception = err.Message;
            return true;
        }

        //拿到真正的方法名之后将对象的哈希值,方法名和参数发送给Browser进程
        CefProcessMessage msg = CefProcessMessage.Create("ExecuteMehtod");
        CefListValue args = msg.Arguments;
        args.SetInt(0, hashcode);
        args.SetString(1, methodName);
        SetArguments(suppliedArguments,ref args);

        if (this.OnSendExecuteMsgHandler != null)
            this.OnSendExecuteMsgHandler(CefProcessId.Browser, msg);

        // 阻塞等待返回值和异常值
        BindingHandler.signal.WaitOne();

        if (!string.IsNullOrEmpty(BindingHandler.exception))
        {
            exception = BindingHandler.exception;
            return true;
        }
        returnValue = TypeUtil.ConvertToCefV8Value(BindingHandler.result, BindingHandler.result.GetType());

        return true;
    }
}

(7)Browser进程中的OnProcessMessageReceived方法接受到Render进程执行方法的消息,首先将方法名参数解析出来,然后调用BindingHandlerExecuteMethod方法,最后将方法返回值发送给Render进程。

public class WebView : WpfCefBrowser
{
    public override bool OnProcessMessageReceived(CefBrowser browser, CefProcessId sourceProcess, CefProcessMessage message)
    {
        // 接收到JS执行结果消息
        if (message.Name == "EvaluteJavaScriptResult")
        {
            // 解析处理结果
            this.exceptionMessage = message.Arguments.GetString(0);
            CefBinaryValue binaryValue = message.Arguments.GetBinary(1);
            byte[] binaryData = binaryValue.ToArray();
            this.result = SerializationUtil.DeserializeObject(binaryData);

            // 发送处理完毕信号
            signal.Set();
            return true;
        }
        // 接收到Render发送的对象的哈希值、方法名和参数列表
       else if (message.Name == "ExecuteMehtod")
        {
            CefListValue args = message.Arguments;
            int hashcode = args.GetInt(0);
            string methodName = args.GetString(1);
            // 参数列表
            object[] arguments = new object[args.Count-2];
            for (int i = 2; i < args.Count; i++)
            {
                var type = args.GetValueType(i);
                if (type == CefValueType.Bool)
                    arguments[i-2] = args.GetBool(i);
                else if (type == CefValueType.Int)
                    arguments[i-2] = args.GetInt(i);
                else if (type == CefValueType.Double)
                    arguments[i-2] = args.GetDouble(i);
                else if (type == CefValueType.String)
                    arguments[i-2] = args.GetString(i);
            }
            // 根据对象的哈希值获取对象
            var obj = CefGlueRuntime.ObjectList[hashcode];

            BindingHandler.ExecuteMethod(obj, methodName, arguments);

            // 方法执行结束后把结果发送给Render进程
            CefProcessMessage msg = CefProcessMessage.Create("ExecuteMethodResult");
                CefListValue retargs = msg.Arguments;
                retargs.SetString(0, BindingHandler.exception);
                byte[] serializeObj = SerializationUtil.SerializeObject(BindingHandler.result);
                retargs.SetBinary(1, CefBinaryValue.Create(serializeObj));
                this.SendProcessMessage(CefProcessId.Renderer, msg);
            return true;
        }
        return false;
    }
}

BindingHandlerExecuteMethod方法如下,它首先调用FindBestMethod匹配最合适的方法,匹配成功后进行Invoke调用

// 方法执行的这个过程是在Browser进程里执行的
public static void ExecuteMethod(object obj, string methodName, object[] suppliedArguments)
{
    // 一个对象的方法可能会被重载,所以根据方法名获取到的方法可能会有多个
    var type = obj.GetType();
    var methods = type.GetMember(methodName, MemberTypes.Method, BindingFlags.Instance | BindingFlags.Public);
    if (methods.Length == 0)
    {
        BindingHandler.exception = "No method named " + methodName + ".";
    }

    // 通过反射寻找最合适的方法和参数列表
    MethodInfo bestMethod = null;
    object[] bestMethodArguments = null;
    FindBestMethod(methods, suppliedArguments, ref bestMethod, ref bestMethodArguments);


    // 找到最好的方法和参数列表,然后进行调用,并且把返回值转换为CefV8Value对象(TypeUtil.ConvertToCef)
    if (bestMethod != null)
    {
        try
        {
            object result = bestMethod.Invoke(obj, bestMethodArguments);
            if(result != null)
                BindingHandler.result = TypeUtil.ConvertToCefV8Value(result, bestMethod.ReturnType);
        }
        catch (TargetInvocationException err)
        {
            BindingHandler.exception = err.Message;
        }
        catch (Exception e)
        {
            BindingHandler.exception = e.Message;
        }
    }
    else
    {
        BindingHandler.exception = "Argument mismatch for method \"" + methodName + "\".";
    }
}

而匹配最佳方法FindBestMethod的逻辑如下:

public static void FindBestMethod(MemberInfo[] methods, object[] suppliedArguments, ref MethodInfo bestMethod,ref object[] bestMethodArguments)
{
    var bestMethodCost = -1;
    // 逐个遍历方法,然后进行参数匹配
    for (int i = 0; i < methods.Length; i++)
    {                
        MethodInfo method = methods[i] as MethodInfo;
        var parametersInfo = method.GetParameters();

        // 如果传递过来的参数个数 == 遍历中的方法的参数列表 == 0,则直接返回(没有参数的情况)
        if (parametersInfo.Length == 0)
        {
            if (suppliedArguments.Length == 0)
            {
                bestMethod = method;
                bestMethodArguments = suppliedArguments;
                bestMethodCost = 0;
                break;
            }
        }
        // 如果二者的参数个数不为0,则进行匹配
        else
        {
            int failed = 0; 
            int cost = 0;
            // 存放转换后的参数(把)
            var arguments = new List<object>();
            try
            {
                // p跟踪方法的参数,a跟踪传递过来的参数
                int p, a;

                for (p = 0, a = 0; failed==0 && p < parametersInfo.Length && a < suppliedArguments.Length; p++)
                {
                    ParameterInfo pi = parametersInfo[p];
                    Type paramType = pi.ParameterType;
                    // 如果参数是一个数组,且元素的类型是对象(该情况还未测试)
                    if (paramType.IsArray && paramType.GetElementType() == typeof(object))
                    {
                        // 该情况只能是最后一个参数,否则匹配失败
                        if (p < parametersInfo.Length - 1){
                            failed++;
                            break;
                        }

                        //将剩余的参数作为一个数组添加到arguments中
                        var parm = new List<object>();
                        for (; a < suppliedArguments.Length; a++)
                            parm.Add(suppliedArguments[a]);
                        arguments.Add(parm.ToArray());
                        cost += arguments.Count * 2;
                    }
                    // 如果是非集合类型,直接计算类型转换代价
                    else
                    {
                        // 计算类型转换代价
                        int paramCost = TypeUtil.GetChangeTypeCost(suppliedArguments[a], paramType);
                        // 代价为负,说明类型无法转换
                        if (paramCost < 0){
                            failed++;
                            break;
                        }
                        // 把转换好的参数添加到arguments中
                        arguments.Add(TypeUtil.ChangeType(suppliedArguments[a++], paramType));
                        cost += paramCost;
                    }
                }
            }catch(Exception e){
                failed++;
            }

            // 通过上述匹配过程确定最佳方法和参数列表
            if (failed > 0)
                continue;                    
            if (cost < bestMethodCost || bestMethodCost < 0)
            {
                bestMethod = method;
                bestMethodArguments = arguments.ToArray();
                bestMethodCost = cost;
            }
            if (cost == 0)
                break;
        }
    }
}

总结

文章中提到了两个问题:
第一个是编译CEF内核,需要有点耐心,多一点坚持,
第二个是支持JS调用Native功能,抓住以下两条路径就可以理顺。
(1)注册路径:CLR对象–>Browser进程保存–>反射解析方法名和参数–>发送给Render进程–>使用BindingHandle在V8引擎注册
(2)调用路径:H5中调用–>Render进程的Execute解析方法名和参数–>发送消息给Browser进程–>Browser进程调用BindingHandle的ExecuteMethod方法–>返回值发送给Render进程

-------------本文结束 感谢您的阅读-------------

本文标题:CEFGlue之回忆篇

文章作者:lingyun

发布时间:2018年05月26日 - 01:05

最后更新:2018年06月04日 - 00:06

原始链接:https://tsuijunxi.github.io/2018/05/26/CEFGlue之回忆篇/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。