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

目录
相关文章
|
开发框架 前端开发 Android开发
Flutter 与原生模块(Android 和 iOS)之间的通信机制,包括方法调用、事件传递等,分析了通信的必要性、主要方式、数据传递、性能优化及错误处理,并通过实际案例展示了其应用效果,展望了未来的发展趋势
本文深入探讨了 Flutter 与原生模块(Android 和 iOS)之间的通信机制,包括方法调用、事件传递等,分析了通信的必要性、主要方式、数据传递、性能优化及错误处理,并通过实际案例展示了其应用效果,展望了未来的发展趋势。这对于实现高效的跨平台移动应用开发具有重要指导意义。
1207 4
|
5月前
|
安全 数据库 Android开发
在Android开发中实现两个Intent跳转及数据交换的方法
总结上述内容,在Android开发中,Intent不仅是活动跳转的桥梁,也是两个活动之间进行数据交换的媒介。运用Intent传递数据时需注意数据类型、传输大小限制以及安全性问题的处理,以确保应用的健壯性和安全性。
365 11
|
7月前
|
存储 XML Java
Android 文件数据储存之内部储存 + 外部储存
简介:本文详细介绍了Android内部存储与外部存储的使用方法及核心原理。内部存储位于手机内存中,默认私有,适合存储SharedPreferences、SQLite数据库等重要数据,应用卸载后数据会被清除。外部存储包括公共文件和私有文件,支持SD卡或内部不可移除存储,需申请权限访问。文章通过代码示例展示了如何保存、读取、追加、删除文件以及将图片保存到系统相册的操作,帮助开发者理解存储机制并实现相关功能。
1795 2
|
9月前
|
API 数据处理 Android开发
Android网络请求演变:从Retrofit到Flow的转变过程。
通过这个比喻,我们解释了 Android 网络请求从 Retrofit 到 Flow 的转变过程。这不仅是技术升级的体现,更是反映出开发者在面对并发编程问题时,持续探索和迭求更好地解决方案的精神。未来,还会有更多新的技术和工具出现,我们期待一同 witness 这一切的发展。
264 36
|
10月前
|
前端开发 Java Shell
【08】flutter完成屏幕适配-重建Android,增加GetX路由,屏幕适配,基础导航栏-多版本SDK以及gradle造成的关于fvm的使用(flutter version manage)-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
【08】flutter完成屏幕适配-重建Android,增加GetX路由,屏幕适配,基础导航栏-多版本SDK以及gradle造成的关于fvm的使用(flutter version manage)-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
639 20
【08】flutter完成屏幕适配-重建Android,增加GetX路由,屏幕适配,基础导航栏-多版本SDK以及gradle造成的关于fvm的使用(flutter version manage)-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
|
开发工具 Android开发 开发者
Android平台如何不推RTMP|不发布RTSP流|不实时录像|不回传GB28181数据时实时快照?
本文介绍了一种在Android平台上实现实时截图快照的方法,尤其适用于无需依赖系统接口的情况,如在RTMP推送、RTSP服务或GB28181设备接入等场景下进行截图。通过底层模块(libSmartPublisher.so)实现了截图功能,封装了`SnapShotImpl.java`类来管理截图流程。此外,提供了关键代码片段展示初始化SDK实例、执行截图、以及在Activity销毁时释放资源的过程。此方案还考虑到了快照数据的灵活处理需求,符合GB/T28181-2022的技术规范。对于寻求更灵活快照机制的开发者来说,这是一个值得参考的设计思路。
302 1
|
存储 API Android开发
"解锁Android权限迷宫:一场惊心动魄的动态权限请求之旅,让你的应用从平凡跃升至用户心尖的宠儿!"
随着Android系统的更新,权限管理成为应用开发的关键。尤其在Android 6.0(API 级别 23)后,动态权限请求机制的引入提升了用户隐私保护,要求开发者进行更精细的权限管理。
291 2
|
存储 大数据 数据库
Android经典面试题之Intent传递数据大小为什么限制是1M?
在 Android 中,使用 Intent 传递数据时存在约 1MB 的大小限制,这是由于 Binder 机制的事务缓冲区限制、Intent 的设计初衷以及内存消耗和性能问题所致。推荐使用文件存储、SharedPreferences、数据库存储或 ContentProvider 等方式传递大数据。
612 0
|
JSON Java Android开发
Android 开发者必备秘籍:轻松攻克 JSON 格式数据解析难题,让你的应用更出色!
【8月更文挑战第18天】在Android开发中,解析JSON数据至关重要。JSON以其简洁和易读成为首选的数据交换格式。开发者可通过多种途径解析JSON,如使用内置的`JSONObject`和`JSONArray`类直接操作数据,或借助Google提供的Gson库将JSON自动映射为Java对象。无论哪种方法,正确解析JSON都是实现高效应用的关键,能帮助开发者处理网络请求返回的数据,并将其展示给用户,从而提升应用的功能性和用户体验。
319 1
|
编解码 网络协议 前端开发
如何实现Android平台GB28181设备接入模块按需打开摄像头并回传数据
后台采集摄像头,如果想再进一步扩展,可以把android平台gb28181的camera2 demo,都移植过来,实现功能更强大的国标设备侧,这里主要是展示,收到国标平台侧的回传请求后,才打开摄像头,才开始编码打包,最大限度的减少资源的占用
267 3