Dagger2的使用,这一篇就够了!

最近博客明显减少哈,主要刚开始写有凑文章之嫌哈,所以这次静下心来写了这篇!纵观Dagger网上的大片文章通篇都是讲源码要么是用MVP讲解,或者直接就是讲一大堆理论,使部分开发人员入门较难,我这篇文章用极其简单易懂的Demo向大家介绍,毕竟大道至简嘛! Demo中也使用了Rxjava,Retrofit,Dagger进行代码的书写,当然写的也是极其简单易懂,不了解Rxjava和Retrofit的可以看我前面的博客,毕竟Demo中的部分代码是直接拷贝之前博客中的代码,至于rxjava有时间我会写一篇的!

大家既然想用Dagger,肯定多少有些了解,我就不介绍太多!,这篇偏向于实战,没有太多的理论!我觉得首先从最简单的入门然后慢慢深入,这样在学习的时候兴趣就会加深,一上来搞一套理论砸晕你,搞半天连个简单的Demo都写不出来,很容易让人止步不前,一但第一个简单demo写出来了,后面就水到渠成了!

本篇内容不讲废话,直接教你用Dagger,不会去介绍什么Dagger的好处,什么是依赖,什么是控制反转等等,这篇就是教你看完拿到手就能用Dagger,当然几个重要的注解必须介绍,

毕竟Dagger的核心就是那几个注解,注解的介绍也是用到才会介绍,文章末尾会放上一些链接用来补充Dagger的介绍以及优缺点以及一些理论等等,当然,如果有时间我也会写一篇!

一 Dagger环境配置

工程Gradle添加apt插件

dependencies {
        classpath 'com.android.tools.build:gradle:2.1.0'
        //添加apt插件
        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }

项目Gradle添加依赖

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    testCompile 'junit:junit:4.12'
    compile 'com.android.support:appcompat-v7:25.3.1'
    
    //Dagger依赖
    compile 'com.google.dagger:dagger:2.4'
    apt 'com.google.dagger:dagger-compiler:2.4'
    compile 'org.glassfish:javax.annotation:10.0-b28'
}

二 Demo演示

1 我们首先创建一个"人类",写一个空参构造函数,在构造函数上添加注解@Inject

public class Person {
    @Inject
    public Person() {
    }

    String name;
    String age;

    public String getName() {
        return name;
    }

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

    public String getAge() {
        return age;
    }

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

}

2 我们在MainAactivity中要将人类的值set到TextView中,所以要用到这个"人类",所以在MainActivity中吧"人类"这个成员变量也需要加上@Inject

public class MainActivity extends AppCompatActivity {
    @Inject
    Person xu;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        DaggerPersonComponent.builder().build().inject(this);  //重点
        TextView person = (TextView) findViewById(R.id.person);
        xu.setName("hha");
        person.setText(xu.getName());
    }
}
好了,这里需要介绍下@Inject这个注解了,我们一般new对象方式  Person xu = new Person();   而Inject的作用就是注解"人类"构造函数和需要被创建(依赖)"人类"对象的变量,注解后就可以被Dagger找到,如果"人类"类中还有其它对象的变量(其实是叫做依赖其他类,我为了考虑可能有些人看不懂就尽量使用白话,不然我还得去解释什么是依赖,见谅 ),那么根据前文的意思,也加上@Inject,这个其他对象的类的构造函数也需要加上@Inject,总结下@Inject就两个作用  1 标记构造函数 2标记依赖对象的变量


3 创建一个接口,这个接口相当于一个连接器,链接被@Inject标记的构造函数和变量,这个接口用@Component标注,让Dagger代码自动实现这个类,用来将"人类"对象注入到MainActivity中

@Component
public interface PersonComponent {
    void inject(MainActivity mainActivity);
}

自动实现这个接口的的类,会在接口名字前面加上Dagger关键字 ,因为我们接下来会用到这个类,在我的第二步时,我贴了MainAactivity的代码,是不是发现一句代码被我注释了重点二字?

