一图胜千言

上一篇有
xml
<!-- 读写外部存储 -->
<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,无需额外权限 -->
- 运行时权限(Activity/Fragment)
java
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();
}
}
- 解压工具类
java
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();
}
}
}
}
- 调用解压
java
private void unzipAssets() {
new Thread(() -> {
try {
// 目标目录:/storage/emulated/0/Android/<package>/web/dist
File 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();
}
- 使用示例
java
checkPermissionAndUnzip();
使用解压结果
java
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/ 协议 |
正确打开姿势
java
// 私有目录 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 或 文件访问
java
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
也打不开。
快速解决(开发阶段)
xml
<application
android:requestLegacyExternalStorage="true"
... />
正式上架 请把文件放到:
getFilesDir()
/getCacheDir()
- 或
assets/
- 或使用 FileProvider 生成
content://
URI
✅ 4. 用了 FileProvider 却给错路径(少见)
若用 FileProvider.getUriForFile()
生成 content://
地址,必须:
java
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" 或放私有目录 |
✅ 最小可用代码(私有目录)
java
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 加代理头:
javawebView.setWebViewClient(new WebViewClient() { @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { return false; // 不拦截 } });
✅ 最小排查脚本(adb 快速验证)
bash
adb shell ping -c 3 192.168.x.x # 先看网络通不通
adb shell curl -I http://192.168.x.x:3000
能通 → 代码问题;不通 → 网络问题。
✅ WebView 侧兜底处理
java
webView.setWebViewClient(new WebViewClient() {
@Override
public 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. 文件真的存在吗?
java
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 |
assets | file:///android_asset/... |
file:///android_asset/web/index.html |
常见拼写错误
❌ file://data/...
(少一个 /
)
❌ file:///android_assets/...
(多了 s
)
✅ 3. 空格 / 中文 / 特殊字符
本地文件含空格或中文 → URLEncoder 编码:
java
String path = new File(dir, "index 1.html").getAbsolutePath();
path = Uri.encode(path); // 空格→%20
webView.loadUrl("file://" + path);
✅ 4. 用 FileProvider 给路径(推荐 Android 7+)
防止 file://
被禁止,统一用 content://
:
xml
<!-- AndroidManifest.xml -->
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
res/xml/file_paths.xml
xml
<?xml version="1.0" encoding="utf-8"?>
<paths>
<files-path name="web" path="web/" />
</paths>
Java 代码:
java
File htmlFile = new File(getFilesDir(), "web/index.html");
Uri uri = FileProvider.getUriForFile(this,
BuildConfig.APPLICATION_ID + ".fileprovider",
htmlFile);
webView.loadUrl(uri.toString());
✅ 5. 兜底日志(复制即用)
java
webView.setWebViewClient(new WebViewClient() {
@Override
public 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
