安卓实践开发之MVP高级封装

前言

话说MVP的模式已经问世好几年了,为什么很多公司还是不愿意接受呢?说实在的我就还是喜欢自己的mvc,不喜欢看见mvp庞大的架构,所以前公司的项目呢也不曾使用过mvp(同事也不接受这种模式),毕竟项目架构不是特别复杂的话使用mvp显示不出他的优势,相反给人的感觉是实现一个界面多出了很多的代码。

这里写图片描述

然而现在最火的应该是mvvm模式了毕竟是数据双向绑定,让我想起了angularjs估计效率很高,但是mvp我们还是要知道的(毕竟还是不错的)。

mvp产生的原因

原生的 MVC 框架遇到大规模的应用,就会变得代码难读,不好维护,无法测试的囧境。因此,Android 开发方面也有很多对应的框架来解决这些问题。

构建框架的最终目的是增强项目代码的 可读性 , 维护性 和 方便测试 ,如果背离了这个初衷,为了使用而使用,最终是得不偿失的

从根本上来讲,要解决上述的三个问题,核心思想无非两种:一个是 分层 ,一个是 模块化 。两个方法最终要实现的就是解耦,分层讲的是纵向层面上的解耦,模块化则是横向上的解耦。下面我们来详细讨论一下 Android 开发如何实现不同层面上的解耦。

解耦的常用方法有两种: 分层 与 模块化

横向的模块化对大家来可能并不陌生,在一个项目建立项目文件夹的时候就会遇到这个问题,通常的做法是将相同功能的模块放到同一个目录下,更复杂的,可以通过插件化来实现功能的分离与加载。

纵向的分层,不同的项目可能就有不同的分法,并且随着项目的复杂度变大,层次可能越来越多。

对于经典的 Android MVC 框架来说,如果只是简单的应用,业务逻辑写到 Activity 下面并无太多问题,但一旦业务逐渐变得复杂起来,每个页面之间有不同的数据交互和业务交流时,activity 的代码就会急剧膨胀,代码就会变得可读性,维护性很差。

总的来说:项目大了我们便于修改和测试,代码重用性高。

MVC和MVP的区别?


从上图我们知道mvc和mvp的结构了:
MVP 是从经典的模式MVC演变而来,它们的基本思想有相通的地方:Controller/Presenter负责逻辑的处理,Model提供数据,View负 责显示。作为一种新的模式,MVP与MVC有着一个重大的区别:在MVP中View并不直接使用Model,它们之间的通信是通过Presenter (MVC中的Controller)来进行的,所有的交互都发生在Presenter内部,而在MVC中View会从直接Model中读取数据而不是通过 Controller。

在MVC里,View是可以直接访问Model的!从而,View里会包含Model信息,不可避免的还要包括一些业务逻辑。 在MVC模型里,更关注的Model的不变,而同时有多个对Model的不同显示,及View。所以,在MVC模型里,Model不依赖于View,但是 View是依赖于Model的。不仅如此,因为有一些业务逻辑在View里实现了,导致要更改View也是比较困难的,至少那些业务逻辑是无法重用的。

1、模型与视图完全分离,我们可以修改视图而不影响模型
2、可以更高效地使用模型,因为所以的交互都发生在一个地方——Presenter内部
3、我们可以将一个Presener用于多个视图,而不需要改变Presenter的逻辑。这个特性非常的有用,因为视图的变化总是比模型的变化频繁。
4、如果我们把逻辑放在Presenter中,那么我们就可以脱离用户接口来测试这些逻辑(单元测试)

未使用MVP程序是这样的:

使用MVP后是这样的:

MVP入门的代码实现(入门级别的实现)

网上很多的例子都是使用登录模块来进行mvp的讲解,但是我们只要知道MVP的流程不管怎么玩还是可以的。

Model层

