focus 应用中核心的部分是,网络请求订阅的xml文件内容,然后解析xml文件存储到本地数据库。
这里网络请求我使用的是retrofit,返回的类型是String,所以使用的是ScalarsConverterFactory
的解析器。
*就会出现中文乱码问题。
EncodingInterceptor.java
package com.ihewro.focus.helper;
import com.blankj.ALog;
import com.ihewro.focus.util.StringUtil;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.net.SocketTimeoutException;
import java.util.logging.Logger;
import okhttp3.Headers;
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
/**
*
* time : 2019/05/23
* desc :
* version: 1.0
*
*/
public class EncodingInterceptor implements Interceptor {
/**
* 自定义编码
*/
private String encoding;
public EncodingInterceptor(String encoding) {
this.encoding = encoding;
}
@Override public Response intercept(Interceptor.Chain chain) throws IOException {
Request request = chain.request();
Response response = chain.proceed(request);
settingClientCustomEncoding(response);
return response;
}
/**
* setting client custom encoding when server not return encoding
* @param response
* @throws IOException
*/
private void settingClientCustomEncoding(Response response) throws IOException {
setBodyContentType(response);
}
/**
* set body contentType
* @param response
* @throws IOException
*/
private void setBodyContentType(Response response) throws IOException {
ResponseBody body = response.body();
// setting body contentTypeString using reflect
Class extends ResponseBody> aClass = body.getClass();
try {
Field field = aClass.getDeclaredField("contentTypeString");
field.setAccessible(true);
String contentTypeString = String.valueOf(field.get(body));
field.set(body, "application/rss+xml;charset=" + encoding);
} catch (NoSuchFieldException e) {
throw new IOException("use reflect to setting header occurred an error", e);
} catch (IllegalAccessException e) {
throw new IOException("use reflect to setting header occurred an error", e);
}
}
}
然后在okhttp里面拦截一下:
builder.addInterceptor(new EncodingInterceptor("ISO-8859-1"));//全部转换成这个编码
说明一下为什么全部转换成这个编码ISO-8859-1,(对全部转换,听我的,全部听我的) 因为这个编码可以后面再无损的转换成gbk或者utf-8。相反gbk和utf-8编码直接是无法转换的。
然后获取到response.body 之后,再进行对内容编码转换。
//获取xml文件的编码
String encode = "UTF-8";//默认编码
String originCode = "ISO-8859-1";
String temp = xmlStr.substring(0,100);
Pattern p = Pattern.compile("encoding=\"(.*?)\"");
Matcher m = p.matcher(temp);
boolean flag = m.find();//【部分匹配】,返回true or false,而且指针会移动下次匹配的位置
if (flag){
int begin = m.start()+10;
int end = m.end();
encode = temp.substring(begin,end-1);
ALog.d("编码:"+encode);
}//否则就是文件没有标明编码格式,按照utf-8进行解码
//如果文件没有乱码,则不需要转换(为什么需要这部分后面分析会写到)
if (!java.nio.charset.Charset.forName("GBK").newEncoder().canEncode(xmlStr.substring(0,Math.min(xmlStr.length(),3000)))){
xmlStr = new String(xmlStr.getBytes(originCode),encode);
}else {
}
正常的网页返回的时候会有一个返回头信息:
里面会标明返回字符串的编码。
有的网页不仅不返回这个编码信息,而且使用gbk编码。这retrofit就默认按照utf-8编码进行读取字符串了。
具体retrofit的执行流程如下:
//业务代码,同步请求
call.execute();
//跳转 retrofit2.OkHttpCall#execute
@Override public Response execute() throws IOException {
okhttp3.Call call;
...(对call的处理)
return parseResponse(call.execute());
}
可以看到对请求是两步处理了:
okhttp3.Response = call.execute()
parseResponse(okhttp3.Response)
我们先分析call.execute()
//okhttp3.RealCall#execute
@Override public Response execute() throws IOException {
...
Response result = getResponseWithInterceptorChain();
...
}
//okhttp3.RealCall#getResponseWithInterceptorChain
Response getResponseWithInterceptorChain() throws IOException {
// Build a full stack of interceptors.
...
return chain.proceed(originalRequest);
}
后面的proceed
根据okhttp一开始绑定的拦截器,进行链式处理服务器返回的内容。
public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
...
Interceptor interceptor = interceptors.get(index);
Response response = interceptor.intercept(next);
...
return response;
}
我使用了com.squareup.okhttp3:logging-interceptor:3.4.1
用来打印请求信息,所以会有下面的处理:
//okhttp3.logging.HttpLoggingInterceptor#intercept
@Override public Response intercept(Chain chain) throws IOException {
...
Charset charset = UTF8;
MediaType contentType = requestBody.contentType();
if (contentType != null) {
charset = contentType.charset(UTF8);
}
...
return response;
}
//okhttp3.MediaType#charset(java.nio.charset.Charset)
public @Nullable Charset charset(@Nullable Charset defaultValue) {
try {
return charset != null ? Charset.forName(charset) : defaultValue;
} catch (IllegalArgumentException e) {
return defaultValue; // This charset is invalid or unsupported. Give up.
}
}
其中requestBody.contentType()
,就是根据服务器返回的header中的content-Type
字段(如:content-type: text/html; charset=UTF-8)获取到结果编码。
如果这个字段中没有注明charset,则默认设置为utf-8
。
关于contentType()
函数 如何从content-Type字段获取到编码信息的,可以看下面的源码:
contentType()源码分析
至于第二步的parseResponse是根据第一步的处理结果进一步处理(比如根据状态码,不同处理等)。与问题无关则不再分析。
//retrofit2.OkHttpCall#parseResponse
Response parseResponse(okhttp3.Response rawResponse) throws IOException {
ResponseBody rawBody = rawResponse.body();
rawResponse = rawResponse.newBuilder()
.body(new NoContentResponseBody(rawBody.contentType(), rawBody.contentLength()))
.build();
...(请求结束的后续处理)
}
按照上面分析,我们只需要在一开始的拦截器,将服务器的contentTypeString
,修改为编码为ISO-8859-1
,然后再根据xml文件的encoding
字段标明的编码重新编码即可。
但是仍然有一种情况下会出现乱码:
服务器中content-type
字段包含utf-8编码信息,按照上面先转ISO-8858-1编码,再转回来,仍然乱码了。
经过debug,发现虽然设置ISO-8858-1的content-type字段,但是结果仍然是utf-8编码,这样的话按照ISO-8858-1解码再UTF-8编码一次就会出现乱码。
所以为什么结果在未二次编码前不是ISO-8858-1编码,而自动变成了utf-8编码了呢?
这一切要从parseResponse()
函数,我们前面忽略的部分说起,因为这类编码器正是在这个函数中开始工作的:
Response parseResponse(okhttp3.Response rawResponse) throws IOException {
ResponseBody rawBody = rawResponse.body();
// Remove the body's source (the only stateful object) so we can pass the response along.
rawResponse = rawResponse.newBuilder()
.body(new NoContentResponseBody(rawBody.contentType(), rawBody.contentLength()))
.build();
...
ExceptionCatchingResponseBody catchingBody = new ExceptionCatchingResponseBody(rawBody);
try {
T body = responseConverter.convert(catchingBody);
return Response.success(body, rawResponse);
} catch (RuntimeException e) {
catchingBody.throwIfCaught();
throw e;
}
}
我们使用的ScalarResponseBodyConverter
太智能了,它能根据服务器数据的字节流判断是否是utf系列编码
//retrofit2.converter.scalars.ScalarResponseBodyConverters.StringResponseBodyConverter#convert
@Override public String convert(ResponseBody value) throws IOException {
return value.string();
}
//okhttp3.ResponseBody#string
public final String string() throws IOException {
BufferedSource source = source();
try {
Charset charset = Util.bomAwareCharset(source, charset());
return source.readString(charset);
} finally {
Util.closeQuietly(source);
}
}
//okhttp3.internal.Util#bomAwareCharset
public static Charset bomAwareCharset(BufferedSource source, Charset charset) throws IOException {
if (source.rangeEquals(0, UTF_8_BOM)) {
source.skip(UTF_8_BOM.size());
return UTF_8;
}
if (source.rangeEquals(0, UTF_16_BE_BOM)) {
source.skip(UTF_16_BE_BOM.size());
return UTF_16_BE;
}
if (source.rangeEquals(0, UTF_16_LE_BOM)) {
source.skip(UTF_16_LE_BOM.size());
return UTF_16_LE;
}
if (source.rangeEquals(0, UTF_32_BE_BOM)) {
source.skip(UTF_32_BE_BOM.size());
return UTF_32_BE;
}
if (source.rangeEquals(0, UTF_32_LE_BOM)) {
source.skip(UTF_32_LE_BOM.size());
return UTF_32_LE;
}
return charset;
}
所以,我们需要在二次编码前判断response.body()
是否是乱码,如果是,才二次编码,否则就不需要二次处理了。