Android Studio中Jsoup无法连接网页的问题求助
问题原因
该问题是Android运行环境和普通Java运行环境的规则差异、代码逻辑缺陷共同导致的,按触发概率从高到低排序:
- 未声明网络访问权限:普通Java程序无需额外配置即可访问网络,但Android系统对网络访问有严格的权限管控,未声明对应权限时所有网络请求会被系统直接拦截,根本无法发出。
- 明文HTTP请求被默认拦截:如果目标站点是
http://开头而非https://,Android 9(API 28)及以上版本默认禁止所有明文HTTP流量,请求会直接抛出异常。 - 线程逻辑存在本质错误:启动子线程后调用
join()等待执行完成的写法,如果retrieveMedia()在主线程(UI线程)被调用,join()会直接阻塞主线程,轻则触发ANR(应用无响应)崩溃,重则在部分定制系统上网络请求还未发出就被判定为应用卡死直接拦截。很多人误以为开了子线程就符合Android的线程规范,实际上只要主线程阻塞等待网络操作完成,和直接在主线程执行网络请求没有本质区别,一样会违反系统规则。 - 错误日志不可见:捕获IO异常时仅用
System.out.println打印提示,Android平台下这类输出默认不会显示在Logcat常规日志通道中,无法看到实际错误类型(权限拒绝、连接超时、SSL握手失败、站点拦截等),很难定位问题。 - 线程安全隐患:子线程直接修改调用方线程持有的
mediaOutput集合,未做任何同步处理,极端场景下会出现数据错乱甚至崩溃。 - 默认请求头被站点拦截:Jsoup默认的请求User-Agent带有Java标识,很多站点会直接拦截这类非浏览器发起的请求。
解决方案
按以下步骤调整即可:
- 补全基础配置
打开项目的AndroidManifest.xml文件,在<manifest>标签下(与<application>标签平级)添加网络权限:
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
如果目标站点是HTTP开头,还需要在<application>标签上添加允许明文流量的属性:
<application android:usesCleartextTraffic="true" <!-- 保留原有其他属性 --> > <!-- 保留原有组件配置 --> </application>
- 重构方法逻辑,废弃同步返回+join的错误写法
Android平台不允许同步等待网络请求返回结果,需要通过回调传递异步执行结果,改造后的代码参考如下:
public class MediaRetreiever { // 定义结果回调接口 public interface MediaLoadCallback { void onLoadSuccess(ArrayList<Media> mediaList); void onLoadFailed(String errorMsg); } public void retrieveMedia(MediaLoadCallback callback) { new Thread(() -> { ArrayList<Media> mediaOutput = new ArrayList<>(); Document doc; try { doc = Jsoup.connect("替换为实际访问的站点地址") .timeout(20000) // 添加浏览器UA,避免被站点反爬拦截 .userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36") .get(); } catch (IOException e) { // 用Android原生日志工具打印完整错误栈,方便排查 Log.e("MediaRetriever", "网页连接失败", e); callback.onLoadFailed("连接失败:" + e.getMessage()); return; } try { Elements mediaFeed = doc.getElementById("main").getElementsByClass("node"); for (Element e : mediaFeed) { String title = e.getElementsByClass("title").text().trim(); String author = e.getElementsByClass("content").tagName("p").select("em").text().trim(); String content = e.getElementsByClass("content").text().replace(author, "").trim(); mediaOutput.add(new Media(title, author, "", content)); } callback.onLoadSuccess(mediaOutput); } catch (Exception e) { Log.e("MediaRetriever", "页面解析失败", e); callback.onLoadFailed("解析失败:" + e.getMessage()); } }).start(); } }
- 调整调用方式
在Activity/Fragment中调用该方法时,通过回调接收结果,如果需要在回调中更新UI,必须切回主线程执行:
// 调用示例 new MediaRetreiever().retrieveMedia(new MediaRetreiever.MediaLoadCallback() { @Override public void onLoadSuccess(ArrayList<Media> mediaList) { runOnUiThread(() -> { // 在此处执行列表刷新、UI更新操作 }); } @Override public void onLoadFailed(String errorMsg) { runOnUiThread(() -> { Toast.makeText(CurrentActivity.this, errorMsg, Toast.LENGTH_SHORT).show(); }); } });
- 定向排查剩余问题
如果调整后仍无法连接,直接查看Logcat中MediaRetriever标签下的错误日志,会打印完整异常栈,根据具体错误信息(比如SSL证书错误、站点403拦截、网络不可达等)针对性调整即可,不要使用System.out打印日志,在Android平台上几乎无法正常查看这类输出。
补充说明:之前在Eclipse普通Java环境中代码可正常运行,是因为普通Java程序既没有权限管控、主线程阻塞的ANR机制,也没有明文流量拦截规则,和Android运行环境的逻辑完全不同,不能直接照搬普通Java的多线程网络写法。
内容的提问来源于stack exchange,提问作者Anon
相关产品推荐
相关产品推荐

