Google官方应用程序架构指南

文导读|   点击标题阅读

互联网寒冬下,程序员如何突围提升自己?

今日头条App 页面秒开方案详解

混合开发框架最全对比,为什么我更推荐Flutter?

作者:心迹风逝2015
链接:https://juejin.im/post/5bb02fe5e51d450e70429d8c

应用程序架构指南
前言-移动应用用户体验

在大多数情况下,App只有一个来自桌面或程序启动器的入口点,然后作为单个整体进程运行。另一方面,Android应用程序具有更复杂的结构。典型的Android应用程序包含多个应用程序组件,包括 activities, fragments, services, content providers, and broadcast receivers等

您在app manifest中声明了大部分这些应用组件。然后,Android操作系统使用此文件来决定如何将您的应用程序集成到设备的整体用户体验中。鉴于正确编写的Android应用程序包含多个组件,并且用户经常在短时间内与多个应用程序进行交互,因此应用程序需要适应不同类型的用户驱动的工作流程和任务。

例如,当您考虑在自己喜欢的社交网络应用中分享照片时会发生什么:

  1. 该应用程序触发相机意图。Android操作系统启动相机应用程序来处理请求。
    此时,用户已离开社交网络应用程序,但他们的体验仍然是无缝的。

  2. 相机应用程序可能会触发其他意图,例如启动文件选择器,这可能会启动另一个应用程序。

  3. 最终,用户返回社交网络应用程序并共享照片。

在此过程中的任何时候,用户都可能被电话或通知中断。在对此中断采取行动后,用户希望能够返回并恢复此照片共享过程。此应用程序跳跃行为在移动设备上很常见,因此您的应用必须正确处理这些问题。

请记住,移动设备也受资源限制,因此在任何时候,操作系统都可能会杀死某些应用程序进程以为新的进程腾出空间。

鉴于此环境的条件,您的应用程序组件可能会单独启动并无序启动,操作系统或用户可以随时销毁它们。由于这些事件不在您的控制之下,因此 您不应在应用程序组件中存储任何应用程序数据或状态,并且您的应用程序组件不应相互依赖。

常见的架构原则

如果您不应该使用应用程序组件来存储应用程序数据和状态,那么您应该如何设计应用程序?

关注点分离

最重要的原则是 分离关注点,在一个 Activity 或一个Fragment 中编写所有代码是一个常见的错误。这些基于UI的类应该只包含处理UI和app交互的逻辑。通过保持这些类的精简,您可以避免许多与生命周期相关的问题发生。

请记住,你没有自己的实现Activity和Fragment; 相反,这些只是表示Android操作系统和应用程序之间合约的胶水类。操作系统可以根据用户交互或低内存等系统条件随时销毁它们。为了提供令人满意的用户体验和更易于管理的应用程序维护体验,最好尽量减少对它们的依赖。
从模型(Model)中驱动UI
另一个重要原则是您应该从模型驱动UI,最好是持久模型。模型是负责处理应用程序数据的组件。它们独立于View应用中的 对象和应用组件,因此它们不受应用生命周期和相关问题的影响。

持久性是理想的,原因如下:

  • 如果Android操作系统销毁您的应用以释放资源,您的用户不会丢失数据。

  • 如果网络连接不稳定或无法使用,您的应用仍可继续使用。

通过将应用程序基于具有明确定义的数据管理职责的模型类,您的应用程序更具可测性和一致性。

Google推荐的应用架构

在本文中,我们将演示如何使用 Android Jetpack Components 构建应用程序,方法是使用端到端的用例。

注意:编写最适合每种情况的应用程序是不可能的。话虽这么说,这个推荐的架构是大多数情况和工作流程的良好起点。如果您已经有一种编写遵循通用架构原则的 Android 应用程序的好方法,则无需更改它。

想象一下,我们正在构建一个显示用户配置文件的UI。我们使用私有后端和REST API来获取给定配置文件的数据。

概述

首先,请考虑下图,该图显示了在设计应用程序后所有模块应如何相互交互:

640?wx_fmt=other

