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

发布于:2025-09-11 ⋅ 阅读:(23) ⋅ 点赞:(0)

一图胜千言

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

<!-- 读写外部存储 -->
<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/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. 使用示例
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 也打不开。

快速解决(开发阶段)

<application
    android: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() {
        @Override
        public 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() {
    @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. 文件真的存在吗?

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 编码

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 -->
<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 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() {
    @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/

在这里插入图片描述


旧版本已停止维护


网站公告

今日签到

点亮在社区的每一天
去签到