* 此文纯粹只是乱谈而已,基本上是只知其一不知其二,还请各位菊苣指正 _(:3
由于某些需要,需要在 JS 里创建一个 Blob(Blob 对象就是包含有只读原始数据的类文件对象)并将数据存入,然后让用户保存它。将其交给用户的方式,对 IE 有一个奇奇怪怪的 navigator.msSaveBlob()
,而对大多数现代浏览器来说,可以用 URL.createObjectURL()
来生成一个以 blob:
开头的地址来指向 Blob。目前的主流浏览器都是支持的(不过很可惜,Opera (Presto) 终于在它的最后一个大版本 12 版本实现了 blob,但没有实现 Blob URLs),然而不同浏览器对 Blob 的数据大小是有限制的。这里有份 FileSaver.js 的统计数据。
Browser | Constructs as | Filenames | Max Blob Size |
---|---|---|---|
Firefox 20+ | Blob | Yes | 800 MiB |
Firefox < 20 | data: URI | No | n/a |
Chrome | Blob | Yes | 500 MiB |
Chrome for Android | Blob | Yes | 500 MiB |
IE 10+ | Blob | Yes | 600 MiB |
Opera 15+ | Blob | Yes | 500 MiB |
Opera < 15 | data: URI | No | n/a |
Safari 6.1+ | Blob | No | ? |
Safari < 6 | data: URI | No | n/a |
* FileSaver.js 开发者对不支持的浏览器做了一个叫 Blob.js 的兼容脚本,但其本质应该就是将 Blob 转为 data URL |
嗯,瞎扯了这么多,现在开始乱谈 _(:3
最近遇到的问题就是可能需要存储用 JSZip(一个可以读取或生成 Zip 文件的 JS 库)生成的一个大于浏览器限制大小的 zip 文件(交给用户前自然是生成 blob 对象啦),所以可能需要考虑分成几个 zip 文件的方式交给用户。首先我们先来测试一下直接生成一个浏览器(下文除非特别声明均使用 Google Chrome 42)可以接受的 blob 大小的 blob 对象并保存。
1 2 |
var blob = new Blob([new ArrayBuffer(500 * 1024 * 1024)]); document.body.innerHTML = '<a href="' + URL.createObjectURL(blob) + '" download="example.tmp">Download</a>' |
嗯,浏览器成功保存了这个 example.tmp 文件,可以看见文件大小为 500 MB。接下来我们刷新当前页面,并生成一个大于浏览器可以接受的 blob 大小的 blob 对象并保存。
1 2 |
var blob = new Blob([new ArrayBuffer(500 * 1024 * 1024 + 1)]); document.body.innerHTML = '<a href="' + URL.createObjectURL(blob) + '" download="example.tmp">Download</a>' |
最后浏览器在下载时告诉我们“失败 – 未发现文件”,所以直接生成大于可以接受的大小的 blob 对象肯定是行不通的啦。
那么如果我们生成好几个小于浏览器限制的文件并逐一储存呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
var blob1 = new Blob([new ArrayBuffer(100000000)]); var url1 = URL.createObjectURL(blob1); document.body.innerHTML += '<a href="' + url1 + '" download="example1.tmp">Download</a>'; // 可以保存 var blob2 = new Blob([new ArrayBuffer(100000000)]); var url2 = URL.createObjectURL(blob2); document.body.innerHTML += '<a href="' + url2 + '" download="example2.tmp">Download</a>'; // 可以保存 var blob3 = new Blob([new ArrayBuffer(100000000)]); var url3 = URL.createObjectURL(blob3); document.body.innerHTML += '<a href="' + url3 + '" download="example3.tmp">Download</a>'; // 可以保存 var blob4 = new Blob([new ArrayBuffer(100000000)]); var url4 = URL.createObjectURL(blob4); document.body.innerHTML += '<a href="' + url4 + '" download="example4.tmp">Download</a>'; // 可以保存 var blob5 = new Blob([new ArrayBuffer(100000000)]); var url5 = URL.createObjectURL(blob5); document.body.innerHTML += '<a href="' + url5 + '" download="example5.tmp">Download</a>'; // 可以保存 var blob6 = new Blob([new ArrayBuffer(100000000)]); var url6 = URL.createObjectURL(blob6); document.body.innerHTML += '<a href="' + url6 + '" download="example6.tmp">Download</a>'; // 不能保存 |
可以看到,从第六个 blob 开始,文件就不能储存了。所以,这个 blob 的限制大小不是单个文件的限制大小,而是所有 blob 的大小的和。那么,这个是不是也和 Object URL 的数量有关呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
var blob = new Blob([new ArrayBuffer(100000000)]); var url1 = URL.createObjectURL(blob); document.body.innerHTML += '<a href="' + url1 + '" download="example1.tmp">Download</a>'; // 可以保存 var url2 = URL.createObjectURL(blob); document.body.innerHTML += '<a href="' + url2 + '" download="example2.tmp">Download</a>'; // 可以保存 var url3 = URL.createObjectURL(blob); document.body.innerHTML += '<a href="' + url3 + '" download="example3.tmp">Download</a>'; // 可以保存 var url4 = URL.createObjectURL(blob); document.body.innerHTML += '<a href="' + url4 + '" download="example4.tmp">Download</a>'; // 可以保存 var url5 = URL.createObjectURL(blob); document.body.innerHTML += '<a href="' + url5 + '" download="example5.tmp">Download</a>'; // 可以保存 var url6 = URL.createObjectURL(blob); document.body.innerHTML += '<a href="' + url6 + '" download="example6.tmp">Download</a>'; // 可以保存 |
可以看到,同一个 blob 对象,无论生成多少个 Object URL 都可以保存。其实,即便没有生成 Object URL 而只是生成了一个 blob 对象,其同样占据了所有 blob 对象的空间。Chrome 内还有一个页面可以提供当前的 blob 存储状态:chrome://blob-internals/,在这里可以看到这些 Object URL 都是指向同一个 blob 对象,所以无论对同一个文件生成多少个 Object URL 都没有影响。
所以,如果需要生成多个 blob 对象,就需要先将之前的 blob 对象销毁。那么该怎么销毁这个 blob 对象呢?覆盖变量?
1 2 |
var blob = new Blob([new ArrayBuffer(100000000)]); blob = new Blob([new ArrayBuffer(100000000)]); |
事实证明浏览器并没有因为覆盖变量而回收之前的 blob,而是生成了一个新的 blob 对象(其实从直觉上看就不可能吧 23333
那么用 delete
呢?delete
并不能删除通过 var
创建的变量。而且即便这个 blob 在某个 Object
内而可以使用 delete
,在 Blob Storage Internals 内它还是存在的。而且实际上 delete 并不是删除变量的内容或者清空其占用的内存。
delete
运算符同样有副作用:删除一个属性就像(但不完全一样)给这个属性赋值undefined
。—— JavaScript 权威指南, David Flanagan (淘宝前端团队 译). §4.7.4
delete
…… 用来删除对象属性或者数组元素(注:如果你是 C++ 程序员,请注意 JavaScript 中的delete
和 C++ 中的delete
是完全不同的。在 JavaScript 中,内存的回收是通过垃圾回收自动回收的……)。—— JavaScript 权威指南, David Flanagan (淘宝前端团队 译). §4.13.3
那么删除 blob 对象有没有什么方法呢?看了下 FileSaver.js,里面有这么个玩意:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
var get_URL = function() { return view.URL || view.webkitURL || view; } // See https://code.google.com/p/chromium/issues/detail?id=375297#c7 and // https://github.com/eligrey/FileSaver.js/commit/485930a#commitcomment-8768047 // for the reasoning behind the timeout and revocation flow , arbitrary_revoke_timeout = 500 // in ms , revoke = function(file) { var revoker = function() { if (typeof file === "string") { // file is an object URL get_URL().revokeObjectURL(file); } else { // file is a File file.remove(); } }; if (view.chrome) { revoker(); } else { setTimeout(revoker, arbitrary_revoke_timeout); } }; |
所以,回收 blob 的方法就是 URL.revokeObjectURL()
_(:3 诶,那我之前用这个怎么不起作用(← 估计是你自己把变量覆盖了吧) 不过话说回来生成了 blob 对象后似乎必须用 URL.createObjectURL()
然后再 URL.revokeObjectURL()
才能销毁,好像好麻烦呢 _(:3
不过有时候用 URL.revokeObjectURL()
在某些浏览器上确实有点问题。比如说下面的例子:
1 2 3 4 5 6 7 |
var blob = new Blob([new ArrayBuffer(500000000)]); var url = URL.createObjectURL(blob); console.log(url); // blob:http%3A//ccloli.com/483b13f6-f801-4f85-9bbf-a88946513686 URL.revokeObjectURL(url); // revoke blob var blob = new Blob([new ArrayBuffer(500000000)]); // create a new blob var url = URL.createObjectURL(blob); console.log(url); // blob:http%3A//ccloli.com/c815c479-0ac8-4802-a64b-a5cce06d1015 |
可以看到这段代码先生成了一个 blob,然后 URL.createObjectURL()
生成了一个 blob URL,然后立即调用 URL.revokeObjectURL()
进行销毁,随后再生成了一个 blob 并生成 blob URL。但是有没有发现如果保存第二个链接会报“失败 – 未发现文件”呢?去 Blob Storage Internals 里看看:
有没有发现和之前的数据有什么不同吗?对,里面只有 Refcount 一项,其他的诸如 Count、Index 都没了,所以相当于这个 blob 并没有分配实际的内存空间。这是为什么呢?这个估计应该是在执行 URL.revokeObjectURL()
后,blob 并没有来得及完全销毁就要求分配新的 blob 空间,结果由于没有足够的空间而没有实际分配。所以,对于这种情况,可以像 FileSaver.js 那样巧妙地用个 setTimeout
来等待之前的 blob 被销毁。但是好像对于较大的文件 Chrome 有点奇葩啊 _(:3
1 2 3 4 5 6 7 8 9 10 11 12 |
var blob = new Blob([new ArrayBuffer(500000000)]); var url = URL.createObjectURL(blob); console.log(url); setTimeout(function(){ URL.revokeObjectURL(url); // revoke blob url successfully, but blob is NOT removed setTimeout(function(){ blob = new Blob([new ArrayBuffer(500000000)]); // create a new blob, but it has no enough space url = URL.createObjectURL(blob); console.log(url); // finally, a few seconds later, the first blob is removed, WTF? }, 10000); }, 10000); |
即便给足了时间让其生成和销毁但是还是有点 bug 啊。唉,看来还是不要弄太大的文件比较安全 _(:3 保险起见还是分割到 250MB 以内吧……
顺带这是开头提到的那个奇怪的东西 _(:3
ccloli / E-Hentai-Downloader
Download E-Hentai archive as zip file
Hen…Hentai! QAQ
最后,你可以自己试试在不同的标签页或者是在不同的域名的标签页上创建一些比较大的 blob,直到无法分配实际可用的大小。你会发现,至少在 Chrome 中,所有的标签页都是共用那 500MB 的 blob 可用空间的。
2015 年 9 月 3 日 02:24:46 更新:
最近重新测试了一下,发现还是有点问题,故追加内容如下,欢迎各位菊苣答疑解惑 _(:3
首先,我们在控制台执行以下语句:
1 |
var blob = new Blob([new ArrayBuffer(1000)]); // 创建一个 1000 B 大小的 Blob 对象 |
然后刷新 chrome://blob-internals ,看到了如下内容:
1 2 3 4 |
1167d550-9c7a-429f-bd27-824552f4cbe6 - Refcount: 1 - Type: data - Length: 1,000 |
接下来生成 blob URL
1 |
var url = URL.createObjectURL(blob); |
再次刷新 chrome://blob-internals ,看到了如下内容:
1 2 3 4 5 6 7 |
1167d550-9c7a-429f-bd27-824552f4cbe6 - Refcount: 2 - Type: data - Length: 1,000 ------------------------------------ blob:http%3A//ccloli.com/f6200637-f320-4cd3-937c-7562d8122acc - Uuid: 1167d550-9c7a-429f-bd27-824552f4cbe6 |
有没有发现什么改变了?对,Refcount。这个玩意是什么呢?先暂时不管它,接下来我们再执行这样的操作:
1 |
URL.revokeObjectURL(url); |
然后再次刷新 chrome://blob-internals :
1 2 3 4 |
1167d550-9c7a-429f-bd27-824552f4cbe6 - Refcount: 1 - Type: data - Length: 1,000 |
欸,很奇怪诶,为什么会还残留呢?明明已经把它给 URL.revokeObjectURL()
掉了啊,为什么还留着呢?而且即便不在控制台,而是在 HTML 里的 <script>
内执行的话也会有这样的问题。别着急,让我们先回来看看这个奇怪的 Refcount。从名字上看应该是引用该 blob 的联系数,如果是这样的话,那么可以推断第一个 Refcount 是来自 blob 这个变量,第二个 Refcount 来自 blob:http%3A//ccloli.com/f6200637-f320-4cd3-937c-7562d8122acc
这个 Object URL。这么理解的话就可以理解为 blob
变量还和这个 blob 对象有联系,所以没有被自动回收。欸,好不爽诶 (~ ̄▽ ̄)→))* ̄▽ ̄*)o
不过既然 Refcount 被理解为引用数的话,那么断开他们的联系应该就会回收了吧 ww 或许 delete
会有效哦。
1 2 3 4 |
var blob = {blob: new Blob([new ArrayBuffer(1000)])}; var url = URL.createObjectURL(blob.blob); URL.revokeObjectURL(url); delete blob.blob; |
诶,很奇怪诶,blob.blob
已经为 undefined 了,为什么那个 blob 还在那啊╰(‵□′)╯
* 其实这个灵感来自 StackOverflow 的这个回答:Delete Javascript blobs?。在这个回答内,作者说明了 delete
的实际意义,并且给出了一个示例代码。代码中作者创建了一个名为 myBlob
的对象,并赋予其 getURL()
和 dispose()
方法,可以使用 myBlob.dispose()
来销毁 myBlob.blob
,甚至还把原型对象中的各项设置了 enumerable
和 configurable
。然而似乎并没有什么卵用,第一次测试的时候貌似成功删除掉了,第二次测试的时候貌似就失效了,刚刚试了一下貌似还是不能用 _(:3
在 Google 搜索 refcount blob 关键字,找到了这个 issue:Issue 468718 – Blob Objects are not getting cleared once the blob object is de-referenced,看来这个问题还是确实存在的 _(:3 不过之后有人提到在新的 W3C File API TR 中提出了 Blob.close()
方法。嘛,不管怎么样我只想知道这个问题该怎么解决啊,这个奇奇怪怪的 blob 有时会销毁有时又会留在那是怎么回事啊 TwT……
由于不清楚 Firefox 有没有方法监视 Blob 状态,所以不确定 Firefox 有没有这个问题。如果各位菊苣有什么补充或者帮助的话,还望各位菊苣指教 _(:3」∠❀)_
2015 年 9 月 4 日 01:23:57 更新:
在章鱼菊苣的某群里问了一下,得到了以下答复:
因为通过 URL.createObjectURL() 创建的引用没法通过 JS 引擎来跟踪,所以有 revokeObjectURL() 来解除引用。但这只是 blob: 链接不可用并通知了 GC 可以回收这个对象,至于 GC 什么时候会实际回收?只有 GC 自己知道。
—— 依然独特@放課後のWeb勉強会
好吧既然菊苣这么说了看来这玩意目前是没法解决了 _(:3
CC越来越菊苣了呢
现在确实木有什么好办法…
对JS不懂,希望赶紧解决哦~
http://www.ccloli.com
ERR_TOO_MANY_REDIRECTS
@Kirito 之前这个域名是解析到 Google PageSpeed 的,现在 PageSpeed 已经停止服务了,所以解析回来了,可能有些配置问题,待会看看
冒个泡,证明我还活着
菊苣你好~
突然想到,能不能把ArrayBuffer给postMessage到其它进程(如WebWorker,注意要transfer不要复制……)由WebWorker生成blob和url,下载并revokeObjectURL之后用worker.terminate(),进程都关闭了,内存自然回收了(仅为设想,不保证能用
以及个人对revokeObjectURL的理解是,WeakMap不能作为key的东西(primitive类型)有可能可以从其它地方拿到,因此回收key时无法将value一并回收,blob:的url也是如此,必须手动解除掉引用才能被回收
说到blob,就想到了GM_xmlhttpRequest的 arraybuffer,respone变量一旦写上(无赋值),就会卡住浏览器8秒之久,太可怕了!
看来你使用的是 Tampermonkey,这个只能等这个 bug 修复:https://github.com/Tampermonkey/tampermonkey/issues/279 ,不过作者并没有回复我。
其他扩展程序像 GreaseMonkey 和 Violentmonkey 应该是没问题的。
ViolentMonkey和Tampermonkey结合了
看不懂… 查看元素找到blob:http://XXX链接的视频, 想要下载有什么办法可以实现呢? YouTube上找到这么个视频https://www.youtube.com/watch?v=IBKRNdBJxMc, 下载jDownloader进度条简直不动
u2b用的是MediaSource,对MediaSource通用的方法大概只有劫持SourceBuffer的appendBuffer方法然后手动拼buffer下载吧
只对u2b来说,可以直接去network面板找写着videoplayback的请求,复制地址,把地址中”&range=数字-数字”去掉就是下载地址……
当然,u2b有很多下载方法,没必要F12就是了(:з」∠) https://www.google.com/search?hl=zh-CN&safe=off&q=youtube+download