技术经验谈 技术经验谈
首页
  • 最佳实践

    • 抓包
    • 数据库操作
  • ui

    • 《JavaScript教程》
    • 《JavaScript高级程序设计》
    • 《ES6 教程》
    • 《Vue》
    • 《React》
    • 《TypeScript 从零实现 axios》
    • 《Git》
    • TypeScript
    • JS设计模式总结
  • 总纲
  • 整体开发框架
  • 技术文档
  • GitHub技巧
  • Nodejs
  • 博客搭建
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 友情链接
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

hss01248

一号线程序员
首页
  • 最佳实践

    • 抓包
    • 数据库操作
  • ui

    • 《JavaScript教程》
    • 《JavaScript高级程序设计》
    • 《ES6 教程》
    • 《Vue》
    • 《React》
    • 《TypeScript 从零实现 axios》
    • 《Git》
    • TypeScript
    • JS设计模式总结
  • 总纲
  • 整体开发框架
  • 技术文档
  • GitHub技巧
  • Nodejs
  • 博客搭建
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 友情链接
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 最佳实践

  • ui

  • 优化

  • aop

  • apm

  • 架构

  • webview

    • webview和deeplink跳转
    • webview和js的一些疑难问题
    • webview工程化
    • webview文件选择-input-file适配
    • webview权限适配和getUserMedia适配
    • webview里blob的下载和保存
      • js注入的方法:
      • 通过html源码里匹配blob:的方法
      • 测试代码:
  • rxjava

  • activity-fragment-view的回调和日志
  • Android加密相关
  • Android命令行操作
  • app后台任务
  • kotlin
  • kotlin漫谈
  • kotlin语言导论
  • sentry上传mapping.txt文件
  • so放于远程动态加载方案
  • states
  • Xposed模块开发
  • 一个关于manifest合并的猥琐操作
  • 玩坏android存储
  • 获取本app的安装来源信息
  • Android
  • webview
hss01248
2025-09-11
目录

webview里blob的下载和保存

# webview里blob的下载和保存

# 先说结论

Android里的blob下载,触发webview的DownloadListener,下载的目标url为blob:null/9296a454-2bc1-4a59-9ee7-bf1e659377a1这种格式,是无法通过注入js,js里使用XMLHttpRequest来转换为base64的,会被webview直接拦掉

需要通过webview的OnLongClickListener,在其回调里能直接拿到blob的base64字符串,解码成对应二进制然后保存就行.

放在google ai studio页面就是

页面上图片上方的下载按钮点击后无法成功下载,

但是长按图片,就可以成功保存图片到相册

Xnip2025-09-11_16-26-17

Xnip2025-09-11_16-26-54

Xnip2025-09-11_16-27-16

# 被拦截掉的老方法

# js注入的方法:

  • 发现下载里的blob协议
  • 注入js到webview
  • js里使用XMLHttpRequest来转换为base64
  • 将base64通过jsbridge传给java代码,java代码里解码base64拿到二进制,保存到文件

结果是第三步直接报错:

(webview的settings全放开,也是过不去)

Android webview禁止通过XMLHttpRequest读取,xhr.send()直接报错:Not allowed to load local resource: blob:null/c9543d96-d22f-4fa8-ba71-3df2cca08958
1

# 通过html源码里匹配blob:的方法

拿到的html源码不全,很多隐藏在js里,所以不能像chrome tools里直接能找到blob:的链接

此路也是不通

# 测试代码:

private void doDownload(boolean hidden, String url, String name, long contentLength, boolean closeAfterSuccess, Activity activity) {
        if(url.startsWith("blob:")){
            WebConfigger.printSettings(webView);
            //<img _ngcontent-ng-c2656717358="" class="main-media-item main-image"
            // src="blob:https://aistudio.google.com/d05f4db1-d82d-43dd-9add-16865de1e3f5"
            // alt="Generated Image September 05, 2025 - 2:50PM.jpeg">
            //点击下载按钮后,回调到这里,url为: blob:null/c9543d96-d22f-4fa8-ba71-3df2cca08958,已经是在本地磁盘的缓存文件了,
            // Android webview禁止通过XMLHttpRequest读取,xhr.send()直接报错:Not allowed to load local resource: blob:null/c9543d96-d22f-4fa8-ba71-3df2cca08958
            //直接拿到src后下载https://aistudio.google.com/d05f4db1-d82d-43dd-9add-16865de1e3f5,报404,似乎一次下载后图片被删除
            //每次查看大图,每次url不一样,所以应该是接到download回调后,拿到url后,去html中查找blob:字符串,拿到真正的url,然后用js侧的XMLHttpRequest转为base64

            if(url.startsWith("blob:null/")){
                quickWebview.getSource(new ValueCallback<String>() {
                    @Override
                    public void onReceiveValue(String html) {
                        List<BlobUrlParser.BlobImageInfo> blobImageInfos = BlobUrlParser.parseHtml(html);
                        if(blobImageInfos ==null ||  blobImageInfos.isEmpty()){
                            MyToast.error("blobImageInfos is empty");
                            return;
                        }
                        BlobUrlParser.BlobImageInfo info = blobImageInfos.get(0);
                        doDownload(hidden, info.getOriginalHttpsUrl(), info.getAltText(), contentLength, closeAfterSuccess, activity);
                    }
                });
                return;
            }


            //调用js桥接:
            //AndroidFileWriterUtil.writeFileToDownloads(null,name,DownloadApi.create(url).getInputStream());
            //args[0] = blob:null/9296a454-2bc1-4a59-9ee7-bf1e659377a1
            //│ args[1] = Mozilla/5.0 (Linux; Android 11; Redmi K30 Build/RKQ1.200826.002; wv) AppleWebKit/537.36 (KHTML,
            // like Gecko) Version/4.0 Chrome/139.0.7258.158 Mobile Safari/537.36 终极图片压缩/3.1.00-debug/3100
            //│ args[2] =
            //  │ args[3] = text/plain
            //  │ args[4] = 362196
            // 注入JavaScript获取blob数据
            // 改进的JS注入代码,避免null origin问题
            // 使用XMLHttpRequest替代Fetch API,解决blob协议不支持问题

            //That's the origin, file system and sandboxed iframes(maybe others) have null as their origin.
            // If you set up a local sever it should say http%3A//localhost, that's http://localhost url encoded
            String js = "javascript:(function() {" +
                    "   var xhr = new XMLHttpRequest();" +
                    "   xhr.open('GET', '" + url + "', true);" +
                    "   xhr.responseType = 'blob';" +
                    "   xhr.onload = function() {" +
                    "       if (this.status === 200) {" +
                    "           var blob = this.response;" +
                    "           var reader = new FileReader();" +
                    "           reader.onload = function(e) {" +
                    "               window.blobDownload.downloadBlobFile('" + name + "', e.target.result);" +
                    "           };" +
                    "           reader.onerror = function() {" +
                    "               console.error('Error reading blob file');" +
                    "           };" +
                    "           reader.readAsDataURL(blob);" +
                    "       } else {" +
                    "           console.error('Failed to fetch blob: ' + this.status);" +
                    "       }" +
                    "   };" +
                    "   xhr.onerror = function() {" +
                    "       console.error('Error fetching blob');" +
                    "   };" +
                    "   xhr.send();" +
                    "})()";

            // 在主线程执行JavaScript
            if(webView ==null){
                MyToast.error("webView ==null, 无法下载blob,请在代码里传入webview引用");
                return;
            }
            webView.post(() -> {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
                    webView.evaluateJavascript(js, new ValueCallback<String>() {

                        @Override
                        public void onReceiveValue(String value) {
                            String filePath = value;
                            if(TextUtils.isEmpty(value) || "null".equals(value)){
                                LogUtils.w("file path return: ",value);
                                MyToast.error("文件保存失败:"+name);
                            }else {
                                DownloadCallbackDbDecorator.cutFileToMediaStore(new File(filePath),
                                        "/"+ AppUtils.getAppName().toLowerCase()+"/nano-banana");
                                //todo hidden的处理
                            }
                        }
                    });
                } else {
                    webView.loadUrl(js);
                }
            });
            return;
        }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96

