ViewModel详解

一.简介

ViewModel 类旨在以注重生命周期的方式存储管理界面相关的数据。ViewModel 类让数据可在发生屏幕旋转等配置更改后继续留存。

ViewModel生命周期

官网:ViewModel 概览  |  Android 开发者  |  Android Developers

二.基本使用

1.Gradle配置

 

非androidX项目

implementation "android.arch.lifecycle:extensions:1.1.1"

AndroidX项目

implementation 'androidx.appcompat:appcompat:1.2.0'

这一行依赖,已经可以使用Jetpack的好多组件了,比如ViewModel,比如LiveData等等。当然也可以单个引入,具体见官方文档

https://developer.android.google.cn/jetpack/androidx/releases/lifecycle?hl=zh_cn

 

 

 

2.代码

ViewModelProvider.Factory实现类

package com.wjn.networkdemo.viewmodel;

import androidx.annotation.NonNull;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;

/**
 * ViewModelProvider.Factory接口实现类 获取Factory对象 获取ViewModelProvider对象时的第二个参数
 */

public class ViewModelFactory implements ViewModelProvider.Factory {
    @NonNull
    @Override
    public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
        try {
            return modelClass.newInstance();
        } catch (IllegalAccessException | InstantiationException e) {
            e.printStackTrace();
        }
        return null;
    }
}

ViewModel实现类

package com.wjn.networkdemo.viewmodel;

import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;

import com.wjn.networkdemo.retrofit.only.FanYiService;

import java.io.IOException;

import okhttp3.ResponseBody;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import retrofit2.Retrofit;

public class FanYiViewModel extends ViewModel {

    /**
     * Retrofit模块 详解:https://blog.csdn.net/weixin_37730482/category_6875815.html
     */
    private Retrofit mRetrofit;
    private FanYiService mFanYiService;
    private String mBaseUrl = "http://fanyi.youdao.com/";

    private MutableLiveData<String> mListLiveData;
    private MutableLiveData<Boolean> mLoadingLiveData;

    public FanYiViewModel() {
        //获取Retrofit对象
        mRetrofit = new Retrofit.Builder()
                .baseUrl(mBaseUrl)//设置BaseUrl 必须以'/'结尾
                .build();

        mListLiveData = new MutableLiveData<>();
        mLoadingLiveData = new MutableLiveData<>();
    }

    /**
     * 获取翻译数据
     */

    public void getFanYiData() {
        if (null != mRetrofit) {
            //进度条状态
            mLoadingLiveData.setValue(false);
            //Retrofit接口请求
            mFanYiService = mRetrofit.create(FanYiService.class);
            Call<ResponseBody> getCall = mFanYiService.getFanYiByGet();
            getCall.enqueue(new Callback<ResponseBody>() {
                @Override
                public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
                    String result = null;//报文 获取报文必须判断响应是否成功
                    if (response.isSuccessful()) {
                        try {
                            result = response.body().string();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                    //进度条状态
                    mLoadingLiveData.setValue(true);
                    //接口报文
                    mListLiveData.setValue(result);
                }

                @Override
                public void onFailure(Call<ResponseBody> call, Throwable t) {
                    //进度条状态
                    mLoadingLiveData.setValue(true);
                    //接口报文
                    mListLiveData.setValue("");
                }
            });
        }
    }

    /**
     * 获取请求结果的LiveData对象
     */

    public MutableLiveData<String> getListLiveData() {
        return mListLiveData;
    }

    /**
     * 获取加载中的LiveData对象
     */

    public MutableLiveData<Boolean> getLoadingLiveData() {
        return mLoadingLiveData;
    }

}

Activity测试类

package com.wjn.networkdemo.viewmodel;

import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.ProgressBar;
import android.widget.TextView;

import androidx.appcompat.app.AppCompatActivity;
import androidx.lifecycle.ViewModelProvider;

import com.wjn.networkdemo.R;

public class ViewModelActivity extends AppCompatActivity {

    private ProgressBar mProgressBar;
    private TextView mTextView;
    private FanYiViewModel mFanYiViewModel;

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

    /**
     * 初始化View
     */

