JetPack之ViewModel使用和原理浅析与DataBinding双向绑定

学习目标:

JetPack之ViewModel与DataBinding双向绑定的解密


学习内容:

  • ViewModel
    首先肯定是了解一下的它的作用,没有什么比它的官网解释的更详细的了

ViewModel的链接

它的主要作用是:ViewModel 类让数据可在发生屏幕旋转等配置更改后继续留存(在横竖屏切换时,可以保留数据),在actvity的onDestroy时/Fragment的onDetach时就会clear。
先引入

implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'

然后愉快使用

public class MyViewModel extends ViewModel {
    public int number;
}

然后在activity中写如下代码

public class MainActivity extends AppCompatActivity {
    MyViewModel viewModel;
    @SuppressLint("RestrictedApi")
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //记住初始化就这么初始化
        //为什么new的时候传this就行了?形参类型不是ViewModelStoreOwner?
        //因为AppCompatActivity的父类FragmentActivity的父类ComponentActivity实现了ViewModelStoreOwner,所以可以传this
        //viewModel永远得到的时同一个ViewModel hashCode是一样的
        //這是因为每次都会从缓存中去取,没有才会新建一个ViewModel
        viewModel = new ViewModelProvider(this).get(MyViewModel.class);
    }

	public void changeData(View view){
		viewModel.number ++;
		//屏幕旋转会保留从上次的值开始+1;
		Log.d("!!!!",viewModel.number+"");
	}
}

这句代码new ViewModelProvider(this).get(MyViewModel.class);
我们从字面意思上看每次都new,你会以为我们拿到的viewModel每次都会不一样,但你错了,看源码

//首先是两个构造函数
 public ViewModelProvider(@NonNull ViewModelStoreOwner owner) {
        this(owner.getViewModelStore(), owner instanceof HasDefaultViewModelProviderFactory
                ? ((HasDefaultViewModelProviderFactory) owner).getDefaultViewModelProviderFactory()
                : NewInstanceFactory.getInstance());
    }
    //由这两个构造函数可知
    //mViewModelStore = ViewModelStoreOwner .getViewModelStore()
public ViewModelProvider(@NonNull ViewModelStore store, @NonNull Factory factory) {
        mFactory = factory;
        mViewModelStore = store;
    }
   
public <T extends ViewModel> T get(@NonNull Class<T> modelClass) {
        String canonicalName = modelClass.getCanonicalName();
        if (canonicalName == null) {
            throw new IllegalArgumentException("Local and anonymous classes can not be ViewModels");
        }
        return get(DEFAULT_KEY + ":" + canonicalName, modelClass);
    }
//而我们get的时候也是先从 mViewModelStore中获取的
//也就是说 结合上面的注释 可以得到
//get的时候也是先从ViewModelStoreOwner .getViewModelStore()中获取的
@SuppressWarnings("unchecked")
    @NonNull
    @MainThread
    public <T extends ViewModel> T get(@NonNull String key, @NonNull Class<T> modelClass) {
        ViewModel viewModel = mViewModelStore.get(key);

        if (modelClass.isInstance(viewModel)) {
            if (mFactory instanceof OnRequeryFactory) {
                ((OnRequeryFactory) mFactory).onRequery(viewModel);
            }
            return (T) viewModel;
        } else {
            //noinspection StatementWithEmptyBody
            if (viewModel != null) {
                // TODO: log a warning.
            }
        }
        if (mFactory instanceof KeyedFactory) {
            viewModel = ((KeyedFactory) (mFactory)).create(key, modelClass);
        } else {
            viewModel = (mFactory).create(modelClass);
        }
        mViewModelStore.put(key, viewModel);
        return (T) viewModel;
    }

所以关键就是看这个ViewModelStoreOwner .getViewModelStore()返回了什么?

public class FragmentActivity extends ComponentActivity implements
        ActivityCompat.OnRequestPermissionsResultCallback,
        ActivityCompat.RequestPermissionsRequestCodeValidator {
        ......
		@NonNull
        @Override
        public ViewModelStore getViewModelStore() {
            return FragmentActivity.this.getViewModelStore();
        }
        ..........
}

