Flutter热更新在鸿蒙端的实现方案:原理剖析、完整适配与性能优化

Flutter热更新在鸿蒙端的实现方案:原理剖析、完整适配与性能优化

引言

随着移动应用开发的快速发展,跨平台框架与新兴操作系统的结合成为技术演进的重要方向。Flutter作为Google推出的高性能UI框架,凭借其优秀的渲染性能和完善的生态,已成为跨平台开发的主流选择。而华为推出的HarmonyOS(鸿蒙操作系统)作为新一代全场景分布式操作系统,正逐步构建自己的生态体系。

在实际开发中,热更新(Hot Update)是提升开发效率和用户体验的关键技术,它允许开发者在不断开应用连接、不重新安装应用的情况下,动态更新部分代码或资源。然而,当Flutter应用运行在鸿蒙端时,基于Android/iOS设计的标准热更新机制面临严峻的兼容性挑战。本文将深入探讨Flutter热更新在鸿蒙端的完整实现方案,包括底层技术原理分析、详细的三方库适配机制、完整的代码实现、可量化的性能优化策略及具体的集成实践。

一、Flutter热更新机制深度解析

1.1 Flutter标准热更新原理

Flutter官方主要提供了两种面向开发阶段的热更新机制:

  1. 热重载(Hot Reload):在开发模式下,将更新后的Dart代码注入到正在运行的Dart虚拟机(VM)中,重建控件树并保持当前应用状态。其核心流程包括:

    • 增量编译:IDE将修改的Dart代码增量编译为kernel二进制文件(.dill)。
    • 协议传输:通过vm-service协议将增量文件传输至设备上运行的Flutter引擎。
    • 状态保持:Dart VM重用当前的Isolate和堆内存,仅重建Widget树,从而近乎实时地看到UI变化。
  2. 热重启(Hot Restart):重新启动Flutter引擎,重新加载Dart代码,但保持应用进程在设备上运行。此过程会重置所有Dart状态,但避免了完整的应用重装。

关键限制:上述两种机制都高度依赖Dart VM的即时编译(JIT)能力。而在发布(Release)版本中,为提高启动性能和减少包体积,Dart代码被提前编译(AOT)为特定平台的机器码(如ARM指令)。AOT编译后的产物失去了动态解释和执行新Dart源码的能力,因此官方机制不适用于生产环境。

1.2 生产环境热更新通用方案

为满足生产环境需求,社区衍生出多种方案,其核心思想是绕过AOT限制,动态下发并执行Dart代码。主流实现原理如下:

  1. 资源动态化:将UI、图片、配置等资源文件打包成独立于主APK/HAP的压缩包,通过网络下发并动态加载。
  2. Dart代码动态化(核心)
    • 方案一:解释执行。将Dart源码或编译后的kernel文件(非AOT)打包下发。在运行时,利用一个内置的、支持JIT的精简版Dart虚拟机(如libapp.so中的调试运行时)来加载并执行这些kernel文件。此方案对引擎有定制要求。
    • 方案二:引擎切换。准备两个Flutter引擎:一个用于加载固定的AOT主业务(libapp.so),另一个轻量级引擎用于动态加载下发的kernel模块。通过平台通道(Platform Channel)在两个引擎间协调通信。
    • 方案三:字节码补丁。此方案更为复杂和定制化,涉及修改Dart编译工具链,将差异代码编译成自定义的中间表示(字节码),由运行时层解释执行或即时编译。

1.3 鸿蒙端适配的核心技术挑战

将上述任一方案迁移至鸿蒙操作系统,均面临以下根本性挑战:

  • 系统架构与API差异:鸿蒙使用ArkTS/JS作为应用开发语言,其底层运行环境、文件系统路径(如/data/app/...)、网络请求、后台任务机制与Android Java/Kotlin环境迥异。所有与平台交互的接口均需重写。
  • Flutter引擎集成方式不同:鸿蒙应用(HAP)通过Native Ability集成Flutter引擎的C++库。引擎的初始化、渲染表面(Surface)的创建、消息循环与Android的FlutterActivity/FlutterViewController有显著区别,热更新所需的动态库加载机制必须适配鸿蒙的Native API。
  • 三方库平台通道失效:大量Flutter热更新方案依赖的三方库(如flutter_downloaderpath_provider)通过Platform Channel调用原生平台功能。这些插件的Android/iOS实现无法在鸿蒙上运行,必须为其开发鸿蒙端(HarmonyOS)的实现。
  • 安全与权限模型:鸿蒙拥有自己严格的分布式权限管理系统。热更新流程中的网络下载、文件读写、安装包解析等操作,必须申请并适配对应的鸿蒙权限。

