Chromium回调机制的隐秘角落:当const &参数遇见base::BindOnce

Chromium Mojo C++ Bindings API(中文) chromium mojo机制最新版 阅读详情

看似简单的类型转换背后,隐藏着Chromium工程师对C++语言特性的深度理解和精妙运用

在Chromium的异步编程世界里,base::BindOnce和base::BindRepeating是每个开发者都离不开的工具。然而,当我们写下看似平凡的代码base::BindOnce(&Func, const_ref_param)时,一个复杂的类型魔术正在幕后悄然上演。

一、表象与现实的落差:const &为何不按预期行事?

让我们从一个常见的场景开始:

void ProcessData(const std::string& data) {
  // 处理数据
}

void Demo() {
  std::string large_data = LoadLargeDataFromFile();
  
  // 直觉:传递const引用,应该避免拷贝
  auto callback = base::BindOnce(&ProcessData, large_data);
  
  // 但实际情况是:这里发生了一次完整的拷贝!
}

为什么?这似乎违背了C++程序员对const &的直觉理解。要解开这个谜团,我们需要深入Chromium回调机制的设计哲学。

二、类型魔术:std::decay_t的隐秘力量

2.1 类型擦除的三重奏

在base::BindOnce的内部实现中,有一个关键的模板元编程技巧:

template <typename T>
using DecayedType = std::decay_t<T>;

// 对于不同类型的处理:
// const std::string&  -> std::string
// volatile int&       -> int  
// const char[10]      -> const char*
// void(&)()           -> void(*)()

template <typename Functor, typename... Args>
auto BindOnceImpl(Functor&& functor, Args&&... args) {
  // 关键转换发生在这里
  using StoredArgs = std::tuple<std::decay_t<Args>...>;
  // ...
}

std::decay_t执行了一个精密的类型转换过程:

  1. 移除引用:&和&&被剥离

  2. 移除cv限定符:const和volatile被移除

  3. 数组退化:数组类型退化为指针

  4. 函数退化:函数类型退化为函数指针

2.2 为什么必须这样做?

考虑回调的生命周期问题:

void AsyncProcess(const BigObject& obj);

void ProblematicCode() {
  BigObject* obj = new BigObject();  // 动态分配
  auto callback = base::BindOnce(&AsyncProcess, *obj);
  
  delete obj;  // 对象被提前销毁
  
  // 如果callback存储的是引用,这里就是悬垂引用!
  std::move(callback).Run();  // 未定义行为
}

Chromium的选择是安全性优先:默认进行拷贝,避免悬垂引用。这是一种防御性编程策略,在大型、多线程的浏览器代码库中尤为重要。

三、深入实现:从源代码看类型流转

让我们追踪一个具体例子在Chromium源码中的旅程:

// 用户代码
void HandleRequest(const HttpRequest& req);
HttpRequest request = GetRequest();
auto cb = base::BindOnce(&HandleRequest, request);

// 进入base::BindOnce实现
template <typename Functor, typename... BoundArgs>
inline CallbackType BindOnce(Functor&& functor, BoundArgs&&... args) {
  // BoundArgs推导为:HttpRequest& (因为request是左值)
  
  // 创建BindState,这里发生关键转换
  using BindState = internal::BindState<
      std::decay_t<Functor>,
      std::decay_t<BoundArgs>...>;
      
  // std::decay_t<HttpRequest&> = HttpRequest
  // 所以存储的是HttpRequest,不是HttpRequest&
  
  return CallbackType(BindState::Create(
      std::forward<Functor>(functor),
      std::forward<BoundArgs>(args)...));
}

3.1 存储阶段:类型擦除与值语义

// bind_internal.h中的实际存储
template <typename... BoundArgs>
class BindState : public BindStateBase {
 private:
  // 注意这里的存储类型
  std::tuple<std::decay_t<BoundArgs>...> bound_args_;
  
 public:
  template <typename... ForwardArgs>
  explicit BindState(ForwardArgs&&... args)
      : bound_args_(std::forward<ForwardArgs>(args)...) {
    // 构造函数参数是完美转发的引用
    // 但tuple的存储类型是decay后的类型
    // 这触发了从HttpRequest&到HttpRequest的转换构造函数
  }
};

