Volley默认请求多次原因解析

今天在提交数据到后台接口时遇到一个奇怪问题,服务端同学通过后台log发现偶尔会打印两次一模一样的请求,时间间隔一般只有几秒钟。刚开始我感觉应该是在用户点击完Button之后没有把Button的状态置为false,在等待服务端返回数据期间用户又点击了一次Button,后来查看代码,发现在onClick()里已经调用了setEnabled(false),并且在收到服务器响应后就会立即跳转Fragment,所以个人感觉不存在多次点击Button的情况。为了定位问题,在onClick()里生成了一个唯一的UUID,当做请求的参数带上,问题复现后,查看服务端log发现接收到的UUID竟然是一样的,这就验证了自己的想法,即:多次请求并不是多次点击Button导致的。那么就只有一个可能了,就是一个请求被发送了两次。我们用的网络请求库是Volley,为了追踪问题,想起了那句经典的RTFSC(read the fucking source code)。

Volley的网络请求都是调用的BasicNetwork的performRequest()方法获取服务器响应。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
@Override
public NetworkResponse performRequest(Request<?> request) throws VolleyError {
long requestStart = SystemClock.elapsedRealtime();
while (true) {
HttpResponse httpResponse = null;
byte[] responseContents = null;
Map<String, String> responseHeaders = Collections.emptyMap();
try {
// Gather headers.
Map<String, String> headers = new HashMap<String, String>();
addCacheHeaders(headers, request.getCacheEntry());
httpResponse = mHttpStack.performRequest(request, headers);
StatusLine statusLine = httpResponse.getStatusLine();
int statusCode = statusLine.getStatusCode();
responseHeaders = convertHeaders(httpResponse.getAllHeaders());
// Handle cache validation.
if (statusCode == HttpStatus.SC_NOT_MODIFIED) {
Entry entry = request.getCacheEntry();
if (entry == null) {
return new NetworkResponse(HttpStatus.SC_NOT_MODIFIED, null,
responseHeaders, true,
SystemClock.elapsedRealtime() - requestStart);
}
// A HTTP 304 response does not have all header fields. We
// have to use the header fields from the cache entry plus
// the new ones from the response.
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5
entry.responseHeaders.putAll(responseHeaders);
return new NetworkResponse(HttpStatus.SC_NOT_MODIFIED, entry.data,
entry.responseHeaders, true,
SystemClock.elapsedRealtime() - requestStart);
}
// Some responses such as 204s do not have content. We must check.
if (httpResponse.getEntity() != null) {
responseContents = entityToBytes(httpResponse.getEntity());
} else {
// Add 0 byte response as a way of honestly representing a
// no-content request.
responseContents = new byte[0];
}
// if the request is slow, log it.
long requestLifetime = SystemClock.elapsedRealtime() - requestStart;
logSlowRequests(requestLifetime, request, responseContents, statusLine);
if (statusCode < 200 || statusCode > 299) {
throw new IOException();
}
return new NetworkResponse(statusCode, responseContents, responseHeaders, false,
SystemClock.elapsedRealtime() - requestStart);
} catch (SocketTimeoutException e) {
attemptRetryOnException("socket", request, new TimeoutError());
} catch (ConnectTimeoutException e) {
attemptRetryOnException("connection", request, new TimeoutError());
} catch (MalformedURLException e) {
throw new RuntimeException("Bad URL " + request.getUrl(), e);
} catch (IOException e) {
int statusCode;
if (httpResponse != null) {
statusCode = httpResponse.getStatusLine().getStatusCode();
} else {
throw new NoConnectionError(e);
}
VolleyLog.e("Unexpected response code %d for %s", statusCode, request.getUrl());
NetworkResponse networkResponse;
if (responseContents != null) {
networkResponse = new NetworkResponse(statusCode, responseContents,
responseHeaders, false, SystemClock.elapsedRealtime() - requestStart);
if (statusCode == HttpStatus.SC_UNAUTHORIZED ||
statusCode == HttpStatus.SC_FORBIDDEN) {
attemptRetryOnException("auth",
request, new AuthFailureError(networkResponse));
} else if (statusCode >= 400 && statusCode <= 499) {
// Don't retry other client errors.
throw new ClientError(networkResponse);
} else if (statusCode >= 500 && statusCode <= 599) {
if (request.shouldRetryServerErrors()) {
attemptRetryOnException("server",
request, new ServerError(networkResponse));
} else {
throw new ServerError(networkResponse);
}
} else {
// 3xx? No reason to retry.
throw new ServerError(networkResponse);
}
} else {
attemptRetryOnException("network", request, new NetworkError());
}
}
}
}