    private void initView() {
        mProgressBar = findViewById(R.id.activity_viewmodel_pb);
        mTextView = findViewById(R.id.activity_viewmodel_tv);
        initViewModel();
        //请求接口
        findViewById(R.id.activity_viewmodel_textview).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mFanYiViewModel.getFanYiData();
            }
        });
    }

    /**
     * 初始化ViewModel
     */

    private void initViewModel() {
        //获取ViewModelProvider实例
        ViewModelProvider viewModelProvider = new ViewModelProvider(this, new ViewModelFactory());
        //获取ViewModel实例
        mFanYiViewModel = viewModelProvider.get(FanYiViewModel.class);
        //观察接口请求
        mFanYiViewModel.getListLiveData().observe(this, s -> {
            mTextView.setText(s);
            Log.d("ViewModelActivity", "接口请求s----:" + s);
            Log.d("ViewModelActivity", "接口请求线程----:" + Thread.currentThread().getName());
        });
        //观察接口请求中...
        mFanYiViewModel.getLoadingLiveData().observe(this, aBoolean -> {
            mProgressBar.setVisibility(aBoolean ? View.GONE : View.VISIBLE);
            Log.d("ViewModelActivity", "观察接口请求aBoolean----:" + aBoolean);
            Log.d("ViewModelActivity", "观察接口请求线程----:" + Thread.currentThread().getName());
        });
    }

}

3.结果

D/ViewModelActivity: 观察接口请求aBoolean----:false

D/ViewModelActivity: 观察接口请求线程----:main


D/ViewModelActivity: 观察接口请求aBoolean----:true


D/ViewModelActivity: 观察接口请求线程----:main



D/ViewModelActivity: 接口请求s----:{"translation":["蓝色的"],"basic":{"us-phonetic":"bluː","phonetic":"bluː","uk-phonetic":"bluː","explains":["adj. 蓝色的;忧郁的,悲观的;(由于冷或呼吸困难)发青的,青紫的;(电影、玩笑或故事)色情的,黄色的;(肉)未熟的;(政治上)保守的","n. 蓝色;蓝色物品;(牛津或剑桥大学的运动员)蓝色荣誉者;失误;红发人;打架","vt. (使)变成蓝色;把......染成蓝色;给\u2026\u2026上蓝色漂白剂;挥霍(钱财)","n. (Blue) (英、美、加、澳、新)布卢(人名)"]},"query":"blue","errorCode":0,"web":[{"value":["蓝色的","蓝色","刀枪不入"],"key":"blue"},{"value":["钴蓝色","艳蓝色","钴蓝","群青"],"key":"Cobalt blue"},{"value":["蓝月","蓝月亮","千载难逢的时机","蓝色月亮"],"key":"Blue Moon"}]}

D/ViewModelActivity: 接口请求线程----:main

三.基本讲解

1.ViewModel用于代替MVP中的Presenter层

<1> 前言

在MVP模式中Presenter层需要持有IView接口来回调结果给界面(Activity/Fargment)。而Presenter层常常调用Model层进行异步网络请求。因为网络请求时间不固定。所以可能Presenter层会保存很长一段时间,如果Presenter层持有View层(Activity/Fargment)的上下文对象。这样就可能导致View层(Activity/Fragment)已近销毁。但是还有Presenter层持有它,导致该Activity/Fragment不能被及时回收。造成内存泄漏。

<2> ViewModel用法

上述代码可以看出,ViewModel并没有持有View层。而是通过LiveData来设置变化

具体步骤

ViewModel继承类中

1.声明LiveData对象

private MutableLiveData<String> mListLiveData;
private MutableLiveData<Boolean> mLoadingLiveData;

2.设置LiveData变化

@Override
public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
   String result = null;//报文 获取报文必须判断响应是否成功
   if (response.isSuccessful()) {
       try {
            result = response.body().string();
       } catch (IOException e) {
            e.printStackTrace();
       }
   }
   //进度条状态
   mLoadingLiveData.setValue(true);
   //接口报文
   mListLiveData.setValue(result);
}

3.对外提供LiveData对象

/**
 * 获取请求结果的LiveData对象
 */

public MutableLiveData<String> getListLiveData() {
    return mListLiveData;
}

/**
 * 获取加载中的LiveData对象
 */

public MutableLiveData<Boolean> getLoadingLiveData() {
    return mLoadingLiveData;
}

View层使用

1.利用ViewModelProvider对象 获取 ViewModel继承类对象

//获取ViewModelProvider实例
ViewModelProvider viewModelProvider = new ViewModelProvider(this, new ViewModelFactory());
//获取ViewModel实例
mFanYiViewModel = viewModelProvider.get(FanYiViewModel.class);

