组件化开发

为什么要组件化开发

组件化的好处

如何组件化

通过一个config.gradle文件,配置是否开启组件化, 并且统一控制版本。

ext {

    // 定义一个项目全局变量isRelease,用于动态切换:组件化模式 / 集成化模式
    // false: 组件化模式(子模块可以独立运行),true :集成化模式(打包整个项目apk,子模块不可独立运行)
    isRelease = true


    // 包名,用于存放APT生成的类文件
    packageNameForAPT = "com.sanguine.gradle.demo.one"

    androidId = [
            compileSdkVersion: 29,
            buildToolsVersion: "29.0.1",
            minSdkVersion    : 21,
            targetSdkVersion : 29,
            versionCode      : 1,
            versionName      : "1.0.0.1"
    ]

    appcompatVersion = "1.1.0"

    ids = [
            "app"     : "com.sanguine.gradle.demo.one",
            "personal": "com.sanguine.gradle.demo.modular.personal",
            "order"     : "com.sanguine.gradle.demo.modular.order"
    ]

    dependencies = [
            "appcompat"       : "androidx.appcompat:appcompat:${appcompatVersion}",
            "recyclerview": "com.android.support:recyclerview-v7:${appcompatVersion}",
            "constraintlayout": "androidx.constraintlayout:constraintlayout:1.1.3",
            "okhttp3"         : "com.squareup.okhttp3:okhttp:3.10.0",
            "retrofit"        : "com.squareup.retrofit2:retrofit:2.5.0",
            "fastjson"        : "com.alibaba:fastjson:1.2.58",
    ]
}

其它module中,使用统一版本的即可,例如order的module中,build.gradle文件可以这么写

if (isRelease) { // 如果是发布版本时,各个模块都不能独立运行
    apply plugin: 'com.android.library'
} else {
    apply plugin: 'com.android.application'
}


def rootApplicationId = rootProject.ext.androidId
def supportDependencies = rootProject.ext.dependencies
def appId = rootProject.ext.ids