public class ComponentActivity extends androidx.core.app.ComponentActivity implements
        LifecycleOwner,
        ViewModelStoreOwner,
        HasDefaultViewModelProviderFactory,
        SavedStateRegistryOwner,
        OnBackPressedDispatcherOwner {
static final class NonConfigurationInstances {
        Object custom;
        ViewModelStore viewModelStore;
    }
        .......
        
	@NonNull
    @Override
    public ViewModelStore getViewModelStore() {
        if (getApplication() == null) {
            throw new IllegalStateException("Your activity is not yet attached to the "
                    + "Application instance. You can't request ViewModel before onCreate call.");
        }
        if (mViewModelStore == null) {
        //获取最后一次的NonConfigurationInstances
            NonConfigurationInstances nc =
                    (NonConfigurationInstances) getLastNonConfigurationInstance();
            //恢复上次的viewModel
            //这个viewModel是被存放在了一个NonConfigurationInstances静态类中
            每次都会从缓存中去取,没有才会新建一个ViewModel
            if (nc != null) {
                // Restore the ViewModelStore from NonConfigurationInstances
                // 从Activtiy NonConfigurationInstances获取viewModel实例
                mViewModelStore = nc.viewModelStore;
            }
            //如果为空 就new一个viewModel
            if (mViewModelStore == null) {
                mViewModelStore = new ViewModelStore();
            }
        }
        return mViewModelStore;
    }
......  
}

所以重点就来到了我们是怎么恢复的,从上面的注释上看我们最终会发现跟这个方法有关getLastNonConfigurationInstance()

