Android 热修复与插件化学习

第1章 class文件与dex文件解析

热修复解决的问题
  • 刚上线就发现严重的问题
  • 一些小功能及时推送给用户使用
插件化解决的问题
  • 应用越来越大带来的各种技术限制
  • 应用越来越大带来的合作开发
    插件化代码结构改变

1、class和dex文件详解

一、class文件
基本概念

能被JVM识别并加载执行的文件格式

class文件作用:记录一个类文件所有信息

生成class文件:通过IDE自动帮我们build;通过javac手动生成

public class Main {
    public static void main(String[] args) {
        System.out.println("hello");
    }
}

通过javac Main.java命令生成Main.class

public class Main {
    public Main() {
    }

    public static void main(String[] var0) {
        System.out.println("hello");
    }
}

通过java Main即可执行

class文件结构
  • 一种8位字节的二进制流文件
  • 各个数据按顺序紧密排列,无间隙
  • 每个类或接口都占据一个单独的class文件

class文件结构

每个字段的含义参考这个文章

通过010Editor这个软件可以查看字节码二进制信息

010Editor

class文件弊端
  • 对于移动端来说,内存占用大,比如每个文件都有很多常量池信息等
  • 堆栈的加载模式,加载速度慢
  • 文件io操作多,类查找和加载慢,每个class文件只存储了一个Java源文件所有的信息
二、dex文件
基本概念

能被DVM识别并加载执行的文件格式

生成dex文件:通过IDE自动帮我们build;通过dx命令手动生成

进入电脑安装sdk的目录,进入build-tools文件夹,进入安装的版本文件夹,dx.bat,将其目录配置到环境变量中

到文件目录下执行命令dx --dex --output Main.dex Main.class 生成 Main.dex,通过adb push Main.dex /storage/emulated/0命令将dex文件push到输手机,然后通过adb shell登录到手机控制台,通过dalvikvm -cp /sdcard/Main.dex Main执行dex文件,然后会看到控制台输出了hello,表名执行成功

dex文件作用:记录整个工程所有类文件的信息

dex文件结构
  • 8位字节的二进制流文件
  • 各个数据按顺序紧密的排列,无间隙
  • 整个应用所有Java源文件都在一个dex中
    看下dex文件结构

dex文件结构

看下dex文件头

dex文件头

通过010Editor软件查看dex文件索引区域

010Editor软件查看dex文件

对比下class文件和dex文件,一个class文件是一个整个结构体,只记录当前java文件内容,而dex文件划分为3个区直接存储整个工程Java文件的各个部分,好多区域可以复用,减少了文件大小

两者异同
  • 本质一样,dex从class演变过来
  • class文件存在很多冗余信息,dex去除冗余并整合

两者异同

第2章 虚拟机深入讲解

一、Java虚拟机结构解析

Java代码的编译和执行过程

类加载器 classloader

类加载器加载流程

  • Loading:类的信息从文件中获取并且载入到JVM的内存里
  • Verifying:检查读入的结构是否符合JVM规范的描述
  • Preparing:分配一个结构用来存储类信息
  • Resolving:把这个类的常量池中的所有的符号引用改变成直接引用
  • Initializing:执行静态初始化程序,把静态变量初始化成指定的值

二、JVM内存管理

内存管理有四个组成部分:Java栈区、本地方法栈、方法区、Java堆区

1.Java栈区

作用:它存放的是Java方法执行时的所有的数据
组成:由栈帧组成,一个栈帧代表一个方法的执行

Java栈帧

每个方法从调用到执行完成就对应一个栈帧在虚拟机栈中入栈到出栈。
每个栈帧都包括:局部变量表、栈操作数、动态链接、方法出口

2.本地方法栈

作用:本地方法栈是专门为native方法服务的

3.方法区

作用:存储被虚拟机加载的类信息、常量、静态变量、及时编译器编译后等数据

4.Java堆区

作用:所有通过new创建的对象的内存都在堆中分配

特点:是虚拟机中最大的一块内存,是GC要回收的部分

三、垃圾回收

引用计数算法

1.2版本之前在使用,创建一个对象时候会为他产生一个引用计数器,为他加1,有新对象引用他时候就加1,引用销毁时候就减1,减为0时候就是垃圾对象就可以被回收

但是2个不可达对象相互引用时候就有问题

可达性算法

