当前位置: 首页 > news >正文

Dagger2从入门到放弃

写在前面

尝试用ds写技术博文,写的过程中也算是温故知新了~
今日题目:Dagger2


文章目录

  • 一、前置知识准备
    • 理解依赖注入(DI)的核心概念
    • 掌握 Java/Kotlin 关键语法
  • 二、核心组件解析
    • 1.四大核心注解
    • 2.作用域(Scope)机制
    • 3.依赖图(Dependency Graph)原理
    • 核心原理总结
  • 三、进阶实战

Dagger2 系统学习路径

一、前置知识准备

理解依赖注入(DI)的核心概念

依赖注入(Dependency Injection,DI)的核心概念是将对象之间的依赖关系从代码内部转移到外部容器管理,从而实现代码解耦、提升可维护性和可测试性。

1. 控制反转(Inversion of Control, IoC)

定义:程序的控制权从内部转移到外部容器或框架,由容器管理对象的生命周期和依赖关系。
与DI的关系:DI是IoC的一种实现方式。IoC强调“控制权反转”,而DI通过“注入依赖”具体实现这一点。

2. 依赖注入的三种方式
构造函数注入:通过构造函数传递依赖项。

优点:强制依赖在对象创建时明确,保证对象始终处于有效状态。

示例:

public class UserService {
    private final UserRepository repository;
    public UserService(UserRepository repository) {
        this.repository = repository;
    }
}
属性注入:通过公共属性或Setter方法设置依赖。

优点:灵活性高,适合可选依赖或动态变更。

示例:

public class UserService {
    private UserRepository repository;
    public void setRepository(UserRepository repository) {
        this.repository = repository;
    }
}
方法注入:通过方法参数传递依赖。

适用场景:依赖仅在特定方法中使用,或需要运行时动态确定。

示例:

public class UserService {
    public void process(UserRepository repository) {
        // 使用repository执行操作
    }
}
3. 解耦与面向接口编程

解耦:通过依赖抽象(接口或抽象类)而非具体实现,减少组件间的直接耦合。

示例:

public interface UserRepository {
    User findById(String id);
}

public class MySqlUserRepository implements UserRepository { /* ... */ }

public class UserService {
    private final UserRepository repository;
    public UserService(UserRepository repository) {
        this.repository = repository; // 依赖抽象,而非具体类
    }
}
4. 依赖注入容器(DI Container)
  • 作用:自动化管理对象的创建、依赖解析和生命周期。
  • 功能:
    • 注册:绑定接口到具体实现(如IUserRepository → MySqlUserRepository)。
    • 解析:自动创建对象并注入所需依赖。
    • 生命周期管理:支持单例(Singleton)、瞬态(Transient)、作用域(Scoped)等模式。

掌握 Java/Kotlin 关键语法

注解处理器(APT)的工作原理
1. 编译阶段触发
  • APT 在 Java 编译过程中被触发,通常在 javac 编译器的编译阶段执行。
  • 编译器会扫描源代码中的注解,并将其传递给注册的注解处理器。
2. 注解扫描
  • 编译器会扫描源代码中的所有注解,并生成一个抽象语法树(Abstract Syntax Tree, AST)。
  • 这些注解信息会被封装成 Element 对象,供注解处理器使用。
3. 注解处理器注册
  • 注解处理器是通过实现 javax.annotation.processing.Processor 接口来定义的。
  • 注解处理器需要注册到编译器中,通常通过 META-INF/services/javax.annotation.processing.Processor 文件来实现。
4. 注解处理
  • 注册的注解处理器会依次处理扫描到的注解。
  • 注解处理器可以通过 ProcessingEnvironment 获取编译上下文信息,如文件管理器、类型检查器等。
  • 注解处理器可以通过 RoundEnvironment 获取当前轮次处理的注解元素。
5. 生成代码或资源
  • 注解处理器可以根据注解信息生成新的源代码文件、资源文件或修改现有的代码。
  • 生成的代码会被编译器进一步编译,成为最终编译输出的一部分。
6. 多轮处理
  • 注解处理可能会进行多轮(Round),每一轮处理结束后,编译器会检查是否有新的注解生成。
  • 如果有新的注解生成,编译器会启动新一轮的处理,直到没有新的注解生成为止。