这个转换的妙处在于:它利用了C++的隐式转换规则。当tuple尝试用HttpRequest&初始化HttpRequest元素时,拷贝构造函数被调用。

3.2 调用阶段:类型恢复与引用重建

更精彩的部分发生在回调执行时:

template <size_t... indices>
auto InvokeImpl(std::index_sequence<indices...>) {
  // 从tuple中提取存储的值
  auto& stored_value = std::get<index>(bound_args_);
  
  // 传递给目标函数
  return functor_(
      // 这里发生隐式转换:HttpRequest → const HttpRequest&
      stored_value,
      ...);
}

魔法就在这里:当HttpRequest类型的变量传递给期望const HttpRequest&参数的函数时,C++会自动创建一个临时引用。这个转换是零成本的,只是类型系统的重新解释。

四、性能权衡:何时需要绕过默认行为

4.1 基准测试:拷贝的成本

让我们量化一下不同策略的性能差异:

// 测试数据大小对性能的影响
struct Data {
  char buffer[1024];  // 1KB数据
  // char buffer[1024 * 1024];  // 1MB数据
};

void Process(const Data& data) {}

// 测试1:默认拷贝
auto cb1 = base::BindOnce(&Process, data);  // 拷贝1KB/1MB

// 测试2:使用std::cref避免拷贝  
auto cb2 = base::BindOnce(&Process, std::cref(data));  // 只传递引用

// 测试3:移动语义(如果data之后不再使用)
auto cb3 = base::BindOnce(&Process, std::move(data));  // 移动

测试结果趋势:

  • 小对象(< 64字节):拷贝成本可忽略,使用默认行为

  • 中等对象(64B - 1KB):根据使用频率权衡

  • 大对象(> 1KB):应优先考虑引用或移动

4.2 安全使用std::cref的准则

// 正确示例:明确的生命周期
class RequestHandler {
 public:
  void StartRequest() {
    // request_的生命周期长于callback
    auto callback = base::BindOnce(&RequestHandler::OnComplete, 
                                  weak_factory_.GetWeakPtr(),
                                  std::cref(request_));
    SendAsync(std::move(callback));
  }
  
 private:
  HttpRequest request_;  // 成员变量,生命周期明确
};

// 危险示例:临时对象的引用
void Dangerous() {
  std::string temp = GenerateTempData();
  // 错误!temp将在函数返回时销毁
  auto callback = base::BindOnce(&Process, std::cref(temp));
  PostTask(std::move(callback));  // 悬垂引用!
}

4.3 现代C++的优化:移动语义的融合

Chromium的回调机制与C++11移动语义深度集成:

void Process(std::unique_ptr<Data> data);

auto data = std::make_unique<Data>();
// 完美支持移动语义
auto cb = base::BindOnce(&Process, std::move(data));

// 内部实现:通过特化处理unique_ptr
template <>
struct BindUnwrapTraits<std::unique_ptr<Data>> {
  static std::unique_ptr<Data>&& Unwrap(
      std::unique_ptr<Data>&& ptr) {
    return std::move(ptr);  // 保持移动语义
  }
};

五、复杂场景:模板、继承与类型推导

5.1 模板函数的绑定挑战

template <typename T>
void TemplateProcess(const T& value);

// 绑定模板函数需要显式实例化
auto cb1 = base::BindOnce(&TemplateProcess<int>, 42);
auto cb2 = base::BindOnce(&TemplateProcess<std::string>, data);

// 类型推导的微妙之处
template <typename T>
void ForwardingProcess(T&& value);  // 通用引用

// 这里的行为更加复杂
auto cb3 = base::BindOnce(&ForwardingProcess<std::string&>, data);
// 存储的是std::string,但目标期望std::string&
// 需要特殊的Unwrap处理

5.2 继承层次中的类型转换

class Base {
 public:
  virtual void Process() const = 0;
};

class Derived : public Base { /* ... */ };

void HandleBase(const Base& obj);

Derived derived;
// 这里发生切片吗?
auto cb = base::BindOnce(&HandleBase, derived);

// 答案:是的,发生切片!
// std::decay_t<Derived&> = Derived
// 存储的是Derived对象,不是Base引用
// 传递给HandleBase时:Derived → const Base&
// 发生对象切片,多态性丢失

解决方案:使用智能指针或自定义包装器:

