上一篇讲了如何构建可调试的多进程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.
void RenderApp::OnContextCreated(CefRefPtr browser, CefRefPtr frame, CefRefPtr context)
{
OutputDebugString(_T("ClientAppRenderer::OnContextCreated, create window binding\r\n"));
// Retrieve the context's window object.
CefRefPtr object = context->GetGlobal();
if (nullptr == m_v8Handler){
m_v8Handler = new V8HandlerImpl;
}
// Create the "NativeLogin" function.
CefRefPtr func = CefV8Value::CreateFunction("NativeLogin", m_v8Handler);
// Add the "NativeLogin" function to the "window" object.
object->SetValue("NativeLogin", func, V8_PROPERTY_ATTRIBUTE_NONE);
//
CefRefPtr fId = CefV8Value::CreateFunction("getIdInfo", m_v8Handler);
CefRefPtr fCm = CefV8Value::CreateFunction("getCameraInfo", m_v8Handler);
object->SetValue("getIdInfo", fId, V8_PROPERTY_ATTRIBUTE_NONE);
object->SetValue("getCameraInfo", fCm, V8_PROPERTY_ATTRIBUTE_NONE);
// test
/*CefRefPtr fcb = CefV8Value::CreateFunction("callbackId", m_v8Handler);
object->SetValue("callbackId", fcb, V8_PROPERTY_ATTRIBUTE_NONE);*/
// js 注册回调
// Create the "register" function.
CefRefPtr 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 object, /*JavaScript调用者对象*/
const CefV8ValueList& arguments, /*JavaScript传递的参数*/
CefRefPtr& 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 msg = CefProcessMessage::Create("login_msg");
// Retrieve the argument list object.
CefRefPtr 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 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 callback_func_ = arguments[0];
CefRefPtr callback_context_ = CefV8Context::GetCurrentContext();
callback_func_->ExecuteFunction(NULL, arguments);//执行回调函数
return true;
}
// 测试回调
//if (arguments.size() == 2 && arguments[0]->IsFunction())
//{
// CefRefPtr callback_func_ = arguments[0];
// CefRefPtr 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 callback_func_ = arguments[0];
int strImgType = arguments.at(0)->GetIntValue();
//TODO: doSomething() in native way
CefRefPtr msg = CefProcessMessage::Create("cbId");
// Retrieve the argument list object.
CefRefPtr 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 msg = CefProcessMessage::Create("getIdInfo");
// Retrieve the argument list object.
CefRefPtr 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 msg = CefProcessMessage::Create(name);
// Retrieve the argument list object.
CefRefPtr 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 browser, CefProcessId source_process, CefRefPtr message)
{
int iState;
if (message->GetName() == _T("getIdInfo"))
{
CefRefPtr 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 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 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 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"));
CefRefPtrmsg_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),回调等特性。