文章目录
  1. 1. 查找原因
  2. 2. 解决方法

前几天使用Fresco加载图片时遇到图片加载不出来的问题,然后调试了一下,发现出现如下错误:

1
javax.net.ssl.SSLException: Connection closed by peer

debug时的截图如下图所示:

SSLException截图

查找原因

上面的截图是在给Fresco中的展示图片的类SimpleDraweeView调用如下代码时出现的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// SimpileDraweeView sdv = ...;
sdv.setController(Fresco.newDraweeControllerBuilder().setControllerListener(new BaseControllerListener<ImageInfo>() {
@Override
public void onFinalImageSet(String id, ImageInfo imageInfo, Animatable animatable) {
File file = getCacheFile(request);
if (file != null && callback != null) {
callback.onImageLoadFinish(file);
}
}

@Override
public void onFailure(String id, Throwable throwable) {
// 这里throwable就是javax.net.ssl.SSLException: Connection closed by peer
super.onFailure(id, throwable);
if (callback != null) {
callback.onFailure(uri, throwable);
}
}
})
.setImageRequest(request)
.build());

看到上面的错误提示,马上反应是不是因为https证书的原因?原来请求的图片链接是https地址。到网上一搜,果然,Fresco默认不支持https,需要自己设置。一看网上的解决方法,都是使用Okhttp库来解决的,由于项目比较老,还没有使用Okhttp库,总不能为了解决这个问题而引入一个库吧?感觉代价太大了。所以跑去看了一下Okhttp默认请求加载图片的实现。

Okhttp库加载图片的网络请求处理在这个类com.facebook.imagepipeline.producers.HttpUrlConnectionNetworkFetcher中,关键的请求处理是在downloadFrom(Uri uri, int maxRedirects)这个方法中,代码如下所示:

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
private HttpURLConnection downloadFrom(Uri uri, int maxRedirects) throws IOException {
HttpURLConnection connection = openConnectionTo(uri);
int responseCode = connection.getResponseCode(); // 请求是在这一步发出的,在这之前设置HTTPS即可

if (isHttpSuccess(responseCode)) {
return connection;

} else if (isHttpRedirect(responseCode)) {
String nextUriString = connection.getHeaderField("Location");
connection.disconnect();

Uri nextUri = (nextUriString == null) ? null : Uri.parse(nextUriString);
String originalScheme = uri.getScheme();

if (maxRedirects > 0 && nextUri != null && !nextUri.getScheme().equals(originalScheme)) {
return downloadFrom(nextUri, maxRedirects - 1);
} else {
String message = maxRedirects == 0
? error("URL %s follows too many redirects", uri.toString())
: error("URL %s returned %d without a valid redirect", uri.toString(), responseCode);
throw new IOException(message);
}

} else {
connection.disconnect();
throw new IOException(String
.format("Image URL %s returned HTTP code %d", uri.toString(), responseCode));
}
}

@VisibleForTesting
static HttpURLConnection openConnectionTo(Uri uri) throws IOException {
URL url = new URL(uri.toString());
return (HttpURLConnection) url.openConnection();
}

解决方法

从上面的代码可以看出来,请求是从int responseCode = connection.getResponseCode();这行代码发出去的,但是又没有对https的证书进行处理,所以在这之前增加处理即可。于是想到继承这个类来扩展处理https的情况,结果从上面的代码来看,想从继承的角度增加https的处理是不可能的。干脆把整个类的代码复制一份,修改类名为HttpsUrlConnectionNetworkFetcher,修改downloadFrom这个方法,增加https证书处理,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

public class HttpsUrlConnectionNetworkFetcher extends BaseNetworkFetcher<FetchState> {

// 中间代码忽略...

private HttpURLConnection downloadFrom(Uri uri, int maxRedirects) throws IOException {
HttpURLConnection connection = openConnectionTo(uri);
// 增加https支持
String scheme = uri.getScheme();
if ("https".equals(scheme)) {
HttpsURLConnection httpsURLConnection = (HttpsURLConnection) connection;
SSLContext sslContext = SSLContextUtils.getSSLContext();
if (sslContext != null) {
SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
httpsURLConnection.setSSLSocketFactory(sslSocketFactory);
httpsURLConnection.setHostnameVerifier(SSLContextUtils.getHostnameVerifier());
}
}

int responseCode = connection.getResponseCode();
// 其它代码忽略...
}

}

SSLContextUtils类中忽略对证书的处理,信任所有证书,这种做法很不安全,仅供参考。代码如下所示:

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
import android.util.Log;

import java.security.SecureRandom;
import java.security.cert.X509Certificate;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

/**
* SSL工具类
*/
public class SSLContextUtils {

private static HostnameVerifier hostnameVerifier = new HostnameVerifier() {
@Override
public boolean verify(String hostname, SSLSession session) {
return true;
}
};

public static HostnameVerifier getHostnameVerifier() {
return hostnameVerifier;
}

public static SSLContext getSSLContext() {
SSLContext sslContext = null;
try {
sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, new TrustManager[]{new X509TrustManager() {
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) {}

@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) {}

@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
}}, new SecureRandom());
} catch (Exception e) {
Log.e("SSLContextUtils", e.getMessage(), e);
}
return sslContext;
}

}

最后需要把这个自定义的类的通过调用ImagePipelineConfig.Builder#setNetworkFetcher()方法设置进来,指定加载图片时使用新的网络请求策略。具体代码如下所示:

1
2
3
4
5
ImagePipelineConfig config = ImagePipelineConfig.newBuilder(context)
.setDownsampleEnabled(true)
.setNetworkFetcher(new HttpsUrlConnectionNetworkFetcher()) // 这里设置自定义类
.build();
Fresco.initialize(context, config);

再重新运行App,图片加载出来了,问题解决。

文章目录
  1. 1. 查找原因
  2. 2. 解决方法