1.2版本之后在使用,通过一些被称为引用链(GC Roots)的对象作为起点,从这些节点开始向下搜索,搜索走过的路径被称为(Reference Chain),当一个对象到GC Roots没有任何引用链相连时(即从GC Roots节点到该节点不可达),则证明该对象是不可用的。

引用类型

强引用,软引用,弱引用和虚引用

几种类型的区别

弱引用创建

Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;
wf.get();
垃圾回收算法
1.标记-清除算法

标记阶段:首先通过根节点,标记所有从根节点开始的可达对象。未被标记的对象就是未被引用的垃圾对象,清除阶段:清除所有未被标记的对象。

好处是不需要对对象进行移动,仅对不存活的对象处理,问题是造成内存碎片

2. 复制算法

将原有的内存空间分为两块,每次只使用一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未被使用的内存块中,然后清除正在使用的内存块中的所有对象。

优势是存活对象较少时高效,成本是需要多一块内存。不适用于存活对象较多的场合,如老年代。

3.标记—整理算法

标记阶段:先通过根节点,标记所有从根节点开始的可达对象,未被标记的为垃圾对象,整理阶段:将所有的存活对象压缩到内存的一段,之后清理边界外所有的空间。 适合用于存活对象较多的场合,如老年代。

触发垃圾回收
  • Java虚拟机无法再为新的对象分配内存空间了
  • 手动调用System.gc()方法(强烈不推荐,即使手动调用也不会立刻执行垃圾回收)
  • 优先级低的GC线程,被运行时就会执行GC

四、Dalvik与JVM的不同

  • 执行的文件不同,一个是class文件,一个是dex文件
  • 类加载系统与JVM区别较大
  • JVM只能存在一个,DVM可以存在多个
  • Dalvik是基于寄存器的(可以运行更快),而JVM是基于栈的

五、ART比Dalvik有哪些优势

JAVA虚拟机、Dalvik虚拟机和ART虚拟机简要对照

  • DVM使用JIT(Just In Time。即时编译技术)来将字节码转换成机器码,效率低
  • ART采用了AOT(Ahead Of Time,预编译技术),执行速度更快
  • ART会占用更多的应用安装时间和存储空间

第三章、ClassLoader原理讲解

一、Android中的ClassLoader作用详解

1、Android中ClassLoader的种类
  1. BootClassLoader
    和Java中Bootstrap ClassLoader基本类似,加载framework层字节码文件
  2. PathClassLoader
    和Java中App ClassLoader基本类似,加载已经安装到系统中的APK文件中的字节码文件
  3. DexClassLoader
    和Java中Costom ClassLoader基本类似,加载指定目录中字节码文件
  4. BaseDexClassLoader
    是PathClassLoader和DexClassLoader的父类

获取ClassLoader的方法:

    private void getAllClassLoader() {
        ClassLoader classLoader = getClassLoader();
        if (classLoader != null) {
            Log.e(classLoader.toString());
 
            while (classLoader.getParent() != null) {
                classLoader = classLoader.getParent();
                Log.e(classLoader.toString());
            }
        }
    }
2、Android中ClassLoader的特点
  1. 双亲代理模型的特点

先询问当前ClassLoader是否加载过类,如果加载过,直接返回,没有的话查询父加载器是否已经加载过此类,如果加载过,直接返回,如果往上都没加载过,才由当前的执行加载
2. 类加载的共享功能

一些framework层的类,一旦被顶层classloader加载,就会缓存在内存,任何地方用的都不会重新加载
3. 类加载的隔离功能

不同继承路线上的ClassLoader加载的类肯定不是同一个类,避免用户自己写代码冒充核心类库

二、ClassLoader源码讲解

我们从ClassLoader.java的loadClass()方法看起。我们知道Android的ClassLoader是实现了双亲委派模型的,我们来从源码角度来看下是如何实现的。

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
            // First, check if the class has already been loaded
            Class c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                }
            }
            return c;
    }

先是判断ClassLoader自身是否加载过该class文件,如果没有再判断父ClassLoader是否加载过,如果都没有加载过再自己去加载。这和我们上述的双亲委派模型思想完全一致。
我们来看下ClassLoader是如何去加载class文件的呢?也就是去看下findClass()方法的具体实现。

protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }

findClass()方法是一个空实现,也就是说它的具体实现是交给子类的。

![])(https://img-blog.csdn.net/201806162008391?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2NvbGluYW5kcm9pZA==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70)

可以看到DexClassLoader和PathClassLoader是ClassLoader的间接实现类。所以,下面我们来看一下DexClassLoader

public class DexClassLoader extends BaseDexClassLoader {

    public DexClassLoader(String dexPath, String optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), libraryPath, parent);
    }
}