public class Activity extends ContextThemeWrapper
        implements LayoutInflater.Factory2,
        Window.Callback, KeyEvent.Callback,
        OnCreateContextMenuListener, ComponentCallbacks2,
        Window.OnWindowDismissedCallback,
        AutofillManager.AutofillClient, ContentCaptureManager.ContentCaptureClient {
        ......
        static final class NonConfigurationInstances {
	        Object activity;
	        HashMap<String, Object> children;
	        FragmentManagerNonConfig fragments;
	        ArrayMap<String, LoaderManager> loaders;
	        VoiceInteractor voiceInteractor;
    	}
 @UnsupportedAppUsage
    /* package */ NonConfigurationInstances mLastNonConfigurationInstances;
        ......
 	@Nullable
    public Object getLastNonConfigurationInstance() {
        return mLastNonConfigurationInstances != null
                ? mLastNonConfigurationInstances.activity : null;
    }
    .......
//重新启动 Activity 时,又会将数据 attach 到新的 Activity放日 实例上,将其作为 getLastNonConfigurationInstance() 方法的返回值
 final void attach(Context context, ActivityThread aThread,
            Instrumentation instr, IBinder token, int ident,
            Application application, Intent intent, ActivityInfo info,
            CharSequence title, Activity parent, String id,
            NonConfigurationInstances lastNonConfigurationInstances,
            Configuration config, String referrer, IVoiceInteractor voiceInteractor,
            Window window, ActivityConfigCallback activityConfigCallback, IBinder assistToken) {
        attachBaseContext(context);

        mFragments.attachHost(null /*parent*/);

        mWindow = new PhoneWindow(this, window, activityConfigCallback);
        mWindow.setWindowControllerCallback(mWindowControllerCallback);
        mWindow.setCallback(this);
        mWindow.setOnWindowDismissedCallback(this);
        mWindow.getLayoutInflater().setPrivateFactory(this);
        if (info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) {
            mWindow.setSoftInputMode(info.softInputMode);
        }
        if (info.uiOptions != 0) {
            mWindow.setUiOptions(info.uiOptions);
        }
        mUiThread = Thread.currentThread();

        mMainThread = aThread;
        mInstrumentation = instr;
        mToken = token;
        mAssistToken = assistToken;
        mIdent = ident;
        mApplication = application;
        mIntent = intent;
        mReferrer = referrer;
        mComponent = intent.getComponent();
        mActivityInfo = info;
        mTitle = title;
        mParent = parent;
        mEmbeddedID = id;
        /*****数据恢复*****/
        mLastNonConfigurationInstances = lastNonConfigurationInstances;
        if (voiceInteractor != null) {
            if (lastNonConfigurationInstances != null) {
                mVoiceInteractor = lastNonConfigurationInstances.voiceInteractor;
            } else {
                mVoiceInteractor = new VoiceInteractor(voiceInteractor, this, this,
                        Looper.myLooper());
            }
        }

        mWindow.setWindowManager(
                (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
                mToken, mComponent.flattenToString(),
                (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
        if (mParent != null) {
            mWindow.setContainer(mParent.getWindow());
        }
        mWindowManager = mWindow.getWindowManager();
        mCurrentConfig = config;

        mWindow.setColorMode(info.colorMode);
        mWindow.setPreferMinimalPostProcessing(
                (info.flags & ActivityInfo.FLAG_PREFER_MINIMAL_POST_PROCESSING) != 0);

        setAutofillOptions(application.getAutofillOptions());
        setContentCaptureOptions(application.getContentCaptureOptions());
    }



//这个方法是保存缓存用的
 NonConfigurationInstances retainNonConfigurationInstances() {
   //activity里面有ViewModelStore实例
        Object activity = onRetainNonConfigurationInstance();
        HashMap<String, Object> children = onRetainNonConfigurationChildInstances();
        FragmentManagerNonConfig fragments = mFragments.retainNestedNonConfig();

        // We're already stopped but we've been asked to retain.
        // Our fragments are taken care of but we need to mark the loaders for retention.
        // In order to do this correctly we need to restart the loaders first before
        // handing them off to the next activity.
        mFragments.doLoaderStart();
        mFragments.doLoaderStop(true);
        ArrayMap<String, LoaderManager> loaders = mFragments.retainLoaderNonConfig();

        if (activity == null && children == null && fragments == null && loaders == null
                && mVoiceInteractor == null) {
            return null;
        }

		//將要銷毀的activity中的NonConfigurationInstances存到新建的
		//NonConfigurationInstances对象中
        NonConfigurationInstances nci = new NonConfigurationInstances();
        nci.activity = activity;
        nci.children = children;
        nci.fragments = fragments;
        nci.loaders = loaders;
        if (mVoiceInteractor != null) {
            mVoiceInteractor.retainInstance();
            nci.voiceInteractor = mVoiceInteractor;
        }
        return nci;
    }
    ......
}

为什么我会说在Acticviy的onDestroy方法中回调retainNonConfigurationInstances()方法呢,看如下代码


public final class ActivityThread extends ClientTransactionHandler {
//ArrayMap 来保存activtiy 的状态数据
final ArrayMap<IBinder, ActivityClientRecord> mActivities = new ArrayMap<>();
.......
//Acticity被destroy时会回调这个方法
/** Core implementation of activity destroy call. */
ActivityClientRecord performDestroyActivity(IBinder token, boolean finishing,
            int configChanges, boolean getNonConfigInstance, String reason) {
        ActivityClientRecord r = mActivities.get(token);
        Class<? extends Activity> activityClass = null;
        if (localLOGV) Slog.v(TAG, "Performing finish of " + r);
        if (r != null) {
            activityClass = r.activity.getClass();
            r.activity.mConfigChangeFlags |= configChanges;
            if (finishing) {
                r.activity.mFinished = true;
            }

            performPauseActivityIfNeeded(r, "destroy");

            if (!r.stopped) {
                callActivityOnStop(r, false /* saveState */, "destroy");
            }
            if (getNonConfigInstance) {
                try {
                //保存 Activity 返回的 NonConfigurationInstances
                    r.lastNonConfigurationInstances
                            = r.activity.retainNonConfigurationInstances();
                } catch (Exception e) {
                    if (!mInstrumentation.onException(r.activity, e)) {
                        throw new RuntimeException(
                                "Unable to retain activity "
                                + r.intent.getComponent().toShortString()
                                + ": " + e.toString(), e);
                    }
                }
            }
            try {
                r.activity.mCalled = false;
                mInstrumentation.callActivityOnDestroy(r.activity);
                if (!r.activity.mCalled) {
                    throw new SuperNotCalledException(
                        "Activity " + safeToComponentShortString(r.intent) +
                        " did not call through to super.onDestroy()");
                }
                if (r.window != null) {
                    r.window.closeAllPanels();
                }
            } catch (SuperNotCalledException e) {
                throw e;
            } catch (Exception e) {
                if (!mInstrumentation.onException(r.activity, e)) {
                    throw new RuntimeException(
                            "Unable to destroy activity " + safeToComponentShortString(r.intent)
                            + ": " + e.toString(), e);
                }
            }
            r.setState(ON_DESTROY);
        }
        schedulePurgeIdler();
        // updatePendingActivityConfiguration() reads from mActivities to update
        // ActivityClientRecord which runs in a different thread. Protect modifications to
        // mActivities to avoid race.
        synchronized (mResourcesManager) {
            mActivities.remove(token);
        }
        StrictMode.decrementExpectedActivityCount(activityClass);
        return r;
    }
......

//启动activity时调用这个方法
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
               ···
        Activity activity = null;
        try {
            java.lang.ClassLoader cl = appContext.getClassLoader();
            activity = mInstrumentation.newActivity(
                    cl, component.getClassName(), r.intent);
            StrictMode.incrementExpectedActivityCount(activity.getClass());
            r.intent.setExtrasClassLoader(cl);
            r.intent.prepareToEnterProcess();
            if (r.state != null) {
                r.state.setClassLoader(cl);
            }
        } catch (Exception e) {
            if (!mInstrumentation.onException(activity, e)) {
                throw new RuntimeException(
                    "Unable to instantiate activity " + component
                    + ": " + e.toString(), e);
            }
        }
       
        ···
            
        //将 r.lastNonConfigurationInstances 传递进去 恢复数据
        activity.attach(appContext, this, getInstrumentation(), r.token,
                        r.ident, app, r.intent, r.activityInfo, title, r.parent,
                        r.embeddedID, r.lastNonConfigurationInstances, config,
                        r.referrer, r.voiceInteractor, window, r.configCallback,
                        r.assistToken);
        ···
        return activity;
    }
}

到这里viewModel的恢复就讲清楚了,接下来将以下在什么时候清除的

public ComponentActivity() {
.......
getLifecycle().addObserver(new LifecycleEventObserver() {
            @Override
            public void onStateChanged(@NonNull LifecycleOwner source,
                    @NonNull Lifecycle.Event event) {
                if (event == Lifecycle.Event.ON_DESTROY) {
                    if (!isChangingConfigurations()) {
                        getViewModelStore().clear();
                    }
                }
            }
        });
......
}

所以viewModel 在宿主生命周期DESTROY的时候 做释放清理工作。
好了原理讲的差不多了,接下来就使用把
在平常项目中一般不像文章开头这么使用ViewModel,都会如下使用ViewModel+liveData

public class MyViewModel extends ViewModel {
    private MutableLiveData<Integer> number;
    public MutableLiveData<Integer> getNumber(){
        if(number == null){
            number = new MutableLiveData<>();
            number.setValue(0);
        }
        return number;
    }
}

在activity中

public class MainActivity extends AppCompatActivity {
    private TextView textView;
    MyViewModel viewModel;
    @SuppressLint("RestrictedApi")
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        textView = findViewById(R.id.text1);
        viewModel = new ViewModelProvider(this).get(MyViewModel.class);
        viewModel.getNumber().observe(this, new Observer<Integer>() {
            @Override
            public void onChanged(Integer integer) {
                textView.setText(integer+"");
            }
        });
    }

    int count = 0;
    public void add(View view) {
        viewModel.getNumber().setValue(count++);
    }
}

这样当我们手机屏幕旋转时我们每次点击值都会比上次大1。
接下来我们再来介绍另一位大将Databinding(运用了apt技术)
首先我们需要在build.gradle加入

 dataBinding(){
        enabled = true
    }


在介绍双向绑定之前先介绍单向绑定

第一种单向绑定
layout布局文件

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
	//自定义指定生成的dataBind类全类名
    <data class="com.example.CustomBinding">
        <variable
            name="User"
            type="com.suyong.livedatabus.User" />

    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity"
        android:orientation="vertical">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{User.name}" />
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{User.age}" />
		//注意必须加一个id才可以
        <Button
        	android:id="@+id/tv_price"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>

    </LinearLayout>

</layout>

User.class

public class User extends BaseObservable {

    public String name ;

    public String age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
        //这个notifyChange会刷新name和age的值
        notifyChange();
    }

    public String getAge() {
        return age;
    }

    public void setAge(String age) {
        this.age = age;
    }
}

