基于 cronet 的单链接性能信息收集

背景

公司的一款基于网络云盘的产品,需要统计每个链接到各个服务器节点的性能,以便后台做更优的调度。常用的性能指标有 DNS 解析耗时、连接耗时、ssl 握手耗时、首分片耗时、总的发送接收字节数、总的请求耗时以及基于它们计算的平均速度等。早先的基于 boost 的版本这些都很好统计,后来该产品底层网络库换成 cronet 就不好统计了,我的工作就是基于 cronet 重新收集上述性能信息。

cronet 网络编程

正式开搞前先简单看下 cronet 网络编程范式与之前有何不同:

/* by 01022.hk - online tools website : 01022.hk/zh/blood.html */
#include <string>
#include <thread>
#include <iostream>
#include <cronet/cronet_c.h>

// 回调:重定向
void on_redirect_received(Cronet_UrlRequestCallback* callback,
                         Cronet_UrlRequest* request,
                         Cronet_UrlResponseInfo* info,
                         const char* new_location) {
    std::cout << "Redirect to: " << new_location << std::endl;
    Cronet_UrlRequest_FollowRedirect(request);
}


// 回调:响应开始
void on_response_started(Cronet_UrlRequestCallback* callback,
                        Cronet_UrlRequest* request,
                        Cronet_UrlResponseInfo* info) {
    std::cout << "Response started" << std::endl;
    Cronet_Buffer* buffer = Cronet_Buffer_Create();
    Cronet_Buffer_InitWithAlloc(buffer, 4096); // 4KB缓冲区
    Cronet_UrlRequest_Read(request, buffer);
}

// 回调:读取完成
void on_read_completed(Cronet_UrlRequestCallback* callback,
                      Cronet_UrlRequest* request,
                      Cronet_UrlResponseInfo* info,
                      Cronet_Buffer* buffer,
                      uint64_t bytes_read) {
    // 处理数据
    if (bytes_read > 0) {
        const char* data = static_cast<const char*>(Cronet_Buffer_GetData(buffer));
        std::cout << "Read " << bytes_read << " bytes" << std::endl;
        std::cout << data << std::endl; 
    }
    
    // 释放当前buffer
    Cronet_Buffer_Destroy(buffer);

    // 继续读取(如果还有数据且未完成)
    if (bytes_read > 0) {
        Cronet_Buffer* new_buffer = Cronet_Buffer_Create();
        Cronet_Buffer_InitWithAlloc(new_buffer, 4096);
        Cronet_UrlRequest_Read(request, new_buffer);
    } else {
        std::cout << "Read completed" << std::endl;
    }
}

// 回调:请求成功
void on_succeeded(Cronet_UrlRequestCallback* callback,
                 Cronet_UrlRequest* request,
                 Cronet_UrlResponseInfo* info) {
    std::cout << "Request succeeded" << std::endl;
}

// 回调:请求失败
void on_failed(Cronet_UrlRequestCallback* callback,
              Cronet_UrlRequest* request,
              Cronet_UrlResponseInfo* info,
              Cronet_Error* error) {
    std::cout << "Request failed" << std::endl;
}

// 回调:取消
void on_canceled(Cronet_UrlRequestCallback* callback,
                Cronet_UrlRequest* request,
                Cronet_UrlResponseInfo* info) {
    std::cout << "Request cancelled" << std::endl;
}

// Executor
void executor_func(Cronet_Executor *executor, Cronet_Runnable *runnable) {
    Cronet_Runnable_Run(runnable);
}

