Dalvik虚拟机源码初探

 

alvik_
内存管理 垃圾收集 JIT、JNI以及进程和线程管理。 


Java Object Heap、Bitmap Memory和Native Heap、Bitmap

Dalvik虚拟使用的垃圾收集机制有以下特点:

Full heap collection,也就是一次可能只收集一部分垃圾;


Dalvik_  虚拟机: Java进程和线程的创建过程。 虚拟机:

下面我们就详细分析每一个步骤。

        Step 1. app_process.main

        这个函数定义在frameworks/base/cmds/app_process/app_main.cpp文件中:


runtime.start("com.android.internal.os.ZygoteInit",
                startSystemServer);

......
 
static AndroidRuntime* gCurRuntime = NULL;
 
......
 
AndroidRuntime::AndroidRuntime()
{
    ......
 
    assert(gCurRuntime == NULL);        // one per process
    gCurRuntime = this;
}


startVm            


/*
 * Create a new VM instance.
 *
 * The current thread becomes the main VM thread.  We return immediately,
 * which effectively means the caller is executing in a native method.
 */
jint JNI_CreateJavaVM(JavaVM** p_vm, JNIEnv** p_env, void* vm_args)
{
    const JavaVMInitArgs* args = (JavaVMInitArgs*) vm_args;
    JNIEnvExt* pEnv = NULL;
    JavaVMExt* pVM = NULL;
    const char** argv;
    int argc = 0;
    ......
 
    /* zero globals; not strictly necessary the first time a VM is started */
    memset(&gDvm, 0, sizeof(gDvm));
 
    /*
     * Set up structures for JNIEnv and VM.
     */
    //pEnv = (JNIEnvExt*) malloc(sizeof(JNIEnvExt));
    pVM = (JavaVMExt*) malloc(sizeof(JavaVMExt));
 
    memset(pVM, 0, sizeof(JavaVMExt));
    pVM->funcTable = &gInvokeInterface;
    pVM->envList = pEnv;
    ......
 
    argv = (const char**) malloc(sizeof(char*) * (args->nOptions));
    memset(argv, 0, sizeof(char*) * (args->nOptions));
    ......
 
    /*
     * Convert JNI args to argv.
     *
     * We have to pull out vfprintf/exit/abort, because they use the
     * "extraInfo" field to pass function pointer "hooks" in.  We also
     * look for the -Xcheck:jni stuff here.
     */
    for (i = 0; i < args->nOptions; i++) {
        ......
    }
 
    ......
 
    /* set this up before initializing VM, so it can create some JNIEnvs */
    gDvm.vmList = (JavaVM*) pVM;
 
    /*
     * Create an env for main thread.  We need to have something set up
     * here because some of the class initialization we do when starting
     * up the VM will call into native code.
     */
    pEnv = (JNIEnvExt*) dvmCreateJNIEnv(NULL);
 
    /* initialize VM */
    gDvm.initializing = true;
    if (dvmStartup(argc, argv, args->ignoreUnrecognized, (JNIEnv*)pEnv) != 0) {
        free(pEnv);
        free(pVM);
        goto bail;
    }
 
    /*
     * Success!  Return stuff to caller.
     */
    dvmChangeStatus(NULL, THREAD_NATIVE);
    *p_env = (JNIEnv*) pEnv;
    *p_vm = (JavaVM*) pVM;
    result = JNI_OK;
 
bail:
    gDvm.initializing = false;
    if (result == JNI_OK)
        LOGV("JNI_CreateJavaVM succeeded\n");
    else
        LOGW("JNI_CreateJavaVM failed\n");
    free(argv);
    return result;
}


argv = const char** malloc();

memset

Exact GC Precise GC
Native Code。 GC

NewLocalRef DeleteLocalRef和NewGlobalRef/DeleteGlobalRef等来显式地引用或者释放Java对象。

