犀牛鸟 2026 研究笔记 estelledc.github.io

Ch13: ONNX Runtime——标准集装箱港口

Part 3: 六大框架架构精讲 前置章节:Ch12: MNN——端侧瑞士军刀 后续章节:Ch14: TNN——ncnn 的架构翻新版


一句话定位

ONNX Runtime(简称 ORT)是六大引擎中唯一一个”标准先行”的框架:它不发明自己的模型格式,而是直接消费 ONNX 标准模型;它不绑定特定硬件优化策略,而是通过 Execution Provider(EP)插件让硬件厂商自己写加速后端。它就像一个国际集装箱港口——不管你的货物从哪个国家来(PyTorch、TensorFlow、PaddlePaddle),只要装进标准集装箱(ONNX 格式),这个港口都能接收、分拣、高效转运。

GitHub 21K star,微软开源,约 8000 个源文件——这个规模比前面所有移动端引擎加在一起还大,因为 ORT 的野心不仅仅是移动端,它要覆盖从云端到边缘到端侧的全场景。


类比:国际集装箱港口

在集装箱被发明之前,港口运输是一件极其低效的事情。每个国家的货物有不同的包装方式,每个港口有不同的装卸设备,每种货物需要不同的处理方式。一艘船到港后,工人需要一件一件地把货物搬下来,按照类型分类,再装上不同的卡车运走。整个过程慢、贵、容易出错。

集装箱改变了一切。不管你运的是电子产品、服装还是食品,统统装进标准尺寸的集装箱。港口只需要标准化的龙门吊和集装箱卡车,就能处理任何货物。你甚至可以在不同港口之间转运——因为所有港口用的都是同一种集装箱标准。

ORT 在推理引擎世界中扮演的就是这个”标准化港口”的角色:

类比边界:港口的类比强调的是 ORT 的”标准化”和”插件化”。但真正的港口是被动接收货物的,而 ORT 的 EP 竞价机制是主动的——多个 EP 会”竞价”每个计算节点的执行权,这更像一个拍卖行而不是港口。后面会详细讲这个机制。


设计哲学精讲

哲学一:Execution Provider 插件——竞价机制认领节点

ORT 最核心的架构创新是 Execution Provider(EP) 插件体系。这是它和所有其他移动端引擎最根本的区别。

在 ncnn/MNN/TNN 中,后端(CPU/GPU)是引擎的”内置组件”——引擎代码和后端代码紧密耦合,加一个新后端需要修改引擎的核心代码。在 ORT 中,后端是”插件”——每个 EP 是一个独立的动态库(或静态库),引擎通过标准接口和 EP 交互,加一个新后端不需要改引擎的一行代码。

EP 的工作方式像一个”竞价”过程:

模型加载后,ORT 把计算图中的每个节点(Op)拿出来"拍卖":

节点 1: Conv2D
  → TensorRT EP: "我能执行,而且我有 GPU 加速"  [竞价]
  → CUDA EP: "我也能执行"                        [竞价]  
  → CPU EP: "我也能执行(但最慢)"                [竞价]
  → 结果:TensorRT EP 中标(优先级最高)

节点 2: CustomOp
  → TensorRT EP: "我不认识这个 Op"                [放弃]
  → CUDA EP: "我不认识"                           [放弃]
  → CPU EP: "我能执行"                            [竞价]
  → 结果:CPU EP 中标(唯一能执行的)

节点 3: MatMul
  → TensorRT EP: "我能执行"                       [竞价]
  → 结果:TensorRT EP 中标

竞价的结果是模型图被”分区”(partition):同一个 EP 认领的连续节点被合并成一个子图,由该 EP 一起执行。不同 EP 的子图之间需要插入数据搬运节点(比如 CPU 和 GPU 之间的数据传输)。

// Execution Provider 接口(简化)
class IExecutionProvider {
public:
    // 返回这个 EP 的名字(如 "CUDAExecutionProvider")
    virtual const std::string& Type() const = 0;
    
    // 竞价:EP 查看图中所有节点,返回它能执行的节点列表
    virtual std::vector<std::unique_ptr<ComputeCapability>>
    GetCapability(const GraphViewer& graph,
                  const IKernelLookup& kernel_lookup) const;
    
