深度理解 图片预加载和缓存机制

本文通过两个图片预加载案例引起的缓存相关问题,探讨了图片预加载处理技术,和浏览器网络请求以及缓存机制的一些问题。

2017-04-25 By Herbert Chow

问题起源分析:

一、腾讯游戏:征服星际 战出未来 移动视频h5中(如图1),采用了预加载图片技术,先有一个loading页,然后一个视频,之后是一个落地页结束。

在预加载图片时,图片会有一定的概率发生重复请求(重复加载)的情况并且被重复加载的图片会出现闪烁,就号像根本就没有预加载一样,原因不明,如(图2),而且如果通过自己手写函数进行预加载图片,会有极大几率发生重复请求的想象;而该用pxloader.js进行图片加载的话,几率大大减少,但不为100%不出现。

a.png

图1

a2.png

图2

二、阴阳师扭蛋预约h5(如图3):与上问题一相似,使用网易内部组件trueload组件进行加载,在预加载图片时,会有一定几率会重复加载zhaohuanzhen_的序列帧套图,本来只有46张,后面重复加载到了85张,或者92张(完全重复请求了一遍)。流程是,先loading页,再到登陆页,再到抽奖页面,最后是结果页面。在抽奖页面,会用到canvas调用loading时缓存的序列帧图片。

a3.png

图3

注:当图片重复请求时,会造成图片闪烁,有可能是因为序列帧动画切换比较连贯,所以看起来闪烁会比较明显,而平时hover时看可能不那么明显。本质问题应该还是图片重复请求的问题。

问题假设:

一、是否只要在页面中请求一次图片A并且成功后,那么图片A在后续的设置img.src或者设置bgi时,都不会发生重复请求,而是直接用缓存;

二、是否只要图片的路径是一样的,读取了一次成功后,就不需再次请求,直接读缓存。

论证猜测(不供证明,仅供参考):

一、用Pxloader或者trueLoad等组件进行加载时,会比自己手写加载函数(大神请忽略)会好很多,实验证明,自己手写预加载函数一般会缺少一些逻辑判断,导致效果不好,会有较大概率出现重复请求的情况。查阅一下Pxloader的源码,发觉,简单的一个图片加载函数,代码量也还是挺大的,不是我们自己一百几十行就能搞定的。

举例:有可能出现bug原因是,我们自己写的加载函数还没加载完图片,然后业务逻辑代码已经又进行了一次图片的请求,这样就会造成同一张图同时被请求了两次。

成因剖析(第一部分):预备概念的理解

1、图片src或者background-image (下面简称bgi)设置和切换问题:

参考链接: https://github.com/jieyou/lazyload#%E4%B8%8Esrcset%E5%B1%9E%E6%80%A7%E4%B8%80%E8%B5%B7%E4%BD%BF%E7%94%A8

参考链接中提到,“创建的Image对象会加载一次,实际DOM树中的元素设置 src 或 background-image 时又会加载一次”,只要image对象被创建,并且设置其src属性时,浏览器就会发起请求,或者html中dom结构中的图片img标签src发生变化,big发生变化时,也会发生请求。

2、浏览器对于图片请求的流程

参考链接:http://er.dadaaierer.com/?p=431

参考链接中提到:

关于图片缓存

如果图片已经在缓存中:

1)浏览器不会发起请求去请求图片,浏览器直接从缓存中拿图片。

2)而设置过期时间会强迫浏览器在访问页面的时候去请求图片,如果图片已经在缓存中,并且正在被重新请求,浏览器会把最后修改时间加入在HTTP头中,这就是传统的GET请求,如果图片没有被修改,服务器会返回一个304代码,所以对于浏览器的请求服务器会返回下面的两种代码:

200–浏览器没有缓存。

304–浏览器已经缓存了图片,但是需要验证最后修改时间

也就是说,浏览器在需要进行图片操作之前,会先查看本地是否有缓存,如果有,会先读取缓存;如果没有,才会去发起网络请求。

成因剖析(第二部分):分析原因

一、如图4,是腾讯外星人h5在一定概率刷新时,发生重复请求,此刻注意到网络部分,size图片请求时间是0ms,而图片大小其实是没有标明的,写着 from menory cachefrom disk cache,此刻还注意到,网络请求时200而不是应该是语气中的304,十分奇怪。

a4.png

图4

二、如图5,阴阳师扭蛋h5,有一定概率,在发起200请求之后预加载图片后,发起重复请求并且返回状态码304。

a5.png

图5

三、这里有几点要注意

1、200和304跟缓存的联系

2、请求发起者initiator

3、size(from memory cache和from disk cache)

四、知识点再次分析

对于上面三点

1、参考链接: https://www.oschina.net/question/1395553_175941

参考文中提到,

其实, 200 OK (from cache) 是浏览器没有跟服务器确认,直接用了浏览器缓存;而 304 Not Modified 是浏览器和服务器多确认了一次缓存有效性,再用的缓存。200(from cache) 是速度最快的,因为不需要访问远程服务器,直接使用本地缓存.304 的过程是, 先请求服务器, 然后服务器告诉我们这个资源没变, 浏览器再使用本地缓存.

结论: 需要设置 200 from cache. 这样才是解决问题之道.