auto cb1 = base::BindOnce(&HandleBase, std::cref(derived));  // 保持多态
auto cb2 = base::BindOnce(&HandleBase, base::Unretained(&derived));
auto cb3 = base::BindOnce(&HandleBase, 
                         std::static_pointer_cast<Base>(derived_ptr));

六、工程实践:Chromium内部的最佳模式

6.1 Blink中的实际应用

在Chromium的渲染引擎Blink中,这种模式被广泛使用:

// Typical Blink pattern
class DocumentLoader {
 public:
  void StartLoading() {
    auto callback = base::BindOnce(&DocumentLoader::DidLoad,
                                  weak_factory_.GetWeakPtr(),
                                  // 传递资源数据的const引用
                                  std::cref(resource_data_));
    
    resource_loader_->Start(std::move(callback));
  }
  
 private:
  // 资源数据作为成员变量,生命周期明确
  ResourceData resource_data_;
};

6.2 网络栈中的优化

Chromium网络模块处理大量数据时,采用分层策略:

class URLRequest {
  void OnReceivedData(const std::vector<char>& data) {
    // 小数据:直接拷贝
    if (data.size() < 4096) {
      auto cb = base::BindOnce(&URLRequest::ProcessData, 
                              weak_factory_.GetWeakPtr(),
                              data);  // 拷贝
    }
    // 大数据:共享所有权
    else {
      auto shared_data = std::make_shared<std::vector<char>>(data);
      auto cb = base::BindOnce(&URLRequest::ProcessLargeData,
                              weak_factory_.GetWeakPtr(),
                              shared_data);  // 只拷贝shared_ptr
    }
  }
};

6.3 性能敏感路径的特别处理

在浏览器内核的关键路径中,有时需要打破常规:

// 在极度性能敏感的代码中
class CriticalPerformancePath {
  void OptimizedCallback(const PerformanceData& data) {
    // 确保data生命周期足够长
    static_assert(sizeof(data) <= 64, "Data too large for fast path");
    
    // 使用Unretained,由调用者保证生命周期
    auto cb = base::BindOnce(&CriticalPerformancePath::Process,
                            base::Unretained(this),
                            base::Unretained(&data));
    
    FastPathScheduler::Post(std::move(cb));
  }
};

七、调试与验证:查看实际类型转换

对于内核开发者,理解这些转换的细节至关重要。以下是一些调试技巧:

#include <type_traits>

// 1. 编译期类型检查
static_assert(std::is_same_v<
  std::decay_t<const std::string&>, 
  std::string>, "Verify decay behavior");

// 2. 运行时类型信息(RTTI)
template <typename T>
void LogType(const T& value) {
  LOG(INFO) << "Type: " << typeid(T).name();
  LOG(INFO) << "Decayed: " << typeid(std::decay_t<T>).name();
}

// 3. 自定义类型追踪器
struct TypeTracker {
  TypeTracker() { ++default_construct; }
  TypeTracker(const TypeTracker&) { ++copy_construct; }
  TypeTracker(TypeTracker&&) { ++move_construct; }
  
  static void Reset() { default_construct = copy_construct = move_construct = 0; }
  static int default_construct, copy_construct, move_construct;
};

八、未来演进:C++20与概念约束

C++20带来的新特性可能改变这种模式:

// C++20概念可以更精确地约束
template <typename T>
requires std::copyable<T>
auto BindOnceWithConcepts(Functor&& f, const T& arg) {
  // 更明确的类型要求
}

// 或许未来的Chromium会采用更现代化的方式
// 但当前的设计已经经受住了十多年的考验

九、设计哲学总结

Chromium的base::BindOnce处理const &参数的方式,体现了几个核心工程原则:

  1. 安全默认原则:默认选择安全的行为(拷贝),即使有性能代价

  2. 显式优于隐式:优化(std::cref)需要开发者显式指定,表明理解相关风险

  3. 渐进式复杂度:提供从简单到复杂的多层次API,满足不同场景需求

  4. 类型系统一致性:充分利用C++类型系统,保持行为可预测

这种设计在Chromium这样的超大型C++项目中证明了自己的价值:它减少了一整类难以调试的悬垂引用错误,同时为有经验的开发者提供了优化的空间。

结语:在安全与性能的钢丝上行走