    // 编译:把认领的节点子图编译成可执行的内核
    virtual common::Status Compile(
        const std::vector<FusedNodeAndGraph>& fused_nodes,
        std::vector<NodeComputeInfo>& node_compute_funcs);
    
    // 内存管理
    virtual AllocatorPtr GetAllocator(OrtMemType mem_type) const = 0;
};

这种插件架构的威力在于:硬件厂商可以自己开发 EP,把自己的硬件加速能力”插入”ORT,而不需要修改 ORT 的核心代码。英伟达开发了 TensorRT EP 和 CUDA EP,高通开发了 QNN EP(支持 Hexagon DSP 和 Adreno GPU),苹果开发了 CoreML EP,华为可以开发 CANN EP。每个 EP 只需要实现 IExecutionProvider 接口,就能无缝集成到 ORT 中。

CPU EP(也叫 Default EP)是 ORT 的”兜底”选择——如果没有任何加速 EP 能处理某个节点,CPU EP 总能执行它。这保证了 ORT 的鲁棒性:任何合法的 ONNX 模型在 ORT 上都能跑,即使没有任何加速硬件。

哲学二:Tensor——最薄的数据容器

ORT 的 Tensor 设计和 ncnn/MNN 形成了鲜明对比。ncnn 的 Mat 自己管理内存(内置引用计数和分配器),MNN 的 Tensor 更是一个”胖描述符”(内含 Region、设备信息等)。ORT 的 Tensor 则是最薄的——它不拥有内存

// ORT 的 Tensor(简化)
class Tensor {
    void* p_data_;               // 数据指针(不拥有)
    TensorShape shape_;          // 形状
    MLDataType dtype_;           // 数据类型
    OrtMemoryInfo* alloc_info_;  // 内存位置描述
    int64_t byte_offset_;        // 在底层 buffer 中的偏移

    // 注意:没有引用计数、没有分配器、没有 Region
    // Tensor 只是一个指向别人管理的内存的"视图"
};

Tensor 不负责分配和释放内存——这些工作由 OrtAllocator 和执行框架(ExecutionFrame)负责。Tensor 只知道”我的数据在哪里、什么形状、什么类型”。

这种”最薄”的设计有几个重要好处:

第一,零拷贝的外部数据接入。用户可以直接把自己分配的内存包装成 ORT Tensor,不需要拷贝数据到 ORT 的内部 buffer。在移动端,这意味着可以直接把相机帧缓冲区作为模型输入,省掉一次内存拷贝。

第二,灵活的内存管理策略。因为 Tensor 不绑定特定的内存管理方式,不同的 EP 可以用不同的内存分配器——CPU EP 用 malloc,CUDA EP 用 cudaMalloc,TensorRT EP 用 TensorRT 自己的内存管理。上层代码不需要知道底层用的是什么分配器。

第三,内存复用更灵活。ORT 的执行框架可以在运行时决定两个 Tensor 是否共享同一块内存(如果它们的生命周期不重叠),而 Tensor 本身不需要做任何修改。

哲学三:OpKernel + KernelRegistry——opset 版本化

ORT 的算子体系有一个独特的维度:opset 版本化。ONNX 标准定义了多个版本的算子集(opset),每个版本可能对同一个算子(如 Conv)有不同的语义定义。比如 opset 1 的 Conv 和 opset 11 的 Conv 在 padding 的处理方式上有区别。

ORT 通过 KernelRegistry 管理不同 opset 版本的算子实现:

// OpKernel 基类(简化)
class OpKernel {
public:
    virtual Status Compute(OpKernelContext* context) const = 0;
    const OpKernelInfo& Info() const { return info_; }
};

// KernelRegistry: 算子注册表
class KernelRegistry {
public:
    // 注册一个内核:指定 Op 名字、opset 范围、数据类型、后端
    Status Register(KernelCreateInfo&& create_info);
    
    // 查找内核:给定 Op 名字和 opset 版本,找到匹配的实现
    Status TryFindKernel(const Node& node,
                          ProviderType exec_provider,
                          const KernelCreateInfo** out) const;
};

// 注册示例
ONNX_CPU_OPERATOR_VERSIONED_KERNEL(
    Conv,                    // Op 名字
    1, 10,                   // opset 版本范围:1 到 10
    KernelDefBuilder()
        .TypeConstraint("T", DataTypeImpl::GetTensorType<float>()),
    Conv<float>              // 实现类
);

