热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

我的Dagger2学习历程:从一头雾水到恍然大悟

前言关于Dagger2的教程在网上已经有很多很多了,对于使用和原理都讲得比较明白,但是对于第一次接触的人们来说(比如我),难免会看得一头雾水,所以这里我就记录一下我学习Dagger

前言

关于Dagger2的教程在网上已经有很多很多了,对于使用和原理都讲得比较明白,但是对于第一次接触的人们来说(比如我),难免会看得一头雾水,所以这里我就记录一下我学习Dagger2的过程,分享最快速的学习方法给大家。

介绍

Dagger2是一个依赖注入的框架,什么是依赖注入?简单的来说就是类中依赖的对象只要声明就可以使用了,它的创建由框架来管理。如下代码所示,application直接就可以拿来用了。

public class LoginActivity extends Activity{
@Inject
Application application;
}

开始

刚开始接触Dagger2的时候大量阅读了网上的教程,主要是一些概念性的东西,篇幅长了一下就看晕了,所以这里推荐大家直接看代码。把代码运行起来,结合文章和代码一起看,相信你很快就能上手了。
gitHub地址

在主项目的build.gradle中添加

classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'

在Module的build.gradle中添加

compile 'com.google.dagger:dagger:2.6'
apt 'com.google.dagger:dagger-compiler:2.6'

@Component

首先这个先定义一个全局的AppComponent,为什么需要全局的AppComponent呢?因为这里放得都是一些公共的对象,它们的生命周期是和Application一致的。通常情况下一个项目定义一个AppComponent,其他每个Activity或Fragment会对应一个Component。

@Singleton
@Component(modules = {AppModule.class, HttpModule.class, ApiServiceModule.class, DBModule.class})
public interface AppComponent { Application application(); Gson gson();
//网络访问Service
ServiceManager serviceManager();
//数据库访问
DBManager DBManager();
}

AppComponent是一个接口,里面定义了提供依赖的方法声明,这个AppComponent提供了Application、Gson、ServiceManager、DBManager依赖(方法名没有限制),dagger框架会根据对象类型把它们注入到需要使用它们的地方。
那这些提供的对象是从哪里来的呢?总不能凭空产生吧,这就要看module了,AppComponent中引用的AppModule、HttpModule、ApiServiceModule、DBModule,现在我们进入AppModule中看看。

@Module & @Provides

@Module
public class AppModule { public AppModule(HuiApplication application) {
this.mApplication = application;
}
private Application mApplication;
@Singleton
@Provides
public Application provideApplication() {
return mApplication;
}
@Singleton
@Provides
public Gson provideGson() {
return new Gson();
}
}

可以看到Module就是提供这些依赖的地方,dagger会根据@Provides标记的方法返回依赖对象,这个AppModule中提供了Application和Gson对象的创建。可能大家都注意到@Singleton这个注解了吧,如果需要让依赖对象是单例的话标注一下就可以,后面还会提到。

如果你的provide方法里需要用到其他provide提供的对象,可以直接通过方法参数传进来,如下所示,provideRetrofit()方法中需要用到OkHttpClient和HttpUrl,直接传进来就可以了。

@Module
public class HttpModule {
@Singleton
@Provides
Retrofit provideRetrofit(OkHttpClient client, HttpUrl baseUrl) {
return new Retrofit.Builder()
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.baseUrl(baseUrl)
.build();
}
@Singleton
@Provides
OkHttpClient provideOkHttpClient() {
OkHttpClient.Builder builder = new OkHttpClient.Builder();
builder.connectTimeout(DEFAULT_CONNECT_TIMEOUT, TimeUnit.SECONDS)
.readTimeout(DEFAULT_READ_TIMEOUT, TimeUnit.SECONDS)
.writeTimeout(DEFAULT_WRITE_TIMEOUT, TimeUnit.SECONDS);
return builder.build();
}
}

AppComponent

现在需要创建AppComponent,因为这是全局的Component,自然是在Application中创建了:

public class HuiApplication extends Application{
private AppComponent mAppComponent;
@Override
public void onCreate() {
super.onCreate();
mAppCompOnent= DaggerAppComponent
.builder()
.appModule(new AppModule(this))
.apiServiceModule(new ApiServiceModule())
.dBModule(new DBModule())
.httpModule(new HttpModule())
.build();
}
public AppComponent getAppComponent() {
return mAppComponent;
}
}

