Chromium 扩展事件系统深度解析:从 C++ 业务回调到 JS 监听器,一次说清楚

Electron与Flutter跨平台开发深度对比:架构、性能与选型指南 跨平台开发框架是构建多端应用的核心技术,其核心原理在于通过一套代码库实现在不同操作系统上的部署。从技术实现上,主要分为基于Web技术栈的“套壳”方案和自绘引擎的统一渲染方案。前者利用成熟的Web生态实现快速开发,后者则通过高性能图形引擎保障一致的UI体验与流畅性能。在工程实践中,框架选型直接影响应用的资源消耗、启动速度、内存占用及长期维护成本。对于需要复杂Web内容集成、深度系统调用或已有Web技术栈的团队,基于Chromium的方案能提供更低的开发门槛和丰富的生态支持;而对于追求高性能渲染、统一多端体验且 阅读详情

Chromium 扩展事件系统深度解析:从 C++ 业务回调到 JS 监听器,一次说清楚

这篇文章以一个非常具体、非常典型的案例为主线:浏览器内核侧某个业务服务在登录成功后触发 OnLoginSucceeded(),最终让扩展侧通过 chrome.account.onStatusChanged.addListener(...) 之类的方式收到事件通知。
文章重点不是停留在“某个函数调用了另一个函数”,而是把 事件源注册、扩展监听器注册、EventRouter 注册表维护、懒监听器处理、Profile 初始化时机、Browser 进程与 Renderer 进程的事件桥接 这几条线完整串起来。


一、为什么要写这篇文章

在 Chromium 扩展体系里,很多人一开始接触事件分发时,会把几个概念混在一起:

  1. 浏览器业务服务的 Observer 注册:比如同步服务、UI 服务、下载服务、阅读模式服务等把某个对象加入自己的观察者列表。
  2. 扩展 JS 侧的 addListener 注册:扩展在 JS 里调用某个 API 的 addListener(),表示自己愿意接收该事件。
  3. EventRouter 的监听器注册表:浏览器进程里真正负责维护“哪个扩展正在监听哪个事件”的核心对象。
  4. 事件的最终分发:浏览器进程把事件通过 Mojo/IPC 发给扩展所在的 Renderer 或 Service Worker。

很多排查问题时,表面上像是在问“为什么事件没收到”,本质上可能是以下任意一环出问题:

  • 业务事件源根本没有注册成功;
  • API schema 定义了事件,但扩展压根没把监听器加上;
  • 扩展加了监听器,但 EventRouter 里没有记录;
  • EventRouter::DispatchEventToExtension() 发的是定向事件,但目标扩展 ID 不对;
  • 扩展是懒加载后台页,事件发出时上下文并未唤起;
  • Profile 初始化顺序不对,事件源先发事件,而扩展系统还没 ready。

所以这篇文章的目标很明确:用一个真实案例,把这整个机制讲透。


二、本文的案例主线:OnLoginSucceeded()

给出的案例,本质上是下面这条链:

  1. 浏览器 Profile 初始化;
  2. 扩展系统初始化;
  3. 某个 browser-side 的 event router 对象被创建,例如 ExtensionAccountEventRouter;
  4. 它在 Init() 里向多个业务服务注册自己为 observer;
  5. 当登录服务通知“登录成功”时,observer 回调 OnLoginSucceeded();
  6. 在这个回调里构造参数,然后调用 DispatchEvent(...);
  7. DispatchEvent(...) 再进入 EventRouter;
  8. EventRouter 找到所有监听该事件的扩展监听器;
  9. 通过 Mojo/IPC 发送给扩展 Renderer / 扩展 Service Worker;
  10. 扩展 JS 中的 addListener(callback) 最终被调用。

看上去只是“一个回调转发事件”,但实际上中间至少跨了:

  • 业务服务层
  • 浏览器扩展桥接层
  • 扩展事件路由层
  • Renderer 绑定层
  • 扩展 JS API 层

这也是为什么这个案例特别适合拿来讲透 Chromium 扩展事件系统。


三、先区分两套完全不同的“注册”

理解这个系统的第一步,是把“注册”拆成两种完全不同的事情。

3.1 第一种注册:业务服务侧的 Observer 注册

这类注册发生在 browser 进程的 C++ 里。典型形式如下:

void ExtensionAccountEventRouter::Init() {
  if (profile_) {
    SyncService* sync_service = g_browser_process->sync_service();
    if (sync_service) {
      sync_service->GetSyncLogin()->AddObserver(this);
    }

    ExtensionUIService* extension_ui_service =
        g_browser_process->extension_ui_service();
    if (extension_ui_service) {
      extension_ui_service->AddObserver(this);
    }

    DownloadFileService* download_file_service =
        g_browser_process->download_file_service();
    if (download_file_service) {
      download_file_service->AddObserver(this);
    }
  }
}

这里的含义是:

  • ExtensionAccountEventRouter 这个对象本身并不是事件真正的源头;
  • 它只是把自己注册到各个业务服务;
  • 一旦业务服务内部状态变化,就会反向回调它。

换句话说,这里注册的是:
“谁来接收浏览器业务层产生的内部通知”。

它注册到的是各个 service 自己维护的 ObserverList。


3.2 第二种注册:扩展侧的 Listener 注册

另一种注册发生在扩展 JS 里,例如:

chrome.channel.onBuiltinPageMessage.addListener(function(data) {
  console.log(data);
});

或者在本文案例里,更抽象地写成:

chrome.account.onStatusChanged.addListener(function(isLoggedIn) {
  console.log('login state changed', isLoggedIn);
});

这里的含义是:

  • 扩展向浏览器声明:“我对这个事件感兴趣”;
  • 浏览器必须把这个监听关系记下来;
  • 将来只要同名事件发生,就要把它发给这个扩展。

这类注册最终会进入 EventRouter 内部,存放在 EventListenerMap 中。

它注册的是:
“哪个扩展在监听哪个扩展事件”。


3.3 两种注册的本质区别

可以直接对比:

维度业务侧注册扩展侧注册
注册者browser 进程里的 C++ 对象扩展 JS / 扩展上下文
注册目标各业务 Service 的 ObserverListEventRouter 的监听器表
解决的问题谁来接收浏览器内部状态变化哪些扩展要收到某个 API 事件
数据结构各服务内部的 observer listEventListenerMap
方向Service → EventRouter bridgeBrowser → Extension

很多时候看到一个 Init()->AddObserver(this),那是在建立 事件源到桥接器 的路径;
而看到 addListener(),那是在建立 桥接器到扩展端 的路径。

两者合在一起,完整链路才成立。


四、从系统启动讲起:这个 bridge 对象什么时候创建

如果要真正看透 OnLoginSucceeded() 为什么能走到扩展侧,不能只看单个 event router 类,还要看它是什么时候被构造、什么时候 Init() 的。