int main() {
    // 1. 创建引擎
    Cronet_EnginePtr engine = Cronet_Engine_Create();
    Cronet_EngineParamsPtr params = Cronet_EngineParams_Create();
    Cronet_Engine_StartWithParams(engine, params);
    
    // 3. 创建回调
    Cronet_UrlRequestCallbackPtr callback = Cronet_UrlRequestCallback_CreateWith(
        on_redirect_received,  // 重定向回调
        on_response_started,
        on_read_completed,
        on_succeeded,
        on_failed,
        on_canceled
    );
    
    // 4. 配置请求
    Cronet_UrlRequestParamsPtr req_params = Cronet_UrlRequestParams_Create();
    Cronet_UrlRequestParams_http_method_set(req_params, "GET");
    
    // 添加请求头
    Cronet_HttpHeaderPtr header = Cronet_HttpHeader_Create();
    Cronet_HttpHeader_name_set(header, "User-Agent");
    Cronet_HttpHeader_value_set(header, "Cronet-C-Client");
    Cronet_UrlRequestParams_request_headers_add(req_params, header);
    
    // 5. 创建执行器
    Cronet_ExecutorPtr executor = Cronet_Executor_CreateWith(executor_func);
    
    // 6. 创建并启动请求
    Cronet_UrlRequestPtr request = Cronet_UrlRequest_Create();
    Cronet_UrlRequest_InitWithParams(request, engine, 
                                     "http://httpbin.org/get", 
                                     req_params, callback, executor);
    Cronet_UrlRequest_Start(request);
    
    // 7. 等待请求完成
    std::this_thread::sleep_for(std::chrono::seconds(15));
    
    // 8. 清理资源
    Cronet_UrlRequest_Destroy(request);
    Cronet_HttpHeader_Destroy(header);
    Cronet_UrlRequestParams_Destroy(req_params);
    Cronet_Executor_Destroy(executor);
    Cronet_UrlRequestCallback_Destroy(callback);
    Cronet_EngineParams_Destroy(params);
    Cronet_Engine_Destroy(engine);
    
    return 0;
}

上面是 deepseek 生成的 cronet 基于 C 语言的示例,运行后有以下输出:

/* by 01022.hk - online tools website : 01022.hk/zh/blood.html */
$ ./cronet_conn_stat
Response started
Read 279 bytes
{
  "args": {},
  "headers": {
    "Accept-Encoding": "gzip, deflate",
    "Host": "httpbin.org",
    "User-Agent": "Cronet-C-Client",
    "X-Amzn-Trace-Id": "Root=1-69425f84-67dbe33a06e303cf4c611b72"
  },
  "origin": "111.108.111.133",
  "url": "http://httpbin.org/get"
}

Request succeeded

与 libcurl 相比,Cronet_UrlRequest_Start 类似 curl_easy_perform 的角色,但变为异步执行,它会立即返回,之后通过回调不断通知连接上的事件,因此示例中是通过 sleep 15 秒来阻塞主线程的,工程实践中这个完全可以和消息、事件循环集成在一起,从而提高线程并发能力;与 boost 相比 (特别是基于 boost::asio::ip::tcp 版本的实现),完全不需要主动 async_resolve、async_connect、async_handeshake 以及 async_write,只需要在 on_read_completed 回调中无脑 Cronet_UrlRequest_Read 即可,底层过程 cronet 都帮你包办了,达到节省心智负担的目的。

不过这也带来一个问题,就是之前可以手动打桩计算的各种耗时,现在都看不到了,最多能获取个首分片耗时和总请求耗时,其中首分片这还包含了解析、连接、ssl 握手时长的耗时,相对失真了都。

cronet 对链接性能信息的支持

cronet 其实也有接口统计链接层的一些信息,主要通过下面的接口获取:

 ///////////////////////
// Struct Cronet_Metrics.
CRONET_EXPORT Cronet_MetricsPtr Cronet_Metrics_Create(void);
CRONET_EXPORT void Cronet_Metrics_Destroy(Cronet_MetricsPtr self);
...
// Cronet_Metrics getters.
CRONET_EXPORT
Cronet_DateTimePtr Cronet_Metrics_request_start_get(
    const Cronet_MetricsPtr self);
CRONET_EXPORT
Cronet_DateTimePtr Cronet_Metrics_dns_start_get(const Cronet_MetricsPtr self);
CRONET_EXPORT
Cronet_DateTimePtr Cronet_Metrics_dns_end_get(const Cronet_MetricsPtr self);
CRONET_EXPORT
Cronet_DateTimePtr Cronet_Metrics_connect_start_get(
    const Cronet_MetricsPtr self);
CRONET_EXPORT
Cronet_DateTimePtr Cronet_Metrics_connect_end_get(const Cronet_MetricsPtr self);
CRONET_EXPORT
Cronet_DateTimePtr Cronet_Metrics_ssl_start_get(const Cronet_MetricsPtr self);
CRONET_EXPORT
Cronet_DateTimePtr Cronet_Metrics_ssl_end_get(const Cronet_MetricsPtr self);
CRONET_EXPORT
Cronet_DateTimePtr Cronet_Metrics_sending_start_get(
    const Cronet_MetricsPtr self);