理解base::BindOnce如何处理const &参数,不仅仅是一个语言细节问题,更是理解Chromium工程文化的一扇窗口。它展示了如何在C++这样一个赋予开发者极大自由度的语言中,构建一套既安全又高效的异步编程基础设施。

每个看似简单的API设计背后,都是无数次权衡、调试和重构的结果。下一次当您写下base::BindOnce(&Func, param)时,不妨想一想:您希望这个参数被拷贝还是被引用?它的生命周期是否明确?这个简单的选择,体现了您对代码深层语义的理解。

在浏览器内核开发的世界里,真正的专业不仅在于知道如何写代码,更在于理解每一行代码背后复杂而精妙的设计决策。base::BindOnce的const &处理逻辑,正是这种专业精神的完美体现。

chromium base::bind 用法 Chromium 提供了 base::Bind 和模版类型 base::Callback 对函数回调提供了支持,下面是一个简单的使用例程,将一个全局函数绑定到一个 Callback 对象,并通过 Callback.Run 调用这个函数: int Return5() { return 5; } base::Callback&lt;int(void)&gt; func_cb = base::Bind(&amp;Return5); LOG(INFO) &lt;&lt; func_cb.Run(); // Pr. 阅读详情

相关推荐

主板USB接口全部失效解决方案(通用串行总线USB控制器有黄色叹号)保姆级教程亲测有效

通用串行总线控制器总是有叹号解决办法 办公室的崩腾时代老电脑,疫情期间没人用,来了开机之后发现鼠标键盘都用不了,本以为是主机放时间长出了问题,后来想起来就百度了“鼠标键盘都失效”的标题,结果发现了“串行总线控制器总是感叹号”的关联问题。正好办公室电脑都装有teamviwer常用来远程办公(在家偷懒)。所以通过远程发现电脑室可以运行的。并在控制面板发现:通用串行总线控制器总是感叹号,插入外设时USB无反应。如图:​ 并且右键选中其中一个,点击最后一个选项:“属性” 可以发现“设备状态”中会提示“由...

xiao_bai_0_0的博客 2万+

多线程开发中,多用消息传递,少用锁

有几个注入点可以从其实现的外部控制绑定行为。};}};如果在方法base::Bind的接收器上为true ,则 检查接收器的评估结果是否为true,并在其评估为false时取消调用。您可以专门将外部智能指针作为弱指针。在调用目标函数之前,为每个绑定的参数调用。您可以专注这个定义参数的包装,例如,,和。Windows提供了非常原始的工具来构建用户界面。该系统提供了一些基本控件和本机窗口容器,但是构建自定义用户界面很困难。

m0_73257876的博客 706

ECharts 实现世界地图

效果图 源代码(仅供参考) &lt;template&gt; &lt;div class="echarts"&gt; &lt;div class="className" id="id" style="width: 100%; height: 450px" ref="myEchart" &gt;&lt;/div&gt; &lt;/div&gt; &lt;/template&gt; &lt;script lang="ts"&gt; imp

EsquireY的博客 4871

Chrome 中的 Threading 和 Tasks

线程 每个 chrome 进程都包含有: 一个主线程: 在 browser 进程中负责:更新 UI; 在 renderer 进程中负责:运行 Blink; 一个 IO 线程: 在 browser 进程中负责:处理 IPC 和网络请求; 在 renderer 进程中负责:处理 IPC 请求; 一些处理特殊操作的线程; 一个处理通用操作的线程池; 大部分的线程都包含有一个...

相见不如怀念 2719

手把手教你配置UEFI Secure Boot:从密钥生成到系统验证(附避坑指南)

本文详细介绍了如何配置UEFI Secure Boot,从密钥生成到系统验证的全过程,包括PK、KEK等关键密钥的生成方法,以及常见问题的解决方案。通过实战案例和避坑指南,帮助用户有效提升系统启动安全性,防止恶意软件植入。

weixin_30776273的博客 340

基于BS21的星闪模块设计电路SLE/BLE

基于BS21的星闪模块设计电路SLE/BLE HI2821

2503_91889531的博客 2736

《Threading and Tasks in Chrome》笔记

学习资料:链接

Haipai1998的博客 1291

保姆级教程:手把手教你为裕泰微YT8521 PHY芯片编写Linux设备树(DTS)节点与驱动