DirectBuffer  ReferenceQueue

    /* make sure we got these [can this go away?] */
    assert(gDvm.classJavaLangClass != NULL);
    assert(gDvm.classJavaLangObject != NULL);
    //assert(gDvm.classJavaLangString != NULL);
    assert(gDvm.classJavaLangThread != NULL);
    assert(gDvm.classJavaLangVMThread != NULL);
    assert(gDvm.classJavaLangThreadGroup != NULL);
 
    /*
     * Make sure these exist.  If they don't, we can return a failure out
     * of main and nip the whole thing in the bud.
     */
    static const char* earlyClasses[] = {
        "Ljava/lang/InternalError;",
        "Ljava/lang/StackOverflowError;",
        "Ljava/lang/UnsatisfiedLinkError;",
        "Ljava/lang/NoClassDefFoundError;",
        NULL
    };
    const char** pClassName;
    for (pClassName = earlyClasses; *pClassName != NULL; pClassName++) {
        if (dvmFindSystemClassNoInit(*pClassName) == NULL)
            goto fail;
    }

dvmPrepMainForJni

Zygote进程在启动时会创建一个Dalvik虚拟机实例 Native堆栈

   从这里就可以看到,线程创建钩子javaCreateThreadEtc被保存在一个函数指针gCreateThreadFn中。注意,函数指针gCreateThreadFn默认是指向函数androidCreateRawThreadEtc的,也就是说,如果我们不设置线程创建钩子的话,函数androidCreateRawThreadEtc就是默认使用的线程创建函数。后面在分析Dalvik虚拟机线程的创建过程时,我们再详细分析函数指针gCreateThreadFn是如何使用的。

        至此,我们就分析完成Dalvik虚拟机在Zygote进程中的启动过程,这个启动过程主要就是完成了以下四个事情:

        1. 创建了一个Dalvik虚拟机实例;

        2. 加载了Java核心类及其JNI方法;

        3. 为主线程的设置了一个JNI环境;

        4. 注册了Android核心类的JNI方法。

        换句话说,就是Zygote进程为Android系统准备好了一个Dalvik虚拟机实例,以后Zygote进程在创建Android应用程序进程的时候,就可以将它自身的Dalvik虚拟机实例复制到新创建Android应用程序进程中去,从而加快了Android应用程序进程的启动过程。此外,Java核心类和Android核心类(位于dex文件中),以及它们的JNI方法(位于so文件中),都是以内存映射的方式来读取的,因此,Zygote进程在创建Android应用程序进程的时候,除了可以将自身的Dalvik虚拟机实例复制到新创建的Android应用程序进程之外,还可以与新创建的Android应用程序进程共享Java核心类和Android核心类,以及它们的JNI方法,这样就可以节省内存消耗。

        同时,我们也应该看到,Zygote进程为了加快Android应用程序进程的启动过程,牺牲了自己的启动速度,因为它需要加载大量的Java核心类,以及注册大量的Android核心类JNI方法。Dalvik虚拟机在加载Java核心类的时候,还需要对它们进行验证以及优化,这些通常都是比较耗时的。又由于Zygote进程是由init进程启动的,也就是说Zygote进程在是开机的时候进行启动的,因此,Zygote进程的牺牲是比较大的。不过毕竟我们在玩手机的时候,很少会关机,也就是很少开机,因此,牺牲Zygote进程的启动速度是值得的,换来的是Android应用程序的快速启动。而且,Android系统为了加快Java类的加载速度,还会想方设法地提前对Dex文件进行验证和优化,这些措施具体参考Dalvik Optimization and Verification With dexopt一文。