# 通过OnLongClickListener回调直接拿base64

    @Override
    public boolean onLongClick(View v) {
        final WebView.HitTestResult hitTestResult = quickWebview.getWebView().getHitTestResult();
        int type = hitTestResult.getType();
      
      // case WebView.HitTestResult.IMAGE_TYPE: // 处理长按图片的菜单项
      
       String extra = hitTestResult.getExtra();
                if(!TextUtils.isEmpty(extra) && extra.contains("data:image/") && extra.contains(";base64,")){
                    saveBase64ImgToGallery(extra);
                }else {
                    downloadAndSaveImgToGallery(extra,hitTestResult.getType());
                }
1
2
3
4
5
6
7
8
9
10
11
12
13
    private void saveBase64ImgToGallery(String base64String) {
        try {
            // 提取纯Base64数据(去除前缀)
            String[] parts = base64String.split(";base64,");
            if (parts.length < 2) {
                LogUtils.w( "Invalid Base64 string format", base64String);
                MyToast.error("Invalid Base64 string format: "+base64String);
                return;
            }

            // 解码Base64字符串为字节数组
            byte[] decodedBytes = Base64.decode(parts[1], Base64.DEFAULT);

            // 将字节数组转换为Bitmap
            Bitmap bitmap = BitmapFactory.decodeByteArray(decodedBytes, 0, decodedBytes.length);

            if (bitmap == null) {
                LogUtils.w("Failed to decode Base64 to Bitmap");
                MyToast.error( "base64图片解码失败");
                return;
            }
            File tmpFile = new File(Utils.getApp().getExternalCacheDir(), "webview-download");
            tmpFile.mkdirs();
            File file = new File(tmpFile, System.currentTimeMillis()+".jpg");
            boolean compress = bitmap.compress(Bitmap.CompressFormat.JPEG, 85, new FileOutputStream(file));
            if(!compress){
                MyToast.error("保存图片失败");
                return;
            }
            MediaStoreUtil.writeMediaToMediaStore(file, AppUtils.getAppName().toLowerCase(), new MyCommonCallback3<String>() {
                @Override
                public void onSuccess(String s) {
                    MyToast.success("保存成功: \n"+s);
                    file.delete();

                }

                @Override
                public void onError(String code, String msg, @Nullable Throwable throwable) {
                    MyCommonCallback3.super.onError(code, msg, throwable);
                    MyToast.error(msg);
                }
            });
        } catch (Exception e) {
            LogUtils.w( "Error saving Base64 image: " ,e);
            MyToast.error("保存图片失败:\n"+e.getMessage());
        }
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
    private void downloadAndSaveImgToGallery(String extra,int type) {
        if(TextUtils.isEmpty(extra)){
            MyToast.debug("图片url为空");
            return;
        }
        String mime = null;
        if(extra.startsWith("blob:")){
            extra = extra.replace("blob:","");
            //需要用到type
            if(type == WebView.HitTestResult.IMAGE_TYPE){
                mime = "image/jpeg";
            }
            //extra.contains("data:image/") && extra.contains(";base64,")
            //todo 这里https://拿到的二进制是base64,所以需要转换成图片,要在下载框架里处理好
        }
        new WebviewDownladListenerImpl()
                .setWebView(quickWebview.getWebView(),quickWebview)
                .onDownloadStart(extra,"","",mime,-1,false);

    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 源代码

https://github.com/hss01248/utilcodeEnhance2/blob/combine-image-loader/baseWebview/src/main/java/com/hss01248/basewebview/TheLongPressListener.java

编辑 (opens new window)
webview权限适配和getUserMedia适配
Rxjava

← webview权限适配和getUserMedia适配 Rxjava→

最近更新
01
一行代码完成startActivityForResult
09-01
02
Android上图片视频选择功能的最佳实践
08-21
03
Android保存图片或视频到相册的最佳实践
08-21
更多文章>
Theme by Vdoing | Copyright © 2020-2025 | 粤ICP备20041795号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式