我们来看下其四个参数都是什么含义。

  • dexPath。要加载的dex文件路径。
  • optimizedDirectory。dex文件要被copy到的目录路径。
  • libraryPath。apk文件中类要使用的c/c++代码。
  • parent。父装载器,也就是真正loadclass的装载器。

看一下PathClassLoader的源码

public class PathClassLoader extends BaseDexClassLoader {
 
    public PathClassLoader(String dexPath, ClassLoader parent) {
        super(dexPath, null, null, parent);
    }

 
    public PathClassLoader(String dexPath, String libraryPath,
            ClassLoader parent) {
        super(dexPath, null, libraryPath, parent);
    }
}

我们来看第二个构造方法,可以看出,它与DexClassLoader的构造方法的区别就是少了一个要把dex文件copy到的目录路径。正是因为缺少这个路径,我们的PathClassLoader只能用来加载安装过的apk中的dex文件。

这两个ClassLoader的真正核心方法都在BaseDexClassLoader中,我们现在来看下源码。

public class BaseDexClassLoader extends ClassLoader {
    private final DexPathList pathList;

    public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String librarySearchPath, ClassLoader parent) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, librarySearchPath, optimizedDirectory);
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        Class c = pathList.findClass(name, suppressedExceptions);
        if (c == null) {
            ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);
           for (Throwable t : suppressedExceptions) {
                cnfe.addSuppressed(t);
            }
            throw cnfe;
        }
        return c;
    }
    /**
     * @hide
     */
    public void addDexPath(String dexPath) {
        pathList.addDexPath(dexPath, null /*optimizedDirectory*/);
    }

    @Override
    protected URL findResource(String name) {
        return pathList.findResource(name);
    }

    @Override
    protected Enumeration<URL> findResources(String name) {
        return pathList.findResources(name);
    }

    @Override
    public String findLibrary(String name) {
        return pathList.findLibrary(name);
    }


    protected synchronized Package getPackage(String name) {
        ...
    }

    /**
     * @hide
     */
    public String getLdLibraryPath() {
        ...
    }

    @Override public String toString() {
        return getClass().getName() + "[" + pathList + "]";
    }
}

我们看到BaseDexClassLoader有一个成员变量DexPathList,其次它的核心方法是findClass()。我们来看下具体实现。

@Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        Class c = pathList.findClass(name, suppressedExceptions);
        if (c == null) {
            ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);
           for (Throwable t : suppressedExceptions) {
                cnfe.addSuppressed(t);
            }
            throw cnfe;
        }
        return c;
    }

findClass其实是通过成员变量pathList的findClass()方法来查找的。所以,我们接下来还需要去看DexPathList的源码。

DexPathList源码详解

构造方法

    public DexPathList(ClassLoader definingContext, String dexPath,
            String librarySearchPath, File optimizedDirectory) {
        ...
        ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
        // save dexPath for BaseDexClassLoader
        this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                           suppressedExceptions, definingContext);

        this.nativeLibraryDirectories = splitPaths(librarySearchPath, false);
        this.systemNativeLibraryDirectories =
                splitPaths(System.getProperty("java.library.path"), true);
        List<File> allNativeLibraryDirectories = new ArrayList<>(nativeLibraryDirectories);
        allNativeLibraryDirectories.addAll(systemNativeLibraryDirectories);

        this.nativeLibraryPathElements = makePathElements(allNativeLibraryDirectories,
                                                          suppressedExceptions,
                                                          definingContext);

        if (suppressedExceptions.size() > 0) {
            this.dexElementsSuppressedExceptions =
                suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]);
        } else {
            dexElementsSuppressedExceptions = null;
        }
    }

构造方法里最重要的一行代码是

this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                           suppressedExceptions, definingContext);

Element数组是DexPathList的一个内部类。有一个非常重要的成员变量DexFile。

static class Element {
        private final File dir;
        private final boolean isDirectory;
        private final File zip;
        private final DexFile dexFile;

        ...
    }

我们通过makeDexElements()方法得到了一个elements数组。那么,makeDexElements()方法具体干了什么呢