给出的调用链非常关键,核心是:

  1. ProfileManager::DoFinalInitForServices()
  2. ExtensionSystem::InitForRegularProfile()
  3. ExtensionSystemImpl::Shared::Init()
  4. ExtensionService::Init()
  5. 构造并初始化 ExtensionAccountEventRouter

我们按层展开。


4.1 Profile 级别入口:ProfileManager::DoFinalInitForServices()

这是 Profile 生命周期中一个很关键的阶段。逻辑核心是:

void ProfileManager::DoFinalInitForServices(Profile* profile,
                                            bool go_off_the_record) {
#if BUILDFLAG(ENABLE_EXTENSIONS_CORE)
  bool extensions_enabled = !go_off_the_record;
  extensions::ExtensionSystem::Get(profile)->InitForRegularProfile(
      extensions_enabled);
#endif
}

这说明:

  • 扩展系统不是在浏览器进程一起来就无条件初始化;
  • 它是挂在某个 Profile 上的;
  • 只有 Profile 走到最终服务初始化阶段,扩展系统才真正开始启动。

这一步很重要,因为后面所有事件路由都依赖 Profile:

  • EventRouter::Get(browser_context) 是按 BrowserContext/Profile 取实例;
  • 扩展启用状态、权限、incognito 可见性,都是 Profile 维度;
  • 浏览器内部服务也经常是 Profile 隔离的。

所以从架构上讲,扩展事件系统本来就是 Profile scoped 的。


4.2 扩展系统入口:InitForRegularProfile()

贴出的逻辑:

void ExtensionSystemImpl::InitForRegularProfile(bool extensions_enabled) {
  if (user_script_manager() || extension_service()) {
    return;
  }

  shared_->Init(extensions_enabled);
}

这里做了两件事:

  1. 防止重复初始化;
  2. 把真正的初始化逻辑委托给 shared_->Init(...)。

这意味着很多后面看到的对象,比如:

  • ExtensionService
  • ServiceWorkerManager
  • UserScriptManager
  • QuotaService
  • 以及各种 extension-side 基础设施

都是在这个阶段准备起来的。


4.3 扩展系统核心构建:ExtensionSystemImpl::Shared::Init()

贴出的这一段最关键的地方是:

extension_service_ = std::make_unique<ExtensionService>(...);
...
extension_service_->Init();

也就是说:

  • 扩展系统内部大量对象先构造;
  • 然后由 ExtensionService::Init() 进入真正的扩展服务初始化;
  • 而我们的业务 bridge event router,正是在这个阶段被创建和初始化的。

这一步在事件系统里有一个很深的含义:
只有扩展系统 ready 之后,业务事件桥接器才应该开始注册并对外发扩展事件。

否则会出现两个典型问题:

  1. 扩展监听关系还没建立,事件已经发了;
  2. EventRouter 还没就绪,事件无法路由。

4.4 真正挂上业务 bridge:ExtensionService::Init()

贴出的关键逻辑类似:

abp_event_router_.reset(new extensions::ExtensionAbpEventRouter(profile_));
abp_event_router_->Init();

account_event_router_.reset(
    new extensions::ExtensionAccountEventRouter(profile_));
account_event_router_->Init();

这里其实就是在做一件事:

  • 给每一种业务域,创建一个专属的 ExtensionXXXEventRouter;
  • 这些对象统一承担“把业务层事件翻译成扩展 API 事件”的职责。

这类对象一般都不是扩展系统最底层基础设施,而是 业务扩展桥接层。

可以把它们理解为:

  • 上游连着浏览器业务服务;
  • 下游连着扩展事件系统;
  • 中间做事件名映射、参数打包、权限/上下文约束。

五、案例核心:OnLoginSucceeded() 到底做了什么

现在进入本文主角。

典型逻辑如下:

void ExtensionAccountEventRouter::OnLoginSucceeded(
    const sync::Account& account) {
  base::Value::List args;
  args.Append(base::Value(true));
  DispatchEvent(profile_, kOnAccountStatusChanged, std::move(args));
}

这段代码看似简单,但它完成了“浏览器业务语义”向“扩展事件语义”的一次翻译。

它做了三件关键事情:

5.1 从业务回调切入

OnLoginSucceeded(...) 不是自己主动执行的,而是由上游服务回调触发。
这说明它的角色不是事件源,而是 事件桥接者。

5.2 把业务状态编码成扩展事件参数

base::Value::List args;
args.Append(base::Value(true));

这一步是 参数协议转换。

上游回调签名可能是:

OnLoginSucceeded(const sync::Account& account)

而扩展事件要求的参数协议可能是:

  • 只告诉登录状态;
  • 或者告诉账号信息、token 状态、来源类型;
  • 或者封成 dict/object。

所以这里并不是机械转发,而是把上游业务数据转换为扩展事件 schema 所要求的参数格式。

5.3 把事件送入扩展事件总线

DispatchEvent(profile_, kOnAccountStatusChanged, std::move(args));

这一步的本质是:
从业务域切入扩展域。

从这里开始,后续链路不再是某个业务 service 自己的 observer 体系,而进入 Chromium 扩展系统统一的事件派发机制。


六、扩展事件系统的核心:EventRouter

如果只用一句话概括 EventRouter:

它是 browser 进程中扩展事件系统的中心路由器,负责接收事件、记录监听关系、筛选目标、唤醒懒上下文,并最终把事件发给正确的扩展上下文。

这里要重点看它的两个职责:

  1. 维护监听器注册表;
  2. 执行事件分发。

6.1 核心数据结构:EventListenerMap

前面总结过,这部分非常关键,而且要保留。
EventRouter 内部通过 listeners() 暴露其监听器注册表:

EventListenerMap& listeners() { return listeners_; }

而 EventListenerMap 的核心就是按事件名组织监听器:

std::map<std::string, ListenerList> listeners_;

这意味着:

  • key:事件名,例如 channel.onBuiltinPageMessage;
  • value:一个监听器列表;
  • 同一个事件可能同时被多个扩展、多个进程、多个上下文监听。

所以它不是 “event_name -> one listener”,而是:
event_name -> 多个 EventListener。


6.2 EventListener 里到底保存了什么

每个 EventListener 本质上就是一个“事件接收端描述对象”。

典型字段包括:

  • event_name_:监听的是哪个事件;
  • extension_id_:属于哪个扩展;
  • listener_url_:如果不是扩展而是 URL listener,则记录 URL;
  • process_:监听器所在的 RenderProcessHost;
  • browser_context_:属于哪个 BrowserContext/Profile;
  • is_for_service_worker_:是否是扩展 service worker;
  • service_worker_version_id_:对应 service worker 版本;
  • worker_thread_id_:worker thread id;
  • filter_:过滤器;
  • matcher_id_:如果设置了 filter,则绑定在 event filter 上的 matcher id。

