Android Cordova 中 `File``toBlob`出现的问题

49 阅读7分钟

解决 Android Cordova 中 toBlob返回非标准 File 对象及 size 为 0 的问题

在混合开发(Hybrid App)场景中,HTML5 的 canvas.toBlob()API 常用于图片裁剪、压缩、上传等需求。然而在 Android Cordova​ 环境下,开发者常遇到诡异问题:

调用 canvas.toBlob()后,回调得到的并非标准 Blob对象,而是一个带有 localURL属性、size为 0 的 Cordova 特有 File对象,导致后续图片上传或预览逻辑完全失效。

本文将深入剖析该问题的根本原因,并从 原生 WebView 配置资源拦截编码Cordova 插件拦截​ 三个维度提供系统性解决方案,助你彻底恢复 toBlob的原生行为。

1. 现象描述

1.1 预期行为

在标准浏览器或普通 WebView 中执行以下代码:

const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
ctx.fillStyle = 'red';
ctx.fillRect(0, 0, 100, 100);

canvas.toBlob((blob) => {
  console.log('标准 Blob:', blob);
  console.log('类型:', Object.prototype.toString.call(blob)); // "[object Blob]"
  console.log('size:', blob.size); // > 0(如 1024)
  console.log('type:', blob.type); // "image/png"
}, 'image/png');

预期输出为标准 Blob对象,包含正确的 sizetype等属性。

1.2 Cordova 环境中的异常表现

在 Android Cordova 应用中执行相同代码,控制台输出可能为:

File {
  name: "[Blob]",
  localURL: "cdvfile://localhost/persistent/1769672182452.png",
  type: { type: "image/png" }, // 注意:type 被包装为对象而非字符串
  size: 0, // 关键异常:size 始终为 0
  lastModified: null,
  __proto__: File.prototype // 原型链指向 Cordova 自定义的 File
}

此时 blob.size为 0,type是对象而非字符串,且存在 localURL等非标准属性,完全不符合 Blob/File接口规范。

2. 核心原因分析

该问题由 原生环境配置缺陷​ 与 Cordova 插件机制冲突​ 共同导致,具体可拆解为以下三点:

2.1 WebView 基础配置缺失:DomStorage 与跨域权限未开启

canvas.toBlob()依赖 HTML5 的 DOM Storage​ 机制存储临时数据,同时 Canvas 绘制本地图片时需避免「污染」(Tainted Canvas)。若 WebView 未开启以下配置,可能导致 toBlob执行失败或返回异常对象:

  • setDomStorageEnabled(true):启用 DOM Storage,否则 toBlob可能因无法存储中间数据而返回空对象。
  • setAllowFileAccessFromFileURLs(true)与 setAllowUniversalAccessFromFileURLs(true):允许本地文件 URL 跨域访问,避免 Canvas 因安全限制拒绝绘制本地图片(导致最终 Blob为空)。

2.2 本地资源拦截的编码错误:二进制流被 UTF-8 破坏

若项目中通过 WebViewClient拦截了本地图片请求(如自定义加载逻辑),并将 WebResourceResponse的 encoding参数错误设置为 "UTF-8",会导致二进制图片数据被强制按文本编码解析,造成数据损坏。例如:

// 错误示例:二进制文件使用 UTF-8 编码
new WebResourceResponse("image/png", "UTF-8", inputStream);

此时图片数据被破坏,Canvas 绘制的是无效内容,最终 toBlob生成的 Blob因无有效像素数据而 size为 0。

2.3 Cordova 插件拦截:cordova-plugin-file覆盖全局 File 对象

这是导致返回「带 localURL的非标准 File」的 最关键原因

cordova-plugin-file为实现 cdvfile://协议支持和大文件持久化,会在 JavaScript 层 重写全局 File和 Blob构造函数,将原生对象替换为 Cordova 自定义的 File实现(包含 localURLname等插件特有属性)。

例如,插件源码中可能包含类似逻辑:

// cordova-plugin-file/www/File.js(简化版)
window.File = function(name, localURL, type, lastModifiedDate, size) {
  this.name = name;
  this.localURL = localURL; // 非标准属性
  this.size = size || 0; // 若未正确初始化,size 可能为 0
  // ...
};

这种覆盖导致 toBlob返回的不再是原生 Blob,而是插件自定义的「伪 File 对象」。

3. 解决方案

针对上述原因,需从 原生配置资源拦截修复插件拦截解除​ 三方面同步调整。

3.1 步骤一:开启 WebView 必要权限与配置

确保 WebView启用 DomStorage和跨域权限,以支持 toBlob正常运行。

修改 WebViewEngineImpl.java(Cordova 默认引擎)

找到 makeWebViewSettings()方法,补充以下配置:

private void makeWebViewSettings() {
    WebSettings settings = webView.getSettings();
    
    // 基础配置
    settings.setJavaScriptEnabled(true);
    settings.setDomStorageEnabled(true); // 关键:启用 DOM Storage,否则 toBlob 可能失效
    
    // 跨域与文件访问权限(避免 Canvas 污染)
    settings.setAllowFileAccess(true); // 允许访问本地文件
    settings.setAllowContentAccess(true); // 允许访问 Content Provider 资源
    settings.setAllowFileAccessFromFileURLs(true); // 允许 file:// URL 访问其他本地文件
    settings.setAllowUniversalAccessFromFileURLs(true); // 允许 file:// URL 跨域访问(如加载远程图片到 Canvas)
    
    // 可选:提升性能与安全
    settings.setLoadWithOverviewMode(true);
    settings.setUseWideViewPort(true);
    settings.setCacheMode(WebSettings.LOAD_DEFAULT);
}

3.2 步骤二:修复本地资源拦截的编码错误