代码有点长,但文档注释很详细,逻辑很清晰。大致的过程为:添加header到request–>发送http–>解析response–>返回。这个请求是循环执行的,直到收到服务端响应,或者抛出异常。在异常处理中,看到有一个attemptRetryOnException()方法,突然想起了以前阅读Volley源码时依稀记得Volley默认有重试的机制,顿时感觉豁然开朗,赶紧看一下这个方法。

阅读全文

Picasso源码解析

接触Android开发的应该对图片加载这块都有过接触,也比较头疼,稍微处理不当就会遇到OOM。这两天在看Picasso,感觉收获很大,在这里记录一下。

1
2
3
4
Picasso
.with(getActivity())
.load("http://i1.mifile.cn/a1/T1pZJgBbZT1RXrhCrK.jpg")
.into(imageView);

这是Picasso最简单的用法,先从入口看。

1
2
3
4
5
6
7
8
9
10
public static Picasso with(Context context) {
if (singleton == null) {
synchronized (Picasso.class) {
if (singleton == null) {
singleton = new Builder(context).build();
}
}
}
return singleton;
}

非常标准的双重检验锁单例模式实现。Picasso实例是通过构造者模式生成的,进去看一下build()方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public Picasso build() {
Context context = this.context;
if (downloader == null) {
downloader = Utils.createDefaultDownloader(context);
}
if (cache == null) {
cache = new LruCache(context);
}
if (service == null) {
service = new PicassoExecutorService();
}
if (transformer == null) {
transformer = RequestTransformer.IDENTITY;
}
Stats stats = new Stats(cache);
Dispatcher dispatcher = new Dispatcher(context, service, HANDLER, downloader, cache, stats);
return new Picasso(context, dispatcher, cache, listener, transformer, requestHandlers, stats, defaultBitmapConfig, indicatorsEnabled, loggingEnabled);
}
}

这里简单说就是生成Picasso构造方法所需要的各个参数,有Downloader(图片下载器)、LruCache(LRU缓存算法实现)、PicassoExecutorService(线程池)、Dispatcher(任务调度)等,这些在后面的图片加载过程中都会用到。

到这儿,Picasso实例已经生成了,再看load()方法:

1
2
3
4
5
6
7
8
9
public RequestCreator load(String path) {
if (path == null) {
return new RequestCreator(this, null, 0);
}
if (path.trim().length() == 0) {
throw new IllegalArgumentException("Path must not be empty.");
}
return load(Uri.parse(path));
}

返回一个RequestCreator对象。看一下它的into()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
public void into(ImageView target) {
into(target, null);
}
public void into(ImageView target, Callback callback) {
long started = System.nanoTime();
checkMain();
if (target == null) {
throw new IllegalArgumentException("Target must not be null.");
}
if (!data.hasImage()) {
picasso.cancelRequest(target);
if (setPlaceholder) {
setPlaceholder(target, getPlaceholderDrawable());
}
return;
}
if (deferred) {
if (data.hasSize()) {
throw new IllegalStateException("Fit cannot be used with resize.");
}
int width = target.getWidth();
int height = target.getHeight();
if (width == 0 || height == 0) {
if (setPlaceholder) {
setPlaceholder(target, getPlaceholderDrawable());
}
picasso.defer(target, new DeferredRequestCreator(this, target, callback));
return;
}
data.resize(width, height);
}
Request request = createRequest(started);
String requestKey = createKey(request);
if (shouldReadFromMemoryCache(memoryPolicy)) {
Bitmap bitmap = picasso.quickMemoryCacheCheck(requestKey);
if (bitmap != null) {
picasso.cancelRequest(target);
setBitmap(target, picasso.context, bitmap, MEMORY, noFade, picasso.indicatorsEnabled);
if (picasso.loggingEnabled) {
log(OWNER_MAIN, VERB_COMPLETED, request.plainId(), "from " + MEMORY);
}
if (callback != null) {
callback.onSuccess();
}
return;
}
}
if (setPlaceholder) {
setPlaceholder(target, getPlaceholderDrawable());
}
Action action =
new ImageViewAction(picasso, target, request, memoryPolicy, networkPolicy, errorResId,
errorDrawable, requestKey, tag, callback, noFade);
picasso.enqueueAndSubmit(action);
}