activity

public class DataBinding extends AppCompatActivity {
    CustomBinding viewMode;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        viewMode = DataBindingUtil.setContentView(this, R.layout.activity_main_suyong);
        final User user = new User("OICQ","22");
        viewMode.setUser(user);
         viewMode.tvPrice.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //点击更新值
                user.setAge("25");
                user.setName("OICQ1");
            }
        });
    }
}

但我们看到我们只在setName()调用了notifyChange(),按道理只会更新一个name,为啥age也更新了,这就是notifyChange()的作用,想要实现只更新name,如下所示
修改上面的两个方法(记得修改完重新build一下)

//注意包名 尤其是BR
import androidx.databinding.BaseObservable;
import androidx.databinding.Bindable;
import androidx.databinding.library.baseAdapters.BR;

 @Bindable
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
        notifyPropertyChanged(BR.name);
    }


这样就只更新了name。第二种单向绑定
user2.class

public class User2 {
    public ObservableField<User> user = new ObservableField<>();

    public ObservableField<String> name = new ObservableField<>();

    public ObservableField<String> age = new ObservableField<>();
}


xml
我们在上面的基础下修改下即可
 

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <data class="com.example.CustomBinding">
        <variable
            name="User2"
            type="com.suyong.livedatabus.User2" />
    </data>

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

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{User2.name}" />
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{User2.age}" />

        <Button
            android:id="@+id/tv_price"
            android:layout_width="wrap_content"
            android:text="asd"
            android:layout_height="wrap_content"/>

    </LinearLayout>

