cef GeneralUsage

本文翻译自https://bitbucket.org/chromiumembedded/cef/wiki/GeneralUsage

介绍

cef是个基于chromium的开源项目。跟chromium项目不同,cef主要聚焦于 google chrome应用开发,cef集中于促进嵌入式浏览器在第三方应用软件中的使用。cef提供了稳定程度达到产品等级的API和使用特定chromium发布分支还有二进制发布包,从而将用户和底层的chromium还有blink代码隔离开来。大部分cef的功能都有默认实现,因此用户只需要很少的或压根不需要额外的工作。

下面是一些cef的使用场景

  • 在一个现有的本机应用程序中嵌入一个兼容html5的web浏览器控件
  • 创建一个轻量级的本地“shell”应用程序,该程序托管一个主要使用Web技术开发的用户界面。
  • 在具有自定义的绘画框架的应用中离屏渲染一个web内容
  • 充当现有Web属性和应用程序自动测试的主机。

cef3是基于多进程的chromium content api实现的。cef多进程架构优势包括:

  • 提升性能表现和稳定性(JavaScript和插件运行在独立的进程)
  • 支持视网膜显示。(Support for Retina displays.)
  • GPU加速
  • 非常酷的新特性例如webrtc和语言输入
  • 通过devtools提供更好的自动化UI测试和远程调试工具还有ChromeDriver2
  • 更快的调用现有的和未来将要有的web特性和标准的速度。

本篇文档将介绍一些有关使用cef3开发应用的基本概念。

开始

使用二进制发布包

cef3的二进制发布包可以从这里下载,包括各种平台(Windows, Mac OS X或者Linux)需要的所有文件和特定版本的cef3。不管是什么平台,都有着同样的基本结构

  • cefclient 包含配置为使用二进制发行版中的文件构建的cefclient示例应用程序。这个应用程序演示了广泛的CEF功能。
  • cefsimple 包含配置为使用二进制发行版中的文件构建的cefclient示例应用程序。这个应用程序演示了创建浏览器窗口所需的最小功能。
  • Deubg 包含了cef shared library的debug构建。
  • include 包含了所有cef需要的头文件
  • libcef_dll 包含所有使用CEF c++ API的应用程序必须链接的libcef_dll_wrapper静态库的源代码。
  • Release 包含一个版本构建CEF共享库(libcef)和任何其他需要在平台上运行的库。
  • Resources 包含使用CEF(仅适用于Windows和Linux)的应用程序所需的资源。这包括.pak文件(带有全局资源的二进制文件)和其他可能的文件,这些文件取决于平台。

每个二进制发布包还包含了一个README.txt文件,用来详细解释平台相关的发布包。还有一个LICENSE.txt文件,包含了cef的BSD许可。当发布一个基于CEF的应用时,你需要在你应用的发布版本中的某个地方包含这个文件。

基于CEF二进制发布文件开发的应用程序可以适用标准平台构建工具。这些工具包括Windows上的Visual Studio,Mac OS X上的Xcode,Linux上的gcc/make。项目下载页面包含关于特定二进制版本所需的操作系统和构建工具版本的信息。在Linux上构建时,还要注意所列出的包依赖项。

重要概念

在开发基于cef3的应用程序之前,需要理解一些重要的基本概念。

C++封装

libcef共享库暴露了一个C API,该API将用户与CEF运行时和代码库隔离开来。libcef_dll_wrapper项目以源代码的形式作为二进制发行包的一部分存在。导出的C API封装在C++ API中,然后链接到客户端应用程序。C/C++ API转换层的代码是用translator工具自动生成的。

进程

CEF3运行时是多进程的。被称作"browser"进程的主进程掌管着窗口创建,绘制和网络连接。这通常与主机应用程序相同,大多数应用程序逻辑将在浏览器进程中运行。Blink渲染和JavaScript执行则在一个独立的被称作"render"进程中。一些应用逻辑,例如JS绑定和DOM访问,也都运行在render进程中。默认的进程模型(默认情况下,进程模型?)将为每一个origin(scheme+域名)提供一个新的render进程。其他进程将在需要时产生,例如掌管类似flash的插件的"plugin"进程,还有掌管加速合成的"gpu"进程

默认情况下,可执行的主应用程序(即二进制程序)将被执行多次用来表示独立的进程(即和chormium类似,所有进程的入口都是主程序),可以通过命令行参数传递给CefExecuteProcess函数,如果主应用程序太大了,需要花费很长的时间加载,或者不适合非browser进程的其他进程,则可以用独立的(另外的)可执行程序提供给非browser进程。能在CefSettings.browser_subprocess_path变量中配置。

由CEF3产生的独立进程使用进程间通信(IPC)进行通信。在browser进程的应用程序逻辑和render进程可以通过来回发送异步消息进行通信。render进程中的JavaScriptIntegration可以暴露用于在browser进程中处理的异步api。

线程

在CEF3中,每个进程都有多个线程在运行。想要知道完整的线程列表,可以查看cef_threaad_id_t枚举。这里有一些常用的线程:

  • TID_UI 线程是browser进程的主线程,这个线程和主程序线程是同一个线程(如果CefSettings.multi_threaded_message_loop为false的话)
  • TID_IO 线程用于browser进程处理IPC和网络信息
  • TID_FILE 线程用于browser进程和文件系统打交道,阻塞操作只能在这个线程上执行,或者再创建一个进程(CefThread)执行阻塞操作。
  • TID_RENDERER 线程是render进程的主线程,所有的Blink和V8交互必须在这个线程上进行。

由于CEF具有多线程特性,因此使用消息传递或锁定来保护数据成员免于访问多个线程非常重要。CefPostTask系列函数支持线程之间轻松的异步消息传递,就是PostTask。