先检查是不是主线程,然后根据RUI生成一个requestKey,根据requestKey看这次请求的图片能不能在内存缓存中取到,能的话直接返回,没有的话根据相关属性生成一个ImageViewAction,并调用Picasso的enqueueAndSubmit()方法,把生成的ImageViewAction传过去。

阅读全文

Android布局优化

  • 去除不必要的嵌套和节点
    这是最基本的一条,但也是最不好做到的一条,往往不注意的时候难免会一些嵌套等。

    • 首次不需要的节点设置为GONE或使用ViewStub.
    • 使用Relativelayout代替LinearLayout.
      平时写布局的时候要多注意,写完后可以通过Hierarchy Viewer或在手机上通过开发者选项中的显示布局边界来查看是否有不必要的嵌套。
  • 使用include
    include可以用于将布局中一些公共的部分提取出来。在需要的时候使用即可,比如项目中一些页面会用到风格统一的title bar
    include标签的layout属性指定所要包含的布局文件,也可以通过android:id或者一些其它的属性来覆盖被引入布局的根节点所对应的属性值。

    1
    2
    3
    <include
    layout="@layout/loading"
    android:id="@+id/loading_main" />

    loading.xml内容为:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <ProgressBar
    android:id="@+id/pb_loadiing"
    android:layout_width="28dip"
    android:layout_height="28dip"
    android:layout_centerInParent="true"
    android:indeterminateDrawable="@drawable/progressbar_anim_drawable" />
    <TextView
    android:layout_below="@id/pb_loadiing"
    android:layout_centerHorizontal="true"
    android:layout_marginTop="10dp"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="loading..."
    android:textSize="15sp" />
    </RelativeLayout>

阅读全文

批量更新Git项目脚本

在平时的工作中,遇到一些优秀的开源项目,如volley、picasso、okhttp等,如果想阅读它的源代码,我通常都会clone项目到本地的GitHub文件夹,这样大神们后面再提交更新的话,只需要git pull更新一下本地的项目就能做到和远程仓库的代码同步了。可是时间长了就会遇到一个问题,如果GitHub文件夹里的项目太多,更新的话每个文件夹进去执行git pull将会是一件很麻烦的事。于是,花了几分钟,写了个批量更新的脚本。

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/sh
for dir in $(ls -d */)
do
cd $dir
echo "into $dir"
if [ -d ".git" ]; then
git pull
elif [ -d ".svn" ]; then
svn update
fi
cd ..
done

代码比较简单,就是遍历文件夹,发现项目目录下有.git文件夹,则执行git pull。很容易理解。

还加入了对svn项目的支持。命名为update.sh,放到GitHub文件夹,添加执行权限,执行./update.sh就可以了。

从Volley看面向对象六大原则

在工作初期,我们可能会经常会有这样的感觉,自己的代码接口设计混乱、代码耦合较为严重、一个类的代码过多等等,自己回头看的时候都觉得汗颜。再看那些知名的开源库,它们大多有着整洁的代码、清晰简单的接口、职责单一的类,这个时候我们通常会捶胸顿足而感叹:什么时候老夫才能写出这样的代码!

在做开发的这些年中,我渐渐的感觉到,其实国内的一些初、中级工程师写的东西不规范或者说不够清晰的原因是缺乏一些指导原则。他们手中挥舞着面向对象的大旗,写出来的东西却充斥着面向过程的气味。也许是他们不知道有这些原则,也许是他们知道但是不能很好运用到实际代码中,亦或是他们没有在实战项目中体会到这些原则能够带来的优点,以至于他们对这些原则并没有足够的重视。

最近一直在看Android网络框架Volley的代码,从中学到了很多。今天就以剖析Volley为例来学习这六大面向对象的基本原则,体会它们带来的强大能量。

在此之前,有一点需要大家知道,熟悉这些原则不会让你写出优秀的代码,只是为你的优秀代码之路铺上了一层栅栏,在这些原则的指导下你才能避免陷入一些常见的代码泥沼,从而让你专心写出优秀的东西。

单一职责原则 ( Single Responsibility Principle )

1.1 简述

