Android最佳实践(译)

来自Futurice 开发者们的Android开发最佳实践,原文地址https://github.com/futurice/android-best-practices
其中的很多道理可能比较浅显,但确实是Android开发中需要注意的。读后感觉有很多有价值的地方,因此翻译出来。

本文摘要

使用Gradle及其推荐的项目结构
将密码等敏感信息保存在gradle.properties文件中
不要自己编写网络框架,使用Volley和Okhttp使用
Jackson库解析JSON数据避免使用Guava,使用轻量级的库以免方法数超出65536上限
使用Fragment来展现UI使用Activity仅用于管理Fragment
布局资源等XML文件也是程序的重要部分,需要很好的组织管理起来
使用主题和样式以避免在布局文件中反复使用同一属性
对样式文件(styles.xml)进行适当拆分,避免使用一个单一冗长的样式文件
保持colors.xml的整洁,只定义颜色变量,避免引入业务相关的命名
保持dimens.xml的整洁,只定义通用的尺寸数值
VIewGroup的布局层次不能过深
使用WebView时需要注意内存泄露问题
使用Robolectric完成单元测试,使用Robotium进行UI测试
使用Genymotion模拟器
发布项目时进行混淆处理(使用ProGuard或者DexGuard)
对于简单数据的持久化处理可以使用SharedPreference,其他情况下使用ContentProvider

Android SDK

将Android SDK放置在与IDE工具无关的位置,这样可以避免在升级和更改IDE工具的时候发生问题。使用Linux系统时,请将IDE工具和Android SDK放置在用户目录下。如果放置在系统根目录下,启动程序时可能需要sudo权限,给操作带来不便。


构建系统(Build System)

请使用gradle。Ant的局限性更大而且使用起来更为繁琐,使用gradle,可以很轻松的完成以下工作:
* 为App构建不同的发布类型
* 创建简单的脚本化的任务
* 管理、下载依赖库
* 自定义的keystores
* 更多


Android Gradle同时也由谷歌官方在持续开发维护,作为官方的构建工具,有什么理由不用呢?

项目结构

有两种可选的项目结构:传统的Ant+Eclipse的项目结构和新近的Gradle+Android Studio的项目结构。应该使用新的项目结构,如果你的项目还在使用旧的项目结构,请逐步将他们迁移到新的项目结构中。

旧的项目结构
old-structure
├─ assets
├─ libs
├─ res
├─ src
│  └─ com/futurice/project
├─ AndroidManifest.xml
├─ build.gradle
├─ project.properties
└─ proguard-rules.pro
新的项目结构
new-structure
├─ library-foobar
├─ app
│  ├─ libs
│  ├─ src
│  │  ├─ androidTest
│  │  │  └─ java
│  │  │     └─ com/futurice/project
│  │  └─ main
│  │     ├─ java
│  │     │  └─ com/futurice/project
│  │     ├─ res
│  │     └─ AndroidManifest.xml
│  ├─ build.gradle
│  └─ proguard-rules.pro
├─ build.gradle
└─ settings.gradle

主要区别在于新的项目结构拆分了源码目录(main、androidTest)。Gradle的一个理念是可以分别管理不同发布版本对应的代码,例如可以将源码目录分为”paid”和”free“两个目录,差异化地管理付费版和免费版app的代码。
将app模块单独划分可以用于区别app模块和其他库模块。settting.gradle文件管理对这些库模块的引用。


Gradle配置

整体结构参考谷歌官方文档
Google’s guide on Gradle for Android

Gradle任务相关
可以使用Gradle替代之间的各种编译脚本(python, perl, shell等等)来构建Android工程。通过Gradle的官方文档Gradle’s documentation来了解更多细节。

密码的相关问题
在工程中的build.gradle文件里需要为构建发布(release)版本定义 signingConfigs节点. 以下是一些需要避免的问题:

下面这种做法是不可取的. 以下代码都会被纳入到版本管理中,因为build.gradle文件需要纳入到版本管理中.

signingConfigs {
    release {
        storeFile file("myapp.keystore")
        storePassword "password123"
        keyAlias "thekey"
        keyPassword "password789"
    }
}

