编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • 性能优化实践

  • 程序编程原理

  • 稳定性与可靠性

  • 工程化与运维

  • 方案设计思想

    • README
    • 架构与组件

    • 数据与存储

    • 通信与协议

    • 稳定性与安全

    • 端侧专项性

      • IoT框架设计方案
      • 动态化技术方案设计
      • 跨端一致性方案
      • 国际化实践方案
      • 离线包与预加载方案
        • 01.5 秒白屏的差评
          • 1.1 用户骂"App 卡爆了"
          • 1.2 性能瓶颈拆解
          • 1.3 反思离线方案
        • 02.要解决的核心矛盾
          • 2.1 秒开是基本盘
          • 2.2 包大小与覆盖
          • 2.3 实时与稳定
          • 2.4 离线包的本质
        • 03.业界主流方案
          • 03.1 主流离线包方案
          • 03.2 横向对比矩阵
          • 03.3 预加载分类
        • 04.设计核心原则
          • 04.1 边走边下原则
          • 04.2 增量更新原则
          • 04.3 安全校验原则
          • 04.4 命中率优先原则
        • 05.方案落地实战
          • 05.1 整体架构
          • 05.2 离线包结构
          • 05.3 资源拦截加载
          • 05.4 增量包设计
          • 05.5 预加载策略
        • 06.关键问题解决
          • 06.1 包冲突管理
          • 06.2 流量与电量
          • 06.3 命中率优化
        • 07.常见陷阱与反例
          • 07.1 全量打包反例
          • 07.2 不限并发反例
          • 07.3 没监控反例
        • 08.演进路线
          • 08.1 V1 简单缓存
          • 08.2 V2 离线包系统
          • 08.3 V3 智能预加载
        • 09.总结与决策
          • 09.1 上线检查表
          • 09.2 选型决策树
    • 研发的效能

  • 专栏
  • 方案设计思想
  • 端侧专项性
杨充
2026-05-21
目录

离线包与预加载方案

# 26.离线包与预加载方案

本篇定位:离线包和预加载是 H5/Hybrid 性能的"加速器"——同样是打开一个 H5 页面,秒开和 3 秒白屏的差距就在这里。本文从一次"H5 页面 5 秒白屏被骂"的故事讲起,回答三个核心问题——离线包究竟解决了什么?预加载有哪几种姿势?怎么设计一套秒开级别的资源体系?

# 目录介绍

  • 01.5 秒白屏的差评
    • 1.1 用户骂"App 卡爆了"
    • 1.2 性能瓶颈拆解
    • 1.3 反思离线方案
  • 02.要解决的核心矛盾
    • 2.1 秒开是基本盘
    • 2.2 包大小与覆盖
    • 2.3 实时与稳定
    • 2.4 离线包的本质
  • 03.业界主流方案
    • 03.1 主流离线包方案
    • 03.2 横向对比矩阵
    • 03.3 预加载分类
  • 04.设计核心原则
    • 04.1 边走边下原则
    • 04.2 增量更新原则
    • 04.3 安全校验原则
    • 04.4 命中率优先原则
  • 05.方案落地实战
    • 05.1 整体架构
    • 05.2 离线包结构
    • 05.3 资源拦截加载
    • 05.4 增量包设计
    • 05.5 预加载策略
  • 06.关键问题解决
    • 06.1 包冲突管理
    • 06.2 流量与电量
    • 06.3 命中率优化
  • 07.常见陷阱与反例
    • 07.1 全量打包反例
    • 07.2 不限并发反例
    • 07.3 没监控反例
  • 08.演进路线
    • 08.1 V1 简单缓存
    • 08.2 V2 离线包系统
    • 08.3 V3 智能预加载
  • 09.总结与决策
    • 09.1 上线检查表
    • 09.2 选型决策树

# 01.5 秒白屏的差评

# 1.1 用户骂"App 卡爆了"

某电商 App 双 11 大促主会场用 H5 写——用户点击后白屏 5 秒。社交媒体上炸开锅:

"什么垃圾 App,点个活动等 5 秒" "网络好的时候都这么慢,4G 直接转圈" "京东淘宝秒开,你们这是石器时代?"