android {
    compileSdkVersion rootApplicationId.compileSdkVersion
    buildToolsVersion rootApplicationId.buildToolsVersion
    defaultConfig {
        if (!isRelease){
            applicationId appId.order
        }
        minSdkVersion rootApplicationId.minSdkVersion
        targetSdkVersion rootApplicationId.targetSdkVersion
        versionCode rootApplicationId.versionCode
        versionName rootApplicationId.versionName
        // 这个方法接收三个非空的参数,第一个:确定值的类型,第二个:指定key的名字,第三个:传值(必须是String)
        // 为什么需要定义这个?因为src代码中有可能需要用到跨模块交互,如果是组件化模块显然不行
        // 切记:不能在android根节点,只能在defaultConfig或buildTypes节点下
        buildConfigField("boolean", "isRelease", String.valueOf(isRelease))

    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    // 遍历添加同一版本的依赖
    supportDependencies.each{ K,V -> implementation V}
    implementation project(':lib') // 公共基础库
}

这样,每个子module都这么定义的话, 如果开启了模块化,则在build.gradle文件,开头使用apply plugin: ‘com.android.application’,且增加appid属性,否则开头使用apply plugin: ‘com.android.library’,且没有appid属性。需要注意的是app这个module,因为是个壳工程,所以其build.gradle比较特殊一点,如下所示

apply plugin: 'com.android.application'


def rootApplicationId = rootProject.ext.androidId
def supportDependencies = rootProject.ext.dependencies
def appId = rootProject.ext.ids

android {
    compileSdkVersion rootApplicationId.compileSdkVersion
    buildToolsVersion rootApplicationId.buildToolsVersion
    defaultConfig {
        // app子模块在组件化开发中,默认为application
        applicationId appId.app
        minSdkVersion rootApplicationId.minSdkVersion
        targetSdkVersion rootApplicationId.targetSdkVersion
        versionCode rootApplicationId.versionCode
        versionName rootApplicationId.versionName

        // 这个方法接收三个非空的参数,第一个:确定值的类型,第二个:指定key的名字,第三个:传值(必须是String)
        // 为什么需要定义这个?因为src代码中有可能需要用到跨模块交互,如果是组件化模块显然不行
        // 切记:不能在android根节点,只能在defaultConfig或buildTypes节点下
        buildConfigField("boolean", "isRelease", String.valueOf(isRelease))
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    // 循环引入第三方库
    supportDependencies.each { K, V -> implementation V }
    implementation project(':lib') // 公共基础库
    // 如果是集成化模式,做发布版本时。各个模块都不能独立运行了
    if (isRelease) {
        implementation project(':order')
        implementation project(':personal')
    }
}

组件化交互

因为各个子模块不应该相互依赖增加耦合度,所以子模块交互是组件化开发最重要的点。常见的子模块之间的交互方式有

  • 全局Map,将各个子模块的Activity都以key-value的形式存在全局的Map中
    需要开发者手动将Activity注册到全局的Map中,工作量打,且不易维护。
  • 反射,通过反射技术找到Activity的Class,从而交互。
    容易出现拼写错误,如果Activity的路径修改了,需要修改代码适用新的路径。

apt+javapoet技术生成辅助类

本文中使用apt+javapoet技术,动态生成辅助类,从辅助类中取的Activity的Class,从而实现交互。最终实现的类如下所示:

public class ARouter$$Group$$order implements ARouterLoadGroup {
  @Override
  public Map<String, Class<? extends ARouterLoadPath>> loadGroup() {
    Map<String, Class<? extends ARouterLoadPath>> groupMap = new HashMap<>();
    groupMap.put("order", ARouter$$Path$$order.class);
    return groupMap;
  }
}
ARouter$$Group$$order

这个类用来以模块名为key,查找该模块中使用了注解的所有类。

public class ARouter$$Path$$order implements ARouterLoadPath {
  @Override
  public Map<String, RouterBean> loadPath() {
    Map<String, RouterBean> pathMap = new HashMap<>();
    pathMap.put("/order/getUserInfo", RouterBean.create(RouterBean.Type.CALL, OrderUserImpl.class, "/order/getUserInfo", "order"));
    pathMap.put("/order/Order_Main2Activity", RouterBean.create(RouterBean.Type.ACTIVITY, Order_Main2Activity.class, "/order/Order_Main2Activity", "order"));
    pathMap.put("/order/Order_MainActivity", RouterBean.create(RouterBean.Type.ACTIVITY, Order_MainActivity.class, "/order/Order_MainActivity", "order"));
    return pathMap;
  }
}
ARouter$$Path$$order

这个类用以path名为key,查找该path注册的Activity的信息,包括type,class,path路径,group名。以此可以通过path即可跳转到相应的Activity了。

注解类

新增java lib的module,编写注解类。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface ARouter {
    /**
     * 路由的路径
     *
     * @return
     */
    String path();

    /**
     * 路由的组名
     *
     * @return
     */
    String group() default "";
}

group可以不传值, 可以通过配置信息读取到group的值。
使用的时候,直接在想注册的Activity的上面增加这个注解。

@ARouter(path = "/app/MainActivity")
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}

例如这样, 那么这个MainActivity 的注册path就为“/app/MainActivity”。

注解处理工具

使用注解处理工具类,处理ARouter 这个注解。新建java lib的module,配置javapoet依赖,并且解决控制台乱码问题。
这个module的gradle文件如下所示:

apply plugin: 'java-library'

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])


    // As-3.4.1 + gradle5.1.1-all + auto-service:1.0-rc4
    compileOnly'com.google.auto.service:auto-service:1.0-rc4'
    annotationProcessor'com.google.auto.service:auto-service:1.0-rc4'

    // 帮助我们通过类调用的形式来生成Java代码
    implementation "com.squareup:javapoet:1.9.0"
    // 引入annotation,处理@ARouter注解
    implementation project(':annotation')
}

// java控制台输出中文乱码
tasks.withType(JavaCompile) {
    options.encoding = "UTF-8"
}