正确的做法应该是这样的, 创建一个 gradle.properties文件,并且不要将该文件纳入到版本管理中:

KEYSTORE_PASSWORD=password123
KEY_PASSWORD=password789

该文件会被gradle自动引入, 于是就可以在 build.gradle文件中这样使用 gradle.properties文件中定义的变量:

signingConfigs {
    release {
        try {
            storeFile file("myapp.keystore")
            storePassword KEYSTORE_PASSWORD
            keyAlias "thekey"
            keyPassword KEY_PASSWORD
        }
        catch (ex) {
            throw new InvalidUserDataException("You should define KEYSTORE_PASSWORD and KEY_PASSWORD in gradle.properties.")
        }
    }
}

通过maven仓库来处理依赖,而不是引入本地jar包
如果直接在项目中引入本地jar包,那么该jar包对应的依赖库版本就被定死了,比如2.1.1。手动下载jar包并且处理版本问题是非常繁琐的。依赖于托管在Maven库中的依赖库,在gradle中添加依赖时附带版本号就能很容易第管理依赖库的版本问题。Gradle也非常鼓励使用这种依赖管理方式,例如:

dependencies {
    compile 'com.squareup.okhttp:okhttp:2.2.0'
    compile 'com.squareup.okhttp:okhttp-urlconnection:2.2.0'
}

添加Maven库依赖时尽量不要使用动态的版本号
避免使用例如 2.1.+这种形式的动态版本号,这会导致构建时使用的依赖版本不可控并且因为依赖库版本不同给项目带来不易发觉且难以追查的问题。最好使用例如 2.1.1这样具体的版本号,以利于构建过程的稳定。

IDE工具和文本编辑器

不管是用什么编辑工具,都需要工具能够很好地适应项目结构
选择什么编辑工具是个人的喜好问题,只要编译工具能很好地和当前项目结构相适应就好。
当前最推荐使用的IDE工具就是Android StudioAndroid Studio,因为它是Google官方的工具,与Gradle可以完美配合,同时使用新的项目结构。
也可以使用EclipseEclipse ADT,但是因为它使用的是Ant工具和旧的项目结构,因此可能需要做相应的配置。使用Vim一类的文本编辑工具也是可以的,类似的还有Sublime Text和Emacs。使用这些工具的时候,就只能使用命令行来运行Gradle和
adb命令了。如果在Eclipse中使用Gradle出现问题,在命令行下执行Gradle命令也是一个可能的解决办法。当然还是建议能够转向使用Android Studio,这是目前最推荐的Android开发工具。
不管是用什么开发工具,请确保使用Gradle构建项目并且使用新的项目结构,并且不要把开发工具本身特有的一些文件添加到版本管理中,例如Ant的build.xml文件。特别不要忘记在Ant的构建配置更改的时候也要同步修改 build.gradle文件。对于团队中的其他开发者,也不要强行让他们更换自己的开发工具。

关于使用的一些依赖库

Jackson是一个JSON序列化-反序列化的java库,我们发现Jackson处理JSON的时候表现更好,因为它支持多种处理JSON的方式:流处理、DOM树处理、以及传统的JSON-POJO数据绑定。但是需要注意,Jackson是一个比GSON更大的库,所以这需要看情况来选择,可能选择GSON更有利于避免方法数达到65536的上限。以下是Jackson的一些相关资料:Json-smart and Boon JSON

网络、缓存和图片. 有很多经过实际检验的处理网络请求的框架,可以给予这些框架实现自己的网络请求功能。推荐使用Volley or Retrofit.Volley同样提供了图片加载和缓存的功能. 如果使用Retrofit,考虑使用Picasso来加载和缓存图片, 使用 OkHttp来提升网络请求的效率.Retrofit, Picasso以及OkHttp 都是由Square开发的, 因此他们相互之间配合的很好. 一些相关的资料:OkHttp can also be used in connection with Volley.