CRONET_EXPORT
Cronet_DateTimePtr Cronet_Metrics_sending_end_get(const Cronet_MetricsPtr self);
CRONET_EXPORT
Cronet_DateTimePtr Cronet_Metrics_push_start_get(const Cronet_MetricsPtr self);
CRONET_EXPORT
Cronet_DateTimePtr Cronet_Metrics_push_end_get(const Cronet_MetricsPtr self);
CRONET_EXPORT
Cronet_DateTimePtr Cronet_Metrics_response_start_get(
    const Cronet_MetricsPtr self);
CRONET_EXPORT
Cronet_DateTimePtr Cronet_Metrics_request_end_get(const Cronet_MetricsPtr self);
CRONET_EXPORT
bool Cronet_Metrics_socket_reused_get(const Cronet_MetricsPtr self);
CRONET_EXPORT
int64_t Cronet_Metrics_sent_byte_count_get(const Cronet_MetricsPtr self);
CRONET_EXPORT
int64_t Cronet_Metrics_received_byte_count_get(const Cronet_MetricsPtr self);

主要是通过 Cronet_Metrics_xxx 的接口获取,所需的 dns、connect、ssl、request 耗时都有,耗时是通过接口返回的两个时间做差值得到的,举例来说:

Cronet_DateTimePtr start = Cronet_Metrics_connect_start(metrics);
Cronet_DateTimePtr end = Cronet_Metrics_connect_end(metrics);
if (start && end) {
    int64_t start_ms = Cronet_DateTime_value_get(start);
    int64_t end_ms = Cronet_DateTime_value_get(end);
    int64_t connect = (start_ms > 0 && end_ms > 0) ? (end_ms - start_ms) : 0;
    // printf("connect elapse %lld\n", connect);
}

注意返回的 Cronet_DateTime 对象到毫秒值,还需要调用一个接口,莫法子,C 语言的接口就是这么废柴~

现在的关键落到了如何获取 Cronet_Metrics 对象,发现只有一个接口可以:

// Cronet_RequestFinishedInfo getters.
CRONET_EXPORT
Cronet_MetricsPtr Cronet_RequestFinishedInfo_metrics_get(
    const Cronet_RequestFinishedInfoPtr self);

需要输入 Cronet_RequestFinishedInfo 对象,这又是个什么东东,经过一番搜索,发现唯一途径是通过一个回调:

// The app implements abstract interface Cronet_RequestFinishedInfoListener by
// defining custom functions for each method.
typedef void (*Cronet_RequestFinishedInfoListener_OnRequestFinishedFunc)(
    Cronet_RequestFinishedInfoListenerPtr self,
    Cronet_RequestFinishedInfoPtr request_info,
    Cronet_UrlResponseInfoPtr response_info,
    Cronet_ErrorPtr error);

这个回调又是经由 Cronet_RequestFinishedInfoListener 对象设置的:

///////////////////////
// Abstract interface Cronet_RequestFinishedInfoListener is implemented by the
// app.

// There is no method to create a concrete implementation.

// Destroy an instance of Cronet_RequestFinishedInfoListener.
CRONET_EXPORT void Cronet_RequestFinishedInfoListener_Destroy(
    Cronet_RequestFinishedInfoListenerPtr self);
// Set and get app-specific Cronet_ClientContext.
...
// The app implements abstract interface Cronet_RequestFinishedInfoListener by
// defining custom functions for each method.
typedef void (*Cronet_RequestFinishedInfoListener_OnRequestFinishedFunc)(
    Cronet_RequestFinishedInfoListenerPtr self,
    Cronet_RequestFinishedInfoPtr request_info,
    Cronet_UrlResponseInfoPtr response_info,
    Cronet_ErrorPtr error);
// The app creates an instance of Cronet_RequestFinishedInfoListener by
// providing custom functions for each method.
CRONET_EXPORT Cronet_RequestFinishedInfoListenerPtr
Cronet_RequestFinishedInfoListener_CreateWith(
    Cronet_RequestFinishedInfoListener_OnRequestFinishedFunc
        OnRequestFinishedFunc);

看这个 CreateWith 接口,它的唯一参数就是上面声明的用户回调。这一系列接口其实是创建了一个侦听器,之后还需要关联到引擎才能生效:

void on_request_finished_listener(
    Cronet_RequestFinishedInfoListenerPtr self,
    Cronet_RequestFinishedInfoPtr request_info,
    Cronet_UrlResponseInfoPtr response_info,
    Cronet_ErrorPtr error)
{
}

...