DaggerPersonComponent.builder().build().inject(this);  //重点
这句代码就把Inject标注的变量注入了对象,这个DaggerPersonComponent类就是我们之前写的接口的实现类,这是代码自动生成的(点击Make Project就会自动生成)

然后跑一遍,没有任何问题,界面显示hha文字


--------------------------------------------------------------------------------------------------------华丽的分割线-------------------------------------------------------------------------------------------------------------

我知道你们看到这会有很多疑问,比如构造函数有参数呢?创建的对象是第三方的我们怎么加@Inject?别急,后面会说!

上面讲的是空参构造函数时的注入方法,现在我们来演示带参数的构造方式或者这个类是第三方提供的我们怎么进行注入呢?我们还在"人类"类的基础上进行Demo演示

如果一个类的构造带参数,那么如果还用Inject标注,那么这个参数怎么来呢?如果是第三方的类库,我们获取实例,是无法加上@Inject的,怎么办呢?为此又要引出@Module注解来提供依赖了,当然,如果你若问我,空参构造函数也可以通过module来注入么?我的回答是,当然可以了!

演示场景

之前的"人类"类,有两个属性name和age,现在构造方法一开始就必须传入name和age,让我们来一步步看下如何实现吧!

 1 修改之前的"人类"类,删掉空参构造函数,因为现在是通过@module来提供依赖,所以这个"人类"用不到@inject了一起删掉(这个"人类"带参数构造,假设这个"人类"是第三方类库)

 

public class Person {
    public Person(String name, String age) {
        this.name = name;
        this.age = age;
    }

    String name;
    String age;

    public String getName() {
        return name;
    }

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

    public String getAge() {
        return age;
    }

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

}
2 创建一个Module类用来生成依赖的对象(也就是给变量赋值时的对象),这里的@Module是用来注解这个类,除此之外还需要用到@Provide,这个注解是用来注解这个类里面的方法,这个类里面的方法专门提供依赖对象的,而这个方法有个约定俗成的命名规则以provide开头命名,不要问为什么照做就是,方便大家提高代码阅读性

@Module
public class MainActivityModule {
    public MainActivityModule(){ }

    @Provides
    Person providePerson(){
        return new Person("xu","20");
    }
}


3 现在module也创建好了,可以看到类被@Module注解,方法被@Provides注解,这个方法返回的是一个"人类"对象,这个对象我们给了参数,但是还不够,我们还要求需改下我们之前写的接口PersonComponent,之前的这个接口添加了注解@Component,但是没有参数,现在我们要带上参数,用来表示提供对象的是MainActivityModule这个类

@Component(modules = MainActivityModule.class)
public interface PersonComponent {
    void inject(MainActivity mainActivity);
}

4 现在我们来看看MainAactivity,在MainActivity中"人类"变量依然是用@Inject注解,但是之前我们的一句代码是将Inject联系在一起

DaggerPersonComponent.builder().build().inject(this);

现在换成由module提供依赖代码也要有所改变,

DaggerPersonComponent.builder().
                mainActivityModule(new MainActivityModule()).build().inject(this);
可以看到链式调用,多调用了一句代码,mainActivityModule(new MainActivityModule()),这相当于告诉Dagger把module提供的对象注入到MainActivity中,为什么是mainActivityModule方法呢,这个方法名字就是我们刚才写module类的类名,只是第一位字母改成小写了,然后将我们写的module类作为对象传入这个方法中

public class MainActivity extends AppCompatActivity {
    @Inject
    Person xu;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //DaggerPersonComponent.builder().build().inject(this);
        DaggerPersonComponent.builder().
                mainActivityModule(new MainActivityModule()).build().inject(this);
        TextView person = (TextView) findViewById(R.id.person);
        //xu.setName("hha");
        person.setText(xu.getName());
    }
}
run一下成功,显示xu,搞定!,你肯定会说这个参数的值是我手动设置进去的!比如我们最常用的context作为参数虽然可以通全局的appContext获取手动设置进module中,如果不这么做如何设置进去呢?很简单,首先module我们用的是无参构造,那就改成一个带context的构造函数,然后再MainAactivity中我们new Module对象时,将this传进去就ok了!


