网络通信是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
<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<String, Void, String> {// 1. 后台执行(子线程):发起网络请求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. 主线程回调:处理结果并更新UIprotected 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()) {public void handleMessage( Message msg) {super.handleMessage(msg);// 3. 主线程处理消息:更新UIif (msg.what == 1) { // 消息标识String result = (String) msg.obj; // 消息内容textView.setText(result);}}};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(10, TimeUnit.SECONDS) // 连接超时.readTimeout(10, TimeUnit.SECONDS) // 读取超时.writeTimeout(10, TimeUnit.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-299String 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() {public void onFailure( Call call, IOException e) {// 请求失败(如网络错误、超时)e.printStackTrace();}public void onResponse( Call call, 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 {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";}}// 配置到OkHttpClientOkHttpClient client = new OkHttpClient.Builder().addInterceptor(new HeaderInterceptor()) // 应用拦截器.build();
二:请求/响应日志打印,使用应用拦截器或网络拦截器
推荐使用官方LoggingInterceptor(需添加依赖),也可自定义:
// 自定义日志拦截器(简化版)public class LogInterceptor implements Interceptor {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 {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; // 10MBCache 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 // 时间单位);// 配置到OkHttpClientOkHttpClient client = new OkHttpClient.Builder().connectionPool(connectionPool).build();
注意:连接池参数需根据业务调整,如高频访问同一服务器可增大最大空闲数,减少连接创建开销。
3.8、OkHttp如何处理HTTPS证书?(如信任所有证书、信任自定义证书、证书pinning配置)
HTTPS通信需验证服务器证书,OkHttp默认信任系统预装的CA证书(如Let's Encrypt),对于自签名证书或私有CA,需手动配置信任策略。
一:信任所有证书(仅用于测试,生产环境禁止)
跳过证书验证(风险高,易受中间人攻击):
// 创建信任所有证书的TrustManagerX509TrustManager trustAllCert = new X509TrustManager() {public void checkClientTrusted(X509Certificate[] chain, String authType) {}public void checkServerTrusted(X509Certificate[] chain, String authType) {}public X509Certificate[] getAcceptedIssuers() {return new X509Certificate[0];}};// 创建SSLSocketFactorySSLContext sslContext = SSLContext.getInstance("TLS");sslContext.init(null, new TrustManager[]{trustAllCert}, new SecureRandom());SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();// 配置OkHttpClientOkHttpClient 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();// 创建SSLSocketFactorySSLContext sslContext = SSLContext.getInstance("TLS");sslContext.init(null, trustManagers, new SecureRandom());SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();// 配置OkHttpClientOkHttpClient 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/"; // 基础URLpublic 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请求:路径参数userIdCall<User> getUserById( int userId);}
步骤三:调用接口
// 获取接口实例UserService userService = RetrofitManager.createService(UserService.class);// 发起异步请求Call<User> call = userService.getUserById(123);call.enqueue(new Callback<User>() {public void onResponse(Call<User> call, Response<User> response) {if (response.isSuccessful()) {User user = response.body(); // 响应体(已被Gson解析为User对象)// 更新UI...} else {// 处理HTTP错误(如404、500)}}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)Call<User> addUser( 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>() {public void onResponse(Call<User> call, Response<User> response) {if (response.isSuccessful()) {User addedUser = response.body(); // 服务器返回的新增用户信息}}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 {Observable<User> getUserById( 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函数,返回Usersuspend fun getUserById( userId: Int): User// POST请求:suspend函数,返回AddUserResponsesuspend fun addUser( 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(1, 20) }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>() {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);}}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. 解析外层JSONObjectJSONObject 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 {// JSON键名为user_id,对象字段名为userIdprivate int userId;private String name;// 其他字段省略}// 序列化后:{"user_id":123,"name":"张三"...}
也支持多个别名(适配不同JSON格式):
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 {// 参与序列化private int userId;(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");public void write(JsonWriter out, Date value) throws IOException {// 序列化:Date → JSON字符串if (value == null) {out.nullValue();return;}out.value(sdf.format(value));}public Date read(JsonReader in) throws IOException {// 反序列化:JSON字符串 → Dateif (in.peek() == JsonToken.NULL) {in.nextNull();return null;}String dateStr = in.nextString();try {return sdf.parse(dateStr);} catch (ParseException e) {throw new IOException("日期格式错误");}}}// 注册TypeAdapter到GsonGson 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类似):
// 开启代码生成(Kotlin必需)data class User(// JSON键名为user_id,对象字段名为userIdval userId: Int,val name: String,val age: Int,val address: Address,val tags: List<String>)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_TIMEoverride 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))}}// 注册到Moshival 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 {suspend fun getUserById( 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> = _userLiveDatafun 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 ->// 更新UItextView.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 {Observable<UserResponse> getUserById( 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 {Observable<User> getUserById( int userId);}public interface OrderService {Observable<List<Order>> getOrdersByUserId( 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 {Observable<User> getUserById( 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中封装网络请求和LiveDataclass 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)观察LiveDataclass 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 = trueviewModelScope.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: Int, val name: String, val age: Int)// 2. 网络请求接口(Retrofit)interface UserService {suspend fun getUserById( 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> = _userLiveDataprivate 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]// 观察用户数据,更新UIviewModel.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 = trueviewModelScope.launch {try {val response = userService.login(username, password)// 处理登录成功} catch (e: Exception) {// 处理登录失败} finally {isRequesting = false // 请求结束,重置标志位}}}}
优点:实现简单,无额外依赖。
缺点:无法处理复杂场景(如用户快速点击多次后,只有第一次有效,后续点击被直接忽略)。
二:RxJava的Debounce(延迟执行,过滤快速重复操作)
Debounce操作符会延迟一段时间执行任务,若在延迟期间有新的操作触发,则取消前一个任务,重新计时,适用于“搜索输入防抖”(用户输入停止后再发起搜索请求)或“按钮点击防抖”(用户停止点击后再执行请求),代码示例(按钮点击防抖):
// 1. 将按钮点击事件转换为ObservableObservable<Object> clickObservable = RxView.clicks(btnLogin)// Debounce:延迟500ms执行,若500ms内再次点击,重新计时.debounce(500, TimeUnit.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. 将按钮点击事件转换为ObservableObservable<Object> clickObservable = RxView.clicks(btnLogin)// ThrottleFirst:1秒内只执行第一次点击.throttleFirst(1, TimeUnit.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(平衡体验和防重复)。