所以说,其实200(from cache)并没有重新请求下载图片,而是和304是相似的原理,就是浏览器并没有重新下载图片,只是用了本地缓存,只不过浏览器会多了一重请求验证。至于这个具体是返回200from cache还是304,貌似需要看服务器返回的东西是否有设置ETag值了(这部分需要详细了解的话,请自行查阅资料)。

2、initiator是请求发起者,说白了就是你在哪里执行了设置img.src属性或者设置了bgi。在扭蛋项目中,笔者进行了预加载(由trueload.js发起)完成之后,马上执行了一段由index.js执行的设置new Image()数组保存图片这么一个操作,用于后面的序列帧动画。而其实initiator:other标明的就是一些html的资源文件,包括了js,html,css之类,所以图5这里的other,就是index.js。相同的,在腾讯外星人h5中的首次请求和重复请求的发起者分别就是pxload.js和index.js(indexjs中执行到了设置bgi的代码)。

3、核心关键

size标明,304的时候,浏览器会向服务器发起请求,这个请求容量非常小,相对于200完整请求的20k完整图片而且,只有20B左右的大小,实际上,它返回的是304的结果本身,而不是图片数据。

重中之重的是这个from menory cache(内存缓存)from disk cache(磁盘缓存)

参考链接:http://blog.csdn.net/myloveyaqiong/article/details/52762795

(图6)文中提及,安卓系统对于网络图片缓存机制是,优先读取Memorycache,找不到之后会读取Diskcache,最后都没有才会发起网络请求。

翻阅《webkit技术内幕》,书中提及,浏览器也是如此,当m cache和d cache中都没找到资源时,会发起网络请求获取最新资源。

a6.png

图6

成因剖析(第三部分):结论总结

一、回顾问题假设一,答案是否定的。原因是进了页面以后,即使用了预加载技术,在后面的逻辑中再次创建并设置新的img.src或者bgi时,依然要经过上面提及的图片资源获取步骤:

优先读取Memorycache,找不到之后会读取Diskcache,最后都没有才会发起网络请求。也就是说,举例,一开始用了trueload预加载了图片a0.jpg之后(此时是trueload发起),图片被加入到了内存缓存和磁盘缓存中,而一段时间(或者一段逻辑跑完之后,来到index.js的某段代码),在index.js里面再次设置新的img.src时,由于某种原因,index.js发起获取图片a0.jpg在memorycache内存缓存里面找不到了,于是只能去diskcache磁盘缓存里面找,这个时候就会引起一个重复请求的现象。

二、回顾问题假设二:答案也是否定的。根据上述获取资源原理,同一路径资源的图片,即使在预加载或页面中有了第一次加载之后,后续代码段中再次访问该路径图片,依然有可能会造成前者能成功访问memorycache里的资源,而后者则失败从而导致要到diskcache里面去找图,这样的情况。

三、笔者尚不熟悉m cache的d cache处理数据的具体原理,尚未弄清为何导致memorycache内图片资源丢失,或者访问不了的情况,导致别的逻辑代码在二次访问的时候需要去d cache里面找资源,这可能和时间,系统内存,或者initiator请求发起者有关。还请各路大神指教一下。

四、至于返回结果是200from cache还是304,则需要服务器那边设置。但是可以明确一点的是,如果图片在访问页面时被请求成果过一次之后,那么后续再次访问则会有缓存,无论了是否重新发出请求,磁盘缓存中已经是有缓存到的了,所以在一定程度上已经是成功达到了图片预加载的目的。也就是说即使是重复请求了,那也是返回200from cache还是304,能够快速使用缓存,不必重新加载200成功完成图片请求。只不过再进一步优化的话,就是重复请求都不用了而已。

解决方案:(暂时只提供思路,后面会补上详细说明以及demo等)

一、采用专业的图片预加载插件,会比自己重新写的预加载函数要严谨和高效(高手请忽略),一般移动推荐pxloader,pc推荐jq的imgpreload。

二、一般情况下,大家只会在预加载时load一遍图片,然后后面访问图片时就直接设置img.src或者bgi:url(地址),这样在通常情况没有问题,但是有可能在一定概率下发生重复请求。建议改成在load图片时,把需要load的图片中的重点图片,比如序列帧这种需要同一个阶段大量调用一组图片,这时就可以把这组图片保存在一个图片数组变量中imgs[ ],然后后面就直接使用这个imgs[ ],不要重新创建对象再赋值图片地址

三、调整预加载和访问的代码逻辑顺序。如果不想像方案二那样浪费一个全局变量,可以再页面loading页加载完成后,不要马上调用“设置imgsrc保存到新数组,以便后续调用”的逻辑代码,而应该放到后面业务中的,比如点击按钮之后才调用,这样可以解决刚进入页面时并发请求过多导致页面卡顿的问题。

四、如果是采用bgi较多的项目,在调用预加载函数时,同时加载css,会发生css内的bgi地址和预加载js调用的地址相撞上,而发生重复请求的想象。所以,解决方法是,进页面时先把该css文件设置disabled="",屏蔽掉css加载,完了之后再remove掉disabled属性。

五、根据垃圾回收机制,可以把只调用一次的大量图片集,用全局图片数组变量保存起来,并在完成业务调用后,清除该全局变量imgs=null,回收不必要的内存。

其他补充:

待定