bulid一下项目,dagger2会为每个component创建Dagger+Component名的类,该类中会提供创建和设置每一个module实例的方法,可以看到这里的module是我们自己new了传进去的,我们可以为各个module做一些初始化的处理。这里为AppComponent创建一个get方法,接下来就要到Activity了。上面说了通常情况下每个Activity会对应一个Component,那现在就为LoginActivity创建一个LoginComponent:

@dependencies

@ActivityScope
@Component(dependencies = AppComponent.class)
public interface LoginComponent {
void inject(LoginActivity activity);
}

首先看到@dependencies,这里就把AppComponent中提供的一些对象依赖了过来,实现了全局共用。同时声明一个inject方法,参数是你要注入到的类(方法名词不限,这里用inject比较形象)。现在就看看LoginActivity是如何注入的:

@Inject

public class LoginActivity extends Activity {
@Inject
ServiceManager serviceManager;
@Inject
DBManager DBManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
HuiApplication mApplication = (HuiApplication) getApplication();
setupActivityComponent(mApplication.getAppComponent());
initData();
}
@Override
protected void setupActivityComponent(AppComponent appComponent) {
DaggerLoginComponent
.builder()
.appComponent(appComponent)
.build()
.inject(this);
}
}

同样定义了LoginComponent后会自动生成DaggerLoginComponent,这里从Application中获取之前创建的AppComponent,最后调用inject,注入就完成了。此时使用@Inject来标记成员变量就可以使用了,有没有感觉很神奇?此时你可能会觉得疑惑,接下来就开始对上面使用的一些注解和方法进行讲解。

概念

dagger2是什么?

Dagger2是一款基于Java注解来实现的完全在编译阶段完成依赖注入的开源库,主要用于模块间解耦、提高代码的健壮性和可维护性。Dagger2在编译阶段通过apt利用Java注解自动生成Java代码,然后结合手写的代码来自动帮我们完成依赖注入的工作。

下图能很好地展示Dagger2这几个注解的作用:

《我的Dagger2学习历程:从一头雾水到恍然大悟》

注解介绍

@Component
用于标注接口,是依赖需求方和依赖提供方之间的桥梁。被Component标注的接口在编译时会生成该接口的实现类(Dagger+Component名字),我们通过调用这个实现类的方法完成注入;Component接口中主要定义一些提供依赖的声明

@Inject有三个作用
一是用来标记需要依赖的变量,以此告诉Dagger2为它提供依赖;
二是用来标记构造函数,Dagger2通过@Inject注解可以在需要这个类实例的时候来找到这个构造函数并把相关实例构造出来,以此来为被@Inject标记了的变量提供依赖,例如:

public class Student {
private String name;
private int age;
@Inject
public Student() {

}
}

三是用来标记普通方法,该方法会在对象注入完成之后调用,可以根据这一特性因此做一些初始化的工作。

@Module
@Module用于标注提供依赖的类。你可能会有点困惑,上面不是提到用@Inject标记构造函数就可以提供依赖了么,为什么还需要@Module?很多时候我们需要提供依赖的构造函数是第三方库的,我们没法给它加上@Inject注解,又比如说提供以来的构造函数是带参数的,如果我们之所简单的使用@Inject标记它,那么他的参数又怎么来呢?@Module正是帮我们解决这些问题的。

@Provides
@Provides用于标注Module所标注的类中的方法,该方法在需要提供依赖时被调用,从而把预先提供好的对象当做依赖给标注了@Inject的变量赋值;

@Scope
@Scope同样用于自定义注解,我能可以通过@Scope自定义的注解来限定注解作用域,实现局部的单例;比如我们前面使用到的@ActivityScope:

@Scope
@Retention(RUNTIME)
public @interface ActivityScope {}

如果需要提供局部单例支持,则需要在Component中和@provides注解的方法上@ActivityScope,这里说的局部单例的意思是在该Component中是唯一的,如果Component是全局唯一的话就是全局单例了,比如AppComponent。

@Singleton
@Singleton其实就是一个通过@Scope定义的注解,我们一般通过它来标记全局单例(AppComponent)。