ONNX_CPU_OPERATOR_KERNEL(
    Conv,                    // Op 名字
    11,                      // opset 版本:11+
    KernelDefBuilder()
        .TypeConstraint("T", DataTypeImpl::GetTensorType<float>()),
    Conv11<float>            // 不同的实现类(处理新的 padding 语义)
);

这意味着同一个 Op 名字可以有多个实现,ORT 根据模型指定的 opset 版本选择正确的实现。这保证了向后兼容性——一个用 opset 7 导出的旧模型和一个用 opset 17 导出的新模型,在 ORT 上都能正确运行,因为 ORT 为不同的 opset 版本注册了不同的内核实现。

ncnn/MNN/TNN 都不需要处理这个问题,因为它们有自己的模型格式(ncnn 的 .param、MNN 的 .mnn、TNN 的 .tnnproto),模型转换工具在转换时已经统一了算子语义。ORT 必须处理这个问题,因为它直接消费 ONNX 模型,而 ONNX 标准有版本演进。

哲学四:InferenceSession 图分区——模型的”分拣”过程

ORT 的推理从创建 InferenceSession 开始。这个过程比 ncnn 的 Net::load_param 或 MNN 的 Interpreter::createSession 复杂得多,因为它要完成一个关键步骤:图分区(Graph Partitioning)

InferenceSession 创建流程:

1. 加载 ONNX 模型(反序列化 protobuf)
   └─ 得到原始计算图

2. 图优化(Graph Transformer)
   └─ 算子融合(Conv+BN+ReLU -> FusedConv)
   └─ 常量折叠
   └─ 冗余节点消除

3. 图分区(按 EP 优先级)
   └─ 遍历注册的 EP(按优先级排序)
   └─ 每个 EP 通过 GetCapability 认领节点
   └─ 认领的连续节点合并为子图
   └─ 子图之间插入数据传输节点

4. 内核绑定
   └─ 为每个节点在对应 EP 的 KernelRegistry 中找到 OpKernel
   └─ 绑定完成后,每个节点都有了具体的执行函数

5. 内存规划
   └─ 分析 tensor 的生命周期
   └─ 决定内存复用策略
// InferenceSession 创建(概念简化)
class InferenceSession {
    Status Initialize() {
        // 1. 加载模型
        Model model;
        LoadModel(model_path_, model);
        Graph& graph = model.MainGraph();
        
        // 2. 图优化
        GraphTransformerManager transformer_mgr;
        transformer_mgr.Register(
            std::make_unique<ConvBNFusion>());
        transformer_mgr.Register(
            std::make_unique<ConstantFolding>());
        transformer_mgr.ApplyTransformers(graph);
        
        // 3. 图分区
        for (auto& ep : execution_providers_) {
            auto capabilities = ep->GetCapability(
                GraphViewer(graph), kernel_lookup);
            // 把认领的节点合并为子图
            PartitionGraph(graph, capabilities);
        }
        
        // 4. 内核绑定
        for (auto& node : graph.Nodes()) {
            auto kernel = LookupKernel(node, ep);
            session_state_.AddKernel(node, kernel);
        }
        
        // 5. 内存规划
        MemoryPatternPlanner planner;
        planner.Plan(graph, session_state_);
        
        return Status::OK();
    }
};

图分区后的模型看起来像这样:

原始模型:
  Input -> Conv -> BN -> ReLU -> Resize -> Conv -> Softmax -> Output

分区后 (假设有 TensorRT EP 和 CPU EP):
  [TensorRT 子图: Conv -> BN -> ReLU] 
    -> [数据传输: GPU->CPU]
    -> [CPU 子图: Resize(TensorRT 不支持这种 Resize)]
    -> [数据传输: CPU->GPU]
    -> [TensorRT 子图: Conv -> Softmax]
    -> Output

这个分区过程对用户完全透明——用户只需要注册 EP,ORT 自动决定每个节点在哪里执行。如果 TensorRT EP 能处理整个模型,整个图就在 GPU 上跑;如果有些节点 TensorRT 不支持,ORT 自动插入 CPU 回退和数据传输。

哲学五:ONNX 标准直用——不发明新格式

这是 ORT 和所有其他引擎最表面的区别:ORT 直接加载和执行 ONNX 模型,不需要额外的模型转换步骤

