android retrofit 请求返回String数据中文乱码解决方案

简介: 问题来源focus 应用中核心的部分是,网络请求订阅的xml文件内容,然后解析xml文件存储到本地数据库。

问题来源

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()是否是乱码,如果是,才二次编码,否则就不需要二次处理了。

目录
相关文章
|
3月前
|
安全 API Android开发
Android网络和数据交互: 解释Retrofit库的作用。
Android网络和数据交互: 解释Retrofit库的作用。
38 0
|
7天前
|
Android开发 开发者
Android网络和数据交互: 请解释Android中的AsyncTask的作用。
Android&#39;s AsyncTask simplifies asynchronous tasks for brief background work, bridging UI and worker threads. It involves execute() for starting tasks, doInBackground() for background execution, publishProgress() for progress updates, and onPostExecute() for returning results to the main thread.
7 0
|
7天前
|
网络协议 安全 API
Android网络和数据交互: 什么是HTTP和HTTPS?在Android中如何进行网络请求?
HTTP和HTTPS是网络数据传输协议,HTTP基于TCP/IP,简单快速,HTTPS则是加密的HTTP,确保数据安全。在Android中,过去常用HttpURLConnection和HttpClient,但HttpClient自Android 6.0起被移除。现在推荐使用支持TLS、流式上传下载、超时配置等特性的HttpsURLConnection进行网络请求。
8 0
|
21天前
|
XML Java Android开发
Android每点击一次按钮就添加一条数据
Android每点击一次按钮就添加一条数据
22 1
|
1月前
|
JSON Android开发 数据格式
android 使用GSON 序列化对象出现字段被优化问题解决方案
android 使用GSON 序列化对象出现字段被优化问题解决方案
|
1月前
|
存储 Android开发 C++
【Android 从入门到出门】第五章:使用DataStore存储数据和测试
【Android 从入门到出门】第五章:使用DataStore存储数据和测试
31 3
|
2月前
|
JavaScript Java 数据安全/隐私保护
安卓逆向 -- POST数据解密
安卓逆向 -- POST数据解密
25 2
|
3月前
|
编解码 测试技术 开发工具
如何实现Android视音频数据对接到GB28181平台(SmartGBD)
如何实现Android视音频数据对接到GB28181平台(SmartGBD)
|
3月前
|
数据采集 编解码 图形学
Android平台Unity下如何通过WebCamTexture采集摄像头数据并推送至RTMP服务器或轻量级RTSP服务
Android平台Unity下如何通过WebCamTexture采集摄像头数据并推送至RTMP服务器或轻量级RTSP服务
101 0
|
3月前
|
JSON Java Android开发
Android网络和数据交互: 请解释Android中的JSON解析库,如Gson。
Android网络和数据交互: 请解释Android中的JSON解析库,如Gson。
24 0