我们提到@Inject和@Module都可以提供依赖,那如果我们即在构造函数上通过标记@Inject提供依赖,有通过@Module提供依赖Dagger2会如何选择呢?具体规则如下:
步骤1:首先查找@Module标注的类中是否存在提供依赖的方法。
步骤2:若存在提供依赖的方法,查看该方法是否存在参数。
a:若存在参数,则按从步骤1开始依次初始化每个参数;
b:若不存在,则直接初始化该类实例,完成一次依赖注入。
步骤3:若不存在提供依赖的方法,则查找@Inject标注的构造函数,看构造函数是否存在参数。
a:若存在参数,则从步骤1开始依次初始化每一个参数
b:若不存在,则直接初始化该类实例,完成一次依赖注入。

Dagger2遇上MVP

如果你的项目是采用MVP架构的,那么结合Dagger2将会是一件非常棒的体验,它让M-V-P进一步解藕,架构更清晰。
在上面的LoginActivity基础上实现MVP模式。

LoginContract

MVP接口契约类,定义view和model的接口

public interface LoginContract {
interface View extends BaseView {
/**
* 登录成功
* @param result
*/
void loginSuccess(String result);
}
interface Model extends IModel {
/**
* 登录
* @param mobile
* @param password
*/
Observable login(String mobile, String password);
}
}

LoginModule

定义Module,只要提供view和model的依赖,可以看到LoginModel是通过方法参数注入进来的,这样model和view就解耦了。

@Module
public class LoginModule {
private LoginContract.View view;
public LoginModule(LoginContract.View view) {
this.view = view;
}
@ActivityScope
@Provides
LoginContract.View provideLoginView() {
return this.view;
}
@ActivityScope
@Provides
LoginContract.Model provideLoginModel(LoginModel model) {
return model;
}
}

LoginComponent

在之前的loginComponent中添加LoginModule

@ActivityScope
@Component(modules = LoginModule.class, dependencies = AppComponent.class)
public interface LoginComponent {
void inject(LoginActivity activity);
}

LoginActivity

在LoginActivity中初始化LoginComponent,我们就从这里开始看看MVP的依赖是怎么行程的:

public class LoginActivity extends BaseActivity implements LoginContract.View {
@Override
protected void setupActivityComponent(AppComponent appComponent) {
DaggerLoginComponent
.builder()
.appComponent(appComponent)
.loginModule(new LoginModule(this))
.build()
.inject(this);
}
}

我们的view就是当前的Activity,所以new LoginModule(this)这里就提供了view的依赖。
这里定义了BaseActivity并使用了泛型,设置成当前界面的presenter,这里可以知道注入的过程是在BaesActivity中完成的,现在看看BaseActivity:

public abstract class BaseActivity

extends AppCompatActivity {
....省略代码 @Inject
protected P mPresenter; ....省略代码
}

BaseActivity中其实做的操作很简单,通过@Inject注解将对应的Presenter注入进来。这样在LoginActivity中就可以使用该Presenter了,现在我们看看LoginPresenter的实现。

LoginPresenter

@ActivityScope
public class LoginPresenter extends BasePresenter {
@Inject
public LoginPresenter() {
}}

Presenter中和Actvity的做法基本类似,对象的注入还是放在父类里面,通过泛型的方式确定类型。这里可以看到这里有一个@Inject标注的空构造方法,这个是必须的,为了就是在LoginActivity中可以依赖到该Presenter。

public class BasePresenter implements IPresenter {
@Inject
protected M mModel;
@Inject
protected V mView;
}

BasePresenter里面就是View和Model的注入。
model的实现和presenter的是原理是一样的,这里就不一一述说了,这样MVP的架构就简历起来了:

  • 在View(Activity)中注入Presenter;
  • 在Presenter中注入View 和 Model
  • 在Model中注入其他一些数据处理的对象(数据库实例和网络请求实例)

Dagger2生成的代码解析

到这里,你是不是觉得为什么Dagger2会如此神奇?我们这里就对生成的代码DaggerAppComponent来进行解析,看看它是怎么实现依赖注入的。

DaggerAppComponent
.builder()
.appModule(new AppModule(this))
.apiServiceModule(new ApiServiceModule())
.dBModule(new DBModule())
.httpModule(new HttpModule())
.build();