--------------------------------------------------------------------------------------------------------华丽的分割线-------------------------------------------------------------------------------------------------------------


到这里,问题又来了,module类的方法提供的对象,我们怎么知道这个方法是为谁提供的对象?为什么那么多变量偏偏用的是这个方法返回的对象?很简单,这是根据方法的返回值来确定的,你的方法返回"人类"的对象,那么势必会找到"人类"的成员进行赋值(前提是被@Inject)
那这个时候问题来了,如果还有一个方法返回也是"人类"的对象,那么此时被@Inject的"人类"变量到底该找哪个方法为自己赋值,比如这两个方法一个是new对象时传一个参数进去,一个是传两个,或者两个方法都是传一个参数,但是参数类型不同,这个时候Dagger就懵逼了,我到底该找那个方法为"人类"变量赋值呢?,这里就要引入@Qulifiler注解了,字面意思就是限定,用来限定某个"人类"变量只能找module中的某个方法来获取象,@Qulifiler的存在意义已经说明了,她是一个自定义注解,让我们去定义,让我们决定变量被哪个方法赋值,@Qulifiler就是用来区分moudule类有多个相同返回值时,就用这个自定义注解


演示场景

1 现在MainAactivity中有三个人类,分别是男人,女人,小孩,男人通过无参构造创建,女人通过有参构造传两个参数,小孩有第三个参数 萌,小孩的萌度用数值表示

现在首先让我们写三个注解,用来区分吧,注解类名没要求,但尽量易懂可读性强

@Qualifier
@Retention(RetentionPolicy.RUNTIME)
public @interface QualifierPersonChild {
}

@Qualifier
@Retention(RetentionPolicy.RUNTIME)
public @interface QualifierPersonMan {
}

@Qualifier
@Retention(RetentionPolicy.RUNTIME)
public @interface QualifierPersonWoMen {
}

2 更改module类,必然要写三个方法返回Person类对象,只不过方法创建对象的构造函数不同,这个module中我打算传参数给module然后赋值给男人   ,"人类"类就加一个int属性表示萌的程度,以及复写toString我就不上代码了!

下面代码可以看到,我给他们标识了自定义注解,然后大家再想一下我在MainAactivity中给变量也标识上对应的自定义注解,那是不是各自对应上了?,module中的无参构造我就注视了,我们强制用module的有参构造创建对象,传入的值,我们只给男人用,女人和小孩都不用(我之所以传参给module就是为了对应模拟之前上文说的context传进来当参数的场景)

@Module
public class MainActivityModule {
    public MainActivityModule(String age, String name) {
        this.age = age;
        this.name = name;
    }

    String name;//这两个属性是我为了做演示,传值给男人,男人专用
    String age;
    @QualifierPersonWoMen
    @Provides
    Person providePersonWomen(){
        return new Person("妹子","20");
    }
    @QualifierPersonMan
    @Provides
    Person providePersonMan(){
        Person man = new Person();
        man.setName(name);
        man.setAge(age);
        return man;
    }

    @QualifierPersonChild
    @Provides
    Person providePersonChild(){
        return new Person("小孩","3",90);
    }
}


 

 

3 我们看下MainActivity中的代码,首先是要写三个人类变量对应男人女人和小孩,其次创建Module类是也需要要传入参数了,三个变量也需要用对应的自定义注解,这样Dagger就通过相同的注解,将module中的对应的方法返回给对应的变量!且看代码!

public class MainActivity extends AppCompatActivity {
    @QualifierPersonMan
    @Inject
    Person man;
    @QualifierPersonWoMen
    @Inject
    Person women;

    @QualifierPersonChild
    @Inject
    Person child;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //DaggerPersonComponent.builder().build().inject(this);
        DaggerPersonComponent.builder().
                mainActivityModule(new MainActivityModule("男人","24")).build().inject(this);
        TextView person = (TextView) findViewById(R.id.person);
        String info = man.toString()+"\n"+women.toString()+"\n"+child.toString();
        person.setText(info);
        Log.e("MainActivity",info);
    }
}