这几个字段很关键,因为它们决定了后续分发时:

  • 发给谁;
  • 发到哪个进程;
  • 发给主线程页面还是 service worker;
  • 是否需要按 filter 精确匹配;
  • 当前是不是 lazy listener。

七、扩展监听器是怎么注册进 EventRouter 的

这一段是整个问题最容易“只知其然,不知其所以然”的地方。

7.1 扩展 JS 侧的起点:addListener()

例如:

chrome.channel.onBuiltinPageMessage.addListener(callback);

或者:

chrome.account.onStatusChanged.addListener(callback);

这个 addListener() 在 JS 看上去只是一个同步调用,但实际上它会通过扩展绑定层一路进入 browser 进程。

7.2 Renderer 到 Browser:Mojo 接口

EventRouter 实现了 mojom::EventRouter 接口,这意味着 renderer 可以通过 Mojo 调 browser 侧的方法。

关键接口包括:

也就是说,扩展 JS 的监听注册,最终会落到这些 browser-side 方法。


7.3 主线程监听注册:AddListenerForMainThread()

EventRouter::AddListenerForMainThread() 的逻辑已经总结过,核心是:

void EventRouter::AddListenerForMainThread(
    mojom::EventListenerPtr event_listener) {
  auto* process = GetRenderProcessHostForCurrentReceiver();
  if (!process)
    return;

  const mojom::EventListenerOwner& listener_owner =
      *event_listener->listener_owner;
  if (listener_owner.is_extension_id()) {
    AddEventListener(event_listener->event_name, process,
                     listener_owner.get_extension_id());
  } else if (listener_owner.is_listener_url()) {
    AddEventListenerForURL(event_listener->event_name, process,
                           listener_owner.get_listener_url());
  }
}

这个函数的作用可以翻译成一句人话:

当前 renderer 进程告诉 browser:“我这个扩展(或者这个 URL 页面)想监听某个事件,请记下来。”

这里的 process 很关键,它来自 GetRenderProcessHostForCurrentReceiver(),说明监听器注册是与当前发起 Mojo 调用的 renderer 进程上下文绑定的。


7.4 最终写入注册表:EventListenerMap::AddListener()

真正把监听器放进注册表的是:

bool EventListenerMap::AddListener(std::unique_ptr<EventListener> listener) {
  if (HasListener(listener.get()))
    return false;
  if (listener->filter()) {
    ...
  }
  EventListener* listener_ptr = listener.get();
  listeners_[listener->event_name()].push_back(std::move(listener));
  delegate_->OnListenerAdded(listener_ptr);
  return true;
}

这段代码有几个细节必须讲清楚:

7.4.1 先判重

if (HasListener(listener.get()))
  return false;

说明同一个监听器不会被重复插入。

7.4.2 如果带 filter,要额外进入 filter 子系统

if (listener->filter()) {
  ...
}

也就是说,监听器注册不仅仅是 list append。
如果事件支持 filter,它还会额外挂进 EventFilter,以后派发时按条件匹配。

7.4.3 核心入表动作

listeners_[listener->event_name()].push_back(std::move(listener));

这就是前面总结的那句核心结论:
按 event_name 分组存入 map。

7.4.4 通知 observer

delegate_->OnListenerAdded(listener_ptr);

这里的 delegate 是 EventRouter 本身。
所以 EventRouter 不只是“持有 map”,它还会在监听器变更时拿到通知,去更新其它状态或者通知外部观察者。


八、EventRouter 如何维护监听器注册表的附加状态

单纯知道有个 map<string, vector<listener>> 还不够,还要理解 EventRouter 围绕这张表做了哪些维护。

8.1 监听器增删回调

在 EventRouter 附近,有 OnListenerRemoved() 这类实现。它会把监听器变化封装成 EventListenerInfo,再通知注册到 observer_map_ 上的观察者。

也就是说:

  • EventListenerMap 负责存储;
  • EventRouter 负责感知变化;
  • 如果某些模块关心“某个事件现在有没有 listener”,它们可以通过 observer 机制得到通知。

8.2 Renderer 进程生命周期绑定

ObserveProcess()、RenderProcessExited()、RenderProcessHostDestroyed() 这几段很重要。

它们说明:

  • 监听器不是永久不变的;
  • 它们和 renderer 进程生命周期绑定;
  • 一旦 renderer 进程退出或销毁,该进程对应的监听器会被清理。

这能避免一个严重问题:
renderer 崩了,但 browser 还以为它在监听事件,导致事件发送到无效目标。

8.3 Lazy listener 的持久化注册状态

在 GetRegisteredEvents() 等逻辑里,可以看到 ExtensionPrefs 被用来读取 lazy/service worker listener 的已注册事件列表。

这意味着:

  • 对于非持久常驻的扩展背景上下文,浏览器需要知道“虽然当前没进程,但它理论上对哪些事件感兴趣”;
  • 所以 lazy listener 并不等于“完全不存在”;
  • 它会以另一种形式保留在系统里,用于后续唤醒。

九、DispatchEventToExtension() 到底做了什么

这是前面明确要求解释的接口,也是文章必须保留和展开的重点。

先给结论:

EventRouter::DispatchEventToExtension() 的作用,是把一个事件定向发给指定扩展,而不是广播给所有监听者。

它与 BroadcastEvent() 的差异非常大。


9.1 三种常见派发接口的定位差异

EventRouter 常用的几个派发接口:

可以把它们理解为:

接口目标集合使用场景
BroadcastEvent()所有 listener全局广播事件
DispatchEventToExtension()单个 extension id定向通知某个扩展
DispatchEventToURL()单个 URLWebUI / 特定页接收
DispatchEventWithLazyListener()单个扩展的 lazy context新装扩展、特殊上下文唤起

9.2 定向分发的核心思路

虽然前面给的伪逻辑是抽象版,但本质描述是对的:

void EventRouter::DispatchEventToExtension(const ExtensionId& extension_id,
                                           std::unique_ptr<Event> event) {
  // 1. 从 listeners_ 中查找该扩展注册的监听器
  // 2. 遍历匹配 event_name 的监听器
  // 3. 对每个监听器:
  //    - 如果是 lazy listener:唤醒扩展后台页/Service Worker
  //    - 如果是普通 listener:直接向 process 发送事件
  // 4. 最终进入 DispatchExtensionMessage(...)
}

这段逻辑的本质不是“拿到 ext_id 就直接发”。
它必须先做 监听匹配,因为一个扩展即便存在,也不代表它监听了这个事件。

所以“扩展存在”与“扩展监听事件”是两回事。


9.3 最终真正跨进程派发的是 DispatchExtensionMessage()

在 event_router.cc 中,真正把事件送往 renderer 的核心函数是 DispatchExtensionMessage():

