大数跨境
0
0

搞定Android网络开发:协议、库、异步、安全、实战,5大维度解决你的所有困惑(上)

搞定Android网络开发:协议、库、异步、安全、实战,5大维度解决你的所有困惑(上) 图解Android开发
2025-10-29
0

网络通信是Android应用的核心能力之一,从基础的数据请求到复杂的长连接、文件传输,几乎所有功能性App都离不开对网络模块的设计与优化,然而,Android网络开发涉及的知识体系繁杂,从HTTP/HTTPS协议基础到原生HttpURLConnection的使用,从OkHttp/Retrofit等主流库的实战到协程、RxJava的异步线程管理,从网络缓存、安全防护到弱网优化、断点续传等实战场景,每个环节都需要开发者系统掌握才能应对实际开发中的问题。

本文围绕Android网络开发全链路,以“从0到1、由浅入深”为逻辑主线,通过问答形式拆解9大核心模块:从网络协议与权限配置的入门知识,到原生API与第三方库的对比使用,从数据序列化、异步处理的基础能力,到缓存策略、网络优化的进阶技巧,最终延伸至网络安全、WebSocket、文件传输等高级主题与实战场景,无论是刚接触Android网络开发的新手,还是需要梳理知识体系、解决复杂问题的资深开发者,都能通过本文的结构化问答,逐步构建完整的网络开发知识框架,避开常见坑点,掌握企业级开发中的最佳实践。

知识点汇总:

一、网络基础与Android网络入门

1.1、Android开发中涉及的常见网络协议有哪些?(如HTTP、HTTPS、TCP、UDP,简述各自适用场景)  

Android开发中常见的网络协议及适用场景如下:  

HTTP(HyperText Transfer Protocol,超文本传输协议):基于TCP的应用层协议,以明文形式传输数据,适用于对安全性要求不高的场景,如普通文本数据(新闻列表、非敏感配置信息)的获取或提交。  

HTTPS(HTTP Secure):HTTP的安全版本,通过SSL/TLS协议对传输数据加密,适用于所有涉及敏感信息的场景,如用户登录(账号密码)、支付(银行卡信息)、Token传输等,可防止数据被窃听或篡改。  

TCP(Transmission Control Protocol,传输控制协议):面向连接的传输层协议,提供可靠的、有序的、基于字节流的传输(通过三次握手建立连接、重传机制保证数据完整),适用于对数据完整性要求高的场景,如文件下载/上传(确保文件不丢失)、即时通讯的消息发送(确保消息不重复/遗漏)。  

UDP(User Datagram Protocol,用户数据报协议):无连接的传输层协议,不保证数据可靠性,但传输速度快、延迟低,适用于对实时性要求高于完整性的场景,如视频通话(允许偶尔丢包,避免卡顿)、语音聊天、实时游戏数据同步(如位置更新)。  

WebSocket:基于TCP的全双工通信协议,支持客户端与服务器双向实时通信(如聊天消息实时推送),适用于需要持续数据交互的场景(替代轮询,减少网络开销)。  

HTTP请求过程图解:

1.2、HTTP协议的基本组成是什么?(请求行、请求头、请求体,响应行、响应头、响应体分别包含哪些关键信息)  

HTTP协议的通信由请求(Request)和响应(Response)两部分组成,结构如下。

一:HTTP请求的组成

请求行(Request Line):包含3部分信息,格式为:请求方法、URL、协议版本。  

请求方法:如GET(获取资源)、POST(提交数据)、PUT(更新资源)、DELETE(删除资源)等。URL:请求的资源路径(如/api/user)。协议版本:如HTTP/1.1、HTTP/2.0。  

请求头(Request Headers):键值对形式的元数据,描述请求的附加信息,常见字段:  

Host:目标服务器域名(如api.example.com)。  User-Agent:客户端标识(如Android应用的包名)。Content-Type:请求体的数据格式(如application/json、application/x-www-form-urlencoded)。  Authorization:身份验证信息(如Token)。 Connection:是否保持连接(如keep-alive)。  

请求体(Request Body):可选,仅在POST、PUT等方法中存在,用于传递数据,格式由Content-Type决定,例如:  

JSON格式:{"name":"张三","age":20}。  表单格式:name=张三&age=20二进制数据(如文件上传)。  

Http请求报文图解:

二:HTTP响应的组成

响应行(Status Line):包含3部分信息,格式为:协议版本、状态码、状态描述。  

协议版本:如HTTP/1.1。

状态码:表示请求处理结果(如200成功、404资源不存在、500服务器错误)。 

状态描述:对状态码的文字解释(如OK、Not Found)。  

响应头(Response Headers):键值对形式的元数据,描述响应的附加信息,常见字段:  

Content-Type:响应体的数据格式(如application/json、image/jpeg)。Content-Length:响应体的长度(字节数)。Cache-Control:缓存策略(如max-age=3600表示缓存1小时)。Set-Cookie:服务器向客户端设置Cookie。Server:服务器标识(如nginx)。  

响应体(Response Body): 服务器返回的实际数据,格式由Content-Type决定,例如:  

JSON格式的接口数据:{"code":200,"data":{"name":"张三"}}。  

二进制数据(如图片、文件)。  

Http响应报文图解:

状态码图解:

1.3、HTTP 1.1、HTTP 2.0、HTTP 3.0的核心区别是什么?Android中如何适配不同HTTP版本?  

三者的核心区别主要体现在性能传输效率底层依赖上,具体如下:  

Android中的适配方式:  

Android网络库(如OkHttp、Retrofit)会自动适配服务器支持的最高HTTP版本,无需开发者手动指定,但需注意以下几点:  

HTTP 1.1:所有Android版本和网络库默认支持,无特殊配置。  

HTTP 2.0:需服务器支持,且客户端需满足:OkHttp 3.0+默认支持HTTP 2.0(需服务器启用TLS ALPN协商),Android 5.0(API 21)及以上系统原生支持HTTP 2.0,低版本需通过OkHttp的内置实现兼容。

HTTP 3.0:需服务器支持QUIC协议,客户端需:使用OkHttp 4.9+(需添加okhttp3-quic依赖),配置OkHttpClient启用HTTP 3.0:client = new OkHttpClient.Builder().protocols(Arrays.asList(Protocol.HTTP_3, Protocol.HTTP_2, Protocol.HTTP_1_1)).build()。  

1.4、HTTPS与HTTP的本质区别是什么?SSL/TLS协议在HTTPS中起到了什么作用?  

一:HTTPS与HTTP的本质区别  

安全性:HTTP以明文传输数据,易被窃听、篡改或伪造,HTTPS通过SSL/TLS协议对数据加密,确保传输过程安全。  

底层依赖:HTTP直接基于TCP,HTTPS是“HTTP + SSL/TLS”,即HTTP在SSL/TLS层之上传输(SSL/TLS基于TCP)。  

端口:HTTP默认使用80端口,HTTPS默认使用443端口。  

证书:HTTPS需要服务器配置CA颁发的数字证书(验证服务器身份),HTTP无需证书。 

二:SSL/TLS协议在HTTPS中的作用

SSL(Secure Sockets Layer)和TLS(Transport Layer Security,SSL的升级版)是HTTPS安全的核心,主要作用包括:  

身份验证:通过数字证书验证服务器身份(防止“中间人”伪造服务器),客户端会校验服务器证书的合法性(如是否由可信CA颁发、是否过期)。  

数据加密:通过握手过程协商对称加密密钥(如AES),后续所有数据均用该密钥加密传输(防止数据被窃听)。  

数据完整性:通过哈希算法(如SHA-256)校验数据传输过程中是否被篡改(接收方会验证数据指纹,不一致则拒绝)。  

Http三次握手建立连接图解:(连接队列)

Http四次挥手断开连接图解:

1.5、AndroidManifest.xml中需要配置哪些网络相关权限?(如INTERNET、ACCESS_NETWORK_STATE,分别有什么作用)  

Android网络功能需在AndroidManifest.xml中声明以下核心权限:  

android.permission.INTERNET:允许应用访问网络(发送/接收HTTP/HTTPS请求、使用TCP/UDP等),必须声明 ,否则所有网络请求会失败(抛出SecurityException)。  

android.permission.ACCESS_NETWORK_STATE:允许应用获取网络连接状态(如是否联网、当前网络类型是Wi-Fi还是移动数据),常用于:

1、发起请求前判断网络是否可用(避免无网时浪费资源)。

2、根据网络类型调整策略(如Wi-Fi下预加载大图,移动数据下加载缩略图)。 

android.permission.ACCESS_WIFI_STATE:允许应用获取Wi-Fi相关状态(如Wi-Fi是否开启、连接的SSID),适用于需要针对Wi-Fi环境优化的场景(如仅在特定Wi-Fi下同步数据)。  

android.permission.CHANGE_NETWORK_STATE(可选):允许应用修改网络状态(如开启/关闭Wi-Fi、切换网络),一般用于系统工具类应用,普通应用极少使用。  

1.6、Android 9.0(API 28)开始的“明文流量限制”是什么?如何通过配置绕过或适配该限制?  

一:明文流量限制的含义  

Android 9.0(API 28)引入“明文流量限制”(Cleartext Traffic Restriction),默认禁止应用使用HTTP(明文)协议传输数据,仅允许HTTPS(加密)流量,目的是提升应用安全性,防止明文数据被窃听或篡改,若应用强制使用HTTP,会抛出IOException: Cleartext HTTP traffic to xxx not permitted异常。  

二:绕过或适配的方法

实际开发中若需访问HTTP服务(如内部测试服务器、老旧系统),可通过以下方式适配:  

方法一:全局允许明文流量(不推荐,安全性低)

在AndroidManifest.xml的application标签中添加android:usesCleartextTraffic="true":  

  <application        ...        android:usesCleartextTraffic="true">    </application>  

方法二:通过网络安全配置文件允许特定域名(推荐)

仅允许指定HTTP域名,其他仍强制HTTPS,步骤如下:  

1、在res/xml目录下创建network_security_config.xml

     <?xml version="1.0" encoding="utf-8"?>       <network-security-config>           <domain-config cleartextTrafficPermitted="true">               <!-- 允许HTTP的域名(支持通配符,如*.example.com) -->               <domain includeSubdomains="true">test.example.com</domain>           </domain-config>       </network-security-config>  

2、在AndroidManifest.xml中引用该配置。  

     <application           ...           android:networkSecurityConfig="@xml/network_security_config">       </application>  

1.7、Android设备中,Wi-Fi、移动数据(4G/5G)的网络连接在底层实现上有什么差异?对上层网络请求有影响吗?  

一:底层实现差异  

Wi-Fi和移动数据的底层实现由不同硬件模块和系统框架管理,核心差异如下。

二:对上层网络请求的影响

底层差异会间接影响上层网络请求,主要体现在:  

连接稳定性:Wi-Fi受距离/障碍物影响大(近距离稳定,远距离易断连),移动数据在高铁、电梯等场景下可能因基站切换导致短暂断连。  

带宽与延迟:Wi-Fi通常带宽更高(适合大文件下载),延迟较低,移动数据带宽受信号强度影响(5G > 4G),但延迟可能因基站负载波动。  

系统限制:移动数据可能被系统限制后台流量(如Android的“后台数据限制”功能),部分设备在Wi-Fi和移动数据同时开启时,会优先使用Wi-Fi(可通过NetworkCapabilities手动选择网络)。  

网络切换处理:当Wi-Fi与移动数据切换时,底层TCP连接会断开(因IP地址变化),需上层应用处理重连(如OkHttp的连接池会自动重建连接)。

综上,上层开发需根据网络类型动态调整策略(如Wi-Fi下预加载、移动数据下节流),并通过ConnectivityManager监听网络变化以优化用户体验。

二、Android原生网络API

2.1、Android原生的HttpURLConnection如何发起一个简单的GET请求?(含请求创建、响应读取、异常处理步骤)  

HttpURLConnection是Android原生的HTTP请求工具类,发起GET请求的核心步骤如下(需在子线程中执行):  

步骤一:创建请求并配置连接 