最后出来的log

05-27 09:37:11.801 24621-24621/com.check.myapplication E/MainActivity: Person{age='男人', name='24', lovelines=0}
                                                                       Person{age='20', name='妹子', lovelines=0}
                                                                       Person{age='3', name='小孩', lovelines=90}

当然,这是自定义的,有点麻烦对吧?所以Dagger提供了@Qualifier的子类@Named注解,使用很简单把上面的自定义注解替换成@Named("随便写,保持唯一即可"),然后保持一致就行了

--------------------------------------------------------------------------------------------------------华丽的分割线-------------------------------------------------------------------------------------------------------------

演示场景 :

现在,我们的项目一般都有全局的变量要用到,既然是全局的那一定是单利,生命周期作用域为整个app,我这里以Retrofit进行网络连接做演示,一般进行网络请求我们都会对网络请求类进行包装

1 首先写一下网络请求Retrofit要用到接口,而且我们是用rxjava,所以接口这么写即可,我直接用公司的接口请求数据了,rxjava其实很好掌握,但是这里的重点是我们建立全局

的对象

public interface  Servies {

    @GET("getAllParts")
    Observable<ArrayList<PartInfo>> loadVersionInfo();

}

//对网络请求框架进行包装

public class WrapServies {
    Retrofit retrofit;
    Servies servicesDemo;

    public WrapServies(Retrofit retrofit) {
        this.retrofit = retrofit;
        this.servicesDemo = retrofit.create(Servies.class);
    }

    public rx.Observable<ArrayList<PartInfo>> loadVersion() {
        rx.Observable<ArrayList<PartInfo>> arrayListObservable = servicesDemo.loadVersionInfo();
        //versionBeanCall.enqueue(callback);
        return arrayListObservable;
    }
}

以上就是我们的封装的简单网络请求!


2 现在我们写全局的module

@Module
public class ApiModule {

    public static final String BASE_URL="http://120.76.233.167/bridgeapis//rest//";

    @Provides
    @Singleton
    Retrofit provideRetrofit() {

        Retrofit retrofit=new Retrofit.Builder().baseUrl(BASE_URL)//配置主机地址
                .addCallAdapterFactory(RxJavaCallAdapterFactory.create())//新的配置,rxjava结合Retrofit使用时配置这个
                .addConverterFactory(GsonConverterFactory.create()).build();//配置数据转换器
        return retrofit;
    }
   
   @Provides
   @Singleton
    WrapServies provideWrapServies(Retrofit retrofit){
       WrapServies wrapServies = new WrapServies(retrofit);
       return wrapServies;
    }
}
可以看到provideWrapServies方法需要Retrofit,你可能会说没人实参只有形参,如何保证传入的Retrofit不为null,其实上面的provideRetrofit方法的返回值就是我们给的参数

在Daggeer自动生成的代码中我们可以看到是通过get方法返回的Retrofit对象


我们将需要注入的对象暴露出去,可以看到与之前的对比我们多了一个注解@Singleton(是@Scope的默认实现),这个注解是用来标识全局的Component类以及标注全局的module中

的方法

@Singleton
@Component(modules = ApiModule.class)
public interface AppComponent {
  
    WrapServies getWrapServies();//配置全局的Component把需要全局的对象暴露出去(返回值)
    //不用再写类似inject方法,这个全局的最后会依赖到其它非全局的Component
}

3 到这里全局的module和component就写好了,但是我们要把全局的对下注入到类中,所以我们需要一个Application创建这个全局唯一的Component,然后这个全局的Component要

依赖到普通Component,这样才能成功注入全局的对象