void EventRouter::DispatchExtensionMessage(
    content::RenderProcessHost* rph,
    int worker_thread_id,
    content::BrowserContext* browser_context,
    const mojom::HostID& host_id,
    int event_id,
    const std::string& event_name,
    base::Value::List event_args,
    UserGestureState user_gesture,
    mojom::EventFilteringInfoPtr info,
    mojom::EventDispatcher::DispatchEventCallback callback)

这个函数做的事情非常核心:

  1. 为这次分发准备 DispatchEventParams;
  2. 填充 event_name、event_id、host_id、worker_thread_id 等;
  3. 调用 RouteDispatchEvent();
  4. 最终通过 mojom::EventDispatcher 把事件发到 renderer / worker。

也就是说:

这个分层非常清晰。


9.4 RouteDispatchEvent() 的意义

RouteDispatchEvent() 负责真正把消息送到目标 dispatcher:

dispatcher->DispatchEvent(std::move(params), std::move(event_args),
                          std::move(callback));

这里说明一件非常重要的事:
扩展事件从 browser 到 renderer 不是手写 IPC message,而是走了统一的 Mojo 接口 EventDispatcher。

所以从架构上看,事件传递是:

  • browser 侧:EventRouter
  • 传输层:Mojo associated interface
  • renderer 侧:EventDispatcher
  • JS 侧:bindings + callback 调用

十、BroadcastEvent() 与 DispatchEventToExtension() 的本质区别

很多人看到这两个接口时,只觉得一个“广播”,一个“定向”,但实际差异比这更深入。

10.1 BroadcastEvent()

适合这种场景:

  • 某类事件对所有监听该 API 的扩展都可见;
  • 比如浏览器状态变化、某个全局通知、某个系统事件。

流程是:

  1. 根据 event_name 找到所有监听器;
  2. 对每个监听器检查上下文和权限;
  3. 对满足条件的监听器逐个发出。

10.2 DispatchEventToExtension()

适合这种场景:

  • 事件天然只属于某个扩展;
  • 或者某业务逻辑明确知道目标扩展 ID;
  • 或者只想避免其它扩展收到同名事件。

流程是:

  1. 先确定目标 extension id;
  2. 再在该扩展已注册的 listener 中找同名事件;
  3. 最后只对该扩展做派发。

10.3 一个容易误判的问题

有些人会想:

我已经知道 extension_id 了,直接发不就行了,为什么还要查 listener?

答案是:

因为 Chromium 扩展事件系统是 listener-driven 的。
扩展是否收到事件,不只取决于它“是谁”,还取决于它“有没有注册监听该事件”。

否则就会破坏 WebExtension / Chrome Extension 事件模型的基本契约。


十一、懒监听器(Lazy Listener)为什么复杂

前面的总结里保留了 lazy listener,这部分必须展开,因为它正是 Chromium 扩展事件系统最有技术含量的点之一。

11.1 什么是 lazy listener

如果扩展后台页不是常驻,或者扩展基于 service worker,那么在某个时间点它可能没有活跃进程。但它依然通过 manifest 和历史注册关系表达了:

“如果某个事件来了,请把我唤醒并投递给我。”

此时该监听器在 browser 侧就会表现为:

即前面总结的:

当扩展后台页未运行时,监听器以 “lazy” 形式存在。

11.2 lazy listener 为什么必要

如果没有 lazy listener,扩展事件模型会有严重缺陷:

  • 浏览器为了让扩展能收到事件,必须永远常驻其后台页;
  • 内存和进程成本会非常高;
  • service worker 模型也无法成立。

所以 Chromium 的做法是:

  • 把监听关系存在 browser 侧;
  • 扩展上下文不活跃时,不代表监听关系消失;
  • 事件到达时,先唤起上下文,再投递事件。

11.3 lazy listener 的处理流程

前面已经总结出最核心的三步:

  1. 检测到 listener->IsLazy() 为 true;
  2. 通过 LazyEventDispatcher 或相关 lazy dispatch 工具唤醒扩展;
  3. 扩展启动并重新建立上下文后,事件被投递。

从设计上看,它其实是在解决一个经典问题:
如何把“事件到达”与“执行上下文当前不存在”解耦。

这是 Chromium 扩展事件机制比普通 observer/listener 模型更复杂的根本原因。


十二、事件源侧的完整初始化链路,再串一次

前面给出的“扩展事件系统完整初始化链路解析”必须保留,而且要讲得更系统。下面重新组织。

12.1 调用链总览

ProfileManager::DoFinalInitForServices()
    ↓
ExtensionSystem::Get(profile)->InitForRegularProfile(extensions_enabled)
    ↓
ExtensionSystemImpl::Shared::Init()
    ↓
extension_service_ = std::make_unique<ExtensionService>(...)
    ↓
ExtensionService::Init()
    ↓
account_event_router_.reset(new ExtensionAccountEventRouter(profile_))
account_event_router_->Init()
    ↓
sync_service->GetSyncLogin()->AddObserver(this)
extension_ui_service->AddObserver(this)
download_file_service->AddObserver(this)
    ↓(业务状态变化触发)
OnLoginSucceeded(const sync::Account& account)
    ↓
DispatchEvent(profile_, kOnAccountStatusChanged, args)
    ↓
EventRouter::DispatchEventToExtension(extension_id, event)
    ↓(经 Mojo IPC)
扩展 JS callback 执行

12.2 每一步的职责边界

层级代表对象职责
Profile 层ProfileManager决定初始化顺序和时机
扩展系统层ExtensionSystemImpl扩展基础设施启动
扩展服务层ExtensionService各业务 bridge event router 的创建
业务桥接层ExtensionAccountEventRouter业务 observer 注册 + 参数打包 + 分发入口
事件路由层EventRouter查找监听器、分发事件
Renderer 层扩展 JS 上下文接收事件、触发 JS callback

12.3 高频误区速览

  • 误区 A:只要 C++ 里 DispatchEvent 了,扩展一定能收到。
    实际:还取决于扩展是否注册监听器、是否有权限、是否在正确 profile、是否需要唤醒懒上下文。

  • 误区 B:只看 browser 侧日志就够了。
    实际:listener 注册可能根本没成功,必须同时看 JS 侧 addListener 是否执行、browser 侧 listener map 是否有记录。

  • 误区 C:observer 注册成功就等于扩展事件注册成功。
    实际:这是两套独立注册体系,前者连接业务源,后者连接扩展接收端。


十三、两个高频疑问的专门解答

13.1 疑问:addListener 来自"扩展进程"还是 renderer 进程?

先给结论:

扩展 JS 上下文运行在 renderer 进程(或 renderer 承载的 Service Worker 线程)。所谓"扩展进程"只是逻辑上的称呼,本质是承载扩展上下文的 renderer 进程实例。