监控数据令人震惊:

  • 首屏白屏:P50 = 3.2s,P99 = 8.5s
  • 白屏率(5s 内未渲染):18%
  • 跳出率:32%——三分之一用户没等到加载完就走了
gantt
    title H5 加载耗时拆解
    dateFormat X
    axisFormat %s s
    
    section 网络
    DNS + TCP    :a, 0, 500
    TLS 握手     :b, 500, 800
    HTML 下载    :c, 800, 1500
    解析 HTML    :d, 1500, 1800
    
    section 资源
    JS 下载      :e, 1800, 3500
    CSS 下载     :f, 1800, 2300
    图片下载     :g, 2300, 4500
    
    section 渲染
    JS 执行      :h, 3500, 4500
    首屏渲染     :crit, i, 4500, 100
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 1.2 性能瓶颈拆解

白屏 5 秒分布:

阶段 耗时 占比
网络(DNS + TCP + TLS) 800ms 16%
HTML 下载 + 解析 700ms 14%
JS / CSS 下载 1.7s 34%
图片下载 1.2s 24%
JS 执行 + 渲染 600ms 12%
合计 5s 100%

根因:所有资源每次都要从远端下——明明是同一个活动页,1000 万人访问就下载 1000 万次。

# 1.3 反思离线方案

事后这个团队总结了三个最深刻的教训:

  1. 离线包能省掉 80% 的网络耗时——HTML/CSS/JS/图片都内置
  2. WebView 预热能省掉冷启动时间——300-800ms
  3. 接口预请求能并行化——把"打开后再请求"提前到"打开前请求"

离线包的本质就是 把"网络下载"换成"本地读取"——速度差 100 倍以上。

# 02.要解决的核心矛盾

# 2.1 秒开是基本盘

业界 H5 秒开标准:

指标 优秀 良好 及格
白屏时间 < 500ms < 1s < 2s
首屏可见 < 1s < 2s < 3s
可交互 < 2s < 3s < 5s
白屏率 < 1% < 5% < 10%

# 2.2 包大小与覆盖

graph LR
    A[全量内置<br/>所有页面打包] --> B[包大]
    A --> C[App 安装包暴涨]
    
    A2[完全在线<br/>都从远端下] --> B2[包小]
    A2 --> C2[每次都慢]
    
    A3[核心内置 + 边缘下载] --> B3[平衡]
    A3 --> C3[首屏快]
    
    style B fill:#fff3e0
    style C2 fill:#fff3e0
    style B3 fill:#e8f5e8
1
2
3
4
5
6
7
8
9
10
11
12
13

实战经验:核心 5-10 个高频页面打入安装包,其他页面用"在用户下次访问前预下载"。

# 2.3 实时与稳定

维度 全量更新 增量更新
包大小 大(每次完整下载) 小(只下载差异)
更新速度 慢 快
流量消耗 高 低
失败容错 容易回滚 复杂
典型节省 - 80%-95%

# 2.4 离线包的本质

离线包 = 把网页"装"进 App 安装包/缓存里

它的核心价值 = WebView 加载本地文件比加载网络快 10-100 倍。

# 03.业界主流方案

# 03.1 主流离线包方案

方案 厂商 特点
支付宝 Nebula 蚂蚁 原生集成、能力强
微信小程序框架 腾讯 严格沙箱
手淘 ZCache 阿里 大规模实战
美团 Knb 美团 集成 H5 容器
JS Service Worker 浏览器原生 PWA 标准
Cordova/Ionic 的 webview-cache 开源 简单

# 03.2 横向对比矩阵

维度 安装内置 启动预下载 用户访问时下 Service Worker
首次访问速度 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐ ⭐⭐
后续访问速度 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐
包大小代价 大 小 0 0
更新延迟 发版 启动后 实时 实时
典型场景 核心页 高频页 长尾页 Web 页

# 03.3 预加载分类

mindmap
  root((预加载))
    资源预加载
      离线包内置
      启动预下载
      Wi-Fi 自动更新
    容器预加载
      WebView 预创建
      RN 实例预热
      引擎预初始化
    数据预加载
      接口预请求
      关键数据本地缓存
      图片预加载
    路径预测
      用户行为预测
      首屏 Tab 预加载
      下一步页面预加载
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 04.设计核心原则