二、鸿蒙端适配:核心原理与完整实现

本章节将以“Dart代码动态化(解释执行kernel)”方案为例,阐述在鸿蒙端的完整适配流程。

2.1 整体架构设计

[云端服务器]
     |
     | (HTTPS) 下发补丁包(.zip含差分kernel文件、资源)
     |
[鸿蒙端 Flutter应用]
     |
     |--- 1. 网络模块:下载、校验(MD5/SHA256)补丁包
     |--- 2. 解压模块:将补丁解压至鸿蒙应用沙箱目录
     |--- 3. 加载引擎:初始化或获取Flutter Native引擎实例
     |--- 4. 热更新管理器:调用鸿蒙定制引擎API,加载沙箱中的kernel文件
     |--- 5. 路由/状态管理:跳转至更新后的Dart模块页面

2.2 关键步骤与三方库适配原理

2.2.1 补丁管理与下载
  • 适配任务:实现鸿蒙端的文件下载与存储。
  • 三方库适配示例(以flutter_downloader为例)
    • 该插件在Android端使用DownloadManagerWorkManager,iOS端使用NSURLSessionDownloadTask
    • 鸿蒙端实现原理:需创建鸿蒙Ability,使用鸿蒙的@ohos.net.http模块创建下载任务,并结合@ohos.backgroundTaskManager管理后台任务。通过HarmonyPlatformChannel将下载进度、状态回调给Dart层。
    • 代码结构:需在鸿蒙工程中创建/entry/src/main/ets/flutter_downloader/目录,实现FlutterDownloaderPlugin鸿蒙类。
2.2.2 路径获取
  • 适配任务:获取鸿蒙应用沙箱内可用于存储补丁的目录。
  • 三方库适配示例(以path_provider为例)
    • Android端对应getExternalStorageDirectory/getApplicationDocumentsDirectory
    • 鸿蒙端实现原理:使用鸿蒙的Context对象获取应用文件路径,如context.filesDir(对应/data/app/.../files)。需实现PathProviderPlugin鸿蒙类,通过HarmonyPlatformChannel返回正确的路径字符串。

2.3 完整代码实现示例

以下提供一个高度简化但结构完整的鸿蒙端热更新管理器核心代码示例。

1. Dart层 - 热更新管理器 (hot_update_manager.dart)

import 'dart:io';
import 'package:flutter/services.dart';
import 'package:path_provider/path_provider.dart';

class HarmonyHotUpdateManager {
  static const MethodChannel _channel = 
      MethodChannel('com.example/hot_update');
  static const String _patchFileName = 'latest_patch.zip';
  static const String _kernelFileName = 'app.dill';

  // 检查并下载更新
  Future<bool> checkAndDownloadUpdate(String patchUrl) async {
    try {
      // 1. 获取鸿蒙应用文档目录
      final Directory appDocDir = await getApplicationDocumentsDirectory();
      final String patchPath = '${appDoc.path}/$_patchFileName';

      // 2. 调用鸿蒙端插件下载(假设已适配)
      final bool downloadResult = await _channel.invokeMethod(
        'downloadPatch',
        {'url': patchUrl, 'savePath': patchPath},
      );
      if (!downloadResult) {
        throw Exception('Download failed');
      }

      // 3. 解压(使用dart:io或调用原生解压)
      final String extractPath = '${appDoc.path}/patch/';
      await _extractZip(patchPath, extractPath);

      // 4. 校验并加载Kernel文件
      final String kernelPath = '$extractPath$_kernelFileName';
      await _loadKernelPatch(kernelPath);

      return true;
    } on PlatformException catch (e) {
      print('Platform error: ${e.message}');
      return false;
    } catch (e) {
      print('Error during update: $e');
      return false;
    }
  }