为什么会混淆

从产品语义看,addListener() 是"扩展行为",会让人自然联想到"扩展进程";
但从 Chromium 多进程模型看,扩展 background page、popup、扩展页面都跑在 renderer 进程 里,只是这个 renderer 进程可能专门服务于扩展,不与普通网页共享。

注册链路

扩展 JS 执行 chrome.account.onStatusChanged.addListener(callback)
    ↓(在 renderer/扩展进程中)
绑定层将调用转换为 Mojo 调用
    ↓(跨进程 IPC)
browser 进程 EventRouter::AddListenerForMainThread()
    ↓
EventListenerMap::AddListener()
    ↓
listeners_["account.onStatusChanged"].push_back(listener)

一句话总结

  • 语义上:扩展端注册;
  • 实现上:扩展所处 renderer 上下文向 browser 发起注册;
  • “扩展进程” ≈ 承载扩展上下文的 renderer 进程,二者不对立。

13.2 疑问:addListener(callback) 底层是否依赖 V8?

结论:依赖,而且是第一层依赖。

分层分析

层级技术组件职责
JS/V8 绑定层(renderer)V8 + 扩展绑定层暴露 addListener 给 JS,接收并保存回调对象
IPC 层Mojo将注册请求从 renderer 发到 browser
browser 路由层EventRouter持久登记监听关系

renderer 绑定层中,事件对象通过如 EventEmitter::AddListener 将方法挂到 V8 对象上:

// extensions/renderer/bindings/event_emitter.cc
return Wrappable<EventEmitter>::GetObjectTemplateBuilder(isolate)
    .SetMethod("addListener", &EventEmitter::AddListener)
    ...

因此:

  • callback 作为 v8::Function 在 V8 上下文里被接收;
  • 其生命周期与 V8 上下文绑定;
  • 浏览器侧 EventListenerMap 登记的是监听关系(扩展 ID + 事件名),不是 V8 函数对象本身;
  • 事件回发时,browser 将事件 payload 通过 Mojo 发回 renderer,renderer 在 V8 上下文里找到 callback 并调用。

一句话总结

addListener 的入口必经 V8(JS 函数绑定、回调接收);监听关系的持久登记在 browser;事件回发仍需回到 renderer 才能真正调用 JS callback。


结束语

Chromium 扩展事件系统是一个横跨 V8/renderer、Mojo IPC、browser 路由、业务 observer 四个层次的完整机制。理解它需要同时掌握:

  • JavaScript 绑定原理(V8 + 扩展绑定层);
  • 多进程通信(Mojo IPC);
  • browser 侧事件路由(EventRouter + EventListenerMap);
  • 业务侧 observer 体系(各 service 的 ObserverList);
  • **懒上下文(Lazy Listener)**处理。

每一层出问题,最终表现都是"扩展收不到事件",排查时需要按层逐一验证,而不是只看某一侧的日志或代码。

本文以 OnLoginSucceeded() 为入口,串联了从业务服务 observer 回调到扩展 JS callback 执行的完整路径,希望能为扩展事件调试提供一个系统化的思维框架。

CEF Python高级应用:构建桌面应用内自动化测试与网络爬虫引擎 浏览器自动化是现代软件开发和数据采集的核心技术,其原理在于通过程序控制浏览器内核执行页面导航、元素交互与数据提取。这项技术的核心价值在于能够处理JavaScript渲染的动态内容、维持会话状态并模拟真实用户行为,从而解决传统HTTP请求库在复杂Web应用面前的局限性。在自动化测试、数据抓取和RPA等应用场景中,直接控制浏览器实例成为关键需求。本文聚焦于CEF Python这一特定技术方案,它允许开发者在已集成Chromium内核的桌面应用内部,通过JavaScript绑定和进程间通信机制,直接操控浏览器组件 阅读详情

相关推荐

Electron桌面开发核心原理:主进程与渲染进程协同机制解析

Electron作为主流跨平台桌面开发框架,其本质是基于Chromium渲染引擎与Node.js运行时的双进程架构。理解主进程(负责系统级能力调度)与渲染进程(专注UI交互)的职责分离,是掌握IPC通信、性能优化与安全加固的前提。该架构通过进程隔离实现UI线程不阻塞,支撑高响应式本地应用开发,广泛应用于SaaS客户端、硬件交互工具及离线优先型生产力软件。本文深入剖析Electron的进程模型、通信范式与工程落地要点,帮助Web开发者高效构建兼具原生体验与迭代速度的桌面应用。

weixin_33851177的博客 392

Chromium 完整安装指南(Windows 版)

Chromium 是一个开源的网页浏览器项目,由 Google 主导开发。它是 Google Chrome 浏览器的基础,Chrome 实际上是基于 Chromium 开发的商业版本。

u011265143的专栏 1万+

Electron桌面开发实战:跨平台、高性能与安全优化指南