void dvmCallMethodV(Thread* self, const Method* method, Object* obj,
    bool fromJni, JValue* pResult, va_list args)
{
    ......
 
    if (dvmIsNativeMethod(method)) {
        TRACE_METHOD_ENTER(self, method);
        /*
         * Because we leave no space for local variables, "curFrame" points
         * directly at the method arguments.
         */
        (*method->nativeFunc)(self->curFrame, pResult, method, self);
        TRACE_METHOD_EXIT(self, method);
    } else {
        dvmInterpret(self, method, pResult);
    }
 
    ......
}
        这个函数定义在文件dalvik/vm/interp/Stack.c中。
        函数dvmCallMethodV首先检查参数method描述的函数是否是一个JNI方法。如果是的话,那么它所指向的一个Method对象的成员变量nativeFunc就指向该JNI方法的地址,因此就可以直接对它进行调用。否则的话,就说明参数method描述的是一个Java函数,这时候就需要继续调用函数dvmInterpret来执行它的代码。

    分支returnFromMethod的实现比较简单,它主要就是恢复上一个执行的成员函数的栈帧,以及该成员函数下一条要执行的指令。由于在前面的Step 8中,我们在当前栈帧中保存了上一个执行的成员函数的下一条要执行的指令及其栈帧,因此,这里对它们进行恢复是很直觉的。

        不过有一点需要注意的是,前面的Step 8保存的是当前正在执行的成员函数的程序计算器,现在由于该程序计算器所指向的指令已经执行完成了,因此,我们需要继续调整从当前栈帧中恢复回来的指令值,使得它指向的是上一个执行的成员函数的下一条要执行的指令的值,这是通过宏FINISH(3)来完成的。

        至此,我们就分析完成Dalvik虚拟机解释器的执行过程了,这个过程也就相当于是Dalvik虚拟机的运行过程,也就是说,Step 7到Step 9实际上是会不断地重复执行,直至进程退出为止的。以Zygote进程为例,Dalvik虚拟机解释器就是以com.android.internal.os.ZygoteInit类的静态成员函数main为入口点执行,然后在一个Socket上进行循环,用来等待和处理ActivityManagerService服务向它发送创建新应用程序进程的请求,直至系统退出为止。又以Android应用程序进程为例,Dalvik虚拟机解释器就是以android.app.ActivityThread类的静态成员函数main为入口点执行,然后在一消息队列上进行循环,用来等待和处理主线程的消息,直到应用程序退出为止。

        当然,Dalvik虚拟机在运行的过程中,除了解释执行之外,还可能会进行JIT。JIT的目的就是将Java代码即时编译成Native代码之后再直接执行,这样对于经常运行的代码来说,可以提高性能。由于在将Java代码即时编译成Native代码的过程中,可以进一步利用运行时信息来进行激进优化,因此,JIT获得的Native代码比AOT获得的Native可能会更优化。关于JIT的实现,可以参考Hello, JIT World: The Joy of Simple JITs一文。

        此外,从Step 4和Step 8可以知道,Dalvik虚拟机在运行的过程中,除了需要执行Java函数之外,还可能需要执行Native函数,这些Native函数也就是我们平时所说的JNI方法。在接下来的一篇文章中,我们就将分析这些JNI方法注册到Dalvik虚拟机里面去的,敬请关注!

Zygote dalvik   AccessibleObject、java ActivityManagerService    ActiviThread

public final class System {
    ......
    
    public static void loadLibrary(String libName) {
        SecurityManager smngr = System.getSecurityManager();
        if (smngr != null) {
            smngr.checkLink(libName);
        }
        Runtime.getRuntime().loadLibrary(libName, VMStack.getCallingClassLoader());
    }
 
    ......
}


static void Dalvik_java_lang_Runtime_nativeLoad(const u4* args,
    JValue* pResult)
{
    StringObject* fileNameObj = (StringObject*) args[0];
    Object* classLoader = (Object*) args[1];
    char* fileName = NULL;
    StringObject* result = NULL;
    char* reason = NULL;
    bool success;
 
    assert(fileNameObj != NULL);
    fileName = dvmCreateCstrFromString(fileNameObj);
 
    success = dvmLoadNativeCode(fileName, classLoader, &reason);
    if (!success) {
        const char* msg = (reason != NULL) ? reason : "unknown failure";
        result = dvmCreateStringFromCstr(msg);
        dvmReleaseTrackedAlloc((Object*) result, NULL);
    }
 
    free(reason);
    free(fileName);
    RETURN_PTR(result);
}

Runtime