  Future<void> _extractZip(String zipPath, String extractPath) async {
    // 实现或调用原生解压
    // 例如,通过另一个Platform Channel调用鸿蒙的zip解压能力
    await _channel.invokeMethod('extractZip', {
      'zipPath': zipPath,
      'targetPath': extractPath
    });
  }

  Future<void> _loadKernelPatch(String kernelPath) async {
    // 关键步骤:通知鸿蒙Native层加载Kernel文件到Flutter引擎
    final bool success = await _channel.invokeMethod(
      'loadKernel',
      {'kernelPath': kernelPath},
    );
    if (!success) {
      throw Exception('Failed to load kernel patch');
    }
    print('Kernel patch loaded successfully. Restarting UI...');
    // 通常需要重启Flutter视图或导航到新页面
  }
}

2. 鸿蒙Native层 - 平台实现 (HotUpdatePlugin.ets)

// entry/src/main/ets/flutter_plugins/HotUpdatePlugin.ets
import plugin from '@ohos.hiviewdfx.HiTraceChain';
import { FlutterPlugin, MethodResult } from '../common/FlutterPlugin'; // 假设的基类
import http from '@ohos.net.http';
import fs from '@ohos.file.fs';
import zlib from '@ohos.zlib';

export class HotUpdatePlugin extends FlutterPlugin {
  private TAG: string = 'HotUpdatePlugin';

  // 注册方法处理器
  onMethodCall(call: plugin.MethodCall, result: MethodResult): void {
    switch (call.method) {
      case 'downloadPatch':
        this.downloadPatch(call.arguments as Record<string, string>, result);
        break;
      case 'extractZip':
        this.extractZip(call.arguments as Record<string, string>, result);
        break;
      case 'loadKernel':
        this.loadKernel(call.arguments as Record<string, string>, result);
        break;
      default:
        result.notImplemented();
    }
  }

  // 下载实现
  private async downloadPatch(args: Record<string, string>, result: MethodResult): Promise<void> {
    const url: string = args['url'];
    const savePath: string = args['savePath'];
    // 使用鸿蒙http模块创建请求
    let httpRequest = http.createHttp();
    try {
      let response = await httpRequest.request(url, {
        method: http.RequestMethod.GET,
        connectTimeout: 60000,
        readTimeout: 60000,
      });
      if (response.responseCode === 200) {
        // 将响应数据写入文件(伪代码,需使用ohos.file.fs API)
        await fs.writeFile(savePath, response.result as ArrayBuffer);
        result.success(true);
      } else {
        result.error('NETWORK_ERROR', `Download failed with code: ${response.responseCode}`, null);
      }
    } catch (error) {
      result.error('DOWNLOAD_FAILED', error.message, error);
    } finally {
      httpRequest.destroy();
    }
  }

  // 解压实现
  private async extractZip(args: Record<string, string>, result: MethodResult): Promise<void> {
    const zipPath: string = args['zipPath'];
    const targetPath: string = args['targetPath'];
    try {
      // 使用鸿蒙zlib或系统能力解压(此处为示意)
      // 实际需调用ohos.zlib或ohos.file.fs相关解压接口
      await this.unzipFile(zipPath, targetPath);
      result.success(true);
    } catch (error) {
      result.error('EXTRACT_FAILED', error.message, error);
    }
  }

  // 核心:加载Kernel到Flutter引擎
  private async loadKernel(args: Record<string, string>, result: MethodResult): Promise<void> {
    const kernelPath: string = args['kernelPath'];
    try {
      // 此处需要调用Flutter C++引擎的Native API。
      // 假设我们有一个已编译的、支持动态加载的Flutter引擎库 (libflutter_engine.so)
      // 并通过Harmony的NAPI暴露了C++函数:`bool LoadDartKernel(const char* path)`
      const engine = globalThis.flutterEngine; // 假设全局可访问的引擎对象
      if (engine && typeof engine.loadDartKernel === 'function') {
        const success = engine.loadDartKernel(kernelPath);
        result.success(success);
      } else {
        result.error('ENGINE_ERROR', 'Flutter engine or method not available', null);
      }
    } catch (error) {
      result.error('LOAD_KERNEL_FAILED', error.message, error);
    }
  }