本文提供了一份详细的保姆级教程,手把手教你为裕泰微YT8521 PHY芯片编写Linux设备树(DTS)节点与驱动。从基础配置到RGMII接口的特殊设置,再到驱动开发的关键步骤和调试技巧,帮助开发者快速掌握PHY芯片的移植与开发。

weixin_33734785的博客 332

chrome-base 如何实现一个BindOnce

BindOnce 是如何实现的呢?翻看源码:base\functional\bind.h写的 非常简洁重要的也就是最后一句:构造一个BindImpl,同时完美转发BindState的实现其中用到了std::tuple??

LIJIWEI0611的博客 1295

欧姆龙PMAC CK3M控制器新手避坑指南:从开箱到EtherCAT总线配置的完整流程

本文详细介绍了欧姆龙PMAC CK3M控制器的完整配置流程,从开箱检查到EtherCAT总线配置,特别针对新手常见问题提供避坑指南。内容包括硬件连接、软件安装、EtherCAT主站初始化、从站配置、电机参数设置等关键步骤,帮助工程师快速掌握PMAC控制器的使用技巧。

weixin_42532938的博客 387

MTK平台LCD驱动开发实战:从零开始点亮你的Android屏幕

本文详细介绍了在MTK平台进行Android LCD驱动开发的完整流程。从理解LK与Kernel双核驱动框架入手,手把手讲解驱动结构体、屏幕参数配置、初始化序列编写以及PLL_CLOCK计算等核心步骤,并提供了系统集成配置与实用的调试避坑指南,帮助开发者高效点亮新屏幕。

terraform7cloud的博客 291

chromium中mojom中增加接口

我们因为使用mojo video decoder,所以增加一个获取底层平台的接口,从DecoderStream中获取,就需要在mojom::VideoDecoder中增加接口。是一个跨平台IPC框架,它诞生于chromium,用来实现chromium进程内/进程间的通信。中得到frames_rendered值之后,然后通过callback调用,把这个值给传出去。的定义,可以看到这个方法是纯虚函数,需要自己实现具体的功能,函数参数变成这个。

技术笔记 1544

【护网必备】Windows平台shellcode免杀加载器

bypass:McAfee、金山(数字签名)、火绒、ESET、卡巴企业版(时间5s 抖动50 Sleep_mask)、360全家桶(物理机)、Norton(加白程序资源)、WDF(PC)、WDF(Server)、Symantec(加白程序资源)bypass:金山(数字签名)、火绒、ESET、360全家桶(物理机)、WDF(PC)、WDF(服务器提示复查(关云保护))、McAfee、Norton(加白程序资源)、卡巴企业版(时间5s 抖动50 Sleep_mask)必须使用Bof操作,shell运行就掉线。

Fly_鹏程万里 1185

chromium线程模型(1)--- 官方文档

这段代码主要介绍了Chrome浏览器的多进程和多线程架构。Chrome具有多进程架构,每个进程都是高度多线程的。本文将介绍每个进程共享的基本线程系统。主要目标是保持主线程(在浏览器进程中称为“UI”线程)和IO线程(每个进程用于接收IPC的线程)响应灵敏。这意味着将任何阻塞的I/O或其他昂贵的操作卸载到其他线程。我们的方法是使用消息传递作为线程之间通信的方式。我们不鼓励使用锁定和线程安全的对象。相反,对象只存在于一个(通常是虚拟的——稍后我们将讨论!)线程上,我们通过这些线程之间传递消息进行通信。

woai110120130的专栏 1602

SAP B1中文培訓教材_SDK

公司内部培训资料,对开发工具 ,DIAPI, UIAPI,Add-ON有详细解说

ssm框架搭建

这个是一个在idea上成功跑起来的ssm框架项目,保存在这里方便以后使用直接下载

Chromium 回调设计实战:BindOnce 与 BindRepeating 的最佳实践

在Chromium的base库中,base::BindOnce和base::BindRepeating是两种用于创建回调的模板函数,主要区别在于调用语义和所有权模型。base::BindOnce生成的base::OnceCallback只能调用一次,适合一次性操作如异步任务,且通过移动语义传递参数,性能更高效。base::BindRepeating生成的base::RepeatingCallback可多次调用,适合重复事件如按钮点击,但可能有额外开销。选择时,优先使用base::BindOnce,除非需要多

John_ToStr的博客 1438

Probability and Statistics - 4th edition