那就从build方法开始:

public AppComponent build() {
if (appModule == null) {
throw new IllegalStateException(AppModule.class.getCanonicalName() + " must be set");
}
if (httpModule == null) {
this.httpModule = new HttpModule();
}
if (apiServiceModule == null) {
this.apiServiceModule = new ApiServiceModule();
}
if (dBModule == null) {
this.dBModule = new DBModule();
}
return new DaggerAppComponent(this);
}

build方法中会对我们传入的module进行NULL检查,可以看出来,如果我们的model的构造函数是无参的话,可以不用设置,dagger2会帮我们初始化,接着看new DaggerAppComponent():

private DaggerAppComponent(Builder builder) {
assert builder != null;
initialize(builder);
}
@SuppressWarnings("unchecked")
private void initialize(final Builder builder) {
this.provideApplicatiOnProvider=
DoubleCheck.provider(AppModule_ProvideApplicationFactory.create(builder.appModule));
this.provideOkHttpClientProvider =
DoubleCheck.provider(HttpModule_ProvideOkHttpClientFactory.create(builder.httpModule));
this.provideBaseUrlProvider =
DoubleCheck.provider(
ApiServiceModule_ProvideBaseUrlFactory.create(builder.apiServiceModule));
this.provideRetrofitProvider =
DoubleCheck.provider(
HttpModule_ProvideRetrofitFactory.create(
builder.httpModule, provideOkHttpClientProvider, provideBaseUrlProvider));
this.provideUserServiceProvider =
DoubleCheck.provider(
ApiServiceModule_ProvideUserServiceFactory.create(
builder.apiServiceModule, provideRetrofitProvider));
this.serviceManagerProvider =
DoubleCheck.provider(ServiceManager_Factory.create(provideUserServiceProvider));
this.provideCommOnSQLiteHelperProvider=
DoubleCheck.provider(
DBModule_ProvideCommonSQLiteHelperFactory.create(
builder.dBModule, provideApplicationProvider));
this.provideUserInfoDaoProvider =
DoubleCheck.provider(
DBModule_ProvideUserInfoDaoFactory.create(
builder.dBModule, provideCommonSQLiteHelperProvider));
this.dBManagerMembersInjector = DBManager_MembersInjector.create(provideUserInfoDaoProvider);
this.dBManagerProvider =
DoubleCheck.provider(
DBManager_Factory.create(dBManagerMembersInjector, provideApplicationProvider));
this.provideGsOnProvider=
DoubleCheck.provider(AppModule_ProvideGsonFactory.create(builder.appModule));
}

在构造方法里面调用了initialize(builder),这里面对所有Provider进行初始,这个方法也是完成注入的方法,这里涉及到两个对象:

  • Provider & Factory
    其实就是一个包装类,里面提供了get方法返回对应的包装对象,比如Provider,get方法就返回它持有的Application对象。这些Provider就是我们所有提供依赖的对象(包括在@module类中使用@Provides注解标注的对象,或者@Inject标记构造方法的对象)
  • XX_MembersInjector
    顾名思义,这个是对象注入器,哪些使用了@Inject注解的类就会生成对应的MembersInjector,通过调用其injectMembers()方法实现对象注入。

DoubleCheck.provider()有什么作用呢,它是实现局部单例的,返回一个实现单例的Provider。

这里我们看下LoginActivity_MembersInjector 是怎么注入LoginPresenter的:

public final class LoginActivity_MembersInjector implements MembersInjector {
private final Provider mPresenterProvider;
public LoginActivity_MembersInjector(Provider mPresenterProvider) {
assert mPresenterProvider != null;
this.mPresenterProvider = mPresenterProvider;
}
public static MembersInjector create(Provider mPresenterProvider) {
return new LoginActivity_MembersInjector(mPresenterProvider);
}
@Override
public void injectMembers(LoginActivity instance) {
if (instance == null) {
throw new NullPointerException("Cannot inject members into a null reference");
}
cn.xdeveloper.dagger2.base.mvp.BaseActivity_MembersInjector.injectMPresenter(
instance, mPresenterProvider);
}
}

在injectMembers方法中调用了父类的injectMembers方法,因为我们把注入的过程抽取到父类了,再看看父类的injectMembers方法:

public static