请注意,每个组件仅取决于其下一级的组件。例如,活动和片段仅依赖于视图模型。存储库是唯一依赖于其他多个类的类; 在此示例中,存储库依赖于持久数据模型和远程后端数据源。
这种设计创造了一种一致和愉快的用户体验。无论用户在上次关闭应用程序几分钟后还是几天后都回到应用程序,他们会立即看到应用程序在本地持续存在的用户信息。如果此数据过时,应用程序的存储库模块将开始从后台更新数据。

构建用户界面

UI由片段 UserProfileFragment 和相应的布局文件组成 user_profile_layout.xml 。

要驱动UI,我们的数据模型需要包含以下数据元素:

  • User ID:用户的标识符。最好使用Fragment参数将此信息传递到片段中。如果Android操作系统破坏了我们的流程,则会保留此信息,因此下次重新启动应用时ID就可用。

  • User object:包含用户详细信息的数据类。

我们使用 UserProfileViewModel 基于 ViewModel 的架构组件来保存此信息。

一个 ViewModel 对象提供针对特定 UI 组件中的数据,如一个 fragment 或 activity,并包含数据处理的业务逻辑与 model 进行通信。例如,ViewModel 可以调用其他组件来加载数据,它可以转发用户请求来修改数据。ViewModel 不知道UI组件,因此它不会受到配置更改的影响,例如旋转设备时,重新创建的 activity 。

我们现在定义了以下文件:

  • user_profile.xml:屏幕的UI布局定义。

  • UserProfileFragment:显示数据的UI控制器。

  • UserProfileViewModel:准备数据以供查看 UserProfileFragment 并对用户交互作出反应的类。

以下代码段显示了这些文件的起始内容。(为简单起见,省略了布局文件。

 
 

UserProfileFragment

 
 

现在我们有了这些代码模块,我们如何连接它们?毕竟,当user在UserProfileViewModel类中设置字段时,我们需要一种方法来通知UI。这就是LiveData架构组件的用武之地。

LiveData 是一个可观察的数据持有者。应用程序中的其他组件可以使用此> holder监视对象的更改,而无需在它们之间创建明确且严格的依赖关系路径。LiveData组件还尊重应用程序组件的生命周期状态(如activities, fragments, and services),并包括清除逻辑以防止对象泄漏和过多的内存消耗。

注意:如果您已经使用了像 RxJava 或 Agera 这样的库 ,则可以继续使用它们而不是 LiveData。但是,当您使用这些库和方法时,请确保正确处理应用程序的生命周期。特别是,确保在相关 LifecycleOwner 内容停止时暂停数据流,并在相关内容 LifecycleOwner 被销毁时销毁这些流。您还可以添加 android.arch.lifecycle:reactivestreams 组件以将 LiveData 与另一个反应流库(如RxJava2)一起使用。

要将LiveData组件合并到我们的应用程序中,我们更改 UserProfileViewModel 中的字段类型变成 LiveData。现在,在UserProfileFragment 更新数据时通知。此外,由于此 LiveData字段可识别生命周期,因此在不再需要引用后会自动清除引用。

UserProfileViewModel

 
 

现在我们修改UserProfileFragment观察数据并更新UI:

 
 

每次更新用户配置文件数据时, onChanged() 都会调用回调,并刷新UI。

如果您熟悉使用可观察回调的其他库,您可能已经意识到我们没有覆盖片段的onStop()方法来停止观察数据。LiveData不需要此步骤,因为它可识别生命周期,这意味着onChanged()除非片段处于活动状态,否则它不会调用回调。也就是说,它已收到onStart()但尚未收到onStop())。调用 fragment's 的onDestroy()方法时,LiveData也会自动删除观察者。

我们也没有添加任何逻辑来处理配置更改,比如用户旋转设备的屏幕。当配置发生变化时,UserProfileViewModel会自动恢复,因此一旦创建新的片段,它就会接收到相同的ViewModel实例,并且使用当前数据立即调用回调。鉴于ViewModel对象的目的是超越它们更新的相应视图对象,您不应该在ViewModel的实现中包含对视图对象的直接引用。有关ViewModel生命周期的更多信息对应于UI组件的生命周期,请参阅 ViewModel的生命周期。

请求数据