Probability and Statistics - 4th edition

chrome怎么调用硬件_利用ASLR薄弱点:Chrome沙箱逃逸漏洞分析

概述我们在Chrome中,发现了一系列可能存在的沙箱逃逸漏洞。在此之后,我们认为可以将这些漏洞中的一个与渲染器存在的漏洞共同利用,形成一条完整的漏洞利用链,同时也能够让我们更好地理解现代Chrome漏洞利用所需的机制。考虑到全部可用的漏洞,最有可能被利用的是Issue 1755,这一Use-After-Free漏洞与经典的JavaScript引擎回调漏洞相似。目前看来,这是一个很好的候...

weixin_39611725的博客 522

假人有限元模型

假人模型,车辆碰撞假人有限元模型,适合做乘员安全的研究者利用

chromium源码中的callback

chromium

qq_45329237的博客 1724

Chrome base 库详解:工具类和常用类库

本文深入解析了Chrome的base库,详细介绍了其中的工具类和常用类库。这些类库为开发者提供了一系列强大的工具和功能,帮助他们更有效地进行Chrome开发。

陆业聪 3509

Callback<> and Bind()

Callback&lt;&gt; and Bind() Introduction The templatedbase::Callback&lt;&gt;class is a generalized function object. Together with thebase::Bind()function in base/bind.h, they provide a ...

weixin_34167043的博客 659

FrameSink(原网页绘图表面Output Surface)创建

站在老罗的肩膀上:https://blog.youkuaiyun.com/luoshengyang/article/details/50995124 CC模块内部的状态机一旦检测到网页的Layer Tree创建和初始化完毕,就会通知调度器触发一个创建绘图表面的操作。CC模块在为网页创建绘图表面的过程中,也有伴随着网页分块管理器、资源池和光栅化工作者线程池等基础设施的创建。一旦这些基础设施准备完毕,网页才能开...

tornmy的博客 905

Chrome中的线程与任务

原文链接:https://chromium.googlesource.com/chromium/src/+/master/docs/threading_and_tasks.md Chrome具有多进程体系结构,并且每个进程都是高度多线程的。 在本文档中,我们将介绍每个进程共享的基本线程系统。 主要目标是使主线程(在浏览器进程中也称为“ UI”线程)和IO线程(用于处理IPC的每个进程的线程)保持响应。 这意味着将任何阻塞的I / O或其他昂贵的操作卸载到其他线程。 我们的方...

LIJIWEI0611的博客 2759

C++11 std::bind std::function 高级用法

C++11 std::bind std::function 高级用法

噬日者 5万+

chromium - base::Bind类成员函数

看到资料上演示base::Bind类成员函数,试试。不能跑的实现都是假的。

博客标题不能为空 2912

深入 C++ 回调

许多面试官会问:你知道回调吗?你在写回调的时候遇到哪些坑?你知道对象生命周期管理吗?为什么这里会崩溃,那里会泄漏? 在设计 C++ 回调时,你是否想过:同步还是异步?回调...

腾讯技术工程 1749

AngularJS中的一次性数据绑定 (bindonce)

一、理解双向数据绑定和监听器 为了实现双向数据绑定,AngularJS使用了$watch API来观察期作用域中的模型变化。具体的作用域取决于你的代码如何编写。如果你没有创建一个自作用域,就是说没有使用ngController指令在你的DOM和你的控制器代码之间创建一个关联,你可能处理的是跟作用域$rootScope,这个作用域由ngApp自动创建,并且是应用中所有作用域的父作用域,当然

js_admin的博客 1149

贪心算法——最大整数问题详解

贪心算法——最大整数问题详解: [最大整数]设有n个正整数,将它们连接成一排,组成一个最大的多位整数。 例如:n=3时,3个整数13,312,343,连成的最大整数为34331213。 又如:n=4时,4个整数7,13,4,246,连成的最大整数为7424613。 输入:n N个数 输出:连成的多位数 算法分析:此题很容易想到使用贪心法,在考试时有很多

John_ToStr的博客 7773
上一篇: 引擎深处的漫游者:构建浏览器JavaScript引擎的哲学与技艺
下一篇: Chromium安全架构深度解析:从悬空指针检测到内存安全防御体系
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

打赏作者

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

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

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

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

打赏作者

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

抵扣说明:

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

余额充值