首页 / 资讯中心 / 文章详情

Android网络编程入门:从HTTP请求到JSON解析的完整实践

Android网络编程入门:从HTTP请求到JSON解析的完整实践 ★ FEATURED ARTICLE
这一讲终于轮到网络编程了。很多新手学到Android入门的中段最容易卡住的就是网络这一块界面上明明写了按钮点了却没反应Logcat里抛一堆异常甚至App直接闪退。其实网络编程没想象中那么玄乎无非是“让App去服务器拿数据、把数据变成界面能显示的内容”这么一件事。不过它确实和前面的基础组件不一样不需要再有“页面布局”“生命周期”这类直观的东西而是引入了并发、数据格式、接口协议这些概念思维要转个弯。这篇内容我打算按实际学习的顺序来写先搞清楚网络访问到底是干嘛的再写一段最基础的HTTP请求代码接着解决“必须在子线程里访问网络”这个关键问题并详细拆解创建线程的两种方法最后专门聊一聊从网络响应里获取多个值的几种常见思路。整篇都是我从带新人和自己调试中踩过坑之后总结下来的内容代码全部可以直接抄。1. 网络访问到底在做些什么1.1 客户端与服务器的“对话”本质网络访问说白了就是你的手机App作为客户端向某个服务器发出一个“请求”然后把服务器返回的“响应”拿回来处理。你可以把它想象成去餐厅点餐你拿着菜单请求参数告诉服务员你要什么菜服务员把订单传给后厨服务器处理后厨做好了端上来响应数据你才能吃到东西。在这个过程里有一个概念必须一开始就建立起来网络数据本质上只是一串字节流。无论服务器返回的是网页、图片还是结构化数据到你的手机里就是一段一段的二进制或文本内容。你在界面上看到的“当前的天气是晴天23度”全都是从这串原始数据里“解析”出来的。Android早在系统层面就为你封装好了网络接口你要做的无非三件事构造一个请求指明你要访问的地址URL和方式GET、POST等。发起请求把数据从服务器拉下来。解析返回内容把字节流转成Java对象或UI能用的数据。很多新手一上来就想着“用什么框架”。我的建议是第一遍学网络编程一定先手写一遍最原始的HTTP请求哪怕代码很啰嗦。因为只有手写过一遍你才会明白框架到底帮你做了什么。后面无论是用OkHttp还是Retrofit心里都有一张底图出了问题也知道去哪找原因。1.2 网络权限与基础环境准备动手写代码之前有一个特别容易忽略的准备工作给App加上网络访问权限。没有这个权限你的网络请求会在运行时直接抛SecurityException而且很多人会花半天时间反复检查代码却找不到原因。在AndroidManifest.xml里在manifest标签下直接加上这一行uses-permission android:nameandroid.permission.INTERNET /注意这个权限是“普通权限”不需要在代码里动态申请只要在清单文件声明即可。大多数新手项目模板默认不带这条所以一写网络就崩溃。另外还有一个坑和Android系统的网络策略有关从Android 9API 28开始系统默认禁止应用访问明文HTTP流量。如果你用来测试的接口是http://而不是https://请求会直接报Cleartext HTTP traffic not permitted。入门阶段为了调通流程可以在manifest的application里临时加一个属性application android:usesCleartextTraffictrue ...注意这只是本地调试的临时方案。做正式项目时接口尽量走HTTPS不要依赖这个开关。我在实际开发里见过有的App图省事一直开着明文流量结果上线后被安全扫描直接打回。网络环境准备好之后就可以正式写代码访问服务器了。下一节我们先从最核心的HTTP请求讲起。2. HTTP访问网络一次请求的完整生命周期2.1 HTTP的基本约定请求方法、URL、响应体HTTP协议是整个网络编程的基石。你写代码时不需要把协议细节全背下来但至少要知道几个关键要素。第一是URL也就是你要访问的地址。它由协议http/https、主机名、端口、路径和查询参数组成。比如https://example.com/api/weather?citybeijing里citybeijing就是传给服务器的查询参数用GET方式传参就是把它拼在URL后面。第二是请求方法。入门阶段最常见的是GET和POST。GET用于获取数据比如查询天气、拉取列表POST用于提交数据比如登录、注册。现代项目里还会用到PUT、DELETE但本质都是“告诉服务器你想做什么”。第三是响应体。服务器返回的数据通常以文本形式出现最常见的就是JSON字符串。比如{ code: 0, message: success, data: { city: 北京, temperature: 23 } }这串文本就是你要解析的“多个值”的来源。Android原生环境并不会自动帮你把JSON变成对象所以你需要自己写解析逻辑或者在工程里引入Gson之类的第三方库。这部分我们在第4节细讲。2.2 手写一段HttpURLConnection请求流程Android入门阶段最推荐的原始HTTP工具是HttpURLConnection。它是JDK自带的类在Android上也能直接使用不需要额外引入依赖。写网络请求的基本步骤是固定的我直接给出一个完整的请求模板import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.URL; public class HttpUtil { public static String get(String urlStr) { HttpURLConnection connection null; BufferedReader reader null; try { URL url new URL(urlStr); connection (HttpURLConnection) url.openConnection(); // 设置请求方式、超时时间 connection.setRequestMethod(GET); connection.setConnectTimeout(8000); connection.setReadTimeout(8000); // 请求头有的服务器会校验User-Agent connection.setRequestProperty(User-Agent, Android); // 获取响应码200表示成功 int code connection.getResponseCode(); if (code 200) { // 得到输入流读取服务器返回的文本 InputStream inputStream connection.getInputStream(); reader new BufferedReader(new InputStreamReader(inputStream, UTF-8)); StringBuilder result new StringBuilder(); String line; while ((line reader.readLine()) ! null) { result.append(line); } return result.toString(); } else { return 请求失败响应码 code; } } catch (Exception e) { e.printStackTrace(); return 网络异常 e.getMessage(); } finally { // 随手关闭资源防止连接泄漏 try { if (reader ! null) reader.close(); } catch (Exception ignored) { } if (connection ! null) connection.disconnect(); } } }这套模板有几个细节值得注意getResponseCode()用来判断请求是否成功200是标准成功码不代表你拿到了正确数据只代表服务器正常回话了。连接超时和读取超时一定要设置。否则服务器一直不响应你的线程就会卡在那里尤其对Android来说卡住的线程很可能最后触发ANR。读取响应时我指定了UTF-8编码这是中文不乱码的关键。很多新手直接读流然后转成字符串遇到中文乱码问题就在这里。实际使用的时候你只要传入URL字符串就能拿到服务器返回的原始文本。注意这个返回值还是JSON字符串、HTML字符串这类文本还需要进一步解析才能真正变成界面上的文字。2.3 为什么不能在主线程直接访问网络上面这段代码如果直接放在onClick事件里执行你会立刻看到一个异常android.os.NetworkOnMainThreadException这个异常是Android为了强制保证流畅性专门设计的。主线程也叫UI线程负责刷新界面、处理用户点击任何耗时操作都会阻塞它。网络请求少则几百毫秒多则几秒如果让主线程干等用户点一下按钮界面就卡死了体验极差系统也会在超过一定时间后提示ANR。所以Android的规则是网络访问必须在子线程里做UI更新只能在主线程里做。这两句话是新手最容易混淆的。很多人的第一反应是“我在子线程请求网络然后直接在子线程里更新UI”这样一样会崩溃因为子线程不能碰UI。正确的流程是子线程里做网络请求得到数据再切换到主线程把数据更新到界面组件上。切换主线程的方法有很多最简单的是runOnUiThread或者用Handler.post。下一节就来解决“子线程从哪来”的问题。3. 创建线程的两种方法继承Thread还是实现Runnable3.1 方法一继承Thread类重写run()创建线程的第一种方法是直接继承Thread类在子类里重写run()方法。run()里写的代码就是子线程要执行的任务。启动线程调用start()而不是直接调run()。class HttpThread extends Thread { private String url; public HttpThread(String url) { this.url url; } Override public void run() { // 这里就是子线程 String result HttpUtil.get(url); // 拿到结果后切回主线程更新UI runOnUiThread(new Runnable() { Override public void run() { textView.setText(result); } }); } }使用的时候在按钮点击事件里创建线程对象并启动new HttpThread(https://example.com/api/weather?citybeijing).start();这种写法足够简单但有个天然限制Java是单继承的如果一个类已经继承了别的父类就不能再继承Thread了。另外继承整个Thread类其实有点儿“重”因为线程对象的创建和任务逻辑耦合在一起不够灵活。3.2 方法二实现Runnable接口配合Thread启动第二种方法是实现Runnable接口。Runnable本身不是一个线程它只是一个“任务”描述了线程要执行的动作。真正要跑线程还需要把这个任务交给一个Thread对象。class HttpRunnable implements Runnable { private String url; public HttpRunnable(String url) { this.url url; } Override public void run() { // 子线程任务内容 String result HttpUtil.get(url); runOnUiThread(new Runnable() { Override public void run() { textView.setText(result); } }); } }启动方式Thread thread new Thread(new HttpRunnable(https://example.com/api/weather?citybeijing)); thread.start();熟悉Java 8之后你还可以直接写Lambda表达式代码会简洁很多new Thread(() - { String result HttpUtil.get(url); runOnUiThread(() - textView.setText(result)); }).start();注意很多人会把run()和start()搞混。直接调用run()只是在当前线程里顺序执行那段代码并没有创建新线程只有调用start()系统才会真的开辟一个子线程去执行run()。3.3 两种方法怎么选我的实践经验从功能上讲两种方法都能实现网络请求。但从工程习惯来说我更推荐实现Runnable接口理由有三点解耦更清晰。让“要执行的任务”和“如何运行任务”分开任务代码可以复用。不受单继承限制。如果这个业务类还需要继承其他基类Runnable写法不会导致冲突。便于后续使用线程池。现代项目里很少直接new Thread而是用ExecutorService或者AsyncTask这类封装好的调度工具。而线程池接受的就是Runnable或Callable不是Thread子类。我带新人时见过很多人因为图省事一直用继承Thread方式直到某天业务类需要继承别的类才发现要重构一堆代码。入门阶段两种都练一下没问题但心里要清楚哪种是主流方向。注意子线程里直接更新UI是绝对不允许的。我在3.1和3.2的代码里都用了runOnUiThread切回主线程这个习惯从第一天就要养成。以前见过一个学员觉得“有时候能跑通”其实那是线程切换时机碰巧赶上了但不代表它是合法的迟早会崩溃。4. 网络获取多个值的方法从JSON文本到结构化数据4.1 为什么拿到的是文本不是“值”当你的HttpUtil.get()方法返回一个字符串时很多新手会愣住我要的是“城市名”“温度”“风力”这些数据这堆文本是什么鬼答案很简单网络传输只能传文本。服务器把结构化数据序列化成一串JSON文本发给你你需要反序列化也就是解析才能得到多个值。比如服务器返回{ city: 北京, temperature: 23, wind: 东北风3级 }这串文本里其实有3个值city、temperature、wind。把它们分别取出来的过程就是你“从网络获取多个值”的核心操作。在Android原生环境里最直接的工具是org.json包里的JSONObject和JSONArray它们不需要额外引第三方库是系统内置的。4.2 JSONObject从对象中提取多个字段JSONObject代表一个JSON对象你可以把它理解成Java里一个Map键是你熟悉的字段名值就是你要的数据。public void parseWeather(String jsonText) { try { JSONObject jsonObject new JSONObject(jsonText); // 一次取出多个值 String city jsonObject.getString(city); String temperature jsonObject.getString(temperature); String wind jsonObject.getString(wind); textView.setText(city temperature wind); } catch (JSONException e) { e.printStackTrace(); } }这里有一个特别常见的坑如果服务器返回的字段键名和代码里写的不一致getString会抛出JSONException代码直接跳到catch里界面没有任何反应。很多新手排查半天找不到原因最后把服务器返回的JSON打印出来一看才发现字段名是temp而不是temperature。所以我建议你在拿到原始文本后第一时间先Log打印一遍确认字段名再写解析逻辑。时刻记住服务器返回什么结构是写接口的人决定的你写的代码必须严格对齐它。4.3 处理JSON数组一次返回好几个同类数据“获取多个值”还有一种常见形态是一个JSON数组比如{ code: 0, list: [ {name: 北京, temperature: 23}, {name: 上海, temperature: 26}, {name: 广州, temperature: 30} ] }这时你需要先用JSONObject拿到外层对象再用getJSONArray(list)取出数组然后循环遍历数组每一个元素又是一个JSONObject。public void parseList(String jsonText) { try { JSONObject root new JSONObject(jsonText); JSONArray array root.getJSONArray(list); StringBuilder sb new StringBuilder(); for (int i 0; i array.length(); i) { JSONObject item array.getJSONObject(i); String name item.getString(name); String temperature item.getString(temperature); sb.append(name).append().append(temperature).append(\n); } textView.setText(sb.toString()); } catch (JSONException e) { e.printStackTrace(); } }这里最容易出的问题是数组下标越界或类型不匹配。比如你以为list里全是对象结果里面有个别字段是数组或字符串getJSONObject(i)就会报错。真遇到这种情况先打印array.get(i)的类型再决定怎么解析。4.4 用Gson批量转成对象列表手写JSON解析其实挺繁琐的尤其是字段多、层级深的时候大量重复代码还容易出错。所以现代Android开发中Gson几乎成了标配。Gson最强大的地方是“一行代码把JSON文本转成Java对象”尤其适合批量获取多个值的场景。先定义好一个与JSON字段对应的Java类public class Weather { private String name; private String temperature; // 必须提供无参构造函数和getter/setter或者直接用public字段 // 这里为了演示直接使用public字段 public String name; public String temperature; }然后解析时如果是单个对象Weather weather new Gson().fromJson(jsonText, Weather.class); String name weather.name;如果是数组服务器返回[{...},{...}]或外层包了list字段的数组// 情况一jsonText整个就是一个数组 ListWeather list new Gson().fromJson(jsonText, new TypeTokenListWeather(){}.getType()); // 情况二jsonText外层是对象list是字段 JsonObject obj new Gson().fromJson(jsonText, JsonObject.class); JsonArray array obj.getAsJsonArray(list); ListWeather list new Gson().fromJson(array, new TypeTokenListWeather(){}.getType());TypeToken是Gson专门用来获取带泛型信息类型的地方。直接写ListWeather.class在Java里是不合法的所以需要借助TypeToken包装一层。这一行代码就把一整段手写的循环解析替代掉了。我在实际项目里Gson用得最多。手写解析只在调试阶段看数据结构时才用。但如果你一点都不会手写解析最好也别直接跳到Gson因为调试JSON时你依然需要看得懂原始结构。4.5 多接口数据汇总网络请求只有一次返回一个值还有一个场景需要单独说一下有些页面要展示的数据来自多个接口。比如一个详情页上半部分来自/api/user下半部分来自/api/order。这就涉及到“多个网络请求同时发起、最后汇总展示”的情况。入门阶段你可以先串行先请求接口A拿到数据再请求接口B最后一起更新UI。但串行请求有两个问题慢而且第二个请求依赖第一个请求时回调嵌套会越来越深俗称“回调地狱”。等你有一定基础后建议了解一下AsyncTask之外的并发方案。在Android里比较简单的做法是用线程池配合CountDownLatch或者直接用协程Kotlin。这类并发方案本讲不展开但你心里要有个概念网络请求是异步的多个请求的数据汇总需要“等所有请求都回来”的机制。我当年第一次做多接口页面时没用任何并发工具直接在两个子线程里各更新一次UI结果画面先出现一半数据、再出现另一半数据看起来非常奇怪。5. 实战避坑常见报错与排查思路5.1 必踩的四个坑我帮你提前排掉新手写网络编程报错其实就那么几类我把它们整理成了一张表每个都是我亲自见过或带着新人排查过的报错或现象原因解决办法SecurityException: Missing INTERNET permission没有声明网络权限在manifest里加uses-permission android:nameandroid.permission.INTERNET/NetworkOnMainThreadException在主线程里发了网络请求必须把请求放到子线程Thread/Runnable/线程池Cleartext HTTP traffic not permitted访问了明文HTTP系统默认禁止本地调试临时给application加android:usesCleartextTraffictrue正式项目用HTTPS中文乱码读取响应时未指定UTF-8编码new InputStreamReader(inputStream, UTF-8)这几类问题占了新手网络编程故障的九成以上。尤其是第一和第三明明代码看起来没问题就是跑不通其实就是一两行的配置问题。5.2 网络请求排错的心法先看Logcat原始数据很多新手一遇到“界面没反应”就到处乱改代码越改越乱。我的建议是先看Logcat里有没有打印请求结果或异常。逻辑其实是这样的——网络请求是黑盒的你眼睛看不到服务器那边发生了什么唯一能告诉你真相的就是日志和返回数据。所以我写网络请求时几乎每次都先打印原始返回文本String result HttpUtil.get(url); Log.d(TAG, 网络返回: result);如果你看到网络返回: {city:北京,...}说明网络请求本身是通的问题出在解析或UI更新。如果看到网络返回: 请求失败响应码404说明URL或参数不对。如果连Log都没有大概率是异常被吞了或者根本没走到网络请求这一步。我见过最离谱的一次问题是学员把url拼错了少了一个斜杠服务器返回404但他一直在检查JSON解析代码折腾了一个多小时。后来我让他先打Log一眼就发现了问题。养成先看返回数据再动手改代码的习惯会帮你省下大量排查时间。5.3 子线程更新UI崩溃怎么安全切回主线程子线程里直接textView.setText()会导致崩溃这个是入门阶段仅次于权限问题的高频坑。安全切回主线程的方式有好几种我列一下常见的你可以根据自己的习惯选runOnUiThread(Runnable)在Activity里可以直接用最简单直观。Handler.post(Runnable)在任意类里new一个Handler(Looper.getMainLooper())然后用它post任务到主线程。View.post(Runnable)持有任意View对象时直接调用view.post(...)也可以它内部会切到主线程执行。比如Handler写法Handler mainHandler new Handler(Looper.getMainLooper()); mainHandler.post(new Runnable() { Override public void run() { textView.setText(result); } });这些都是切主线程的标准姿势。选一个用就行不用都背下来。我个人的偏好是runOnUiThread因为它在Activity里写起来最顺几乎不会用错。注意网络请求完成后还要判断一下Activity是否已经被用户销毁。如果用户退出页面时请求才刚返回直接更新UI会报CalledFromWrongThreadException或空指针。入门阶段可以先不管这个场景但做真实项目时一定要加上生命周期判断。5.4 老生常谈框架怎么取舍入门还要不要手写关于网络框架这里顺便多聊几句。Android官方早期的HttpClient已经被废弃了现在的方向主要是OkHttp、Retrofit或者两者配合。Retrofit基于OkHttp封装用注解定义接口代码非常简洁但入门时直接用Retrofit反而容易让人困惑“它到底帮我做了啥”。我的建议是入门阶段至少手写一遍HttpURLConnection的完整流程再用原生JSONObject解析一遍返回值。这个过程会让你体会网络请求的真实链路。之后再用OkHttp你会秒懂它只是把连接管理和线程切换封装好了再用Retrofit你会轻松理解注解怎么被转化成一次HTTP请求。我自己带新人的固定路径就是“原始写法 → OkHttp → Retrofit Gson → 协程”。每个阶段配合一两个小项目效果比直接上框架好得多。等你熟练以后大部分工业项目确实不用手写这些框架能省太多重复代码但底层原理的“感觉”一定是在入门阶段建立的。最后说个我自己实践中的小经验调试网络请求时别总盯着代码看把服务器返回的JSON复制到格式化工具里看层级通常一眼就能找到问题。初学者踩得最多的坑不是代码语法而是对“服务器返回结构”没有直观认识导致解析逻辑和真实数据对不上。把这个问题解决了你的网络编程水平基本就跨过入门门槛了。后面再学JSON解析的高级用法、图片加载、文件上传下载都是在这个基础上做加法而已。
阅读完成 · 觉得有帮助?
咨询建站