Electron是一种基于Chromium和Node.js的跨平台桌面应用开发框架,其核心原理是通过主进程与渲染进程的物理隔离实现崩溃防护、安全沙箱与性能解耦。技术价值在于复用Web技术栈(React/Vue/HTML/CSS)、统一调试体验(Chrome DevTools直连)及极简分发流程(三端安装包一键生成)。典型应用场景包括SaaS离线化工具、IoT设备配套软件、企业级本地化应用等。本文深入解析Electron架构设计、IPC通信机制、预加载脚本安全模型,并结合真实项目验证内存优化(320MB稳定占

chuange6363的博客 406

Chromium 源码目录结构与主流程逐行解析 —— 架构精解与实战指南

Chromium 作为 Chrome 浏览器的核心开源项目,以其高性能、跨平台和强扩展性著称。理解其源码结构与主流程不仅有助于浏览器内核开发,还为高性能客户端、WebApp、嵌入式系统等应用提供坚实基础。本文将以主流程为线索,深入源码行级剖析,结合架构设计思想,配以流程图与代码注释,助你知其然、知其所以然。本文从目录结构、主流程、源代码、设计思想、业务场景、调试优化、高阶集成、底层演进八大维度,系统性剖析了 Chromium 的架构与源码。

IT技术学习与工作笔记分享 2577

虚幻引擎WebUI插件:用Web前端技术构建高效游戏UI

在现代软件开发中,前后端分离架构与跨平台用户界面(UI)开发是提升工程效率的关键技术。其核心原理在于通过定义清晰的通信协议(如基于JSON的消息传递),将界面逻辑与业务逻辑解耦,从而实现技术栈独立、并行开发和快速迭代。这种架构的技术价值在于能充分利用各领域的最优工具链:前端可使用React、Vue等现代框架与丰富生态库(如ECharts)高效构建复杂、数据驱动的可视化界面;后端则可专注于核心业务与性能。在游戏开发领域,这一模式的应用场景尤为突出,特别是当项目需要复杂信息展示(如模拟经营游戏的动态数据仪表盘)

weixin_33947521的博客 476

指纹浏览器生态闭环:如何设计灵活的插件系统让第三方扩展浏览器功能?

本文探讨了指纹浏览器与风控系统对抗中的核心问题及解决方案。传统指纹浏览器的单体架构无法应对动态风控规则,导致脚本注入污染、isTrusted属性泄露等问题。文章提出工业级指纹浏览器需解耦底层物理拟态与业务扩展性,构建基于Chromium的IsolatedWorld隔离、C++级IPC通信和动态生命周期的插件引擎闭环。通过双向沙箱、安全RPC桥接和资源硬隔离机制,实现指纹安全与业务灵活性的平衡。最终形成一个既能抵御风控探测,又能支持第三方扩展的数字生态体系。

数据知道的博客 900

Chromium网页输入事件捕捉和手势检测过程分析

连续的输入事件可能会产生一定的手势操作,例如滑动手势和捏合手势。在Chromium中,网页的输入事件是在Browser进程中捕捉的。Browser进程捕获输入事件之后,会进行手势操作检测。检测出来的手势操作将会发送给Render进程处理,因为它们需要应用在网页之上。与此同时,Browser进程也会将原始的输入事件发送给Render进程处理。本文接下来就分析Browser进程处理网页输入事件的过程。

老罗的Android之旅 2万+

指纹浏览器行为生物指纹(下):键盘敲击节奏与滚动行为的仿生学建模

本文探讨了现代风控系统对自动化脚本的深度检测机制,以及如何构建仿生学引擎绕过这些防护。文章指出传统自动化工具(如Puppeteer的直接输入和滚动)在高级风控面前存在致命缺陷:固定间隔的键盘输入、无物理惯性的滚动行为以及非硬件触发的事件,都会被风控系统通过击键动力学和滚动熵分析轻易识别。风控系统通过微秒级时间差、手指映射熵和物理惯性等特征判断操作真实性。为实现有效绕过,必须在宿主机层构建基于神经认知学和机械动力学的物理引擎,通过极速IPC通道与浏览器底层直连,合成带有真实生物节律的输入事件。文章还深入解析了

数据知道的博客 481

老罗的Chromium之旅

快来跟老罗一起学习Chromium吧!

weixin_42111148的博客 5062

【大模型微调】魔搭社区GPU进行LLaMA-Factory微调大模型自我认知

本教程介绍如何使用魔搭社区GPU资源和LLaMA-Factory工具进行大模型微调。主要内容包括环境配置、工具安装、模型训练和效果评估。通过LoRA算法进行自我认知微调,详细说明了从数据准备到模型导出的完整流程,并提供了参数调优和显存管理等实用技巧,帮助读者快速掌握大模型定制化训练方法。

LChuck的博客 2924

Android 12 HAL开发避坑实录:手把手教你用AIDL在Native层实现一个可开机自启动的Service

本文详细介绍了在Android 12 HAL开发中,如何通过AIDL在Native层实现一个可开机自启动的Service。从环境准备、AIDL接口定义、服务实现到VINTF集成与SELinux策略配置,提供了完整的实战指南,帮助开发者避开常见陷阱,提升开发效率。

weixin_28314575的博客 321

Chromium框架

1.chromium框架概述 chromium框架主要描述chromium模块、层次划分,强调各个模块之间如何协作,而不会具体到内部如何工作。 2.视图一

zhuiyuanqingya的博客 3838

从玩具车到手机充电:手把手带你用Multisim仿真Buck/Boost电路(附工程文件)

本文通过Multisim仿真软件,手把手教你搭建Buck降压电路和Boost升压电路,实现从12V到5V或3.7V到5V的电压转换。文章详细介绍了电路搭建步骤、关键波形观测技巧及常见故障排查方法,并附赠工程文件,帮助电子爱好者快速掌握电源转换技术。

weixin_29159127的博客 241

一条命令在本地完成二进制逆向:rea AI 逆向 CLI 完整上手指南

rea 是一个 AI 逆向 CLI,面向陌生的二进制或 Electron 应用:一条命令在本地完成二进制逆向,符号、函数、引用连同原始证据一起返回。你不需要先学会 IDA 或 Ghidra 这类图形界面工具。这篇文章带你走完整条终端链路:先做环境体检,再编译一个 stripped 练手目标,把 `main` 追到引用,最后讲出错时该看什么。 ## 先跑 rea doctor 体检,再选三档安装方

gitblog_00583的博客 249

vivado Block Design 设计 .tcl 文件使用,导出,修改,添加方式

在vivado中生成 Block Design可以导出tcl文件进行保存(体积小),或者在别的版本vivado中打开(版本互通),这是一个非常好的功能。另外GitHub中很多项目都是采用这种方式上传的,学会如何使用可以快速修改历程项目。

2203_75345038的博客 2493

天池长期赛:二手车价格预测(422方案分享)

本次比赛是一次很好的锻炼数据挖掘能力的入门赛,从EDA-数据清洗-特征工程-模型调参-模型融合,能够完整的学习到大数据的分析和挖掘,值得历练。

weixin_46685991的博客 1万+

深入解析Chromium内置WebUI资源加载:从GRIT编译到页面渲染的完整链路

在大型C++应用开发中,如何安全、高效地集成和管理前端资源是一个核心工程挑战。其原理在于设计一套独立的资源加载体系,将HTML、JS、CSS等前端资源在编译时嵌入二进制文件,实现平台无关的快速加载。这项技术的核心价值在于平衡了性能、安全与跨平台一致性,它避免了网络依赖,通过严格的权限控制和内容安全策略(CSP)确保内置界面的可信度。典型的应用场景包括浏览器设置页、历史记录等需要直接调用底层API的本地界面。本文聚焦于Chromium的WebUI系统,详细剖析了其如何利用GRIT工具链进行资源编译与国际化,并

WGH100817的博客 408

QT Android开发之Android端usb调试模式设置与问题解决

QT Android开发之Android端usb调试模式设置与问题解决 一.QT android开发android端usb调试模式设置 QT android开发环境搭建完成后,android设备需要打开usb调试模式才能正常连接,下面以小米ipad为例进行设置(其他手机和ipad设置方法类似) 二.小米ipad打开usb调试模式 1.设置----我的设备-----全部参数信息-----OS版本---点击这里6次以上就进入开发者模式。

热土程序园,利他愉己~ 1127

JavaScript高手成长路径:执行深度、认知宽度与反馈速度

JavaScript作为前端核心语言,其能力本质远超语法记忆——它要求开发者构建可推演的运行时心智模型(如this绑定、事件循环微任务机制),建立横跨浏览器/V8/Node.js的技术栈坐标系,并将调试升维为‘假设-验证’的实验科学。真正的进阶不靠碎片化学习,而在于对ECMAScript规范、V8引擎实现与真实生产问题的三层穿透。本文围绕执行深度、认知宽度、反馈速度三大关键词,系统拆解从语法搬运工到架构决策者的四阶跃迁路径,覆盖反向工程标准库、手写最小可行架构、开源负向贡献及技术债清算等硬核实践,直击中高级

weixin_40192258的博客 667

Unreal Engine开发:多人联机与网络同步_延迟补偿与预测

通过延迟补偿和预测技术,我们可以显著减少网络延迟对多人在线动作游戏的影响,提升玩家的体验。延迟补偿主要在服务器端实现,通过回溯时间点来更准确地处理输入和事件。预测技术主要在客户端实现,通过预先计算玩家的动作结果来减少延迟感。结合这两种技术,可以实现更平滑、更公平的多人游戏体验。

chenlz2007的博客 1191

Agent应用实践之三 - 基础:AgentScope-ReActAgent源码解析

本章通过总结AgentScope的核心类,再通过分析ReActAgent调用大模型的底层源码原理。使我们基本了解了Agent的基本原理,这个实现方式与OpenClaw使用pi-mono的Agent基础框架是一致的。了解完ReActAgent的基本原理,下一章我们会将一下SpringBoot集成方式的代码原理,这两章是我们构建后续内容的基本框架。

代码让AI扣 467

在虚幻引擎中集成React:现代化UI开发方案全解析

在现代应用开发中,声明式UI框架因其高效的开发模式和卓越的可维护性,已成为构建复杂用户界面的主流选择。其核心原理是通过状态驱动视图更新,将开发者从繁琐的DOM操作中解放出来,专注于业务逻辑。这种模式在Web前端领域,尤其是React生态中得到了充分验证,其技术价值在于显著提升了开发效率与团队协作体验。随着数字孪生、虚拟仿真等复杂可视化应用场景的兴起,传统的游戏引擎UI方案在应对频繁迭代和动态界面时面临挑战。本文将探讨如何通过集成Unreal.js与React,将这套成熟的Web前端开发范式引入虚幻引擎,实现

hqmln的博客 385

老版QQ原生表情gif动态

这是老版的QQ表情,不过全是动态的的,Windows照片查看器是不会动的,可以右击用浏览器打开查看,再说一遍是老版的,只是分享给宁愿用老版也要动态的人。

QtWebEngine性能瓶颈深度解析与全方位优化实战指南

在桌面应用开发中,嵌入现代Web内容以实现富媒体交互是常见需求,这通常涉及浏览器内核的集成。其核心原理在于通过一个基于Chromium的封装层,将Web技术栈与原生应用框架桥接,从而在单一应用中融合Web的灵活性与原生的性能。这项技术的核心价值在于能够快速构建功能丰富、界面现代的跨平台应用,广泛应用于数据可视化大屏、工业上位机软件等复杂场景。然而,随着应用复杂度提升,**性能瓶颈**问题逐渐凸显,尤其是在处理**WebGL**动画、复杂DOM操作及长时间运行时,可能出现渲染卡顿、内存泄漏等问题。本文聚焦于Q

weixin_29284885的博客 227

深度学习工具包 DeepLearningToolbox

深度学习工具包Deprecation notice.-----This toolbox is outdated and no longer maintained.There are much better tools available for deep learning than this toolbox, e.g. [Theano](http://deeplearning.net/software/theano/), [torch](http://torch.ch/) or [tensorflow](http://www.tensorflow.org/)I would suggest you use one of the tools mentioned above rather than use this toolbox.Best, Rasmus.DeepLearnToolbox================A Matlab toolbox for Deep Learning.Deep Learning is a new subfield of machine learning that focuses on learning deep hierarchical models of data.It is inspired by the human brain's apparent deep (layered, hierarchical) architecture.A good overview of the theory of Deep Learning theory is[Learning Deep Architectures for AI](http://www.iro.umontreal.ca/~bengioy/papers/ftml_book.pdf)For a more informal introduction, see the following videos by Geoffrey Hinton and Andrew Ng.* [The Next Generation of Neural Networks](http://www.youtube.com/watch?v=AyzOUbkUf3M) (Hinton, 2007)* [Recent Developments in Deep Learning](http://www.youtube.com/watch?v=VdIURAu1-aU) (Hinton, 2010)* [Unsupervised Feature Learning and Deep Learning](http://www.youtube.com/watch?v=ZmNOAtZIgIk) (Ng, 2011)If you use this toolbox in your research please cite [Prediction as a candidate for learning deep hierarchical models of data](http://www2.imm.dtu.dk/pubdb/views/publication_details.php?id=6284)```@MASTERSTHESIS\{IMM2012-06284, author = "R. B. Palm", title = "Prediction as a candidate for learning deep hierarchical models of data", year = "2012",}```Contact: rasmusbergpalm at gmail dot comDirectories included in the toolbox-----------------------------------`NN/` - A library for Feedforward Backpropagation Neural Networks`CNN/` - A library for Convolutional Neural Networks`DBN/` - A library for Deep Belief Networks`SAE/` - A library for Stacked Auto-Encoders`CAE/` - A library for Convolutional Auto-Encoders`util/` - Utility functions used by the libraries`data/` - Data used by the examples`tests/` - unit tests to verify toolbox is workingFor references on each library check REFS.mdSetup-----1. Download.2. addpath(genpath('DeepLearnToolbox'));Example: Deep Belief Network---------------------```matlabfunction test_example_DBNload mnist_uint8;train_x = double(train_x) / 255;test_x = double(test_x) / 255;train_y = double(train_y);test_y = double(test_y);%% ex1 train a 100 hidden unit RBM and visualize its weightsrand('state',0)dbn.sizes = [100];opts.numepochs = 1;opts.batchsize = 100;opts.momentum = 0;opts.alpha = 1;dbn = dbnsetup(dbn, train_x, opts);dbn = dbntrain(dbn, train_x, opts);figure; visualize(dbn.rbm{1}.W'); % Visualize the RBM weights%% ex2 train a 100-100 hidden unit DBN and use its weights to initialize a NNrand('state',0)%train dbndbn.sizes = [100 100];opts.numepochs = 1;opts.batchsize = 100;opts.momentum = 0;opts.alpha = 1;dbn = dbnsetup(dbn, train_x, opts);dbn = dbntrain(dbn, train_x, opts);%unfold dbn to nnnn = dbnunfoldtonn(dbn, 10);nn.activation_function = 'sigm';%train nnopts.numepochs = 1;opts.batchsize = 100;nn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.10, 'Too big error');```Example: Stacked Auto-Encoders---------------------```matlabfunction test_example_SAEload mnist_uint8;train_x = double(train_x)/255;test_x = double(test_x)/255;train_y = double(train_y);test_y = double(test_y);%% ex1 train a 100 hidden unit SDAE and use it to initialize a FFNN% Setup and train a stacked denoising autoencoder (SDAE)rand('state',0)sae = saesetup([784 100]);sae.ae{1}.activation_function = 'sigm';sae.ae{1}.learningRate = 1;sae.ae{1}.inputZeroMaskedFraction = 0.5;opts.numepochs = 1;opts.batchsize = 100;sae = saetrain(sae, train_x, opts);visualize(sae.ae{1}.W{1}(:,2:end)')% Use the SDAE to initialize a FFNNnn = nnsetup([784 100 10]);nn.activation_function = 'sigm';nn.learningRate = 1;nn.W{1} = sae.ae{1}.W{1};% Train the FFNNopts.numepochs = 1;opts.batchsize = 100;nn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.16, 'Too big error');```Example: Convolutional Neural Nets---------------------```matlabfunction test_example_CNNload mnist_uint8;train_x = double(reshape(train_x',28,28,60000))/255;test_x = double(reshape(test_x',28,28,10000))/255;train_y = double(train_y');test_y = double(test_y');%% ex1 Train a 6c-2s-12c-2s Convolutional neural network %will run 1 epoch in about 200 second and get around 11% error. %With 100 epochs you'll get around 1.2% errorrand('state',0)cnn.layers = { struct('type', 'i') %input layer struct('type', 'c', 'outputmaps', 6, 'kernelsize', 5) %convolution layer struct('type', 's', 'scale', 2) %sub sampling layer struct('type', 'c', 'outputmaps', 12, 'kernelsize', 5) %convolution layer struct('type', 's', 'scale', 2) %subsampling layer};cnn = cnnsetup(cnn, train_x, train_y);opts.alpha = 1;opts.batchsize = 50;opts.numepochs = 1;cnn = cnntrain(cnn, train_x, train_y, opts);[er, bad] = cnntest(cnn, test_x, test_y);%plot mean squared errorfigure; plot(cnn.rL);assert(er<0.12, 'Too big error');```Example: Neural Networks---------------------```matlabfunction test_example_NNload mnist_uint8;train_x = double(train_x) / 255;test_x = double(test_x) / 255;train_y = double(train_y);test_y = double(test_y);% normalize[train_x, mu, sigma] = zscore(train_x);test_x = normalize(test_x, mu, sigma);%% ex1 vanilla neural netrand('state',0)nn = nnsetup([784 100 10]);opts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samples[nn, L] = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.08, 'Too big error');%% ex2 neural net with L2 weight decayrand('state',0)nn = nnsetup([784 100 10]);nn.weightPenaltyL2 = 1e-4; % L2 weight decayopts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex3 neural net with dropoutrand('state',0)nn = nnsetup([784 100 10]);nn.dropoutFraction = 0.5; % Dropout fraction opts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex4 neural net with sigmoid activation functionrand('state',0)nn = nnsetup([784 100 10]);nn.activation_function = 'sigm'; % Sigmoid activation functionnn.learningRate = 1; % Sigm require a lower learning rateopts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex5 plotting functionalityrand('state',0)nn = nnsetup([784 20 10]);opts.numepochs = 5; % Number of full sweeps through datann.output = 'softmax'; % use softmax outputopts.batchsize = 1000; % Take a mean gradient step over this many samplesopts.plot = 1; % enable plottingnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex6 neural net with sigmoid activation and plotting of validation and training error% split training data into training and validation datavx = train_x(1:10000,:);tx = train_x(10001:end,:);vy = train_y(1:10000,:);ty = train_y(10001:end,:);rand('state',0)nn = nnsetup([784 20 10]); nn.output = 'softmax'; % use softmax outputopts.numepochs = 5; % Number of full sweeps through dataopts.batchsize = 1000; % Take a mean gradient step over this many samplesopts.plot = 1; % enable plottingnn = nntrain(nn, tx, ty, opts, vx, vy); % nntrain takes validation set as last two arguments (optionally)[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');```[![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/rasmusbergpalm/deeplearntoolbox/trend.png)](https://bitdeli.com/free "Bitdeli Badge")

VC中sqlite的使用

在VC中对sqlite的使用,实现数据库大批数据的快速插入,读取数据库中的数据在ListCtrl中显示

Webkit架构解析与实战:从渲染原理到性能优化

浏览器渲染引擎是现代Web应用的核心技术组件,负责将HTML、CSS和JavaScript代码转换为用户可视的网页界面。其工作原理遵循从解析、构建对象模型到布局、绘制、合成的完整流水线,这一流程直接决定了页面的加载速度、交互流畅度和渲染一致性。在工程实践中,深入理解渲染引擎的内部机制,特别是布局(Reflow)与绘制(Painting)的触发条件与性能开销,对于进行有效的前端性能优化至关重要。通过开发者工具深入分析渲染过程,并结合对内存管理与图层合成机制的理解,开发者可以系统性解决常见的性能瓶颈,如布局抖动

weixin_34357928的博客 355

pscad IEEE标准系统搭建说明

pscad IEEE标准系统搭建说明,包含典型的IEEE 9节点、14节点、30节点、39节点和118节点系统。内含标准测试系统的原始数据和潮流结果。

UE4游戏内数据可视化:WEBUI与ECharts打造实时动态仪表盘

数据可视化是现代软件开发和游戏工程中的关键技术,它通过图形化手段将抽象数据转化为直观的视觉信息,帮助开发者洞察系统状态与用户行为。其核心原理在于利用图表库将数据映射为视觉元素,如折线、柱状或热力区域。在游戏开发领域,这项技术的价值尤为突出,它能将后台海量的实时运行数据(如玩家分布、资源流动、系统负载)从枯燥的日志变为可交互的仪表盘,直接服务于游戏平衡性调优、运营健康度监控和玩家体验增强。通过集成WEBUI插件与ECharts图表库,开发者可以在UE4游戏内部构建高性能的实时数据看板,实现从蓝图到网页前端的高

weixin_30263073的博客 336

CEF性能调优实战:如何用Visual Studio Profiler精准定位内存泄漏

本文详细介绍了如何使用Visual Studio Profiler进行CEF性能调优,精准定位内存泄漏问题。通过构建可复现的分析环境、定制化Profiler配置以及多进程关联分析技术,帮助开发者有效解决CEF特有的内存管理挑战,包括渲染进程与主进程间的对象生命周期管理、CefRefPtr智能指针的最佳实践等。

js777的博客 427
上一篇: 深入 Chromium 与 V8 底层:扩展 API 的注入、执行与回调全链路解析
下一篇: 深度还原:一段代码在 V8 中的奇幻漂流与 Gin 的跨界魔法
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ปรัชญา แค้วคำมูล

你的鼓励将是我创作的最大动力!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值