# 04.1 边走边下原则

铁律:用户使用时不让他等——下载在用户没察觉的时候完成。

graph TB
    A[App 启动] --> B[空闲时检查更新]
    B --> C[Wi-Fi 下后台下载]
    C --> D[下载完成校验]
    D --> E[切换到新版本]
    
    F[用户访问页面] --> G{包已就绪?}
    G -->|是| H[本地秒开]
    G -->|否| I[fallback 到在线]
    
    style H fill:#e8f5e8
    style I fill:#fff3e0
1
2
3
4
5
6
7
8
9
10
11
12

# 04.2 增量更新原则

只下载变化的部分——bsdiff、xdelta 等算法。

flowchart LR
    Old[v1.0 包<br/>2MB] --> Diff[diff 计算]
    New[v1.1 包<br/>2.1MB] --> Diff
    Diff --> Patch[patch 包<br/>50KB]
    
    Note[节省 95% 流量]
    
    style Patch fill:#e8f5e8
    style Note fill:#fff3e0
1
2
3
4
5
6
7
8
9

典型节省:

包大小变化 全量 增量 节省
2MB → 2.1MB 2.1MB 50KB 97%
5MB → 5.5MB 5.5MB 200KB 96%

# 04.3 安全校验原则

离线包必须签名校验——防止中间人替换。

sequenceDiagram
    participant App as App
    participant CDN as CDN
    
    App->>CDN: 下载离线包 + 签名
    CDN-->>App: 包 + signature
    
    App->>App: 1. 校验包哈希(防损坏)
    App->>App: 2. 校验签名(防篡改)
    App->>App: 3. 校验来源(白名单域名)
    
    alt 校验通过
        App->>App: 应用新包
    else 校验失败
        App->>App: 丢弃 + 上报
    end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 04.4 命中率优先原则

命中率 = 离线包成功命中 / 页面访问总数——业界优秀水平 > 90%。

mindmap
  root((命中率优化))
    覆盖核心页
      首页
      列表
      详情
      下单
    Wi-Fi 主动预下载
      启动时
      App 后台时
    用户行为预测
      搜索后预加载详情
      列表页预加载下一页
    版本兜底
      旧版本仍可用
      渐进升级
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 05.方案落地实战

# 05.1 整体架构

graph TB
    subgraph "管理后台"
        Console[运营后台]
        Build[构建系统]
        Console --> Build
    end
    
    subgraph "下发系统"
        CDN[CDN]
        VersionAPI[版本查询接口]
        Build --> CDN
    end
    
    subgraph "客户端"
        Update[更新管理器]
        Cache[本地缓存]
        Hijack[资源拦截]
        Container[H5 容器]
        
        Update --> Cache --> Hijack --> Container
    end
    
    Update --> VersionAPI
    Update --> CDN
    
    Container --> Page[H5 页面<br/>从 Cache 加载]
    
    Monitor[监控] --> Update
    
    style Update fill:#fff3e0
    style Cache fill:#e8f5e8
    style Hijack fill:#e3f2fd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

# 05.2 离线包结构

典型离线包结构:

offline-pkg-v1.2.zip
├── manifest.json          ← 包描述(版本、入口、签名)
├── index.html             ← 页面 HTML
├── assets/
│   ├── main.js
│   ├── main.css
│   └── img/
│       ├── banner.png
│       └── icons.png
└── pages/                 ← 多页面应用
    ├── list.html
    └── detail.html
1
2
3
4
5
6
7
8
9
10
11
12

manifest.json 示例:

{
    "name": "promotion-page",
    "version": "1.2.0",
    "appId": "promotion",
    "entry": "index.html",
    "files": {
        "index.html": "sha256:abc...",
        "assets/main.js": "sha256:def...",
        "assets/main.css": "sha256:ghi..."
    },
    "signature": "RSA-SHA256-encoded-signature"
}
1
2
3
4
5
6
7
8
9
10
11
12

# 05.3 资源拦截加载

WebView 拦截网络请求,从本地加载:

class OfflinePackageInterceptor(private val pkg: OfflinePackage) {
    