ncnn 需要 onnx2ncnn,MNN 需要 MNNConvert,TNN 需要 onnx2tnn,Paddle-Lite 需要 opt 工具。这些转换工具把通用的 ONNX 模型转换为各自引擎的私有格式。转换过程中可能丢失信息、引入兼容性问题,而且增加了部署流程的复杂度。

ORT 的做法是:ONNX 模型直接用。用户用 PyTorch 导出一个 ONNX 模型,直接丢给 ORT 就能跑。

# PyTorch 导出 ONNX
torch.onnx.export(model, dummy_input, "model.onnx", opset_version=17)

# ORT 直接加载(不需要转换!)
import onnxruntime as ort
session = ort.InferenceSession("model.onnx")
output = session.run(None, {"input": data})

这种”零转换”体验的代价是:ORT 需要内置 ONNX protobuf 的解析器(增加了对 protobuf 库的依赖),而且需要支持 ONNX 标准中定义的所有算子和所有 opset 版本(工作量巨大)。但好处也很明显:最小化部署摩擦。在企业环境中,”不需要额外的转换步骤”本身就是一个巨大的优势——少一步转换就少一个出错点、少一个需要维护的工具。

哲学六:EP 动态库插件——运行时加载后端

ORT 的 EP 不仅可以在编译时静态链接,还可以作为动态库在运行时加载。这意味着你可以发布一个不含 GPU 支持的 ORT 基础包,然后根据用户设备的硬件能力,在运行时动态加载对应的 EP 动态库。

// 运行时注册 EP(C API)
OrtSessionOptions* session_options;
OrtSessionOptionsAppendExecutionProvider_CUDA(session_options, 0);
// 或者加载自定义 EP 动态库
OrtSessionOptionsAppendExecutionProvider(
    session_options, "MyCustomEP", keys, values, num_keys);

这种动态库插件机制让 ORT 特别适合”平台化”部署:一个 ML 平台只需要集成 ORT 核心,然后根据不同的部署环境(有 GPU 的服务器、只有 CPU 的边缘设备、有 NPU 的手机)加载不同的 EP 插件。


核心数据结构:Tensor

ORT 的 Tensor 是六大框架中最”薄”的:

// ORT 的 Tensor(简化)
class Tensor final {
    void* p_data_;               // 数据指针(不拥有这块内存)
    TensorShape shape_;          // 形状 {batch, channel, height, width}
    const DataTypeImpl* dtype_;  // 数据类型(float, int8, ...)
    std::shared_ptr<IAllocator> allocator_;  // 分配器(可选)
    OrtMemoryInfo location_;     // 内存位置描述
    int64_t byte_offset_;        // 偏移量
    gsl::span<int64_t> strides_; // 步长(可选)
};

和其他框架的对比:

特性 ncnn Mat MNN Tensor ORT Tensor
拥有内存 是(引用计数) 可能(取决于 MemoryType)
数据类型 隐式(通过 elemsize) 显式(halide_type_t) 显式(DataTypeImpl)
通道打包 elempack 字段 NC4HW4 约定 无(标准 NCHW)
设备信息 无(CPU=Mat, GPU=VkMat) 内含 OrtMemoryInfo
视图/Region 有(MEMORY_VIRTUAL) 无(通过 offset/stride 实现)

ORT Tensor 不做通道打包(没有 elempack 或 NC4HW4)——这是因为 ORT 遵循 ONNX 标准的数据布局(默认 NCHW)。如果某个 EP 需要打包布局(比如 ARM NEON 后端需要 NC4HW4),那是 EP 内部的事情——EP 在内部做布局转换,对 ORT 核心透明。


推理流程

阶段一:创建 Session

Ort::Env env(ORT_LOGGING_LEVEL_WARNING, "test");
Ort::SessionOptions session_options;

// 注册 EP(按优先级顺序)
OrtSessionOptionsAppendExecutionProvider_CUDA(session_options, 0);
// CPU EP 默认存在,不需要手动注册

// 创建 Session(触发图分区和内核绑定)
Ort::Session session(env, "model.onnx", session_options);

阶段二:准备输入

auto memory_info = Ort::MemoryInfo::CreateCpu(
    OrtArenaAllocator, OrtMemTypeDefault);

std::vector<float> input_data(3 * 224 * 224);
// ... 填充数据 ...