可以使用CefCurrentlyOn函数来验证当前线程是否是预期线程。CEF示例应用程序使用以下定义来验证方法是否在预期线程上执行。这些定义包含在include / wrapper / cef_helpers.h头文件中。

#define CEF_REQUIRE_UI_THREAD()       DCHECK(CefCurrentlyOn(TID_UI));
#define CEF_REQUIRE_IO_THREAD()       DCHECK(CefCurrentlyOn(TID_IO));
#define CEF_REQUIRE_FILE_THREAD()     DCHECK(CefCurrentlyOn(TID_FILE));
#define CEF_REQUIRE_RENDERER_THREAD() DCHECK(CefCurrentlyOn(TID_RENDERER));

为了支持对代码块的同步访问,CEF通过include/base/cef_lock.h头文件提供了base::Lock和base::AutoLock类型。例如:

#include "include/base/cef_lock.h"

// Class declaration.
class MyClass : public CefBase {
 public:
  MyClass() : value_(0) {}
  // Method that may be called on multiple threads.
  void IncrementValue();
 private:
  // Value that may be accessed on multiple theads.
  int value_;
  // Lock used to protect access to |value_|.
  base::Lock lock_;
  IMPLEMENT_REFCOUNTING(MyClass);
};

// Class implementation.
void MyClass::IncrementValue() {
  // Acquire the lock for the scope of this method.
  base::AutoLock lock_scope(lock_);
  // |value_| can now be modified safely.
  value_++;
}

引用计数

所有框架类都实现了CefBase接口,并且所有实例指针均使用CefRefPtr智能指针实现处理,该智能指针实现通过调用AddRef()和Release()自动处理引用计数。实现这些类的最简单方法如下:

class MyClass : public CefBase {
 public:
  // Various class methods here...

 private:
  // Various class members here...

  IMPLEMENT_REFCOUNTING(MyClass);  // Provides atomic refcounting implementation.
};

// References a MyClass instance
CefRefPtr<MyClass> my_class = new MyClass();

字符串

CEF定义了自己的数据类型来表示字符串,有下面几个原因

  • libcef库和主机应用程序可以使用不同的运行时来管理堆内存。需要使用分配内存的相同运行时释放所有对象(包括字符串)。
  • 可以编译libcef库以支持不同的基础字符串类型(UTF8,UTF16或宽)。默认值为UTF16,但可以通过修改cef_string.h中的定义并重新编译CEF 来更改。选择宽字符串类型时,请记住大小会因平台而异。

命令行参数

许多CEF3和Chromium的功能特才行都可以用命令行参数来设置。这些参数采用 --some-argument [= optional-param]的形式,并通过CefExecuteProcess()CefMainArgs结构传递到CEF中

  • 要从命令行禁用参数处理,请在将CefSettings传递到CefInitialize()之前,将CefSettings.command_line_args_disabled设置为true。
  • 要在主机应用程序内部指定CEF / Chromium命令行参数,请实现CefApp:: OnBeforeCommandLineProcessing()方法。
  • 要将特定于应用程序的(非CEF / Chromium)命令行参数传递给子流程,请实现CefBrowserProcessHandler :: OnBeforeChildProcessLaunch()方法。

想知道更多参数?可以看shared/common/client_switches.cc中的注释

应用结构

每一个CEF3应用都有通用的结构

  • 提供一个入口函数用于初始化CEF并运行子进程可执行逻辑或CEF消息循环的其中之一。
  • 提供一个CefApp的实现用于处理特定的进程回调。
  • 提供一个CefClient的实现用于处理特定的浏览器实例回调。
  • 调用CefBrowserHost::CreateBrowser()来创建一个browser实例并使用CefLifeSpanHandler来管理这个browser的声明周期

入口函数

按照上述的进程小节的描述,CEF3应用程序是多进程的。这些进程可以都用同一个可执行程序或者用独立的可执行程序们。进程的执行起始于一个入口函数。例子程序在cefclient/cefclient_win.cc``cefclient/cefclient_gtk.cc``cefclient/cefclient_mac.mm中。

当启动子进程时,CEF将必须使用CefMainArgs结构传递到CefExecuteProcess来指定配置信息。Liunux和Mac OS X上,就是main函数的argc和argv。在Windows平台。则是实例句柄(HINSTANCE),它被传递给wWinMain()函数。还可以通过GetModuleHandle(NUll)检索实例句柄。

单一可执行文件

当作为单个可执行文件运行时,需要入口函数来区分不同的进程类型。Windows和Linux都支持单一可执行结构,而Mac OS X不支持。

int main(int argc, char* argv[]) {
  // Structure for passing command-line arguments.
  // The definition of this structure is platform-specific.
  CefMainArgs main_args(argc, argv);

  // Optional implementation of the CefApp interface.
  CefRefPtr<MyApp> app(new MyApp);

  // Execute the sub-process logic, if any. This will either return immediately for the browser
  // process or block until the sub-process should exit.
  int exit_code = CefExecuteProcess(main_args, app.get());
  if (exit_code >= 0) {
    // The sub-process terminated, exit now.
    return exit_code;
  }

  // Populate this structure to customize CEF behavior.
  CefSettings settings;

  // Initialize CEF in the main process.
  CefInitialize(main_args, settings, app.get());

  // Run the CEF message loop. This will block until CefQuitMessageLoop() is called.
  CefRunMessageLoop();

  // Shut down CEF.
  CefShutdown();

  return 0;
}

CefExecuteProcess函数就能执行和区分不同的进程,browser进程会立刻返回-1,其他进程则会执行完毕后返回对应进程的exit_code

独立子进程可执行程序

使用独立子进程可执行程序时候,需要两个独立的可执行文件,和两个独立的入口函数

主应用入口函数:

// Program entry-point function.
int main(int argc, char* argv[]) {
  // Load the CEF framework library at runtime instead of linking directly
  // as required by the macOS sandbox implementation.
  CefScopedLibraryLoader library_loader;
  if (!library_loader.LoadInMain())
    return 1;

  // Structure for passing command-line arguments.
  // The definition of this structure is platform-specific.
  CefMainArgs main_args(argc, argv);

  // Optional implementation of the CefApp interface.
  CefRefPtr<MyApp> app(new MyApp);

  // Populate this structure to customize CEF behavior.
  CefSettings settings;

  // Specify the path for the sub-process executable.
  CefString(&settings.browser_subprocess_path).FromASCII(“/path/to/subprocess”);

  // Initialize CEF in the main process.
  CefInitialize(main_args, settings, app.get());

  // Run the CEF message loop. This will block until CefQuitMessageLoop() is called.
  CefRunMessageLoop();

  // Shut down CEF.
  CefShutdown();

  return 0;
}

子进程应用入口函数:

// Program entry-point function.
int main(int argc, char* argv[]) {
  // Initialize the macOS sandbox for this helper process.
  CefScopedSandboxContext sandbox_context;
  if (!sandbox_context.Initialize(argc, argv))
    return 1;

  // Load the CEF framework library at runtime instead of linking directly
  // as required by the macOS sandbox implementation.
  CefScopedLibraryLoader library_loader;
  if (!library_loader.LoadInHelper())
    return 1;

  // Structure for passing command-line arguments.
  // The definition of this structure is platform-specific.
  CefMainArgs main_args(argc, argv);

  // Optional implementation of the CefApp interface.
  CefRefPtr<MyApp> app(new MyApp);

  // Execute the sub-process logic. This will block until the sub-process should exit.
  return CefExecuteProcess(main_args, app.get());
}

消息循环集成

CEF也能集成到已有应用的消息循环机制中,而不是使用CEF自带的消息循环。有两种方法可以做到。

  1. 调用CefDoMessageLoopWork()来代替CefRunMessageLoop()。每一次调用都将表示一个CEF循环执行一次。这个方法应该谨慎使用,调用不够频繁将使CEF消息循环陷入饥饿,并对浏览器性能产生负面影响。过于频繁的调用该方法则会占用CPU。
  2. 设置CefSettings.multi_threaded_message_loop = true(仅限Windows)。这将导致CEF在与主应用程序线程不同的线程上运行浏览器UI线程。使用这种方法,就不需要调用CefDoMessageLoopWork()CefRunMessageLoop()CefInitialize()CefShutDown()仍应该在主应用程序的主线程上调用。您将需要提供自己的与主应用程序线程进行通信的机制(例如,参见cefclient_win.cpp中的消息窗口用法)。您可以在Windows的cefclient中通过运行“ --multi-threaded-message-loop”命令行标志来测试此模式。

CefSettings