// 1. 定义请求URL(需在子线程中执行)  new Thread(() -> {      HttpURLConnection connection = null;      BufferedReader reader = null;      try {          // 创建URL对象          URL url = new URL("https://api.example.com/user?userId=123");          // 打开连接(默认是GET请求)          connection = (HttpURLConnection) url.openConnection();          // 2. 配置连接参数          connection.setRequestMethod("GET"); // 明确指定GET方法(默认就是GET,可省略)          connection.setConnectTimeout(5000); // 连接超时(5秒)          connection.setReadTimeout(5000);    // 读取超时(5秒)          connection.setDoInput(true);        // 允许读取响应(默认true,可省略)          // GET请求无需设置doOutput(默认false,POST才需要设为true)      } catch (Exception e) {          e.printStackTrace();      }  }).start();  

步骤二:读取响应数据  

连接成功后,通过输入流读取服务器返回的响应体。

try {      // ... 延续上面的代码      // 3. 获取响应码(200表示成功)      int responseCode = connection.getResponseCode();      if (responseCode == HttpURLConnection.HTTP_OK) {          // 4. 读取响应体(通过输入流)          InputStream inputStream = connection.getInputStream();          reader = new BufferedReader(new InputStreamReader(inputStream));          StringBuilder response = new StringBuilder();          String line;          while ((line = reader.readLine()) != null) {              response.append(line);          }          // 响应数据:response.toString()          Log.d("GET_RESPONSE""结果:" + response.toString());      } else {          Log.e("GET_ERROR""请求失败,响应码:" + responseCode);      }  catch (Exception e) {      e.printStackTrace();  }  

步骤三:关闭资源与异常处理  

需在finally中关闭连接和流,避免资源泄漏,常见异常包括网络错误、超时等。 

finally {      // 5. 关闭资源      if (reader != null) {          try {              reader.close();          } catch (IOException e) {              e.printStackTrace();          }      }      if (connection != null) {          connection.disconnect(); // 关闭连接      }  }  

2.2、HttpURLConnection发起POST请求时,如何设置请求体(如表单数据、JSON数据)?  

POST请求需通过请求体传递数据,核心是设置doOutput=true并通过输出流写入数据,不同数据格式的配置方式如下。

一:表单数据(application/x-www-form-urlencoded)  

适用于键值对数据(如登录表单):  

new Thread(() -> {      HttpURLConnection connection = null;      OutputStream outputStream = null;      try {          URL url = new URL("https://api.example.com/login");          connection = (HttpURLConnection) url.openConnection();          // 1. 配置POST请求          connection.setRequestMethod("POST");          connection.setConnectTimeout(5000);          connection.setReadTimeout(5000);          connection.setDoOutput(true); // 允许写入请求体(POST必须设置)          connection.setDoInput(true);          // 2. 设置请求头:表单数据格式          connection.setRequestProperty("Content-Type""application/x-www-form-urlencoded");          // 3. 构建表单数据(键值对)          String postData = "username=test&password=123456";          // 4. 写入请求体(通过输出流)          outputStream = connection.getOutputStream();          outputStream.write(postData.getBytes(StandardCharsets.UTF_8));          outputStream.flush(); // 刷新缓冲区          // 5. 读取响应(同GET步骤)          int responseCode = connection.getResponseCode();          if (responseCode == HttpURLConnection.HTTP_OK) {              // 处理响应...          }      } catch (Exception e) {          e.printStackTrace();      } finally {          // 关闭资源...      }  }).start();  

二:JSON数据(application/json)  

适用于复杂结构数据(如接口提交对象)。  

new Thread(() -> {      HttpURLConnection connection = null;      OutputStream outputStream = null;      try {          URL url = new URL("https://api.example.com/user");          connection = (HttpURLConnection) url.openConnection();          // 1. 配置POST请求(同表单)          connection.setRequestMethod("POST");          connection.setDoOutput(true);          // 2. 设置请求头:JSON格式          connection.setRequestProperty("Content-Type""application/json; charset=UTF-8");          // 3. 构建JSON数据          String jsonData = "{\"name\":\"张三\",\"age\":20}";          // 4. 写入请求体          outputStream = connection.getOutputStream();          outputStream.write(jsonData.getBytes(StandardCharsets.UTF_8));          outputStream.flush();          // 5. 读取响应...      } catch (Exception e) {          e.printStackTrace();      } finally {          // 关闭资源...      }  }).start();  

2.3、Android中的HttpClient(Apache HttpClient)与HttpURLConnection有什么区别?为什么Android 6.0(API 23)后HttpClient被标记为过时?  

核心区别:

HttpClient被标记为过时的原因:

维护成本高:HttpClient是第三方库,Android团队无法直接修改其源码,难以适配Android系统的演进(如权限、性能优化)。  

原生API更适配移动场景:HttpURLConnection由Android团队主导优化,针对移动设备的内存、网络特性(如弱网)做了专项改进(如Android 4.0后加入连接池、压缩支持)。  

功能冗余:HttpClient的复杂功能(如完整的Cookie管理)在移动场景中使用率低,反而增加了App体积和内存占用。  

官方推荐转向:Android 6.0(API 23)后,HttpClient的类被从android.net.http包中移除,若需使用需手动引入Apache的jar包,但官方明确不推荐。  

2.4、为什么不能在Android主线程(UI线程)中直接发起网络请求?会抛出什么异常?  

一:禁止主线程发起网络请求的原因  

Android主线程(UI线程)的核心职责是处理UI渲染和用户交互(如点击事件、界面刷新),其响应时间直接影响用户体验,网络请求(如HTTP请求)属于耗时操作(可能因网络延迟、服务器响应慢等耗时数百毫秒甚至几秒),若在主线程执行:  

1、会阻塞主线程,导致UI无法刷新、用户操作无响应。  

2、若阻塞时间超过5秒,系统会触发“应用无响应”(ANR,Application Not Responding),提示用户强制关闭App。  

二:抛出的异常  

为避免上述问题,Android从API 11(Android 3.0)开始强制禁止在主线程发起网络请求,若违反会抛出:android.os.NetworkOnMainThreadException(运行时异常)。  

2.5、早期Android中,如何通过AsyncTask配合HttpURLConnection实现异步网络请求?AsyncTask的缺点是什么?  

一:AsyncTask配合HttpURLConnection的实现  

AsyncTask是早期Android提供的轻量级异步工具,封装了线程池和Handler,可在后台执行任务并将结果回调到主线程,示例如下:  

// 定义AsyncTask:泛型参数分别为输入参数、进度、结果  class NetworkTask extends AsyncTask<StringVoidString> {      // 1. 后台执行(子线程):发起网络请求      @Override      protected String doInBackground(String... urls) {          String url = urls[0];          HttpURLConnection connection = null;          BufferedReader reader = null;          try {              URL requestUrl = new URL(url);              connection = (HttpURLConnection) requestUrl.openConnection();              connection.setRequestMethod("GET");              connection.setConnectTimeout(5000);              if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) {                  reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));                  StringBuilder response = new StringBuilder();                  String line;                  while ((line = reader.readLine()) != null) {                      response.append(line);                  }                  return response.toString(); // 返回结果给onPostExecute              }          } catch (Exception e) {              e.printStackTrace();              return "请求失败:" + e.getMessage();          } finally {              // 关闭资源...          }          return null;      }      // 2. 主线程回调:处理结果并更新UI      @Override      protected void onPostExecute(String result) {          super.onPostExecute(result);          // 更新UI(如TextView显示结果)          textView.setText(result);      }  }  // 启动任务(在主线程调用)  new NetworkTask().execute("https://api.example.com/data");  

二:AsyncTask的缺点  

AsyncTask因设计缺陷,在复杂场景中问题明显,最终在API 30(Android 11)被标记为过时,核心缺点包括:  

生命周期绑定问题:若AsyncTask持有Activity引用,当Activity销毁(如旋转屏幕)时,AsyncTask仍可能继续执行,导致内存泄漏(Activity无法被回收)。  

线程池管理混乱:AsyncTask的默认线程池(SERIAL_EXECUTOR)是串行执行任务,大量任务时会排队阻塞,且线程池参数(如核心线程数)不可灵活配置。  

取消机制不可靠:cancel(true)无法真正终止正在执行的doInBackground(需手动判断isCancelled())。  

回调方法有限:仅提供onPreExecute、onPostExecute等有限回调,复杂异步流程(如链式请求)需手动嵌套,代码可读性差。  

2.6、使用Handler+Thread的方式实现异步网络请求时,如何将请求结果回传到主线程更新UI?  

Handler是Android的线程通信工具,可在子线程中发送消息,主线程中处理消息(更新UI),核心流程如下:  

步骤一:在主线程创建Handler(处理消息)  

public class MainActivity extends AppCompatActivity {      private TextView textView;      // 主线程的Handler(默认与主线程Looper绑定)      private Handler mainHandler = new Handler(Looper.getMainLooper()) {          @Override          public void handleMessage(@NonNull Message msg) {              super.handleMessage(msg);              // 3. 主线程处理消息:更新UI              if (msg.what == 1) { // 消息标识                  String result = (String) msg.obj// 消息内容                  textView.setText(result);              }          }      };      @Override      protected void onCreate(Bundle savedInstanceState) {          super.onCreate(savedInstanceState);          setContentView(R.layout.activity_main);          textView = findViewById(R.id.text_view);          // 启动网络请求线程          startNetworkRequest();      }  }  

步骤二:子线程执行网络请求并发送消息  

private void startNetworkRequest() {      new Thread(() -> {          // 1. 子线程执行网络请求(使用HttpURLConnection)          String result = fetchDataFromNetwork("https://api.example.com/data");          // 2. 发送结果到主线程(通过Handler)          Message message = Message.obtain();          message.what = 1// 消息标识(区分不同类型消息)          message.obj = result; // 携带数据(需序列化,如String、Parcelable等)          mainHandler.sendMessage(message); // 发送消息      }).start();  }  // 网络请求实现(同前文HttpURLConnection GET逻辑)  private String fetchDataFromNetwork(String url) {      // ... 省略HttpURLConnection请求代码 ...      return "请求结果";  }  

核心原理: 

Handler通过绑定主线程的Looper(消息循环),在主线程中接收消息。 

子线程通过Handler.sendMessage()将结果封装为Message,放入主线程的消息队列。  

主线程的Looper循环取出消息,调用handleMessage()处理,从而在主线程更新UI。  

2.7、原生API中如何设置网络请求的超时时间(连接超时、读取超时)?超时后如何处理?  

一:设置超时时间  

HttpURLConnection通过以下方法设置超时(单位:毫秒):  

连接超时(setConnectTimeout):客户端与服务器建立TCP连接的最大等待时间(如服务器未响应则超时)。  

读取超时(setReadTimeout):连接建立后,等待服务器返回数据的最大时间(如服务器已连接但未发送数据则超时)。  

代码示例:  

HttpURLConnection connection = (HttpURLConnection) url.openConnection();  connection.setConnectTimeout(5000); // 连接超时:5秒  connection.setReadTimeout(10000);   // 读取超时:10秒  

二:超时后的处理  

超时会抛出SocketTimeoutException(IOException的子类),需捕获异常并处理,常见处理方式:  

1、提示用户“网络超时,请重试”,  

2、实现有限次数的自动重试(避免无限重试消耗资源), 

3、记录错误日志,便于排查问题。  

代码示例:

try {      // 执行网络请求...  catch (SocketTimeoutException e) {      // 超时处理      Log.e("TIMEOUT""网络超时:" + e.getMessage());      // 发送消息到主线程提示用户      mainHandler.sendMessage(Message.obtain(mainHandler, 2"网络超时,请稍后重试"));  catch (IOException e) {      // 其他网络错误(如无网络)      e.printStackTrace();  }  

注意:超时时间需根据业务场景设置(如弱网环境可适当延长,实时性要求高的场景可缩短),避免过短导致频繁超时,或过长影响用户体验。

三、主流第三方网络库(OkHttp/Retrofit)

OkHttp相关:

3.1、OkHttp是什么?它相比Android原生网络API有哪些核心优势?  

OkHttp是由Square公司开发的一套高效、轻量的HTTP客户端库,支持HTTP/1.1、HTTP/2、HTTP/3(QUIC)及HTTPS,广泛用于Android和Java项目中,它封装了底层网络通信细节,提供了简洁的API,相比Android原生的HttpURLConnection,核心优势如下:  

连接池复用:通过ConnectionPool管理TCP连接的复用(基于HTTP的keep-alive机制),避免频繁创建/销毁连接的开销(原生API需手动管理,复杂且低效)。  

拦截器机制:提供分层拦截器(应用拦截器、网络拦截器),可轻松实现请求头统一处理、日志打印、缓存控制、签名验证等功能(原生API需手动嵌入代码,耦合度高)。  

自动处理常见场景:支持自动重定向(3xx状态码)、自动重试(如连接失败)、Gzip压缩解压(减少数据传输量),原生API需手动实现这些逻辑。

异步回调简化:内置异步请求机制(enqueue()),回调在子线程执行,避免Handler+Thread的繁琐代码(原生API需手动管理线程通信)。  

缓存支持:内置HTTP缓存(基于Cache-Control头),可配置磁盘缓存目录和大小,实现离线访问(原生API需手动处理缓存逻辑,易出错)。  

HTTP/2与HTTP/3支持:原生支持多路复用(HTTP/2)和QUIC协议(HTTP/3),大幅提升并发请求性能(原生HttpURLConnection对高版本HTTP支持有限)。  

3.2、如何在Android项目中集成OkHttp?(含Gradle依赖配置、基本初始化步骤)  

一:Gradle依赖配置  

在模块的build.gradle(通常是app/build.gradle)中添加依赖:  

dependencies {      // OkHttp核心库      implementation 'com.squareup.okhttp3:okhttp:4.12.0'      // 可选:日志拦截器(用于打印请求/响应日志)      implementation 'com.squareup.okhttp3:logging-interceptor:4.12.0'  }  

同步项目后,OkHttp库会被引入工程。  

二:基本初始化步骤  

OkHttp通过OkHttpClient实例发起请求,推荐全局单例模式(避免重复创建连接池等资源):  

import okhttp3.OkHttpClient;  import java.util.concurrent.TimeUnit;  public class OkHttpManager {      // 单例实例      private static OkHttpClient sClient;      // 初始化OkHttpClient(配置超时、拦截器等)      public static OkHttpClient getInstance() {          if (sClient == null) {              synchronized (OkHttpManager.class) {                  if (sClient == null) {                      sClient = new OkHttpClient.Builder()                              .connectTimeout(10TimeUnit.SECONDS// 连接超时                              .readTimeout(10TimeUnit.SECONDS)    // 读取超时                              .writeTimeout(10TimeUnit.SECONDS)   // 写入超时                              // 可添加拦截器、缓存等配置                              .build();                  }              }          }          return sClient;      }  }  

注意:OkHttpClient是线程安全的,全局单例即可满足所有请求需求,如需不同配置(如不同超时),可创建多个OkHttpClient实例。  

3.3、使用OkHttp发起GET请求的基本代码流程是什么?(创建OkHttpClient、Request、Call,同步/异步执行)  

OkHttp发起GET请求的核心是构建Request对象,通过Call执行,分为同步和异步两种方式:  

一:同步执行(execute())  

同步请求会阻塞当前线程,必须在子线程中执行,否则会导致ANR:  

// 子线程中执行  new Thread(() -> {      // 1. 获取OkHttpClient实例      OkHttpClient client = OkHttpManager.getInstance();      // 2. 构建GET请求(Request)      Request request = new Request.Builder()              .url("https://api.example.com/user?userId=123"// 请求URL              .get() // 默认为GET,可省略              .addHeader("Accept""application/json"// 可选:添加请求头              .build();      // 3. 创建Call对象(请求任务)      Call call = client.newCall(request);      try {          // 4. 同步执行请求(阻塞当前线程)          Response response = call.execute();          // 5. 处理响应          if (response.isSuccessful()) { // 响应码200-299              String responseBody = response.body().string(); // 获取响应体(注意:string()只能调用一次)              Log.d("GET_SYNC""成功:" + responseBody);          } else {              Log.e("GET_SYNC""失败,响应码:" + response.code());          }      } catch (IOException e) {          e.printStackTrace(); // 处理异常(如网络错误、超时)      }  }).start();  

二:异步执行(enqueue())  

异步请求通过回调处理结果,不会阻塞当前线程(推荐使用):

// 可在主线程调用(内部自动切换到子线程执行)  OkHttpClient client = OkHttpManager.getInstance();  Request request = new Request.Builder()          .url("https://api.example.com/user?userId=123")          .build();  Call call = client.newCall(request);  // 异步执行:回调在子线程中  call.enqueue(new Callback() {      @Override      public void onFailure(@NonNull Call call, @NonNull IOException e) {          // 请求失败(如网络错误、超时)          e.printStackTrace();      }      @Override      public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {          // 请求完成(无论成功与否)          if (response.isSuccessful()) {              String responseBody = response.body().string();              Log.d("GET_ASYNC""成功:" + responseBody);              // 如需更新UI,需切换到主线程(如用Handler、runOnUiThread)              runOnUiThread(() -> textView.setText(responseBody));          } else {              Log.e("GET_ASYNC""失败,响应码:" + response.code());          }      }  });  

3.4、OkHttp的RequestBody有哪些常用子类?分别适用于什么场景?  

RequestBody是OkHttp中用于封装POST、PUT等请求的请求体的抽象类,常用子类及适用场景如下:  

一:FormBody

用途:提交表单数据(application/x-www-form-urlencoded格式,键值对)。  

适用场景:简单表单提交(如登录、搜索,数据为字符串键值对)。  

代码示例:  

  // 构建表单数据    FormBody formBody = new FormBody.Builder()            .add("username""test")            .add("password""123456")            .build();    // 构建POST请求    Request request = new Request.Builder()            .url("https://api.example.com/login")            .post(formBody)            .build();  

二:MultipartBody

用途:提交混合类型数据(multipart/form-data格式,支持文本+文件)。  

适用场景:文件上传(如头像、文档),同时需携带表单字段。  

代码示例:  

  // 构建文件请求体(上传图片)    RequestBody fileBody = RequestBody.create(            new File("/sdcard/avatar.jpg"),            MediaType.parse("image/jpeg")    );    // 构建混合请求体    MultipartBody multipartBody = new MultipartBody.Builder()            .setType(MultipartBody.FORM) // 必须指定类型为FORM            .addFormDataPart("username""test"// 文本字段            .addFormDataPart("avatar""avatar.jpg", fileBody) // 文件字段(name、文件名、文件体)            .build();    // 构建POST请求    Request request = new Request.Builder()            .url("https://api.example.com/upload")            .post(multipartBody)            .build();  

三:RequestBody.create()(静态方法创建)  

用途:提交自定义格式数据(如JSON、纯文本、二进制)。  

适用场景:API接口提交JSON数据、发送纯文本等。  

代码示例(提交JSON):  

  // JSON数据    String json = "{\"name\":\"张三\",\"age\":20}";    // 创建JSON请求体(指定媒体类型为application/json)    RequestBody jsonBody = RequestBody.create(            json,            MediaType.parse("application/json; charset=utf-8")    );    // 构建POST请求    Request request = new Request.Builder()            .url("https://api.example.com/user")            .post(jsonBody)            .build();  

四:ByteStringRequestBody(较少用)  

用途:提交ByteString(OkHttp的高效字节容器)数据。  

适用场景:内存中已存在的二进制数据(如加密后的字节流)。  

3.5、OkHttp的拦截器(Interceptor)是什么?应用拦截器(Application Interceptor)和网络拦截器(Network Interceptor)的区别是什么?  

拦截器的定义:拦截器是OkHttp中用于拦截、修改、监控HTTP请求与响应的组件,类似“中间件”,它可以在请求发送到服务器前处理请求(如添加头信息、签名),或在响应返回客户端后处理响应(如解析数据、打印日志),是OkHttp灵活性的核心。  

应用拦截器与网络拦截器的区别:两者通过OkHttpClient.Builder的addInterceptor()(应用拦截器)和addNetworkInterceptor()(网络拦截器)添加,核心区别如下:  

示例:若请求命中缓存,应用拦截器会收到缓存响应,而网络拦截器不会被调用(因未发起真实网络请求)。  

OkHttp拦截器图解:

OkHttp请求过程图解:

3.6、如何通过OkHttp的拦截器实现请求头统一添加(如Token)、请求/响应日志打印、缓存控制?  

一:统一添加请求头(如Token),使用应用拦截器  

public class HeaderInterceptor implements Interceptor {      @Override      public Response intercept(Chain chain) throws IOException {          // 1. 获取原始请求          Request originalRequest = chain.request();          // 2. 构建新请求,添加统一头信息(如Token)          Request newRequest = originalRequest.newBuilder()                  .addHeader("Authorization""Bearer " + getToken()) // 添加Token                  .addHeader("App-Version""1.0.0"// 添加App版本                  .build();          // 3. 继续执行请求          return chain.proceed(newRequest);      }      // 获取本地Token(示例)      private String getToken() {          return "user_token_123456";      }  }  // 配置到OkHttpClient  OkHttpClient client = new OkHttpClient.Builder()          .addInterceptor(new HeaderInterceptor()) // 应用拦截器          .build();  

二:请求/响应日志打印,使用应用拦截器或网络拦截器  

推荐使用官方LoggingInterceptor(需添加依赖),也可自定义:  

// 自定义日志拦截器(简化版)  public class LogInterceptor implements Interceptor {      @Override      public Response intercept(Chain chain) throws IOException {          // 打印请求信息          Request request = chain.request();          Log.d("OkHttp""请求URL:" + request.url());          Log.d("OkHttp""请求头:" + request.headers());          // 执行请求          long startTime = System.currentTimeMillis();          Response response = chain.proceed(request);          long endTime = System.currentTimeMillis();          // 打印响应信息          Log.d("OkHttp""响应码:" + response.code());          Log.d("OkHttp""耗时:" + (endTime - startTime) + "ms");          Log.d("OkHttp""响应体:" + response.body().string()); // 注意:string()会消耗响应体,需复制后使用          return response;      }  }  // 配置(推荐用网络拦截器打印真实网络请求)  OkHttpClient client = new OkHttpClient.Builder()          .addNetworkInterceptor(new LogInterceptor()) // 网络拦截器          .build();  

三:缓存控制,使用应用拦截器  

通过修改请求头的Cache-Control实现缓存策略(如强制缓存、禁止缓存):  

public class CacheInterceptor implements Interceptor {      @Override      public Response intercept(Chain chain) throws IOException {          Request request = chain.request();          // 无网络时,强制使用缓存(仅适用于GET请求)          if (!isNetworkAvailable()) {              request = request.newBuilder()                      .cacheControl(CacheControl.FORCE_CACHE) // 强制使用缓存                      .build();          }          Response response = chain.proceed(request);          // 有网络时,设置缓存有效期(如60秒)          if (isNetworkAvailable()) {              response = response.newBuilder()                      .header("Cache-Control""public, max-age=60"// 缓存60秒                      .removeHeader("Pragma"// 移除干扰缓存的头                      .build();          } else {              // 无网络时,延长缓存有效期(如1小时)              response = response.newBuilder()                      .header("Cache-Control""public, only-if-cached, max-stale=3600")                      .removeHeader("Pragma")                      .build();          }          return response;      }      // 判断网络是否可用(需权限ACCESS_NETWORK_STATE)      private boolean isNetworkAvailable() {          // 实现略(通过ConnectivityManager判断)          return true;      }  }  // 配置缓存目录和拦截器  File cacheDir = new File(context.getCacheDir(), "okhttp_cache");  int cacheSize = 10 * 1024 * 1024// 10MB  Cache cache = new Cache(cacheDir, cacheSize);  OkHttpClient client = new OkHttpClient.Builder()          .cache(cache)          .addInterceptor(new CacheInterceptor()) // 应用拦截器          .build();  

3.7、OkHttp的连接池(ConnectionPool)原理是什么?如何配置连接池的最大空闲连接数、连接存活时间?  

连接池原理:OkHttp的ConnectionPool基于HTTP的keep-alive机制,管理TCP连接的复用,避免频繁创建/销毁连接的性能损耗(TCP三次握手、四次挥手耗时),核心逻辑:  

1、当请求完成后,TCP连接不会立即关闭,而是被放入连接池(标记为“空闲”)。  

2、新请求若访问相同的主机和端口(且协议版本兼容),会优先复用连接池中的空闲连接。  

3、连接池通过后台线程(CleanupThread)定期清理过期连接(超过最大存活时间)或超出最大空闲数的连接。  

配置连接池参数:默认连接池配置:最大空闲连接数5个,连接存活时间5分钟(300秒),可通过ConnectionPool构造函数自定义:  

// 自定义连接池:最大空闲连接数10,存活时间10分钟  ConnectionPool connectionPool = new ConnectionPool(          10// 最大空闲连接数          10// 存活时间          TimeUnit.MINUTES // 时间单位  );  // 配置到OkHttpClient  OkHttpClient client = new OkHttpClient.Builder()          .connectionPool(connectionPool)          .build();  

注意:连接池参数需根据业务调整,如高频访问同一服务器可增大最大空闲数,减少连接创建开销。  

3.8、OkHttp如何处理HTTPS证书?(如信任所有证书、信任自定义证书、证书pinning配置)  

HTTPS通信需验证服务器证书,OkHttp默认信任系统预装的CA证书(如Let's Encrypt),对于自签名证书或私有CA,需手动配置信任策略。  

一:信任所有证书(仅用于测试,生产环境禁止)  

跳过证书验证(风险高,易受中间人攻击):  

// 创建信任所有证书的TrustManager  X509TrustManager trustAllCert = 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];      }  };  // 创建SSLSocketFactory  SSLContext sslContext = SSLContext.getInstance("TLS");  sslContext.init(nullnew TrustManager[]{trustAllCert}, new SecureRandom());  SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();  // 配置OkHttpClient  OkHttpClient client = new OkHttpClient.Builder()          .sslSocketFactory(sslSocketFactory, trustAllCert)          .hostnameVerifier((hostname, session) -> true// 跳过主机名验证          .build();  

二:信任自定义证书(如自签名证书)  

仅信任指定的自签名证书或私有CA证书:  

1、将证书文件(如server.crt)放入res/raw目录。  

2、加载证书并配置信任:  

// 加载证书  InputStream certInputStream = context.getResources().openRawResource(R.raw.server);  CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");  X509Certificate cert = (X509Certificate) certificateFactory.generateCertificate(certInputStream);  // 创建KeyStore并添加证书  KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());  keyStore.load(null);  keyStore.setCertificateEntry("server", cert);  // 创建TrustManagerFactory(仅信任KeyStore中的证书)  TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(          TrustManagerFactory.getDefaultAlgorithm());  trustManagerFactory.init(keyStore);  TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();  // 创建SSLSocketFactory  SSLContext sslContext = SSLContext.getInstance("TLS");  sslContext.init(null, trustManagers, new SecureRandom());  SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();  // 配置OkHttpClient  OkHttpClient client = new OkHttpClient.Builder()          .sslSocketFactory(sslSocketFactory, (X509TrustManager) trustManagers[0])          .build();  

三:证书固定(Certificate Pinning)  

通过“证书固定”强制客户端仅信任指定服务器的证书(防止中间人替换证书),无需信任CA:

OkHttpClient client = new OkHttpClient.Builder()          .certificatePinner(new CertificatePinner.Builder()                  // 指定主机名和允许的证书公钥哈希(SHA-256)                  .add("api.example.com",                          "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="// 证书哈希1                  .add("api.example.com",                          "sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB="// 证书哈希2(备用)                  .build())          .build();  

获取证书哈希:可通过openssl工具生成(如openssl x509 -in server.crt -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | base64)。  

总结:生产环境优先使用证书固定或信任自定义证书,绝对禁止信任所有证书。

Retrofit相关:

3.9、Retrofit是什么?它与OkHttp的关系是什么?(为什么说Retrofit是OkHttp的“包装器”)  

Retrofit是由Square公司开发的一款RESTful API请求框架,它通过注解将HTTP请求抽象为Java/Kotlin接口,简化了网络请求的定义与调用,其核心价值是将网络请求的“配置逻辑”(如URL、请求方法、参数)与“执行逻辑”解耦,让代码更简洁、可维护。  

它与OkHttp的关系是“上层封装与底层实现”:  

1、Retrofit本身不执行网络请求,而是将接口定义的请求参数转换为OkHttp可识别的Request对象,最终通过OkHttp完成TCP连接、数据传输等底层操作。  

2、可以理解为:Retrofit是“翻译官”,将开发者定义的接口注解翻译成OkHttp能执行的请求指令,OkHttp是“执行者”,负责实际的网络通信,因此Retrofit被称为OkHttp的“包装器”(Wrapper)。  

3.10、如何在Android项目中集成Retrofit?(含依赖配置、与OkHttp的关联配置)  

一:Gradle依赖配置  

Retrofit需依赖核心库,同时需根据数据格式(如JSON)添加对应的序列化库(如Gson),并关联OkHttp(Retrofit 2.0+默认依赖OkHttp,但需确保版本兼容):  

dependencies {      // Retrofit核心库(最新版本参考官网:https://square.github.io/retrofit/)      implementation 'com.squareup.retrofit2:retrofit:2.9.0'      // JSON序列化库(可选,根据需求选择,这里以Gson为例)      implementation 'com.squareup.retrofit2:converter-gson:2.9.0'      // OkHttp(Retrofit默认依赖,若需自定义配置可单独添加,版本需匹配)      implementation 'com.squareup.okhttp3:okhttp:4.12.0'  }  

二:与OkHttp的关联配置

Retrofit默认使用内置的OkHttpClient,但实际开发中通常需要自定义OkHttp配置(如超时、拦截器、缓存),步骤如下:  

自定义OkHttpClient(配置超时、拦截器等):

OkHttpClient okHttpClient = new OkHttpClient.Builder()             .connectTimeout(10, TimeUnit.SECONDS) // 连接超时             .readTimeout(10, TimeUnit.SECONDS)    // 读取超时             .addInterceptor(new HeaderInterceptor()) // 自定义拦截器(如添加Token)             .build();

初始化Retrofit并关联OkHttp:  

public class RetrofitManager {         private static Retrofit sRetrofit;         private static final String BASE_URL = "https://api.example.com/"// 基础URL         public static Retrofit getInstance() {             if (sRetrofit == null) {                 synchronized (RetrofitManager.class) {                     if (sRetrofit == null) {                         sRetrofit = new Retrofit.Builder()                                 .baseUrl(BASE_URL// 接口基础URL(必须以/结尾)                                 .client(okHttpClient) // 关联自定义的OkHttpClient                                 .addConverterFactory(GsonConverterFactory.create()) // 配置JSON解析器                                 .build();                     }                 }             }             return sRetrofit;         }         // 获取API接口实例         public static <T> T createService(Class<T> serviceClass) {             return getInstance().create(serviceClass);         }     }  

3.11、Retrofit的核心注解有哪些?(如@GET/@POST、@Path/@Query/@Field/@Body,分别如何使用)  

Retrofit通过注解定义HTTP请求的核心信息,常见注解可分为HTTP方法注解、参数注解、其他辅助注解三类:  

一:HTTP方法注解  

用于指定请求方法(GET、POST等)和相对URL,核心包括:  

@GET(String url):GET请求,url为相对路径(基于baseUrl)。  

示例:@GET("user/list") → 完整URL:baseUrl + "user/list"。  

@POST(String url):POST请求,用法同@GET。  

其他:@PUT(更新资源)、@DELETE(删除资源)、@PATCH(部分更新),用法类似。  

二:参数注解  

用于将方法参数映射为HTTP请求的参数(路径、查询、请求体等),核心包括:  

@Path(String name):替换URL路径中的占位符({}包裹),适用于路径参数。  

示例:@GET("user/{userId}") Call<User> getUser(@Path("userId") int id);

→ 实际URL:baseUrl + "user/123"(若id=123)。  

@Query(String name):添加URL查询参数(?key=value),适用于GET/POST的查询参数。  

示例:@GET("user/list") Call<UserList> getUserList(@Query("page") int page, @Query("size") int size);

→ 实际URL:baseUrl + "user/list?page=1&size=20"。  

@QueryMap:将Map集合转换为多个查询参数(适用于参数较多的场景)。  

示例:@GET("user/list") Call<UserList> getUserList(@QueryMap Map<String, Integer> params);

@Field(String name):POST请求的表单参数(需配合@FormUrlEncoded`注解),适用于application/x-www-form-urlencoded格式。  

示例:@FormUrlEncoded @POST("login") Call<LoginResponse> login(@Field("username") String username, @Field("password") String pwd); 

@FieldMap:将Map集合转换为多个表单参数(适用于表单参数较多的场景)。  

@Body(Object obj):将对象作为请求体(如JSON格式),适用于application/json格式(需配合ConverterFactory)。  

示例:@POST("user") Call<User> addUser(@Body User user);(User对象会被Gson转换为JSON)。  

@Part/@PartMap:POST请求的混合表单参数(需配合`@Multipart`注解),适用于文件上传(multipart/form-data格式)。  

示例:@Multipart @POST("upload") Call<UploadResponse> upload(@Part MultipartBody.Part file);。  

三:其他辅助注解  

@FormUrlEncoded:标记POST请求为表单格式(application/x-www-form-urlencoded),需与@Field/@FieldMap配合使用。  

@Multipart:标记POST请求为混合表单格式(multipart/form-data),需与@Part/@PartMap配合使用(用于文件上传)。  

@Headers:添加静态请求头(编译期固定,不可动态修改)。  

示例:@Headers("Content-Type: application/json") @POST("user") Call<User> addUser(@Body User user);

@Header:添加动态请求头(参数值可动态变化)。  

示例:@GET("user") Call<User> getUser(@Header("Authorization") String token);

3.12、如何通过Retrofit定义一个网络请求接口?(如GET请求带路径参数、POST请求带JSON请求体)  

Retrofit的核心是“接口定义”,通过注解将HTTP请求抽象为Java/Kotlin接口,以下是两个典型场景的示例:  

一:GET请求带路径参数(查询用户信息)  

需求:通过用户ID查询用户详情,URL为https://api.example.com/user/{userId}。  

步骤一:定义数据模型(User类)  

public class User {      private int userId;      private String name;      private int age;      //  getter/setter 省略  }  

步骤二:定义网络请求接口  

import retrofit2.Call;  import retrofit2.http.GET;  import retrofit2.http.Path;  public interface UserService {      // GET请求:路径参数userId      @GET("user/{userId}")      Call<User> getUserById(@Path("userId") int userId);  }  

步骤三:调用接口  

// 获取接口实例  UserService userService = RetrofitManager.createService(UserService.class);  // 发起异步请求  Call<User> call = userService.getUserById(123);  call.enqueue(new Callback<User>() {      @Override      public void onResponse(Call<User> call, Response<User> response) {          if (response.isSuccessful()) {              User user = response.body(); // 响应体(已被Gson解析为User对象)              // 更新UI...          } else {              // 处理HTTP错误(如404、500)          }      }      @Override      public void onFailure(Call<User> call, Throwable t) {          // 处理网络异常(如无网络、超时)          t.printStackTrace();      }  });  

二:POST请求带JSON请求体(新增用户)  

需求:提交用户信息到服务器,URL为https://api.example.com/user,请求体为JSON格式。  

步骤一:复用User数据模型(同上述User类)。  

步骤二:定义网络请求接口  

import retrofit2.Call;  import retrofit2.http.Body;  import retrofit2.http.POST;  public interface UserService {      // POST请求:JSON请求体(User对象会被Gson转换为JSON)      @POST("user")      Call<User> addUser(@Body User user);  }  

步骤三:调用接口  

// 构建请求体对象  User newUser = new User();  newUser.setName("张三");  newUser.setAge(20);  // 发起异步请求  UserService userService = RetrofitManager.createService(UserService.class);  Call<User> call = userService.addUser(newUser);  call.enqueue(new Callback<User>() {      @Override      public void onResponse(Call<User> call, Response<User> response) {          if (response.isSuccessful()) {              User addedUser = response.body(); // 服务器返回的新增用户信息          }      }      @Override      public void onFailure(Call<User> call, Throwable t) {          // 处理异常...      }  });  

3.13、Retrofit的Converter是什么?如何配置Gson/Moshi/Jackson作为JSON数据的序列化/反序列化工具?  

Converter的定义:Converter是Retrofit的数据转换接口,负责将HTTP请求的“请求体”(如Java对象)序列化为网络传输的字节流(如JSON字符串),以及将HTTP响应的“响应体”(如JSON字符串)反序列化为Java/Kotlin对象。  

Retrofit默认不提供Converter,需通过添加ConverterFactory扩展(不同ConverterFactory对应不同数据格式,如JSON、XML)。  

配置常见JSON Converter:

配置Gson:(最常用)  

步骤一:添加Gson Converter依赖(已在“集成Retrofit”部分提及):  

implementation 'com.squareup.retrofit2:converter-gson:2.9.0'  

步骤二:初始化Retrofit时添加GsonConverterFactory:  

Retrofit retrofit = new Retrofit.Builder()         .baseUrl(BASE_URL)         .client(okHttpClient)         .addConverterFactory(GsonConverterFactory.create()) // 配置Gson         .build();  自定义Gson配置(如字段重命名、日期格式):  Gson gson = new GsonBuilder()          .setDateFormat("yyyy-MM-dd HH:mm:ss"// 自定义日期格式          .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) // 下划线命名(如user_name → userName)          .create();  Retrofit retrofit = new Retrofit.Builder()          .baseUrl(BASE_URL)          .addConverterFactory(GsonConverterFactory.create(gson)) // 使用自定义Gson          .build();  

配置Moshi:(Square推荐,轻量替代Gson)  

步骤一:添加Moshi依赖:  

implementation 'com.squareup.retrofit2:converter-moshi:2.9.0'  implementation 'com.squareup.moshi:moshi:1.15.0'

步骤二:初始化Retrofit时添加MoshiConverterFactory:  

Moshi moshi = new Moshi.Builder().build();  Retrofit retrofit = new Retrofit.Builder()          .baseUrl(BASE_URL)          .addConverterFactory(MoshiConverterFactory.create(moshi))          .build();  

配置Jackson(适用于复杂JSON结构)  

步骤一:添加Jackson依赖:  

implementation 'com.squareup.retrofit2:converter-jackson:2.9.0'  implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.2'  

步骤二:初始化Retrofit时添加JacksonConverterFactory:  

ObjectMapper objectMapper = new ObjectMapper();  Retrofit retrofit = new Retrofit.Builder()          .baseUrl(BASE_URL)          .addConverterFactory(JacksonConverterFactory.create(objectMapper))          .build();  

3.14、Retrofit的CallAdapter是什么?如何集成RxJava/RxJava2作为CallAdapter实现异步请求?  

CallAdapter的定义:CallAdapter是Retrofit的返回值转换接口,负责将Retrofit默认的Call<T>返回值转换为其他类型(如RxJava的Observable<T>、Kotlin协程的suspend函数),从而扩展异步请求的实现方式。  

Retrofit默认仅支持Call<T>返回值,需通过添加CallAdapterFactory扩展其他类型。  

集成RxJava2作为CallAdapter:

步骤一:添加RxJava2和Retrofit-RxJava2依赖:  

// RxJava2核心库  implementation 'io.reactivex.rxjava2:rxjava:2.2.21'  // RxJava2 Android绑定(用于切换到主线程)  implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'  // Retrofit-RxJava2适配器  implementation 'com.squareup.retrofit2:adapter-rxjava2:2.9.0'  

步骤二:初始化Retrofit时添加RxJava2CallAdapterFactory:  

Retrofit retrofit = new Retrofit.Builder()          .baseUrl(BASE_URL)          .client(okHttpClient)          .addConverterFactory(GsonConverterFactory.create())          .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) // 配置RxJava2适配器          .build();  

步骤三:定义接口(返回值为Observable<T>):  

import io.reactivex.Observable;  import retrofit2.http.GET;  import retrofit2.http.Path;  public interface UserService {      @GET("user/{userId}")      Observable<User> getUserById(@Path("userId") int userId); // 返回Observable<User>  }  

步骤四:调用接口(使用RxJava2的线程切换和订阅):

UserService userService = RetrofitManager.createService(UserService.class);  userService.getUserById(123)          .subscribeOn(Schedulers.io()) // 网络请求在IO线程执行          .observeOn(AndroidSchedulers.mainThread()) // 结果回调在主线程          .subscribe(                  user -> { /* 成功:处理User对象,更新UI */ },                  throwable -> { /* 失败:处理异常 */ }          ); 

3.15、如何使用Retrofit + Kotlin协程实现异步网络请求?(suspend函数的使用场景)  

Retrofit 2.6.0+原生支持Kotlin协程,通过suspend函数实现异步请求(无需Call<T>或RxJava),核心是利用协程的挂起特性,避免回调嵌套。  

一:集成与配置  

无需额外添加CallAdapter依赖(Retrofit 2.6.0+内置CoroutineCallAdapterFactory),仅需确保Retrofit版本≥2.6.0,步骤如下:  

步骤一:依赖配置(Retrofit版本≥2.6.0):  

implementation 'com.squareup.retrofit2:retrofit:2.9.0'  implementation 'com.squareup.retrofit2:converter-gson:2.9.0'  

步骤二:初始化Retrofit(无需手动添加CoroutineCallAdapterFactory,默认自动支持):  

val retrofit = Retrofit.Builder()      .baseUrl("https://api.example.com/")      .client(okHttpClient)      .addConverterFactory(GsonConverterFactory.create())      .build()  

二:定义接口(使用suspend函数)  

在接口方法前添加suspend关键字,返回值直接为数据模型(无需Call<T>):  

interface UserService {      // GET请求:suspend函数,返回User      @GET("user/{userId}")      suspend fun getUserById(@Path("userId") userId: Int): User      // POST请求:suspend函数,返回AddUserResponse      @POST("user")      suspend fun addUser(@Body user: User): AddUserResponse  }  

三:调用接口(在协程作用域中执行)  

suspend函数必须在协程作用域(如viewModelScope、lifecycleScope、GlobalScope)中调用,代码示例:  

// 在ViewModel中使用viewModelScope(需添加Jetpack ViewModel依赖)  class UserViewModel : ViewModel() {      private val userService = RetrofitManager.createService(UserService::class.java)      fun fetchUser(userId: Int) {          viewModelScope.launch { // 协程作用域,自动在子线程执行              try {                  // 调用suspend函数(挂起,不阻塞主线程)                  val user = userService.getUserById(userId)                  // 成功:更新UI(viewModelScope.launch默认在主线程回调)                  _userLiveData.value = user              } catch (e: Exception) {                  // 失败:处理异常(如网络错误、解析错误)                  _errorLiveData.value = e.message              }          }      }  }  

四:suspend函数的使用场景  

替代Call.enqueue()和RxJava,简化异步代码(无回调嵌套,逻辑线性),配合Jetpack组件(如ViewModel、Lifecycle),自动管理协程生命周期(如页面销毁时取消请求,避免内存泄漏),支持多个请求串行/并行(通过async/await),代码示例:  

  viewModelScope.launch {        // 并行请求        val userDeferred = async { userService.getUserById(123) }        val userListDeferred = async { userService.getUserList(120) }        val user = userDeferred.await()        val userList = userListDeferred.await()        // 处理结果...    }  

3.16、Retrofit中如何设置全局超时时间、拦截器?与直接配置OkHttp有什么区别?  

Retrofit本身不提供超时、拦截器的配置能力,所有网络相关的配置(超时、拦截器、缓存、HTTPS)均需通过OkHttp实现,Retrofit仅负责将配置好的OkHttpClient“传入并使用”。  

一:设置全局超时时间、拦截器  

步骤一:自定义OkHttpClient,配置超时和拦截器:  

OkHttpClient okHttpClient = new OkHttpClient.Builder()          // 全局超时时间          .connectTimeout(10, TimeUnit.SECONDS)          .readTimeout(10, TimeUnit.SECONDS)          .writeTimeout(10, TimeUnit.SECONDS)          // 全局拦截器(如添加Token、打印日志)          .addInterceptor(new HeaderInterceptor()) // 应用拦截器          .addNetworkInterceptor(new LogInterceptor()) // 网络拦截器          .build();  

步骤二:初始化Retrofit时关联该OkHttpClient:  

Retrofit retrofit = new Retrofit.Builder()          .baseUrl(BASE_URL)          .client(okHttpClient) // 传入配置好的OkHttpClient          .addConverterFactory(GsonConverterFactory.create())          .build();  

二:与直接配置OkHttp的区别  

本质上没有区别,Retrofit的网络请求最终由OkHttp执行,因此配置OkHttpClient的超时、拦截器,就是配置Retrofit的网络行为,两者的关系是“Retrofit依赖OkHttp的配置”,而非“Retrofit有独立的配置体系”。  

唯一的注意点:若需为不同接口配置不同的超时/拦截器,需创建多个OkHttpClient实例,再分别初始化多个Retrofit实例(每个Retrofit关联一个OkHttpClient)。  

3.17、Retrofit如何处理网络请求的错误?(如HTTP错误码、网络异常、数据解析异常)  

Retrofit的错误可分为三类:网络异常HTTP错误码数据解析异常,需在请求回调或协程中分别捕获和处理。  

一:错误类型及捕获方式  

网络异常(无网络、超时、连接失败):

原因:设备未联网、网络信号差、服务器不可达、请求超时。  

异常类型:IOException(如SocketTimeoutException、UnknownHostException)。  

捕获:在Call.enqueue()的onFailure()或协程的catch块中捕获。  

HTTP错误码(4xx、5xx):

原因:请求参数错误(400)、未授权(401)、资源不存在(404)、服务器内部错误(500)等。  

特征:Response.isSuccessful()返回false(响应码不在200-299范围内)。  

处理:通过Response.code()获取错误码,Response.errorBody()获取错误信息(需转换为字符串)。  

数据解析异常(JSON格式不匹配):

原因:服务器返回的JSON与本地数据模型字段不匹配(如字段缺失、类型错误)。  

异常类型:JsonParseException(Gson)、MoshiJsonException(Moshi)。  

捕获:在Call.enqueue()的onFailure()或协程的catch块中捕获。  

二:处理示例  

使用Call<T>时的错误处理:  

userService.getUserById(123).enqueue(new Callback<User>() {      @Override      public void onResponse(Call<User> call, Response<User> response) {          if (response.isSuccessful()) {              // 成功:处理User对象              User user = response.body();          } else {              // 处理HTTP错误码              int errorCode = response.code();              String errorMsg = "";              try {                  // 解析错误响应体                  errorMsg = response.errorBody().string();              } catch (IOException e) {                  e.printStackTrace();              }              Log.e("ERROR""HTTP错误:" + errorCode + ",信息:" + errorMsg);          }      }      @Override      public void onFailure(Call<User> call, Throwable t) {          // 处理网络异常或解析异常          if (t instanceof IOException) {              Log.e("ERROR""网络异常:" + t.getMessage());          } else if (t instanceof JsonParseException) {              Log.e("ERROR""解析异常:" + t.getMessage());          }      }  });  

使用Kotlin协程时的错误处理:

viewModelScope.launch {      try {          val user = userService.getUserById(123)          // 成功:处理User对象      } catch (e: IOException) {          // 网络异常          Log.e("ERROR""网络异常:" + e.message)      } catch (e: JsonParseException) {          // 解析异常          Log.e("ERROR""解析异常:" + e.message)      } catch (e: HttpException) {          // HTTP错误(Retrofit将4xx/5xx包装为HttpException)          val errorCode = e.code()          val errorMsg = e.response()?.errorBody()?.string() ?: "未知错误"          Log.e("ERROR""HTTP错误:$errorCode,信息:$errorMsg")      }  }  

三:最佳实践  

统一错误处理:封装工具类(如ErrorHandler),集中处理不同类型的错误,避免重复代码。  

友好提示:根据错误类型向用户展示提示(如网络异常提示“检查网络连接”,401提示“登录已过期”)。  

日志记录:在Release版本中仅记录关键错误信息,避免敏感数据泄露,Debug版本可打印详细日志便于调试。  

四、数据序列化与反序列化(JSON解析)

4.1、Android中常用的JSON解析方式有哪些?(原生JSONObject/JSONArray、Gson、FastJson、Moshi)  

Android中常用的JSON解析方式可分为原生解析第三方库解析两类,核心差异在于易用性、性能和功能扩展性,具体如下:  

4.2、使用原生JSONObject解析一个复杂JSON对象(含嵌套结构)的步骤是什么?有什么缺点?  

一:复杂JSON示例  

假设需要解析以下嵌套JSON(用户信息,包含嵌套的地址对象和标签数组):  

{    "userId": 123,    "name": "张三",    "age": 20,    "address": {      "city": "北京",      "street": "朝阳路"    },    "tags": ["学生", "Android开发"]  }  

二:解析步骤  

需通过JSONObject(解析对象)和JSONArray(解析数组)逐层提取数据,步骤如下:  

public class User {      private int userId;      private String name;      private int age;      private Address address;      private List<String> tags;      // getter/setter 省略      public static class Address {          private String city;          private String street;          // getter/setter 省略      }  }  // 解析方法  public User parseUser(String jsonStr) {      User user = new User();      try {          // 1. 解析外层JSONObject          JSONObject rootObj = new JSONObject(jsonStr);          // 2. 提取基本类型字段          user.setUserId(rootObj.getInt("userId"));          user.setName(rootObj.getString("name"));          user.setAge(rootObj.getInt("age"));          // 3. 解析嵌套的Address对象          JSONObject addressObj = rootObj.getJSONObject("address");          User.Address address = new User.Address();          address.setCity(addressObj.getString("city"));          address.setStreet(addressObj.getString("street"));          user.setAddress(address);          // 4. 解析tags数组          JSONArray tagsArray = rootObj.getJSONArray("tags");          List<String> tags = new ArrayList<>();          for (int i = 0; i < tagsArray.length(); i++) {              tags.add(tagsArray.getString(i));          }          user.setTags(tags);      } catch (JSONException e) {          e.printStackTrace(); // 处理解析异常(如字段缺失、类型不匹配)      }      return user;  }  

三:原生解析的缺点  

代码繁琐:嵌套结构需逐层创建JSONObject/JSONArray,字段多时代码冗余,可读性差。  

易出错:需手动处理JSONException(如字段名拼写错误、类型不匹配),且无编译期检查。  

不支持泛型:解析List<T>等泛型集合时,需手动循环添加元素,无法自动映射。  

性能较差:复杂JSON(如多层嵌套、大数组)解析效率低于第三方库(如Gson、Moshi)。  

维护成本高:JSON结构变更时,需手动修改解析逻辑,扩展性差。  

4.3、Gson是什么?如何使用Gson将JSON字符串转换为Java/Kotlin对象(反序列化)?  

Gson的定义:Gson是Google开源的JSON解析库,核心能力是将JSON字符串自动反序列化为Java/Kotlin对象,以及将对象序列化为JSON字符串,它支持泛型、嵌套结构、自定义解析规则,是Android开发中最常用的JSON库之一。  

Gson反序列化的基本步骤:

1、集成Gson

在build.gradle中添加依赖:  

implementation 'com.google.code.gson:gson:2.10.1' // 最新版本参考官网  

2、定义数据模型(Java/Kotlin)

数据模型的字段名需与JSON的键名一致(或通过注解映射),代码示例:

data class User(        val userId: Int,        val name: String,        val age: Int,        val address: Address,        val tags: List<String>    )    data class Address(        val city: String,        val street: String    )  

3、执行反序列化  

通过Gson.fromJson()方法将JSON字符串转换为对象,核心代码:  

// 1. 创建Gson实例(可全局单例)  Gson gson = new Gson();  // 2. 反序列化:JSON字符串 → User对象  String jsonStr = "{\"userId\":123,\"name\":\"张三\",\"age\":20,...}";  User user = gson.fromJson(jsonStr, User.class);  // 3. 反序列化泛型集合(如List<User>)  String userListJson = "[{\"userId\":123,...}, {\"userId\":456,...}]";  Type userListType = new TypeToken<List<User>>(){}.getType(); // 解决泛型擦除  List<User> userList = gson.fromJson(userListJson, userListType);  

关键注意点:  

字段匹配:默认情况下,对象字段名需与JSON键名完全一致(大小写敏感),若不一致,需使用@SerializedName注解映射。  

泛型处理:解析List<T>、Map<K,V>等泛型类型时,需通过TypeToken获取泛型类型(因Java泛型擦除,无法直接用List<User>.class)。  

空值处理:JSON中缺失的字段,对象中对应字段会设为默认值(如int为0,String为null)。  

4.4、如何使用Gson将Java/Kotlin对象转换为JSON字符串(序列化)?如何自定义序列化规则(如字段重命名、忽略字段)?  

Gson序列化的基本步骤:通过Gson.toJson()方法将对象转换为JSON字符串,代码示例:  

// 1. 创建对象  User.Address address = new User.Address("北京""朝阳路");  User user = new User(123"张三"20, address, Arrays.asList("学生""Android开发"));  // 2. 序列化:User对象 → JSON字符串  Gson gson = new Gson();  String jsonStr = gson.toJson(user);  // 输出结果:{"userId":123,"name":"张三","age":20,"address":{"city":"北京","street":"朝阳路"},"tags":["学生","Android开发"]}  

自定义序列化规则:Gson支持通过注解或GsonBuilder自定义序列化规则,核心场景如下:  

字段重命名(@SerializedName):当对象字段名与JSON键名不一致时,使用@SerializedName映射:  

public class User {      @SerializedName("user_id") // JSON键名为user_id,对象字段名为userId      private int userId;      private String name;      // 其他字段省略  }  // 序列化后:{"user_id":123,"name":"张三"...}  

也支持多个别名(适配不同JSON格式):  

@SerializedName(value = "user_id", alternate = {"uid""userID"})  private int userId;  

忽略字段:

方式一:transient关键字:标记的字段不会参与序列化/反序列化。  

public class User {      private int userId;      transient private String password; // 忽略password字段      // 其他字段省略  }  // 序列化后:{"userId":123,...}(无password字段)

方式二:@Expose注解:配合GsonBuilder.excludeFieldsWithoutExposeAnnotation(),仅序列化标记@Expose的字段。  

public class User {      @Expose // 参与序列化      private int userId;      @Expose(serialize = false// 仅反序列化,不序列化      private String name;      private String password; // 未标记,不参与序列化    }  // 创建Gson实例  Gson gson = new GsonBuilder()          .excludeFieldsWithoutExposeAnnotation()          .build();  

方式三:GsonBuilder排除字段:通过字段名或类型排除。  

Gson gson = new GsonBuilder()         .excludeFieldsWithName("password"// 排除name为password的字段         .excludeFieldsWithModifiers(Modifier.TRANSIENT) // 排除transient字段        .build();  

自定义日期格式:默认情况下,Gson将Date对象序列化为时间戳,可通过GsonBuilder指定格式:  

Gson gson = new GsonBuilder()          .setDateFormat("yyyy-MM-dd HH:mm:ss"// 自定义日期格式          .build();  Date date = new Date();  String json = gson.toJson(date); // 输出:"2024-05-20 14:30:00"

4.5、Gson的TypeAdapter和TypeToken分别是什么?适用于什么场景?(如解析泛型集合、复杂数据类型)  

TypeToken:解决泛型擦除问题  

定义:TypeToken是Gson提供的工具类,用于获取泛型类型的Type对象(因Java编译时会擦除泛型信息,无法直接通过List<User>.class获取泛型类型)。  

适用场景:解析泛型集合(如List<T>、Map<K,V>)或复杂泛型类型(如Result<User>)。  

代码示例:

// 解析List<User>  Type userListType = new TypeToken<List<User>>(){}.getType();  List<User> userList = gson.fromJson(userListJson, userListType);  // 解析复杂泛型(如网络响应Result<User>)  public class Result<T> {     private int code;     private String msg;     private T data;     // getter/setter 省略  }  Type resultType = new TypeToken<Result<User>>(){}.getType();  Result<User> result = gson.fromJson(resultJson, resultType);  

TypeAdapter:自定义序列化/反序列化逻辑  

定义:TypeAdapter是Gson的核心接口,用于对特定类型(如Date、自定义对象)实现自定义的序列化和反序列化逻辑,相比注解,它更灵活,可处理复杂场景(如JSON结构与对象结构差异大、特殊数据格式转换)。

适用场景:  

处理特殊数据类型(如LocalDateTime、自定义枚举)。  

适配JSON与对象结构不一致的情况(如JSON中是字符串,对象中是int)。  

优化解析性能(相比默认解析,自定义TypeAdapter可减少反射开销)。 

代码示例(自定义Date类型的TypeAdapter):  

// 自定义TypeAdapter:将JSON中的日期字符串(yyyy-MM-dd)转换为Date对象  public class CustomDateTypeAdapter extends TypeAdapter<Date> {        private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");        @Override        public void write(JsonWriter out, Date value) throws IOException {            // 序列化:Date → JSON字符串            if (value == null) {                out.nullValue();                return;            }            out.value(sdf.format(value));        }        @Override        public Date read(JsonReader in) throws IOException {            // 反序列化:JSON字符串 → Date            if (in.peek() == JsonToken.NULL) {                in.nextNull();                return null;            }            String dateStr = in.nextString();            try {                return sdf.parse(dateStr);            } catch (ParseException e) {                throw new IOException("日期格式错误");            }        }    }    // 注册TypeAdapter到Gson    Gson gson = new GsonBuilder()            .registerTypeAdapter(Date.class, new CustomDateTypeAdapter())            .build();    // 使用:JSON中的"2024-05-20"会被解析为Date对象    String json = "{\"createTime\":\"2024-05-20\"}";    User user = gson.fromJson(json, User.class);  

4.6、FastJson相比Gson有哪些优势和风险?在Android开发中使用FastJson需要注意什么?

FastJson的优势:

解析速度快:FastJson采用“预编译”和“优化算法”,在解析大JSON或复杂数组时,性能通常优于Gson(官方benchmark数据)。  

API简洁:核心方法JSON.parseObject()(反序列化)和JSON.toJSONString()(序列化),无需创建实例,调用更便捷。  

  // FastJson反序列化(无需Gson的TypeToken,直接支持泛型)  

  List<User> userList = JSON.parseArray(userListJson, User.class);  

功能丰富:支持自动类型识别、JSONPath(快速提取JSON片段)、注解自定义等功能。  

FastJson的风险:

安全漏洞:历史上存在严重的反序列化漏洞(如autoType功能开启时,可能被注入恶意代码),虽后续版本修复,但仍需谨慎使用。  

稳定性问题:在解析复杂嵌套JSON(如多层泛型、特殊字符)时,可能出现解析错误或数据丢失,兼容性不如Gson。  

版本兼容问题:不同版本的API差异较大,升级时易出现兼容性问题(如旧版本的注解在新版本失效)。  

Kotlin支持差:对Kotlin的空安全、数据类(data class)支持不完善,易出现解析异常。  

Android开发中的注意事项:

使用最新稳定版本:避免使用存在安全漏洞的旧版本(如≤1.2.60),优先选择官方维护的最新版本。  

禁用autoType功能:autoType是反序列化漏洞的主要来源,非必要时禁用:  

// 禁用autoType  

ParserConfig.getGlobalInstance().setAutoTypeSupport(false);  

避免解析未知JSON:仅解析可信服务器返回的JSON,不解析用户输入或第三方未知JSON,防止注入攻击。  

谨慎处理复杂结构:复杂嵌套JSON优先使用Gson/Moshi,FastJson仅用于简单、可信的JSON解析场景。  

测试充分:集成后需针对不同JSON结构(正常、异常、边界值)进行测试,确保解析稳定性。  

4.7、Moshi与Gson的核心区别是什么?如何集成和使用?  

Moshi与Gson的核心区别:Moshi是Square公司开发的JSON解析库(与OkHttp/Retrofit同生态),设计上更轻量、更现代,与Gson的核心区别如下:  

Moshi的集成与使用:

集成Moshi,根据项目语言(Java/Kotlin)添加依赖:(Java项目) 

implementation 'com.squareup.moshi:moshi:1.15.1'  

Kotlin项目(推荐添加代码生成依赖,避免反射):  

implementation 'com.squareup.moshi:moshi:1.15.1'  implementation 'com.squareup.moshi:moshi-kotlin:1.15.1'  kapt 'com.squareup.moshi:moshi-kotlin-codegen:1.15.1' // 代码生成  

定义数据模型:(Kotlin示例)  

使用@Json注解映射字段(与Gson的@SerializedName类似):  

@JsonClass(generateAdapter = true) // 开启代码生成(Kotlin必需) data class User(      @Json(name = "user_id") // JSON键名为user_id,对象字段名为userId      val userId: Int,      val name: String,      val age: Int,      val address: Address,      val tags: List<String>  )  @JsonClass(generateAdapter = true)  data class Address(      val city: String,      val street: String  )  序列化与反序列化:// 1. 创建Moshi实例(Kotlin需传入KotlinJsonAdapterFactory)  val moshi = Moshi.Builder()      .add(KotlinJsonAdapterFactory()) // Kotlin项目必需      .build()  // 2. 获取JsonAdapter(数据模型与JSON的适配器)  val userAdapter = moshi.adapter(User::class.java)  // 3. 反序列化:JSON字符串 → User对象  val jsonStr = "{\"user_id\":123,\"name\":\"张三\",\"age\":20,...}"  val user = userAdapter.fromJson(jsonStr)  // 4. 序列化:User对象 → JSON字符串  val json = userAdapter.toJson(user)  // 5. 解析泛型集合(如List<User>)  val listAdapter = moshi.adapter<List<User>>(Types.newParameterizedType(List::class.java, User::class.java))  val userList = listAdapter.fromJson(userListJson)  

自定义JsonAdapter:(类似Gson的TypeAdapter)  

Moshi的JsonAdapter用于自定义解析逻辑,示例(处理LocalDateTime):  

class LocalDateTimeAdapter : JsonAdapter<LocalDateTime>() {      private val formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME      override fun fromJson(reader: JsonReader): LocalDateTime? {          return reader.nextString()?.let { LocalDateTime.parse(it, formatter) }      }      override fun toJson(writer: JsonWriter, value: LocalDateTime?) {          if (value == null) {              writer.nullValue()              return          }          writer.value(formatter.format(value))      }  }  // 注册到Moshi  val moshi = Moshi.Builder()      .add(KotlinJsonAdapterFactory())      .add(LocalDateTimeAdapter()) // 注册自定义JsonAdapter      .build()  

五、网络请求的异步处理与线程管理

5.1、Android中为什么必须在子线程执行网络请求?主线程的角色和限制是什么?  

Android必须在子线程执行网络请求,核心原因是主线程的职责性能限制,直接在主线程执行会导致严重的用户体验问题。  

一:主线程的角色  

主线程(又称UI线程)是Android应用的核心线程,主要负责两件事:  

处理UI渲染:执行onDraw()等方法绘制界面,刷新UI元素(如TextView、Button)。  

处理用户交互**:响应点击、滑动等输入事件,确保操作反馈及时(如按钮点击后立即响应)。  

二:主线程的限制  

Android对主线程有严格的响应时间限制:若主线程被阻塞超过5秒,系统会触发“应用无响应”(ANR),提示用户强制关闭App。  

网络请求(如HTTP请求)属于耗时操作,可能因以下原因耗时数百毫秒甚至几秒:  

1、网络延迟(如弱网、跨地域服务器)。

2、服务器处理时间(如复杂计算、数据库查询)。

3、数据传输时间(如大文件下载)。  

若在主线程执行网络请求,会直接阻塞主线程,导致:  

1、UI无法刷新(界面卡顿、白屏)。 

2、用户操作无响应(如点击按钮没反应)。

3、触发ANR,严重影响应用可用性。  

三:系统强制限制  

为避免上述问题,Android从API 11(Android 3.0)开始强制禁止主线程发起网络请求,若违反会抛出运行时异常:android.os.NetworkOnMainThreadException。  

5.2、Kotlin协程在Android网络请求中如何使用?(如viewModelScope、lifecycleScope的作用,如何处理异常)  

Kotlin协程是轻量级的异步编程框架,通过“挂起函数”(suspend)实现非阻塞异步,在Android网络请求中配合viewModelScope/lifecycleScope可自动管理生命周期,避免内存泄漏。  

一:核心Scope的作用  

协程必须在协程作用域(CoroutineScope)中执行,Android提供了两个与生命周期绑定的Scope,无需手动管理取消。

二:网络请求的基本使用(结合Retrofit)  

Retrofit 2.6.0+原生支持协程,接口方法只需添加suspend关键字:  

// 1. 定义Retrofit接口(suspend函数)  interface UserService {      @GET("user/{userId}")      suspend fun getUserById(@Path("userId") userId: Int): User  }  // 2. 在ViewModel中使用viewModelScope发起请求  class UserViewModel : ViewModel() {      private val userService = RetrofitManager.createService(UserService::class.java)      // 用于向UI层发送数据(如LiveData/StateFlow)      private val _userLiveData = MutableLiveData<User>()      val userLiveData: LiveData<User> = _userLiveData      fun fetchUser(userId: Int) {          // 启动协程(自动在子线程执行)          viewModelScope.launch {              try {                  // 调用suspend函数(挂起,不阻塞主线程)                  val user = userService.getUserById(userId)                  // 发送结果到UI层(viewModelScope.launch默认在主线程回调)                  _userLiveData.postValue(user)              } catch (e: Exception) {                  // 3. 异常处理(网络错误、解析错误等)                  when (e) {                      is IOException -> _errorLiveData.postValue("网络异常")                      is JsonParseException -> _errorLiveData.postValue("数据解析错误")                      else -> _errorLiveData.postValue("请求失败")                  }              }          }      }  }  // 3. UI层(Activity/Fragment)观察数据  class UserActivity : AppCompatActivity() {      override fun onCreate(savedInstanceState: Bundle?) {          super.onCreate(savedInstanceState)          val viewModel = ViewModelProvider(this)[UserViewModel::class.java]          viewModel.userLiveData.observe(this) { user ->              // 更新UI              textView.text = user.name          }      }  }  

三:异常处理关键点  

所有网络请求相关的异常(如IOException、HttpException、JsonParseException)都会被抛到catch块,需分类处理并提示用户。  

viewModelScope会在ViewModel销毁时自动取消协程,若请求已发起,suspend函数会抛出CancellationException,可选择性捕获(一般无需处理,因页面已销毁)。  

5.3、RxJava + Retrofit实现网络请求时,如何通过Observable/Flowable的操作符(如map、flatMap)处理数据流转?  

RxJava通过“可观测序列”(Observable/Flowable)和“操作符”(Operator)实现数据流转,结合Retrofit可灵活处理网络请求中的数据转换、嵌套请求等场景。  

核心概念:

Observable/Flowable:代表一个可观测的数据源(如网络请求结果),可发射0~N个数据,最终以完成(onComplete)或错误(onError)结束。  

Observable:适用于普通场景,不支持背压(数据产生速度超过消费速度时可能OOM)。  

Flowable:支持背压,适用于大数据量场景(如大列表、文件流)。  

操作符:对Observable/Flowable发射的数据进行转换、过滤、组合等处理,核心用于“数据流转”。  

map:数据转换(一对一)  

map操作符用于将上游发射的数据转换为另一种类型,适用于“单一数据转换”场景(如将网络请求返回的UserResponse转换为UI层需要的User模型)。  

代码示例:将Retrofit返回的UserResponse转换为User。

// 1. 定义Retrofit接口(返回Observable<UserResponse>)  public interface UserService {      @GET("user/{userId}")      Observable<UserResponse> getUserById(@Path("userId") int userId);  }  // 2. 使用map转换数据  UserService userService = RetrofitManager.createService(UserService.class);  userService.getUserById(123)          .map(userResponse -> {              // 转换:UserResponse → User(提取UI需要的字段)              return new User(                      userResponse.getUserId(),                      userResponse.getUserName(),                      userResponse.getUserAge()              );          })          .subscribeOn(Schedulers.io()) // 网络请求在IO线程          .observeOn(AndroidSchedulers.mainThread()) // 结果在主线程处理          .subscribe(                  user -> { /* 成功:更新UI */ },                  throwable -> { /* 失败:处理异常 */ }          );  

flatMap:嵌套请求(一对多)  

flatMap操作符用于“嵌套网络请求”场景(如先获取用户ID,再根据用户ID获取其订单列表),它会将上游发射的数据转换为新的Observable,并合并所有子Observable的结果。  

代码示例:先获取用户,再获取用户的订单列表:  

// 1. 定义Retrofit接口(用户、订单)  public interface UserService {      @GET("user/{userId}")      Observable<User> getUserById(@Path("userId") int userId);  }  public interface OrderService {      @GET("order")      Observable<List<Order>> getOrdersByUserId(@Query("userId") int userId);  }  // 2. 使用flatMap实现嵌套请求  UserService userService = RetrofitManager.createService(UserService.class);  OrderService orderService = RetrofitManager.createService(OrderService.class);  userService.getUserById(123)          .flatMap(user -> {              // 第一个请求成功后,发起第二个请求(根据用户ID获取订单)              return orderService.getOrdersByUserId(user.getUserId());          })          .subscribeOn(Schedulers.io())          .observeOn(AndroidSchedulers.mainThread())          .subscribe(                  orders -> { /* 成功:获取到用户的订单列表,更新UI */ },                  throwable -> { /* 失败:处理任一请求的异常 */ }          );

5.4、RxJava中如何切换线程?(subscribeOn和observeOn的区别,如何指定网络请求在子线程、结果回调在主线程)  

RxJava通过subscribeOn和observeOn实现线程切换,两者分工明确,核心区别在于“作用时机”和“生效范围”。  

subscribeOn与observeOn的核心区别: 

线程切换的实际使用:(网络请求+UI更新)  

Android开发中,网络请求需在IO子线程执行,结果处理需在主线程(更新UI),通过subscribeOn(Schedulers.io())和observeOn(AndroidSchedulers.mainThread())实现:  

// 1. 定义Retrofit接口(返回Observable<User>)  public interface UserService {      @GET("user/{userId}")      Observable<User> getUserById(@Path("userId") int userId);  }  // 2. 线程切换示例  UserService userService = RetrofitManager.createService(UserService.class);  userService.getUserById(123)          // 1. 指定网络请求(订阅)的线程:IO子线程(只生效一次)          .subscribeOn(Schedulers.io())          // 2. 第一个observeOn:切换到计算线程处理数据(如解析、转换)          .observeOn(Schedulers.computation())          .map(user -> {              // 数据处理:在计算线程执行              user.setNickname("用户:" + user.getName());              return user;          })          // 3. 第二个observeOn:切换到主线程更新UI          .observeOn(AndroidSchedulers.mainThread())          // 4. 订阅:结果在主线程处理          .subscribe(                  user -> {                      // 更新UI(主线程安全)                      textView.setText(user.getNickname());                  },                  throwable -> {                      // 异常处理(主线程)                      Toast.makeText(context, "请求失败", Toast.LENGTH_SHORT).show();                  }          );  

关键注意点:

subscribeOn的唯一性:即使多次调用subscribeOn(Schedulers.io()),也只会生效第一次,后续调用无效。  

observeOn的多次使用:可根据需求多次切换线程(如IO线程请求 → 计算线程处理数据 → 主线程更新UI)。  

线程池选择:  

Schedulers.io():用于IO密集型操作(网络请求、文件读写)。  

Schedulers.computation():用于CPU密集型操作(数据计算、解析)。  

AndroidSchedulers.mainThread():Android主线程(仅用于更新UI)。  

5.5、Jetpack中的LiveData如何与网络请求结合?如何避免屏幕旋转等配置变更导致的请求重复执行?  

LiveData是具有生命周期感知能力的数据持有者,与网络请求结合时,可确保数据自动同步到UI,且在配置变更(如屏幕旋转)时避免重复请求。  

一:LiveData与网络请求的结合方式  

核心思路:在ViewModel中发起网络请求,将结果存入LiveData,UI层(Activity/Fragment)观察LiveData,实现“数据驱动UI”,代码示例:  

// 1. ViewModel中封装网络请求和LiveData  class UserViewModel : ViewModel() {      private val userService = RetrofitManager.createService(UserService::class.java)      // 私有MutableLiveData(仅ViewModel内部修改)      private val _userLiveData = MutableLiveData<User>()      // 公开LiveData(UI层只读观察)      val userLiveData: LiveData<User> = _userLiveData      // 发起网络请求      fun fetchUser(userId: Int) {          viewModelScope.launch {              try {                  val user = userService.getUserById(userId)                  // 将结果存入LiveData(自动通知观察者)                  _userLiveData.postValue(user)              } catch (e: Exception) {                  // 处理异常(可通过另一个LiveData发送错误信息)              }          }      }  }  // 2. UI层(Activity)观察LiveData  class UserActivity : AppCompatActivity() {      override fun onCreate(savedInstanceState: Bundle?) {          super.onCreate(savedInstanceState)          val viewModel = ViewModelProvider(this)[UserViewModel::class.java]          // 观察LiveData(生命周期感知,页面销毁时自动取消观察)          viewModel.userLiveData.observe(this) { user ->              // 配置变更(如旋转)后,LiveData会重新发送最新数据,无需重复请求              textView.text = user.name          }          // 发起请求(仅在首次创建时调用,配置变更后不会重复调用)          if (savedInstanceState == null) {              viewModel.fetchUser(123)          }      }  }  

二:避免配置变更重复请求的核心方法  

配置变更(如屏幕旋转)会导致Activity/Fragment重建,若不处理,可能重复调用fetchUser()发起网络请求,关键解决思路是“数据缓存”和“请求状态管理”:  

1、利用ViewModel缓存数据  

ViewModel的生命周期独立于UI(Activity重建时ViewModel不销毁),因此LiveData中存储的网络请求结果会被缓存,UI重建后,观察LiveData会立即收到最新数据,无需重新请求。  

2、检查请求状态(避免重复发起)  

若请求耗时较长,可在ViewModel中添加“请求状态”标记(如isLoading),确保同一时间只发起一次请求:  

class UserViewModel : ViewModel() {      private var isLoading = false // 请求状态标记      fun fetchUser(userId: Int) {          if (isLoading) return // 若正在请求,直接返回          isLoading = true          viewModelScope.launch {              try {                  val user = userService.getUserById(userId)                  _userLiveData.postValue(user)              } catch (e: Exception) {                  // 处理异常              } finally {                  isLoading = false // 请求结束,重置状态              }          }      }  }  

3、使用Transformations.switchMap(动态请求)  

若请求参数(如用户ID)动态变化,可使用Transformations.switchMap将参数变化转换为新的LiveData,避免手动管理请求:  

class UserViewModel : ViewModel() {      // 存储用户ID的LiveData(参数变化触发新请求)      private val _userIdLiveData = MutableLiveData<Int>()      // 通过switchMap,userId变化时自动发起新请求      val userLiveData: LiveData<User> = Transformations.switchMap(_userIdLiveData) { userId ->          liveData(viewModelScope.coroutineContext) {              try {                  val user = userService.getUserById(userId)                  emit(user) // 发送结果              } catch (e: Exception) {                  // 处理异常              }          }      }      // 外部调用:更新用户ID,触发新请求      fun setUserId(userId: Int) {          _userIdLiveData.value = userId      }  }  

5.6、Jetpack中的ViewModel在网络请求中起到什么作用?如何通过ViewModel + 网络库实现数据与UI的解耦?  

ViewModel是Android Jetpack的核心组件,在网络请求中扮演“数据持有者”和“逻辑封装者”的角色,核心价值是实现“数据与UI的解耦”和“配置变更时数据持久化”。  

一:ViewModel在网络请求中的核心作用  

数据持久化(配置变更无关):ViewModel的生命周期与Activity/Fragment的“配置变更”无关(如屏幕旋转、语言切换时,ViewModel不销毁),因此网络请求的结果会被缓存,UI重建后无需重复请求。  

封装网络请求逻辑:将网络请求的发起、数据处理、异常捕获等逻辑封装在ViewModel中,UI层(Activity/Fragment)只需调用ViewModel的方法,不关心请求细节(如用Retrofit还是OkHttp,如何切换线程)。  

隔离UI与数据:ViewModel不持有Activity/Fragment的引用(避免内存泄漏),通过LiveData/StateFlow向UI层发送数据,UI层只负责“观察数据并更新UI”,不参与数据获取逻辑。  

生命周期安全:结合viewModelScope,网络请求会在ViewModel销毁时自动取消,避免内存泄漏(如页面关闭后请求仍在执行,导致回调时Activity已销毁)。  

二:实现数据与UI解耦的示例  

通过“ViewModel封装请求 + LiveData传递数据 + UI层观察数据”的流程,实现解耦:  

// 1. 数据模型(UI与ViewModel共享,无业务逻辑)  data class User(val userId: Intval name: String, val age: Int)  // 2. 网络请求接口(Retrofit)  interface UserService {      @GET("user/{userId}")      suspend fun getUserById(@Path("userId") userId: Int): User  }  // 3. ViewModel(封装请求逻辑,持有数据)  class UserViewModel : ViewModel() {      private val userService = RetrofitManager.createService(UserService::class.java)      private val _userLiveData = MutableLiveData<User>()      val userLiveData: LiveData<User> = _userLiveData      private val _errorLiveData = MutableLiveData<String>()      val errorLiveData: LiveData<String> = _errorLiveData      // 对外暴露的方法(UI层调用)      fun loadUser(userId: Int) {          viewModelScope.launch {              try {                  val user = userService.getUserById(userId)                  _userLiveData.postValue(user) // 发送数据到UI              } catch (e: IOException) {                  _errorLiveData.postValue("网络异常,请重试")              } catch (e: Exception) {                  _errorLiveData.postValue("请求失败")              }          }      }  }  // 4. UI层(Activity,仅观察数据和更新UI)  class UserActivity : AppCompatActivity() {      override fun onCreate(savedInstanceState: Bundle?) {          super.onCreate(savedInstanceState)          setContentView(R.layout.activity_user)          val viewModel = ViewModelProvider(this)[UserViewModel::class.java]          // 观察用户数据,更新UI          viewModel.userLiveData.observe(this) { user ->              tvUserId.text = "ID: ${user.userId}"              tvUserName.text = "姓名: ${user.name}"              tvUserAge.text = "年龄: ${user.age}"          }          // 观察错误信息,提示用户          viewModel.errorLiveData.observe(this) { errorMsg ->              Toast.makeText(this, errorMsg, Toast.LENGTH_SHORT).show()          }          // 发起请求(UI层只调用方法,不关心内部逻辑)          btnLoadUser.setOnClickListener {              viewModel.loadUser(123)          }      }  }  

三:解耦的核心体现  

UI层无网络逻辑:Activity中没有Retrofit、协程、线程切换的代码,只负责用户交互(点击按钮)和UI更新。  

ViewModel无UI引用:ViewModel通过LiveData发送数据,不持有Activity的TextView、Button等引用,避免内存泄漏。  

可测试性提升:ViewModel的网络请求逻辑可单独测试(如用MockWebServer模拟服务器响应),无需依赖UI。  

5.7、什么是“网络请求防抖”?如何避免用户快速点击按钮导致的重复网络请求?(如使用Debounce、ThrottleFirst、标志位)  

网络请求防抖”(Debounce)是指过滤短时间内的重复请求,避免因用户快速操作(如连续点击按钮)导致多次发起相同网络请求(如重复提交订单、重复登录),从而减少服务器压力和避免数据异常。  

一:常见实现方式  

1、标志位(简单直接,适用于基础场景)  

通过一个布尔变量(如isRequesting)标记请求状态,请求发起时设为true,请求结束(成功/失败)后设为false,避免重复发起,代码示例:  

class UserActivity : AppCompatActivity() {      private val userService = RetrofitManager.createService(UserService::class.java)      private var isRequesting = false // 请求状态标志位      override fun onCreate(savedInstanceState: Bundle?) {          super.onCreate(savedInstanceState)          btnLogin.setOnClickListener {              if (isRequesting) return // 若正在请求,直接返回              val username = etUsername.text.toString()              val password = etPassword.text.toString()              login(username, password)          }      }      private fun login(username: String, password: String) {          isRequesting = true          viewModelScope.launch {              try {                  val response = userService.login(username, password)                  // 处理登录成功              } catch (e: Exception) {                  // 处理登录失败              } finally {                  isRequesting = false // 请求结束,重置标志位              }          }      }  }  

优点:实现简单,无额外依赖。

缺点:无法处理复杂场景(如用户快速点击多次后,只有第一次有效,后续点击被直接忽略)。  

二:RxJava的Debounce(延迟执行,过滤快速重复操作)  

Debounce操作符会延迟一段时间执行任务,若在延迟期间有新的操作触发,则取消前一个任务,重新计时,适用于“搜索输入防抖”(用户输入停止后再发起搜索请求)或“按钮点击防抖”(用户停止点击后再执行请求),代码示例(按钮点击防抖):  

// 1. 将按钮点击事件转换为Observable  Observable<Object> clickObservable = RxView.clicks(btnLogin)          // Debounce:延迟500ms执行,若500ms内再次点击,重新计时          .debounce(500TimeUnit.MILLISECONDS)          .subscribeOn(AndroidSchedulers.mainThread());  // 2. 订阅点击事件,发起网络请求  clickObservable.flatMap(o -> {      String username = etUsername.getText().toString();      String password = etPassword.getText().toString();      // 发起登录请求(IO线程)      return userService.login(username, password)              .subscribeOn(Schedulers.io())              .observeOn(AndroidSchedulers.mainThread());  })  .subscribe(          response -> { /* 处理成功 */ },          throwable -> { /* 处理失败 */ }  );  

优点:灵活过滤快速重复操作。

缺点:依赖RxJava,有一定学习成本,按钮点击后需等待延迟时间才执行,可能影响用户体验。  

三:RxJava的ThrottleFirst(一段时间内只执行一次)  

ThrottleFirst操作符会在指定时间窗口内只执行第一次操作,忽略后续所有操作,适用于“按钮点击防抖”(如1秒内只允许点击一次),相比Debounce,用户点击后立即执行,体验更好,代码示例(按钮点击防抖):  

// 1. 将按钮点击事件转换为Observable  Observable<Object> clickObservable = RxView.clicks(btnLogin)          // ThrottleFirst:1秒内只执行第一次点击          .throttleFirst(1TimeUnit.SECONDS)          .subscribeOn(AndroidSchedulers.mainThread());  // 2. 订阅点击事件,发起网络请求  clickObservable.flatMap(o -> {      String username = etUsername.getText().toString();      String password = etPassword.getText().toString();      return userService.login(username, password)              .subscribeOn(Schedulers.io())              .observeOn(AndroidSchedulers.mainThread());  })  .subscribe(          response -> { /* 处理成功 */ },          throwable -> { /* 处理失败 */ }  );  

优点:用户点击后立即执行,体验好。

缺点:依赖RxJava;时间窗口内的后续点击被直接忽略,需合理设置窗口时长(如500ms~1s)。  

选型建议:

简单场景(如登录按钮):优先使用标志位,实现成本低。  

搜索输入防抖:必须使用Debounce(延迟执行,避免频繁请求)。  

按钮点击(需立即反馈):使用ThrottleFirst(平衡体验和防重复)。  

【声明】内容源于网络
0
0
图解Android开发
该公众号精心绘制安卓开发知识总结图谱,将零散的知识点串联起来,形成完整的知识体系,无论是新手搭建知识框架,还是老手查漏补缺,这些图谱都能成为你学习路上的得力助手,帮助你快速定位重点、难点,提升学习效率。
内容 21
粉丝 0
图解Android开发 该公众号精心绘制安卓开发知识总结图谱,将零散的知识点串联起来,形成完整的知识体系,无论是新手搭建知识框架,还是老手查漏补缺,这些图谱都能成为你学习路上的得力助手,帮助你快速定位重点、难点,提升学习效率。
总阅读126
粉丝0
内容21