std::vector<int64_t> input_shape = {1, 3, 224, 224};
Ort::Value input_tensor = Ort::Value::CreateTensor<float>(
    memory_info, input_data.data(), input_data.size(),
    input_shape.data(), input_shape.size());

注意这里的关键:CreateTensor 传入的是用户自己分配的 input_data 的指针,不做内存拷贝。ORT 的 Tensor 只是包装了这个指针——这就是”Tensor 不拥有内存”设计的直接体现。

阶段三:执行推理

const char* input_names[] = {"input"};
const char* output_names[] = {"output"};

auto output_tensors = session.Run(
    Ort::RunOptions{nullptr},
    input_names, &input_tensor, 1,
    output_names, 1);

Run 的内部流程:按照图分区的结果,依次执行每个 EP 的子图。如果两个子图在不同的 EP 上(比如第一个在 GPU,第二个在 CPU),中间自动插入数据搬运。

阶段四:读取输出

float* output_data = output_tensors[0].GetTensorMutableData<float>();
auto output_shape = output_tensors[0].GetTensorTypeAndShapeInfo().GetShape();

关键差异:ORT 与其他框架

vs ncnn/MNN/TNN:标准 vs 私有

ORT 直接消费 ONNX 模型,其他三个需要格式转换。这不仅是工程便利性的区别,更是生态位的区别:ORT 是 ONNX 标准的”参考实现”,是连接训练框架和各种硬件的”中间件”;ncnn/MNN/TNN 是面向特定场景优化的”终端引擎”。

vs ncnn:抽象层次

ncnn 的代码几乎没有抽象层——Layer 基类加上具体实现,一共两层。ORT 有 IExecutionProvider、OpKernel、KernelRegistry、ExecutionFrame、SequentialExecutionPlan 等多层抽象。ncnn 的优势是简单可控,ORT 的优势是灵活可扩展。

vs MNN:编译器 vs 分区

MNN 用 Geometry Compiler 把非计算 Op 降级为 Region;ORT 没有类似机制——它把所有 Op 都交给 EP 处理,由 EP 决定怎么优化。MNN 的方法在框架层面解决了跨层优化问题,ORT 把这个问题下放给了 EP(每个 EP 可以在自己的子图内做优化)。


适用场景与局限

最适合 ORT 的场景

企业级 ML 平台。如果你需要一个能在服务端(CUDA/TensorRT)、边缘(ARM CPU/Qualcomm DSP)和端侧(Android/iOS)统一使用的推理引擎,ORT 是最好的选择。一套代码、一套 API、一套模型格式,通过 EP 适配不同硬件。

ONNX 模型的直接部署。如果你的团队已经用 ONNX 作为模型交换格式,ORT 提供了最无缝的部署体验——不需要额外的模型转换步骤。

硬件厂商的推理加速集成。硬件厂商可以通过实现 EP 接口,把自己的加速能力集成到 ORT 中,获得 ORT 庞大生态的流量。这比让用户学习一个全新的推理框架要容易得多。

ORT 的局限

移动端优化深度不如 ncnn/MNN。ORT 在纯 ARM CPU 推理的性能基准测试中,通常不如 ncnn(手写汇编的优势)和 MNN(Geometry Compiler 的优势)。ORT 的 CPU EP 使用的是标准的算子实现(部分使用 MLAS 库做矩阵运算),没有 ncnn 那种极致的手工优化。

二进制体积大。完整编译的 ORT 库在 ARM64 上约 8-15 MB,远大于 ncnn 的约 700KB。虽然可以通过裁剪减小体积(ORT Mobile),但整体还是偏大。在嵌入式设备上可能不可接受。

复杂度高。约 8000 个文件、多层抽象、模板元编程——ORT 的代码复杂度在六大框架中遥遥领先。如果你的目标是”学习推理引擎原理”,ORT 绝对不是一个好的起点。

依赖 protobuf。因为 ONNX 模型格式基于 protobuf,ORT 必须链接 protobuf 库。这增加了编译的复杂度和最终的二进制体积,和 ncnn 的”零依赖”形成鲜明对比。

缺少端侧 LLM 和训练支持。虽然 ORT 有 ORT Training(服务端训练加速),但缺少像 MNN 那样的端侧 LLM 推理优化和端侧微调能力。