sourceCompatibility = "7"
targetCompatibility = "7"

然后新建一个注解处理工具类。如下所示。

// AutoService则是固定的写法,加个注解即可
// 通过auto-service中的@AutoService可以自动生成AutoService注解处理器,用来注册
// 用来生成 META-INF/services/javax.annotation.processing.Processor 文件
@AutoService(Processor.class)
// 允许/支持的注解类型,让注解处理器处理
@SupportedAnnotationTypes({Constants.AROUTER_ANNOTATION_TYPES})
// 指定JDK编译版本
@SupportedSourceVersion(SourceVersion.RELEASE_7)
// 注解处理器接收的参数,从每个module中的build.gradle文件中赋值
@SupportedOptions({Constants.MODULE_NAME, Constants.APT_PACKAGE})
public class ARouterProcessor extends AbstractProcessor {
    // 操作Element工具类 (类、函数、属性都是Element)
    private Elements elementUtils;

    // type(类信息)工具类,包含用于操作TypeMirror的工具方法
    private Types typeUtils;

    // Messager用来报告错误,警告和其他提示信息
    private Messager messager;

    // 文件生成器 类/资源,Filter用来创建新的类文件,class文件以及辅助文件
    private Filer filer;

    // 子模块名,如:app/order/personal。需要拼接类名时用到(必传)ARouter$$Group$$order
    private String moduleName;

    // 包名,用于存放APT生成的类文件
    private String packageNameForAPT;

    // 临时map存储,用来存放路由组Group对应的详细Path类对象,生成路由路径类文件时遍历
    // key:组名"app", value:"app"组的路由路径"ARouter$$Path$$app.class"
    private Map<String, List<RouterBean>> tempPathMap = new HashMap<>();

    // 临时map存储,用来存放路由Group信息,生成路由组类文件时遍历
    // key:组名"app", value:类名"ARouter$$Path$$app.class"
    private Map<String, String> tempGroupMap = new HashMap<>();

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        elementUtils = processingEnv.getElementUtils();
        typeUtils = processingEnv.getTypeUtils();
        messager = processingEnv.getMessager();
        filer = processingEnv.getFiler();

        Map<String, String> options = processingEnv.getOptions();
        if (!EmptyUtils.isEmpty(options)) {
            moduleName = options.get(Constants.MODULE_NAME);
            packageNameForAPT = options.get(Constants.APT_PACKAGE);
            // 有坑:Diagnostic.Kind.ERROR,异常会自动结束,不像安卓中Log.e
            messager.printMessage(Diagnostic.Kind.NOTE, "moduleName >>> " + moduleName);
            messager.printMessage(Diagnostic.Kind.NOTE, "packageNameForAPT >>> " + packageNameForAPT);
        }