void injectMPresenter(
BaseActivity

instance, Provider

mPresenterProvider) {
instance.mPresenter = mPresenterProvider.get();
}

通过简单的对象赋值就完成了注入,那调用LoginActivity_MembersInjector的injectMembers方法的地方是哪里呢?

@Override
public void inject(LoginActivity activity) {
loginActivityMembersInjector.injectMembers(activity);
}

没错这个方法就是我们在LoginComponent中定义的inject接口的实现方法。
这里你可能会觉得奇怪,为什么我定义了inject方法名就会生成injectMembers的实现呢?我们再看看之前定义的LoginComponent的代码:

@ActivityScope
@Component(modules = LoginModule.class, dependencies = AppComponent.class)
public interface LoginComponent {
void inject(LoginActivity activity);
}

这里的方法申明是有讲究的:

  • 如果参数有值的,代表这个方法是注入方法,注入的类就是该参数类,框架会为其创建injectMembers的实现,这个时候是不允许有返回值的,这里方法名是可以随意填写,叫inject比较形象;
  • 如果参数是没有值的时候,则代表该component提供了依赖,返回类型就是该依赖对象,比如之前的AppComponet中定义的:

@Singleton
@Component(modules = {AppModule.class, HttpModule.class, ApiServiceModule.class, DBModule.class})
public interface AppComponent {
Application application();
ServiceManager serviceManager();
DBManager DBManager();
Gson gson();
}

那么这些定义的方法有什么用呢?最简单的方法就是看代码中哪里引用了它们就知道了:

this.applicatiOnProvider=
new Factory() {
private final AppComponent appCompOnent= builder.appComponent;
@Override
public Application get() {
return Preconditions.checkNotNull(
appComponent.application(),
"Cannot return null from a non-@Nullable component method");
}
};

这里是DaggerLoginActivityComponent的初始化方法里,因为我们的LoginActivityComponent是依赖AppComponnet的,那要怎么引用这些AppComponent中已经初始化好的对象呢?则是通过上面定义的这些接口方法来访问的。

后续问题

因为Dagger2是在编译阶段完成依赖注入,没有了反射带来的效率问题,但同时就会缺乏了灵活性。
我在重构项目的时候就遇到了这么一个问题,由于我的项目是多数据库的,一个用户对应一个数据库,这样在依赖SQLDataBaseHelper的时候就无从下手了,因为这个在编译期间是无法知道用户信息的,思前想后终于想到了一个办法:
我们可以在AppComponent中管理一个叫DBManager的对象,在DBManager里面含有各种Dao对象,但是这些Dao的创建是由我们自己去创建而不是靠dagger2注入的,这样的话我们就可以在其他需要使用数据库的地方@Inject DBManager就可以了,附上代码仅供参考:

@Singleton
public class DBManager {
private Application application;
private ContactDao contactDao;
@Inject
public DBManager(Application application) {
this.application = application;
}
public ContactDao getContactDao(Long userId) {
if (cOntactDao== null) {
synchronized (DBManager.class) {
if (cOntactDao== null)
cOntactDao= new ContactDao(PrivateDBHelper.getInstance(application, userId));
}
}
return contactDao;
}
}

结尾

以上就是全部我对dagger2的了解,最初我也是从一头雾水,现在终算恍然大悟,还是那句老话:代码是最好的老师。对于了解dagger2的注入原理的话多看生成的代码,逻辑还是挺清晰的。
最后,希望大家能早日拥抱dagger2。

代码地址 GitHub
QQ:318531018