代码结构

ORT 的源码是六大框架中最庞大的,约 8000 个文件:

onnxruntime/
├── core/
│   ├── framework/                    # 核心框架
│   │   ├── tensor.h                  # Tensor 数据结构
│   │   ├── op_kernel.h               # OpKernel 基类
│   │   ├── kernel_registry.h         # 算子注册表
│   │   ├── execution_frame.h         # 执行帧(管理运行时状态)
│   │   ├── execution_provider.h      # EP 接口
│   │   ├── sequential_execution_plan.h
│   │   └── ...
│   │
│   ├── session/                      # 推理会话
│   │   ├── inference_session.h       # 核心入口
│   │   └── ...
│   │
│   ├── graph/                        # 图表示
│   │   ├── graph.h                   # ONNX 图
│   │   ├── graph_transformer.h       # 图优化
│   │   └── ...
│   │
│   ├── optimizer/                    # 图优化 pass
│   │   ├── conv_bn_fusion.h
│   │   ├── constant_folding.h
│   │   └── ...
│   │
│   ├── providers/                    # 各种 EP
│   │   ├── cpu/                      # CPU EP(默认)
│   │   │   ├── cpu_execution_provider.h
│   │   │   ├── math/
│   │   │   │   ├── matmul.cc
│   │   │   │   └── ...
│   │   │   ├── nn/
│   │   │   │   ├── conv.cc
│   │   │   │   └── ...
│   │   │   └── ...
│   │   ├── cuda/                     # CUDA EP
│   │   ├── tensorrt/                 # TensorRT EP
│   │   ├── coreml/                   # CoreML EP (iOS)
│   │   ├── nnapi/                    # NNAPI EP (Android NPU)
│   │   ├── qnn/                      # 高通 QNN EP
│   │   └── ...
│   │
│   └── mlas/                         # MS线性代数子程序库
│       ├── lib/
│       │   ├── sgemm.cpp             # 单精度矩阵乘
│       │   ├── convolve.cpp          # 卷积实现
│       │   └── ...
│       └── inc/
│
├── contrib_ops/                      # 社区贡献的算子
│   ├── cpu/
│   └── cuda/
│
└── test/                             # 测试

关键阅读路径:

  1. 入门core/framework/tensor.h -> core/framework/op_kernel.h -> core/framework/execution_provider.h
  2. 理解 Sessioncore/session/inference_session.hInitialize 函数,图分区的入口)
  3. 理解图优化core/optimizer/ 目录下的各个 pass
  4. 理解 EPcore/providers/cpu/cpu_execution_provider.h(最简单的 EP)
  5. 理解 MLAScore/mlas/(ORT 的高性能数学库,类似 ncnn 的手写内核但用 intrinsics 而不是汇编)

小结

ORT 的设计哲学可以用一个词概括:标准化。它不发明新的模型格式(用 ONNX),不绑定特定硬件(用 EP 插件),不假设特定场景(从云到端全覆盖)。每一个设计决策都指向同一个目标:做一个万物互联的中间层,让任何模型能在任何硬件上跑

它的核心创新 Execution Provider 竞价机制,解决了”一个引擎怎么适配无限多种硬件”的问题:不是引擎去适配硬件,而是让硬件通过 EP 接口来适配引擎。这种”反转控制”的思路,和 ncnn/MNN 的”引擎内置所有后端”形成了完全不同的架构范式。

它的 Tensor 是六大框架中最薄的——不拥有内存、不做通道打包、不含 Region 描述——极简到只剩一个数据指针和形状描述。这种极简让它可以和任何外部内存管理系统对接,但也意味着它不像 ncnn 的 Mat 或 MNN 的 Tensor 那样”开箱即用”。

对于犀牛鸟竞赛的参赛者来说,ORT 不太适合作为学习推理引擎内部原理的入门材料(太复杂了),但它的 EP 架构思想值得学习——当你的引擎需要支持新硬件时,”插件化”的思路比”硬编码”要优雅得多。

下一章,我们将看到 TNN 如何在 ncnn 的基础上做”架构翻新”——用 BaseLayer/LayerAcc 双层设计解决 ncnn 的可维护性问题。


导读目录:README.md 上一章:Ch12: MNN——端侧瑞士军刀 下一章:Ch14: TNN——ncnn 的架构翻新版