private static Element[] makeElements(List<File> files, File optimizedDirectory,
                                          List<IOException> suppressedExceptions,
                                          boolean ignoreDexFiles,
                                          ClassLoader loader) {
        Element[] elements = new Element[files.size()];
        int elementsPos = 0;
        /*
         * Open all files and load the (direct or contained) dex files
         * up front.
         */
        for (File file : files) {
            File zip = null;
            File dir = new File("");
            DexFile dex = null;
            String path = file.getPath();
            String name = file.getName();

            if (path.contains(zipSeparator)) {
                String split[] = path.split(zipSeparator, 2);
                zip = new File(split[0]);
                dir = new File(split[1]);
            } else if (file.isDirectory()) {
                // We support directories for looking up resources and native libraries.
                // Looking up resources in directories is useful for running libcore tests.
                elements[elementsPos++] = new Element(file, true, null, null);
            } else if (file.isFile()) {
                if (!ignoreDexFiles && name.endsWith(DEX_SUFFIX)) {
                    // Raw dex file (not inside a zip/jar).
                    try {
                        dex = loadDexFile(file, optimizedDirectory, loader, elements);
                    } catch (IOException suppressed) {
                        System.logE("Unable to load dex file: " + file, suppressed);
                        suppressedExceptions.add(suppressed);
                    }
                } else {
                    zip = file;

                    if (!ignoreDexFiles) {
                        try {
                            dex = loadDexFile(file, optimizedDirectory, loader, elements);
                        } catch (IOException suppressed) {
                            suppressedExceptions.add(suppressed);
                        }
                    }
                }
            } else {
                System.logW("ClassLoader referenced unknown path: " + file);
            }

            if ((zip != null) || (dex != null)) {
                elements[elementsPos++] = new Element(dir, false, zip, dex);
            }
        }
        if (elementsPos != elements.length) {
            elements = Arrays.copyOf(elements, elementsPos);
        }
        return elements;
    }

它的作用是通过loadDexFile()方法把dex文件都加载出来,然后返回一个elements数组。接着,我们来看DexPathList中最重要的方法findClass()。

public Class findClass(String name, List<Throwable> suppressed) {
        for (Element element : dexElements) {
            DexFile dex = element.dexFile;

            if (dex != null) {
                Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
                if (clazz != null) {
                    return clazz;
                }
            }
        }
        if (dexElementsSuppressedExceptions != null) {
            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
        }
        return null;
    }

它就是通过遍历elements数组,拿到里面的每一个dex文件,通过DexFile的loadClassBinaryName()方法找到class字节码。通过查看DexFile源码,可以得知loadClassBinaryName()方法最终是调用的底层C++方法来load class。

几个文件源码的查看地址。
DexPathList.java
DexClassLoader.java
PathClassLoader.java
DexFile.java

三、Android中的动态加载比一般Java程序复杂在哪里

  • 有许多组件类需要注册才能使用,如activity,service要在AndroidManifest注册
  • 资源的动态加载很复杂,资源是用id注册的
  • 每个版本加载方式不同

问题就是:Android程序运行需要一个上下文环境

第四章、主流的热修复方案

主流的热修复方案

第五章、AndFix介绍

https://github.com/alibaba/AndFix
这个库6年前就停止维护,只支持到7.0版本,只是学习下原理

第六章、Tinker详解

一、Tinker介绍

https://github.com/Tencent/tinker/wiki

二、Tinker核心原理

  • 基于android原生的ClassLoader,开发了自己的ClassLoader
  • 基于android原生的aapt,开发了自己的aapt,通过定义自己的AssertManager完成资源加载
  • 微信团队自己基于Dex文件的格式,研发了DexDiff算法

第七章、插件化原理

一、Manifest处理

  1. 构建期进行全量merge操作
  2. Bundle的依赖单独merge,生成Bundle的Merge Manifest
  3. 解析各个Bundle的Merge Manifest,得到整包的BundleInfoList

插件类加载

  1. DelegateClassLoader以PatchClassLoader为父ClassLoader,找不到的情况下根据BundleList找到对应的BundleClassLoader
  2. BundleClassLoader的父对象为BootClassLoader,包含PatchClassLoader对象,先查找当前ClassLoader,在查找当前PatchClassLoader

一个工程下的bundle 模块代码

public class BundleUtil {
    public static void printLog() {
        Log.e("Bundle", "I am a class in the Bundle");
    }
}

