问题来源
focus 应用中核心的部分是,网络请求订阅的xml文件内容,然后解析xml文件存储到本地数据库。
这里网络请求我使用的是retrofit,返回的类型是String,所以使用的是ScalarsConverterFactory
的解析器。
*就会出现中文乱码问题。
解决方法
给okhttp添加拦截器
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; /** * <pre> * time : 2019/05/23 * desc : * version: 1.0 * </pre> */ 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<T> 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<T> 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<T> 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()
是否是乱码,如果是,才二次编码,否则就不需要二次处理了。