这个是application
public class MyApplication extends Application {
    AppComponent appComponent;
    @Override
    public void onCreate() {
        super.onCreate();
        //很关键,这样才能保证实例化的对象是全局唯一,因为AppComponent是全局唯一了
        //但这只是其中一个原因,这就是为什么全局的我们卸载application中
        appComponent = DaggerAppComponent.builder()
                .apiModule(new ApiModule())
                .build();

    }
    public AppComponent getAppComponent() {
        return appComponent;
    }
}
让我们普通的Component依赖全局的Component,这样注入时,就会把全局的对象也一起注入,建议自定义注解,让对象绑定Activity的生命周期
@ActivityScope //自定义注解用来将对象绑定Activity生命周期
@Component(dependencies = AppComponent.class,modules = MainActivityModule.class)
public interface PersonComponent {//可以看到仅仅是添加了dependencies=AppComponent.class这一行
    void inject(MainActivity mainActivity);
}

这样基本就已经完成了所有铺垫
 
4 现在让我们看看如何在Aactivity如何使用吧,我这里使用了rxjava,一些重点注意点我写在demo中了,可以github下载下来自行浏览
 
 
 
public class MainActivity extends AppCompatActivity {
    @QualifierPersonMan
    @Inject
    Person man;


    @QualifierPersonWoMen
    @Inject
    Person women;

    @QualifierPersonChild
    @Inject
    Person child;

    @Inject
    WrapServies mWrapServies;//这就是全局的对象了

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //DaggerPersonComponent.builder().build().inject(this);
        AppComponent appComponent = ((MyApplication) getApplication()).getAppComponent();//获取全局的AppComponent
        DaggerPersonComponent.builder().
                appComponent(appComponent).//这里添加全局的Component
                mainActivityModule(new MainActivityModule("男人", "24")).build().inject(this);


        TextView person = (TextView) findViewById(R.id.person);
        String info = man.toString() + "\n" + women.toString() + "\n" + child.toString();
        //String setRetrofit = retrofit==null?retrofit.baseUrl().toString():"null";
        person.setText(info);
        Log.e("MainActivity", info);
        Button btn = (Button) findViewById(R.id.btn);
        btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mWrapServies.loadVersion().subscribeOn(Schedulers.newThread())//请求在新的线程中执行
                        .observeOn(AndroidSchedulers.mainThread())//切换导致线程,切不切无所谓,毕竟没修改ui
                        .subscribe(new Action1<ArrayList<PartInfo>>() {
                            @Override
                            public void call(ArrayList<PartInfo> partInfos) {
                                //TODO 如果使用lambad语法会更加简洁,但是考虑到可读性以及基础较差的读者,就不写了,有兴趣的可以自己按照我的博客自己配置
                                if (partInfos == null) {
                                    Log.e("返回的数据", "数据为0");
                                } else {
                                    for (int i = 0; i < partInfos.size(); i++) {
                                        Log.e("返回的数据", partInfos.get(i).getId() + "");
                                    }
                                }

                            }
                        }, new Action1<Throwable>() {
                            @Override
                            public void call(Throwable throwable) {
                                throwable.printStackTrace();
                            }
                        });
            }

        });
    }
}

到这里基本的注解就学完了,已经写得很简单了,看不懂我也没办法了..你也可以留言告诉我哪些没看懂,有时间就会回复
 
 
在介绍几个简单的注解,因为太简单就不写例子了
一 Lazy<T>
上面的例子是MainActivity一进来,所有的对象全被初始化好了,但我们只想在使用到的时候在进行初始化,这里就是懒加载了,用法很简单,Lazy<T>包装一下对象,用的时候
使用get方法获取即可
比如
    @QualifierPersonMan
    @Inject
    Lazy<Person> man;//延迟加载


    //用的时候
    Person person = man.get();
二 Provider<T>
上面都是纸创建一个类型的对象,如果我需要创建多个类的对象呢?那么使用Provider包装一下要创建的类,然后get获取,获取到的对象都是新的
    @QualifierPersonMan
    @Inject
    Provider<Person> mPersonProvider;

    Person person1 = mPersonProvider.get();

三 注入的优先级
 