RxJava是一个Reactive编程库,主要用来处理异步事件。他是一个非常稳定可控的编程模型,但是有时候也会造成困惑因为它和通常的java语法有所区别。建议使用该库搭建项目框架时要审慎考量。已经有一些项目在使用RxJava,你可以与以下这些开发者联系: Timo Tuominen, Olli Salonen, Andre Medeiros, Mark Voit, Antti Lammi, Vera Izrailit, Juha Ristolainen. 这里是几篇我们编写的关于RxJava的博客: [1], [2], [3], [4].如果你之前对响应式(Rx)编程缺乏经验,可以先从把它应用到处理API响应开始。然后,逐步把它运用到简单的UI事件处理,例如点击事件或者键盘输入事件。如果你有信心将响应式编程应用到整个项目结构上,请编写一份Javadoc文档来说明其中的关键部分。请考虑团队协作中的问题,因为有些不熟悉响应式编程的开发者可能需要花一些时间才能熟悉这种编程方式。尽力让他们理解响应式编程以及你的代码。

Retrolambda是一个可以将Lamba表达式语法引用到JDK8之前版本的Java库.如果你的代码是函数语言风格(例如RxJava
)的,该库可以让你的代码更加简洁同时可读性更好。使用该库之前需要先安装JDK8,然后在Android Studio的Structure对话框中设置使用的JDK路径为JDK8的路径。同时还需要设置JAVA8_HOMEJAVA7_HOME 环境变量。之后在整个工程的根目录下的build.gradle文件中设置以下依赖:

dependencies {
    classpath 'me.tatarka:gradle-retrolambda:2.4.1'
}

对于工程中每个模块的build.gradle文件,还需要添加如下部分:

apply plugin: 'retrolambda'
android {
    compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
}
retrolambda {
    jdk System.getenv("JAVA8_HOME")
    oldJdk System.getenv("JAVA7_HOME")
    javaVersion JavaVersion.VERSION_1_7
}

Android Studio提供对Java8 Lamba语法的代码帮助功能,如果需要编写Lamba表达式代码,可以先从以下步骤开始:

  • 任何只有一个方法的接口都是“Lamba友好”的,并且可以折叠成更加精简的语句。
  • 如果你对于参数之类的问题不是很明确,那么可以先使用正常的Java匿名内部类写法然后使用Android Studio来把它转换成Lamba表达式写法。

警惕dex的方法数限制,避免引入过多的库对于Android应用来说,当被打包为dex文件时,有硬性的65536方法数限制[1] [2] [3]。超过方法数限制会导致编译报错。由于这个原因,尽量使用较为轻量的方法数较少的库,并使用该工具dex-method-counts来判断使用哪些库能够让方法数保持在最大上限之下。特别是要避免使用Guava库,因为它包含了13000+个方法。


关于Activity和Fragment

关于在构建Android项目结构时Activity和Fragment的使用方式和组织结构,目前还没有一个统一的观点。Square甚至还有一个完全基于View来构建Android项目的框架a library for building architectures mostly with Views, 完全不需要使用Fragment。但是目前这并不是一种普遍做法,也不是开发者社区所推荐的组织项目结构的方式。
可以简单认为Fragment就是UI的一些碎片。也就是说,Fragment一般来说是跟UI相关的。Activity可以简单认为是控制器,它的关注点在生命周期和状态管理。但是在以下场景下也有例外情况(delivering transitions between screens), 以及 fragments might be used solely as controllers。我们的建议还是小心处理这个问题并且综合考虑可能带来的影响。无论是只基于Activity的,还是只基于Fragment或View的项目结构都可能有各自的缺点。以下也仅是一些建议,而并非是绝对的准则:

  • 不要使用嵌套Fragment , 因为以下问题 matryoshka bugs 会出现. 只有绝对有必要的时候才使用嵌套Fragment,例如ViewPage中的某一页内部还需要一个Fragment,或者是其他已经经过谨慎思考需要使用嵌套Fragment的场景。
  • 避免在Activity中编写过多代码。尽量让Activity作为轻量级的控制器存在,主要用于管理生命周期并且处理如Trasition一类的交互。推荐使用只具有一个单一Fragment的Activity而不是仅使用Activity。
  • 将UI相关代码放置在Activity的Fragment中,这有利于重用Fragment代码。例如在tab切换几个页面或者在应用到平板产品上时(多个Fragment在同一个页面)。避免仅使用Activity而不使用Fragment,除非必须这样做。
  • 不要过度使用Android级别的API(例如Intent)来处理应用内部的通信工作。这可能会影响到Android系统或者是其他应用,造成bug或者卡顿。例如,如果某一个应用再开机后立刻启动并且使用Intent来与其他App进行通信,就会造成数秒的卡顿。

