一图胜千言

在这里插入图片描述
上一篇有

<!-- 读写外部存储 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"android:maxSdkVersion="28"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"android:maxSdkVersion="28"/><!-- Android 10+ 用 MediaStore/SAF,无需额外权限 -->
  1. 运行时权限(Activity/Fragment)
private static final int REQ_CODE = 100;private void checkPermissionAndUnzip() {if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)!= PackageManager.PERMISSION_GRANTED) {requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},REQ_CODE);return;}}unzipAssets();
}@Override
public void onRequestPermissionsResult(int requestCode,@NonNull String[] permissions,@NonNull int[] grantResults) {super.onRequestPermissionsResult(requestCode, permissions, grantResults);if (requestCode == REQ_CODE && grantResults.length > 0&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {unzipAssets();} else {Toast.makeText(this, "需要存储权限", Toast.LENGTH_SHORT).show();}
}
  1. 解压工具类
public class ZipUtils {public static void unzipAsset(Context ctx, String assetName,File destDir) throws IOException {if (!destDir.exists()) destDir.mkdirs();try (InputStream in = ctx.getAssets().open(assetName);ZipInputStream zin = new ZipInputStream(in)) {ZipEntry entry;byte[] buffer = new byte[4096];while ((entry = zin.getNextEntry()) != null) {File file = new File(destDir, entry.getName());if (entry.isDirectory()) {file.mkdirs();} else {// 确保父目录存在File parent = file.getParentFile();if (!parent.exists()) parent.mkdirs();try (FileOutputStream out = new FileOutputStream(file)) {int len;while ((len = zin.read(buffer)) != -1) {out.write(buffer, 0, len);}}}zin.closeEntry();}}}
}
  1. 调用解压
private void unzipAssets() {new Thread(() -> {try {// 目标目录:/storage/emulated/0/Android/<package>/web/distFile destDir = new File(Environment.getExternalStorageDirectory(),"Android/" + getPackageName() + "/web/dist");ZipUtils.unzipAsset(this, "dist.zip", destDir);runOnUiThread(() ->Toast.makeText(this, "解压完成", Toast.LENGTH_SHORT).show());} catch (IOException e) {e.printStackTrace();runOnUiThread(() ->Toast.makeText(this, "解压失败:" + e.getMessage(),Toast.LENGTH_SHORT).show());}}).start();
}
  1. 使用示例
checkPermissionAndUnzip();

使用解压结果

File webDir = new File(getFilesDir(), "web");
File indexHtml = new File(webDir, "index.html");

其他

net:ERR_ACCESS_DENIED

net::ERR_ACCESS_DENIED 并不是网络错误,而是 WebView 拒绝访问本地文件 的通用提示。
99% 的场景只踩了下面 3 个坑 之一,按清单逐条检查即可解决。


✅ 1. 文件不在「允许路径」里(最常见)

场景是否允许
/data/data/<包>/files/xxx✅ 私有目录,允许
/storage/emulated/0/xxx❌ 需 可读权限 + file:// 白名单
assets/res/raw/✅ 需 file:///android_asset/ 协议

正确打开姿势

// 私有目录 files/web/index.html
webView.loadUrl("file:///data/data/" + getPackageName() + "/files/web/index.html");// assets 目录
webView.loadUrl("file:///android_asset/web/index.html");

✅ 2. 忘记开 JavaScript文件访问

WebSettings ws = webView.getSettings();
ws.setJavaScriptEnabled(true);          // 必须
ws.setAllowFileAccess(true);            // 必须
ws.setAllowContentAccess(true);         // 建议
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {ws.setAllowFileAccessFromFileURLs(true);   // assets 里调 JS 需要ws.setAllowUniversalAccessFromFileURLs(true);
}

✅ 3. Android 10+ 分区存储 限制

  • targetSdkVersion ≥ 29 且文件在 外部存储 时,
    即使申请了 READ_EXTERNAL_STORAGE 也打不开。

快速解决(开发阶段)

<applicationandroid:requestLegacyExternalStorage="true"... />

正式上架 请把文件放到:

  • getFilesDir() / getCacheDir()
  • assets/
  • 或使用 FileProvider 生成 content:// URI

✅ 4. 用了 FileProvider 却给错路径(少见)

若用 FileProvider.getUriForFile() 生成 content:// 地址,必须

intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);

WebView 不支持 content:// 直接 loadUrl(),请转回 file:// 或拷贝到私有目录。


🎯 一键排查清单

检查项正确示例
文件存在?new File(path).exists() 返回 true
路径协议?file:///data/data/包名/files/xxx.html
JS 开关?setJavaScriptEnabled(true)
文件访问?setAllowFileAccess(true)
外部存储?requestLegacyExternalStorage="true" 或放私有目录

✅ 最小可用代码(私有目录)

WebView webView = findViewById(R.id.webView);
WebSettings ws = webView.getSettings();
ws.setJavaScriptEnabled(true);
ws.setAllowFileAccess(true);File htmlFile = new File(getFilesDir(), "web/index.html");
webView.loadUrl("file://" + htmlFile.getAbsolutePath());

🎯 一句话总结

ERR_ACCESS_DENIED 99% 是 路径不在白名单没开 setAllowFileAccess(true);把文件放进 私有目录assets 并按上面设置即可秒解。

net::ERR_ADDRESS_UNREACHABLE

code=-2 / desc=net::ERR_ADDRESS_UNREACHABLE 不是 WebView 的 BUG,而是 TCP 三层无法与目标地址建立连接 的通用报错。
把常见成因做成「检查清单」,按顺序 1→7 秒定位即可。


🔍 1. 地址写错 / 端口未监听

  • 用系统浏览器访问 同一 URL,若也打不开 → 服务器或地址问题。
  • 检查 IP、端口、大小写、http/https

🔍 2. 本机网络不通

  • 手机 飞行模式 / VPN / 代理 忘记关。
  • 电脑热点 无 Internet
  • 公司/校园网 禁止设备互访

🔍 3. DNS 解析失败

  • 域名拼错 → ping 域名 返回 unknown host
  • 手机 DNS 被污染 → 换 8.8.8.8 再测。

🔍 4. 防火墙 / 安全组拦截

  • 服务器 未放行端口 → telnet IP 端口 连不上。
  • 本地防火墙(Windows Defender / macOS 防火墙)阻断入站。

🔍 5. 局域网地址在模拟器里写错

  • 模拟器 不是真机localhost / 127.0.0.1 指向 模拟器自己
  • 正确写法:http://10.0.2.2:端口(Android 模拟器专用宿主机地址)。

🔍 6. 公司/测试 Wi-Fi 开启 AP 隔离

  • 路由器后台 → 关闭 AP 隔离 即可设备互通。

🔍 7. 代理 / VPN 全局拦截

  • 关闭 Charles / Fiddler / Clash 全局代理再测。
  • WebView 加代理头:
    webView.setWebViewClient(new WebViewClient() {@Overridepublic boolean shouldOverrideUrlLoading(WebView view, String url) {return false;   // 不拦截}
    });
    

✅ 最小排查脚本(adb 快速验证)

adb shell ping -c 3 192.168.x.x      # 先看网络通不通
adb shell curl -I http://192.168.x.x:3000

能通 → 代码问题;不通 → 网络问题。


✅ WebView 侧兜底处理

webView.setWebViewClient(new WebViewClient() {@Overridepublic void onReceivedError(WebView view,WebResourceRequest request,WebResourceError error) {if (error.getErrorCode() == ERROR_HOST_LOOKUP ||error.getErrorCode() == ERROR_CONNECT ||error.getErrorCode() == ERROR_TIMEOUT) {view.loadUrl("file:///android_asset/offline.html");}}
});

🎯 一句话总结

ERR_ADDRESS_UNREACHABLE = 地址不可达,按「浏览器能否打开 → 模拟器地址是否正确 → 防火墙/代理是否拦截 → DNS 是否解析」四步排查,99% 秒定位。

net::ERR_FILE_NOT_FOUND

code=-1 / desc=net::ERR_FILE_FOUND 并不是网络错误,而是 WebView 访问本地文件时路径写错或文件根本不存在
按下面 4 步一次性排查:


✅ 1. 文件真的存在吗?

File f = new File(path);
Log.d("WEBVIEW", "exists=" + f.exists() + "  abs=" + f.getAbsolutePath());

exists=false → 路径拼错 / 没拷进去 / 大小写错误。


✅ 2. 路径前缀必须拼对

位置正确前缀示例
私有目录 getFilesDir()file:///data/data/包名/files/...file:///data/data/com.demo/files/web/index.html
外部存储(SD)file:///storage/emulated/0/...file:///storage/emulated/0/Android/com.demo/web/index.html
assetsfile:///android_asset/...file:///android_asset/web/index.html

常见拼写错误
file://data/... (少一个 /
file:///android_assets/... (多了 s


✅ 3. 空格 / 中文 / 特殊字符

本地文件含空格或中文 → URLEncoder 编码

String path = new File(dir, "index 1.html").getAbsolutePath();
path = Uri.encode(path);          // 空格→%20
webView.loadUrl("file://" + path);

✅ 4. 用 FileProvider 给路径(推荐 Android 7+)

防止 file:// 被禁止,统一用 content://

<!-- AndroidManifest.xml -->
<providerandroid:name="androidx.core.content.FileProvider"android:authorities="${applicationId}.fileprovider"android:exported="false"android:grantUriPermissions="true"><meta-dataandroid:name="android.support.FILE_PROVIDER_PATHS"android:resource="@xml/file_paths" />
</provider>

res/xml/file_paths.xml

<?xml version="1.0" encoding="utf-8"?>
<paths><files-path name="web" path="web/" />
</paths>

Java 代码:

File htmlFile = new File(getFilesDir(), "web/index.html");
Uri uri = FileProvider.getUriForFile(this,BuildConfig.APPLICATION_ID + ".fileprovider",htmlFile);
webView.loadUrl(uri.toString());

✅ 5. 兜底日志(复制即用)

webView.setWebViewClient(new WebViewClient() {@Overridepublic void onReceivedError(WebView view,WebResourceRequest request,WebResourceError error) {Log.e("WEBVIEW", "code=" + error.getErrorCode()+ " desc=" + error.getDescription()+ " url=" + request.getUrl().toString());}
});

打印出的 url 就是 WebView 实际访问的地址,直接拷到浏览器/文件管理器 即可验证是否存在。


🎯 一句话总结

ERR_FILE_NOT_FOUND = 路径错 or 文件不在,用 File.exists() 确认 → 拼对 file:///... → 特殊字符 Uri.encode() → 推荐 FileProvider 一步到位。

好用的开发工具

推荐理由

postman在国内使用已经越来越困难:
1、登录问题严重
2、Mock功能服务基本没法使用
3、版本更新功能已很匮乏
4、某些外力因素导致postman以后能否使用风险较大
5、postman会导致电脑卡顿,而且使用的功能越多越慢,尤其是win电脑,太让人郁闷了
出于以上考虑因此笔者自己开发了一款api调试开发工具SmartApi,满足基本日常开发调试api需求

SmartApi
win版本不大于1M;运行消耗性能极低
macos 版本不大于100M;运行消耗性能极低
非常适合开发设备或性能有限的开发环境

SmartApi只为开发服务

官网地址SmartApi

http://www.smartapi.site/

在这里插入图片描述


旧版本已停止维护

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/diannao/98768.shtml
繁体地址,请注明出处:http://hk.pswp.cn/diannao/98768.shtml
英文地址,请注明出处:http://en.pswp.cn/diannao/98768.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

线程的创建.销毁

线程线程的创建在 C 中&#xff0c;线程的创建核心是通过std::thread类实现的&#xff0c;其构造函数需要传入一个可调用对象&#xff08;Callable Object&#xff09;作为线程入口。可调用对象包括普通函数、lambda 表达式、函数对象&#xff08;functor&#xff09;、类的成员…

MySQL基础全面解析

MySQL作为最流行的关系型数据库管理系统之一&#xff0c;是每一位开发者必备的核心技能。本文将系统性地解析MySQL的基础知识&#xff0c;结合关键概念与实战应用&#xff0c;帮助您构建扎实的数据库基础。1. SQL与NoSQL的本质区别SQL&#xff08;结构化查询语言&#xff09;数…

4、幽络源微服务项目实战:后端公共模块创建与引入多租户模块

前言 上节我们将电网巡检系统的前端vue2项目创建、配置&#xff0c;并构建了最基础的多租户界面&#xff0c;本节来继续构建后端的公共模块、多租户模块&#xff0c;并将公共模块引入到多租户模块中。 创建公共模块和多租户模块 在back父工程下创建两个Module&#xff0c;和…

STM32学习路线开启篇:芯片简介与课程简介

编写不易,请多多指教,觉得不错可以关注一下,相互学习 前言 一、课程配套资源 1、面包板 2、面包板专用的跳线 3、面包板的飞线 4、杜邦线 5、STM32F103C8T6最小系统板 6、0.96寸的OLED显示屏模块 7、电位器 8、按钮 9、LED灯 10、STLINK 11、USB转串口(TTL)模块 12、源蜂鸣器模…

图像直方图

图像直方图就是用来统计图像像素值分布的。灰度图分布读取灰度图phone cv2.imread(phone.png, cv2.IMREAD_GRAYSCALE) a phone.ravel() plt.hist(a, bins256) plt.show()如何可以获得当前像素值分布读取各通道的像素值分布img cv2.imread(phone.png) colors (b, g, r) for …

分类别柱状图(Vue3)

效果图&#xff1a;需求&#xff1a;男女年龄段占比<template><div class"go-ClassifyBar01"><v-chartref"vChartRef":option"option"style"width: 100%; height: 800px"></v-chart></div> </templa…

Apache Dubbo学习笔记-使用Dubbo发布、调用服务

Apache Dubbo经常作为一个RPC框架来使用&#xff0c;这篇文章主要介绍使用Dubbo配合注册中心来发布和调用服务。 Apache Dubbo和Spring Boot、JDK的版本对应关系。 Dubbo 分支最新版本JDKSpring Boot组件版本详细说明3.3.x (当前文档)3.3.08, 17, 212.x、3.x详情- 版本变更记录…

Python学习——字典和文件

前面python的学习中我们已经学习了python的函数和列表元组相关的内容&#xff0c;接下来我们来学习剩下的python语法&#xff1a;字典和文件 相关代码已经上传到作者的个人gitee&#xff1a;楼田莉子/Python 学习喜欢请点个赞谢谢 目录 字典 创建字典 查找key 新增/修改元素 …

swiper插件的使用

官方网址&#xff1a;https://www.swiper.com.cn/ 1、点击导航栏&#xff0c;获取Swiper里边的下载Swiper 2、选择要下载的版本【本次案例版本5.4.5】&#xff0c;然后解压缩文件夹&#xff0c;拿到swiper.min.js和swiper.min.css文件&#xff0c;放到项目对应的css文件和js文…

Vue3+JS 组合式 API 实战:从项目痛点到通用 Hook 封装

Vue3 组合式 API 的实战技巧 —— 组合式 API 帮我解决了不少 Options API 难以应对的问题&#xff0c;尤其是在代码复用和复杂组件维护上。一、为什么放弃 Options API&#xff1f;聊聊三年项目里的真实痛点​刚接触 Vue3 时&#xff0c;我曾因 “惯性” 继续用 Options API 写…

把 AI 塞进「电梯按钮」——基于 64 kB 零样本声纹的离线故障预测按钮

标签&#xff1a;零样本声纹、电梯按钮、离线 AI、TinyML、RISC-V、低功耗、GD32V303、故障预警 ---- 1. 背景&#xff1a;为什么按钮要「听声音」&#xff1f; 全国 700 万台电梯&#xff0c;按钮故障率 0.3 %/年&#xff0c;却常出现&#xff1a; • 机械卡滞、触点氧化&…

清华大学联合项目 论文解读 | MoTo赋能双臂机器人:实现零样本移动操作

研究背景 移动操作是机器人领域的核心挑战&#xff0c;它使机器人能够在各种任务和动态日常环境中为人类提供帮助。传统的移动操作方法由于缺乏大规模训练&#xff0c;往往难以在不同任务和环境中实现泛化。而现有操作基础模型虽在固定基座任务中表现出强泛化性&#xff0c;却无…

go webrtc - 2 webrtc重要概念

webrtc是一套音视频传输技术生态&#xff0c;不是一个协议或一个什么东西。3种模式本文基于 SFU 形式阐述&#xff01;重要概念&#xff1a;sfu 服务负责&#xff1a;信令 服务负责&#xff1a;peerConnection&#xff1a;track&#xff1a;房间&#xff1a;虚拟分组概念用户&a…

“下游任务”概念详解:从定义到应用场景

“下游任务”概念详解&#xff1a;从定义到应用场景 一、什么是“下游任务”&#xff1f; 在机器学习&#xff08;尤其是深度学习&#xff09;中&#xff0c;“下游任务”&#xff08;Downstream Task&#xff09;是相对“上游过程”而言的目标任务——可以理解为&#xff1a;我…

视频怎么做成 GIF?用 oCam 一键录制 GIF 动画超简单

GIF 动图因其生动直观、无需点击播放的特点&#xff0c;越来越受欢迎。你是否也曾看到一段有趣的视频&#xff0c;想把它做成 GIF 发给朋友或用在PPT里&#xff1f;其实&#xff0c;将视频片段转换为 GIF 并不需要复杂的视频剪辑技术&#xff0c;使用一款支持直接录制为 GIF 的…

Vue.config.js中的Webpack配置、优化及多页面应用开发

Vue.config.js中的Webpack配置、优化及多页面应用开发 在Vue CLI 3项目中&#xff0c;vue.config.js文件是工程化配置的核心入口&#xff0c;它通过集成Webpack配置、优化策略和多页面开发支持&#xff0c;为项目构建提供高度可定制化的解决方案。本文将从基础配置、性能优化、…

行业学习【电商】:直播电商的去头部化、矩阵号?

声明&#xff1a;以下部分内容含AI生成这两个词是当前直播电商和MCN领域的核心战略&#xff0c;理解了它们就理解了行业正在发生的深刻变化。一、如何理解“去头部化”&#xff1f;“去头部化” 指的是平台或MCN机构有意识地减少对超头部主播&#xff08;如曾经的李佳琦、薇娅&…

【MFC视图和窗口基础:文档/视图的“双胞胎”魔法 + 单文档程序】

大家好&#xff0c;我是你的MFC编程小伙伴&#xff01;学MFC就像探险古墓&#xff1a;到处是神秘的“房间”&#xff08;窗口&#xff09;和“宝藏”&#xff08;数据&#xff09;。今天咱们聊聊核心概念 – 视图、窗口和文档。这些是MFC的“骨架”&#xff0c;懂了它们&#x…

深度学习(六):代价函数的意义

在深度学习的浩瀚世界中&#xff0c;代价函数&#xff08;Cost Function&#xff09;&#xff0c;又称损失函数&#xff08;Loss Function&#xff09;或目标函数&#xff08;Objective Function&#xff09;&#xff0c;扮演着至关重要的角色&#xff0c;它就像一个导航员&…

Kable使用指南:Android BLE开发的现代化解决方案

概述 Kable&#xff08;com.juul.kable:core&#xff09;是一个专为Android蓝牙低功耗&#xff08;BLE&#xff09;开发设计的Kotlin协程友好库。它通过提供简洁的API和响应式编程模式&#xff0c;极大地简化了BLE设备交互的复杂性。本文将详细介绍Kable的使用方法&#xff0c;…