CefSettings允许配置CEF设置,下面是一些通常使用的配置成员:

  • browser_subprocess_path 配置子进程启动独立的可以执行文件的路径。
  • multi_threaded_message_loop 设置为true可以使浏览器进程消息循环在单独的线程中运行。
  • command_line_args_disabled 设置为true可以禁用命令行参数配置。
  • ``cache_path` 将缓存数据存储在磁盘上的路径,如果为空,则内存高速缓存将用于某些功能,而临时磁盘高速缓存将用于其他功能。如果指定了缓存路径,则诸如localStorage之类的HTML5数据库将仅在会话之间持久存在。
  • locale 将传递给Blink的语言环境字符串。如果为空,则使用默认的"en-US"。在使用语言环境以优先级顺序解析的语言环境确定语言环境的Linux上,将忽略此值:LANGUAGE,LC_ALL,LC_MESSAGES和LANG。也可以使用“ lang”命令行开关进行配置。
  • log_file 用于设置调试日志的目录+文件名,如果为空,则使用默认的"debug.log",并将文件写入应用程序目录,也能使用命令行参数进行配置。
  • log_severity 日志等级,日志文件中仅记录此等级或比此等级还要高的日志。也能用命令行参数配置,等级有"verbose""info""warning""error""error-report"和"disable"
  • resources_dir_path 资源目录的标准路径。如果此值为空,则cef.pak和/或devtools_resources.pak文件必须位于Windows / Linux上的模块目录中或Mac OS X上的应用程序包Resources目录中。也可以使用“ resources-dir-path”命令进行配置线路开关。
  • locales_dir_path 语言环境目录的标准路径。如果此值为空,那么语言环境目录必须位于模块目录中。在Mac OS X上,始终从应用程序包Resources目录中加载打包文件的情况下,此值将被忽略。也可以使用“ locales-dir-path”命令行开关进行配置。
  • remote_debugging_port 设置为1024到65535之间的值,以在指定的端口上启用远程调试。例如,如果指定8080,则远程调试URL将为http:// localhost:8080。可以从任何CEF或Chrome浏览器窗口中远程调试CEF。也可以使用“ remote-debugging-port”命令行开关进行配置。

CefBrowser和CefFrame

CefBrowser和CefFrame对象用于给browser发送命令和在回调方法中检索状态信息。每个CefBrowser对象将有一个主CefFrame对象代表top-level frame,有0或多个CefFrame对象代表sub-frame。例如,一个浏览器加载了两个iframe的话将会有三个CefFrame对象(一个top-level frame和两个iframe)

在浏览器main frame加载一个URL:

browser->GetMainFrame()->LoadURL(some_url);

让浏览器后退:

browser->GoBack();

检索获取main frame HTML contents:

// Implementation of the CefStringVisitor interface.
class Visitor : public CefStringVisitor {
 public:
  Visitor() {}

  // Called asynchronously when the HTML contents are available.
  virtual void Visit(const CefString& string) OVERRIDE {
    // Do something with |string|...
  }

  IMPLEMENT_REFCOUNTING(Visitor);
};

browser->GetMainFrame()->GetSource(new Visitor());

CefBrowser和CefFrame对象存在于浏览器进程和renderer进程中。

CefApp

CefApp接口提供了特定进程的回调,重要的回调包括:

  • OnBeforeCommandLineProcessing 提供了编程方式设置命令行参数
  • OnRegisterCustomSchemes 提供了注册自定义scheme
  • GetBrowserProcessHandler 可以返回browser进程的特定函数,包括OnContextInitialized()函数
  • GetRenderProcessHandler 可以返回render进程的特定的函数,包括相关JS的回调和进程消息。

一个实现CefApp的例子可以看cefsimple/simple_app.hcefsimple/simple_app.cc

CefClient

CefClient接口提供了可以访问特定的browser实例的回调,一个简单的CefClient实例可以被任意数量的browser对象共享。重要的回调包括:

  • 回调函数例如browser的生命周期的各个回调,右键菜单,对话框,显示通知,拖拽事件,焦点事件,键盘事件等等。这些回调都是可选的,可以实现也可以不实现。
  • OnProcessMessageReceived 当收到render进程发来的IPC通信时,这个回调就会被触发。

一个实现CefClient的例子可以看cefsimple/simple_handler.hcefsimple/simple_handler.cc

browser生命周期

browser的生命周期起始于调用CefBrowserHost::CreateBrowser()或者CefBrowserHost::CreateBrowserSync()。执行这俩函数的一个比较便利的地方就是在CefBrowserProcesHandler::OnContextInitialized()回调中或者对应平台的消息回调中,例如windows上的WM_CREATE。

// Information about the window that will be created including parenting, size, etc.
// The definition of this structure is platform-specific.
CefWindowInfo info;
// On Windows for example...
info.SetAsChild(parent_hwnd, client_rect);

// Customize this structure to control browser behavior.
CefBrowserSettings settings;

// CefClient implementation.
CefRefPtr<MyClient> client(new MyClient);

// Create the browser asynchronously. Initially loads the Google URL.
CefBrowserHost::CreateBrowser(info, client.get(), “http://www.google.com”, settings, NULL);

CefLifeSpanHandler类提供了管理browser生命周期的必要的回调。下面是从相关函数和成员中抽取出的代码。

class MyClient : public CefClient,
                 public CefLifeSpanHandler,
                 ... {
  // CefClient methods.
  virtual CefRefPtr<CefLifeSpanHandler> GetLifeSpanHandler() OVERRIDE {
    return this;
  }

  // CefLifeSpanHandler methods.
  void OnAfterCreated(CefRefPtr<CefBrowser> browser) OVERRIDE;
  bool DoClose(CefRefPtr<CefBrowser> browser) OVERRIDE;
  void OnBeforeClose(CefRefPtr<CefBrowser> browser) OVERRIDE;

  // Member accessors.
  CefRefPtr<CefBrowser> GetBrower() { return m_Browser; }
  bool IsClosing() { return m_bIsClosing; }

 private:
  CefRefPtr<CefBrowser> m_Browser;
  int m_BrowserId;
  int m_BrowserCount;
  bool m_bIsClosing;

  IMPLEMENT_REFCOUNTING(MyClient);
};

OnAfterCreated()函数在browser对象创建完成后会被立刻调用。主机应用可以使用这个方法来保证一个正确的browser对象的引用。

void MyClient::OnAfterCreated(CefRefPtr<CefBrowser> browser) {
  // Must be executed on the UI thread.
  REQUIRE_UI_THREAD();

  if (!m_Browser.get())   {
    // Keep a reference to the main browser.
    m_Browser = browser;
    m_BrowserId = browser->GetIdentifier();
  }

  // Keep track of how many browsers currently exist.
  m_BrowserCount++;
}

销毁browser的话可以调用CefBrowserHost::CloseBrowser()

// Notify the browser window that we would like to close it. This will result in a call to 
// MyHandler::DoClose() if the JavaScript 'onbeforeunload' event handler allows it.
browser->GetHost()->CloseBrowser(false);

如果browser的父亲是另一个窗口,那么关闭事件可能是来源于该父窗口的OS功能(例如,单击父窗口的关闭按钮)。然后,父窗口需要调用CloseBrowser(false)并等待第二个OS关闭事件,以告知浏览器已允许关闭。如果通过JavaScriptonbeforeunload事件回调处理或者DoClose()回调取消了关闭操作,则不会发送第二个操作系统关闭事件。请注意以下示例中的IsClosing()检查,对于第一个操作系统关闭事件,它将返回false,而对第二个操作系统关闭事件,则返回true(在调用DoClose之后)。

在Windows,父窗口来处理:

case WM_CLOSE:
  if (g_handler.get() && !g_handler->IsClosing()) {
    CefRefPtr<CefBrowser> browser = g_handler->GetBrowser();
    if (browser.get()) {
      // Notify the browser window that we would like to close it. This will result in a call to 
      // MyHandler::DoClose() if the JavaScript 'onbeforeunload' event handler allows it.
      browser->GetHost()->CloseBrowser(false);

      // Cancel the close.
      return 0;
    }
  }

  // Allow the close.
  break;

case WM_DESTROY:
  // Quitting CEF is handled in MyHandler::OnBeforeClose().
  return 0;
}

在Linux上,通过"delete_event"处理:

gboolean delete_event(GtkWidget* widget, GdkEvent* event,
                      GtkWindow* window) {
  if (g_handler.get() && !g_handler->IsClosing()) {
    CefRefPtr<CefBrowser> browser = g_handler->GetBrowser();
    if (browser.get()) {
      // Notify the browser window that we would like to close it. This will result in a call to 
      // MyHandler::DoClose() if the JavaScript 'onbeforeunload' event handler allows it.
      browser->GetHost()->CloseBrowser(false);

      // Cancel the close.
      return TRUE;
    }
  }

  // Allow the close.
  return FALSE;
}

在OS X上关闭是更加复杂的,看下cefsimple/cefsimple_mac.mm文件里面的注释来全面地了解一下。

DoClose()方法设置m_blsClose标志并返回false发送第二个OS关闭事件。

bool MyClient::DoClose(CefRefPtr<CefBrowser> browser) {
  // Must be executed on the UI thread.
  REQUIRE_UI_THREAD();

  // Closing the main window requires special handling. See the DoClose()
  // documentation in the CEF header for a detailed description of this
  // process.
  if (m_BrowserId == browser->GetIdentifier()) {
    // Set a flag to indicate that the window close should be allowed.
    m_bIsClosing = true;
  }

  // Allow the close. For windowed browsers this will result in the OS close
  // event being sent.
  return false;
}

当OS函数收到第二个OS关闭事件时,就允许父窗口关闭了。这之后将调用OnBeforeClose()。确保在OnBeforeClose()函数中释放关于browser对象的任何引用。

void MyHandler::OnBeforeClose(CefRefPtr<CefBrowser> browser) {
  // Must be executed on the UI thread.
  REQUIRE_UI_THREAD();

  if (m_BrowserId == browser->GetIdentifier()) {
    // Free the browser pointer so that the browser can be destroyed.
    m_Browser = NULL;
  }

  if (--m_BrowserCount == 0) {
    // All browser windows have closed. Quit the application message loop.
    CefQuitMessageLoop();
  }
}

详细的例子实现可以看cefclient。

离屏渲染

使用离屏渲染时,CEF不会创建本机浏览器窗口。相反,CEF向主机应用程序提供无效区域和像素缓冲区,并且主机应用程序将鼠标,键盘和焦点事件通知给CEF。离屏渲染目前不支持加速合成,因此与窗口浏览器相比,性能可能会受到影响。离屏浏览器将收到与窗口浏览器相同的通知,包括上一节中所述的生命周期回调。要使用屏幕外渲染你需要:

  1. 实现CefRenderHandler接口。所有的方法都应该被实现,除非特殊指定。
  2. 在传递CefWindowInfo结构体给CefBrowserHost::CreateBrowser()之前调用CefWindowInfo::SetAsWindowLess()。如果没有父窗口传递给CefWindowInfo::SetAsWindowLess()一些功能可能无法使用,例如右键菜单等。
  3. CefRenderHandler::GetViewRect()方法用于获取期望的view矩形区域。
  4. CefRenderHandler::OnPaint()方法用于提供一个无效区域并更新像素buffer。cefclient使用OpenGL绘制buffer,但你的应用可以使用任何你想用的技术来绘制。
  5. 通过CefBrowserHost::WasResized()来调整browser的大小。内部将会调用GetViewRect()来获取新的size然后并调用OnPaint()
  6. 调用CefBrowserHost::SendXXX() 来发送一个鼠标、键盘或者聚焦事件给browser
  7. 调用CefBrowserHost::CloseBrowser()来销毁browser

运行cefclient的时候加上--off-screen-rendering-enabled命令行参数来看看例子。

Posting Tasks

tasks能在单个进程的多个不同线程之间用CefPostTask系列函数传递(详情见include/cef_task.h头文件)。task将会在目标线程的消息循环中异步地执行。

CEF提供base::Bindbase::Callback模板回调类型来传递和绑定方法,对象和参数来传递给CefPostTask。完整版使用方法可以看include/base/cef_callback.h头文件的注释。include/wrapper/cef_closure_task.h头文件提供了将base::Closure转换成CefTask的一些辅助函数,见下面的例子

#include “include/base/cef_bind.h”
#include “include/wrapper/cef_closure_task.h”

// To execute a bound function:

// Define a function.
void MyFunc(int arg) { /* do something with |arg| on the UI thread */ }

// Post a task that will execute MyFunc on the UI thread and pass an |arg|
// value of 5.
CefPostTask(TID_UI, base::Bind(&MyFunc, 5));

// To execute a bound method:

// Define a class.
class MyClass : public CefBase {
 public:
  MyClass() {}
  void MyMethod(int arg) { /* do something with |arg| on the UI thread */ }
 private:
  IMPLEMENT_REFCOUNTING(MyClass);
};

// Create an instance of MyClass.
CefRefPtr<MyClass> instance = new MyClass();

// Post a task that will execute MyClass::MyMethod on the UI thread and pass
// an |arg| value of 5. |instance| will be kept alive until after the task
// completes.
CefPostTask(TID_UI, base::Bind(&MyClass::MyMethod, instance, 5));

如果本机应用需要持有一个任务循环的引用,可以使用CefTaskRunner类。例如,获取UI现成的任务循环

CefRefPtr<CefTaskRunner> task_runner = CefTaskRunner::GetForThread(TID_UI);

Inter-Process Communication (IPC)

自从CEF可以运行在多进程模式下,提供进程间的交流机制就变得十分必要。CefBrowserCefFrame对象存在于browser进程和renderer进程中,这有助于简化该过程。每个CefBrowserCefFrame对象还具有与之关联的唯一ID值,这个ID在互相通信的进程之间相匹配。

进程启动消息

为了在启动时为所有renderer进程提供相同的信息,请在浏览器进程中实现CefBrowserProcessHandler::OnRenderProcessThreadCreated()函数。在这个函数中会将信息传递给渲染进程的CefRenderProcessHandler :: OnRenderThreadCreated()函数。

处理运行时消息

可以用CefProcessMessage类在运行时的进程间传递消息。这些消息与特定的CefBrowserCefFrame相关联,并使用CefFrame::SendProcessMessage()方法发送。处理消息应包含通过CefProcessMessage :: GetArgumentList()所需的任何状态信息

// Create the message object.
CefRefPtr<CefProcessMessage> msg= CefProcessMessage::Create(“my_message”);

// Retrieve the argument list object.
CefRefPtr<CefListValue> args = msg>GetArgumentList();

// Populate the argument values.
args->SetString(0, “my string”);
args->SetInt(0, 10);

// Send the process message to the main frame in the render process.
// Use PID_BROWSER instead when sending a message to the browser process.
browser->GetMainFrame()->SendProcessMessage(PID_RENDERER, msg);

CefRenderProcessHandler::OnProcessMessageReceived()中使用从browser进程发往renderer进程的消息。从renderer进程发往browser进程的消息则可以在CefClient::OnProcessMessageReceived()中使用。

bool MyHandler::OnProcessMessageReceived(
    CefRefPtr<CefBrowser> browser,
    CefRefPtr<CefFrame> frame,
    CefProcessId source_process,
    CefRefPtr<CefProcessMessage> message) {
  // Check the message name.
  const std::string& message_name = message->GetName();
  if (message_name == “my_message”) {
    // Handle the message here...
    return true;
  }
  return false;
}

异步JavaScript绑定

JavaScriptIntegration在renderer进程中实现,但是需要频繁的与browser进程交流。JavaScript API通过closures和promises设计成了异步的。

通用消息路由

CEF提供了一种通用实现,用于在renderer进程中运行的JavaScript和browser进程中运行的C++之间路由异步消息。应用程序通过从标准CEF C++回调(OnBeforeBrowseOnProcessMessageRecievedOnContextCreated等)传递数据来与路由器进行交互。渲染器侧路由器支持通用的JavaScript回调注册和执行,而浏览器侧路由器则通过一个或多个应用程序提供的Handler实例支持特定于应用程序的逻辑。

自定义实现

一个基于CEF的应用程序也可以提供属于它自己实现的异步JavaScript绑定。一个最简单的实现如下所示

  1. 在renderer进程的JavaScript绑定传递一个回调函数
// In JavaScript register the callback function.
app.setMessageCallback('binding_test', function(name, args) {
  document.getElementById('result').value = "Response: "+args[0];
});
  1. renderer进程保存一个回调函数的引用
// Map of message callbacks.
typedef std::map<std::pair<std::string, int>,
                 std::pair<CefRefPtr<CefV8Context>, CefRefPtr<CefV8Value> > >
                 CallbackMap;
CallbackMap callback_map_;

// In the CefV8Handler::Execute implementation for “setMessageCallback”.
if (arguments.size() == 2 && arguments[0]->IsString() &&
    arguments[1]->IsFunction()) {
  std::string message_name = arguments[0]->GetStringValue();
  CefRefPtr<CefV8Context> context = CefV8Context::GetCurrentContext();
  int browser_id = context->GetBrowser()->GetIdentifier();
  callback_map_.insert(
      std::make_pair(std::make_pair(message_name, browser_id),
                     std::make_pair(context, arguments[1])));
}
  1. renderer过程将异步IPC消息发送到浏览器过程,请求执行工作。
  2. browser进程收到IPC消息并执行工作。
  3. 完成工作后,browser进程会将结果通过异步IPC发回给renderer进程
  4. renderer进程收到IPC消息并执行result中的回调函数。
// Execute the registered JavaScript callback if any.
if (!callback_map_.empty()) {
  const CefString& message_name = message->GetName();
  CallbackMap::const_iterator it = callback_map_.find(
      std::make_pair(message_name.ToString(),
                     browser->GetIdentifier()));
  if (it != callback_map_.end()) {
    // Keep a local reference to the objects. The callback may remove itself
    // from the callback map.
    CefRefPtr<CefV8Context> context = it->second.first;
    CefRefPtr<CefV8Value> callback = it->second.second;

    // Enter the context.
    context->Enter();

    CefV8ValueList arguments;

    // First argument is the message name.
    arguments.push_back(CefV8Value::CreateString(message_name));

    // Second argument is the list of message arguments.
    CefRefPtr<CefListValue> list = message->GetArgumentList();
    CefRefPtr<CefV8Value> args = CefV8Value::CreateArray(list->GetSize());
    SetList(list, args);  // Helper function to convert CefListValue to CefV8Value.
    arguments.push_back(args);

    // Execute the callback.
    CefRefPtr<CefV8Value> retval = callback->ExecuteFunction(NULL, arguments);
    if (retval.get()) {
      if (retval->IsBool())
        handled = retval->GetBoolValue();
    }

    // Exit the context.
    context->Exit();
  }
}
  1. CefRenderProcessHandler :: OnContextReleased()中释放与上下文有关联的所有v8引用。
void MyHandler::OnContextReleased(CefRefPtr<CefBrowser> browser,
                                  CefRefPtr<CefFrame> frame,
                                  CefRefPtr<CefV8Context> context) {
  // Remove any JavaScript callbacks registered for the context that has been released.
  if (!callback_map_.empty()) {
    CallbackMap::iterator it = callback_map_.begin();
    for (; it != callback_map_.end();) {
      if (it->second.first->IsSame(context))
        callback_map_.erase(it++);
      else
        ++it;
    }
  }
}

同步请求

在一些少见的场景下有必要实现browser进程和renderer进程之间的同步通信。应该尽可能避免这种情况,因为这样会对render进程产生性能上的影响。但是,如果你必须要同步,那么考虑一下使用同步XMLHttpRequests,它将会阻塞render进程并等待browser进程网络层处理。browser进程可以处理使用自定义协议或网络拦截的请求。

网络层

默认情况下,将以对主机应用程序透明的方式处理CEF3中的网络请求。对于希望与网络层建立更紧密关系的应用程序,CEF3公开了一系列与网络相关的功能。与网络相关的回调可能发生在不同的线程上,因此请确保注意文档并适当保护您的数据成员。

自定义请求

最简单的加载网页的方式就是通过CefFrame::LoadURL()

browser->GetMainFrame()->LoadURL(some_url);

如果应用程序想要发送一些复杂的请求,例如包括一些自定义的请求头或者上传一些数据,可以使用CefFrame::LoadRequest()方法。这个方法只有CefRequest对象一个参数。

// Create a CefRequest object.
CefRefPtr<CefRequest> request = CefRequest::Create();

// Set the request URL.
request->SetURL(some_url);

// Set the request method. Supported methods include GET, POST, HEAD, DELETE and PUT.
request->SetMethod(“POST”);

// Optionally specify custom headers.
CefRequest::HeaderMap headerMap;
headerMap.insert(
    std::make_pair("X-My-Header", "My Header Value"));
request->SetHeaderMap(headerMap);

// Optionally specify upload content.
// The default “Content-Type” header value is "application/x-www-form-urlencoded".
// Set “Content-Type” via the HeaderMap if a different value is desired.
const std::string& upload_data = “arg1=val1&arg2=val2”;
CefRefPtr<CefPostData> postData = CefPostData::Create();
CefRefPtr<CefPostDataElement> element = CefPostDataElement::Create();
element->SetToBytes(upload_data.size(), upload_data.c_str());
postData->AddElement(element);
request->SetPostData(postData);

浏览器独立请求

应用程序可以通过CefURLRequest类发送与特定浏览器不相关的网络请求。实现CefURLRequestClient接口以处理结果响应。CefURLRequest可以在browser和render过程中使用。

class MyRequestClient : public CefURLRequestClient {
 public:
  MyRequestClient()
    : upload_total_(0),
      download_total_(0) {}

  void OnRequestComplete(CefRefPtr<CefURLRequest> request) OVERRIDE {
    CefURLRequest::Status status = request->GetRequestStatus();
    CefURLRequest::ErrorCode error_code = request->GetRequestError();
    CefRefPtr<CefResponse> response = request->GetResponse();

    // Do something with the response...
  }

  void OnUploadProgress(CefRefPtr<CefURLRequest> request,
                        int64 current,
                        int64 total) OVERRIDE {
    upload_total_ = total;
  }

  void OnDownloadProgress(CefRefPtr<CefURLRequest> request,
                          int64 current,
                          int64 total) OVERRIDE {
    download_total_ = total;
  }

  void OnDownloadData(CefRefPtr<CefURLRequest> request,
                      const void* data,
                      size_t data_length) OVERRIDE {
    download_data_ += std::string(static_cast<const char*>(data), data_length);
  }

  bool GetAuthCredentials(bool isProxy,
                          const CefString& host,
                          int port,
                          const CefString& realm,
                          const CefString& scheme,
                          CefRefPtr<CefAuthCallback> callback) OVERRIDE {
    return false;  // Not handled.
  }

 private:
  int64 upload_total_;
  int64 download_total_;
  std::string download_data_;

 private:
  IMPLEMENT_REFCOUNTING(MyRequestClient);
};

发送请求:

// Set up the CefRequest object.
CefRefPtr<CefRequest> request = CefRequest::Create();
// Populate |request| as shown above...

// Create the client instance.
CefRefPtr<MyRequestClient> client = new MyRequestClient();

// Start the request. MyRequestClient callbacks will be executed asynchronously.
CefRefPtr<CefURLRequest> url_request = CefURLRequest::Create(request, client.get(), nullptr);
// To cancel the request: url_request->Cancel();

CefURLRequest发出的请求还可以通过CefRequest :: SetFlags()方法指定自定义行为。支持的标志位包括:

  • UR_FLAG_SKIP_CACHE如果设置,则在处理请求时将跳过缓存。
  • UR_FLAG_ALLOW_CACHED_CREDENTIALS如果设置了cookie,则可能与请求一起发送并从响应中保存。还必须设置UR_FLAG_ALLOW_CACHED_CREDENTIALS。
  • UR_FLAG_REPORT_UPLOAD_PROGRESS如果设置了请求,则在有正文的情况下将生成上传进度事件。
  • UR_FLAG_NO_DOWNLOAD_DATA如果设置,则不会调用CefURLRequestClient :: OnDownloadData方法。
  • UR_FLAG_NO_RETRY_ON_5XX如果设置为5XX,重定向错误将传播到观察者,而不是自动重试。当前,这仅适用于源自浏览器进程的请求。

例如,跳过缓存而不报告下载数据:

request-> SetFlags(UR_FLAG_SKIP_CACHE | UR_FLAG_NO_DOWNLOAD_DATA);

请求处理

CEF3支持两种方法来处理应用程序内部的网络请求。方案处理程序方法允许为针对特定来源(方案+域)的请求注册处理程序。请求拦截方法允许根据应用程序的判断处理任意请求。

使用HTTP方案而不是自定义方案可以避免一系列潜在问题。

如果选用自定义方案(“ HTTP”,“ HTTPS”等以外的其他方案),则必须在CEF中进行注册,以使其发挥预期的作用。如果您希望自定义方案的行为类似于HTTP(支持POST请求并强制执行HTTP访问控制(CORS)限制),则应将其注册为“标准”方案。如果您打算对其他方案执行跨域请求或通过XMLHttpRequest将POST请求发送到方案处理程序,则应使用HTTP方案而不是自定义方案,以避免潜在的问题。如果希望使用自定义方案,则必须通过CefApp :: OnRegisterCustomSchemes()回调来注册属性,该回调必须在所有进程中实现。

void MyApp::OnRegisterCustomSchemes(CefRefPtr<CefSchemeRegistrar> registrar) {
  // Register "client" as a standard scheme.
  registrar->AddCustomScheme("client", true, ...);
}

通用资源管理器

CEF提供了一种通用的实现,用于管理来自一个或多个数据源的资源请求。该用户注册用于不同数据源的处理程序,例如磁盘上的目录,zip存档或自定义实现,并且管理器处理请求。应用程序通过从标准CEF C++回调(OnBeforeResourceLoadGetResourceHandler)传递数据来与路由器进行交互。

Scheme 处理

一个scheme handler是通过CefRegisterSchemeHandlerFactory()函数注册的,调用该函数的一个比较好的地方是在CefBrowserProcessHandler::OnContextInitialized()函数。例如,你可以注册一个client://myapp/请求的回调处理:

CefRegisterSchemeHandlerFactory("client", “myapp”, new MySchemeHandlerFactory());

处理程序可与内置方案(HTTP,HTTPS等)和自定义方案一起使用。使用内置方案时,请为您的应用程序选择一个唯一的域名(例如“ myapp”或“ internal”)。实现CefSchemeHandlerFactoryCefResourceHandler类来处理请求并提供响应数据。如果使用自定义方案,请不要忘记实现上述CefApp :: OnRegisterCustomSchemes方法。

如果在请求时知道响应数据,则CefStreamResourceHandler类提供CefResourceHandler的便捷默认实现。

// CefStreamResourceHandler is part of the libcef_dll_wrapper project.
#include “include/wrapper/cef_stream_resource_handler.h”

const std::string& html_content = “<html><body>Hello!</body></html>”;

// Create a stream reader for |html_content|.
CefRefPtr<CefStreamReader> stream =
    CefStreamReader::CreateForData(
        static_cast<void*>(const_cast<char*>(html_content.c_str())),
        html_content.size());

// Constructor for HTTP status code 200 and no custom response headers.
// There’s also a version of the constructor for custom status code and response headers.
return new CefStreamResourceHandler("text/html", stream);

请求拦截

CefRequestHandler::GetResourceHandler()方法支持随心所欲的拦截请求。它使用与方案处理程序方法相同的CefResourceHandler类。如果使用自定义方案,请不要忘记实现上述CefApp::OnRegisterCustomSchemes方法。

CefRefPtr<CefResourceHandler> MyHandler::GetResourceHandler(
      CefRefPtr<CefBrowser> browser,
      CefRefPtr<CefFrame> frame,
      CefRefPtr<CefRequest> request) {
  // Evaluate |request| to determine proper handling...
  if (...)
    return new MyResourceHandler();

  // Return NULL for default handling of the request.
  return NULL;
}

响应过滤

CefRequestHandler::GetResourceHandler()方法支持过滤请求回复数据

其他回调

CefRequestHandler接口为多样的网络相关事件提供回调(包括认证,cookie处理,多余协议处理,认证错误等等)

代理解析

使用与Google Chrome相同的命令行标志在CEF3中配置代理设置。

--proxy-server=host:port
      Specify the HTTP/SOCKS4/SOCKS5 proxy server to use for requests. An individual proxy
      server is specified using the format:

        [<proxy-scheme>://]<proxy-host>[:<proxy-port>]

      Where <proxy-scheme> is the protocol of the proxy server, and is one of:

        "http", "socks", "socks4", "socks5".

      If the <proxy-scheme> is omitted, it defaults to "http". Also note that "socks" is equivalent to
      "socks5".

      Examples:

        --proxy-server="foopy:99"
            Use the HTTP proxy "foopy:99" to load all URLs.

        --proxy-server="socks://foobar:1080"
            Use the SOCKS v5 proxy "foobar:1080" to load all URLs.

        --proxy-server="sock4://foobar:1080"
            Use the SOCKS v4 proxy "foobar:1080" to load all URLs.

        --proxy-server="socks5://foobar:66"
            Use the SOCKS v5 proxy "foobar:66" to load all URLs.

      It is also possible to specify a separate proxy server for different URL types, by prefixing
      the proxy server specifier with a URL specifier:

      Example:

        --proxy-server="https=proxy1:80;http=socks4://baz:1080"
            Load https://* URLs using the HTTP proxy "proxy1:80". And load http://*
            URLs using the SOCKS v4 proxy "baz:1080".

--no-proxy-server
      Disables the proxy server.

--proxy-auto-detect
      Autodetect  proxy  configuration.

--proxy-pac-url=URL
      Specify proxy autoconfiguration URL.

如果代理需要认证, CefRequestHandler::GetAuthCredentials()回调执行的时候将会有一个值为true的|isProxy|参数并检索用户名和密码

bool MyHandler::GetAuthCredentials(
    CefRefPtr<CefBrowser> browser,
    CefRefPtr<CefFrame> frame,
    bool isProxy,
    const CefString& host,
    int port,
    const CefString& realm,
    const CefString& scheme,
    CefRefPtr<CefAuthCallback> callback) {
  if (isProxy) {
    // Provide credentials for the proxy server connection.
    callback->Continue("myuser", "mypass");
    return true;
  }
  return false;
}

由于网络代理解析(例如,如果在Windows上选中了“自动检测代理设置”,则可能会延迟应用程序启动期间Web内容的加载)。为了获得最佳的用户体验,请考虑将您的应用程序设计为首先显示静态初始页面,然后使用元刷新将其重定向到实际网站-重定向将被阻止,直到代理解析完成为止。出于测试目的,可以使用“ --no-proxy-server”命令行标志禁用代理解析。通过在命令行中运行“ chrome --url = ...”,代理解析延迟也可以在Google Chrome中复制。

posted @ 2020-01-17 11:12  leno米雷  阅读(1650)  评论(0编辑  收藏  举报