Java包结构

Java包的项目结构基本上类似于MVC模式的项目结构。在Android里,Fragment和Activity是控制器;同时,他们也是用户界面的一部分,所以他们也是视图。

由于这些原因,很难严格界定Fragment和Activity扮演的角色究竟是控制器还是视图。所以比较好的处理方式就是所有Fragment放置在一个包里。如果遵守了之前所提及的一些建议,Activity可以放到顶层的包里。如果Activity数量较多,同样应该把Activity组织到一个包里。

类似MVC类型的项目结构,同样可以建立一个models模型类的包用放置POJO类以便于处理JSON解析和API响应,还需要一个views包放置自定义view。Adapter也是一个灰色地带,兼有数据和视图的属性。Adapter需要通过getView()方法来暴露仕途,所以也可以把Adapter放置到views包下的子包当中。

一些控制器类用于整个应用范围内并且与贴近Android系统,可以将他们放置在managers包里。其他一些工具类,例如”DataUtils”,放置在utils包里。与后台交互相关的类放置在netword包里。

最后,按从贴近后台处理到贴近用户界面的顺序管理包:

com.futurice.project
├─ network
├─ models
├─ managers
├─ utils
├─ fragments
└─ views
   ├─ adapters
   ├─ actionbar
   ├─ widgets
   └─ notifications

Resources资源相关

资源文件命名:最好使用资源类型作为名称前缀。例如fragment_contact_detail.xmlview_primary_button.xml
组织好布局资源文件:以下使一些建议
- 每个属性一行,前置4个空格
- android:id作为第一个属性
- android:layout_xxxx属性放置在前面
- style属性放置在最后
- 使用\>关闭标签
- 不要硬编码类似于android:text这类树形,使用Android Studio的设计专用属性

良好的布局xml文件应该像以下这样:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    >

    <TextView
        android:id="@+id/name"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true"
        android:text="@string/name"
        style="@style/FancyText"
        />

    <include layout="@layout/reusable_part" />

</LinearLayout>

例如android:layout_xxxx这种属性应该定义在布局xml里,其他的一些属性应该定义在style xml文件里。当然也有一些例外的情况,不过总体来说应该这样。推荐的原则是将布局(位置、边距、大小)以及内容(文字)类型的属性在布局xml中设置,其他与外观相关的属性(颜色、内边距、字体)应该定义在style xml中。
使用样式:所有的项目都应该使用样式,因为很多视图的外观都需要复用。
将style xml拆分:不要只使用一个styles.xml文件。Android SDK对样式xml文件的命名没有要求,只要是<style>节点即可。因此你可以定义例如styles.xmlstyles_home.xmlstyles_item_details.xmlstyles_forms.xml。与构建系统中的文件命名必须固定不同,资源文件的名称可以是任意的。
colors.xml是一个 *调色板,其中应该只有颜色名称和对应的RGB颜色值,不要针对不同的button名称来给颜色命名,
例如以下做法就是错误的:

<resources>
    <color name="button_foreground">#FFFFFF</color>
    <color name="button_background">#2A91BD</color>
    <color name="comment_background_inactive">#5F5F5F</color>
    <color name="comment_background_active">#939393</color>
    <color name="comment_foreground">#FFFFFF</color>
    <color name="comment_foreground_important">#FF9D2F</color>
    <color name="comment_shadow">#323232</color>

如果使用这种方式添加颜色值,很容易添加重复的色值。另外,这些颜色是与某种控件相关的,他们应该被定义在样式文件中,而不是颜色文件中。
正确的做法应该是这样:

<resources>

    <!-- grayscale -->
    <color name="white"     >#FFFFFF</color>
    <color name="gray_light">#DBDBDB</color>
    <color name="gray"      >#939393</color>
    <color name="gray_dark" >#5F5F5F</color>
    <color name="black"     >#323232</color>

    <!-- basic colors -->
    <color name="green">#27D34D</color>
    <color name="blue">#2A91BD</color>
    <color name="orange">#FF9D2F</color>
    <color name="red">#FF432F</color>

</resources>

向应用的设计人员索要颜色值。色值命名并非一定要像green、blue、red这种名称,像brand_primary、brand_secondary、brand_negative这种名称也是OK的。对colors.xml做这种管理能够很容易修改主题颜色或者对颜色做变换,同时也能清楚的看到正在使用多少种不同的颜色。通常对UI设计来说,不宜同时使用过多的颜色。
对dimens.xml也做同样的处理:dimens.xml可以如下这样组织:

<resources>

    <!-- font sizes -->
    <dimen name="font_larger">22sp</dimen>
    <dimen name="font_large">18sp</dimen>
    <dimen name="font_normal">15sp</dimen>
    <dimen name="font_small">12sp</dimen>

    <!-- typical spacing between two views -->
    <dimen name="spacing_huge">40dp</dimen>
    <dimen name="spacing_large">24dp</dimen>
    <dimen name="spacing_normal">14dp</dimen>
    <dimen name="spacing_small">10dp</dimen>
    <dimen name="spacing_tiny">4dp</dimen>

    <!-- typical sizes of views -->
    <dimen name="button_height_tall">60dp</dimen>
    <dimen name="button_height_normal">40dp</dimen>
    <dimen name="button_height_short">32dp</dimen>

</resources>

使用space_xxxx这种尺寸值来设置margin和padding,而不是硬编码。这样可以让应用的边距值有一个统一的样式,而且也比较容易修改。
strings.xml:使用类似于命名空间那种的命名风格来命名字符串,不要计较名称过于复杂的问题,这样更有利于组织字符串资源。
Bad

<string name="network_error">Network error</string>
<string name="call_failed">Call failed</string>
<string name="map_failed">Map loading failed</string>

Good

<string name="error.message.network">Network error</string>
<string name="error.message.call">Call failed</string>
<string name="error.message.map">Map loading failed</string>

避免过深的布局层次
例如下面这种布局xml:

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    >

    <RelativeLayout
        ...
        >

        <LinearLayout
            ...
            >

            <LinearLayout
                ...
                >

                <LinearLayout
                    ...
                    >
                </LinearLayout>

            </LinearLayout>

        </LinearLayout>

    </RelativeLayout>

</LinearLayout>

对于布局层次过深的这种情况,可能在设置布局的时候没有发生问题,但是在使用Java代码向布局中继续填充其他布局,导致布局层次进一步加深的时候,可能就会出现问题。例如性能问题,由于渲染过于复杂导致UI卡顿。另一个严重的问题就是 StackOverflowError。改进的方法就是使用RelativeLayout,或者使用merge标签,还可以进一步优化布局
警惕WebView的问题
不要使用WebView在客户端进行HTML的处理,让后端开发人员提供“纯净”的HTML。WebViews引用一个Activity Context的时候还可能发生内存泄漏,所以要将WebView绑定到ApplicationContext上。需要显示简单文本的时候,也不要使用WebView,使用TextView即可。


测试框架

Android SDK的测试框架还在逐步完善中,特别是UI测试。Android Gradle目前有一个测试任务connectedAndroidTest可以借助于JUnit面向Android的扩展用来运行JUnit测试,测试时需要连接到Android设备或者模拟器上,参考官方文档[1][2]
使用Robolectric只能进行单元测试,不能测试UI。该测试框架的为了提升开发速度,提供了无需连接设备就能进行的测试,特别适合对数据模型和视图模型进行单元测试。然而,使用该框架进行UI测试还很不完善。在测试UI元素、动画效果、对话框等问题上还存在很多问题。由于不在实际交互的状态下进行测试,可能还会产生其他一些问题。
Robotium让UI测试变得简单。使用该框架无需连接设备就能进行UI测试。测试用例类似以下形式:

solo.sendKey(Solo.MENU);
solo.clickOnText("More"); // searches for the first occurence of "More" and clicks on it
solo.clickOnText("Preferences");
solo.clickOnText("Edit File Extensions");
Assert.assertTrue(solo.searchText("rtf"));

模拟器

如果专业开发Android应用,建议购买Genymotion付费版。Genymotion比自带模拟器快很多,可以模拟联网、GPS等功能,也非常适合进行适配测试。因为需要在很多不同设备上进行测试所以Genymotion的费用相比购买一堆不同设备来说还是很便宜的。
需要注意的是,Genymotion并不支持所有的Google服务例如Google应用商店。还有时候可能需要测试特定制造商的API,例如三星手机的API,这种时候需要在真实的三星手机上进行测试。


关于混淆

Proguard用于Android来减小代码大小同时混淆代码增强安全性。是否启用代码混淆取决于工程配置,对于发布版apk,通常在Gradle配置中启用混淆:

buildTypes {
    debug {
        minifyEnabled false
    }
    release {
        signingConfig signingConfigs.release
        minifyEnabled true
        proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
    }
}

需要通过一个混淆配置文件来控制代码中哪些部分将被混淆,Android框架提供了一个默认配置,在SDK_HOME/tools/proguard/proguard-android.txt中。如果使用以上的Gradle配置,启动自定义的混淆规则,这些规则定义在my-project/app/proguard-rules.pro文件中。
混淆通常导致的问题就是因为ClassNotFoundException或者NoSuchFieldException导致程序崩溃。这可能是两个原因导致的:
1. 混淆移除了类、枚举、方法、域或者注解。
2. 混淆重命名了以上这些,对于需要名称的情况(例如反射),导致不能找到相应名称。

检查app/build/outputs/proguard/release/usage.txt看是否有对象被移除的问题,检查app/build/outputs/proguard/release/mapping.txt看是否有对象被混淆的问题。
使用keep选项防止混淆移除了必须的类或者方法等:

-keep class com.futurice.project.MyClass { *; }

使用keepnames选项防止混淆对必须的类或者方法进行重命名:

-keepnames class com.futurice.project.MyClass { *; }

这里是一个混淆配置文件示例,可以参考Proguard来了解更多配置和示例。
在程序开发的初期就构建发布版本,以便检查混淆规则是否正确保留了必须的类或者方法等。每次添加新的库,都要构建发布版本的apk进行测试。不要等到app已经开发差不多的时候在构建发布版本,可能会出现很多意想不到的问题需要花时间处理。

小贴士,每次发布app进行混淆时,都要保存mapping.txt文件,这样当发布版本发生问题,并且bug给出的堆栈信息是被混淆过时,可以通过该文件找到混淆类对应的实际类文件,更好地定位问题。

DexGuard
如果需要进一步优化,或者对发布版本的代码做特别的混淆,可以考虑使用DexGuard,它是由ProGuard开发团队开发的商用版本工具。该工具同样可以通过拆分dex文件来突破65536的方法数限制。


数据存储

SharedPreferences

如果只是存储一些标识项,并且app始终运行在同一进程中时,SharedPreferences就足够用了。
在以下两种情况下使用SharedPreferences可能不是很合适:
- 性能问题:数据可能很复杂或者数量很多
- 多进程数据访问问题:可能有运行在其他进程中的组件或服务需要进行数据同步

ContentProviders

在SharedPreferences 无法满足需求的时候,可以使用ContentProviders,它是进程安全的,同时速度很快。
ContentProviders 唯一的问题是设置起来代码量比较多,可以通过Schematic来生成ContentProvider,这减少了很多的工作量。

使用ORM

除非要经常处理复杂数据或者确实有其他必要的需求,否则不建议使用ORM库。如果使用了ORM,需要注意他是否是进程安全的,因为很多ORM解决方案都存在多进程访问的问题。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值