int main() {
    // 1. 创建引擎
    Cronet_EnginePtr engine = Cronet_Engine_Create();
    Cronet_EngineParamsPtr params = Cronet_EngineParams_Create();
    Cronet_Engine_StartWithParams(engine, params);
    ...
    // 5. 创建执行器
    Cronet_ExecutorPtr executor = Cronet_Executor_CreateWith(executor_func);
    ...
    Cronet_RequestFinishedInfoListenerPtr listener = Cronet_RequestFinishedInfoListener_CreateWith(on_request_finished_listener);
    if (listener) {
        Cronet_Engine_AddRequestFinishedListener(engine, listener, executor);
        std::cout << "request finished listener registered" << std::endl;
    }
    else {
        std::cout << "setup request finished listener failed, no connection statistic provided" << std::endl;
    }
    
    ...
        
    // 8. 清理资源
    Cronet_UrlRequest_Destroy(request);
    Cronet_HttpHeader_Destroy(header);
    Cronet_UrlRequestParams_Destroy(req_params);
    
    if (listener) {
        Cronet_Engine_RemoveRequestFinishedListener(engine, listener);
        Cronet_RequestFinishedInfoListener_Destroy(listener);
    }
    
    Cronet_Executor_Destroy(executor);
    Cronet_UrlRequestCallback_Destroy(callback);
    Cronet_EngineParams_Destroy(params);
    Cronet_Engine_Destroy(engine);
    
    return 0;
}

关联侦听器的接口如下:

CRONET_EXPORT
void Cronet_Engine_AddRequestFinishedListener(
    Cronet_EnginePtr self,
    Cronet_RequestFinishedInfoListenerPtr listener,
    Cronet_ExecutorPtr executor);
CRONET_EXPORT
void Cronet_Engine_RemoveRequestFinishedListener(
    Cronet_EnginePtr self,
    Cronet_RequestFinishedInfoListenerPtr listener);

这样整个流程就串起来了:在 Cronet_Engine 初始化时创建并关联一个 Cronet_RequestFinishedInfoListener 对象,该对象持有一个 Cronet_RequestFinishedInfoListener_OnRequestFinished 类型的用户回调,当连接结束时 cronet 会将性能信息通过该回调通知到用户,用户通过回调的第二个参数 request_info 获取链接性能信息,即 Cronet_RequestFinishedInfo -> Cronet_Metrics,再通过后者的一系列接口获取感兴趣的信息。

从整个流程可以看出:

* 性能信息只有在连接关闭时才能获取到

* 性能信息并不是关联到单链接 (Cronet_UrlRequest),而是关联到全局 (Cronet_Engine)

* 可以关联多个 Listener 对象,但感觉没什么必要

性能信息投递

回到业务层面,每个下载任务包含若干链接,在任务结束时 (成功、失败或取消) 对链接信息进行上报,平时这些信息是由链接对象管理的,因此需要将位于全局回调的性能信息进行投递。

用户定义的链接对象一般是关联到 Cronet_UrlRequest,即通过下面的接口:

///////////////////////
// Concrete interface Cronet_UrlRequest.

CRONET_EXPORT void Cronet_UrlRequest_SetClientContext(
    Cronet_UrlRequestPtr self,
    Cronet_ClientContext client_context);
CRONET_EXPORT Cronet_ClientContext
Cronet_UrlRequest_GetClientContext(Cronet_UrlRequestPtr self);

顺便插一句,cronet 中各种对象都支持设置用户数据,命名也非常统一: XXX_Get/SetClientContext。

这样就可以通过 Cronet_UrlRequest 对象找到关联的链接对象,回过头来再看性能信息的回调:

// The app implements abstract interface Cronet_RequestFinishedInfoListener by
// defining custom functions for each method.
typedef void (*Cronet_RequestFinishedInfoListener_OnRequestFinishedFunc)(
    Cronet_RequestFinishedInfoListenerPtr self,
    Cronet_RequestFinishedInfoPtr request_info,
    Cronet_UrlResponseInfoPtr response_info,
    Cronet_ErrorPtr error);

这里提供的不是 Cronet_UrlRequest 而是 Cronet_UrlResponseInfo,两者对不上,于是问题演化为如何通过 Cronet_UrlResponseInfo 找到 Cronet_UrlRequest。

梳理 cronet 请求生命周期:

 

image

一般有这么几条路径:

* onResponseStarted -> onReadCompleted -> onSucceeded

