【混合开发】vue+Android、iPhone、鸿蒙、win、macOS、Linux之android 把assert里的dist.zip 包解压到sd卡里

一图胜千言

上一篇有

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,无需额外权限 -->
  1. 运行时权限(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();
    }
}
  1. 解压工具类
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();
            }
        }
    }
}
  1. 调用解压
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();
}
  1. 使用示例
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 加代理头:

    java 复制代码
    webView.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

http://www.smartapi.site/


旧版本已停止维护

相关推荐
千里马学框架2 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
A黄俊辉A2 天前
uniapp webview中实现 app和内嵌的H5双向通信
vue.js·json
美狐美颜SDK开放平台2 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone2 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc2 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
honkun62 天前
vue 表格组件 vxe-table 配置 ajax 请求自动加载数据与表单查询
vue.js·vxe-table
ttyyttemo2 天前
Kotlin 协程中的 Job 结构化并发与取消
android
kybs19912 天前
全球灾害数据分析可视化 毕业设计-附源码66794
vue.js·spring boot·mysql·安全·django·c#·asp.net
雪芽蓝域zzs2 天前
第26章:ECharts 自定义主题 + 页面一键切换主题(深色 / 浅色两套)
vue.js·echars
雪芽蓝域zzs2 天前
第22章:ECharts 图表联动(多图表交互联动)
vue.js·echars