当有inject和module提供对象,那么是用的是哪个对象?这酒确定了优先级问题,在Dagger2中优先级如下
懒得打字,直接拷贝牛晓伟大神的,稍后会提供他的链接,很好的文章,偏向理论,我本篇文章的Demo大家自己敲一遍,再看一遍大他的文章你会豁然开朗,这就是为什么前期我不说任何理论
步骤1:查找Module中是否存在创建该类的方法。
步骤2:若存在创建类方法,查看该方法是否存在参数
    步骤2.1:若存在参数,则按从**步骤1**开始依次初始化每个参数
    步骤2.2:若不存在参数,则直接初始化该类实例,一次依赖注入到此结束
步骤3:若不存在创建类方法,则查找Inject注解的构造函数,
           看构造函数是否存在参数
    步骤3.1:若存在参数,则从**步骤1**开始依次初始化每个参数
    步骤3.2:若不存在参数,则直接初始化该类实例,一次依赖注入到此结束

四 Component的依赖
 
相信上面大家也看到普通Component和全局的Component的依赖了,全局的Component中必须暴露处,否则依赖依然会失败
Component可以相互依赖,被依赖的可以继续依赖其他Component
五  @scope有一个子类就是@Singleton,永安里保证全局唯一对象,上面也介绍了!如果通过@Scope自定义注解,并将自定义的注解标识在module对应的方法以及对应的Compoent
这样在初始化时,如果这个类被Inject两次,但是只会初始化一次(也就是两个对象地址相同),这样就实现了局部的单例(当然也可以不写,也不会报错,但是建议写上)
大体上,到这里的就全部讲完了,会提供几个讲解的比较好的Dagger文章,让大家相互参考提高理解,我会把Demo上传到github供大家下载
http://www.jianshu.com/p/cd2c1c9f68d4 一共三篇
http://www.jianshu.com/p/1d42d2e6f4a5
http://www.jianshu.com/p/65737ac39c44

Scope作用域:没有指定作用域的@Provides方法将会在每次注入的时候都创建新的对象。

https://www.jianshu.com/p/01d3c014b0b1

  • 由此推断,@Scope只能在Component所注入的作用域内保持“单例”
  • 在Application中注入的是全局单例
https://blog.csdn.net/u013262051/article/details/51283456

scope注解的provide方法,其Component类也需要注解,被Scope注解的Provide方法在被Component注入

的范围内会是单利,也就是"局部单利",该范围内多次注入的都会是同一个对象,是同一个实例

比如一个类 两个成员变量都被@Inject注解了,那么实例化他们的时候,如果Provide方法提供的对象被@sope

注解了那么就会返回同一对象,否则就会new 2次

module内提供对象的provide方法,不是需要全部注解@scope,而是需要哪个单例才加,但是只要有一个加了,那么

Component也需要添加

这里要注意了如果Component和 Component有相互依赖的关系或者继承关系,那么他们的scope必须不能相同

https://www.jianshu.com/p/3028f491006b

Component可以通过其他的Component完成依赖注入,可以划分为:依赖方式 和 继承方式

Component可以依赖其他的Component,通过dependencies

@Component(modules = LoginActivityModule.class, dependencies = BaseApplicationComponent.class)

但是被依赖的Component必须暴露出对象,比如平时我们的Component是注入对象的通过

inject方式将module或者Component提供的对象注入到指定的类中,比如:

@Component(modules = LoginActivityModule.class, dependencies = BaseApplicationComponent.class)
public interface LoginActivityComponent {
    void inject(LoginActivity loginActivity);
}

但是被依赖的Component需要暴露出来,否则无法提供比如

@Singleton
@Component(modules = {BaseApplicationModule.class})
public interface BaseApplicationComponent {

    BaseApplication getApplication();

    DataServiceSource getBridgeService();
}
而这些暴露出来的方法提供的对象都是由这个BaseApplicationModulemodule中提供的


下面介绍继承方式

懒得写代码了,直接提供连接看

https://www.jianshu.com/p/3028f491006b

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值