bool dvmLoadNativeCode(const char* pathName, Object* classLoader,
        char** detail)
{
    SharedLib* pEntry;
    void* handle;
    ......
 
    pEntry = findSharedLibEntry(pathName);
    if (pEntry != NULL) {
        if (pEntry->classLoader != classLoader) {
            ......
            return false;
        }
        ......
 
        if (!checkOnLoadResult(pEntry))
            return false;
        return true;
    }
    ......
 
    handle = dlopen(pathName, RTLD_LAZY);
    ......
 
    /* create a new entry */
    SharedLib* pNewEntry;
    pNewEntry = (SharedLib*) calloc(1, sizeof(SharedLib));
    pNewEntry->pathName = strdup(pathName);
    pNewEntry->handle = handle;
    pNewEntry->classLoader = classLoader;
    ......
 
    /* try to add it to the list */
    SharedLib* pActualEntry = addSharedLibEntry(pNewEntry);
 
    if (pNewEntry != pActualEntry) {
        ......
        freeSharedLibEntry(pNewEntry);
        return checkOnLoadResult(pActualEntry);
    } else {
        ......
 
        bool result = true;
        void* vonLoad;
        int version;
 
        vonLoad = dlsym(handle, "JNI_OnLoad");
        if (vonLoad == NULL) {
            LOGD("No JNI_OnLoad found in %s %p, skipping init\n",
                pathName, classLoader);
        } else {
            ......
 
            OnLoadFunc func = vonLoad;
            ......
 
            version = (*func)(gDvm.vmList, NULL);
            ......
 
            if (version != JNI_VERSION_1_2 && version != JNI_VERSION_1_4 &&
                version != JNI_VERSION_1_6)
            {
                .......
                result = false;
            } else {
                LOGV("+++ finished JNI_OnLoad %s\n", pathName);
            }
 
        }
 
        ......
 
        if (result)
            pNewEntry->onLoadResult = kOnLoadOkay;
        else
            pNewEntry->onLoadResult = kOnLoadFailed;
 
        ......
 
        return result;
    }

(*func) (gDvm.vmList,NULL);

 

Dalvik虚拟机JNI方法的注册过程分析

这些Bridage函数实际上仍然不是直接调用地调用JNI方法的,

这是因为Dalvik虚拟机是可以运行在各种不同的平台之上,而每一种平台可能都定义有自己的一套函数调用规范,
也就是所谓的ABI(Application Binary Interface),这是一个API(Application Programming Interface)不同的概念。
ABI是在二进制级别上定义的一套函数调用规范,

例如参数是通过寄存器来传递还是堆栈来传递,而API定义是一个应用程序编程接口规范。

换句话说,API定义了源代码和库之间的接口,因此同样的代码可以在支持这个API的任何系统中编译 ,

而ABI允许编译好的目标代码在使用兼容ABI的系统中无需改动就能运行。 
寄存器传递,堆栈传递,  

void dvmSetNativeFunc(Method* method, DalvikBridgeFunc func,
    const u2* insns)
{
    ......
 
    if (insns != NULL) {
        /* update both, ensuring that "insns" is observed first */
        method->insns = insns;
        android_atomic_release_store((int32_t) func,
            (void*) &method->nativeFunc);
    } else {
        /* only update nativeFunc */
        method->nativeFunc = func;
    }
 
    ......
}

      假设在前面的Step 11中选择的Bridge函数为dvmCallJNIMethod_general,并且结合前面Dalvik虚拟机的运行过程分析一文,
我们就可以得到Dalvik虚拟机在运行过程中调用JNI方法的过程:

        1. 调用函数dvmCallJNIMethod_general,执行一些必要的准备工作;

        2. 函数dvmCallJNIMethod_general再调用函数dvmPlatformInvoke来以统一的方式来调用对应的JNI方法;

        3. 函数dvmPlatformInvoke通过libffi库来调用对应的JNI方法,以屏蔽Dalvik虚拟机运行在不同目标平台的细节。

 至此,我们就分析完成Dalvik虚拟机JNI方法的注册过程了。这样,我们就打通了Java代码和Native代码之间的道路。
 实际上,很多Java和Android核心类的功能都是通过本地操作系统提供的系统调用来完成的,
 例如,Zygote类的成员函数forkAndSpecialize最终是通过Linux系统调用fork来创建一个Android应用程序进程的,
 又如,Thread类的成员函数start最终是通过pthread线程库函数pthread_create来创建一个Android应用程序线程的。

Zygote  forkAndSpecialize   fork  Android   应用进程

Thread  start  pthread pthread_create Android  创建Android应用线程
 

 

           一. Dalvik虚拟机进程的创建过程

  int pid = Process.start("android.app.ActivityThread",  
                mSimpleProcessManagement ? app.processName : null, uid, uid,  
                gids, debugFlags, null);  


         它调用了Process.start函数开始为应用程序创建新的进程,注意,它传入一个第一个参数为"android.app.ActivityThread",
        这就是进程初始化时要加载的Java类了,把这个类加载到进程之后,就会把它里面的静态成员函数main作为进程的入口点,
        后面我们会看到。

  Dalvik虚拟机进程实际上就是通常我们所说的Android应用程序进程。从前面Android应用程序进程启动过程的源代码分析一文可以知道,Android应用程序进程是由ActivityManagerService服务通过android.os.Process类的静态成员函数start来请求Zygote进程创建的,而Zyogte进程最终又是通过dalvik.system.Zygote类的静态成员函数forkAndSpecialize来创建该Android应用程序进程的。因此,接下来我们就从dalvik.system.Zygote类的静态成员函数forkAndSpecialize开始分析Dalvik虚拟机进程的创建过程,如图1所示:


    pid = fork();

由Zygote进程创建出来的Android应用程序进程是不具有任何的Root用户特权的。
        
        对于函数forkAndSpecializeCommon的实现,还有两个地方是需要注意的。
        forkAndSpecializeCommon   
        
               第一个地方是只有Zygote进程才有权限创建System进程和Android应用程序进程
               
               
               Zygote进程在启动运行在它里面的Dalvik虚拟机的时候,gDvm.zygote的值会等于true,
               
               这时候函数forkAndSpecializeCommon才可以使用系统调用fork来创建一个新的进程
        
        Dalvik虚拟机除了可以执行Java代码之外,还可以执行Native代码,也就是C/C
        
        Java和Android核心类库(dex文件)及其JNI方法(so文件)
        
    java Android dex文件)及其JNI方法(so文件)  
    
    都会一直在Zygote进程、System进程和Android应用程序进程中进行共享。
        此外,运行在Zygote进程中的Dalvik虚拟机开始的时候也会与System进程和Android应用程序进程一起共享,
    
    但是由于上述的COW机制,在必要的时候,System进程和Android应用程序进程还是会复制一份出来的,
    
    从而使得它们都具有独立的Dalvik虚拟机实例。

    调用函数 dvmGcStartupAfterZygote来进行一次GC。
    dvmGcStartupAfterZygote来进行一次GC。 GC
    
    一个Dalvik虚拟机进程实际上就是一个Linux进程。

   二. Dalvik虚拟机线程的创建过程

 

 

           
           在Java代码中,我们可以通过java.lang.Thread类的成员函数start来创建一个Dalvik虚拟机线程