7. 编译完成
  • 当所有注解处理器处理完毕,且没有新的注解生成时,编译器会完成编译过程,生成最终的 .class 文件。
Kotlin 中 @JvmStatic 在 Module 中的应用

在 Kotlin 的 Module 中,@JvmStatic 注解主要用于将 companion objectobject 中的成员暴露为 Java 兼容的静态成员,方便 Java 代码调用。它在工具类、单例模式和常量定义等场景中非常有用,能够提升 Kotlin 与 Java 混合开发的兼容性。

@JvmStatic 的作用
  1. 兼容 Java 调用:Kotlin 中的 companion object 成员默认不是静态的,Java 调用时需要先访问 Companion 实例。@JvmStatic 可以将其暴露为真正的静态成员,方便 Java 代码直接调用。
  2. 简化访问方式:通过 @JvmStatic,Java 代码可以直接通过类名访问方法或属性,而不需要通过 Companion
@JvmStatic 在 Module 中的应用场景

Module 中,@JvmStatic 通常用于以下场景:

  1. 工具类或工具方法:在 Module 中提供静态工具方法,方便其他模块(尤其是 Java 模块)调用。
  2. 单例模式:在 Module 中实现单例模式时,通过 @JvmStatic 提供静态访问方式。
  3. 常量定义:在 Module 中定义常量时,通过 @JvmStatic 暴露为静态常量。
示例代码
1. 工具类中的 @JvmStatic
// 在 Module 中定义一个工具类
class MathUtils {
    companion object {
        @JvmStatic
        fun add(a: Int, b: Int): Int {
            return a + b
        }
    }
}

在 Java 中调用:

// Java 代码调用
int result = MathUtils.add(2, 3); // 直接通过类名调用
2. 单例模式中的 @JvmStatic
// 在 Module 中实现单例模式
class AppConfig private constructor() {
    companion object {
        @JvmStatic
        val instance: AppConfig by lazy { AppConfig() }
    }

    fun getConfigValue(): String {
        return "Config Value"
    }
}

在 Java 中调用:

// Java 代码调用
String value = AppConfig.instance.getConfigValue();
3. 常量定义中的 @JvmStatic
// 在 Module 中定义常量
class AppConstants {
    companion object {
        @JvmStatic
        val MAX_COUNT: Int = 100
    }
}

在 Java 中调用:

// Java 代码调用
int maxCount = AppConstants.MAX_COUNT;
注意事项
  1. @JvmStatic 只能用于 companion objectobject 中的成员:普通类中的成员不能使用 @JvmStatic
  2. Kotlin 中优先使用 objectcompanion object:Kotlin 本身推荐使用 objectcompanion object 来实现单例或静态成员,而不是直接使用静态成员。
  3. @JvmStatic 不会改变 Kotlin 中的调用方式:在 Kotlin 中,companion object 的成员仍然需要通过类名或 companion 访问,@JvmStatic 主要是为了兼容 Java。

二、核心组件解析

1.四大核心注解

1. @Module
  • 作用:定义一个提供依赖的模块。@Module 类中包含一些用 @Provides 注解标记的方法,这些方法负责创建和提供依赖对象。
  • 实现原理
    • Dagger 2 在编译时会扫描所有 @Module 注解的类。
    • 这些类中的 @Provides 方法会被提取出来,生成对应的工厂类(Factory),用于创建依赖实例。
    • 这些工厂类会被集成到 @Component 生成的代码中,供依赖注入时使用。
  • 使用场景:当某个依赖无法通过构造函数注入(例如第三方库的类)时,可以使用 @Module 来提供这些依赖。
  • 示例
    @Module
    public class NetworkModule {
        @Provides
        OkHttpClient provideOkHttpClient() {
            return new OkHttpClient();
        }
    }
    
  • 编译后,Dagger 2 会生成一个 NetworkModule_ProvideOkHttpClientFactory 类,用于创建 OkHttpClient 实例。