        // 必传参数判空(乱码问题:添加java控制台输出中文乱码)
        if (EmptyUtils.isEmpty(moduleName) || EmptyUtils.isEmpty(packageNameForAPT)) {
            throw new RuntimeException("注解处理器需要的参数moduleName或者packageName为空,请在对应build.gradle配置参数");
        }
    }

    /**
     * 相当于main函数,开始处理注解
     * 注解处理器的核心方法,处理具体的注解,生成Java文件
     *
     * @param annotations 使用了支持处理注解的节点集合
     * @param roundEnv    当前或是之前的运行环境,可以通过该对象查找的注解。
     * @return true 表示后续处理器不会再处理(已经处理完成)
     */
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        if (EmptyUtils.isEmpty(annotations) || roundEnv == null) {
            return false;
        }
        // 所有被@ARouter注解的元素集合
        Set<? extends Element> set = roundEnv.getElementsAnnotatedWith(ARouter.class);
        // 解析元素
        try {
            parseElements(set);
            return true;
        } catch (IOException e) {
            e.printStackTrace();
        }

        return true;
    }


    /**
     * 解析所有被 @ARouter 注解的 类元素集合
     *
     * @param annotations
     */
    private void parseElements(Set<? extends Element> annotations) throws IOException {
        // 通过Element工具类,获取Activity、Callback类型
        TypeElement activityType = elementUtils.getTypeElement(Constants.ACTIVITY);
        TypeElement callType = elementUtils.getTypeElement(Constants.CALL);
        // 显示类信息(获取被注解节点,类节点)这里也叫自描述 Mirror
        TypeMirror activityMirror = activityType.asType();
        TypeMirror callMirror = callType.asType();

        // 遍历所有@ARouter注解的元素
        for (Element element : annotations) {
            if (element != null) {
                // 获取每个元素类信息,用于比较,比较是否是Activity
                TypeMirror elementMirror = element.asType();
                messager.printMessage(Diagnostic.Kind.NOTE, "遍历元素信息:" + elementMirror.toString());
                // 获取每个类上的@ARouter注解中的注解值
                ARouter aRouter = element.getAnnotation(ARouter.class);
                // 路由详细信息,最终实体封装类
                RouterBean bean = new RouterBean.Builder()
                        .setElement(element)
                        .setGroup(aRouter.group())
                        .setPath(aRouter.path())
                        .build();
                // 高级判断:ARouter注解仅能用在类之上,并且是规定的Activity
                // 类型工具类方法isSubtype,相当于instance一样
                if (typeUtils.isSubtype(elementMirror, activityMirror)) {
                    bean.setType(RouterBean.Type.ACTIVITY);
                }else if(typeUtils.isSubtype(elementMirror,callMirror)){
                    bean.setType(RouterBean.Type.CALL);
                } else {
                    // 不匹配抛出异常,这里谨慎使用!考虑维护问题
                    throw new RuntimeException("@ARouter注解目前仅限用于Activity类之上");
                }
                // 赋值临时map存储,用来存放路由组Group对应的详细Path类对象
                valueOfPathMap(bean);
            }
        }
        // routerMap遍历后,用来生成类文件
        // 获取ARouterLoadGroup、ARouterLoadPath类型(生成类文件需要实现的接口)
        TypeElement groupLoadType = elementUtils.getTypeElement(Constants.AROUTE_GROUP); // 组接口
        TypeElement pathLoadType = elementUtils.getTypeElement(Constants.AROUTE_PATH); // 路径接口

        // 第一步:生成路由组Group对应详细Path类文件,如:ARouter$$Path$$app
        createPathFile(pathLoadType);
        // 第二步:生成路由组Group类文件(没有第一步,取不到类文件),如:ARouter$$Group$$app
        createGroupFile(groupLoadType, pathLoadType);
    }

    /**
     * 生成路由组Group对应详细Path,如:ARouter$$Path$$app
     *
     * @param pathLoadType ARouterLoadPath接口信息
     */
    private void createPathFile(TypeElement pathLoadType) throws IOException {
        // 判断是否有需要生成的类文件
        if (EmptyUtils.isEmpty(tempPathMap)) return;

        /**
         *  Map<String, RouterBean> loadPath();
         */
        // 定义返回值类型
        TypeName returnType = ParameterizedTypeName.get(
                ClassName.get(Map.class),
                ClassName.get(String.class),
                ClassName.get(RouterBean.class)

        );

        // 遍历分组,每一个分组创建一个路径类文件,如:ARouter$$Path$$app
        for (Map.Entry<String, List<RouterBean>> entry : tempPathMap.entrySet()) {
            // 方法配置:public Map<String, RouterBean> loadPath() {
            MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder(Constants.PATH_METHOD_NAME)
                    .addAnnotation(Override.class)
                    .addModifiers(Modifier.PUBLIC)
                    .returns(returnType);

            // 遍历之前:Map<String, RouterBean> pathMap = new HashMap<>();
            methodBuilder.addStatement("$T<$T, $T> $N = new $T<>()",
                    ClassName.get(Map.class),
                    ClassName.get(String.class),
                    ClassName.get(RouterBean.class),
                    Constants.PATH_PARAMETER_NAME,
                    ClassName.get(HashMap.class));

            // 开始遍历
            // 一个分组,如:ARouter$$Path$$app。有很多详细路径信息,如:/app/MainActivity、/app/OtherActivity
            List<RouterBean> pathList = entry.getValue();
            for (RouterBean bean : pathList) {
                // 类似String.format("hello %s net163 %d", "net", 163)通配符
                // pathMap.put("/app/MainActivity", RouterBean.create(
                //        RouterBean.Type.ACTIVITY, MainActivity.class, "/app/MainActivity", "app"));
                methodBuilder.addStatement("$N.put($S, $T.create($T.$L, $T.class, $S, $S))",
                        Constants.PATH_PARAMETER_NAME,// N
                        bean.getPath(),// S
                        ClassName.get(RouterBean.class),// T
                        ClassName.get(RouterBean.Type.class),// T
                        bean.getType(),// L
                        ClassName.get((TypeElement) bean.getElement()), // T
                        bean.getPath(), // S
                        bean.getGroup()); // S
            }

            // 遍历结束
            // return groupMap;
            methodBuilder.addStatement("return $N", Constants.PATH_PARAMETER_NAME);

            // 最终生成的类文件名
            //  entry.getKey() = bean.getGroup();,所以最后一个module一个文件
            String finalClassName = Constants.PATH_FILE_NAME + entry.getKey();
            messager.printMessage(Diagnostic.Kind.NOTE, "APT生成路由Path类文件:" +
                    packageNameForAPT + "." + finalClassName);

            // 生成类文件:ARouter$$Path$$app
            JavaFile.builder(packageNameForAPT,
                    TypeSpec.classBuilder(finalClassName)
                            .addSuperinterface(ClassName.get(pathLoadType))
                            .addModifiers(Modifier.PUBLIC)
                            .addMethod(methodBuilder.build())
                            .build())
                    .build()
                    .writeTo(filer);
            // 非常重要一步!!!!!路径文件生成出来了,才能赋值路由组tempGroupMap
            // 用以生成组Group的类文件
            tempGroupMap.put(entry.getKey(), finalClassName);
        }

    }

    /**
     * 生成路由组Group文件,如:ARouter$$Group$$app
     *
     * @param groupLoadType ARouterLoadGroup接口信息
     * @param pathLoadType  ARouterLoadPath接口信息
     */
    private void createGroupFile(TypeElement groupLoadType, TypeElement pathLoadType) throws IOException {
        // 判断是否有需要生成的类文件
        if (EmptyUtils.isEmpty(tempGroupMap) || EmptyUtils.isEmpty(tempPathMap)) return;

        // Map<String, Class<? extends ARouterLoadPath>>
        // 方法返回值
        TypeName methodReturn = ParameterizedTypeName.get(
                ClassName.get(Map.class),
                ClassName.get(String.class),
                // 某某Class是否属于ARouterLoadPath接口的实现类
                ParameterizedTypeName.get(ClassName.get(Class.class), WildcardTypeName.subtypeOf(ClassName.get(pathLoadType)))
        );

        // 方法声明
        MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder(Constants.GROUP_METHOD_NAME)
                .addAnnotation(Override.class)
                .addModifiers(Modifier.PUBLIC)
                .returns(methodReturn);

        // 方法体
        // Map<String, Class<? extends ARouterLoadPath>> groupMap = new HashMap<>();
        methodBuilder.addStatement("$T<$T, $T> $N = new $T<>()",
                ClassName.get(Map.class),
                ClassName.get(String.class),
                ParameterizedTypeName.get(ClassName.get(Class.class), WildcardTypeName.subtypeOf(ClassName.get(pathLoadType))),
                Constants.GROUP_PARAMETER_NAME,
                ClassName.get(HashMap.class));

        // 方法体
        // groupMap.put("order", ARouter$$Path$$order.class);
        // 方法内容配置
        for (Map.Entry<String, String> entry : tempGroupMap.entrySet()) {
            // 类似String.format("hello %s net163 %d", "net", 163)通配符
            // groupMap.put("main", ARouter$$Path$$app.class);
            methodBuilder.addStatement("$N.put($S, $T.class)",
                    Constants.GROUP_PARAMETER_NAME, // groupMap.put
                    entry.getKey(),
                    // 类文件在指定包名下
                    ClassName.get(packageNameForAPT, entry.getValue()));
        }

        // 方法体
        // return groupMap;
        methodBuilder.addStatement("return $N", Constants.GROUP_PARAMETER_NAME);
        // 最终生成的类文件名
        String finalClassName = Constants.GROUP_FILE_NAME + moduleName;
        messager.printMessage(Diagnostic.Kind.NOTE, "APT生成路由组Group类文件:" +
                packageNameForAPT + "." + finalClassName);

        JavaFile.builder(packageNameForAPT,
                TypeSpec.classBuilder(finalClassName)
                        .addModifiers(Modifier.PUBLIC)
                        .addSuperinterface(ClassName.get(groupLoadType))
                        .addMethod(methodBuilder.build()).build())
                .build().writeTo(filer);
    }

    /**
     * 赋值临时map存储,用来存放路由组Group对应的详细Path类对象,生成路由路径类文件时遍历
     *
     * @param bean 路由详细信息,最终实体封装类
     */
    private void valueOfPathMap(RouterBean bean) {
        // 校验注解入参的合法性, 要以"/"开头, group的名字要和module的名字一致
        if (checkRouterPath(bean)) {
            messager.printMessage(Diagnostic.Kind.NOTE, "RouterBean >>> " + bean.toString());
            // 从map中找key为bean.getGroup()的数据
            List<RouterBean> routerBeans = tempPathMap.get(bean.getGroup());
            if (EmptyUtils.isEmpty(routerBeans)) {
                // 如果从Map中找不到key为:bean.getGroup()的数据,就新建List集合再添加进Map
                routerBeans = new ArrayList<>();
                routerBeans.add(bean);
                tempPathMap.put(bean.getGroup(), routerBeans);
            } else {
                // 找到了key,直接加入List集合
                routerBeans.add(bean);
            }
        } else {
            messager.printMessage(Diagnostic.Kind.ERROR, "@ARouter注解未按规范配置,如:/app/MainActivity");
        }
    }

    /**
     * 校验@ARouter注解的值,如果group未填写就从必填项path中截取数据
     *
     * @param bean 路由详细信息,最终实体封装类
     */
    private boolean checkRouterPath(RouterBean bean) {
        if (bean == null) {
            return false;
        }
        String group = bean.getGroup();
        String path = bean.getPath();
        // @ARouter注解中的path值,必须要以 / 开头(模仿阿里Arouter规范)
        if (EmptyUtils.isEmpty(path) || !path.startsWith("/")) {
            messager.printMessage(Diagnostic.Kind.ERROR, "@ARouter注解中的path值,必须要以 / 开头");
            return false;
        }

        // 比如开发者代码为:path = "/MainActivity",最后一个 / 符号必然在字符串第1位
        if (path.lastIndexOf("/") == 0) {
            // 架构师定义规范,让开发者遵循
            messager.printMessage(Diagnostic.Kind.ERROR, "@ARouter注解未按规范配置,如:/app/MainActivity");
            return false;
        }

        // 从第一个 / 到第二个 / 中间截取,如:/app/MainActivity 截取出 app 作为group
        String finalGroup = path.substring(1, path.indexOf("/", 1));
        // @ARouter注解中的group有赋值情况
        if (!EmptyUtils.isEmpty(group) && !group.equals(moduleName)) {
            // 架构师定义规范,让开发者遵循
            messager.printMessage(Diagnostic.Kind.ERROR, "@ARouter注解中的group值必须和子模块名一致!");
            return false;
        } else {
            bean.setGroup(finalGroup);
        }
        return true;
    }
}

最终rebuild项目之后 , 就可以生成辅助类了,使用辅助类即可实现各个子模块之间的交互。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值