public class Thread implements Runnable {
    ......
 
    public synchronized void start() {
        if (hasBeenStarted) {
            throw new IllegalThreadStateException("Thread already started."); // TODO Externalize?
        }
 
        hasBeenStarted = true;
 
        VMThread.create(this, stackSize);
    }
 
    ......
}

VMThread.create


class VMThread
{
    ......
 
    native static void create(Thread t, long stacksize);
 
    ......
}


bool dvmCreateInterpThread(Object* threadObj, int reqStackSize)
{
    pthread_attr_t threadAttr;
    pthread_t threadHandle;
    ......
    Thread* newThread = NULL;
    ......
    int stackSize;
    ......
 
    if (reqStackSize == 0)
        stackSize = gDvm.stackSize;
    else if (reqStackSize < kMinStackSize)
        stackSize = kMinStackSize;
    else if (reqStackSize > kMaxStackSize)
        stackSize = kMaxStackSize;
    else
        stackSize = reqStackSize;
 
    pthread_attr_init(&threadAttr);
    pthread_attr_setdetachstate(&threadAttr, PTHREAD_CREATE_DETACHED);
    ......
 
    newThread = allocThread(stackSize);
    ......
 
    newThread->threadObj = threadObj;
    ......
 
    int cc = pthread_create(&threadHandle, &threadAttr, interpThreadStart,
            newThread);
    ......
 
    while (newThread->status != THREAD_STARTING)
        pthread_cond_wait(&gDvm.threadStartCond, &gDvm.threadListLock);
    ......
 
    newThread->next = gDvm.threadList->next;
    if (newThread->next != NULL)
        newThread->next->prev = newThread;
    newThread->prev = gDvm.threadList;
    gDvm.threadList->next = newThread;
    ......
 
    newThread->status = THREAD_VMWAIT;
    pthread_cond_broadcast(&gDvm.threadStartCond);
    ......
 
    return true;
}


