上一篇讲了如何构建可调试的多进程CEF应用,这篇讲一下各个进程之间的通信,包括C++ 调JS, js 调C++,以及browser和render进程的消息流动。为什么先从多进程入手,个人试了一下,单进程模式,各个线程切换以及执行的时机不是很清楚,也没找到调试的正确姿势。多进程把线程分解出来变成独立应用,也算是以空间换时间的一种策略。这一篇中个人遇到好几个问题,虽然最终解决了,但是原因一直不是特别清楚。由于个人对于CEF理解有限,好多看法仅供参考。
-
browser和render进程
-
browser 进程姑且叫UI进程,主要负责一些UI交互,比如页面加载,窗口弹出、菜单、前一页后一页、上传下载等;
-
render进程姑且叫Web进程,主要做JS的解释和执行,以及DOM结构的解析等;
-
明白各个进程的作用或者角色,然后分析一下,几种消息的流动(以下流程,基于自己当前理解,可能会有错误)
-
运行浏览器 - >browser进程根据url拉取服务端web数据 -> render进程解析DOM/JS ->GPU进程完成渲染 -> browser进程显示页面;
-
触发JS搞事情 -> render进程处理js->browser进程收保护费->然后再给render开一张发票;
-
第一种方式的通信
现在最常用的,打开浏览器,输入地址或者关键字,从服务器拉取数据到本地浏览器做解释,然后渲染显示。
// Create the first browser window.
//CefString url = "http://172.16.2.159";
CefString url = strURL;//"file:///E://cef//index.html";
CefBrowserHost::CreateBrowser(window_info, client, url, browser_settings, NULL);
创建浏览器进程,然后传入url地址,拉取数据解析和渲染过程没做进一步研究,这里就不瞎说了。
- 第二种方式,是重点介绍的部分
先把大的问题说下,最后说一下简单的实现。进程之间通信,无非就那几种方式,信号、共享内存,邮件槽,管道,windows平台的剪切板。CEF中browser进程和render进程是通过管道通信的。CEF做了一层封装所有行为都以接口的形式对外提供,通过继承接口,重写接口函数,处理不同的消息。
我们的需求是js触发调用,render拦截到调用,browser进程做一些额外操作,然后把操作结果返回js.
- js调用C++,走CEF流程处理,一般有两种方式。第一种render进程往js 全局对象window添加函数供调用,需要继承CefRenderProcessHandler接口,重写OnContextCreated方法
-
void RenderApp::OnContextCreated(CefRefPtr<CefBrowser> browser, CefRefPtr<CefFrame> frame, CefRefPtr<CefV8Context> context) { OutputDebugString(_T("ClientAppRenderer::OnContextCreated, create window binding\r\n")); // Retrieve the context's window object. CefRefPtr<CefV8Value> object = context->GetGlobal(); if (nullptr == m_v8Handler){ m_v8Handler = new V8HandlerImpl; } // Create the "NativeLogin" function. CefRefPtr<CefV8Value> func = CefV8Value::CreateFunction("NativeLogin", m_v8Handler); // Add the "NativeLogin" function to the "window" object. object->SetValue("NativeLogin", func, V8_PROPERTY_ATTRIBUTE_NONE); // CefRefPtr<CefV8Value> fId = CefV8Value::CreateFunction("getIdInfo", m_v8Handler); CefRefPtr<CefV8Value> fCm = CefV8Value::CreateFunction("getCameraInfo", m_v8Handler); object->SetValue("getIdInfo", fId, V8_PROPERTY_ATTRIBUTE_NONE); object->SetValue("getCameraInfo", fCm, V8_PROPERTY_ATTRIBUTE_NONE); // test /*CefRefPtr<CefV8Value> fcb = CefV8Value::CreateFunction("callbackId", m_v8Handler); object->SetValue("callbackId", fcb, V8_PROPERTY_ATTRIBUTE_NONE);*/ // js 注册回调 // Create the "register" function. CefRefPtr<CefV8Value> func1 = CefV8Value::CreateFunction("register", m_v8Handler);//第一个参数和SetValue参数保持一致,否则无法调用 // Add the "register" function to the "window" object. object->SetValue("register", func1, V8_PROPERTY_ATTRIBUTE_NONE); }
m_v8Handler 是一个类成员,对象初始化的时候就创建了,但是CEF多进程Debug模式下,由当前浏览器页面跳转其他web页面的时候(这时候会重新加载frame,构造新的context )会出现m_v8Handler为空的情况,不知道原因,但release下无法复现,所以这里会判空,然后重新创建v8 handler。
-
完成以上这步,js 的window 对象内就注册了几个方法,可以调用。js调用注册函数的时候,render需要拦截调用,需要继承CefV8Handler,然后重写Execute函数
-
bool V8HandlerImpl::Execute(const CefString& name, /*JavaScript调用的C++方法名字*/ CefRefPtr<CefV8Value> object, /*JavaScript调用者对象*/ const CefV8ValueList& arguments, /*JavaScript传递的参数*/ CefRefPtr<CefV8Value>& retval, /*返回给JS的值设置给这个对象*/ CefString& exception) { if (name == "NativeLogin") {//Window Binding if (arguments.size() == 2) { CefString strUser = arguments.at(0)->GetStringValue(); CefString strPassword = arguments.at(1)->GetStringValue(); //TODO: doSomething() in native way CefRefPtr<CefProcessMessage> msg = CefProcessMessage::Create("login_msg"); // Retrieve the argument list object. CefRefPtr<CefListValue> args = msg->GetArgumentList(); // Populate the argument values. args->SetSize(2); args->SetString(0, strUser); args->SetString(1, strPassword); // Send the process message to the browser process. CefV8Context::GetCurrentContext()->GetBrowser()->SendProcessMessage(PID_BROWSER, msg); retval = CefV8Value::CreateInt(0); //函数的返回值 我们可以拿这个返回值做判断或者其他操作 //var result = window.NativeLogin(document.getElementById("userName").value, document.getElementById("password").value); //document.getElementById("text").innerHTML = result } else { retval = CefV8Value::CreateInt(2); } return true; } else if (name == "GetId") {//JS Extensions if (arguments.size() == 0) { // execute javascript // just for test CefRefPtr<CefFrame> frame = CefV8Context::GetCurrentContext()->GetBrowser()->GetMainFrame(); frame->ExecuteJavaScript("alert('Hello, I came from native world.')", frame->GetURL(), 0); // return to JS retval = CefV8Value::CreateString("72395678"); return true; } } else if (name == "register") { if (arguments.size() == 1 && arguments[0]->IsFunction()) { CefRefPtr<CefV8Value> callback_func_ = arguments[0]; CefRefPtr<CefV8Context> callback_context_ = CefV8Context::GetCurrentContext(); callback_func_->ExecuteFunction(NULL, arguments);//执行回调函数 return true; } // 测试回调 //if (arguments.size() == 2 && arguments[0]->IsFunction()) //{ // CefRefPtr<CefV8Value> callback_func_ = arguments[0]; // CefRefPtr<CefV8Context> callback_context_ = CefV8Context::GetCurrentContext(); // //callback_func_->ExecuteFunction(NULL, arguments);//执行回调函数 // return true; //} } else if (name == "callbackId") { int iSz = arguments.size(); if (iSz == 2 && arguments[0]->IsFunction()) { CefRefPtr<CefV8Value> callback_func_ = arguments[0]; int strImgType = arguments.at(0)->GetIntValue(); //TODO: doSomething() in native way CefRefPtr<CefProcessMessage> msg = CefProcessMessage::Create("cbId"); // Retrieve the argument list object. CefRefPtr<CefListValue> args = msg->GetArgumentList(); callback_func_->AddRef(); // Populate the argument values. args->SetSize(2); args->SetInt(0, strImgType); //args->SetValue(1, callback_func_); CefV8Context::GetCurrentContext()->GetBrowser()->SendProcessMessage(PID_BROWSER, msg); } } else if (name == "getIdInfo") { int iSz = arguments.size(); int strImgType = arguments.at(0)->GetIntValue(); //TODO: doSomething() in native way CefRefPtr<CefProcessMessage> msg = CefProcessMessage::Create("getIdInfo"); // Retrieve the argument list object. CefRefPtr<CefListValue> args = msg->GetArgumentList(); // Populate the argument values. args->SetSize(1); args->SetInt(0, strImgType); CefV8Context::GetCurrentContext()->GetBrowser()->SendProcessMessage(PID_BROWSER, msg); } else if (name == "getCameraInfo") { int iSz = arguments.size(); int strImgType = arguments.at(0)->GetIntValue(); //TODO: doSomething() in native way CefRefPtr<CefProcessMessage> msg = CefProcessMessage::Create(name); // Retrieve the argument list object. CefRefPtr<CefListValue> args = msg->GetArgumentList(); // Populate the argument values. args->SetSize(1); args->SetInt(0, strImgType); CefV8Context::GetCurrentContext()->GetBrowser()->SendProcessMessage(PID_BROWSER, msg); } // Function does not exist. return false; }
当拦截到getIdInfo后,不要通知browser做一些处理,然后把数据返回js。browser进程需要继承CefClient接口,然后重写 OnProcessMessageReceived方法,用于接收render进程发送的消息
-
bool CustomClient::OnProcessMessageReceived(CefRefPtr<CefBrowser> browser, CefProcessId source_process, CefRefPtr<CefProcessMessage> message) { int iState; if (message->GetName() == _T("getIdInfo")) { CefRefPtr<CefListValue> argList = message->GetArgumentList(); int strImgType = argList->GetInt(0); // getIdCardInfo CefString strContent(_T("Welcode getIdInfo")); getMediaInfo(browser, CefPoint(), strImgType, strContent, iState); std::wstring strFunc(_T("whenreadover")); CefRefPtr<CefProcessMessage> msg_back = CefProcessMessage::Create(strFunc); msg_back->GetArgumentList()->SetString(0, strFunc); msg_back->GetArgumentList()->SetString(1, strContent); //browser->SendProcessMessage(PID_RENDERER, msg_back); std::wstring s2(strContent); CString strReplace= s2.c_str(); strReplace.Replace(L"\r\n", L""); // 2018.8.4 这里是个坑,传入复合字符包换换行符,无法传递参数 CString str; str.Format(L"whenreadover('%s')", strReplace); browser->GetMainFrame()->ExecuteJavaScript((LPCTSTR)str, browser->GetMainFrame()->GetURL(), 0); return true; } else if (message->GetName() == _T("getCameraInfo")) { CefRefPtr<CefListValue> argList = message->GetArgumentList(); int strImgType = argList->GetInt(0); // getCameraInfo CefString strContent(_T("Welcome getCameraInfo")); getMediaInfo(browser, CefPoint(), 2, strContent, iState); std::wstring s2(strContent); CString strReplace = s2.c_str(); strReplace.Replace(L"\r\n", L""); strReplace.Replace(L"\n\t", L""); strReplace.Replace(L" ", L""); std::wstring s3(strReplace); std::wstring strFunc(_T("whenreadover")); CefRefPtr<CefProcessMessage> msg_back = CefProcessMessage::Create(strFunc); msg_back->GetArgumentList()->SetString(0, strFunc); msg_back->GetArgumentList()->SetString(1, s3); //browser->SendProcessMessage(PID_RENDERER, msg_back); 2018.8.4 这里是个坑,传入复合字符包换换行符,无法传递参数 CString str; str.Format(L"whenreadover('%s')", strReplace); browser->GetMainFrame()->ExecuteJavaScript((LPCTSTR)str, browser->GetMainFrame()->GetURL(), 0); } return false; // has handle for the message }
browser进程拦截消息后,可以做交互,获取数据,然后可以再次发消息给render进程(需要继承CefRenderProcessHandler,然后重写OnProcessMessageReceived方法),或者可以直接调用约定的js函数,不需要返回render进程再调用,这是因为之前render进程已经把对应browser对象的指针传入了(这里姑且理解为一个web页面对应一个browser对象,一个browser对象对应一个frame,也对应一个v8 context, 这个context决定了js对象、函数的作用域和可见性,这个地方可能有误,注意),用程序验证,确实是可以的
-
// getIdCardInfo
CefString strContent(_T("Welcode getIdInfo"));
getMediaInfo(browser, CefPoint(), strImgType, strContent, iState);std::wstring strFunc(_T("whenreadover"));
CefRefPtr<CefProcessMessage> msg_back = CefProcessMessage::Create(strFunc);
msg_back->GetArgumentList()->SetString(0, strFunc);
msg_back->GetArgumentList()->SetString(1, strContent);
//browser->SendProcessMessage(PID_RENDERER, msg_back);std::wstring s2(strContent);
CString strReplace= s2.c_str();
strReplace.Replace(L"\r\n", L"");// 2018.8.4 这里是个坑,传入复合字符包换换行符,无法传递参数
CString str;
str.Format(L"whenreadover('%s')", strReplace);
browser->GetMainFrame()->ExecuteJavaScript((LPCTSTR)str, browser->GetMainFrame()->GetURL(), 0);
whenreadover是js中定义的方法,不需要提前注册,所以这里的上下文环境很重要。调用的方式是构造字符串,格式为func('params'),URL可有可无。
参数传递的时候不能包含"\r\n" 空格等特殊格式化字符,也不能包含用json第三方库格式化出来的json字符串(c++ jsonCpp这个库格式化出来的字符无法成功传递)。原因推测v8引擎需要分析js的语义,部分格式化字符可能有冲突了,所以产生了异常,导致js调用失败。格式化字符串作为参数传递的时候经常会有这样的问题,尤其是在一些协议里,涉及到二次解析的可能就有问题,这里要注意。
篇幅太长了,下一篇单独讲如何通过js扩展实现js与C++通信,包括但参数(json),回调等特性。