2.拿到对应的LiveData对象 添加观察者 更新UI等操作

//观察接口请求
mFanYiViewModel.getListLiveData().observe(this, s -> {
    mTextView.setText(s);
    Log.d("ViewModelActivity", "接口请求s----:" + s);
    Log.d("ViewModelActivity", "接口请求线程----:" + Thread.currentThread().getName());
});
        



//观察接口请求中...
mFanYiViewModel.getLoadingLiveData().observe(this, aBoolean -> {
    mProgressBar.setVisibility(aBoolean ? View.GONE : View.VISIBLE);
    Log.d("ViewModelActivity", "观察接口请求aBoolean----:" + aBoolean);
    Log.d("ViewModelActivity", "观察接口请求线程----:" + Thread.currentThread().getName());
});

3.适当时机 比如点击某个按钮 调用ViewModel继承类中请求接口相关的方法

//请求接口
findViewById(R.id.activity_viewmodel_textview).setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        mFanYiViewModel.getFanYiData();
    }
});

<3> 总结

也就是说ViewModel和Presenter层的区别就是,不持有View层的引用。通过LiveData来设置数据(相等于Presenter层给View层的回调)。然后通过LiveData的观察者接收数据变化。而LiveData具有生命周期感知性,可以自动销毁。这样就避免了Activity/Fragment层可能造成的内存泄漏问题。

四.Fragment间共享数据

1.代码

ViewModel继承类

package com.wjn.networkdemo.viewmodel;

import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;

public class SharedViewModel extends ViewModel {

    private MutableLiveData<String> mSharedLiveData;

    public SharedViewModel() {
        mSharedLiveData = new MutableLiveData<>();
    }

    /**
     * 设置LiveData值
     */

    public void shareValue(String value) {
        mSharedLiveData.setValue(value);
    }

    /**
     * 获取LiveData
     */

    public MutableLiveData<String> getSharedLiveData() {
        return mSharedLiveData;
    }

}

Fragment1

package com.wjn.networkdemo.viewmodel;

import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;

import com.wjn.networkdemo.R;

public class Fragment1 extends Fragment {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_1, container, false);
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);

        //设置共享数据
        SharedViewModel model = new ViewModelProvider(requireActivity(), new ViewModelFactory()).get(SharedViewModel.class);
        model.shareValue("Fragment1共享给Fragment2的数据");
    }
}

Fragment2

package com.wjn.networkdemo.viewmodel;

import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProvider;

import com.wjn.networkdemo.R;

public class Fragment2 extends Fragment {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_2, container, false);
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);

        //获取Fragment1共享的数据
        SharedViewModel model = new ViewModelProvider(requireActivity(), new ViewModelFactory()).get(SharedViewModel.class);
        model.getSharedLiveData().observe(requireActivity(), new Observer<String>() {
            @Override
            public void onChanged(String s) {
                Log.d("Fragment", "第二个Fragment接收的数据:" + s);
            }
        });

        Log.d("Fragment", "第二个Fragment onViewCreated方法");
    }
}

2.结果

D/Fragment: 第二个Fragment onViewCreated方法



D/Fragment: 第二个Fragment接收的数据:Fragment1共享给Fragment2的数据

3.总结

<1> 使用ViewModel来在Fragment之间共享数据。方便简洁。不需要宿主Activity做任何操作。相比EventBus和接口回调要简单的多。

<2> 因为ViewModel使用LiveData来观察数据变化,不需手动销毁。

<3> Fragment中创建ViewModel或者添加观察者时ViewModelStoreOwner对象要传宿主Activity的对象

五.源码分析

1.创建ViewModel对象

从代码中可知,创建ViewModel对象,没有使用ViewModel类。而是使用的ViewModelProvider类。

ViewModelProvider viewModelProvider = new ViewModelProvider(this, new ViewModelFactory());

点击查看两个参数的ViewModelProvider类构造方法。

/**
 * Creates {@code ViewModelProvider}, which will create {@code ViewModels} via the given
 * {@code Factory} and retain them in a store of the given {@code ViewModelStoreOwner}.
 *
 * @param owner   a {@code ViewModelStoreOwner} whose {@link ViewModelStore} will be used to
 *                retain {@code ViewModels}
 * @param factory a {@code Factory} which will be used to instantiate
 *                new {@code ViewModels}
 */