2. @Provides
  • 作用:在 @Module 类中标记方法,表示该方法用于提供某个依赖实例。
  • 实现原理
    • Dagger 2 在编译时会为每个 @Provides 方法生成一个工厂类(Factory)。
    • 这些工厂类实现了 Provider<T> 接口,用于在运行时创建依赖实例。
    • 当需要注入某个依赖时,Dagger 2 会调用对应的工厂类来创建实例。
  • 使用场景:当需要手动创建依赖对象时,使用 @Provides 注解的方法。
  • 示例
    @Module
    public class NetworkModule {
        @Provides
        Retrofit provideRetrofit(OkHttpClient client) {
            return new Retrofit.Builder()
                    .baseUrl("https://api.example.com")
                    .client(client)
                    .build();
        }
    }
    
  • 编译后,Dagger 2 会生成一个 NetworkModule_ProvideRetrofitFactory 类,用于创建 Retrofit 实例。
3. @Inject
  • 作用
    • 标记构造函数、字段或方法,表示需要注入依赖。
    • 在构造函数上使用 @Inject 时,Dagger 2 会自动创建该类的实例并注入所需的依赖。
  • 实现原理
    • Dagger 2 在编译时会扫描所有 @Inject 注解的字段、构造函数或方法。
    • 对于构造函数注入,Dagger 2 会生成一个工厂类(Factory),用于创建该类的实例。
    • 对于字段或方法注入,Dagger 2 会生成代码,在运行时将依赖注入到目标类中。
  • 使用场景
    • 构造函数注入:用于标记需要注入依赖的构造函数。
    • 字段注入:用于标记需要注入依赖的字段。
    • 方法注入:用于标记需要注入依赖的方法。
  • 示例
    public class UserService {
        private final Retrofit retrofit;
    
        @Inject
        public UserService(Retrofit retrofit) {
            this.retrofit = retrofit;
        }
    }
    
  • 编译后,Dagger 2 会生成一个 UserService_Factory 类,用于创建 UserService 实例。
4. @Component
  • 作用:定义一个依赖注入的桥梁,将 @Module 提供的依赖注入到目标类中。
  • 实现原理
    • Dagger 2 在编译时会扫描所有 @Component 注解的接口或抽象类。
    • 根据 @Component 的配置(如 modulesdependencies),Dagger 2 会生成一个实现类(以 Dagger 为前缀)。
    • 生成的实现类会集成所有 @Module@Provides 生成的工厂类,并在运行时完成依赖注入。
  • 使用场景:通过 @Component 接口或抽象类,指定需要注入的模块(@Module)和目标类。
  • 示例
    @Component(modules = {NetworkModule.class})
    public interface AppComponent {
        void inject(MainActivity activity);
    }
    
  • 编译后,Dagger 2 会生成一个 DaggerAppComponent 类,用于将 NetworkModule 提供的依赖注入到 MainActivity 中。
  • 注意
    • @Component 可以依赖其他 @Component
    • 通过 @Component 生成的类会以 Dagger 为前缀,例如 DaggerAppComponent

2.作用域(Scope)机制

1. 作用域的基本概念
  • 作用域:作用域是一个注解,用于标记某个依赖对象在特定范围内的生命周期。
  • 单例性:在同一个作用域内,依赖对象是单例的,即每次请求都会返回同一个实例。
  • 作用域链:Dagger 2 支持嵌套作用域,子作用域可以访问父作用域的依赖,但父作用域不能访问子作用域的依赖。
2. 常用的作用域注解
  • @Singleton:默认的全局作用域,表示依赖对象在整个应用生命周期内是单例的。
  • 自定义作用域:可以根据需要定义自定义作用域,例如 @ActivityScope@FragmentScope 等。
3. 作用域的实现原理
  • 作用域与 @Component 的绑定:作用域必须与 @Component 绑定,@Component 的作用域决定了其提供的依赖对象的生命周期。
  • 作用域与 @Provides 的绑定:在 @Module 中,使用作用域注解标记 @Provides 方法,表示该方法提供的依赖对象在特定作用域内是单例的。
  • 作用域与依赖图:Dagger 2 在编译时会根据作用域生成依赖图,确保在同一个作用域内,依赖对象是单例的。