pthread_cond_wait(&gDvm.threadStartCond,&gDvm.threadListLock);


newThread.next= gDvm.threadList.next

newThread.next.prev = newThread

newThread.prev = gDvm.threadList
gDvm.threadList.new = newThread
newThread.status = THREAD_VMWAIT
pthread_cond_broadcast(&gDvm.threadStartCond)

pthread,gDvm


gDvm.stackSize
数threadObj描述的是Java层的一个Thread对象,它在Dalvik虚拟机中对应有一个Native层的Thread对象。
这个Native层的Thread对象是通函数allocThread来分配的,
并且与它对应的Java层的Thread对象会保存在它的成员变量threadObj中。


Dalvik虚拟机线程实际上就是本地操作系统线程。


static void* interpThreadStart(void* arg)
{
    Thread* self = (Thread*) arg;
    ......
 
    prepareThread(self);
    ......
 
    self->status = THREAD_STARTING;
    pthread_cond_broadcast(&gDvm.threadStartCond);
    ......
 
    while (self->status != THREAD_VMWAIT)
        pthread_cond_wait(&gDvm.threadStartCond, &gDvm.threadListLock);
    ......
 
    self->jniEnv = dvmCreateJNIEnv(self);
    ......
 
    dvmChangeStatus(self, THREAD_RUNNING);
    ......
 
    if (gDvm.debuggerConnected)
        dvmDbgPostThreadStart(self);
    ......
 
    int priority = dvmGetFieldInt(self->threadObj,
                        gDvm.offJavaLangThread_priority);
    dvmChangeThreadPriority(self, priority);
    ......
 
    Method* run = self->threadObj->clazz->vtable[gDvm.voffJavaLangThread_run];
    JValue unused;
    ......
 
    dvmCallMethod(self, run, self->threadObj, &unused);
    ......
 
    dvmDetachCurrentThread();
 
    return NULL;
}


self threadObj clazz vtable    gDvm.voffJavaLangThread_run

 9. 从函数dvmCallMethod返回来之后,新创建的Dalvik虚拟机线程就完成自己的使命了,这时候就可以调用函数dvmDetachCurrentThread来执行清理工作。

 
 dvmDetachCurrentThread

 dvmAttachCurrentThread
 
 
 
     7. 调用函数freeThread来释放当前线程的Java栈和各个引用表(即前面Step 5所描述的三个引用表)所占用的内存,以及释放Thread对象self所占用的内存。
 
 
 
 
 int Thread::_threadLoop(void* user)
{
    Thread* const self = static_cast<Thread*>(user);
    sp<Thread> strong(self->mHoldSelf);
    wp<Thread> weak(strong);
    ......
 
    bool first = true;
 
    do {
        bool result;
        if (first) {
            first = false;
            self->mStatus = self->readyToRun();
            result = (self->mStatus == NO_ERROR);
 
            if (result && !self->mExitPending) {
                ......
 
                result = self->threadLoop();
            }
        } else {
            result = self->threadLoop();
        }
 
        if (result == false || self->mExitPending) {
            ......
            break;
        }
 
        // Release our strong reference, to let a chance to the thread
        // to die a peaceful death.
        strong.clear();
        // And immediately, re-acquire a strong reference for the next loop
        strong = weak.promote();
    } while(strong != 0);
 
    return 0;
}
 
 
 javaAttachThread
 
 
 funcTable        gDvm.threadList
 
    3. 调用函数dvmCreateJNIEnv来当前线程创建一个JNI上下文环境,
  
  它的具体实现可以参考前面Dalvik虚拟机的启动过程分析一文。
  这个创建出来的JNI上下文环境使用一个JNIEnvExt结构体来描述。JNIEnvExt结构体有一个重要的成员变量funcTable,
  它指向的是一系列的Dalvik虚拟机回调函数。正是因为有了这个Dalvik虚拟机回调函数表,
  
  当前线程才可以访问Dalvik虚拟机中的Java对象或者执行Dalvik虚拟机中的Java代码。

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值