若项目中通过 WebViewClient拦截了图片请求(如自定义 shouldInterceptRequest),需确保二进制资源的 WebResourceResponse不设置 encoding(或显式设为 null)。

修改 WebViewClientImpl.java(或自定义 WebViewClient)

拦截本地图片时,正确构造 WebResourceResponse

@Override
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
    String url = request.getUrl().toString();
    if (url.startsWith("file:///android_asset/images/")) { // 假设拦截 assets 下的图片
        try {
            String path = url.replace("file:///android_asset/", "");
            InputStream fis = getAssets().open(path);
            
            // 关键:二进制文件(如 PNG/JPG)的 encoding 必须为 null,否则数据会被 UTF-8 编码破坏!
            return new WebResourceResponse("image/png", null, fis); 
            // 错误示例:new WebResourceResponse("image/png", "UTF-8", fis); 
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    return super.shouldInterceptRequest(view, request);
}

原理:二进制数据(如图片)按 UTF-8 编码解析会导致字节丢失或替换(如 \x89PNG可能被错误解码),最终 Canvas 绘制的内容无效,toBlob生成的 Blob因无像素数据而 size为 0。

3.3 步骤三:解除 Cordova 插件对 File 对象的拦截

最根本的解决方案是阻止 cordova-plugin-file覆盖全局 File对象,保留原生 Blob/File行为。具体操作分两种场景:

场景 A:仅需禁用 File 拦截,保留插件其他功能

修改 cordova-plugin-file的 JS 源码,避免其覆盖 window.File

  1. 定位插件 JS 文件

    路径通常为 app/src/main/assets/plugin.apis/plugins/cordova-plugin-file/www/File.js(或通过 cordova_plugins.js查看实际注入路径)。

  2. 注释或移除 File 构造函数定义

    打开 File.js,找到类似以下的代码并注释:

    cordova.define("cordova-plugin-file.File", function(require, exports, module) {
        // 原始插件定义的 File 构造函数(覆盖全局)
        /*
        var File = function(name, localURL, type, lastModifiedDate, size) {
            this.name = name || '';
            this.localURL = localURL || null;
            this.type = type || null; // 注意:这里 type 可能被错误包装为对象
            this.size = size || 0;
            this.lastModifiedDate = lastModifiedDate ? new Date(lastModifiedDate) : null;
        };
        File.prototype.slice = Blob.prototype.slice; // 强行继承 Blob 方法(但原型已被污染)
        */
    
        // 若需保留插件内部逻辑,可仅导出但不挂载到 window
        // module.exports = File; // 不推荐直接导出,避免其他代码引用
    });
    
  3. 检查插件注入逻辑

    部分 Cordova 版本会通过 plugin.xml自动向 index.html注入脚本(如 <js-module src="www/File.js" name="File">)。若注入代码中包含 window.File = require(...),需在注入逻辑中移除对 window.File的赋值。

场景 B:彻底移除插件对 Blob/File 的干扰(推荐)

若无需 cordova-plugin-file的文件系统功能,可直接卸载插件;若需保留,可通过以下方式隔离插件作用域:

  1. 修改插件导出方式

    在 File.js中,将 module.exports改为局部变量,避免插件内部引导代码将其挂载到 window

    cordova.define("cordova-plugin-file.File", function(require, exports, module) {
        // 不定义全局 File,仅作为模块导出(若其他代码显式引用需调整)
        const LocalFile = function(name, localURL, type, lastModifiedDate, size) {
            // 插件内部使用的 File 实现(不影响全局)
            this.name = name;
            // ...
        };
        module.exports = LocalFile; // 仅模块内可见
    });
    
  2. 手动恢复原生 File 原型(极端情况):

    若全局 File已被覆盖,可在 deviceready事件中尝试恢复原生原型(需谨慎):

    document.addEventListener('deviceready', () => {
        // 检查当前 File 是否为原生(原生 Blob 有 size、type 等属性)
        if (window.File && (!window.File.prototype.slice || window.File.prototype.localURL)) {
            console.warn("检测到 File 被覆盖,尝试恢复原生...");
            // 注:无法直接恢复原生构造函数,需通过 iframe 沙箱获取原生对象(复杂且不推荐)
            // 更可靠的方式是修改插件源码(见场景 A)
        }
    });
    

4. 验证与扩展

4.1 验证解决方案

完成上述调整后,重新构建并运行 App,执行以下测试代码:

canvas.toBlob((blob) => {
  console.log("修复后 Blob:", blob);
  console.log("是否为原生 Blob:", Object.prototype.toString.call(blob) === "[object Blob]");
  console.log("size:", blob.size); // 应 > 0(如 1024)
  console.log("type:", blob.type); // 应为 "image/png"(字符串)
}, 'image/png');

若输出符合预期,则问题解决。

4.2 扩展:处理其他可能的干扰因素

  • 第三方插件冲突:部分图片处理插件(如 cordova-plugin-camera)可能修改 Canvas 行为,需检查其是否重写了 toBlob
  • Android 版本差异:Android 5.0 以下 WebView 对 toBlob支持不完善,建议最低支持 Android 5.0+。
  • 内存限制:大尺寸 Canvas 生成 Blob时可能触发 OOM,需控制 Canvas 分辨率(如压缩后再转 Blob)。

5. 总结

问题现象根本原因解决方案
size为 01. DomStorage 未开启 2. 资源拦截编码错误(UTF-8 破坏二进制流)1. 开启 setDomStorageEnabled(true) 2. 资源拦截时 encoding设为 null
返回带 localURL的对象cordova-plugin-file覆盖全局 File构造函数修改插件 JS 源码,禁止覆盖 window.File

通过 原生配置补全资源编码修正插件拦截解除​ 三步操作,可彻底恢复 canvas.toBlob()的原生行为,确保混合开发中图片处理逻辑的可靠性。