public ViewModelProvider(@NonNull ViewModelStoreOwner owner, @NonNull Factory factory) {
    this(owner.getViewModelStore(), factory);
}

然后内部调用另外一个两个参数的构造方法

/**
 * Creates {@code ViewModelProvider}, which will create {@code ViewModels} via the given
 * {@code Factory} and retain them in the given {@code store}.
 *
 * @param store   {@code ViewModelStore} where ViewModels will be stored.
 * @param factory factory a {@code Factory} which will be used to instantiate
 *                new {@code ViewModels}
 */
public ViewModelProvider(@NonNull ViewModelStore store, @NonNull Factory factory) {
    mFactory = factory;
    mViewModelStore = store;
}

也就是咱们创建时传的两个参数是

参数1:ViewModelStoreOwner对象。即Activity/Fragment对象。两者已经实现了ViewModelStoreOwner接口。如下

public class ComponentActivity extends androidx.core.app.ComponentActivity implements
        LifecycleOwner,
        ViewModelStoreOwner,
        SavedStateRegistryOwner,
        OnBackPressedDispatcherOwner {

所以可是直接传 this即可。

参数2:Factory对象。是一个接口。在ViewModelProvider类内部。一般实现

public class ViewModelFactory implements ViewModelProvider.Factory {
    @NonNull
    @Override
    public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
        try {
            return modelClass.newInstance();
        } catch (IllegalAccessException | InstantiationException e) {
            e.printStackTrace();
        }
        return null;
    }
}

最后传到构造方法里,参数是

参数1:变成 ViewModelStore 。

参数2:未变。

参数1变化

owner.getViewModelStore()

也就是说,通过传递的ViewModelStoreOwner对象,获取ViewModelStore对象。如果ViewModelStoreOwner对象不一致,可能造成获取ViewModelStore对象不一致。而ViewModelStore对象又是用来存储ViewModel的集合。

这就解释了,同一个Activity中好说,前面说的两个Fragment共享数据时。在两个Fragment中ViewModel的继承类都是重新创建的。参数传宿主Activity时能收到消息。参数传各自的Fragment对象时不能收到消息。就是这个原因。

2.获取具体的ViewModel继承类对象

上面说到了,使用ViewModel时,首先获取ViewModelProvider对象。那么获取到ViewModelProvider对象后。怎么获得具体的ViewModel对象呢?继续

 FanYiViewModel mFanYiViewModel = viewModelProvider.get(FanYiViewModel.class);

点击get方法 get方法源码

 /**
 * Returns an existing ViewModel or creates a new one in the scope (usually, a fragment or
 * an activity), associated with this {@code ViewModelProvider}.
 * <p>
 * The created ViewModel is associated with the given scope and will be retained
 * as long as the scope is alive (e.g. if it is an activity, until it is
 * finished or process is killed).
 *
 * @param modelClass The class of the ViewModel to create an instance of it if it is not
 *                   present.
 * @param <T>        The type parameter for the ViewModel.
 * @return A ViewModel that is an instance of the given type {@code T}.
 */
@NonNull
@MainThread
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方法。继续

/**
 * Returns an existing ViewModel or creates a new one in the scope (usually, a fragment or
 * an activity), associated with this {@code ViewModelProvider}.
 * <p>
 * The created ViewModel is associated with the given scope and will be retained
 * as long as the scope is alive (e.g. if it is an activity, until it is
 * finished or process is killed).
 *
 * @param key        The key to use to identify the ViewModel.
 * @param modelClass The class of the ViewModel to create an instance of it if it is not
 *                   present.
 * @param <T>        The type parameter for the ViewModel.
 * @return A ViewModel that is an instance of the given type {@code T}.
 */
@NonNull
@MainThread
public <T extends ViewModel> T get(@NonNull String key, @NonNull Class<T> modelClass) {
    ViewModel viewModel = mViewModelStore.get(key);

    if (modelClass.isInstance(viewModel)) {
        //noinspection unchecked
        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);
    //noinspection unchecked
    return (T) viewModel;
}

ViewModel viewModel = mViewModelStore.get(key);

首先根据上面生成的Key,去ViewModelStore对象的集合中取ViewModel对象。继续