推荐阅读
  • 1.在gradle中添加依赖在主项目的build.gradle中添加Dagger2库的依赖dependencies{compilecom.google.dagger:dagger: ... [详细]
  • Android Studio Bumblebee | 2021.1.1(大黄蜂版本使用介绍)
    本文介绍了Android Studio Bumblebee | 2021.1.1(大黄蜂版本)的使用方法和相关知识,包括Gradle的介绍、设备管理器的配置、无线调试、新版本问题等内容。同时还提供了更新版本的下载地址和启动页面截图。 ... [详细]
  • android 自定义模板下载,android studio 自定义模板
    由于项目用上了mvp架构,基本上一个页面就至少需要新创建6个类,分别是modelviewpresenter的接口以及其对应的实现类,再加上使用dagger的话就要更多了,所以这时候 ... [详细]
  • Uberlicenseforandroidlist:1.ButterKnife:项目地址:https:github.comJakeWhartonbutterknife这个开源库可以 ... [详细]
  • 本文介绍了如何使用PHP向系统日历中添加事件的方法,通过使用PHP技术可以实现自动添加事件的功能,从而实现全局通知系统和迅速记录工具的自动化。同时还提到了系统exchange自带的日历具有同步感的特点,以及使用web技术实现自动添加事件的优势。 ... [详细]
  • CF:3D City Model(小思维)问题解析和代码实现
    本文通过解析CF:3D City Model问题,介绍了问题的背景和要求,并给出了相应的代码实现。该问题涉及到在一个矩形的网格上建造城市的情景,每个网格单元可以作为建筑的基础,建筑由多个立方体叠加而成。文章详细讲解了问题的解决思路,并给出了相应的代码实现供读者参考。 ... [详细]
  • 基于移动平台的会展导游系统APP设计与实现的技术介绍与需求分析
    本文介绍了基于移动平台的会展导游系统APP的设计与实现过程。首先,对会展经济和移动互联网的概念进行了简要介绍,并阐述了将会展引入移动互联网的意义。接着,对基础技术进行了介绍,包括百度云开发环境、安卓系统和近场通讯技术。然后,进行了用户需求分析和系统需求分析,并提出了系统界面运行流畅和第三方授权等需求。最后,对系统的概要设计进行了详细阐述,包括系统前端设计和交互与原型设计。本文对基于移动平台的会展导游系统APP的设计与实现提供了技术支持和需求分析。 ... [详细]
  • zuul 路由不生效_Zuul网关到底有何牛逼之处?竟然这么多人在用~
    作者:kosamino来源:cnblogs.comjing99p11696192.html哈喽,各位新来的小伙伴们,大家好& ... [详细]
  • 开发笔记:Dagger2 探索记3——两大进阶组件
        今天要讲的时@Scope这个组件。为什么说它是进阶组件,就是因为它基本上没作用,但在理解了基本组件之后又必须用到。 ... [详细]
  • dagger2简单使用与理解笔记
    文章目录使用dagger2好处具体案例查看github1.使用dagger2注入基本使用流程概念2.dagger2中各种注解基本使用引入dagger20.写两个对象用来实际操作的1 ... [详细]
  • 关键词:Golang, Cookie, 跟踪位置, net/http/cookiejar, package main, golang.org/x/net/publicsuffix, io/ioutil, log, net/http, net/http/cookiejar ... [详细]
  • 利用Visual Basic开发SAP接口程序初探的方法与原理
    本文介绍了利用Visual Basic开发SAP接口程序的方法与原理,以及SAP R/3系统的特点和二次开发平台ABAP的使用。通过程序接口自动读取SAP R/3的数据表或视图,在外部进行处理和利用水晶报表等工具生成符合中国人习惯的报表样式。具体介绍了RFC调用的原理和模型,并强调本文主要不讨论SAP R/3函数的开发,而是针对使用SAP的公司的非ABAP开发人员提供了初步的接口程序开发指导。 ... [详细]
  • 树莓派语音控制的配置方法和步骤
    本文介绍了在树莓派上实现语音控制的配置方法和步骤。首先感谢博主Eoman的帮助,文章参考了他的内容。树莓派的配置需要通过sudo raspi-config进行,然后使用Eoman的控制方法,即安装wiringPi库并编写控制引脚的脚本。具体的安装步骤和脚本编写方法在文章中详细介绍。 ... [详细]
  • 本文介绍了互联网思维中的三个段子,涵盖了餐饮行业、淘品牌和创业企业的案例。通过这些案例,探讨了互联网思维的九大分类和十九条法则。其中包括雕爷牛腩餐厅的成功经验,三只松鼠淘品牌的包装策略以及一家创业企业的销售额增长情况。这些案例展示了互联网思维在不同领域的应用和成功之道。 ... [详细]
  • 高级工程师_免费Android高级工程师学习资源,内容太过真实
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了免费Android高级工程师学习资源,内容太过真实相关的知识,希望对你有一定的参考价值。 ... [详细]
author-avatar
万幼南
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有