  private async unzipFile(src: string, dest: string): Promise<void> {
    // 具体的鸿蒙文件解压实现
    console.log(`Unzipping ${src} to ${dest}`);
    // ... 实现代码
  }
}

2.4 集成步骤简述

  1. 环境准备:安装HarmonyOS SDK、DevEco Studio,配置Flutter for HarmonyOS环境。
  2. 创建鸿蒙Flutter项目:使用Flutter命令行或DevEco Studio模板创建。
  3. 定制Flutter引擎:编译一份支持动态加载Kernel的Flutter引擎库(libflutter_engine.so),并集成到鸿蒙HAP的libs/arm64-v8a/目录下。
  4. 实现平台插件:在鸿蒙工程entry/src/main/ets/下,为path_providerflutter_downloader及自定义的hot_update插件实现对应的鸿蒙Ability。
  5. 注册插件:在鸿蒙应用的EntryAbility.ets中,于onCreate阶段初始化并注册上述插件。
  6. Dart层集成:在Flutter项目的pubspec.yaml中依赖所需插件,并在应用初始化时调用HarmonyHotUpdateManager.checkAndDownloadUpdate()

三、性能优化策略与实践

3.1 优化维度

  1. 补丁包大小
    • 策略:使用BsDiff等二进制差分算法,生成相对于基线版本的增量补丁。
    • 实践:在CI/CD流水线中集成差分工具,确保下发的补丁最小化。
  2. 加载速度
    • 策略:并行下载与解压;预热Flutter引擎。
    • 实践:将补丁分为“关键启动资源”和“懒加载资源”两阶段加载。
  3. 内存占用
    • 策略:及时释放旧的Kernel和资源引用;避免内存泄漏。
    • 实践:在鸿蒙Native层,确保loadKernel后,旧的动态代码模块能被GC正确回收。使用鸿蒙性能分析工具SmartPerf进行内存检测。
  4. UI流畅度
    • 策略:在独立Isolate中执行下载、解压等耗时操作。
    • 实践:Dart层使用computeIsolate.spawn,确保主UI线程不阻塞。

3.2 性能对比数据(模拟示例)

在搭载HarmonyOS 3.0的设备上,对同一页面(包含复杂列表)的更新进行测试:

场景补丁大小下载+解压耗时加载渲染耗时总耗时内存增长 (PSS)
全量更新 (HAP)15 MBN/A (需重新安装)N/A~12sN/A
差量热更新256 KB~1.2s (Wi-Fi)~0.8s~2.0s~15 MB
资源热更新1.5 MB (图片)~0.5s即时~0.5s~8 MB

数据说明:差量热更新总耗时远低于全量安装,用户体验提升显著。内存增长在可控范围内,且后续可被回收。

四、总结与展望

4.1 总结

本文系统性地阐述了Flutter热更新技术在鸿蒙操作系统上的完整实现方案。我们从Flutter热更新的核心原理出发,剖析了生产环境动态化方案与鸿蒙平台适配所面临的系统架构差异、引擎集成、三方库兼容性及安全模型等根本性挑战。通过提供完整的架构设计、核心代码示例(涵盖Dart与鸿蒙ETS层)以及三方库适配原理,我们证明了在鸿蒙端实现Flutter热更新的技术可行性。

关键成功因素在于:

  1. 深度定制Flutter引擎:使其在鸿蒙上保留或具备动态加载Dart Kernel的能力。
  2. 全面适配平台通道:为关键的三方插件实现鸿蒙端的具体能力,打通Dart与鸿蒙原生系统的交互。
  3. 构建完整的管理流程:集成安全的网络下载、文件校验、差分更新和异常处理机制。

4.2 展望

尽管当前方案已能解决问题,但未来仍有优化与演进方向:

  1. 标准化工具链:期待Flutter官方或社区提供更标准化的鸿蒙热更新工具链和插件,降低集成复杂度。
  2. 与鸿蒙原生能力深度融合:探索热更新与鸿蒙原子化服务、跨端迁移等独特能力的结合,创造更动态的用户体验。
  3. 性能与安全极致化:进一步研究字节码级别的动态化方案,在提升性能的同时,利用鸿蒙的硬件级安全能力(如TEE)对补丁进行加密和签名验证,保障更新安全。