现在我们已经使用 LiveData 连接 UserProfileViewModel 到了 UserProfileFragment,我们如何获取用户配置文件数据?

对于此示例,我们假设我们的后端提供REST API。我们使用 Retrofit 库来访问我们的后端,尽管您可以自由地使用不同的库来实现相同的目的。

以下是我们 Webservice 与后端通信的定义:

Webservice

 
 

实现 ViewModel 的第一个想法可能就是直接调用 Webservice获取数据并将此数据分配给我们的LiveData对象。这种设计有效,但通过使用它,我们的应用程序随着它的发展变得越来越难以维护。它给 UserProfileViewModel 类带来了太多的责任 ,这违反了 关注点分离 原则。此外,ViewModel的范围与 Activity or Fragment 生命周期联系在一起,这意味着当关联的UI对象的生命周期结束时,来自Webservice的数据就会丢失。这种行为会产生不良的用户体验。

相反,我们的ViewModel将数据抓取过程委托给一个新的模块,即一个存储库。

存储库模块处理数据操作。它们提供了一个干净的API,以便应用程序的其余部分可以轻松地检索这些数据。他们知道从何处获取数据以及在更新数据时要进行的API调用。您可以将存储库视为不同数据源之间的调解器,例如持久性models,web services 和 caches。

我们的UserRepository类(如以下代码段所示)使用一个实例WebService来获取用户的数据:

UserRepository

 
 

即使存储库模块看起来不必要,它也有一个重要的目的:它从应用程序的其余部分抽象出数据源。现在,我们 UserProfileViewModel不知道如何获取数据,因此我们可以为视图模型提供从几个不同的数据获取实现获得的数据。

注意:为简单起见,我们省略了网络错误情况。有关公开错误和加载状态的替代实现,请参阅 附录:公开网络状态。

管理组件之间的依赖关系

UserRepository 上面的类需要一个 Webservice 获取用户数据的实例。它可以简单地创建实例,但要做到这一点,它还需要知道 Webservice 类的依赖关系。另外, UserRepository 可能不是唯一需要的Webservice 的类。这种情况要求我们复制代码,因为需要引用的每个类都需要 Webservice 知道如何构造它及其依赖项。如果每个类创建一个新的WebService,我们的应用程序可能会变得非常消耗资源。

您可以使用以下设计模式来解决此问题:

  • 依赖注入(DI):依赖注入允许类在不构造它们的情况下定义它们的依赖关系。在运行时,另一个类负责提供这些依赖项。我们建议使用 Dagger 2 库在Android应用程序中实现依赖注入。Dagger 2通过遍历依赖树自动构造对象,并为依赖关系提供编译时保证。
    服务定位器:服务定位器模式提供了一个注册表,其中类可以获取它们的依赖关系而不是构造它们。

  • 实现服务注册表比使用 依赖注入 更容易,因此如果您不熟悉DI,请改用服务定位器模式。
    这些模式允许您扩展代码,因为它们提供了清晰的模式来管理依赖项,而无需复制代码或增加复杂性。此外,这些模式允许您在测试和生产数据获取实现之间快速切换。

我们的示例应用程序使用Dagger 2来管理 Webservice对象的依赖项。

连接ViewModel和存储库

现在,我们修改我们 UserProfileViewModel 使用 UserRepository 对象:

UserProfileViewModel

 
 
缓存数据

UserRepository 实现将对 Webservice 对象的调用抽象出来,但因为它只依赖于一个数据源,它不是很灵活。

UserRepository实现的关键问题是,在它从我们的后端获取数据之后,它不会在任何地方存储这些数据。因此,如果用户离开 UserProfileFragment,然后返回到它,我们的应用程序必须重新取回数据,即使它没有改变。

由于以下原因,此设计不是最理想的:

  • 它浪费了宝贵的网络带宽。

  • 它强制用户等待新查询完成。

为了解决这些缺点,我们向 UserRepository 添加了一个新的数据源,它在内存中缓存 User 对象:

UserRepository

 
 
持续存储数据

使用我们当前的实现,如果用户旋转设备或离开并立即返回应用程序,现有的UI将立即可见,因为存储库从内存缓存中检索数据。

