Okhttp是由Sqare公司开发的开源网络访问库,是目前比较火的网络框架, 它处理了很多网络疑难杂症:会从很多常用的连接问题中自动恢复。如果你的服务器配置了多个IP地址,当第一个IP连接失败的时候,OkHttp会自动尝试下一个IP,此外OkHttp还处理了代理服务器问题和SSL握手失败问题。
首先介绍下OkHttp的简单使用,主要包含:
Eclipse的用户,下载最新的okhttp jar包,导入工程。同时okhttp内部依赖okio,所以别忘了同时导入okio jar包。
okhttp 3.4.1.jar和okio-1.10.0.jar下载地址
Studio用户,直接添加依赖包:
compile 'com.squareup.okhttp3:okhttp:3.4.1'
然后添加网络访问权限:
<uses-permission android:name="android.permission.INTERNET"/>
1.异步GET请求
最简单的GET请求
private void getAsynHttp() {
OkHttpClient mOkHttpClient = new OkHttpClient();
Request request = new Request.Builder()
.url("http://www.baidu.com")
.build();
Call call = mOkHttpClient.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
showlog(e.getMessage());
}
@Override
public void onResponse(Call call, Response response) throws IOException {
final String str = response.body().string();
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(getApplication(), str, Toast.LENGTH_SHORT).show();
}
});
}
});
}
基本的步骤很简单,就是创建OkHttpClient、Request和Call,最后调用Call的enqueue()方法。但是每次这么写肯定是很麻烦,肯定是要进行封装的。需要注意的是onResponse回调并不是在UI线程。
注:onResponse回调的参数是response,一般情况下,比如我们希望获得返回的字符串,可以通过response.body().string()获取;如果希望获得返回的二进制字节数组,则调用response.body().bytes();如果你想拿到返回的inputStream,则调用response.body().byteStream()。
2.同步GET请求
同步Get请求和异步调用区别就是调用了call的execute()方法。
private String getSyncHttp() throws IOException{
OkHttpClient mOkHttpClient = new OkHttpClient();
Request request = new Request.Builder()
.url("http://www.baidu.com")
.build();
Call call = mOkHttpClient.newCall(request);
Response mResponse=call.execute();
if (mResponse.isSuccessful()) {
return mResponse.body().string();
} else {
throw new IOException("Unexpected code " + mResponse);
}
}
注意同步GET请求的调用必须放在子线程中执行,不然会报NetworkOnMainThreadException。
3.异步POST请求
post与get不同的就是要创建RequestBody并传进Request中,同样onResponse回调不是在UI线程。
private void postAsynHttp() {
OkHttpClient mOkHttpClient = new OkHttpClient();
RequestBody formBody = new FormBody.Builder()
.add("topicId", "1002")
.add("maxReply", "-1")
.add("reqApp", "1")
.build();
Request request = new Request.Builder()
.url("http://61.129.89.191/SoarAPI/api/SoarTopic")
.post(formBody)
.build();
Call call = mOkHttpClient.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
showlog(e.getMessage());
}
@Override
public void onResponse(Call call, Response response) throws IOException {
final String str = response.body().string();
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(getApplicationContext(), str, Toast.LENGTH_SHORT).show();
}
});
}
});
}
1.异步上传文件
上传文件本身也是一个POST请求,首先定义上传文件类型:
public final MediaType MEDIA_TYPE_MARKDOWN = MediaType.parse("text/x-markdown; charset=utf-8");
将sdcard根目录的demo.txt文件上传到服务器上:
private void postAsynFile() {
OkHttpClient mOkHttpClient=new OkHttpClient();
File file = new File("/sdcard/demo.txt");
Request request = new Request.Builder()
.url("https://api.github.com/markdown/raw")
.post(RequestBody.create(MEDIA_TYPE_MARKDOWN, file))
.build();
mOkHttpClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
showlog(e.getMessage());
}
@Override
public void onResponse(Call call, Response response) throws IOException {
showlog(response.body().string());
}
});
}
当然如果想要改为同步的上传文件只要调用 mOkHttpClient.newCall(request).execute()就可以了。
在demo.txt文件中有一行字“测试OkHttp异步上传”我们运行程序点击发送文件按钮,最终请求网络返回的结果就是我们txt文件中的内容 :
当然不要忘了添加如下权限:
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
2.异步下载图片
下载图片本身也是一个GET请求
private void getAsynFile() {
OkHttpClient mOkHttpClient = new OkHttpClient();
Request request = new Request.Builder()
.url("https://img-my.csdn.net/uploads/201309/01/1378037128_5291.jpg")
.build();
Call call = mOkHttpClient.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
showlog(e.getMessage());
}
@Override
public void onResponse(Call call, Response response) throws IOException {
final byte[] data = response.body().bytes();
runOnUiThread(new Runnable() {
@Override
public void run() {
Bitmap bmp = BitmapFactory.decodeByteArray(data, 0, data.length);
image.setImageBitmap(bmp);
}
});
}
});
}
对于图片下载,文件下载其实是类似的;图片下载是通过回调的Response拿到byte[]然后decode成图片;文件下载则是拿到inputStream做写文件操作,我们这里就不赘述了。
如果每次请求网络都需要写重复的代码绝对是令人头疼的,网上也有很多对OkHttp封装的优秀开源项目,譬如OkHttpUtils,封装的意义就在于更加方便的使用,具有拓展性,但是对OkHttp封装最需要解决的是以下的两点:
根据以上两点,我也做一个简单的封装,首先呢我们写一个抽象类用于请求回调:
public interface ReqCallBack {
/**响应成功*/
void onReqSuccess(T result);
/**响应失败*/
void onReqFailed(String errorMsg);
}
接下来创建一个OkHttpManager类封装OkHttp,并实现了异步GET请求:
public class OkHttpManager {
private static volatile OkHttpManager mInstance;//单例引用
private OkHttpClient mOkHttpClient;//okHttpClient实例
private Handler okHttpHandler;//全局处理子线程和主线程通信
/**
* 初始化OkHttpManager
*/
public OkHttpManager(Context context) {
//初始化OkHttpClient
mOkHttpClient = new OkHttpClient().newBuilder()
.connectTimeout(10, TimeUnit.SECONDS)//设置超时时间
.readTimeout(10, TimeUnit.SECONDS)//设置读取超时时间
.writeTimeout(10, TimeUnit.SECONDS)//设置写入超时时间
.build();
//初始化Handler
okHttpHandler = new Handler(context.getMainLooper());
}
/**
* 获取单例引用
*/
public static OkHttpManager getInstance(Context context) {
if (mInstance == null) {
synchronized (OkHttpManager.class) {
if (mInstance == null) {
mInstance = new OkHttpManager(context);
}
}
}
return mInstance;
}
/**
* okHttp get异步请求
* @param actionUrl 接口地址
* @param callBack 请求返回数据回调
* @param 数据泛型
* @return
*/
public Call getAsynHttp(String url, final ReqCallBack callBack) {
try {
final Request request = new Request.Builder()
.url(url)
.build();
Call call = mOkHttpClient.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
failedCallBack("访问失败", callBack);
}
@Override
public void onResponse(Call call, Response response) throws IOException {
if (response.isSuccessful()) {
String string = response.body().string();
successCallBack((T) string, callBack);
} else {
failedCallBack("服务器错误", callBack);
}
}
});
return call;
} catch (Exception e) {}
return null;
}
/**
* 统一处理成功信息
* @param result
* @param callBack
* @param
*/
private void successCallBack(final T result, final ReqCallBack callBack) {
okHttpHandler.post(new Runnable() {
@Override
public void run() {
if (callBack != null) {
callBack.onReqSuccess(result);
}
}
});
}
/**
* 统一处理失败信息
* @param errorMsg
* @param callBack
* @param
*/
private void failedCallBack(final String errorMsg, final ReqCallBack callBack) {
okHttpHandler.post(new Runnable() {
@Override
public void run() {
if (callBack != null) {
callBack.onReqFailed(errorMsg);
}
}
});
}
}
最后使用这个OkHttpManager来请求网络:
OkHttpManager.getInstance(this).getAsynHttp("http://www.baidu.com", new ReqCallBack() {
@Override
public void onReqSuccess(String result) {
Toast.makeText(getApplicationContext(), result, Toast.LENGTH_SHORT).show();
}
@Override
public void onReqFailed(String errorMsg) {
showlog(errorMsg);
}
});
是不是很简单呢?就几句代码就实现了GET请求,而且请求结果回调是在UI线程的。这里只是一个很简单的封装例子,如果用在正式项目里推荐OkHttpUtils。
一般情况下服务端默认返回的是Json字符串,上面通过封装我们直接将Json返回给客户端。其实封装内部还能通过GSON解析让客户端直接获取对象。
首先新建两个Bean文件:
public class Weather {
public WeatherInfo weatherinfo;
}
public class WeatherInfo {
public String city;
public String cityid;
public String date_y;
public String temp1;
public String weather1;
public String toString() {
return "[city:"+city+" cityid:"+cityid+" date_y:"+date_y+" temp1:"+temp1+" weather1:"+weather1+"]";
}
}
改写上面的OkHttpManager,在服务器端成功返回Json后做一次转换,将Json字符串转换成Weather对象:
@Override
public void onResponse(Call call, Response response) throws IOException {
if (response.isSuccessful()) {
String string = response.body().string();
Weather weather = new Gson().fromJson(string, Weather.class);
successCallBack((T) weather, callBack);
} else {
failedCallBack("服务器错误", callBack);
}
}
请求网络,这里泛型类型我们传入Weather:
OkHttpManager.getInstance(this).getAsynHttp("http://weather.51wnl.com/weatherinfo/GetMoreWeather?cityCode=101020100&weatherType=0", new ReqCallBack() {
@Override
public void onReqSuccess(Weather weather) {
WeatherInfo weatherInfo = weather.weatherinfo;
showlog(weatherInfo.toString());
}
@Override
public void onReqFailed(String errorMsg) {
showlog(errorMsg);
}
});
可以看到,这里返回的已经是Weather对象了。注意,在正式项目里对OkHttp做封装时要考虑全面,对传进来的不同类型要生成不同的对象,而不仅仅是这里的Weather。
我们可以通过自定义Interceptor来实现很多操作,打印日志,缓存,重试等等。
要实现自己的拦截器需要有以下步骤
(1) 需要实现Interceptor接口,并复写intercept(Chain chain)方法,返回response
(2) Request 和 Response的Builder中有header,addHeader,headers方法,需要注意的是使用header有重复的将会被覆盖,而addHeader则不会。
标准的 Interceptor写法:
public class OAuthInterceptor implements Interceptor {
private final String username;
private final String password;
public OAuthInterceptor(String username, String password) {
this.username = username;
this.password = password;
}
@Override public Response intercept(Chain chain) throws IOException {
String credentials = username + ":" + password;
String basic = "Basic " + Base64.encodeToString(credentials.getBytes(), Base64.NO_WRAP);
Request originalRequest = chain.request();
String cacheControl = originalRequest.cacheControl().toString();
Request.Builder requestBuilder = originalRequest.newBuilder()
//Basic Authentication,也可用于token验证,OAuth验证
.header("Authorization", basic)
.header("Accept", "application/json")
.method(originalRequest.method(), originalRequest.body());
Request request = requestBuilder.build();
Response originalResponse = chain.proceed(request);
Response.Builder responseBuilder =
//Cache control设置缓存
originalResponse.newBuilder().header("Cache-Control", cacheControl);
return responseBuilder.build();
}
}
使用缓存可以让我们的app不用长时间地显示令人厌烦的加载圈,提高了用户体验,而且还节省了流量,在数据更新不是很频繁的地方使用缓存就非常有必要了。想要加入缓存不需要我们自己来实现,Okhttp已经内置了缓存,默认是不使用的,如果想使用缓存我们需要手动设置。
设置缓存就需要用到OkHttp的interceptors,缓存的设置需要靠请求和响应头。如果想要弄清楚缓存机制,则需要了解一下HTTP语义,其中控制缓存的就是Cache-Control字段,OkHttp3中有一个Cache类是用来定义缓存的,此类详细介绍了几种缓存策略,具体可看此类源码。
noCache :不使用缓存,全部走网络
noStore : 不使用缓存,也不存储缓存
onlyIfCached : 只使用缓存
maxAge :设置最大失效时间,失效则不使用
maxStale :设置最大失效时间,失效则不使用
minFresh :设置最小有效时间,失效则不使用
FORCE_NETWORK : 强制走网络
FORCE_CACHE :强制走缓存
服务器支持缓存
如果服务器支持缓存,请求返回的Response会带有这样的Header:Cache-Control, max-age=xxx,这种情况下我们只需要手动给okhttp设置缓存就可以让okhttp自动帮你缓存了。这里的max-age的值代表了缓存在你本地存放的时间,可以根据实际需要来设置其大小。
首先我们要提供了一个文件路径用来存放缓存,另外还需要指定缓存的大小就可以创建一个缓存了。
File sdcache = getExternalCacheDir();
int cacheSize = 10 * 1024 * 1024;
Cache cache = new Cache(sdcache.getAbsoluteFile(), cacheSize);
创建了这个缓存后我们还需要将其设置到okttpClient对象里面:
OkHttpClient mOkHttpClient = new OkHttpClient().newBuilder()
.cache(cache)
.connectTimeout(20, TimeUnit.SECONDS)
.readTimeout(20, TimeUnit.SECONDS)
.build();
服务器不支持缓存
如果服务器不支持缓存就可能没有指定这个头部,或者指定的值是如no-store等,但是我们还想在本地使用缓存的话要怎么办呢?这种情况下我们就需要使用Interceptor来重写Respose的头部信息,从而让okhttp支持缓存。
如下所示,我们重写的Response的Cache-Control字段:
public class CacheInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Response response = chain.proceed(request);
return response.newBuilder()
.removeHeader("Pragma")
.removeHeader("Cache-Control")
//cache for 30 days
.header("Cache-Control", "max-age=" + 3600 * 24 * 30)
.build();
}
}
然后将该Intercepter作为一个NetworkInterceptor加入到okhttpClient中:
OkHttpClient okHttpClient = new OkHttpClient();
OkHttpClient mOkHttpClient = okHttpClient.newBuilder()
.addNetworkInterceptor(new CacheInterceptor())
.cache(cache)
.connectTimeout(20, TimeUnit.SECONDS)
.readTimeout(20, TimeUnit.SECONDS)
.build();
Request request = new Request.Builder()
.url("http://www.baidu.com")
.build();
Call call = mOkHttpClient.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {}
@Override
public void onResponse(Call call, final Response response) throws IOException {
if (null != response.cacheResponse()) {
String str = response.cacheResponse().toString();
showlog("cache--->" + str);
} else {
response.body().string();
String str=response.networkResponse().toString();
showlog("network--->" + str);
}
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(getApplicationContext(), "请求成功", Toast.LENGTH_SHORT).show();
}
});
}
});
这样我们就可以在服务器不支持缓存的情况下使用缓存了。第一次请求会请求网络得到数据,第二次以及后面的请求则会从缓存中取出数据:
当然也有种情况是有的请求每次都需要最新的数据,则在创建Request,来设置cacheControl为“CacheControl.FORCE_NETWORK”,用来表示请求会一直请求网络得到数据:
final Request request = new Request.Builder()
.url("http://www.baidu.com")
.cacheControl(CacheControl.FORCE_NETWORK)
.build();
可以看到每次返回结果都来自网络。
两个CacheControl常量介绍:
CacheControl.FORCE_CACHE; //仅仅使用缓存
CacheControl.FORCE_NETWORK;//仅仅使用网络
但是大家必须注意一点,okhttp的缓存设计和浏览器的一样,是用来提升用户体验降低服务器负荷的,比如:我们在有网的时候也会先调用缓存,但是有个时间限制,例如1分钟之内,有网和没有网都是先读缓存,这个可以参考下面讲解的第一种类型。
如果你想要做成那种离线可以缓存,在线就获取最新数据的功能,可以参考第二种类型。
1、创建拦截器:
Interceptor interceptor = new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Response response = chain.proceed(request);
String cacheControl = request.cacheControl().toString();
if (TextUtils.isEmpty(cacheControl)) {
cacheControl = "public, max-age=60";
}
return response.newBuilder()
.header("Cache-Control", cacheControl)
.removeHeader("Pragma")
.build();
}
};
设置max-age为60s之后,这60s之内不管你有没有网,都读缓存。如果cache没有过期会直接返回cache而不会发起网络请求,若过期会自动发起网络请求。
2、设置client
/**创建OkHttpClient,并添加拦截器和缓存代码**/
OkHttpClient client = new OkHttpClient.Builder()
.addNetworkInterceptor(interceptor)
.cache(cache).build();
这种方法和第一种方法的区别是在设置的拦截器上,这里同时使用NetworkInterceptor和AppInterceptor。
先讲一下步骤:
1、首先,给OkHttp设置拦截器
2、然后,在拦截器内做Request拦截操作,在每个请求发出前,判断一下网络状况,如果没问题继续访问,如果有问题,则设置从本地缓存中读取
3、接下来是设置Response,先判断网络,网络好的时候,移除header后添加cache失效时间为0小时,网络未连接的情况下设置缓存时间为4周
代码:
1、给OkHttp设置拦截器(用Interceptor)
OkHttpClient client = new OkHttpClient.Builder()
.addNetworkInterceptor(interceptor)
.addInterceptor(interceptor)
.cache(cache).build();
2、Request拦截操作
Request request = chain.request();
/**注意:如果您使用FORCE_CACHE和网络的响应需求,OkHttp则会返回一个504提示,告诉你不可满足请求响应。所以我们加一个判断在没有网络的情况下使用*/
if (!isNetworkAvailable(MainActivity.this)) {
request = request.newBuilder()
.cacheControl(CacheControl.FORCE_CACHE) //无网络时,强制从缓存取
.build();
showlog("no network");
}
3、设置Response
Response response = chain.proceed(request);
if (isNetworkAvailable(MainActivity.this)) {
int maxAge = 0 * 60; // 有网络时,设置缓存超时时间0个小时
showlog("has network maxAge="+maxAge);
response.newBuilder()
.header("Cache-Control", "public, max-age=" + maxAge)
.removeHeader("Pragma")// 清除头信息,因为服务器如果不支持,会返回一些干扰信息,不清除下面无法生效
.build();
} else {
showlog("network error");
int maxStale = 60 * 60 * 24 * 28; // 无网络时,设置超时为4周
showlog("has maxStale="+maxStale);
response.newBuilder()
.header("Cache-Control", "public, only-if-cached, max-stale=" + maxStale)
.removeHeader("Pragma")
.build();
showlog("response build maxStale="+maxStale);
}
return response;
查看缓存文件:
运行程序,在Android/data/com.hx.okhttp.cache下会发现很多缓存文件,这些缓存文件全是以url的md5加密字段为文件名,每一个response分两个文件保存,以.0和.1结尾的文件区分。 进去看里面的内容如下: .0的文件里面是header,而.1文件里面是返回的具体内容,即json数据。
分别看一下内容吧:
因为百度html页面含有中文,这里看到数据有乱码现象
注意的是:okhttp只会对get请求进行缓存,post请求是不会进行缓存,这也是有道理的,因为get请求的数据一般是比较持久的,而post一般是交互操作,没太大意义进行缓存。
使用call.cancel()可以立即停止掉一个正在执行的call。如果一个线程正在写请求或者读响应,将会引发IOException。当用户离开一个应用时或者跳到其他界面时,使用Call.cancel()可以节约网络资源,另外不管同步还是异步的call都可以取消。
也可以通过tags来同时取消多个请求。当你构建一请求时,使用RequestBuilder.tag(tag)来分配一个标签。之后你就可以用OkHttpClient.cancel(tag)来取消所有带有这个tag的call。
为了模拟这个场景我们首先创建一个定时的线程池:
private ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
private void cancelCall(){
OkHttpClient mOkHttpClient = new OkHttpClient();
Request request = new Request.Builder()
.url("http://www.baidu.com")
.cacheControl(CacheControl.FORCE_NETWORK)
.build();
Call call = null;
call = mOkHttpClient.newCall(request);
final Call finalCall = call;
//100毫秒后取消call
executor.schedule(new Runnable() {
@Override
public void run() {
finalCall.cancel();
}
}, 100, TimeUnit.MILLISECONDS);
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {}
@Override
public void onResponse(Call call,final Response response) {
if (null != response.cacheResponse()) {
String str = response.cacheResponse().toString();
showlog("cache--->" + str);
} else {
try {
response.body().string();
} catch (IOException e) {
showlog("IOException");
e.printStackTrace();
}
String str = response.networkResponse().toString();
showlog("network--->" + str);
}
}
});
showlog("是否取消成功"+call.isCanceled());
}
100毫秒后调用call.cancel(),为了能让请求耗时,我们设置每次请求都要请求网络,运行程序并且不断的快速点击发送请求按钮:
很明显每次cancel()都失败了,仍旧成功的访问了网络。每隔100毫秒来调用call.cancel()显然时间间隔太长,这段时间网络已经返回了数据,我们设置为1毫秒并不断的快速的点击发送请求按钮:
这次每次cancel()操作都成功了。
Demo下载地址
统一的文件下载管理(DownloadManager)
默认使用的是 get 请求,同时下载数量为3个,支持断点下载,断点信息使用ORMLite数据库框架保存,默认下载路径/storage/emulated/0/download,下载路径和下载数量都可以在代码中配置,下载管理使用了服务提高线程优先级,避免后台下载时被系统回收。
当你的项目需要做大量的下载的时候,并且多个页面需要监听下载进度,使用该扩展让你更方便。原生支持下载任务的开始、暂停、停止、出错、完成五个状态,当同时下载任务数量超过3个(可代码动态设置)时,后续任务自动等待,当有任务下载完成时,等待任务按照优先级自动开始下载。
统一的文件上传管理(UploadManager)
默认使用的是 post 上传请求,该上传管理为简单管理,不支持断点续传和分片上传,只是简单的将所有上传任务使用线程池进行了统一管理,默认同时上传数量为1个。由于断点分片上传的技术需要大量的服务端代码配合,同时也会极大的增加客户端代码量,所以综合考虑,该框架不做实现。如果确实有特殊需要,可以自己做扩展。
优势一:性能高,专注于简单易用的网络请求,使用主流的okhttp进行封装,对于okhttp大家都知道,在Android4.4的源码中可以看到HttpURLConnection已经替换成OkHttp实现了,并且支持HTTP2/SPDY黑科技,支持socket自动选择最好路线,并支持自动重连,拥有自动维护的socket连接池,减少握手次数,拥有队列线程池,轻松写并发。
优势二:特有的网络缓存模式,是大多数网络框架所不具备的,说一个应用场景,老板说我们的app不仅需要在有网的情况下展示最新的网络数据,还要在没网的情况下使用缓存数据,这时候是不是项目中出现了大量的代码判断当前网络状况,根据不同的状态保存不同的数据,然后决定是否使用缓存。细想一下,这是个通用的写法,于是OkHttpUtils提供了四种缓存模式,让你不用关心缓存的实现,而专注于数据的处理。
优势三:方便易用的扩展接口,可以添加全局的公共参数,全局拦截器,全局超时时间,更可以对单个请求定制拦截器,超时时间,请求参数修改等等,在使用上更是方便,原生支持的链式调用让你的请求更加清晰。
优势四:强大的Cookie保持策略,我们知道在客户端对cookie的获取是个不太简单的事情,特别是还要处理cookie的过期时间,持久化策略等等,OkHttpUtils帮你彻底解决Cookie的难题,默认拥有内存存储和持久化存储两种实现,cookie全程自动管理,并且提供了额外的addCookie方式,允许介入到自动管理的过程中,添加你想创建的任何cookie。
Eclipse的用户,导入JAR包或添加依赖工程:
见我后面的Demo,用得是依赖工程。
添加网络访问权限和读写SD卡权限:
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
一般在 Aplication,或者基类中,只需要调用一次即可,可以配置调试开关,全局的超时时间,公共的请求头和请求参数等信息,所有的请求参数都支持中文。
@Override
public void onCreate() {
super.onCreate();
//---------这里给出的是示例代码,告诉你可以这么传,实际使用的时候,根据需要传,不需要就不传-------------//
HttpHeaders headers = new HttpHeaders();
headers.put("commonHeaderKey1", "commonHeaderValue1"); //header不支持中文
headers.put("commonHeaderKey2", "commonHeaderValue2");
HttpParams params = new HttpParams();
params.put("commonParamsKey1", "commonParamsValue1"); //param支持中文,直接传,不要自己编码
params.put("commonParamsKey2", "这里支持中文参数");
//-----------------------------------------------------------------------------------//
//必须调用初始化
OkHttpUtils.init(this);
//以下设置的所有参数是全局参数,同样的参数可以在请求的时候再设置一遍,那么对于该请求来讲,请求中的参数会覆盖全局参数
//好处是全局参数统一,特定请求可以特别定制参数
try {
//以下都不是必须的,根据需要自行选择,一般来说只需要 debug,缓存相关,cookie相关的 就可以了
OkHttpUtils.getInstance()
//打开该调试开关,控制台会使用 红色error 级别打印log,并不是错误,是为了显眼,不需要就不要加入该行
.debug("OkHttpUtils")
//如果使用默认的 60秒,以下三行也不需要传
.setConnectTimeout(OkHttpUtils.DEFAULT_MILLISECONDS) //全局的连接超时时间
.setReadTimeOut(OkHttpUtils.DEFAULT_MILLISECONDS) //全局的读取超时时间
.setWriteTimeOut(OkHttpUtils.DEFAULT_MILLISECONDS) //全局的写入超时时间
//可以全局统一设置缓存模式,默认是不使用缓存,可以不传
.setCacheMode(CacheMode.NO_CACHE)
//可以全局统一设置缓存时间,默认永不过期
.setCacheTime(CacheEntity.CACHE_NEVER_EXPIRE)
//如果不想让框架管理cookie,以下不需要
// .setCookieStore(new MemoryCookieStore()) //cookie使用内存缓存(app退出后,cookie消失)
.setCookieStore(new PersistentCookieStore()) //cookie持久化存储,如果cookie不过期,则一直有效
//可以设置https的证书,以下几种方案根据需要自己设置
// .setCertificates() //方法一:信任所有证书(选一种即可)
// .setCertificates(getAssets().open("srca.cer")) //方法二:也可以自己设置https证书(选一种即可)
// .setCertificates(getAssets().open("aaaa.bks"), "123456", getAssets().open("srca.cer"))//方法三:传入bks证书,密码,和cer证书,支持双向加密
//可以添加全局拦截器,不会用的千万不要传,错误写法直接导致任何回调不执行
// .addInterceptor(new Interceptor() {
// @Override
// public Response intercept(Chain chain) throws IOException {
// return chain.proceed(chain.request());
// }
// })
//这两行同上,不需要就不要传
.addCommonHeaders(headers) //设置全局公共头
.addCommonParams(params); //设置全局公共参数
} catch (Exception e) {
e.printStackTrace();
}
}
1.基本的网络请求
OkHttpUtils.get("http://www.baidu.com") // 请求方式和请求url
.tag(this) // 请求的 tag, 主要用于取消对应的请求
.cacheKey("cacheKey") // 设置当前请求的缓存key,建议每个不同功能的请求设置不同值,防止覆盖
.cacheMode(CacheMode.DEFAULT) // 缓存模式,详细请看缓存介绍
.execute(new StringCallback() {
@Override
public void onSuccess(String t, Call call, Response response) {
showlog("t =" + t);
showHeader(call, response);
}
});
注意:这里我们将request和response的头部信息用log打印出来了:
public void showHeader(Call call, Response response){
/**request*/
showlog(call.request().toString());
/**request请求头信息*/
Headers requestHeadersString = call.request().headers();
Set requestNames = requestHeadersString.names();
StringBuilder sb = new StringBuilder();
for (String name : requestNames) {
sb.append(name).append(" : ").append(requestHeadersString.get(name)).append("\n");
}
showlog(sb.toString());
/**response*/
showlog(response.toString());
/**response响应头信息*/
Headers responseHeadersString = response.headers();
Set responseNames = responseHeadersString.names();
StringBuilder sb1 = new StringBuilder();
for (String name : responseNames) {
sb1.append(name).append(" : ").append(requestHeadersString.get(name)).append("\n");
}
showlog(sb1.toString());
}
OkHttpUtils.get("http://weather.51wnl.com/weatherinfo/GetMoreWeather?cityCode=101020100&weatherType=0")
.tag(this)
.cacheKey("cacheKey2") // 设置当前请求的缓存key,建议每个不同功能的请求设置不同值,防止覆盖
.cacheMode(CacheMode.DEFAULT) // 缓存模式,详细请看缓存介绍
.execute(new JsonCallback(Weather.class) {
@Override
public void onSuccess(Weather weather, Call call, Response response) {
showHeader(call, response);
WeatherInfo weatherInfo = weather.weatherinfo;
showlog("city is " + weatherInfo.city);
showlog("cityid is " + weatherInfo.cityid);
showlog("date_y is " + weatherInfo.date_y);
showlog("temp1 is " + weatherInfo.temp1);
showlog("weather1 is " + weatherInfo.weather1);
}
});
注意:这里请求Json返回的是Weather对象,因为添加到了缓存,所以Weather必须实现Serializable接口,否者会报NotSerializableException。
来看看Weather的Bean文件写法:
public class Weather implements Serializable {
private static final long serialVersionUID = 1L;
public WeatherInfo weatherinfo;
}
public class WeatherInfo implements Serializable{
private static final long serialVersionUID = 1L;
public String city;
public String cityid;
public String date_y;
public String temp1;
public String weather1;
}
2.请求 Bitmap 对象
OkHttpUtils.get("https://img-my.csdn.net/uploads/201309/01/1378037128_5291.jpg")
.tag(this)
.execute(new BitmapCallback(){
@Override
public void onSuccess(Bitmap t, Call call, Response response) {
showHeader(call, response);
image.setImageBitmap(t);
}
});
3.请求文件下载
OkHttpUtils.get("https://img-my.csdn.net/uploads/201308/31/1377949598_9982.jpg")
.tag(this)
//文件下载时,需要指定下载的文件目录和文件名
.execute(new FileCallback("/sdcard/temp/", "file.jpg") {
@Override
public void onSuccess(File file, Call call, Response response) {
showHeader(call, response);}
@Override
public void downloadProgress(long currentSize, long totalSize, float progress, long networkSpeed) {
String downloadLength = Formatter.formatFileSize(getApplicationContext(), currentSize);
String totalLength = Formatter.formatFileSize(getApplicationContext(), totalSize);
size.setText(downloadLength + "/" + totalLength);
String netSpeed = Formatter.formatFileSize(getApplicationContext(), networkSpeed);
speed.setText(netSpeed + "/S");
percent.setText((Math.round(progress * 10000) * 1.0f / 100) + "%");
progressBar.setMax(100);
progressBar.setProgress((int) (progress * 100));
}
});
4.Post上传文件
可以使用的方式有一个key对应一个文件和一个key对应多个文件。注意,在这里上传之前需要在/sdcard/temp路径下存在file.jpg和file2.jpg两个文件。
ArrayList files = new ArrayList();
files.add(new File("/sdcard/temp/file1.jpg"));
files.add(new File("/sdcard/temp/file2.jpg"));
// 拼接参数
OkHttpUtils.post("http://server.jeasonlzy.com/OkHttpUtils/upload")
.tag(this)
.headers("header1", "headerValue1")
.headers("header2", "headerValue2")
.params("param1", "paramValue1")
.params("param2", "paramValue2")
//这种方式为一个key,对应一个文件
/** .params("file1",new File("文件路径"))
.params("file2",new File("文件路径")) */
// 这种方式为同一个key,上传多个文件
.addFileParams("file", files)
.execute(new JsonCallback(ServerModel.class) {
@Override
public void onSuccess(ServerModel model, Call call, Response response) {
showHeader(call, response);
showlog(model.toString());
}
@Override
public void upProgress(long currentSize, long totalSize, float progress, long networkSpeed) {
String downloadLength = Formatter.formatFileSize(getApplicationContext(), currentSize);
String totalLength = Formatter.formatFileSize(getApplicationContext(), totalSize);
size.setText(downloadLength + "/" + totalLength);
String netSpeed = Formatter.formatFileSize(getApplicationContext(), networkSpeed);
speed.setText(netSpeed + "/S");
percent.setText((Math.round(progress * 10000) * 1.0f / 100) + "%");
progressBar.setMax(100);
progressBar.setProgress((int) (progress * 100));
}
});
6.请求功能的所有配置讲解
以下代码包含了以下内容:
/**
* 该类的回调具有如下顺序,虽然顺序写的很复杂,但是理解后,是很简单,并且合情合理的
*
1.无缓存模式{@link CacheMode#NO_CACHE}
* ---网络请求成功 onBefore -> parseNetworkResponse -> onSuccess -> onAfter
* ---网络请求失败 onBefore -> parseNetworkFail -> onError -> onAfter
*
2.默认缓存模式,遵循304头{@link CacheMode#DEFAULT}
* ---网络请求成功,服务端返回非304 onBefore -> parseNetworkResponse -> onSuccess -> onAfter
* ---网络请求成功服务端返回304 onBefore -> onCacheSuccess -> onAfter
* ---网络请求失败 onBefore -> parseNetworkFail -> onError -> onAfter
*
3.请求网络失败后读取缓存{@link CacheMode#REQUEST_FAILED_READ_CACHE}
* ---网络请求成功,不读取缓存 onBefore -> parseNetworkResponse -> onSuccess -> onAfter
* ---网络请求失败,读取缓存成功 onBefore -> parseNetworkFail -> onError -> onCacheSuccess -> onAfter
* ---网络请求失败,读取缓存失败 onBefore -> parseNetworkFail -> onError -> onCacheError -> onAfter
*
4.如果缓存不存在才请求网络,否则使用缓存{@link CacheMode#IF_NONE_CACHE_REQUEST}
* ---已经有缓存,不请求网络 onBefore -> onCacheSuccess -> onAfter
* ---没有缓存请求网络成功 onBefore -> onCacheError -> parseNetworkResponse -> onSuccess -> onAfter
* ---没有缓存请求网络失败 onBefore -> onCacheError -> parseNetworkFail -> onError -> onAfter
*
5.先使用缓存,不管是否存在,仍然请求网络{@link CacheMode#FIRST_CACHE_THEN_REQUEST}
* ---无缓存时,网络请求成功 onBefore -> onCacheError -> parseNetworkResponse -> onSuccess -> onAfter
* ---无缓存时,网络请求失败 onBefore -> onCacheError -> parseNetworkFail -> onError -> onAfter
* ---有缓存时,网络请求成功 onBefore -> onCacheSuccess -> parseNetworkResponse -> onSuccess -> onAfter
* ---有缓存时,网络请求失败 onBefore -> onCacheSuccess -> parseNetworkFail -> onError -> onAfter
*/
OkHttpUtils.get(Urls.URL_METHOD) // 请求方式和请求url, get请求不需要拼接参数,支持get,post,put,delete,head,options请求
.tag(this) // 请求的 tag, 主要用于取消对应的请求
.connTimeOut(10000) // 设置当前请求的连接超时时间
.readTimeOut(10000) // 设置当前请求的读取超时时间
.writeTimeOut(10000) // 设置当前请求的写入超时时间
.cacheKey("cacheKey") // 设置当前请求的缓存key,建议每个不同功能的请求设置一个
.cacheMode(CacheMode.FIRST_CACHE_THEN_REQUEST) // 缓存模式,详细请看第四部分,缓存介绍
.setCertificates(getAssets().open("srca.cer")) // 自签名https的证书,可变参数,可以设置多个
.addInterceptor(interceptor) // 添加自定义拦截器
.headers("header1", "headerValue1") // 添加请求头参数
.headers("header2", "headerValue2") // 支持多请求头参数同时添加
.params("param1", "paramValue1") // 添加请求参数
.params("param2", "paramValue2") // 支持多请求参数同时添加
.params("file1", new File("filepath1")) // 可以添加文件上传
.params("file2", new File("filepath2")) // 支持多文件同时添加上传
.addUrlParams("key", List values) //这里支持一个key传多个参数
.addFileParams("key", List files) //这里支持一个key传多个文件
.addFileWrapperParams("key", List fileWrappers) //这里支持一个key传多个文件
.addCookie("aaa", "bbb") // 这里可以传递自己想传的Cookie
.addCookie(cookie) // 可以自己构建cookie
.addCookies(cookies) // 可以一次传递批量的cookie
//这里给出的泛型为 RequestInfo,同时传递一个泛型的 class对象,即可自动将数据结果转成对象返回
.execute(new DialogCallback(this, RequestInfo.class) {
@Override
public void onBefore(BaseRequest request) {
// UI线程 请求网络之前调用
// 可以显示对话框,添加/修改/移除 请求参数
}
@Override
public RequestInfo parseNetworkResponse(Response response) throws Exception{
// 子线程,可以做耗时操作
// 根据传递进来的 response 对象,把数据解析成需要的 RequestInfo 类型并返回
// 可以根据自己的需要,抛出异常,在onError中处理
return null;
}
@Override
public void onResponse(boolean isFromCache, RequestInfo requestInfo, Request request, @Nullable Response response) {
// UI 线程,请求成功后回调
// isFromCache 表示当前回调是否来自于缓存
// requestInfo 返回泛型约定的实体类型参数
// request 本次网络的请求信息,如果需要查看请求头或请求参数可以从此对象获取
// response 本次网络访问的结果对象,包含了响应头,响应码等,如果数据来自于缓存,该对象为null
}
@Override
public void onError(boolean isFromCache, Call call, @Nullable Response response, @Nullable Exception e) {
// UI 线程,请求失败后回调
// isFromCache 表示当前回调是否来自于缓存
// call 本次网络的请求对象,可以根据该对象拿到 request
// response 本次网络访问的结果对象,包含了响应头,响应码等,如果网络异常 或者数据来自于缓存,该对象为null
// e 本次网络访问的异常信息,如果服务器内部发生了错误,响应码为 400~599之间,该异常为 null
}
@Override
public void onAfter(boolean isFromCache, @Nullable RequestInfo requestInfo, Call call, @Nullable Response response, @Nullable Exception e) {
// UI 线程,请求结束后回调,无论网络请求成功还是失败,都会调用,可以用于关闭显示对话框
// isFromCache 表示当前回调是否来自于缓存
// requestInfo 返回泛型约定的实体类型参数,如果网络请求失败,该对象为 null
// call 本次网络的请求对象,可以根据该对象拿到 request
// response 本次网络访问的结果对象,包含了响应头,响应码等,如果网络异常 或者数据来自于缓存,该对象为null
// e 本次网络访问的异常信息,如果服务器内部发生了错误,响应码为 400~599之间,该异常为 null
}
@Override
public void upProgress(long currentSize, long totalSize, float progress, long networkSpeed) {
// UI 线程,文件上传过程中回调,只有请求方式包含请求体才回调(GET,HEAD不会回调)
// currentSize 当前上传的大小(单位字节)
// totalSize 需要上传的总大小(单位字节)
// progress 当前上传的进度,范围 0.0f ~ 1.0f
// networkSpeed 当前上传的网速(单位秒)
}
@Override
public void downloadProgress(long currentSize, long totalSize, float progress, long networkSpeed) {
// UI 线程,文件下载过程中回调
//参数含义同 上传相同
}
});
7.取消请求
每个请求前都设置了一个参数tag,取消则通过OkHttpUtils.cancel(tag)执行。例如:在Activity中,当Activity销毁取消请求,可以在onDestory里面统一取消。
@Override
protected void onDestroy() {
super.onDestroy();
//根据 Tag 取消请求
OkHttpUtils.getInstance().cancelTag(this);
}
8.同步的请求
execute方法不传入callback即为同步的请求,返回Response对象,需要自己解析
Response response = OkHttpUtils.get("http://www.baidu.com")
.tag(this)
.headers("aaa", "111")
.params("bbb", "222")
.execute();
目前内部提供的包含AbsCallback, StringCallBack ,BitmapCallback ,FileCallBack ,可以根据自己的需求去自定义Callback
该网络框架的核心使用方法即为Callback的继承使用。因为不同的项目需求,可能对数据格式进行了不同的封装,于是在 Demo 中的进行了详细的代码示例,以下是详细介绍:
使用缓存前,必须让缓存的数据javaBean对象实现Serializable接口,否者会报NotSerializableException。因为缓存的原理是将对象序列化后直接写入数据库中,如果不实现Serializable接口,会导致对象无法序列化,进而无法写入到数据库中,也就达不到缓存的效果。
目前提供了四种CacheMode缓存模式:
注:无论对于哪种缓存模式,都可以指定一个cacheKey,建议针对不同需要缓存的页面设置不同的cacheKey,如果相同,会导致数据覆盖。
下载任务的管理
/** 添加一个下载任务,taskTag用来标识每一个下载任务,fileName默认为null */
public void addTask(String taskTag, BaseRequest request, DownloadListener listener);
/** 添加一个下载任务,taskTag用来标识每一个下载任务,fileName指定文件名 */
public void addTask(String fileName, String taskTag, BaseRequest request, DownloadListener listener);
/** 开始所有任务 */
public void startAllTask();
/** 暂停 */
public void pauseTask(String taskKey);
/** 暂停全部任务 */
public void pauseAllTask();
/** 停止 */
public void stopTask(String taskKey);
/** 停止全部任务 */
public void stopAllTask();
/** 删除一个任务,不会删除下载文件 */
public void removeTask(String taskKey);
/** 删除一个任务,会删除下载文件 */
public void removeTask(String taskKey, boolean isDeleteFile);
/** 删除所有任务 */
public void removeAllTask();
/** 重新下载 */
public void restartTask(String taskKey);
/** 重新开始下载任务 */
private void restartTaskByKey(String taskKey);
/** 获取一个任务 */
public DownloadInfo getDownloadInfo(String taskKey);
/** 移除一个任务 */
private void removeTaskByKey(String taskKey);
添加一个下载任务
public void downloadFile(String url) {
if (downloadManager.getDownloadInfo(url) != null) {
Toast.makeText(getApplicationContext(), "任务已经在下载列表中", Toast.LENGTH_SHORT).show();
} else {
GetRequest request = OkHttpUtils.get(url)
.headers("headerKey1", "headerValue1")
.headers("headerKey2", "headerValue2")
.params("paramKey1", "paramValue1")
.params("paramKey2", "paramValue2");
downloadManager.addTask(url, request, null);
}
}
更新下载进度
首先获取所有下载信息的列表:
List allTask = downloadManager.getAllTask();
在Adapter的getView中我们获取当前下载信息,对每个DownloadInfo设置一个下载监听器:
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
DownloadInfo downloadInfo = allTask.get(position);
ViewHolder holder;
if (convertView == null) {
convertView = View.inflate(DownloadManagerActivity.this, R.layout.item_download_manager, null);
holder = new ViewHolder(convertView);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
holder.refresh(downloadInfo);
/**对于非进度更新的ui放在这里,对于实时更新的进度ui,放在holder中*/
holder.name.setText(downloadInfo.getFileName());
holder.download.setOnClickListener(holder);
holder.remove.setOnClickListener(holder);
holder.restart.setOnClickListener(holder);
/** 给每一个downloadInfo设置下载监听器downloadListener*/
DownloadListener downloadListener = new MyDownloadListener();
downloadListener.setUserTag(holder);
downloadInfo.setListener(downloadListener);
return convertView;
}
重写监听器DownloadListener方法:
private class MyDownloadListener extends DownloadListener {
@Override
public void onProgress(DownloadInfo downloadInfo) {
if (getUserTag() == null) return;
ViewHolder holder = (ViewHolder) getUserTag(); //根据downloadInfo获取holder
holder.refresh(); //每次进度更新回调触发holder的refresh
}
@Override
public void onFinish(DownloadInfo downloadInfo) {
Toast.makeText(DownloadManagerActivity.this, "下载完成:" + downloadInfo.getTargetPath(), Toast.LENGTH_SHORT).show();
}
@Override
public void onError(DownloadInfo downloadInfo, String errorMsg, Exception e) {
if (errorMsg != null) Toast.makeText(DownloadManagerActivity.this, errorMsg, Toast.LENGTH_SHORT).show();
}
}
在holder的refresh方法中更新下载进度:
private class ViewHolder implements View.OnClickListener {
private DownloadInfo downloadInfo;
private TextView name;
private TextView downloadSize;
private TextView tvProgress;
private TextView netSpeed;
private ProgressBar pbProgress;
private Button download;
private Button remove;
private Button restart;
public ViewHolder(View convertView) {
name = (TextView) convertView.findViewById(R.id.name);
downloadSize = (TextView) convertView.findViewById(R.id.downloadSize);
tvProgress = (TextView) convertView.findViewById(R.id.tvProgress);
netSpeed = (TextView) convertView.findViewById(R.id.netSpeed);
pbProgress = (ProgressBar) convertView.findViewById(R.id.progress);
download = (Button) convertView.findViewById(R.id.start);
remove = (Button) convertView.findViewById(R.id.remove);
restart = (Button) convertView.findViewById(R.id.restart);
}
public void refresh(DownloadInfo downloadInfo) {
this.downloadInfo = downloadInfo;
refresh();
}
/**对于实时更新的进度ui,放在这里,例如进度的显示,而图片加载等,不要放在这,会不停的重复回调,也会导致内存泄漏*/
private void refresh() {
String downloadLength = Formatter.formatFileSize(DownloadManagerActivity.this, downloadInfo.getDownloadLength());
String totalLength = Formatter.formatFileSize(DownloadManagerActivity.this, downloadInfo.getTotalLength());
downloadSize.setText(downloadLength + "/" + totalLength);
if (downloadInfo.getState() == DownloadManager.NONE) {
netSpeed.setText("停止");
download.setText("下载");
} else if (downloadInfo.getState() == DownloadManager.PAUSE) {
netSpeed.setText("暂停中");
download.setText("继续");
} else if (downloadInfo.getState() == DownloadManager.ERROR) {
netSpeed.setText("下载出错");
download.setText("出错");
} else if (downloadInfo.getState() == DownloadManager.WAITING) {
netSpeed.setText("等待中");
download.setText("等待");
} else if (downloadInfo.getState() == DownloadManager.FINISH) {
netSpeed.setText("下载完成");
} else if (downloadInfo.getState() == DownloadManager.DOWNLOADING) {
String networkSpeed = Formatter.formatFileSize(DownloadManagerActivity.this, downloadInfo.getNetworkSpeed());
netSpeed.setText(networkSpeed + "/s");
download.setText("暂停");
}
tvProgress.setText((Math.round(downloadInfo.getProgress() * 10000) * 1.0f / 100) + "%");
pbProgress.setMax((int) downloadInfo.getTotalLength());
/**更新进度显示*/
pbProgress.setProgress((int) downloadInfo.getDownloadLength());
}
@Override
public void onClick(View v) {
if (v.getId() == download.getId()) {
switch (downloadInfo.getState()) {
case DownloadManager.PAUSE:
case DownloadManager.NONE:
case DownloadManager.ERROR:
downloadManager.addTask(downloadInfo.getUrl(), downloadInfo.getRequest(), downloadInfo.getListener());
break;
case DownloadManager.DOWNLOADING:
downloadManager.pauseTask(downloadInfo.getUrl());
break;
case DownloadManager.FINISH:
break;
}
refresh();
} else if (v.getId() == remove.getId()) {
downloadManager.removeTask(downloadInfo.getUrl());
adapter.notifyDataSetChanged();
} else if (v.getId() == restart.getId()) {
downloadManager.restartTask(downloadInfo.getUrl());
}
}
}
上传任务的管理
/** 添加一个上传任务,默认使用post请求 */
public void addTask(String url, File resource, String key, UploadListener listener) {
PostRequest request = OkHttpUtils.post(url).params(key, resource);
addTask(url, request, listener);
}
/** 添加一个上传任务 */
public void addTask(String taskKey, BaseBodyRequest request, UploadListener listener);
/**获取所有的上传任务*/
public List getAllTask();
添加上传任务
public void uploadFiles(){
if (images != null) {
for (int i = 0; i < images.size(); i++) {
MyUploadListener listener = new MyUploadListener();
//给每个listener设置一个tag值,就是listview的子View
listener.setUserTag(listView.getChildAt(i));
PostRequest postRequest = OkHttpUtils.post("http://server.jeasonlzy.com/OkHttpUtils/upload")
.headers("headerKey1", "headerValue1")
.headers("headerKey2", "headerValue2")
.params("paramKey1", "paramValue1")
.params("paramKey2", "paramValue2")
.params("fileKey" + i, new File(images.get(i)));
uploadManager.addTask(images.get(i), postRequest, listener);
}
}
}
注意:这里上传路径下必须存在对应的文件,否则上传时会走到监听器的onError方法。
更新上传进度
从上面看出,添加上传任务时,每个任务传入一个监听器:
...
MyUploadListener listener = new MyUploadListener();
...
uploadManager.addTask(images.get(i), postRequest, listener);
我们重写的UploadListener类:
private class MyUploadListener extends UploadListener {
private ViewHolder holder;
@Override
public void onProgress(UploadInfo uploadInfo) {
//通过子View的getTag获取holder
holder = (ViewHolder) ((View) getUserTag()).getTag();
//每次进度更新回调触发holder的refresh
holder.refresh(uploadInfo);
}
@Override
public void onFinish(String s) {
Log.e("MyUploadListener", "finish:" + s);
holder.finish();
}
@Override
public void onError(UploadInfo uploadInfo, String errorMsg, Exception e) {
Log.e("MyUploadListener", "onError:" + errorMsg);
}
@Override
public String parseNetworkResponse(Response response) throws Exception {
Log.e("MyUploadListener", "parseNetworkResponse");
return response.body().string();
}
}
在holder的refresh中刷新当前上传的状态值和上传进度:
public void refresh(UploadInfo uploadInfo) {
if (uploadInfo.getState() == DownloadManager.NONE) {
status.setText("请上传");
percent.setText("请上传");
} else if (uploadInfo.getState() == UploadManager.ERROR) {
status.setText("上传出错");
percent.setText("错误");
} else if (uploadInfo.getState() == UploadManager.WAITING) {
status.setText("等待中");
percent.setText("等待");
} else if (uploadInfo.getState() == UploadManager.FINISH) {
status.setText("上传成功");
percent.setText("成功");
} else if (uploadInfo.getState() == UploadManager.UPLOADING) {
status.setText("上传中");
percent.setText((Math.round(uploadInfo.getProgress() * 10000) * 1.0f / 100) + "%"); //更新上传进度
}
}
OkHttpUtils Demo下载地址
上面是OKHttp总体设计图,主要是通过Diapatcher不断从RequestQueue中取出请求(Call),根据是否已缓存调用Cache或Network这两类数据获取接口之一,从内存缓存或是服务器取得请求的数据。该引擎有同步和异步请求,同步请求通过Call.execute()直接返回当前的Response,而异步请求会把当前的请求Call.enqueue添加(AsyncCall)到请求队列中,并通过回调(Callback) 的方式来获取最后结果。
调用execute方法会立即执行我们的请求,execute()的代码逻辑如下:
public Response execute() throws IOException {
synchronized (this) {
//判断是否已经执行过,如果已经执行了则直接抛出异常,也就是说一个Call实例只能调用一次execute方法
if (executed) throw new IllegalStateException("Already Executed");
executed = true;
}
try {
//立即把本次Call放入已知双端行队列中
client.getDispatcher().executed(this);
//调用getResponseWithInterceptorChain
Response result = getResponseWithInterceptorChain(false);
if (result == null) throw new IOException("Canceled");
return result;
} finally {
//从执行完成队列中移除
client.getDispatcher().finished(this);
}
}
调用enqueue方法执行代码逻辑如下:
public void enqueue(Callback responseCallback) {
enqueue(responseCallback, false);
}
void enqueue(Callback responseCallback, boolean forWebSocket) {
synchronized (this) {
//同样的也进行了判断本次Call是否是第一次调用
if (executed) throw new IllegalStateException("Already Executed");
executed = true;
}
//调用enqueue(AsyncCall call)进行入队操作
client.getDispatcher().enqueue(new AsyncCall(responseCallback, forWebSocket));
}
可以看到最终的请求是dispatcher来完成的。Dispatcher主要用于控制并发的请求,它主要维护了以下变量:
/** 最大并发请求数*/
private int maxRequests = 64;
/** 每个主机最大请求数*/
private int maxRequestsPerHost = 5;
/** 消费者线程池 */
private ExecutorService executorService;
/** 将要运行的异步请求队列 */
private final Deque readyAsyncCalls = new ArrayDeque<>();
/**正在运行的异步请求队列 */
private final Deque runningAsyncCalls = new ArrayDeque<>();
/** 正在运行的同步请求队列 */
private final Deque runningSyncCalls = new ArrayDeque<>();
接下来看一下入队函数:
synchronized void enqueue(AsyncCall call) {
if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
runningAsyncCalls.add(call);
executorService().execute(call);
} else {
readyAsyncCalls.add(call);
}
}
当正在运行的异步请求队列中的数量小于64并且正在运行的请求主机数小于5时则把请求加载到runningAsyncCalls中并在线程池中执行,否则就再入到readyAsyncCalls中进行缓存等待。
AsyncCall 是Call的内部类,final修饰,继承了NamedRunnable,而NamedRunnable 实现了Runnable接口,可以指定线程的名称。NamedRunnbale提供了一个抽象方法execute()来供AsyncCall 实现,我们看下AsyncCall 的实现逻辑:
@Override protected void execute() {
boolean signalledCallback = false;
try {
//同样调用getResponseWithInterceptorChain方法来获取Response对象
Response response = getResponseWithInterceptorChain(forWebSocket);
if (canceled) {
signalledCallback = true;
//取消执行的时候调用
responseCallback.onFailure(originalRequest, new IOException("Canceled"));
} else {
signalledCallback = true;
//执行成功的回调
responseCallback.onResponse(response);
}
} catch (IOException e) {
if (signalledCallback) {
// Do not signal the callback twice!
logger.log(Level.INFO, "Callback failure for " + toLoggableString(), e);
} else {
//网络请求失败的时候
responseCallback.onFailure(engine.getRequest(), e);
}
} finally {
//从dispatcher删除本次Call
client.getDispatcher().finished(this);
}
}
}
当AsyncCall中的execute执行的时候,代码会走到finally块,OkHttpClient 调用getDispatcher()方法获取Dispathcer对象,然后调用finished方法。下面我们看一下finished方法里面的代码逻辑:
synchronized void finished(AsyncCall call) {
//从正在执行队列中移除本次Call
if (!runningCalls.remove(call)) throw new AssertionError("AsyncCall wasn't running!");
///把准备队列中的请求加入到执行队列中,并且执行其他请求
promoteCalls();
}
finished方法将此次请求从runningAsyncCalls移除后还执行了promoteCalls方法:
//把准备队列中的请求加入到执行队列中,并且执行其他请求
private void promoteCalls() {
//判断是否超过了最大的请求数量(默认为64个)
if (runningCalls.size() >= maxRequests) return; // Already running max capacity.
if (readyCalls.isEmpty()) return; // No ready calls to promote.
//遍历准备队列
for (Iterator i = readyCalls.iterator(); i.hasNext(); ) {
AsyncCall call = i.next();
if (runningCallsForHost(call) < maxRequestsPerHost) {
i.remove();
//把准备队列中的请求加入到执行队列中
runningCalls.add(call);
//调用线程池,执行本次线线程
getExecutorService().execute(call);
}
//执行队列已经满了,不再添加执行任务。
if (runningCalls.size() >= maxRequests) return; // Reached max capacity.
}
}
可以看到最关键的点就是会从readyAsyncCalls取出下一个请求,并加入runningAsyncCalls中并交由线程池处理。
好了让我们再回到上面的AsyncCall的execute方法,我们会发现OkHttpClient同步请求和异步请求调用的接口不一样,但它们最后都是殊途同归地走到Call里面的getResponseWithInterceptorChain,很明显这是在请求网络。
private Response getResponseWithInterceptorChain(boolean forWebSocket) throws IOException {
//实例化一个拦截器
Interceptor.Chain chain = new ApplicationInterceptorChain(0, originalRequest, forWebSocket);
//调用proceed方法获取Response对象
return chain.proceed(originalRequest);
}
创建了一个ApplicationInterceptorChain ,并且第一个参数传入0,这个0是有特殊用法的,涉及到OKHttp里面的一个功能叫做拦截器,从getResponseWithInterceptorChain这个名字里其实也能看出一二。先看看proceed做了什么:
@Override public Response proceed(Request request) throws IOException {
//是否有请求之前的拦截器,如果有先执行拦截器
if (index < client.interceptors().size()) {
// There's another interceptor in the chain. Call that.
Interceptor.Chain chain = new ApplicationInterceptorChain(index + 1, request, forWebSocket);
return client.interceptors().get(index).intercept(chain);
} else {
// No more interceptors. Do HTTP.
//当前没有拦截器,调用getResponse方法返回Response对象
return getResponse(request, forWebSocket);
}
}
proceed方法每次从拦截器列表中取出拦截器,当存在多个拦截器时都会在第6行阻塞,并等待下一个拦截器的调用返回。下面分别以 拦截器链中有1个、2个拦截器的场景加以模拟:
注:这里碰到一个拦截器,OKHttp增加了一个拦截器机制,拦截器主要用来观察,修改以及可能拦截请求输出和响应的回来。通常情况下拦截器用来添加,移除或者转换请求或者响应的头部信息。来看看官方文档里提供的例子
首先自定义一个拦截器用于打印一些发送信息
class LoggingInterceptor implements Interceptor {
@Override public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
long t1 = System.nanoTime();
logger.info(String.format("Sending request %s on %s%n%s", request.url(), chain.connection(), request.headers()));
Response response = chain.proceed(request);
long t2 = System.nanoTime();
logger.info(String.format("Received response for %s in %.1fms%n%s", response.request().url(), (t2 - t1) / 1e6d, response.headers()));
return response;
}
}
然后使用的时候把它添加到okhttpclient
OkHttpClient client = new OkHttpClient();
client.interceptors().add(new LoggingInterceptor());
Request request = new Request.Builder()
.url("http://www.publicobject.com/helloworld.txt")
.header("User-Agent", "OkHttp Example")
.build();
Response response = client.newCall(request).execute();
response.body().close();
当我们执行到proceed,就会去判断是否有拦截器有的话先执行拦截器里的intercept,而在intercept里一般会进行一些自定义操作并且调用procced去判断是否要继续执行拦截器操作还是直接去获取网络请求,上面的例子执行结果如下:
INFO: Sending request http://www.publicobject.com/helloworld.txt on null
User-Agent: OkHttp Example
INFO: Received response for https://publicobject.com/helloworld.txt in 1179.7ms
Server: nginx/1.4.6 (Ubuntu)
Content-Type: text/plain
Content-Length: 1759
Connection: keep-alive
拦截器的整个机制如下图
上图中把拦截器分为应用拦截器和网络拦截器,其实这个取决于你在拦截器里做了哪方面的操作,比如改变请求头部之类的就可以使用网络拦截器。
在处理完拦截器操作后,就进入到重要的getResponse方法,真正的去进行发送请求,处理请求,接收返回结果。
Response getResponse(Request request, boolean forWebSocket) throws IOException {
// Copy body metadata to the appropriate request headers.
//post有http请求body
RequestBody body = request.body();
if (body != null) {
//完善http请求头信息添加Content-Type,Content-Length
Request.Builder requestBuilder = request.newBuilder();
MediaType contentType = body.contentType();
if (contentType != null) {
requestBuilder.header("Content-Type", contentType.toString());
}
//获取请求body的长度
long contentLength = body.contentLength();
if (contentLength != -1) {
//body长度确定 一次性发送给服务器
requestBuilder.header("Content-Length", Long.toString(contentLength));
requestBuilder.removeHeader("Transfer-Encoding");
} else {
//body长度不确定设置标记为Transfer-Encoding:chunked
requestBuilder.header("Transfer-Encoding", "chunked");
requestBuilder.removeHeader("Content-Length");
}
request = requestBuilder.build();
}
// Create the initial HTTP engine. Retries and redirects need new engine for each attempt.
//初始化Http 引擎,重新连接,重定向都需要一个新的引擎。
engine = new HttpEngine(client, request, false, false, forWebSocket, null, null, null, null);
//记录重新请求的次数
int followUpCount = 0;
while (true) {
//是否已经取消执行
if (canceled) {
//释放连接
engine.releaseConnection();
throw new IOException("Canceled");
}
try {
//发起请求
engine.sendRequest();
//获取响应
engine.readResponse();
} catch (RequestException e) {
// The attempt to interpret the request failed. Give up.
throw e.getCause();
} catch (RouteException e) {
// The attempt to connect via a route failed. The request will not have been sent.
//路由失败,视图重新构建HttpEngine进行连接
HttpEngine retryEngine = engine.recover(e);
if (retryEngine != null) {
//如果恢复过来重新发送请求,重新获取response对象
engine = retryEngine;
continue;
}
// Give up; recovery is not possible.
throw e.getLastConnectException();
} catch (IOException e) {
// An attempt to communicate with a server failed. The request may have been sent.
//网络连接失败从新获取连接
HttpEngine retryEngine = engine.recover(e, null);
if (retryEngine != null) {
//如果恢复过来重新发送请求,重新获取response对象
engine = retryEngine;
continue;
}
// Give up; recovery is not possible.
//抛出异常 无法重新连接
throw e;
}
//获取响应
Response response = engine.getResponse();
//获取重新请求对象
Request followUp = engine.followUpRequest();
//重新发情请求对象为空,说明无需重新构建请求,直接返回响应对象response
if (followUp == null) {
//没有采用websocket,无需进维护连接,释放连接
if (!forWebSocket) {
//释放连接
engine.releaseConnection();
}
//返回response退出循环
return response;
}
//如果重连的次数超过最大连接数这里默认是20,则抛出异常
if (++followUpCount > MAX_FOLLOW_UPS) {
throw new ProtocolException("Too many follow-up requests: " + followUpCount);
}
if (!engine.sameConnection(followUp.httpUrl())) {
engine.releaseConnection();
}
Connection connection = engine.close();
request = followUp;
//从新构建http引擎
engine = new HttpEngine(client, request, false, false, forWebSocket, connection, null, null,
response);
}
}
代码比较多,看重点,可以看到如果是post请求,先做一定的头部处理,然后新建一个HttpEngine去处理具体的操作,通过sendRequest发送具体请求操作,readResponse对服务器的答复做一定处理,在代码70行处getResponse得到从服务器返回的Response。我们先来看看sendRequest方法:
public void sendRequest() throws RequestException, RouteException, IOException {
if (cacheStrategy != null) return; // Already sent.
if (httpStream != null) throw new IllegalStateException();
//请求头部添加
Request request = networkRequest(userRequest);
//获取client中的Cache,同时Cache在初始化的时候会去读取缓存目录中关于曾经请求过的所有信息。
InternalCache responseCache = Internal.instance.internalCache(client);
//cacheCandidate为上次与服务器交互缓存的Response
Response cacheCandidate = responseCache != null
? responseCache.get(request)
: null;
long now = System.currentTimeMillis();
//创建CacheStrategy.Factory对象,进行缓存配置
cacheStrategy = new CacheStrategy.Factory(now, request, cacheCandidate).get();
//网络请求
networkRequest = cacheStrategy.networkRequest;
//缓存的响应
cacheResponse = cacheStrategy.cacheResponse;
if (responseCache != null) {
//记录当前请求是网络发起还是缓存发起
responseCache.trackResponse(cacheStrategy);
}
if (cacheCandidate != null && cacheResponse == null) {
closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
}
//不进行网络请求并且缓存不存在或者过期则返回504错误
if (networkRequest == null && cacheResponse == null) {
userResponse = new Response.Builder()
.request(userRequest)
.priorResponse(stripBody(priorResponse))
.protocol(Protocol.HTTP_1_1)
.code(504)
.message("Unsatisfiable Request (only-if-cached)")
.body(EMPTY_BODY)
.build();
return;
}
// 不进行网络请求,而且缓存可以使用,直接返回缓存
if (networkRequest == null) {
userResponse = cacheResponse.newBuilder()
.request(userRequest)
.priorResponse(stripBody(priorResponse))
.cacheResponse(stripBody(cacheResponse))
.build();
userResponse = unzip(userResponse);
return;
}
//需要访问网络时
boolean success = false;
try {
httpStream = connect();
httpStream.setHttpEngine(this);
if (writeRequestHeadersEagerly()) {
long contentLength = OkHeaders.contentLength(request);
if (bufferRequestBody) {
if (contentLength > Integer.MAX_VALUE) {
throw new IllegalStateException("Use setFixedLengthStreamingMode() or "
+ "setChunkedStreamingMode() for requests larger than 2 GiB.");
}
if (contentLength != -1) {
// Buffer a request body of a known length.
httpStream.writeRequestHeaders(networkRequest);
requestBodyOut = new RetryableSink((int) contentLength);
} else {
// Buffer a request body of an unknown length. Don't write request headers until the
// entire body is ready; otherwise we can't set the Content-Length header correctly.
requestBodyOut = new RetryableSink();
}
} else {
httpStream.writeRequestHeaders(networkRequest);
requestBodyOut = httpStream.createRequestBody(networkRequest, contentLength);
}
}
success = true;
} finally {
// If we're crashing on I/O or otherwise, don't leak the cache body.
if (!success && cacheCandidate != null) {
closeQuietly(cacheCandidate.body());
}
}
}
上面的代码显然是在发送请求,但是最主要的是做了缓存的策略。cacheCandidate是上次与服务器交互缓存的Response,这里的缓存都是基于Map,key是请求中url的md5,value是在文件中查询到的缓存,页面置换基于LRU算法,我们现在只需要知道它是一个可以读取缓存Header的Response即可。根据cacheStrategy的处理得到了networkRequest和cacheResponse这两个值,根据这两个值的数据是否为null来进行进一步的处理,当networkRequest和cacheResponse都为null的情况也就是不进行网络请求并且缓存不存在或者过期,这时候则返回504错误;当networkRequest 为null时也就是不进行网络请求,而且缓存可以使用时则直接返回缓存;其他的情况则请求网络。
接下来我们查看readResponse方法:
public void readResponse() throws IOException {
...省略
else{
//读取网络响应
networkResponse = readNetworkResponse();
}
//将响应头部存入Cookie中
receiveHeaders(networkResponse.headers());
// If we have a cache response too, then we're doing a conditional get.
if (cacheResponse != null) {
//检查缓存是否可用,如果可用。那么就用当前缓存的Response,关闭网络连接,释放连接。
if (validate(cacheResponse, networkResponse)) {
userResponse = cacheResponse.newBuilder()
.request(userRequest)
.priorResponse(stripBody(priorResponse))
.headers(combine(cacheResponse.headers(), networkResponse.headers()))
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
networkResponse.body().close();
releaseStreamAllocation();
// Update the cache after combining headers but before stripping the
// Content-Encoding header (as performed by initContentStream()).
InternalCache responseCache = Internal.instance.internalCache(client);
responseCache.trackConditionalCacheHit();
// 更新缓存
responseCache.update(cacheResponse, stripBody(userResponse));
userResponse = unzip(userResponse);
return;
} else {
closeQuietly(cacheResponse.body());
}
}
userResponse = networkResponse.newBuilder()
.request(userRequest)
.priorResponse(stripBody(priorResponse))
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
if (hasBody(userResponse)) {
maybeCache();
userResponse = unzip(cacheWritingResponse(storeRequest, userResponse));
}
}
这个方法发起刷新请求头部和请求体,解析HTTP响应头部。如果有缓存并且可用则用缓存的数据并更新缓存,否则就用网络请求返回的数据。
我们再来看看validate(cacheResponse, networkResponse)方法是如何判断缓存是否可用的:
private static boolean validate(Response cached, Response network) {
//如果服务器返回304则缓存有效
if (network.code() == HTTP_NOT_MODIFIED) {
return true;
}
//通过缓存和网络请求响应中的Last-Modified来计算是否是最新数据,如果是则缓存有效
Date lastModified = cached.headers().getDate("Last-Modified");
if (lastModified != null) {
Date networkLastModified = network.headers().getDate("Last-Modified");
if (networkLastModified != null
&& networkLastModified.getTime() < lastModified.getTime()) {
return true;
}
}
return false;
}
如缓存果过期或者强制放弃缓存,在此情况下,缓存策略全部交给服务器判断,客户端只用发送条件get请求即可,如果缓存是有效的,则返回304 Not Modifiled,否则直接返回body。条件get请求有两种方式一种是Last-Modified-Date,一种是 ETag。这里采用了Last-Modified-Date,通过缓存和网络请求响应中的Last-Modified来计算是否是最新数据,如果是则缓存有效。
最后我们再回到它的getResponse方法
Response getResponse(Request request, boolean forWebSocket) throws IOException {
...省略
boolean releaseConnection = true;
try {
engine.sendRequest();
engine.readResponse();
releaseConnection = false;
} catch (RequestException e) {
// The attempt to interpret the request failed. Give up.
throw e.getCause();
} catch (RouteException e) {
// The attempt to connect via a route failed. The request will not have been sent.
HttpEngine retryEngine = engine.recover(e.getLastConnectException(), null);
if (retryEngine != null) {
releaseConnection = false;
engine = retryEngine;
continue;
}
// Give up; recovery is not possible.
throw e.getLastConnectException();
} catch (IOException e) {
// An attempt to communicate with a server failed. The request may have been sent.
HttpEngine retryEngine = engine.recover(e, null);
if (retryEngine != null) {
releaseConnection = false;
engine = retryEngine;
continue;
}
// Give up; recovery is not possible.
throw e;
} finally {
// We're throwing an unchecked exception. Release any resources.
if (releaseConnection) {
StreamAllocation streamAllocation = engine.close();
streamAllocation.release();
}
}
...省略
engine = new HttpEngine(client, request, false, false, forWebSocket, streamAllocation, null,
response);
}
}
查看代码第11行和21行当发生IOException或者RouteException时会执行HttpEngine的recover方法:
public HttpEngine recover(IOException e, Sink requestBodyOut) {
if (!streamAllocation.recover(e, requestBodyOut)) {
return null;
}
if (!client.retryOnConnectionFailure()) {
return null;
}
StreamAllocation streamAllocation = close();
// For failure recovery, use the same route selector with a new connection.
return new HttpEngine(client, userRequest, bufferRequestBody, callerWritesRequestBody,
forWebSocket, streamAllocation, (RetryableSink) requestBodyOut, priorResponse);
}
最后一行可以看到就是重新创建了HttpEngine并返回,用来完成失败重连。
到这里OkHttp请求网络的流程基本上讲完了,下面是关于OKHttp的请求流程图:
延伸:
底层如何建立连接和服务器进行交互?
这里要使用到一个关键的接口:Transport
可以看出该接口有两个实现类,一个是HttpTransport,支持用来http协议。另一个是FramedTransport用来支持spdy协议。该接口定义了一系列的方法来支持我们向服务器发送请求头、请求体、创建请求体等。
HttpTransport:
public HttpTransport(HttpEngine httpEngine, HttpConnection httpConnection) {
//传入HttpEngine 对象
this.httpEngine = httpEngine;
//传入HttpConnection 对象
this.httpConnection = httpConnection;
}
FramedTransport:
public FramedTransport(HttpEngine httpEngine, FramedConnection framedConnection) {
//传入HttpEngine 对象
this.httpEngine = httpEngine;
//传入HttpConnection 对象
this.framedConnection = framedConnection;
}
我们可以看到实际上Transport实现类的相关方法是调用HttpConnection(支持Http协议)或FramedConnection(支持spdy协议)来发送请求头、请求体、获取响应对象的。
OKHttpClient是一个基于java优秀的网络请求框架,
当你的网络出现拥挤的时候,就是OkHttp大显身手的时候, 它可以避免常见的网络问题,如果你的服务是部署在不同的IP上面的,如果第一个连接失败, OkHttp会尝试其他的连接. 这个对现在IPv4+IPv6中常见的把服务冗余部署在不同的数据中心上. OkHttp将使用现在TLS特性(SNI ALPN) 来初始化新的连接. 如果握手失败, 将切换到SLLv3使用OkHttp很容易, 同时支持异步阻塞请求和回调。我们可以结合自己的项目合理应用起来帮助我们构建健壮的应用程序。