if (modelClass.isInstance(viewModel)) {
   //noinspection unchecked
   return (T) viewModel;
} else {
    //noinspection StatementWithEmptyBody
    if (viewModel != null) {
        // TODO: log a warning.
    }
}

如果获取的ViewModel对象不为空 直接返回集合中的ViewModel。否则继续。

if (mFactory instanceof KeyedFactory) {
    viewModel = ((KeyedFactory) (mFactory)).create(key, modelClass);
} else {
    viewModel = (mFactory).create(modelClass);
}

如果,第二个参数的工厂对象是KeyedFactory。就用KeyedFactory创建新的ViewModel。否则,就用传入的对象创建新的ViewModel对象。即自己实现Factory接口返回的ViewModel对象。

public class ViewModelFactory implements ViewModelProvider.Factory {
    @NonNull
    @Override
    public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
        try {
            return modelClass.newInstance();
        } catch (IllegalAccessException | InstantiationException e) {
            e.printStackTrace();
        }
        return null;
    }
}

继续

mViewModelStore.put(key, viewModel);
return (T) viewModel;

创建完新的ViewModel后,将新的ViewModel存储到ViewModelStore对象的集合中。

3.ViewModel对象存储

上述已分析,ViewModel对象的存储是用的ViewModelStore。

ViewModelStore源码

/**
 * Class to store {@code ViewModels}.
 * <p>
 * An instance of {@code ViewModelStore} must be retained through configuration changes:
 * if an owner of this {@code ViewModelStore} is destroyed and recreated due to configuration
 * changes, new instance of an owner should still have the same old instance of
 * {@code ViewModelStore}.
 * <p>
 * If an owner of this {@code ViewModelStore} is destroyed and is not going to be recreated,
 * then it should call {@link #clear()} on this {@code ViewModelStore}, so {@code ViewModels} would
 * be notified that they are no longer used.
 * <p>
 * Use {@link ViewModelStoreOwner#getViewModelStore()} to retrieve a {@code ViewModelStore} for
 * activities and fragments.
 */
public class ViewModelStore {

    private final HashMap<String, ViewModel> mMap = new HashMap<>();

    final void put(String key, ViewModel viewModel) {
        ViewModel oldViewModel = mMap.put(key, viewModel);
        if (oldViewModel != null) {
            oldViewModel.onCleared();
        }
    }

    final ViewModel get(String key) {
        return mMap.get(key);
    }

    Set<String> keys() {
        return new HashSet<>(mMap.keySet());
    }

    /**
     *  Clears internal storage and notifies ViewModels that they are no longer used.
     */
    public final void clear() {
        for (ViewModel vm : mMap.values()) {
            vm.clear();
        }
        mMap.clear();
    }
}

此类,有三个方法需要格外关注。

put方法:在上面获取具体的ViewModel对象时,如果新创建ViewModel时,会将新创建的ViewModel对象存储起来。就会调用这个方法。

get方法:在上面获取具体的ViewModel对象时,调用。获取Key对应的存储过的ViewModel对象。

clear方法:点击调用这个方法的地方,发现ComponentActivity类有调用。而从26开始或者Androidx版本的Activity都会继承这个类。也就是说都会调用这个clear方法。

public ComponentActivity() {
   Lifecycle lifecycle = getLifecycle();
       
   if (lifecycle == null) {
            throw new IllegalStateException("getLifecycle() returned null in ComponentActivity's "
                    + "constructor. Please make sure you are lazily constructing your Lifecycle "
                    + "in the first call to getLifecycle() rather than relying on field "
                    + "initialization.");
   }

   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();
               }
           }
       }
   });

}

也就是说,创建Activity时,注册了一个观察者。当LifecycleOwner拥有类也就是Activity变成ON_DESTROY状态,即销毁时候。会清空存储在集合中的ViewModel对象。

这也就证明ViewModel的生命周期是在Activity销毁时才销毁

4.补充

onSaveInstanceState方法也可以保存页面中的数据,ViewModel也可以保存页面中的数据。那么两者有什么区别呢?

onSaveInstanceState:存储在磁盘。需要序列化。大小有限制(一般Bundle限制大小1M)。且onSaveInstanceState只能存可序列化和反序列化的对象。

ViewModel:存在内存中(集合 创建ViewModelProvider对象时,初始化 获取对应的ViewModel时,获取集合&存入结合。Activity变成ON_DESTROY状态时清空集合) 读写速度快。大小限制就是App的可用内存。

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值