但是,如果用户离开应用程序并在Android操作系统杀死进程后几小时后回来会发生什么?在这种情况下依靠我们当前的实现,我们需要从网络再次获取数据。这种重新获取过程不仅仅是糟糕的用户体验; 这也是浪费,因为它消耗了宝贵的移动数据。

您可以通过缓存Web请求来解决此问题,但这会产生一个关键的新问题:如果相同的用户数据显示来自其他类型的请求,例如获取朋友列表,会发生什么?该应用程序将显示不一致的数据,这充其量令人困惑。例如,如果用户在不同时间发出好友列表请求和单用户请求,我们的应用可能会显示同一用户数据的两个不同版本。我们的应用程序需要弄清楚如何合并这些不一致的数据。
处理这种情况的正确方法是使用持久模型。这是Room persistence library来救援的地方。

Room 是一个对象映射库,它通过最少的样板代码提供本地数据持久性。在编译时,它会根据您的数据模式验证每个查询,因此损坏的SQL查询会导致编译时错误而不是运行时故障。房间抽象出了使用原始SQL表和查询的一些底层实现细节。它还允许您观察对数据库数据的更改,包括收藏品和连接查询,使用LiveData对象公开这些更改。它甚至显式地定义了处理常见线程问题的执行约束,比如访问主线程上的存储器。

注意:如果您的应用已经使用了其他持久性解决方案,例如SQLite对象关系映射(ORM),则无需使用Room替换现有解决方案。但是,如果您正在编写新应用或重构现有应用,我们建议您使用Room来保留应用的数据。这样,您就可以利用库的抽象和查询验证功能。

要使用Room,我们需要定义本地模式。首先,我们将@Entity注释添加 到User数据模型类中,并将 @PrimaryKey注释添加到类的id字段中。这些注释标记User为数据库id中的表和表的主键:

User

 
 

然后,我们通过实现 RoomDatabase 我们的应用程序来创建数据库类 :

UserDatabase

 
 

注意这UserDatabase是抽象的。Room自动提供它的实现。有关详细信息,请参阅Room 文档。

我们现在需要一种将用户数据插入数据库的方法。对于此任务,我们创建了一个数据访问对象(DAO)

UserDao

 
 

请注意,该load方法返回一个类型的对象LiveData。Room 知道数据库何时被修改,并在数据发生变化时自动通知所有活动观察者。由于Room使用LiveData,因此该操作非常有效; 它仅在至少有一个活动观察者时才更新数据。

注意:Room根据表格修改检查失效,这意味着它可能会发送误报通知。

在UserDao定义了我们的类之后,我们从数据库类中引用DAO:

UserDatabase

 
 

现在我们可以修改我们 UserRepository 以合并Room数据源:

 
 

请注意,即使我们更改了数据的来源 UserRepository,我们也不需要更改我们的 UserProfileViewModel 或 UserProfileFragment。这个小范围的更新展示了我们的应用程序架构提供的灵活性。它也非常适合测试,因为我们可以提供假数据 UserRepository 并同时测试我们的产品 UserProfileViewModel。

如果用户在返回使用此体系结构的应用程序之前等待几天,那么在存储库可以获取更新信息之前,他们可能会看到过时的信息。根据您的使用情况,您可能不希望显示此过时信息。相反,您可以显示占位符数据,该数据显示虚拟值并指示您的应用当前正在获取并加载最新信息。

单一的真实来源

不同的REST API端点返回相同的数据是很常见的。例如,如果我们的后端有另一个端点,它返回一个朋友列表,同一个用户对象可能来自两个不同的API端点,甚至可能使用不同的粒度级别。如果UserRepository原样返回来自Webservice请求的响应,而不检查一致性,我们的ui可能会显示令人困惑的信息,因为存储库中数据的版本和格式将取决于最近调用的端点。

因此,我们的UserRepository实现将web服务响应保存到数据库中。对数据库的更改会触发对活动LiveData对象的回调。使用这个模型,数据库可作为单一真实来源,应用程序的其他部分可使用UserRepository访问它。无论您是否使用磁盘缓存,我们建议您的存储库指定一个数据源作为应用程序其余部分的唯一真实来源。

显示正在进行的操作