* onCanceled / onResponseStarted -> onCanceled / onResponseStarted -> onReadCompleted -> onCanceled

* onFailed / onResponseStarted -> onReadCompleted -> onFailed

302 重定向就不单独列出了,可以在 follow 和 cancel 中选一种继续。结合相关的回调函数原型观察:

// The app implements abstract interface Cronet_UrlRequestCallback by defining
// custom functions for each method.
typedef void (*Cronet_UrlRequestCallback_OnRedirectReceivedFunc)(
    Cronet_UrlRequestCallbackPtr self,
    Cronet_UrlRequestPtr request,
    Cronet_UrlResponseInfoPtr info,
    Cronet_String new_location_url);
typedef void (*Cronet_UrlRequestCallback_OnResponseStartedFunc)(
    Cronet_UrlRequestCallbackPtr self,
    Cronet_UrlRequestPtr request,
    Cronet_UrlResponseInfoPtr info);
typedef void (*Cronet_UrlRequestCallback_OnReadCompletedFunc)(
    Cronet_UrlRequestCallbackPtr self,
    Cronet_UrlRequestPtr request,
    Cronet_UrlResponseInfoPtr info,
    Cronet_BufferPtr buffer,
    uint64_t bytes_read);
typedef void (*Cronet_UrlRequestCallback_OnSucceededFunc)(
    Cronet_UrlRequestCallbackPtr self,
    Cronet_UrlRequestPtr request,
    Cronet_UrlResponseInfoPtr info);
typedef void (*Cronet_UrlRequestCallback_OnFailedFunc)(
    Cronet_UrlRequestCallbackPtr self,
    Cronet_UrlRequestPtr request,
    Cronet_UrlResponseInfoPtr info,
    Cronet_ErrorPtr error);
typedef void (*Cronet_UrlRequestCallback_OnCanceledFunc)(
    Cronet_UrlRequestCallbackPtr self,
    Cronet_UrlRequestPtr request,
    Cronet_UrlResponseInfoPtr info);

发现它们都提供 Cronet_UrlRequest & Cronet_UrlResponseInfo 两个对象,于是一个大胆的想法诞生了:在所有回调中建立二者的映射关系,最终在侦听器回调中再通过 Cronet_UrlResponseInfo 反查 Cronet_UrlRequest !

这个想法是可行的,特别是 Cronet_RequestFinishedInfoListener_OnRequestFinishedFunc 保证在上述所有回调之后被调用。

整合在一起

假设上述关系通过全局 rr_map 变量映射在一起,那么最终的 listener 回调可以这样实现:

extern on_request_finished(Cronet_ClientContext obj, int64_t connect); 
void on_request_finished_listener(
    Cronet_RequestFinishedInfoListenerPtr self,
    Cronet_RequestFinishedInfoPtr request_info,
    Cronet_UrlResponseInfoPtr response_info,
    Cronet_ErrorPtr error)
{
    int64_t connect = 0; 
    Cronet_MetricsPtr metrics = Cronet_RequestFinishedInfo_metrics(request_info); 
    if (metrics) {
        Cronet_DateTimePtr start = Cronet_Metrics_connect_start(metrics);
        Cronet_DateTimePtr end = Cronet_Metrics_connect_end(metrics);
        if (start && end) {
            int64_t start_ms = Cronet::instance()->Cronet_DateTime_value_get(start);
            int64_t end_ms = Cronet::instance()->Cronet_DateTime_value_get(end);
            connect = (start_ms > 0 && end_ms > 0) ? (end_ms - start_ms) : 0;
        }
    }

    auto it = rr_map.find(response_info); 
    if (it != rr_map.end()) { 
        Cronet_UrlRequestPtr req = it->second;
        Cronet_ClientContext obj = Cronet_UrlRequest_GetClientContext(req); 
        if (obj) {
            on_request_finished(obj, connect);
        }
    }
}

其中 on_request_finished 是用户实现的回调,它的两个参数 obj 和 connect 分别是用户定义的链接层对象与连接耗时。其它的像 dns 耗时、ssl 握手耗时、首分片耗时都可以如法泡制,这里就不一一赘述了。

下面是整合后的示意图:

image

其中演示了 Cronet_UrlResponseInfo 与 Cronet_UrlRequestInfo 之间建立映射的过程,以及整个过程涉及的主要 Cronet 类型和回调。

后记

功能上线后,确实可用,解决了之前 cronet 收集不到链接性能数据的问题

