[TOC]
# 概述
* Retrofit 对网络请求接口进行了封装,实际执行网络请求的依然是 OkHttp
* Retrofit 接口层实际是对 OkHttp 中 Request 的封装,采用注解的形式来描述网络请求参数
* Retrofit采用动态代理模式创建请求接口对象,请求执行调用接口方法时,Retrofit会根据注解创建相应的Call对象,接下来使用OkHttp发起请求
# Retrofit 使用
在学习 Retrofit 使用之前我们先回顾下 OKHttp 的工作方式,一个标准的 OkHttp 请求格式为 `mClient.newCall(request).enqueue(callback);`。可以看到,首先构造出 Request 对象(经过封装的 http 请求信息)和 OkHttpClient 对象,然后调用 OkHttpRequest 的 newCall 方法,传入封装的 request 对象后,就得到了一个 RealCall 对象,然后就可以操作 RealCall 对象执行请求、取消等一系列操作。
而 Retrofit 对于 http 请求信息并不是直接构造成 Request 对象,而是声明为接口的形式,相关请求信息(地址、方法、请求头等)使用注解和参数的形式传入。同样会有一个 Retrofit 客户端对象,调用其 create 方法会帮我们创建出请求接口的实例对象,接着调用请求接口实例对象的方法发起请求。流程如下:
* 创建请求接口
* 构建 Retrofit 实例
* 创建网络请求接口实例
* 发起网络请求
可以看到,Retrofit 对于 OkHttp 中的 Request 进行了抽象化为接口,在使用时首先构造出请求对象。一个标准的 Retrofit 请求格式为 `mRetrofit.create(RequestService.class).doGet("params").enqueue(callback)`,其中 doGet 方法返回的是一个 Call 对象。
# Retrofit 注解使用
来源:[Retrofit2.0中注解使用套路](https://blog.csdn.net/stven_king/article/details/52372172)、[Retrofit网络请求参数注解,@Path、@Query、@QueryMap](https://www.jianshu.com/p/7687365aa946)
## 静态 url 请求
### GET 请求
```java
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.github.com/")
.build();
public interface GitHubService {
//无参数
// https://api.github.com/users/xcy396/repos
@GET("users/xcy396/repos")
Call<List<Repo>> listRepos();
//少数参数
//https://api.github.com/users/xcy396/repos?time={time}
@GET("users/xcy396/repos")
Call<List<Repo>> listRepos(@Query("time") long time);
//参数较多
//https://api.github.com/users/xcy396/repos?time={time}&author={author}...
@GET("users/xcy396/repos")
Call<List<Repo>> listRepos(@QueryMap Map<String, String> params);
}
```
### POST 请求
```java
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.github.com/")
.build();
public interface GitHubService {
//无参数
@POST("users/xcy396/repos")
Call<List<Repo>> listRepos();
//少数参数(参数拼接在url后面)
//https://api.github.com/users/xcy396/repos?time={time}
@POST("users/xcy396/repos")
Call<List<Repo>> listRepos(@Query("time") long time);
//少数参数(使用表单形式提交,参数在请求体)
@FormUrlEncoded
@POST("users/xcy396/repos")
Call<List<Repo>> listRepos(@Field("time") long time);
//参数较多(使用表单形式提交,参数在请求体)
@FormUrlEncoded
@POST("users/xcy396/repos")
Call<List<Repo>> listRepos(@FieldMap Map<String, String> params);
//参数较多(使用Plain形式提交,参数在请求体)
@POST("users/xcy396/repos")
Call<List<Repo>> listRepos(@Body Map<String, String> params);
}
```
### DELETE请求
```java
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.github.com/")
.build();
public interface GitHubService {
//无参数
// https://api.github.com/users/xcy396/repos
@DELETE("users/xcy396/repos")
Call<List<Repo>> listRepos();
//少数参数(参数拼接在url后面)
//https://api.github.com/users/xcy396/repos?time={time}
@DELETE("users/xcy396/repos")
Call<List<Repo>> listRepos(@Query("time") long time);
//参数较多(使用Plain形式提交,参数在请求体)
//https://api.github.com/users/xcy396/repos
@HTTP(method = "DELETE", path = "users/xcy396/repos", hasBody = true)
Call<List<Repo>> listRepos(@Body Map<String, String> params);
}
```
注意:
* 当@GET或@POST注解的url为全路径时(可能和baseUrl不是一个域),会直接使用注解的url的域
* 如果请求为 Post 实现,那么最好传递参数时使用@Field、@FieldMap和@FormUrlEncoded。因为@Query和@QueryMap都是将参数拼接在url后面的,而@Field或@FieldMap传递的参数时放在请求体的
* 使用@Path时,path对应的路径不能包含”/”,否则会将其转化为%2F。在遇到想动态的拼接多节url时,还是使用@Url
## 半静态 url 请求
https://api.github.com/users/{user}/repos
```java
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.github.com/")
.build()
public interface GitHubService {
@GET("users/{user}/repos")
Call<List<Repo>> listRepos(@Path("user") String user);
}
```
## 动态 url 请求
```java
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.github.com/")
.build()
public interface GitHubService {
@GET
Call<List<Repo>> listRepos(@Url String user);
}
```
# 注解说明
## 网络请求方法
网络请求方法注解对应于 Http 请求方式,包括:
注解名称|说明
---|---
@GET|从服务器取出资源(一项或多项)
@POST|在服务器新建一个资源
@PUT|在服务器更新资源(客户端提供改变后的完整资源)
@DELETE|从服务器删除资源
@HEAD|获取资源的元数据
@OPTIONS|获取信息,关于资源的哪些属性是客户端可以改变的
## 标记类
标记类注解,作用于网络请求接口的方法,包括:
注解名称|说明
---|---
@FormUrlEncoded|表示请求体是一个 Form 表单
@Multipart|表示请求体是一个 Multipart 表单
@Streaming|表示返回的数据以流的形式返回
### 提交表单
代码示例:
```java
public interface Add2GankApi {
@POST("add2gank")
@FormUrlEncoded
Call<ResponseBody> add(@Field("url") String url, @Field("desc") String desc, @Field("who") String who,
@Field("type") String type, @Field("debug") boolean debug);
}
```
### 提交 Multipart
代码示例:
```java
public interface Add2GankApi {
@POST("add2gank")
@Multipart
Call<ResponseBody> addFile(@Part("name") RequestBody name, @Part("age") RequestBody age,
@Part MultipartBody.Part file);
}
```
其中 @Part 后面括号内的字符用于生成每个 Part 请求头 Content-Disposition 字段的信息,接收的参数则用于生成请求体。生成的 Content-Disposition 字段示例如下:
```plain
Content-Disposition: form-data; name=”name”
或
Content-Disposition: form-data; name=”test”; filename=”test.txt”
```
调用:
```java
RequestBody name = RequestBody.create(mediaType, "Tom");
RequestBody age = RequestBody.create(mediaType, "12");
MultipartBody.Part file= MultipartBody.Part.createFormData("test", "test.txt",
RequestBody.create(mediaType, new File("test.txt")));
RetrofitUtil.getInstance()
.createAdd2GankApiRequest()
.addFile(name, age, file);
```
## 网络请求参数注解
网络请求参数注解包括:
注解名称|说明
---|---
@Headers|添加固定的请求头
@Header|添加不固定值的 Header
示例:
```java
@GET("user")
Call<ResponseBody> getUser(@Header("Authorization") String authorization);
@GET("user")
@Headers("Authorization: authorization")
Call<ResponseBody> getUser();
```
注解名称|说明
---|---
@Body|发送自定义数据类型(非表单数据)给服务器
注解名称|说明
---|---
@Field|表单字段
@FieldMap|表单字段
示例:
```java
@POST("add2gank")
@FormUrlEncoded
Call<ResponseBody> add(@Field("username") String name, @Field("age") int age);
@POST("add2gank")
@FormUrlEncoded
Call<ResponseBody> add(@FieldMap Map<String, Object> map);
```
使用:
```java
RetrofitUtil.getInstance()
.createAdd2GankApiRequest()
.add("Tom", 18);
Map<String, Object> map = new HashMap<>();
map.put("username", "Tom");
map.put("age", 18);
RetrofitUtil.getInstance()
.createAdd2GankApiRequest()
.add(map);
```
注解名称|说明
---|---
@Part|Multipart 表单字段
@PartMap|Multipart 表单字段
示例:
```java
public interface Add2GankApi {
@POST("add2gank")
@Multipart
Call<ResponseBody> addFile(@Part("name") RequestBody name, @Part("age") RequestBody age,
@Part MultipartBody.Part file);
@POST("add2gank")
@Multipart
Call<ResponseBody> addFile(@PartMap Map<String, RequestBody> args, @Part MultipartBody.Part file);
}
```
使用
```java
// Part 使用
RequestBody name = RequestBody.create(mediaType, "Tom");
RequestBody age = RequestBody.create(mediaType, "12");
MultipartBody.Part file= MultipartBody.Part.createFormData("test", "test.txt",
RequestBody.create(mediaType, new File("test.txt")));
RetrofitUtil.getInstance()
.createAdd2GankApiRequest()
.addFile(name, age, file);
// PartMap 使用
Map<String, RequestBody> map = new HashMap<>();
map.put("name", name);
map.put("age", age);
RetrofitUtil.getInstance()
.createAdd2GankApiRequest()
.addFile(map, file);
```
注解名称|说明
---|---
@Query|作用于 GET 方法的查询参数,作用于 Url
@QueryMap|作用于 GET 方法的查询参数,作用于 Url
示例:
```java
@GET("user")
Call<ResponseBody> getUser(@Query("age") int age, @Query("gender") int gender);
```
在 baseUrl = "http://gank.io/api/",调用 getUser 方法传参为(18, 0)时,请求 url 为 "http://gank.io/api/?age=18&gender=0"
注解名称|说明
---|---
@Path|URL 地址的缺省值
@Url|直接设置 Url 变量
示例:
```java
@GET("users/{user}/photos")
Call<ResponseBody> getUserPhotos(@Path("user") String user);
```
当传参为 "tom" 时,请求 url 为 "http://gank.io/api/users/tom/photos"。
示例:
```java
@GET
Call<ResponseBody> getUSer(@Url String url, @Query("age") int age);
```
当有 @Url 注解时,@GET 传入的 Url 可以忽略。
# 源码分析
## 具体分析
### Retrofit 的构造
我们首先来从 Retrofit 对象的构建说起。和 OkHttpClient 一样,Retrofit 对象我们也建议项目中只维护一个。一个标准的 Retrofit 构造代码如下:
```java
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.example.com/")
.addConverterFactory(GsonConverterFactory.create())
.build();
```
可以看到 Retrofit 也使用了建造者模式,
### Request对象的封装与Call的生成
一个标准的 Retrofit 请求格式为 `mRetrofit.create(RequestService.class).doGet("params").enqueue(callback)`。
在OkHttp中,我们手动创建了Request对象,并创建Call对象。而在Retrofit中,则是首先声明请求接口(通过注解声明请求地址、方法、请求体等),Retrofit的create方法会为我们创建一个请求接口对象。先来看看create方法:
```java
public <T> T create(final Class<T> service) {
// 验证接口的合法性
Utils.validateServiceInterface(service);
if (validateEagerly) {
eagerlyValidateMethods(service);
}
return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },
new InvocationHandler() {
private final Platform platform = Platform.get();
@Override public Object invoke(Object proxy, Method method, @Nullable Object[] args)
throws Throwable {
// If the method is a method from Object then defer to normal invocation.
if (method.getDeclaringClass() == Object.class) {
return method.invoke(this, args);
}
if (platform.isDefaultMethod(method)) {
return platform.invokeDefaultMethod(method, service, proxy, args);
}
// 创建 ServiceMethod
ServiceMethod<Object, Object> serviceMethod = (ServiceMethod<Object, Object>) loadServiceMethod(method);
// 创建 OkHttpCall
OkHttpCall<Object> okHttpCall = new OkHttpCall<>(serviceMethod, args);
// 返回
return serviceMethod.adapt(okHttpCall);
}
});
}
```
Retrofit使用了动态代理模式(可参考[代理模式]([http://wiki.xuchongyang.com/1091154#2\_\_57](http://wiki.xuchongyang.com/1091154#2__57))),create方法返回了请求接口的代理对象。当我们调用代理对象的方法时,会被拦截,执行最后三行关键代码,下面我们先看看ServiceMethod类,再依次看最后三行代码。
1、ServiceMethod类就是我们定义的请求接口到Call的转换适配器。
```java
public class ServiceMethod {
Builder(Retrofit retrofit, Method method) {
this.retrofit = retrofit;
this.method = method;
this.methodAnnotations = method.getAnnotations();
this.parameterTypes = method.getGenericParameterTypes();
this.parameterAnnotationsArray = method.getParameterAnnotations();
}
public ServiceMethod build() {
//...
// 解析方法注解
for (Annotation annotation : methodAnnotations) {
parseMethodAnnotation(annotation);
}
//...
int parameterCount = parameterAnnotationsArray.length;
parameterHandlers = new ParameterHandler<?>[parameterCount];
for (int p = 0; p < parameterCount; p++) {
Type parameterType = parameterTypes[p];
if (Utils.hasUnresolvableType(parameterType)) {
throw parameterError(p, "Parameter type must not include a type variable or wildcard: %s",
parameterType);
}
Annotation[] parameterAnnotations = parameterAnnotationsArray[p];
if (parameterAnnotations == null) {
throw parameterError(p, "No Retrofit annotation found.");
}
// 解析参数
parameterHandlers[p] = parseParameter(p, parameterType, parameterAnnotations);
}
// ...
return new ServiceMethod<>(this);
}
private void parseMethodAnnotation(Annotation annotation) {
if (annotation instanceof DELETE) {
parseHttpMethodAndPath("DELETE", ((DELETE) annotation).value(), false);
} else if (annotation instanceof GET) {
parseHttpMethodAndPath("GET", ((GET) annotation).value(), false);
} else if (annotation instanceof HEAD) {
parseHttpMethodAndPath("HEAD", ((HEAD) annotation).value(), false);
if (!Void.class.equals(responseType)) {
throw methodError("HEAD method must use Void as response type.");
}
} else if (annotation instanceof PATCH) {
parseHttpMethodAndPath("PATCH", ((PATCH) annotation).value(), true);
} else if (annotation instanceof POST) {
parseHttpMethodAndPath("POST", ((POST) annotation).value(), true);
} else if (annotation instanceof PUT) {
parseHttpMethodAndPath("PUT", ((PUT) annotation).value(), true);
} else if (annotation instanceof OPTIONS) {
parseHttpMethodAndPath("OPTIONS", ((OPTIONS) annotation).value(), false);
} else if (annotation instanceof HTTP) {
HTTP http = (HTTP) annotation;
parseHttpMethodAndPath(http.method(), http.path(), http.hasBody());
} else if (annotation instanceof retrofit2.http.Headers) {
String[] headersToParse = ((retrofit2.http.Headers) annotation).value();
if (headersToParse.length == 0) {
throw methodError("@Headers annotation is empty.");
}
headers = parseHeaders(headersToParse);
} else if (annotation instanceof Multipart) {
if (isFormEncoded) {
throw methodError("Only one encoding annotation is allowed.");
}
isMultipart = true;
} else if (annotation instanceof FormUrlEncoded) {
if (isMultipart) {
throw methodError("Only one encoding annotation is allowed.");
}
isFormEncoded = true;
}
}
}
```
从代码中可以看到,我们使用注解定义的所有信息,在构造ServiceMethod时进行了解析并存储。
2、接下来看看刚刚create方法的倒数第三行代码`ServiceMethod<Object, Object> serviceMethod = (ServiceMethod<Object, Object>) loadServiceMethod(method);`。
其中loadServiceMethod方法源码如下:
```java
ServiceMethod<?, ?> loadServiceMethod(Method method) {
ServiceMethod<?, ?> result = serviceMethodCache.get(method);
if (result != null) return result;
synchronized (serviceMethodCache) {
result = serviceMethodCache.get(method);
if (result == null) {
result = new ServiceMethod.Builder<>(this, method).build();
serviceMethodCache.put(method, result);
}
}
return result;
}
```
可以看到Retrofit对ServiceMethod实例进行了缓存,来提高请求时的解析效率。
3、看下倒数第二行代码`OkHttpCall<Object> okHttpCall = new OkHttpCall<>(serviceMethod, args);`。这行代码根据serviceMethod对象构造了一个OkHttpCall对象。
OkHttpCall类源码如下:
```java
final class OkHttpCall<T> implements Call<T> {
private final ServiceMethod<T, ?> serviceMethod;
private final @Nullable Object[] args;
OkHttpCall(ServiceMethod<T, ?> serviceMethod, @Nullable Object[] args) {
this.serviceMethod = serviceMethod;
this.args = args;
}
private okhttp3.Call createRawCall() throws IOException {
okhttp3.Call call = serviceMethod.toCall(args);
if (call == null) {
throw new NullPointerException("Call.Factory returned null.");
}
return call;
}
}
```
可以看到,OkHttpCall实现了Call接口,可以直接作为Call执行一系列操作,并且有一个createRawCall方法,会在调用request、enqueue、execute方法时进行调用。
4、得到OkHttpCall对象后,来看看最后一行代码`return serviceMethod.adapt(okHttpCall);`
来看下ServiceMethod的adapt方法:
```java
T adapt(Call<R> call) {
return callAdapter.adapt(call);
}
```
下面我们按两条线来走,一是CallAdapter是怎么来的呢,二是CallAdapter的adapt方法做了什么。
a、首先来看看CallAdapter是怎么来的呢?
```java
private CallAdapter<T, R> createCallAdapter() {
Type returnType = method.getGenericReturnType();
Annotation[] annotations = method.getAnnotations();
//...
return (CallAdapter<T, R>) retrofit.callAdapter(returnType, annotations);
}
// Retrofit类的callAdapter方法如下:
public CallAdapter<?, ?> callAdapter(Type returnType, Annotation[] annotations) {
return nextCallAdapter(null, returnType, annotations);
}
public CallAdapter<?, ?> nextCallAdapter(@Nullable CallAdapter.Factory skipPast, Type returnType,
Annotation[] annotations) {
int start = callAdapterFactories.indexOf(skipPast) + 1;
for (int i = start, count = callAdapterFactories.size(); i < count; i++) {
CallAdapter<?, ?> adapter = callAdapterFactories.get(i).get(returnType, annotations, this);
if (adapter != null) {
return adapter;
}
}
}
```
可以看到,从CallAdapterFactories中遍历查询,看是否有返回类型相匹配的CallAdpater。下面再看看CallAdapterFactories是怎么来的。
```java
public Retrofit build() {
//...
// 添加默认的CallAdapterFactory
List<CallAdapter.Factory> callAdapterFactories = new ArrayList<>(this.callAdapterFactories);
callAdapterFactories.add(platform.defaultCallAdapterFactory(callbackExecutor));
//...
}
```
在Retrofit的构造方法中,会添加平台的默认CallAdapterFactory。CallAdapterFactory有两个子类:DefaultCallAdapterFactory和ExecutorCallAdapterFactory。默认的CallAdapterFactory是ExecutorCallAdapterFactory:
```java
final class ExecutorCallAdapterFactory extends Factory {
public CallAdapter<?, ?> get(Type returnType, Annotation[] annotations, Retrofit retrofit) {
//...
return new CallAdapter<Object, Call<?>>() {
public Type responseType() {
return responseType;
}
public Call<Object> adapt(Call<Object> call) {
return new ExecutorCallAdapterFactory.ExecutorCallbackCall(ExecutorCallAdapterFactory.this.callbackExecutor, call);
}
};
}
static final class ExecutorCallbackCall<T> implements Call<T> {
final Executor callbackExecutor;
final Call<T> delegate;
ExecutorCallbackCall(Executor callbackExecutor, Call<T> delegate) {
this.callbackExecutor = callbackExecutor;
this.delegate = delegate;
}
public void enqueue(final Callback<T> callback) {
//...
}
public Response<T> execute() throws IOException {
//...
}
}
}
```
b、下面看看第二个问题,CallAdapter的adapt方法到底做了什么呢?由上面的ExecutorCallAdapterFactory可看到,adapt方法返回了一个Call接口的实现类。
c、最后,我们再多来看一下CallAdapter的声明
```java
public interface CallAdapter<R, T> {
Type responseType();
T adapt(Call<R> call);
abstract class Factory {
public abstract @Nullable CallAdapter<?, ?> get(Type returnType, Annotation[] annotations,
Retrofit retrofit);
protected static Type getParameterUpperBound(int index, ParameterizedType type) {
return Utils.getParameterUpperBound(index, type);
}
protected static Class<?> getRawType(Type type) {
return Utils.getRawType(type);
}
}
}
```
# 参考
[这是一份很详细的 Retrofit 2.0 使用教程(含实例讲解)](http://blog.csdn.net/carson_ho/article/details/73732076)
[RESTful API 设计指南](http://www.ruanyifeng.com/blog/2014/05/restful_api.html)
[Android:手把手带你 深入读懂 Retrofit 2.0 源码]([https://www.jianshu.com/p/0c055ad46b6c](https://www.jianshu.com/p/0c055ad46b6c))
- 导读
- Java知识
- Java基本程序设计结构
- 【基础知识】Java基础
- 【源码分析】Okio
- 【源码分析】深入理解i++和++i
- 【专题分析】JVM与GC
- 【面试清单】Java基本程序设计结构
- 对象与类
- 【基础知识】对象与类
- 【专题分析】Java类加载过程
- 【面试清单】对象与类
- 泛型
- 【基础知识】泛型
- 【面试清单】泛型
- 集合
- 【基础知识】集合
- 【源码分析】SparseArray
- 【面试清单】集合
- 多线程
- 【基础知识】多线程
- 【源码分析】ThreadPoolExecutor源码分析
- 【专题分析】volatile关键字
- 【面试清单】多线程
- Java新特性
- 【专题分析】Lambda表达式
- 【专题分析】注解
- 【面试清单】Java新特性
- Effective Java笔记
- Android知识
- Activity
- 【基础知识】Activity
- 【专题分析】运行时权限
- 【专题分析】使用Intent打开三方应用
- 【源码分析】Activity的工作过程
- 【面试清单】Activity
- 架构组件
- 【专题分析】MVC、MVP与MVVM
- 【专题分析】数据绑定
- 【面试清单】架构组件
- 界面
- 【专题分析】自定义View
- 【专题分析】ImageView的ScaleType属性
- 【专题分析】ConstraintLayout 使用
- 【专题分析】搞懂点九图
- 【专题分析】Adapter
- 【源码分析】LayoutInflater
- 【源码分析】ViewStub
- 【源码分析】View三大流程
- 【源码分析】触摸事件分发机制
- 【源码分析】按键事件分发机制
- 【源码分析】Android窗口机制
- 【面试清单】界面
- 动画和过渡
- 【基础知识】动画和过渡
- 【面试清单】动画和过渡
- 图片和图形
- 【专题分析】图片加载
- 【面试清单】图片和图形
- 后台任务
- 应用数据和文件
- 基于网络的内容
- 多线程与多进程
- 【基础知识】多线程与多进程
- 【源码分析】Handler
- 【源码分析】AsyncTask
- 【专题分析】Service
- 【源码分析】Parcelable
- 【专题分析】Binder
- 【源码分析】Messenger
- 【面试清单】多线程与多进程
- 应用优化
- 【专题分析】布局优化
- 【专题分析】绘制优化
- 【专题分析】内存优化
- 【专题分析】启动优化
- 【专题分析】电池优化
- 【专题分析】包大小优化
- 【面试清单】应用优化
- Android新特性
- 【专题分析】状态栏、ActionBar和导航栏
- 【专题分析】应用图标、通知栏适配
- 【专题分析】Android新版本重要变更
- 【专题分析】唯一标识符的最佳做法
- 开源库源码分析
- 【源码分析】BaseRecyclerViewAdapterHelper
- 【源码分析】ButterKnife
- 【源码分析】Dagger2
- 【源码分析】EventBus3(一)
- 【源码分析】EventBus3(二)
- 【源码分析】Glide
- 【源码分析】OkHttp
- 【源码分析】Retrofit
- 其他知识
- Flutter
- 原生开发与跨平台开发
- 整体归纳
- 状态及状态管理
- 零碎知识点
- 添加Flutter到现有应用
- Git知识
- Git命令
- .gitignore文件
- 设计模式
- 创建型模式
- 结构型模式
- 行为型模式
- RxJava
- 基础
- Linux知识
- 环境变量
- Linux命令
- ADB命令
- 算法
- 常见数据结构及实现
- 数组
- 排序算法
- 链表
- 二叉树
- 栈和队列
- 算法时间复杂度
- 常见算法思想
- 其他技术
- 正则表达式
- 编码格式
- HTTP与HTTPS
- 【面试清单】其他知识
- 开发归纳
- Android零碎问题
- 其他零碎问题
- 开发思路