4. 作用域的使用示例
示例 1:全局单例作用域(@Singleton
@Module
public class AppModule {
    @Provides
    @Singleton
    ApiService provideApiService() {
        return new ApiService();
    }
}

@Component(modules = {AppModule.class})
@Singleton
public interface AppComponent {
    ApiService getApiService();
}
  • 解释
    • @Singleton 注解标记了 ApiService 的作用域为全局单例。
    • AppComponent 的作用域为 @Singleton,因此 ApiService 在整个应用生命周期内是单例的。
示例 2:自定义作用域(@ActivityScope
@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface ActivityScope {}

@Module
public class ActivityModule {
    @Provides
    @ActivityScope
    UserManager provideUserManager() {
        return new UserManager();
    }
}

@Component(modules = {ActivityModule.class}, dependencies = {AppComponent.class})
@ActivityScope
public interface ActivityComponent {
    UserManager getUserManager();
}
  • 解释
    • @ActivityScope 是一个自定义作用域,表示 UserManagerActivityComponent 的作用域内是单例的。
    • 每次创建 ActivityComponent 时,UserManager 都是一个新的实例,但在同一个 ActivityComponent 内是单例的。
5. 作用域链与嵌套作用域
  • 作用域链:Dagger 2 支持嵌套作用域,子作用域可以访问父作用域的依赖,但父作用域不能访问子作用域的依赖。
  • 示例
    @Component(modules = {ActivityModule.class}, dependencies = {AppComponent.class})
    @ActivityScope
    public interface ActivityComponent {
        UserManager getUserManager();
        ApiService getApiService(); // 从父作用域(AppComponent)获取
    }
    
  • 解释
    • ActivityComponentAppComponent 的子作用域,因此可以访问 AppComponent 提供的依赖(如 ApiService)。
    • AppComponent 不能访问 ActivityComponent 提供的依赖(如 UserManager)。

3.依赖图(Dependency Graph)原理

1. 依赖图的基本概念
  • 节点:依赖图中的每个节点代表一个依赖对象(如类实例、接口实现等)。
  • :依赖图中的边表示依赖关系,例如类 A 依赖类 B,则在 A 和 B 之间会有一条边。
  • 工厂类:每个节点对应一个工厂类(Factory),用于在运行时创建依赖对象。
2. 依赖图的构建过程

Dagger 2 在编译时通过注解处理器扫描代码,根据 @Module@Provides@Inject@Component 等注解生成依赖图。具体步骤如下:

2.1 扫描注解
  • 扫描所有 @Module 类,提取其中的 @Provides 方法。
  • 扫描所有 @Inject 注解,包括构造函数、字段和方法。
  • 扫描所有 @Component 接口或抽象类,提取其依赖的 @Module 和其他 @Component
2.2 生成工厂类
  • 为每个 @Provides 方法和 @Inject 构造函数生成一个工厂类(Factory)。
  • 工厂类实现了 Provider<T> 接口,用于在运行时创建依赖对象。
2.3 构建依赖图
  • 根据 @Component 的配置,将 @Module@Inject 生成的工厂类组织成一个依赖图。
  • 依赖图描述了各个依赖对象之间的关系,例如类 A 依赖类 B,类 B 依赖类 C。
2.4 生成组件类
  • 根据依赖图,生成 @Component 的实现类(以 Dagger 为前缀)。
  • 组件类负责在运行时根据依赖图创建和注入依赖对象。
3. 依赖图的核心原理
3.1 依赖解析
  • 当需要注入某个依赖时,Dagger 2 会从依赖图中查找该依赖的工厂类。
  • 如果依赖对象还依赖其他对象,Dagger 2 会递归解析这些依赖,直到所有依赖都被满足。
3.2 单例性
  • 如果依赖对象的作用域是单例的(如 @Singleton),Dagger 2 会在依赖图中缓存该对象,确保每次请求都返回同一个实例。
  • 如果依赖对象没有作用域,Dagger 2 会每次请求时都创建一个新的实例。
3.3 作用域链
  • 如果 @Component 依赖其他 @Component,Dagger 2 会将它们的依赖图合并,形成作用域链。
  • 子作用域可以访问父作用域的依赖,但父作用域不能访问子作用域的依赖。
4. 依赖图的示例

假设有以下代码:

@Module
public class NetworkModule {
    @Provides
    OkHttpClient provideOkHttpClient() {
        return new OkHttpClient();
    }

    @Provides
    Retrofit provideRetrofit(OkHttpClient client) {
        return new Retrofit.Builder()
                .baseUrl("https://api.example.com")
                .client(client)
                .build();
    }
}

@Component(modules = {NetworkModule.class})
public interface AppComponent {
    Retrofit getRetrofit();
}

Dagger 2 的依赖图构建过程:

  1. 扫描 NetworkModule,生成两个工厂类:
    • NetworkModule_ProvideOkHttpClientFactory:用于创建 OkHttpClient
    • NetworkModule_ProvideRetrofitFactory:用于创建 Retrofit
  2. 构建依赖图:
    • Retrofit 依赖 OkHttpClient
    • OkHttpClient 不依赖其他对象。
  3. 生成 AppComponent 的实现类 DaggerAppComponent,集成上述工厂类。
5. 依赖图的运行时行为

在运行时,Dagger 2 会根据依赖图完成依赖注入。例如:

AppComponent component = DaggerAppComponent.create();
Retrofit retrofit = component.getRetrofit();
  • 调用 component.getRetrofit() 时,Dagger 2 会从依赖图中查找 Retrofit 的工厂类。
  • 发现 Retrofit 依赖 OkHttpClient,于是先调用 OkHttpClient 的工厂类创建 OkHttpClient 实例。
  • 最后调用 Retrofit 的工厂类,传入 OkHttpClient 实例,创建 Retrofit 实例并返回。
6. 依赖图的优点
  • 编译时验证:依赖图在编译时生成,因此可以提前发现依赖注入的问题,避免运行时错误。
  • 高效性:依赖图的解析和注入在编译时已经确定,运行时不需要使用反射,性能高效。
  • 灵活性:通过 @Module@Component 的配置,可以灵活地管理依赖关系。
7. 依赖图的注意事项
  • 循环依赖:如果依赖图中存在循环依赖,Dagger 2 会抛出编译时错误。
  • 作用域滥用:过度使用作用域会导致依赖图复杂化,可能引发内存泄漏或不必要的资源占用。
  • 依赖冲突:如果同一个类型有多个依赖提供者,需要使用 @Qualifier 注解来区分。

核心原理总结

  1. 编译时生成代码:Dagger 2 在编译时通过注解处理器(APT)扫描代码,生成工厂类(Factory)和组件类(Component)。
  2. 依赖图的构建:Dagger 2 会根据 @Module@Provides 生成依赖图,描述各个依赖之间的关系。
  3. 运行时注入:在运行时,Dagger 2 通过生成的工厂类和组件类,按照依赖图创建实例并完成注入。
  4. 高效和类型安全:由于依赖注入的逻辑在编译时已经确定,Dagger 2 在运行时不会使用反射,因此性能高效且类型安全。

三、进阶实战

1. 全局组件配置

1.1 在 Application 类初始化 AppComponent
  • Application 类中初始化全局的 AppComponent,并将其存储为单例,供整个应用使用。
  • 示例:
    public class MyApplication extends Application {
        private AppComponent appComponent;
    
        @Override
        public void onCreate() {
            super.onCreate();
            appComponent = DaggerAppComponent.builder()
                    .appModule(new AppModule(this))
                    .build();
        }
    
        public AppComponent getAppComponent() {
            return appComponent;
        }
    }
    
1.2 通过 AndroidInjection 自动注入
  • 使用 Dagger 2 的 AndroidInjection 自动注入 ActivityFragment 等 Android 组件。
  • 示例:
    public class MainActivity extends AppCompatActivity {
        @Inject
        ApiService apiService;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            AndroidInjection.inject(this);
            super.onCreate(savedInstanceState);
            // 使用 apiService
        }
    }
    
  • Application 中设置 AndroidInjection
    public void onCreate() {
        super.onCreate();
        DaggerAppComponent.builder()
                .application(this)
                .build()
                .inject(this);
    }
    

2. 多模块工程结构

2.1 按功能模块拆分 Component
  • 将应用按功能模块拆分为多个 Component,例如 AppComponentFeatureComponent 等。
  • 示例:
    @Component(modules = {AppModule.class})
    @Singleton
    public interface AppComponent {
        FeatureComponent featureComponent();
    }
    
2.2 使用 @Subcomponent 实现层级依赖
  • 使用 @Subcomponent 实现 Component 的层级依赖,子组件可以访问父组件的依赖。
  • 示例:
    @Subcomponent(modules = {FeatureModule.class})
    public interface FeatureComponent {
        void inject(FeatureActivity activity);
    }
    
  • 在父组件中声明子组件:
    @Component(modules = {AppModule.class})
    @Singleton
    public interface AppComponent {
        FeatureComponent featureComponent(FeatureModule module);
    }
    

3. 动态参数注入

3.1 通过 @BindsInstance 传递运行时参数
  • 使用 @BindsInstanceComponent 构建时传递运行时参数。
  • 示例:
    @Component(modules = {AppModule.class})
    @Singleton
    public interface AppComponent {
        @Component.Builder
        interface Builder {
            @BindsInstance
            Builder application(Application application);
            AppComponent build();
        }
    }
    
3.2 构建动态 Module(如不同环境的基础 URL)
  • 通过动态 Module 传递运行时参数,例如不同环境的基础 URL。
  • 示例:
    @Module
    public class NetworkModule {
        private final String baseUrl;
    
        public NetworkModule(String baseUrl) {
            this.baseUrl = baseUrl;
        }
    
        @Provides
        Retrofit provideRetrofit(OkHttpClient client) {
            return new Retrofit.Builder()
                    .baseUrl(baseUrl)
                    .client(client)
                    .build();
        }
    }
    
  • Component 中动态传递参数:
    AppComponent appComponent = DaggerAppComponent.builder()
            .networkModule(new NetworkModule("https://api.example.com"))
            .build();
    

4. 完整示例

4.1 Application 类
public class MyApplication extends Application {
    private AppComponent appComponent;

    @Override
    public void onCreate() {
        super.onCreate();
        appComponent = DaggerAppComponent.builder()
                .application(this)
                .networkModule(new NetworkModule("https://api.example.com"))
                .build();
    }

    public AppComponent getAppComponent() {
        return appComponent;
    }
}
4.2 AppComponent
@Component(modules = {AppModule.class, NetworkModule.class})
@Singleton
public interface AppComponent {
    void inject(MainActivity activity);

    @Component.Builder
    interface Builder {
        @BindsInstance
        Builder application(Application application);
        Builder networkModule(NetworkModule networkModule);
        AppComponent build();
    }
}
4.3 NetworkModule
@Module
public class NetworkModule {
    private final String baseUrl;

    public NetworkModule(String baseUrl) {
        this.baseUrl = baseUrl;
    }

    @Provides
    Retrofit provideRetrofit(OkHttpClient client) {
        return new Retrofit.Builder()
                .baseUrl(baseUrl)
                .client(client)
                .build();
    }
}
4.4 MainActivity
public class MainActivity extends AppCompatActivity {
    @Inject
    Retrofit retrofit;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        AndroidInjection.inject(this);
        super.onCreate(savedInstanceState);
        // 使用 retrofit
    }
}