image

其中 connect (xx) 标识的即为连接耗时。

本文完整的 demo 可参考 github 上的 cronet_conn_stat 项目,支持在 mac & windows 上进行验证。

下面是 demo 的一个典型输出:

$ ./cronet_conn_stat
request finished listener registered
Response started
Read 279 bytes
{
  "args": {},
  "headers": {
    "Accept-Encoding": "gzip, deflate",
    "Host": "httpbin.org",
    "User-Agent": "Cronet-C-Client",
    "X-Amzn-Trace-Id": "Root=1-694268b2-27badad81ccc4cdb11ccc5f3"
  },
  "origin": "111.108.111.133",
  "url": "http://httpbin.org/get"
}

request finished listen
request finish, connect elapse 346 ms
Request succeeded

最后一行输出了连接耗时。

早先 deepseek 给的一版示例中,未给 Cronet_Executor 提供回调函数:

    Cronet_ExecutorPtr executor = Cronet_Executor_CreateWith(NULL);

编译正常,但运行到第一个回调时,就会崩溃:

$ ./cronet_conn_stat
request finished listener registered
Segmentation fault: 11

windows 上挂上调度器看甚至有详细的崩溃堆栈:

cronet_crash

明显是在在第一个回调 (on_response_started) 中访问空指针崩了,增加 executor 回调设置后,就正常了。所以有时候 AI 给出的结果也不完全靠谱,还得自己去实际跑跑才行。

github demo 中还有一个开关 (ENABLE_EXECUTOR_THREAD),可以控制是否将各种事件的回调放在一个单独的线程中去执行:

void custom_executor_func(Cronet_Executor *executor, Cronet_Runnable *cronet_task) {
    ExecutorThread* et = (ExecutorThread*)Cronet_Executor_GetClientContext(executor); 
    if (!et) {
        std::cerr << "Executor not initialized!" << std::endl;
        return;
    }

    // 将Cronet的任务包装成std::function
    if (cronet_task) {
        et->postTask([cronet_task]() {
            // 执行Cronet任务
            Cronet_Runnable_Run(cronet_task);
        });
    }
}

为此还引入了一个线程与函数的投递封装类 (ExectorThread),有兴趣的读者可以去研究下。

参考

[1]. blob/main/components/cronet/native/test/url_request_test.cc

[2]. Cronet 请求生命周期

本文来自博客园,作者:goodcitizen,转载请注明原文链接:https://www.cnblogs.com/goodcitizen/p/19253773/connection_performance_information_collection_based_on_cronet

源码地址: https://pan.quark.cn/s/3916362e5d0a 在C#编程平台下,构建一个曲线编辑器是一项融合了图形用户界面(GUI)构建、数据管理及数学运算的应用开发任务。 接下来将系统性地介绍这个曲线编辑器开发过程中的核心知识点:1. **定制曲线面板展示数据曲线**: - 控件选用:在C#的Windows Forms或WPF框架中,有多种控件可用于曲线呈现,例如PictureBox或用户自定义的UserControl。 通过处理重绘事件,借助Graphics对象执行绘图动作,如运用DrawCurve方法。 - 数据图形化:通过线性或贝塞尔曲线连接数据点,以呈现数据演变态势。 这要求掌握直线与曲线的数学描述,例如两点间的直线公式、三次贝塞尔曲线等。 - 坐标系统与缩放比例:构建X轴和Y轴,设定坐标标记,并开发缩放功能,使用户可察看不同区间内的数据。 2. **在时间轴上配置多个关键帧数据**: - 时间轴构建:开发一个时间轴组件,显示时间单位刻度,并允许用户在特定时间点设置关键帧。 时间可表现为连续形式或离散形式,关键帧对应于时间轴上的标识。 - 关键帧维护:利用数据结构(例如List或Dictionary)保存关键帧,涵盖时间戳和关联值。 需考虑关键帧的添加、移除及调整位置功能。 3. **调整关键帧数据,通过插值方法获得曲线**: - 插值方法:依据关键帧信息,选用插值方法(如线性插值、样条插值,特别是Catmull-Rom样条)生成平滑曲线。 这涉及数学运算,确保曲线在关键帧之间无缝衔接。 - 即时反馈:在编辑关键帧时,即时刷新曲线显示,优化用户体验。 4. **曲线数据的输出**: - 文件类型:挑选适宜的文件格式存储数据,例如XML、JSON或...
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值