在某些用例中,例如pull-to-refresh,UI向用户显示当前正在进行网络操作非常重要。将UI操作与实际数据分开是一种很好的做法,因为数据可能会因各种原因而更新。例如,如果我们获取了一个好友列表,可能会再次以编程方式获取相同的用户,从而触发 LiveData更新。从UI的角度来看,有请求在运行这一事实只是另一个数据点,类似于User对象本身的任何其他数据。

我们可以使用以下策略之一在UI中显示一致的数据更新状态,无论更新数据的请求来自何处:

  • 更改getUser()以返回类型的对象LiveData。该对象将包括网络操作的状态。有关示例,请参阅 NetworkBoundResource 中 android-architecture-components GitHub项目中的实现。

  • 在UserRepository类中提供另一个可以返回刷新状态的公共函数User。如果要仅在数据获取过程源自显式用户操作(例如,下拉刷新pull-to-refresh)时才在UI中显示网络状态,则此选项会更好。

测试每个组件

在关注点分离部分,我们提到遵循这一原则的一个关键好处是可测试性。

以下列表显示了如何从扩展示例中测试每个代码模块:

用户界面和交互:使用Android UI工具测试。创建此测试的最佳方法是使用 Espresso库。您可以创建片段并为其提供模拟 UserProfileViewModel。因为片段只与片段进行通信,所以 UserProfileViewModel 模拟这个类就足以完全测试应用的UI。

ViewModel:您可以 UserProfileViewModel使用JUnit测试来测试该类。你只需要模拟一个类,UserRepository。

UserRepository:您也可以使用JUnit test来测试 UserRepository。您需要模拟 Webservice 和 UserDao。在这些测试中,验证以下行为:

  • 存储库进行正确的Web服务调用。

  • 存储库将结果保存到数据库中。

  • 如果数据被缓存并且是最新的,则存储库不会发出不必要的请求。

因为Webservice和UserDao都是接口,所以您可以对它们进行模拟,或者为更复杂的测试用例创建假数据的实现。

-UserDao:使用检测测试来测试DAO类。由于这些检测测试不需要任何UI组件,因此它们可以快速运行。

对于每个测试,创建一个内存数据库以确保测试没有任何副作用,例如更改磁盘上的数据库文件。

注意:Room允许指定数据库实现,因此可以通过提供JUnit实现来测试DAO 。但是,不建议使用此方法,因为设备上运行的SQLite版本可能与开发计算机上的SQLite版本不同。
SupportSQLiteOpenHelper

  • Web服务:在这些测试中,避免对后端进行网络调用。对于所有测试,尤其是基于Web的测试,独立于外部世界非常重要。
    包括 MockWebServer 在内的几个库 可以帮助您为这些测试创建虚假的本地服务器。

  • 测试工件:Architecture Components提供了一个maven工件来控制其后台线程。该 android.arch.core:core-testing 工件包含以下JUnit的规则:

  • InstantTaskExecutorRule:使用此规则立即执行调用线程上的任何后台操作。

  • CountingTaskExecutorRule:使用此规则等待架构组件的后台操作。您还可以将此规则与 Espresso 关联为空闲资源。

最佳做法

编程是一个创造性的领域,构建Android应用程序也不例外。有许多方法可以解决问题,无论是在多个活动或片段之间传递数据,检索远程数据并在本地持久保存以用于脱机模式,还是任何其他非常重要的应用程序遇到的常见场景。