写在后面

写完这篇文章的感受是:
1.大模型极大的缩短了打字和排版的时间
2.大模型比我自己写,用词更加简洁、精准,内容更加全面

If you like this article, it is written by Johnny Deng.
If not, I don’t know who wrote it.

相关文章:

  • c++ - 笔记
  • C/C++结构体简单介绍
  • 深度学习:从零开始的DeepSeek-R1-Distill有监督微调训练实战(SFT)
  • Python 中下划线 “_” 的多面性:从变量到约定
  • java agent 学习
  • 阿里云平台服务器操作以及发布静态项目
  • 模拟实现string
  • 数据表100多字段如何写mapper文件的xml
  • 蓝桥杯单片机之AT24C02(基于自己对AT24C02的学习和理解)
  • spring源码(bean的实例化)——determineCandidateConstructors篇
  • Mac 上自动安装DeepSeek-R1 1.5B
  • DEFI币生态重构加速,XBIT去中心化交易所引领DEX安全新范式
  • springboot操作redis集群,注意事项
  • 如何写一个网关的系统
  • 网络安全漏洞的种类分为哪些?
  • chrome.webRequest API 和 Performance API
  • Java多线程与高并发专题——ThreadLocal 适合用在哪些实际生产的场景中?
  • JavaScript 导出功能全解析:从数据到文件
  • 算法刷题记录——专题目录汇总
  • 【css酷炫效果】纯CSS实现球形阴影效果
  • 西安建设市场诚信信息平台网站/怎么做品牌推广和宣传
  • 网站中的游戏是怎么做的/流量查询网站
  • 骨干专业群建设任务书网站/软文推广渠道
  • 温州网站建设服务电子商务网络公司/百度 seo 工具
  • 网站开发设计图片/免费有效的推广网站
  • seo基础理论/汕头seo不错