package com.losileeya.mvpsimpledemo.model;
import android.os.Handler;
import android.os.Looper;
/**

- User: Losileeya (847457332@qq.com)
- Date: 2016-09-09
- Time: 11:33
- 类描述:
   *
- @version :
   */
  public class UserModel {
   private Handler handler=new Handler(Looper.getMainLooper());//主线程handler一步处理
   /**
  - model层业务逻辑处理
  - @param username   用户名
  - @param password   密码
  - @param callBack   结果回调
    */
public  void  login(final String username,final  String password,final  CallBack callBack){
    handler.postDelayed(new Runnable() {
        @Override
        public void run() {
            if(username.equals("123")&&password.equals("123"))
                callBack.onSuccess();
            else
                callBack.onFilure("帐号或者密码错误");
        }
    },2000);
  }
    }
   
   

    代码很简单就是一个登录方法的处理,正常情况就里就是调服务器登录接口,我们这里就用延时操作来模拟网络请求。这里对用户名密码做了一个判断,如果用户名为123密码为123就去成功的回调,否则返回“帐号或密码错误”。

    public interface CallBack {
      /**
     * model处理逻辑:成功回调
     */
    void onSuccess();
    /**
     * model处理逻辑:失败回调
     */
    void onFilure(String fail);
      }
       
       

      CallBack就是我们对model的逻辑的具体处理,提供这个插座去做你具体的实现逻辑。

      View层

      public interface LoginView {
        /**
       * 显示进度条
       * @param msg   进度条加载内容
       */
      void showLoding(String msg);
      /**
       * 隐藏进度条
       */
      void  hideLoding();
      /**
       * 显示登录的结果
       * @param result
       */
      void showResult(String result);
      /**
       * 显示加载错误
       * @param err 错误内容
       */
      void showErr(String err);
      /**
       * 获得界面上用户名的值
       * @return
       */
      String getUsername();
      /**
       * 获得界面上密码的值
       * @return
       */
      String getPassword();
        }
         
         

        我们把登录模块的ui逻辑作为一个接口的形式来处理,方便我们的activity处理的时候来进行界面显示,以及把它交给Presenter层来处理具体的ui展示逻辑。

        一部份是给Presenter提供数据的(View->Presenter)

        getPassword

        getUsername

        另一部份是从Presenter接收数据用来显示的(Presenter->View)

        hideLoading

        showResult

        showErr

        showLoading

        说白了就是数据与UI的交互。

        Presenter

        这一层就是mvp的核心,把view层和model层建立起来联系:

        public class LoginPresenter {
          private UserModel userModel;//model层具体实现类
          private LoginView loginView;//loginView接口
          public LoginPresenter(UserModel userModel, LoginView loginView) {
            this.userModel = userModel;
            this.loginView = loginView;
        }
        public void login(){
            loginView.showLoding("正在登录中...");//loginView的ui逻辑处理
            userModel.login(loginView.getUsername(), loginView.getPassword(), new CallBack() {
                @Override
                public void onSuccess() {
                    loginView.hideLoding();
                    loginView.showResult("登录成功");
                }
                 @Override
                public void onFilure(String fail) {
                    loginView.hideLoding();
                    loginView.showErr(fail);
                }
            });
        }}
           
           

          就一个登录方法,在login方法被调用时,LoginPresenter会从loginView获取到用户输入的用户名和密码,并以此为参数调用userModel的login方法,然后通过回调把结果返回给loginView。

          Activity(View 层也就是loginView的实现)

          public class LoginActivity extends AppCompatActivity implements LoginView, View.OnClickListener {
            private LoginPresenter loginPresenter;
          private ProgressDialog progressDialog;
          private EditText username;
          private EditText password;
          private Button login;
            @Override
          protected void onCreate(Bundle savedInstanceState) {
              super.onCreate(savedInstanceState);
              setContentView(R.layout.activity_login);
              initView();
              loginPresenter = initPresenter();//初始化presenter
          }
          public LoginPresenter initPresenter() {
              return new LoginPresenter(new UserModel(), this);
          }
          @Override
          public void showLoding(String msg) {
              progressDialog.setMessage(msg);
              if(!progressDialog.isShowing())
                   progressDialog.show();
          }
          @Override
          public void hideLoding() {
             if(progressDialog.isShowing())
                 progressDialog.dismiss();
          }
          @Override
          public String getUsername() {
              return username.getText().toString().trim();
          }
          @Override
          public String getPassword() {
              return password.getText().toString().trim();
          }
          @Override
          public void showResult(String result) {
              Toast.makeText(this, result, Toast.LENGTH_SHORT).show();
          }
          @Override
          public void showErr(String err) {
              Toast.makeText(this, err, Toast.LENGTH_SHORT).show();
          }
            @Override
          protected void onDestroy() {
              if (loginPresenter != null) {
                  loginPresenter = null;
              }
              super.onDestroy();
          }
          private void initView() {
              username = (EditText) findViewById(R.id.username);
              password = (EditText) findViewById(R.id.password);
              login = (Button) findViewById(R.id.login);
              progressDialog =new ProgressDialog(this);
              login.setOnClickListener(this);
          }
            @Override
          public void onClick(View v) {
              switch (v.getId()) {
                  case R.id.login:
                       loginPresenter.login();//loginPresenter的login
                      break;
              }
          }
            }
             
             
          • 1
          • 2
          • 3
          • 4
          • 5
          • 6
          • 7
          • 8
          • 9
          • 10
          • 11
          • 12
          • 13
          • 14
          • 15
          • 16
          • 17
          • 18
          • 19
          • 20
          • 21
          • 22
          • 23
          • 24
          • 25
          • 26
          • 27
          • 28
          • 29
          • 30
          • 31
          • 32
          • 33
          • 34
          • 35
          • 36
          • 37
          • 38
          • 39
          • 40
          • 41
          • 42
          • 43
          • 44
          • 45
          • 46
          • 47
          • 48
          • 49
          • 50
          • 51
          • 52
          • 53
          • 54
          • 55
          • 56
          • 57
          • 58
          • 59
          • 60
          • 61
          • 62
          • 63
          • 64
          • 65
          • 66

          代码可以看出逻辑清晰,只需要提供一下view 层的方法需要我们传入一个Presenter来处理登录的逻辑。我们只需要记住要初始化Presenter并且在view销毁的时候释放资源。

          上面代码我们可以看出还是有很多需要优化的地方,接下来我们就来重新封装我们的mvp。

          MVP的高级封装(从入门到放弃)

          鉴于装逼和简化解耦的需求,我们需要封装一下mvp的架构,容我喝口奶压压惊。
          这里写图片描述
          当然装逼我们就摆脱不了泛型的支持,哈哈,泛型真的是封装中的利器,是应该需要好好的学习。

          Model层的封装

          我们 以前都是直接使用UserModel类,这样呢不利于扩展,所以呢我们先抽取一个IUserModel的接口,便于我们对代码逻辑更加清晰的梳理,也对代码的维护性有好处。

          public interface IUserModel {
            /**
           *  登录逻辑处理
           * @param username   用户名
           * @param password   密码
           * @param callBack   结果回调
           */
          void login(String username,String password,CallBack  callBack);
            }
             
             

            我们只需要简单的抽取成接口好了,加个代码的实现是不是看起来更加的逻辑清楚:

            public class UserModel implements IUserModel {
                private Handler handler=new Handler(Looper.getMainLooper());//主线程handler一步处理
            
                /**
                 * model层业务逻辑处理
                 * @param username   用户名
                 * @param password   密码
                 * @param callBack   结果回调
                 */
                @Override
                public void login(final String username, final String password, final CallBack callBack) {
                    handler.postDelayed(new Runnable() {
                        @Override
                        public void run() {
                            if(username.equals("123")&&password.equals("123"))
                                callBack.onSuccess();
                            else
                                callBack.onFailure("帐号或者密码不正确");
                        }
                    }, 2000);
                }
            }
               
               

              请注意:我们这里为了刷新Ui所以都使用了handler,这里是处理数据的逻辑,项目中换成其他的网络框架也好,handler在这里只是业务的需要。

              View层

              我们就给view提供了一个接口,显然是可以继续扩展的,因为每次一个view我们就得写一个接口,而且view的显示还是有一些公用的方法,如果我们不去抽取就显得代码又重复无法很好的体现代码的复用性和低耦合。

              在这里 我需要新建一个base的包,用于放一下通用的类和接口来降低耦合度:

              public interface BaseMvpView {
                /**
               * 显示进度条
               * @param msg   进度条加载内容
               */
              void showLoding(String msg);
                /**
               * 隐藏进度条
               */
              void hideLoding();
                /**
               * 显示加载错误
               * @param err 错误内容
               */
              void showErr(String err);
                }
                 
                 

                再来看下LoginView的写法:

                public interface LoginView extends BaseMvpView{
                  /**
                 * 获得界面上用户名的值
                 * @return
                 */
                String getUsername();
                  /**
                 * 获得界面上密码的值
                 * @return
                 */
                String getPassword();
                  /**
                 * 显示登录的结果
                 * @param result
                 */
                void showResult(String result);
                  }
                   
                   

                  是不是显得清爽多了。

                  Presenter层(最难的)

                  由于presenter层是联系view和model层,所以封装相对来说困难的多。入门的代码在处理Presenter的时候还是有很多的问题的,比如view销毁了,然后presenter层没有销毁还在调用view层的方法就会导致空指针异常。

                  所以我们就的做如下修改:

                  
                  public interface Presenter<V extends BaseMvpView> {
                  /**
                   * presenter和对应的view绑定
                   * @param mvpView  目标view
                   */
                  void attachView(V mvpView);
                  /**
                   * presenter与view解绑
                   */
                  void detachView();
                  }
                     
                     

                    写一个基础接口Presenter它接收BaseMvpView的子类,有两个方法,attachView,detachView分别用于与MvpView建立与断开连接,然后我们再来写一个BaseMvpPresenter来具体操作。

                    public class BaseMvpPresenter<V extends BaseMvpView> implements Presenter<V> {
                    private  V mvpView;
                    @Override
                    public void attachView(V mvpView) {
                        this.mvpView = mvpView;
                    }
                    @Override
                    public void detachView() {
                         mvpView = null;
                    }
                    /**
                     * 判断 view是否为空
                     * @return
                     */
                    public  boolean isAttachView(){
                        return mvpView != null;
                    }
                    /**
                     * 返回目标view
                     * @return
                     */
                    public  V getMvpView(){
                        return mvpView;
                    }
                    /**
                     * 检查view和presenter是否连接
                     */
                    public void checkViewAttach(){
                        if(! isAttachView()){
                           throw  new MvpViewNotAttachedException();
                        }
                    }
                    /**
                     * 自定义异常
                     */
                       public static   class  MvpViewNotAttachedException extends RuntimeException{
                           public  MvpViewNotAttachedException(){
                            super("请求数据前请先调用 attachView(MvpView) 方法与View建立连接");
                        }
                    }
                    }    
                       
                       

                      这个类用来判断view是否和presenter建立连接,并且提供方法让我们获得目标view便于presenter来处理相关逻辑。

                      具体的LoginPresenter我们也就好实现了:

                      为了便于梳理项目流程,我们还是提供一个基础接口:

                        public interface ILoginPresenter {
                          /**
                           * presenter login
                           */
                          void login();
                          }
                         
                         

                        以上代码只不过是为了让我们更加清晰的看清业务逻辑。下面才是LoginPresenter:

                        public class LoginPresenter extends BaseMvpPresenter<LoginView> implements ILoginPresenter{
                          private UserModel userModel;//model层具体实现类
                          public LoginPresenter(UserModel userModel) {
                            this.userModel = userModel;
                        }
                          @Override
                        public void login() {
                            checkViewAttach();//检查是否绑定
                            final LoginView loginView=getMvpView();//获得LoginView
                            loginView.showLoding("正在登录中...");//loginView的ui逻辑处理
                            userModel.login(loginView.getUsername(), loginView.getPassword(), new CallBack() {
                                @Override
                                public void onSuccess() {
                                    loginView.hideLoding();
                                    loginView.showResult("登录成功");
                                }
                                     @Override
                                public void onFailure(String fail) {
                                    loginView.hideLoding();
                                    loginView.showResult(fail);
                                }
                            });
                        } }
                           
                           

                          逻辑很简单,调用login方法,里面我们 需要先检查view和presenter是否已经建立连接,建立联系后我们再来获取目标的view来进行view逻辑的操作,同时我们通过model层来进行相关的业务处理逻辑,并且把处理结果传递给ui层。

                          然后我们初步的优化已经差不多了:(Activity具体使用)

                          1. 在activity的onCreate方法中我们记得实例化presenter
                          presenter = new LoginPresenter(new UserModel());
                             
                             
                          • 1
                          1. 在activity的onCreate方法中我们记得绑定View
                          presenter.attachView(this);//这里与View建立连接
                             
                             
                          • 1

                          3.在activity的onDestroy方法中presenter

                          presenter.detachView();//这里与View断开连接
                             
                             
                          • 1

                          显然我们还有优化的空间。


                          话说当Activity会在很多情况下被系统重启:当用户旋转屏幕、在后台时内存不足、改变语言设置、attache 一个外部显示器等。

                          我们如何控制presenter的生命周期呢:

                          1.将Presenter保存在一个地方,再次onCreate时还原
                          我见到的最多的就是这个方案,不过具体的实现方法千差万别。
                          最简单最naive的实现方式,就是在Activity/Fragment中保留对Presenter的静态引用。这样我们可以再手机旋转时保留Presenter对象,但整个Applicaiton中只能有一个同样的Activity/Fragment-Presenter实例组合。但当一个ViewPager中有多个相同的Fragment时,这种方法就行不通了。
                          2.另一个方式就是调用Fragment的setRetainInstance(true)方法。
                          设置Fragment的这个属性可以保证Fragment不会被destroy,这样Presenter就随之被保留。但这种方式仍有局限,对一个子Fragment或是Activity这样就不起作用。
                          3.最后一种方式就是使用单例缓存机制并通过标识符来存储一个Presenter类的不同实例。
                          为了保留Presenter,Activity/Fragment需要在onSaveInstanceState()中传递Presenter实例的标识符。这里的问题是如何实现这种逻辑以及何时将Presenter从单例缓存中移除。
                          4.通过Loader延长Presenter生命周期

                          Loader是Android框架中提供的在手机状态改变时不会被销毁的工具。Loader的生命周期是是由系统控制的,只有在向Loader请求数据的Activity/Fragment被永久销毁时才会被清除,所以也不需要自己写代码来清空它。

                          一般Loader是用来在后台加载数据的,而且是用它的子类CursorLoader或AsyncTaskLoader,尤其是CursorLoader,直接就绑定了Content Provider和数据库。当然如果写个类继承Loader基类的话也不需要开启后台线程。
                          自定义loader:

                          public class PresenterLoder<P extends Presenter> extends Loader<P> {
                              private  final PresenterFactory<P>  factory;
                              private P presenter;
                              public PresenterLoder(Context context, PresenterFactory<P> factory) {
                                  super(context);
                                  this.factory = factory;
                              }
                          ​
                              /**
                               * 在Activity的onStart()调用之后
                               */
                              @Override
                              protected void onStartLoading() {
                                  if(presenter != null){
                                      deliverResult(presenter);//会将Presenter传递给Activity/Fragment。
                                      return;
                                  }
                                  forceLoad();
                              }
                          ​
                              /**
                               * 在调用forceLoad()方法后自动调用,我们在这个方法中创建Presenter并返回它。
                               */
                              @Override
                              protected void onForceLoad() {
                                  presenter = factory.crate();//创建presenter
                                  deliverResult(presenter);
                              }
                          ​
                              /**
                               * 会在Loader被销毁之前调用,我们可以在这里告知Presenter以终止某些操作或进行清理工作。
                               */
                              @Override
                              protected void onReset() {
                                 presenter = null;
                              }
                          }
                             
                             

                            PresenterFactory:这个接口可以隐藏创建Presenter所需要的参数。通过这个接口我们可以调用各种构造器,这样可以避免写一堆PresenterLoader的子类来返回不同类型的Presenter。

                            public interface PresenterFactory<P extends Presenter>{
                                  P crate();//创建presenter
                            }
                               
                               
                            • 1
                            • 2
                            • 3

                            上诉代码只是创建一个presenter而已。

                            Activity的实现

                            从初级的代码我们知道activity里面的登录请求的进度条是可以抽取为通用的方法,这里再次封装一个BaseMvpActivity

                            public class BaseMvpActivity<P extends Presenter<V>,V extends BaseMvpView> extends AppCompatActivity implements BaseMvpView,LoaderManager.LoaderCallbacks<P>{
                                private final int BASE_LODER_ID = 1000;//loader的id值
                                private ProgressDialog progressDialog;//登录进度条
                                protected  P presenter;
                                @Override
                                protected void onCreate(@Nullable Bundle savedInstanceState) {
                                    super.onCreate(savedInstanceState);
                                    progressDialog = new ProgressDialog(this);//实例化progressDialog
                                    getSupportLoaderManager().initLoader(BASE_LODER_ID,null,this);//初始化loader
                                }
                            
                                @Override
                                protected void onStart() {
                                    super.onStart();
                                    presenter.attachView((V)this);//presenter与view断开连接
                                }
                            
                                @Override
                                public void showLoding(String msg) {
                                   progressDialog.setMessage(msg);//设置进度条加载内容
                                    if (! progressDialog.isShowing())//如果进度条没有显示
                                        progressDialog.show();//显示进度条
                                }
                            
                                @Override
                                public void hideLoding() {
                                   if (progressDialog.isShowing())
                                            progressDialog.dismiss();
                                }
                            
                                @Override
                                public void showErr(String err) {
                                    Toast.makeText(this, err, Toast.LENGTH_SHORT).show();
                                }
                            
                            
                                @Override
                                public Loader<P> onCreateLoader(int id, Bundle args) {
                                    return null;
                                }
                            
                                @Override
                                public void onLoadFinished(Loader<P> loader, P data) {
                                    presenter = data;
                                }
                            
                                @Override
                                public void onLoaderReset(Loader<P> loader) {
                                  presenter = null;
                                }
                            
                                @Override
                                protected void onDestroy() {
                                    presenter.detachView();
                                    super.onDestroy();
                                }
                            }
                               
                               

                              getSupportLoaderManager().initLoader(LOADER_ID, null, this);

                              onStart()方法执行时会创建Loader或重新连接到一个已经存在的Loader。在这里我们在onLoadFinished()里创建并传递Presenter对象。由于这些代码都是同步的,所以当onStart()方法执行完后Presenter也可以正常工作了。

                              其他方法都差不多,从原来的activity搬过来的,你会疑问为什么我们的onCreateLoader是空实现,因为这是我们的baseActivity肯定不会

                              创建具体的presenter得留给他的子类去实现。

                              LoginActivity的具体实现:

                              public class LoginActivity extends BaseMvpActivity<LoginPresenter, LoginView> implements LoginView, View.OnClickListener {
                                  private EditText username;
                                  private EditText password;
                                  private Button login;
                                  @Override
                                  protected void onCreate(Bundle savedInstanceState) {
                                      super.onCreate(savedInstanceState);
                                      setContentView(R.layout.activity_login);
                                      initView();
                                  }
                                  /**
                                   * 创建LoginPresenter实例
                                   * @param id
                                   * @param args
                                   * @return
                                   */
                                  @Override
                                  public Loader<LoginPresenter> onCreateLoader(int id, Bundle args) {
                                      return new PresenterLoder<>(this, new PresenterFactory<LoginPresenter>() {
                                          @Override
                                          public LoginPresenter crate() {
                                              return new LoginPresenter(new UserModel());
                                          }
                                      });
                                  }
                                  @Override
                                  public String getUsername() {
                                      return username.getText().toString().trim();
                                  }
                                  @Override
                                  public String getPassword() {
                                      return password.getText().toString().trim();
                                  }
                                  @Override
                                  public void showResult(String result) {
                                      Toast.makeText(this, result, Toast.LENGTH_SHORT).show();
                                  }
                                  private void initView() {
                                      username = (EditText) findViewById(R.id.username);
                                      password = (EditText) findViewById(R.id.password);
                                      login = (Button) findViewById(R.id.login);
                                      login.setOnClickListener(this);
                                  }
                                  @Override
                                  public void onClick(View v) {
                                      switch (v.getId()) {
                                          case R.id.login:
                                              presenter.login();//登录操作
                                              break;
                                      }
                                  }
                              }
                                 
                                 

                                看到没有,我们在这里实现了了onCreateLoader,在这里产生LoginPresenter

                                哈哈,整个LoginActivity又变干净了有木有,大部份Presenter,Loaders都放在了BaseActivity中。LoginActivity里只要实现onCreateLoader就行了。

                                写到这里也就差不多了,再来看下效果吧:
                                这里写图片描述

                                如何更高效的使用MVP以及官方MVP架构解析
                                T-MVP:泛型深度解耦下的MVP大瘦身

                                简单的 Android Clean Architecture 实现

                                   讲再多都是扯淡,还得自己动手写一下.
                                

                                demo 传送门:MVPDemo.zip

                                (function () {('pre.prettyprint code').each(function () { var lines = (this).text().split(\n).length;var numbering = $('
                                • ').addClass('pre-numbering').hide(); (this).addClass(hasnumbering).parent().append( numbering); for (i = 1; i
                                评论 1
                                添加红包

                                请填写红包祝福语或标题

                                红包个数最小为10个

                                红包金额最低5元

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

                                抵扣说明:

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

                                余额充值