虽然以下建议不是强制性的,但我们的经验是,遵循它们可以使您的代码库在长期运行中更加健壮,可测试和可维护:

  • 避免将应用的入口点(如活动,服务和广播接收器)指定为数据源。
    相反,它们应该只与其他组件进行协调,以检索与该入口点相关的数据子集。每个应用程序组件都是相当短暂的,这取决于用户与设备的交互以及系统的整体当前健康状况。

  • 在应用的各个模块之间创建明确定义的责任范围。
    例如,不要将代码库中的数据加载到代码库中的多个类或包中。同样,不要将多个不相关的职责(例如数据缓存和数据绑定)定义到同一个类中。

  • 从每个模块尽可能少地暴露。
    不要试图创建“只是那个”的快捷方式,从一个模块公开内部实现细节。您可能会在短期内获得一些时间,但随着代码库的发展,您会多次承担技术债务。

  • 考虑如何使每个模块独立可测试。
    例如,具有用于从网络获取数据的定义良好的API使得更容易测试将该数据保存在本地数据库中的模块。相反,如果您将这两个模块的逻辑混合在一个地方,或者在整个代码库中分发网络代码,那么测试就变得更加困难 - 如果不是不可能的话。

  • 专注于您应用的独特核心,以便从其他应用中脱颖而出。
    不要一次又一次地编写相同的样板代码来重新发明轮子。相反,请将时间和精力集中在使应用程序独一无二的地方,并让Android架构组件和其他推荐的库处理重复的样板。

  • 保持尽可能多的相关和新鲜数据。
    这样,即使设备处于离线模式,用户也可以享受应用的功能。请记住,并非所有用户都享受恒定的高速连接。

  • 将一个数据源指定为单一事实来源。
    每当您的应用需要访问此数据时,它应始终源于此单一事实来源。

附录:暴露网络状态

在上面 推荐的应用程序架构 部分中,我们省略了网络错误和加载状态以保持代码片段的简单性。

本节演示如何使用Resource封装数据及其状态的类来公开网络状态。

以下代码段提供了以下示例实现Resource:

 
 

因为在显示该数据的磁盘副本时从网络加载数据是很常见的,所以创建一个可以在多个位置重用的帮助程序类是很好的。在本例中,我们创建了一个名为的类NetworkBoundResource。

下图显示了以下决策树NetworkBoundResource:

640?wx_fmt=other

它首先观察资源的数据库。第一次从数据库加载条目时,NetworkBoundResource检查结果是否足以分派或是否应从网络重新获取。请注意,这两种情况都可能同时发生,因为您可能希望在从网络更新缓存数据时显示缓存数据。

如果网络调用成功完成,它会将响应保存到数据库中并重新初始化流。如果网络请求失败,则 NetworkBoundResource直接发送失败。

注意:将新数据保存到磁盘后,我们从数据库重新初始化流。但是,我们通常不需要这样做,因为数据库本身恰好发送了更改。

请记住,依赖数据库来分派更改涉及依赖于相关的副作用,这是不好的,因为如果数据库因为数据没有更改而最终没有调度更改,则可能会发生这些副作用的未定义行为。

此外,不要发送从网络到达的结果,因为这会违反单一的真实原则。毕竟,数据库可能包含在“保存”操作期间更改数据值的触发器。同样,不要在没有SUCCESS新数据的情况下进行调度,因为那时客户端会收到错误版本的数据。

以下代码段显示了NetworkBoundResource类为其子级提供的公共API :

 
 

请注意有关类定义的这些重要细节:

它定义了两个类型的参数,ResultType并且RequestType,因为从API返回的数据类型可能不符合当地使用的数据类型。

它使用一个ApiResponse为网络请求调用的类。ApiResponse是一个简单的包装Retrofit2.Call类,它将响应转换为实例LiveData。

NetworkBoundResource该类的完整实现作为android-architecture-components GitHub项目的一部分出现 。

创建后NetworkBoundResource,我们可以用它来写我们的磁盘和网络结合实现User的UserRepository类:

UserRepository

Content and code samples on this page are subject to the licenses described in the Content License. Java is a registered trademark of Oracle and/or its affiliates.

更多学习和讨论,欢迎加入我们的知识星球,这里有1000+小伙伴,让你的学习不寂寞~·

看完本文有收获?请转发分享给更多人


我们的知识星球第三期开期了,已达到1100人了,能连续做三期已很不容易了,有很多老用户续期,目前续期率达到50%,说明了大家对我们的知识星球还是很认可的,欢迎大家加入尽早我们的知识星球,更多星球信息参见:

欢迎加入Java和Android架构社群

如何进阶成为Java的Android版和架构师?

说两件事

640?wx_fmt=jpeg

微信扫描或者点击上方二维码领取的Android \ Python的\ AI \的Java等高级进阶资源

更多学习资料点击下面的“阅读原文 ”获取

640?wx_fmt=gif

谢谢老板,点个好看↓

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值