Flutter与鸿蒙的结合是跨平台技术在新兴系统上的重要实践。成功实现热更新,不仅能极大提升混合开发的效率与灵活性,也为未来构建更动态、更智能的全场景应用奠定了坚实的技术基础。


附录:快速集成命令示例

# 1. 创建支持HarmonyOS的Flutter模块
flutter create --template=module --platforms=harmony my_hot_update_module

# 2. 在鸿蒙主工程中引用该模块(修改主工程的`build-profile.json5`)
# 3. 编译HAP
cd /path/to/harmony/main/project
./gradlew assembleRelease # 或使用DevEco Studio构建
<think>好的,我现在需要解决用户的问题:如何让Flutter蓝牙App适配鸿蒙原生系统。用户提到他们需要解决方案实现方案,特别是关于原生支持的部分。我需要结合提供的引用资料,整理出可行的步骤和注意事项。 首先,根据引用[1]和[2],美团外卖团队已经实现Flutter鸿蒙的原生支持,主要是通过管理多个Flutter SDK版本,使用壳工程分离代码,以及替换鸿蒙化的三方库。这可能意味着用户需要类似的步骤来适配蓝牙功能。接下来,引用[3]提到,如果现有插件不支持鸿蒙,需要自行添加原生代码支持,这涉及到修改插件代码,可能包括平台通道的实现。蓝牙功能通常会使用到平台特定的插件,比如flutter_blue,所以需要检查这些插件是否已有鸿蒙支持,如果没有,就需要自己适配。 引用[4]和[5]指出,使用跨平台框架如Flutter时,如果涉及原生功能的插件,必须进行鸿蒙兼容。特别是通过channel调用的三方库,需要开发者处理。所以蓝牙插件可能需要修改其原生部分的代码,替换为鸿蒙的API。例如,鸿蒙可能使用不同的蓝牙管理类,需要重写MethodChannel的处理部分。 接下来,我需要考虑具体的步骤。首先,用户需要搭建鸿蒙开发环境,可能包括安装DevEco Studio和配置Flutter的ohos版本,如引用[3]提到的下载鸿蒙支持的Flutter SDK。然后,检查现有蓝牙插件是否已有鸿蒙适配,如果没有,就需要分叉插件代码,添加鸿蒙的原生实现,比如在插件中增加ohos目录,实现相应接口。 另外,依赖管理是关键。引用[2]和[5]提到使用dependency_overrides来替换为鸿蒙适配的三方库,所以用户可能需要覆盖原有的蓝牙插件依赖,指向自己修改后的版本。同时,模块化管理项目,如使用melos,可以帮助分离不同平台的代码,方便维护。 还需要考虑平台通道的兼容性。蓝牙相关的功能,如扫描、连接、数据传输,在鸿蒙上可能需要调用鸿蒙的蓝牙API,如ohos.bluetooth包中的类。因此,在Flutter插件的原生代码部分,需要将Android的BluetoothAdapter替换为鸿蒙对应的实现。 测试和调试也是重要环节。用户需要在鸿蒙设备或模拟器上运行适配后的应用,验证蓝牙功能是否正常工作。可能遇到权限问题,需要检查鸿蒙的权限配置,确保应用有访问蓝牙的权限。 总结下来,步骤可能包括:环境搭建、插件适配、依赖管理、代码分离、测试调试。同时,需要注意鸿蒙Android API的差异,可能需要查阅鸿蒙的蓝牙开发文档,确保正确使用其API。</think>针对Flutter蓝牙应用适配鸿蒙系统的需求,结合跨平台框架特性和鸿蒙原生支持要求,以下是分步骤实现方案: ### 一、环境准备 1. **配置鸿蒙专用Flutter SDK** 使用FVM工具管理多版本SDK,通过`fvm install ohos-flutter`安装鸿蒙分支版本[^2][^3] ```bash fvm install ohos-flutter fvm use ohos-flutter --force ``` 2. **搭建双编译环境** 在`pubspec.yaml`中声明双平台支持: ```yaml environment: sdk: ">=2.17.0 <3.0.0" flutter: ">=3.7.0" ohos: ">=3.2.0" # 鸿蒙API版本 ``` ### 二、蓝牙插件适配 #### 1. 现有插件分析 检查`flutter_blue`等蓝牙插件是否包含鸿蒙实现: ```dart // 检测插件结构 plugins/ android/ ios/ ohos/ ← 需要新增的鸿蒙实现 lib/ ``` #### 2. 创建鸿蒙平台实现 在插件项目中新建`ohos`目录,实现蓝牙功能: ```java // ohos/src/main/java/com/example/flutterblue/FlutterBluePlugin.java public class FlutterBluePlugin implements MethodCallHandler { private final HiAbilitySlice slice; private BluetoothHost bluetoothHost; public static void registerWith(PluginRegistry registry) { final MethodChannel channel = new MethodChannel( registry.registrarFor("com.example.flutterblue").getMessenger(), "flutter_blue" ); channel.setMethodCallHandler(new FlutterBluePlugin(registry.activity())); } @Override public void onMethodCall(MethodCall call, Result result) { switch (call.method) { case "enableBluetooth": // 调用鸿蒙蓝牙API bluetoothHost = BluetoothHost.getDefaultHost(slice); if (!bluetoothHost.isBluetoothEnabled()) { bluetoothHost.enableBluetooth(); } result.success(true); break; // 其他方法实现... } } } ``` #### 3. 配置插件依赖覆盖 在应用项目的`pubspec.yaml`中覆盖原插件: ```yaml dependency_overrides: flutter_blue: git: url: https://gitee.com/your_account/flutter_blue_ohos.git path: packages/flutter_blue ``` ### 三、平台特性抽象 #### 1. 创建平台抽象层 ```dart // lib/common/bluetooth_adapter.dart abstract class BluetoothAdapter { Future<bool> enable(); Stream<BluetoothDevice> scanDevices(); // 其他通用方法... } // 实现鸿蒙专用适配器 class HarmonyOSBluetoothAdapter implements BluetoothAdapter { @override Future<bool> enable() async { return await platform.invokeMethod('enableBluetooth'); } } ``` #### 2. 动态平台检测 ```dart void main() { if (Platform.isHarmonyOS) { BluetoothAdapter.instance = HarmonyOSBluetoothAdapter(); } else { BluetoothAdapter.instance = DefaultBluetoothAdapter(); } runApp(MyApp()); } ``` ### 四、权限配置 在鸿蒙工程的`config.json`中添加蓝牙权限: ```json { "module": { "reqPermissions": [ { "name": "ohos.permission.USE_BLUETOOTH", "reason": "蓝牙设备连接" }, { "name": "ohos.permission.DISCOVER_BLUETOOTH", "reason": "设备扫描" } ] } } ``` ### 五、构建优化 使用melos进行多包管理,分离平台特定代码: ```yaml # melos.yaml packages: - "packages/core" - "packages/bluetooth_android" - "packages/bluetooth_harmony" ``` 通过`melos run build:harmony`执行鸿蒙专用构建流程[^2] ### 六、调试技巧 1. **日志过滤** ```bash hdc shell hilog -T "FlutterBlue" ``` 2. **特征值测试** 鸿蒙蓝牙UUID格式校验要求严格,需注意格式转换: ```dart String formatHarmonyUuid(String uuid) { return uuid.toLowerCase().replaceAll(RegExp(r'{|}|-'), ""); } ``` ### 适配难点解决方案 1. **设备发现差异** 鸿蒙需处理`BluetoothHost.ACTION_DISCOVERY_STARTED`广播: ```java Intent intent = new Intent(); intent.setAction(BluetoothHost.ACTION_DISCOVERY_STARTED); slice.registerReceiver(discoveryReceiver, intent); ``` 2. **低功耗特性适配** 鸿蒙BLE接口位于`ohos.bluetooth.ble`包,需重新实现扫描逻辑: ```java BleScanCallback callback = new BleScanCallback() { @Override public void onScanResult(BleScanResult result) { BluetoothDevice device = result.getDevice(); // 发送到Flutter层... } }; bluetoothHost.getBleScanner().startScan(callback); ```
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值