</layout>

activity

public class DataBinding extends AppCompatActivity {
    CustomBinding viewMode;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        viewMode = DataBindingUtil.setContentView(this, R.layout.activity_main_suyong);
        final User2 user2 = new User2();
        user2.age.set("OICQ");
        user2.name.set("22");
        viewMode.setUser2(user2);
                viewMode.tvPrice.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //点击更新值
                user2.age.set("OICQ");
                user2.name.set("22");
            }
        });
    }
}

第三中单向绑定
activiy

public class DataBinding extends AppCompatActivity {
    CustomBinding viewMode;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        viewMode = DataBindingUtil.setContentView(this, R.layout.activity_main_suyong);
        ObservableField<String> name = new ObservableField<>();
        name.set("OICQ");
        viewMode.setName(name);
    }
}


xml

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <data class="com.example.CustomBinding">
        <variable
            name="name"
            type="androidx.databinding.ObservableField&lt;String&gt;" />
    </data>

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

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{name}" />

    </LinearLayout>

</layout>

到这儿单向绑定就结束了,但是你会不会发现我们的变量其实跟之前的变量个数没有改变,最关键这些变量还是在我们的activity里面,所以我们可以做如下操作(ViewModel+DataBinding),为了解决上面的问题

.
public class MyViewModel extends ViewModel {
    public MutableLiveData<String> number;
    public MutableLiveData<String> getNumber(){
        if(number == null){
            number = new MutableLiveData<>();
            number.setValue("0");
        }
        return number;
    }
}

public class DataBinding extends AppCompatActivity {
    CustomBinding viewMode;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        viewMode = DataBindingUtil.setContentView(this, R.layout.activity_main_suyong);
        MyViewModel viewModel = new MyViewModel();
        viewModel.getNumber().setValue("23");
        dataBinding.setModel(viewModel);
    }
}

xml

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <data class="com.example.CustomBinding">
        <variable
            name="model"
            type="com.suyong.livedatabus.MyViewModel" />
    </data>

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

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{model.number}" />

    </LinearLayout>

</layout>

双向绑定
只需要

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <data class="com.example.CustomBinding">
        <variable
            name="model"
            type="com.suyong.livedatabus.MyViewModel" />
    </data>

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

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{model.number}" />
        <EditText
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@={model.number}" />

    </LinearLayout>

</layout>

这样就完成了双向绑定,这样当我们在EditText中输入值的话,textview也会改变。

好了,差不多了,篇幅太长了,也只是基本的使用,高阶使用还得看官方文档。

DataBinding官方链接
————————————————
版权声明:本文为CSDN博主「SYOICQ」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/sunlifeall/article/details/114215707

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值