单一职责原则的英文名称是Single Responsibility Principle,简称是SRP,简单来说一个类只做一件事。这个设计原则备受争议却又及其重要的原则。只要你想和别人争执、怄气或者是吵架,这个原则是屡试不爽的。因为单一职责的划分界限并不是如马路上的行车道那么清晰,很多时候都是需要靠个人经验来界定。当然最大的问题就是对职责的定义,什么是类的职责,以及怎么划分类的职责。

试想一下,如果你遵守了这个原则,那么你的类就会划分得很细,每个类都有自己的职责。恩,这不就是高内聚、低耦合么! 当然,如何界定类的职责这需要你的个人经验了。

1.2 示例

在Volley中,我觉得很能够体现SRP原则的就是HttpStack这个类族了。HttpStack定义了一个执行网络请求的接口,代码如下 :

1
2
3
4
5
6
7
8
9
10
11
/**
* An HTTP stack abstraction.
*/
public interface HttpStack {
/**
* 执行Http请求,并且返回一个HttpResponse
*/
public HttpResponse performRequest(Request<?> request, Map<String, String> additionalHeaders)
throws IOException, AuthFailureError;
}

可以看到,HttpStack只有一个函数,清晰明了,它的职责就是执行网络请求并且返回一个Response。它的职责很单一,这样在需要修改执行网络请求的相关代码时我们只需要修改实现HttpStack接口的类,而不会影响其它的类的代码。如果某个类的职责包含有执行网络请求、解析网络请求、进行gzip压缩、封装请求参数等等,那么在你修改某处代码时你就必须谨慎,以免修改的代码影响了其它的功能。但是当职责单一的时候,你修改的代码能够基本上不影响其它的功能。这就在一定程度上保证了代码的可维护性。注意,单一职责原则并不是说一个类只有一个函数,而是说这个类中的函数所做的工作必须要是高度相关的,也就是高内聚。HttpStack抽象了执行网络请求的具体过程,接口简单清晰,也便于扩展。

我们知道,Api 9以下使用HttpClient执行网络请求会好一些,api 9及其以上则建议使用HttpURLConnection。这就需要执行网络请求的具体实现能够可扩展、可替换,因此我们对于执行网络请求这个功能必须要抽象出来,HttpStack就是这个职责的抽象。

1.3 优点

  • 类的复杂性降低,实现什么职责都有清晰明确的定义;
  • 可读性提高,复杂性降低,那当然可读性提高了;
  • 可维护性提高,可读性提高,那当然更容易维护了;
  • 变更引起的风险降低,变更是必不可少的,如果接口的单一职责做得好,一个接口修改只对相应的实现类有影响,对其他的接口无影响,这对系统的扩展性、维护性都有非常大的帮助。

阅读全文

HttpURLConnection实现HTTP文件上传

实现功能前我们先想办法弄明白文件上传过程中请求的完整格式。
先创建一个Spring MVC的服务,实现一个简单的Form表单文件上传功能。偏服务端方向,在这里就不累述了,其实Spring MVC的官方sample里集成了文件上传的demo,代码直接拿来用就行,这不是本篇文章的重点。
打开浏览器,访问localhost:8080,选择一个图片上传

同时用Charles抓包,点击upload按钮后整个请求的request如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
POST /upload HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (X11; Linux i586; rv:31.0) Gecko/20100101 Firefox/31.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://localhost:8080/upload
Cookie: CNZZDATA1258283573=524895976-1459330515-%7C1459476924; JSESSIONID=7863BDEF16DC5F5D91F72010F74D118F
Connection: keep-alive
Content-Type: multipart/form-data; boundary=---------------------------17158034114497953721118848252
Content-Length: 1026
-----------------------------17158034114497953721118848252
Content-Disposition: form-data; name="file"; filename="1394108530_download2.png"
Content-Type: image/png
�PNG××××××××××
-----------------------------17158034114497953721118848252--

阅读http协议的格式说明可知,header和body之间会有一个空行,所以1~11行为header的内容,13~18行为body的内容。在这里说明一下,第17行由于是上传文件的字节数据,存在乱码,所以粘贴到这里显示会有问题,删除了大部分的内容,完整格式可查看截图!

这是一个最标准的http POST Form 方式上传文件的请求原始协议内容,我们只要按照这个格式模拟请求,就可以实现文件上传功能了。

阅读全文