    fun shouldInterceptRequest(view: WebView, req: WebResourceRequest): WebResourceResponse? {
        val url = req.url.toString()
        val path = extractPath(url)
        
        // 1. 检查是否在离线包里
        val localFile = pkg.findFile(path) ?: return null
        
        // 2. 校验完整性
        if (!verifyHash(localFile, pkg.manifest.files[path])) {
            return null  // fallback 到在线
        }
        
        // 3. 返回本地资源
        return WebResourceResponse(
            getMimeType(path),
            "UTF-8",
            FileInputStream(localFile)
        )
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 05.4 增量包设计

bsdiff 增量包流程:

sequenceDiagram
    participant App as App<br/>当前 v1.0
    participant Server as 版本服务
    
    App->>Server: 查询更新 当前 v1.0
    Server-->>App: 最新 v1.2 + diff URL
    
    Note over App: diff 路径: 1.0 → 1.2 ?<br/>1.0 → 1.1 → 1.2 ?
    
    App->>Server: 下载 diff 包 50KB
    App->>App: 用 bsdiff 算法合并<br/>v1.0 + diff = v1.2
    App->>App: 校验合成包哈希
    
    alt 哈希一致
        App->>App: 切换到 v1.2
    else 哈希不对
        App->>Server: 降级全量下载
    end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 05.5 预加载策略

多层预加载组合:

graph TB
    L1[L1 安装内置<br/>核心页]
    L2[L2 启动预下载<br/>高频页]
    L3[L3 用户行为预测<br/>下一步可能页]
    L4[L4 实时下载<br/>长尾页]
    
    L1 --> L2 --> L3 --> L4
    
    Note1[100% 命中]
    Note2[90% 命中]
    Note3[60% 命中]
    Note4[fallback]
    
    L1 -.- Note1
    L2 -.- Note2
    L3 -.- Note3
    L4 -.- Note4
    
    style L1 fill:#e8f5e8
    style L2 fill:#fff3e0
    style L3 fill:#e3f2fd
    style L4 fill:#f3e5f5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

接口预请求 + 数据预加载:

// 用户在列表页时,预测下一步操作
class PreloadManager {
    
    fun onListPageVisible(items: List<Item>) {
        // 预加载前 3 个详情接口数据
        items.take(3).forEach { item ->
            scope.launch {
                val data = api.getDetail(item.id)
                cache.put("detail:${item.id}", data)
            }
        }
    }
    
    fun onDetailPageOpen(itemId: String): Item {
        // 优先用预加载的数据
        return cache.get("detail:$itemId") 
            ?: api.getDetail(itemId)
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 06.关键问题解决

# 06.1 包冲突管理

多业务团队各自做离线包,可能冲突:

graph TB
    Issue[100 个业务包<br/>各自更新] --> Risk{问题}
    
    Risk --> R1[存储爆炸]
    Risk --> R2[启动检查接口风暴]
    Risk --> R3[版本管理混乱]
    
    Solution[统一离线包平台] --> S1[统一构建]
    Solution --> S2[统一存储 LRU]
    Solution --> S3[批量版本检查]
    Solution --> S4[全局灰度]
    
    style Risk fill:#ffebee
    style Solution fill:#e8f5e8
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 06.2 流量与电量

用户流量敏感的考虑:

策略 描述
Wi-Fi 优先 默认仅 Wi-Fi 下载
大包警告 > 10MB 提示用户
分时段 夜间静默更新
流量上限 每月最多 N MB 后台下载
电量阈值 低电量时停止下载

# 06.3 命中率优化

命中率监控指标:

graph LR
    A[页面访问] --> B{离线包就绪?}
    B -->|是| Hit[命中 ✅]
    B -->|否, 在下载| Miss1[未命中 - 在下载]
    B -->|否, 没下| Miss2[未命中 - 未启用]
    
    Hit --> Stats[命中率统计]
    Miss1 --> Stats
    Miss2 --> Stats
    
    Stats --> Optimize[优化方向<br/>提前预下载<br/>扩大 Wi-Fi 覆盖]
    
    style Hit fill:#e8f5e8
    style Optimize fill:#fff3e0
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 07.常见陷阱与反例

# 07.1 全量打包反例

反例:某电商把 50 个 H5 页面全部打入安装包 → App 包从 50MB 涨到 120MB → 安装转化率掉 25%。

教训:

  • 只内置真正的"高频核心页"
  • 边缘页面用"动态下载"
  • 安装包大小 > 100MB 是大忌

# 07.2 不限并发反例

反例:App 启动后,100 个业务包同时请求 CDN → 网关被打、用户主流程加载慢。

教训:

  • 控制并发(最多 3-5 个)
  • 优先级排队(核心包优先)
  • 主流程加载完才下载离线包

# 07.3 没监控反例

反例:上线了离线包系统,但不知道命中率——以为效果很好,其实命中率才 30%。

教训:必须监控:

  • 离线包命中率
  • 加载耗时
  • 失败率(哈希校验失败、解压失败)
  • 流量消耗
mindmap
  root((三大反例))
    全量打包
      安装包暴涨
      安装转化下降
      只打高频核心
    不限并发
      启动风暴
      网关被打
      限并发 + 优先级
    没监控
      命中率不知
      白屏率不知
      建立监控大盘
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 08.演进路线

# 08.1 V1 简单缓存

特征:业务起步、H5 不多。

做法:

  • WebView 默认缓存
  • HTTP cache-control
  • 无主动管理

适用阶段:少量 H5 页面

# 08.2 V2 离线包系统

特征:H5 业务规模化。

做法:

  • 离线包 + manifest
  • 资源拦截加载
  • 全量包下发
  • WebView 预创建池

适用阶段:中型 App

# 08.3 V3 智能预加载

特征:超大规模 / 极致体验追求。

做法:

  • 增量更新(bsdiff)
  • 用户行为预测
  • 接口预请求
  • AI 辅助命中率优化
  • 全链路监控

适用阶段:头部 App / 电商大促

flowchart LR
    V1[V1 简单缓存<br/>起步] --> V2[V2 离线包系统<br/>主流]
    V2 --> V3[V3 智能预加载<br/>头部]
    
    style V1 fill:#e3f2fd
    style V2 fill:#e8f5e8
    style V3 fill:#fff3e0
1
2
3
4
5
6
7

# 09.总结与决策

# 09.1 上线检查表

离线包系统上线前对照:

  • [ ] 包结构标准化(manifest + 资源)
  • [ ] 包签名 + 哈希校验
  • [ ] WebView 资源拦截
  • [ ] 增量更新机制
  • [ ] Wi-Fi 优先下载
  • [ ] 并发控制(最多 N 个)
  • [ ] LRU 存储管理(不无限膨胀)
  • [ ] 灰度发布机制
  • [ ] 失败回退在线版本
  • [ ] WebView 预创建池
  • [ ] 接口预请求
  • [ ] 命中率监控
  • [ ] 白屏率 / 加载耗时监控
  • [ ] 流量消耗监控

# 09.2 选型决策树

flowchart TD
    Start([我要做离线包]) --> Q1{H5 业务规模?}
    
    Q1 -->|< 5 个 H5 页面| L1[WebView 缓存就够<br/>简单]
    Q1 -->|5-50 个页面| L2[简单离线包<br/>+ 资源拦截]
    Q1 -->|> 50 个页面| L3[完整离线包平台<br/>+ 增量更新<br/>+ 预加载]
    
    Q2([首屏要求?]) --> Speed{秒开?}
    Speed -->|是| Aggr[+ 安装内置核心页<br/>+ 启动预下载]
    Speed -->|普通| Norm[访问时下载即可]
    
    style L1 fill:#e3f2fd
    style L2 fill:#e8f5e8
    style L3 fill:#fff3e0
    style Aggr fill:#ffebee
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

最后一句话:离线包是 H5 性能的"作弊器"——开篇 5 秒白屏的根因,就是把"网络下载"放在了"用户等待"的关键路径上。

好的离线包方案 = 核心内置、增量更新、安全可靠、命中率高。

上次更新: 2026/06/07, 10:26:12
国际化实践方案
通用轮训方案设计

← 国际化实践方案 通用轮训方案设计→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式