ARouter 源码分析

概述:

arouter-annotation: ARouter路由框架所使用的全部注解,及其相关类
arouter-compiler:注解编译处理器,引入“arouter-annotation”,在编译期完成了 构造路由表逻辑的创建
arouter-api:在运行期加载逻辑构建路由表,并实现路由控制

1.arouter-annotation注解

1.1 @Route路由注解

@Route 是 ARouter 最重要的注解,也是路由最基本的节点,该注解主要用于描述路由中的路径URL信息,使用该注解标注的类将被自动添加至路由表中。
值得说明的一点是 ARouter 并非仅提供页面(Activity)的路由功能,还可以用来路由模块想要暴露给其他模块调用的接口。

也就是说 @Route 不仅可用于 Activity 类,还可用于模块对外接口的实现类,实现类似于 AIDL 的功能,也就是IOC

1.2 @Interceptor拦截器注解

@Interceptor 是拦截器注解,拦截器是全应用全局的,不分module,只要集成进apk就起效

1.3 @Autowired自动装载注解

@Autowired 是页面跳转时参数传递用的。目标Class中使用该注解标志的变量,会在页面被路由打开的时候,在调用Inject(“`)后自动赋予传递的参数值

1.4 RouteMeta路由元信息

如果全部路由信息认为是一张表格,那么RouteMeta就是表格的一行,代表路由表的一条元信息

2.arouter-r注解编译器

实现了“自动注册映射关系”也就是在编译期间自动生成映射文件,所以该module其实就是实现了一些注解处理器,目标在于生成映射文件与辅助文件(构造路由表逻辑的创建)
compile-out

2.1 组别的清单列表 【工程名&&Root&&模块名】

Map< String, Class< ? extends IRouteGroup>> routes
包含了组名与对应组内的路由清单列表Class的映射关系
是Arouter的“分组管理,按需加载”的实现。
ARouter在初始化的时候只会一次性地加载所有的root结点,而不会加载任何一个Group结点,这样就会极大地降低初始化时加载结点的数量
那么什么时候加载分组结点呢?其实就是当某一个分组下的某一个页面第一次被访问的时候,整个分组的全部页面都会被加载进去,这就是ARouter的按需加载

2.1.1 组内的路由清单列表 【工程名&&Group&&分组名】

(Map< String, RouteMeta> atlas
包含了对应分组下的,路由URL与目标对象Class的映射关系;
注意Router注解中无分组的话,默认以“/xx/xx”的第一个xx为分组名

ARouter$$Root$$core
/**
 * DO NOT EDIT THIS FILE!!! IT WAS GENERATED BY AROUTER. */
public class ARouter$$Root$$core implements IRouteRoot {
  @Override
  public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {
    routes.put("core", ARouter$$Group$$core.class);
    routes.put("degrade", ARouter$$Group$$degrade.class);
    routes.put("service", ARouter$$Group$$service.class);
  }
}

degrade 特殊,同时在provider和group里?

degrade

2.2 Ioc的动作路由清单列表 【工程名&&Providers&&模块名】

Map< String, RouteMeta> providers
PROVIDER 类型的路由节点的清单列表
包含了使用依赖注入方式的某class(实现了IProvide接口的直接子类)的 路由URL 与class映射关系
目标Class都实现了IProvider接口,借此实现部分路由转到该清单中
需要注意的是:Ioc动作路由清单其实只是 Route注解的一种特殊用法,总的来说,还是一种URL与目标类的映射关系
其实想一下,依赖注入,无非也就是指定好目标接口的目标类,然而实例化后进行赋值。URL就是指定说明

/**
 * DO NOT EDIT THIS FILE!!! IT WAS GENERATED BY AROUTER. */
public class ARouter$$Providers$$core implements IProviderGroup {
  @Override
  public void loadInto(Map<String, RouteMeta> providers) {
    providers.put("com.vivo.test.core.service.HelloService", RouteMeta.build(RouteType.PROVIDER, HelloService.class, "/service/hello", "service", null, -1, -2147483648));
    providers.put("com.alibaba.android.arouter.facade.service.SerializationService", RouteMeta.build(RouteType.PROVIDER, JsonServiceImpl.class, "/service/json", "service", null, -1, -2147483648));
    providers.put("com.alibaba.android.arouter.facade.service.PathReplaceService", RouteMeta.build(RouteType.PROVIDER, PathReplaceServiceImpl.class, "/service/pathrepalce", "service", null, -1, -2147483648));
    providers.put("com.alibaba.android.arouter.facade.service.DegradeService", RouteMeta.build(RouteType.PROVIDER, DegradeServiceImpl.class, "/degrade/service", "degrade", null, -1, -2147483648));
  }
}

2.3 模块内的拦截器清单列表 【工程名&&Interceptors&&模块名】

Map< Integer, Class< ? extends IInterceptor>> interceptors
包含了某个模块下的拦截器 与 优先级的映射关系
一个模块下的所有拦截器都在该类中包含,无分组特性,所以直接以模块名命名类文件

/**
 * DO NOT EDIT THIS FILE!!! IT WAS GENERATED BY AROUTER. */
public class ARouter$$Interceptors$$core implements IInterceptorGroup {
  @Override
  public void loadInto(Map<Integer, Class<? extends IInterceptor>> interceptors) {
    interceptors.put(8, TestInterceptor.class);
  }
}

root 节点是包含activity,service,degrade 单独是一个service?

2.4 Autowired注解处理

分析下TestActivity1自动生成的路由辅助文件。

autowired

通过Arouter路由框架的IOc的ByType方式对SerializationService进行注入,该类为Json转换的工具类
注意助理使用的是ByType方式,也就是直接找到实现了SerializationService接口的唯一类进行实例化并注入,如果实现了SerializationService接口的有多个类,那么就会出现问题
所以全局应用的所有模块中,只能存在一个实现了SerializationService接口的类
获取目标对象实例
利用目标对象的对应传值方式,对目标对象的实例中的成员变量进行赋值
Acitivty使用的getIntent()—–由框架自身的参数传递决定,详见 4.3API部分
Fragment使用getArguments()—–由框架自身的参数传递决定,详见 4.3API部分
OBJ对象使用JSon辅助类进行实例化转换—– 详见 4.3API部分,传递参数时会将对象封装为json字符串
IOc依赖注入对象,默认使用byType方式,如果Autowired注解中有标识name,则使用name指向的类实例并赋值

3. arouter-api路由控制

arouter
最基础的就是Compiler这个SDK,其内部有三个处理器,分别是:Route Processor,Interceptor Processor以及Autowire Processor,通过名字就可以看出这三个处理器分别是处理路径路由、拦截器和进行自动装配的。而API的SDK是用户在运行期使用的,这一部分主要分为四层。
最上层是Launcher层,这一层是开发者可以直接用到的,其实所有的API都是在这一层中。
在Launcher层的下一层就是Frossard层,从上图中可以看到Frossard层也是绿色的,表示这一层也是可以被外部调用的,Frossard层其实包含了三部分,分别是:Service、Callback和Template,这里的Service概念和服务端的Service概念是相似的,也是在客户端的简单引申,但是却不同于Android组件中的Service,这里的Service是ARouter抽象出来的概念,从本质上讲,这里的Service是接口,从意义上讲是将一定的功能和组件封装成接口,并对外提供能力。Template则是模板,主要用于在编译期执行的SDK,这个SDK会在编译期生成一些映射文件,而这些映射文件会按照Template组件中提供的模板来生成,这样按照一定的规则和约束生成映射文件也方便Route在运行的时候进行读取。
再往下一层就完全是SDK的内部实现了,这一层包括了Ware House、Thread、Log、Exception以及Class工具。Ware House主要存储了ARouter在运行期间加载的一些配置文件以及映射关系;而Thread则是提供了线程池,因为存在多个拦截器的时候以及跳转过程中都是需要异步执行的;Class工具则是用于解决不同类型APK的兼容问题的。
再下一层就是Logistics Center,从名字上翻译就是物流中心,整个SDK的流转以及内部调用最终都会下沉到这一层,当然也会按照功能模块进行划分。

3.1 init过程

init

3.1.1 ARouter.init()

public static void init(Application application) {
    if (!hasInit) {
        logger = _ARouter.logger;
        _ARouter.logger.info(Consts.TAG, "ARouter init start.");
        hasInit = _ARouter.init(application);

        if (hasInit) {
            _ARouter.afterInit();
        }

        _ARouter.logger.info(Consts.TAG, "ARouter init over.");
    }
}

3.1.2 _ARouter.init(application)

protected static synchronized boolean init(Application application) {
    mContext = application;
    LogisticsCenter.init(mContext, executor);
    logger.info(Consts.TAG, "ARouter init success!");
    hasInit = true;
    mHandler = new Handler(Looper.getMainLooper());

    return true;
}

3.1.3 LogisticsCenter.init(mContext, executor);

public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException {
        mContext = context;
        executor = tpe;
        
        try {
            long startInit = System.currentTimeMillis();
            //billy.qi modified at 2017-12-06
            //load by plugin first
            loadRouterMap();
            if (registerByPlugin) {
                logger.info(TAG, "Load router map by arouter-auto-register plugin.");
            } else {
                Set<String> routerMap;

                // It will rebuild router map every times when debuggable.
                if (ARouter.debuggable() || PackageUtils.isNewVersion(context)) {
                    logger.info(TAG, "Run with debug mode or new install, rebuild router map.");
                    // These class was generated by arouter-compiler.
                    routerMap = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE);
                    if (!routerMap.isEmpty()) {
                        context.getSharedPreferences(AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE).edit().putStringSet(AROUTER_SP_KEY_MAP, routerMap).apply();
                    }

                    PackageUtils.updateVersion(context);    // Save new version name when router map update finishes.
                } else {
                    logger.info(TAG, "Load router map from cache.");
                    routerMap = new HashSet<>(context.getSharedPreferences(AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE).getStringSet(AROUTER_SP_KEY_MAP, new HashSet<String>()));
                }

                logger.info(TAG, "Find router map finished, map size = " + routerMap.size() + ", cost " + (System.currentTimeMillis() - startInit) + " ms.");
                startInit = System.currentTimeMillis();

                for (String className : routerMap) {
                    if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_ROOT)) {
                        // This one of root elements, load root.
                        ((IRouteRoot) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.groupsIndex);
                    } else if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_INTERCEPTORS)) {
                        // Load interceptorMeta
                        ((IInterceptorGroup) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.interceptorsIndex);
                    } else if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_PROVIDERS)) {
                        // Load providerIndex
                        ((IProviderGroup) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.providersIndex);
                    }
                }
            ...
        } catch (Exception e) {
            ...
        }
    }

3.1.4 Warehouse 路由仓库

class Warehouse {
    // Cache route and metas
    static Map<String, Class<? extends IRouteGroup>> groupsIndex = new HashMap<>();
    static Map<String, RouteMeta> routes = new HashMap<>();

    // Cache provider
    static Map<Class, IProvider> providers = new HashMap<>();
    static Map<String, RouteMeta> providersIndex = new HashMap<>();

    // Cache interceptor
    static Map<Integer, Class<? extends IInterceptor>> interceptorsIndex = new UniqueKeyTreeMap<>("More than one interceptors use same priority [%s]");
    static List<IInterceptor> interceptors = new ArrayList<>();

    static void clear() {
        routes.clear();
        groupsIndex.clear();
        providers.clear();
        providersIndex.clear();
        interceptors.clear();
        interceptorsIndex.clear();
    }
}

33 - 43 行:分组管理,按需加载
load
初始化时会加载内存应用的组别的清单列表、Ioc的动作路由清单列表、模块内的拦截器清单列表到路由仓库中Warehouse中,组内的路由清单列表此时并不会加载。

3.1.5 _ARouter.afterInit() 初始化拦截器控制器

根据 Ioc.ByName()方式获取拦截器控制器,注意这个拦截器并不是我们定义的拦截器,而是Arouter实现的拦截器逻辑,它持有我们定义的拦截器,可以理解为“拦截器截面控制器

static void afterInit() {
    // Trigger interceptor init, use byName.
    interceptorService = (InterceptorService) ARouter.getInstance().build("/arouter/service/interceptor").navigation();
}

@Route(path = "/arouter/service/interceptor")
public class InterceptorServiceImpl implements InterceptorService {
    private static boolean interceptorHasInit;
    private static final Object interceptorInitLock = new Object();

    @Override
    public void doInterceptions(final Postcard postcard, final InterceptorCallback callback) {
        if (null != Warehouse.interceptors && Warehouse.interceptors.size() > 0) {
            ...
        }
    }
}    

12-15行:拦截器控制器的主要逻其实就是获取Warehouse中所有的拦截器,在需要拦截时执行他们的拦截方法。

3.2 路由寻址过程

ARouter.getInstance().build("/core/activity").navigation();

3.2.1 build Postcard

// ARouter.build

public Postcard build(String path) {
    return _ARouter.getInstance().build(path);
}

// _ARouter.build
protected Postcard build(String path) {
    if (TextUtils.isEmpty(path)) {
        throw new HandlerException(Consts.TAG + "Parameter is invalid!");
    } else {
        PathReplaceService pService = ARouter.getInstance().navigation(PathReplaceService.class);
        if (null != pService) {
            path = pService.forString(path);
        }
        return build(path, extractGroup(path));
    }
}

protected Postcard build(String path, String group) {
    if (TextUtils.isEmpty(path) || TextUtils.isEmpty(group)) {
        throw new HandlerException(Consts.TAG + "Parameter is invalid!");
    } else {
        PathReplaceService pService = ARouter.getInstance().navigation(PathReplaceService.class);
        if (null != pService) {
            path = pService.forString(path);
        }
        return new Postcard(path, group);
    }
}

第9和21行分别是2种build Postcard的方式,可以看到build(path)也是调用了build(String path, String group)。如果采用的是build(path),group字段默认取"/group/path/“第一个”/“和第二个”/“之的字段,当有多个”/group/path/childpath/",第二个"/"之后都算path。

13、25行 展示了PathReplaceService 的工作原理

29 完成创建Postcard

3.2.2 navigation 过程

省略Arouter调用过程,直接看最终的_ARouter.navigation,一次路由的跳转包含查找回调的调用、拦截器处理、绿色通道校验、和具体路由操作。

protected Object navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) {
    try {
        LogisticsCenter.completion(postcard);
    } catch (NoRouteFoundException ex) {
        ...
        if (null != callback) {
            callback.onLost(postcard);
        } else {    // No callback for this invoke, then we use the global degrade service.
            DegradeService degradeService = ARouter.getInstance().navigation(DegradeService.class);
            if (null != degradeService) {
                degradeService.onLost(context, postcard);
            }
        }

        return null;
    }

    if (null != callback) {
        callback.onFound(postcard);
    }

    if (!postcard.isGreenChannel()) {   // It must be run in async thread, maybe interceptor cost too mush time made ANR.
        interceptorService.doInterceptions(postcard, new InterceptorCallback() {
            /**
             * Continue process
             *
             * @param postcard route meta
             */
            @Override
            public void onContinue(Postcard postcard) {
                _navigation(context, postcard, requestCode, callback);
            }

            /**
             * Interrupt process, pipeline will be destory when this method called.
             *
             * @param exception Reson of interrupt.
             */
            @Override
            public void onInterrupt(Throwable exception) {
                if (null != callback) {
                    callback.onInterrupt(postcard);
                }

                logger.info(Consts.TAG, "Navigation failed, termination by interceptor : " + exception.getMessage());
            }
        });
    } else {
        return _navigation(context, postcard, requestCode, callback);
    }

    return null;
}

第3行:完善路由信息,详细分析见3.2.4节
6-13行:路由异常时处理,可以看到,手动添加了降级处理的callback要优先于全局的降级处理全局的降级处理,在没有主动添加降级处理时才会触发全局的降级。

22-47行:拦截器的工作原理,在没有设置为greenChannel时,会调用到拦截器控制器的doInterceptions,循环遍历所有的拦截器处理拦截逻辑,结合3.1.5内容理解。

第49行:真正的路由过程

3.2.3 真正的navigation过程

private Object _navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) {
    final Context currentContext = null == context ? mContext : context;

    switch (postcard.getType()) {
        case ACTIVITY:
            // Build intent
            final Intent intent = new Intent(currentContext, postcard.getDestination());
            intent.putExtras(postcard.getExtras());

            // Set flags.
            int flags = postcard.getFlags();
            if (-1 != flags) {
                intent.setFlags(flags);
            } else if (!(currentContext instanceof Activity)) {    // Non activity, need less one flag.
                intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            }

            // Set Actions
            String action = postcard.getAction();
            if (!TextUtils.isEmpty(action)) {
                intent.setAction(action);
            }

            // Navigation in main looper.
            runInMainThread(new Runnable() {
                @Override
                public void run() {
                    startActivity(requestCode, currentContext, intent, postcard, callback);
                }
            });

            break;
        case PROVIDER:
            return postcard.getProvider();
        case BOARDCAST:
        case CONTENT_PROVIDER:
        case FRAGMENT:
            Class fragmentMeta = postcard.getDestination();
            try {
                Object instance = fragmentMeta.getConstructor().newInstance();
                if (instance instanceof Fragment) {
                    ((Fragment) instance).setArguments(postcard.getExtras());
                } else if (instance instanceof android.support.v4.app.Fragment) {
                    ((android.support.v4.app.Fragment) instance).setArguments(postcard.getExtras());
                }

                return instance;
            } catch (Exception ex) {
                logger.error(Consts.TAG, "Fetch fragment instance error, " + TextUtils.formatStackTrace(ex.getStackTrace()));
            }
        case METHOD:
        case SERVICE:
        default:
            return null;
    }

    return null;
}

5-30行:处理Activity的跳转,可以看到ARouter底层启动Activity时也是通过系统startActivity方法。同时在这里会设置intent的action,flag,另外postcard.getDestination() 返回的其实是.Class对象。

33-47行:处理PROVIDER、BOARDCAST、CONTENT_PROVIDER、FRAGMENT,返回一个对应类的实例,如果是Fragment,则返回实例,并填充bundle,另外这里的provider指的是ARouter的服务IProvider。

3.2.4 完善路由信息 LogisticsCenter.completion(postcard)

public synchronized static void completion(Postcard postcard) {
        if (null == postcard) {
            throw new NoRouteFoundException(TAG + "No postcard!");
        }

        RouteMeta routeMeta = Warehouse.routes.get(postcard.getPath());
        if (null == routeMeta) {    // Maybe its does't exist, or didn't load.
            Class<? extends IRouteGroup> groupMeta = Warehouse.groupsIndex.get(postcard.getGroup());  // Load route meta.
            if (null == groupMeta) {
                throw new NoRouteFoundException(TAG + "There is no route match the path [" + postcard.getPath() + "], in group [" + postcard.getGroup() + "]");
            } else {
                // Load route and cache it into memory, then delete from metas.
                try {

                    IRouteGroup iGroupInstance = groupMeta.getConstructor().newInstance();
                    iGroupInstance.loadInto(Warehouse.routes);
                    Warehouse.groupsIndex.remove(postcard.getGroup());

                } catch (Exception e) {
                    throw new HandlerException(TAG + "Fatal exception when loading group meta. [" + e.getMessage() + "]");
                }

                completion(postcard);   // Reload
            }
        } else {
            postcard.setDestination(routeMeta.getDestination());
            postcard.setType(routeMeta.getType());
            postcard.setPriority(routeMeta.getPriority());
            postcard.setExtra(routeMeta.getExtra());

            Uri rawUri = postcard.getUri();
            if (null != rawUri) {   // Try to set params into bundle.
                Map<String, String> resultMap = TextUtils.splitQueryParameters(rawUri);
                Map<String, Integer> paramsType = routeMeta.getParamsType();

                if (MapUtils.isNotEmpty(paramsType)) {
                    // Set value by its type, just for params which annotation by @Param
                    for (Map.Entry<String, Integer> params : paramsType.entrySet()) {
                        setValue(postcard,
                                params.getValue(),
                                params.getKey(),
                                resultMap.get(params.getKey()));
                    }

                    // Save params name which need auto inject.
                    postcard.getExtras().putStringArray(ARouter.AUTO_INJECT, paramsType.keySet().toArray(new String[]{}));
                }

                // Save raw uri
                postcard.withString(ARouter.RAW_URI, rawUri.toString());
            }

            switch (routeMeta.getType()) {
                case PROVIDER:  // if the route is provider, should find its instance
                    // Its provider, so it must implement IProvider
                    Class<? extends IProvider> providerMeta = (Class<? extends IProvider>) routeMeta.getDestination();
                    IProvider instance = Warehouse.providers.get(providerMeta);
                    if (null == instance) { // There's no instance of this provider
                        IProvider provider;
                        try {
                            provider = providerMeta.getConstructor().newInstance();
                            provider.init(mContext);
                            Warehouse.providers.put(providerMeta, provider);
                            instance = provider;
                        } catch (Exception e) {
                            throw new HandlerException("Init provider failed! " + e.getMessage());
                        }
                    }
                    postcard.setProvider(instance);
                    postcard.greenChannel();    // Provider should skip all of interceptors
                    break;
                case FRAGMENT:
                    postcard.greenChannel();    // Fragment needn't interceptors
                default:
                    break;
            }
        }
    }

6 - 23行:

1.根据路径URL获取到路径元信息 
2.如果未获取到路径元信息,可能是由于 未加载对应分组的【组内清单列表】 or 的确没有
3.从【组别的清单列表】拿到对应组的组内清单创建逻辑,如果为空,则丢出异常,未找到,不为空时,走如下逻辑(15 - 18行):
(1)实例化【组内清单创建逻辑】
(2)将该组的【组内清单列表】加入到内存仓库中
(3)从【组别的清单列表】移除当前组 
(4)第23行重新加载路径元信息

26 - 29行:

1.设置目标 class
2.设置路由类型
3.设置路由优先级
4.设置额外的配置开关信息

31 - 50行:
处理uri的跳转,如果有URI,则根据路由元信息的目标Class的需要注入的参数

1.获取参数名和value
2.获取参数名和类型 (定义参数类型的类 TypeKind) 
3.将参数名和类型和值放到postcard对应的属性里。

53 - 76行:处理不同的路由类型
54 - 71:PROVIDER类型的路由则实现实例化目标类绿色通道(byType方式的核心实现),可以看到Arouter所有的服务都是单例的。

73:如果是Fragment则设置绿色通道,不做任何拦截

4.其他

4.1 CallBack

InterceptorCallback 拦截器的callbck
NavigationCallback 降级逻辑的全部回调
NavCallback 降级逻辑的部分回调

4.2 Service

AutowiredService 实现了“加载并调用辅助类以实现自动装载”, IOC.byName(/arouter/service/autowired)方式被_ARouter调用
ClassLoaderService 未使用
DegradeService 实现了全局降级逻辑, IOC.byType方式被_ARouter调用
InterceptorService实现了拦截器截面逻辑, IOC.byName(/arouter/service/interceptor)方式被_ARouter调用
PathReplaceService 实现了动态改变路由逻辑, IOC.byType方式被_ARouter调用
SerializationService 全局对Object对象的json转换工具类,IOC.byType方式被PostCard调用

参考:

https://yq.aliyun.com/articles/71687?t=t1
https://blog.csdn.net/fei20121106/article/details/73743235

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值