app模块代码

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //要加载的apk路径
        String apkPath = getExternalCacheDir().getAbsolutePath() + "/bundle.apk";
        loadApk(apkPath);
    }

    private void loadApk(String apkPath) {
        //解压到的目录
        File optDir = getDir("opt", MODE_PRIVATE);
        //由于要加载一个未安装的apk的class文件
        //要创建一个classLoader去加载,参数:apk目录,解压到的目录,查找的关联路径,他的父ClassLoader
        DexClassLoader classLoader = new DexClassLoader(apkPath,
                optDir.getAbsolutePath(), null, this.getClassLoader());

        try {
            //加载类文件
            Class cls = classLoader.loadClass("com.imooc.bundle.BundleUtil");
            if (cls != null) {
                //通过反射创建对象,调用方法
                Object instacne = cls.newInstance();
                Method method = cls.getMethod("printLog");
                method.invoke(instacne);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

将bundle模块生成的apk放在指定目录下不安装,然后安装app模块生成的apk,运行发现成功输出。

自定义CustomClassLoader类

public class CustomClassLoader extends DexClassLoader {

    public CustomClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
        super(dexPath, optimizedDirectory, librarySearchPath, parent);
    }


    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {

        byte[] classData = getClassData(name);
        if (classData != null) {
            return defineClass(name, classData, 0, classData.length);
        } else {

            throw new ClassNotFoundException();
        }
    }

    private byte[] getClassData(String name) {

        try {
            InputStream inputStream = new FileInputStream(name);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int buffersize = 4096;
            byte[] buffer = new byte[buffersize];
            int bytesNumRead = -1;
            while ((bytesNumRead = inputStream.read(buffer)) != -1) {
                baos.write(buffer, 0, bytesNumRead);
            }
            return baos.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        }

        return null;
    }
}

资源加载

  1. 所有的Bundle Res都是加入到一个DelegateResources
  2. Bundle install的时候将Res,SO等目录通过反射加入AssetManager.addAssetPath
  3. 通过不同的packagedId区分Bundle的资源id
  4. 覆盖getIndetifier方法,兼容5.0以上系统

核心技术
  • 处理所有插件APK中的Manifist文件
  • 管理宿主apk中所有插件APK信息
  • 为每个插件apk创建对应的类加载器,资源管理器

创建PluginInfo管理插件信息

public class PluginInfo {
    public DexClassLoader mClassLoader;
    public AssetManager mAssetManager;
    public Resources mResouces;
}

创建PluginManager管理插件

public class PluginManager {

    private static PluginManager mInstacne;

    private static Context mContext;
    private static File mOptFile;
    private static HashMap<String, PluginInfo> mPluginMap;

    private PluginManager(Context context) {
        mContext = context;
        mOptFile = mContext.getDir("opt", mContext.MODE_PRIVATE);
        mPluginMap = new HashMap<>();
    }

    //获取单例对象方法
    public static PluginManager getInstance(Context context) {

        if (mInstacne == null) {
            synchronized (PluginManager.class) {
                if (mInstacne == null) {

                    mInstacne = new PluginManager(context);
                }
            }
        }

        return mInstacne;
    }


    public static PluginInfo loadApk(String apkPath) {

        if (mPluginMap.get(apkPath) != null) {
             return mPluginMap.get(apkPath);
        }

        PluginInfo pluginInfo = new PluginInfo();
        pluginInfo.mClassLoader = createPluginDexClassLoader(apkPath);
        pluginInfo.mAssetManager = createPluginAssetManager(apkPath);
        pluginInfo.mResouces = createPluginResources(apkPath);

        mPluginMap.put(apkPath, pluginInfo);

        return pluginInfo;
    }

    //为插件apk创建对应的classLoader
    private static DexClassLoader createPluginDexClassLoader(String apkPath) {

        DexClassLoader classLoader = new DexClassLoader(apkPath,
                mOptFile.getAbsolutePath(), null, null);
        return classLoader;
    }

    //为对应的插件创建AssetManager
    private static AssetManager createPluginAssetManager(String apkPath) {

        try {
            AssetManager assetManager = AssetManager.class.newInstance();
            Method addAssetPath = assetManager.getClass().getMethod("addAssetPath",
                    String.class);

            addAssetPath.invoke(assetManager, apkPath);
            return assetManager;
        } catch (Exception e) {
            e.printStackTrace();
        }

        return null;
    }

    //为对应的插件创建resoures
    private static Resources createPluginResources(String apkPath) {

        AssetManager assetManager = createPluginAssetManager(apkPath);

        Resources superResources = mContext.getResources();

        Resources pluginResources = new Resources(assetManager,
                superResources.getDisplayMetrics(), superResources.getConfiguration());

        return pluginResources;
    }
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值