正確使用GPU動(dòng)畫

最近看到一篇關(guān)于GPU動(dòng)畫的神文絮宁,原文地址:https://www.smashingmagazine.com/2016/12/gpu-animation-doing-it-right/样悟。
特此翻譯出來,供自己以及他人學(xué)習(xí)和查看馒胆。轉(zhuǎn)載請(qǐng)標(biāo)明出處稍坯,謝謝腰鬼。
另外那婉,由于簡(jiǎn)書和GitHub的markdown均不支持內(nèi)嵌iframe。而文中的大多數(shù)效果圖是嵌在iframe中的,可以移步github浑侥,下載后在支持內(nèi)嵌iframe的markdown環(huán)境下查看姊舵。

如今,絕大多數(shù)人知道現(xiàn)代瀏覽器采用GPU來渲染網(wǎng)頁的部分內(nèi)容寓落,尤其是動(dòng)畫部分括丁。比如,采用transform屬性的CSS動(dòng)畫看起來比使用lefttop屬性的動(dòng)畫更流暢零如。但如果你要問:“我如何利用GPU實(shí)現(xiàn)流暢的動(dòng)畫躏将?”絕大多數(shù)情況下,你會(huì)聽到如下回答:“使用transform: translateZ(0)或者will-change: transform考蕾』霰铮”

在開始GPU動(dòng)畫-或者合成(compositing,瀏覽器廠商喜歡這么稱呼)之前肖卧,從某種意義上來說蚯窥,這些個(gè)屬性就有點(diǎn)像IE6中使用的zoom:1一樣。(譯者注:有點(diǎn)拗口塞帐,意思應(yīng)該是許多人只知道這樣設(shè)置就行了拦赠,但是并不知道具體的原因)

但有時(shí)候,簡(jiǎn)單demo中絲滑流暢的動(dòng)畫葵姥,在實(shí)際網(wǎng)站中運(yùn)行非常慢荷鼠,造成視覺假象,甚至讓瀏覽器崩潰榔幸。為什么會(huì)這樣允乐?如何修復(fù)?讓我們來了解一下削咆。

免責(zé)聲明

在深入研究GPU合成之前牍疏,我想告訴你們一件十分重要的事:這是一個(gè)大大的hack。至少到目前為止拨齐,合成的工作原理鳞陨,如何明確地將元素置于合成層,或者合成本身瞻惋,關(guān)于這些問題厦滤,你在W3C規(guī)范上找不到任何答案。它只是瀏覽器執(zhí)行特定任務(wù)時(shí)的一種優(yōu)化操作歼狼,每個(gè)瀏覽器廠商有自己的實(shí)現(xiàn)方式馁害。

本篇文章中你學(xué)到的所有東西,并不是合成原理的官方解釋蹂匹,而是我實(shí)驗(yàn)的結(jié)果,加上一點(diǎn)對(duì)瀏覽器子系統(tǒng)差異的常識(shí)和理解凹蜈。有些東西可能是錯(cuò)的限寞,有些可能隨著時(shí)間而變化——提醒過你了忍啸。

合成如何工作

在開始GPU動(dòng)畫頁面之前,我們得知道瀏覽器是如何工作的履植,不要簡(jiǎn)單地聽從網(wǎng)上的或者本篇文章中的一些隨意的建議计雌。

假設(shè)我們有一個(gè)頁面,其中有AB兩個(gè)元素玫霎,每一個(gè)都設(shè)置了position: absolute和不同的z-index值凿滤。瀏覽器會(huì)用CPU繪制,之后把完成的圖像發(fā)送給GPU庶近,由它顯示在屏幕上翁脆。

<style>
#a, #b {
 position: absolute;
}

#a {
 left: 30px;
 top: 30px;
 z-index: 2;
}

#b {
 z-index: 1;
}
</style>
<div id="a">A</div>
<div id="b">B</div>

<iframe src="https://sergeche.github.io/gpu-article-assets/examples/example1.html" height="280" frameborder="no" allowtransparency="true" style="width: 100%;"></iframe>

我們決定采用left屬性和CSS動(dòng)畫,讓A元素動(dòng)起來:

<style>
#a, #b {
 position: absolute;
}

#a {
 left: 10px;
 top: 10px;
 z-index: 2;
 animation: move 1s linear;
}

#b {
 left: 50px;
 top: 50px;
 z-index: 1;
}

@keyframes move {
 from { left: 30px; }
 to { left: 100px; }
}
</style>
<div id="a">A</div>
<div id="b">B</div>

<iframe src="https://sergeche.github.io/gpu-article-assets/examples/example1.html#.a:anim-left" height="280" frameborder="no" allowtransparency="true" style="width: 100%;"></iframe>

此種情況下鼻种,對(duì)于每個(gè)動(dòng)畫幀反番,瀏覽器都必須重新計(jì)算元素的位置(即reflow),渲染頁面新狀態(tài)的圖像(即repaint)叉钥,之后再發(fā)送給GPU顯示到屏幕上罢缸。我們知道,重繪是非常消耗性能的投队,但是每個(gè)現(xiàn)代瀏覽器都足夠智能枫疆,只重繪頁面中變化的部分,而不是整個(gè)頁面敷鸦。盡管絕大多數(shù)情況下息楔,瀏覽器可以很快地重繪,但我們的動(dòng)畫仍然不是太流暢轧膘。

在動(dòng)畫的每個(gè)階段回流钞螟、重繪整個(gè)頁面(即便是增量繪制),聽起來就很慢谎碍,尤其是又大又復(fù)雜的布局鳞滨。僅繪制兩個(gè)獨(dú)立的圖像可能更高效——一個(gè)為A元素,另一個(gè)為A元素以外的整個(gè)頁面——之后簡(jiǎn)單地偏移兩個(gè)圖片的相對(duì)位置蟆淀。也就是說拯啦,合成緩存元素的圖像可能更快。這就是GPU的優(yōu)勢(shì)所在:它能夠以亞像素精度快速合成圖像熔任,使得動(dòng)畫如絲般順滑褒链。

為了優(yōu)化合成,瀏覽器必須確保添加動(dòng)畫的CSS屬性:

  • 不會(huì)影響文檔流疑苔,
  • 不依賴文檔流甫匹,
  • 不會(huì)造成重繪。

有人可能會(huì)以為,topleft屬性兵迅,輔之以positionabsolutefixed抢韭,不依賴元素的環(huán)境,但其實(shí)并不是這樣恍箭。例如刻恭,left屬性可能是個(gè)百分比值,其依賴于.offsetParent的尺寸扯夭;另外鳍贾,emvh和其它單位依賴于它們的環(huán)境交洗。相反骑科,transformopacity是僅有的滿足上述條件的CSS屬性。

讓我們用transform而不是left來實(shí)現(xiàn)動(dòng)畫:

<style>
#a, #b {
 position: absolute;
}

#a {
 left: 10px;
 top: 10px;
 z-index: 2;
 animation: move 1s linear;
}

#b {
 left: 50px;
 top: 50px;
 z-index: 1;
}

@keyframes move {
 from { transform: translateX(0); }
 to { transform: translateX(70px); }
}
</style>
<div id="a">A</div>
<div id="b">B</div>

此處藕筋,我們以聲明的方式描述動(dòng)畫:起始位置纵散,結(jié)束位置,持續(xù)時(shí)間等隐圾。這等于提前告訴瀏覽器哪些CSS屬性會(huì)更新伍掀。因?yàn)闉g覽器發(fā)現(xiàn)沒有屬性會(huì)造成回流或者重繪,它就會(huì)采用合成優(yōu)化:畫兩幅圖像作為合成層暇藏,之后發(fā)送到GPU蜜笤。

這種優(yōu)化的優(yōu)點(diǎn)是什么呢?

  • 我們獲得了一個(gè)亞像素精度的盐碱、如絲般順滑的動(dòng)畫把兔,運(yùn)行在專門為圖形任務(wù)優(yōu)化的單元上。并且運(yùn)行得非澄屯纾快县好。
  • 動(dòng)畫再也不受限于CPU。即使運(yùn)行繁重的JavaScript任務(wù)暖混,動(dòng)畫依然很快缕贡。

一切聽起來似乎簡(jiǎn)單明了,不是嗎拣播?我們會(huì)遇到哪些問題晾咪?讓我們看看這種優(yōu)化的工作原理。

GPU是一個(gè)獨(dú)立的計(jì)算機(jī)贮配,這可能讓你覺得吃驚谍倦。確實(shí)如此:每個(gè)現(xiàn)代設(shè)備必不可缺的部分是一個(gè)獨(dú)立的單元,它有自己的處理器和內(nèi)存泪勒、數(shù)據(jù)處理模塊昼蛀。如同其它應(yīng)用和游戲一樣宴猾,瀏覽器必須與GPU進(jìn)行通信,好像和外設(shè)一樣曹洽。

為了更好地理解其工作原理鳍置,想像下Ajax。假設(shè)你想用填寫的表單數(shù)據(jù)注冊(cè)網(wǎng)站用戶送淆。你不能簡(jiǎn)單地告訴遠(yuǎn)端的服務(wù)器,“嗨怕轿,把這些表單數(shù)據(jù)和JavaScript變量保存到數(shù)據(jù)庫中偷崩。”遠(yuǎn)端數(shù)據(jù)庫無法訪問用戶瀏覽器的內(nèi)存撞羽。反而阐斜,你必須把頁面中的數(shù)據(jù)以易解析的格式(比如JSON),收集在一個(gè)payload中诀紊,然后發(fā)給遠(yuǎn)端服務(wù)器谒出。

合成的過程也差不多。GPU就像個(gè)遠(yuǎn)端的服務(wù)器邻奠,瀏覽器必須先創(chuàng)建一個(gè)payload笤喳,之后再發(fā)送給GPU。顯然碌宴,GPU不是遠(yuǎn)離CPU千里之外杀狡;它就在那。在很多情況下贰镣,對(duì)遠(yuǎn)端服務(wù)器的請(qǐng)求和響應(yīng)間隔時(shí)間在2S內(nèi)是可以接受的呜象。而對(duì)于GPU,3到5毫秒的延遲卻能導(dǎo)致動(dòng)畫卡頓碑隆。

GPU payload長(zhǎng)什么樣恭陡?一般由層圖像組成,還有一些附加的說明上煤,比如層的尺寸休玩,偏移,動(dòng)畫參數(shù)等楼入。以下是GPU的payload生成和傳輸?shù)拇蟾胚^程:

  • 將每個(gè)合成層繪制為獨(dú)立的圖像
  • 準(zhǔn)備層數(shù)據(jù)(尺寸哥捕,偏移,不透明度等)
  • 為動(dòng)畫準(zhǔn)備著色器(如果可用的話)
  • 發(fā)送數(shù)據(jù)給GPU

如你所見嘉熊,每次給元素添加神奇的transform: translateZ(0)或者will-change: transform屬性時(shí)遥赚,都開啟了同樣的過程。然而重繪是非常耗性能的阐肤,此時(shí)會(huì)變得更慢凫佛。多數(shù)情況下讲坎,瀏覽器無法增量重繪。它必須用新創(chuàng)建的合成層繪制之前覆蓋的區(qū)域:
<iframe src="https://sergeche.github.io/gpu-article-assets/examples/before-after-compositing.html" height="270" frameborder="no" allowtransparency="true" style="width: 100%;"></iframe>

隱式合成

讓我們回到之前A B元素的例子愧薛。早先晨炕,我們將A做成動(dòng)畫,它處在頁面所有元素之上毫炉。這會(huì)生成兩個(gè)合成層:A元素一個(gè)瓮栗,B元素和頁面背景一個(gè)。
現(xiàn)在瞄勾,我們讓B元素動(dòng)起來:
<iframe src="https://sergeche.github.io/gpu-article-assets/examples/example3.html#.b:anim-translate" height="280" frameborder="no" allowtransparency="true" style="width: 100%;"></iframe>
我們遇到了一個(gè)邏輯問題费奸。元素B應(yīng)該在一個(gè)獨(dú)立的合成層,屏幕最終呈現(xiàn)的圖像應(yīng)該在GPU中合成进陡。但是A元素應(yīng)該出現(xiàn)在B元素上面愿阐,并且我們沒有指定A提升到自己的層。

記得之前的免責(zé)聲明:GPU合成模式并不是CSS規(guī)范的一部分趾疚;它只是瀏覽器內(nèi)部使用的一種優(yōu)化策略缨历。如z-index定義的那樣,我們強(qiáng)制A出現(xiàn)在B的上面糙麦。那么辛孵,瀏覽器會(huì)怎么做呢?

猜對(duì)了喳资!瀏覽器會(huì)強(qiáng)制為A創(chuàng)建新的合成層——當(dāng)然觉吭,增加了一次繁重的重繪:
<iframe src="https://sergeche.github.io/gpu-article-assets/examples/example4.html#.b:anim-translate" height="280" frameborder="no" allowtransparency="true" style="width: 100%;"></iframe>

這稱為隱式合成:按照棧順序,一個(gè)或多個(gè)非合成元素出現(xiàn)在合成元素上面時(shí)仆邓,會(huì)被提升到合成層——即被繪制成獨(dú)立的圖像發(fā)送到GPU中鲜滩。

我們遇到隱式合成的情況比你想象的要頻繁的多。瀏覽器會(huì)因很多原因?qū)⒁粋€(gè)元素提升為合成層节值,比如:

  • 3D 變換:translate3d徙硅,translateZ等等;
  • <video>搞疗、<canvas><iframe>元素嗓蘑;
  • 通過Element.animate()實(shí)現(xiàn)的transformopacity動(dòng)畫;
  • 通過CSS transition animation實(shí)現(xiàn)的transformopacity動(dòng)畫匿乃;
  • position: fixed桩皿;
  • will-change
  • filter幢炸;

更多情況請(qǐng)參考Chromium項(xiàng)目的“CompositingReasons.h”文件泄隔。

似乎GPU動(dòng)畫的主要問題是意想不到的大量重繪。但并不是宛徊。最大的問題是佛嬉。逻澳。。

內(nèi)存消耗

再一次溫馨提示:GPU是獨(dú)立的計(jì)算機(jī):它不僅需要發(fā)送渲染好的圖片給GPU暖呕,而且需要對(duì)其進(jìn)行存儲(chǔ)斜做,以便后續(xù)動(dòng)畫復(fù)用。

一個(gè)合成層需要消耗多少內(nèi)存湾揽?讓我們看個(gè)簡(jiǎn)單點(diǎn)的例子瓤逼。猜猜存儲(chǔ)一個(gè)320×240像素,填滿#FF0000顏色的長(zhǎng)方形需要多少內(nèi)存钝腺。
<iframe src="https://sergeche.github.io/gpu-article-assets/examples/rect.html" height="270" frameborder="no" allowtransparency="true" style="width: 100%;"></iframe>

一個(gè)標(biāo)準(zhǔn)的web開發(fā)者這樣想:“嗯抛姑,這是個(gè)純色的圖像,我會(huì)將其保存為PNG然后查看其大小艳狐。應(yīng)該小于1KB”。沒錯(cuò)皿桑,這個(gè)PNG圖片大概104字節(jié)毫目。

問題是,PNG以及JPEG诲侮,GIF等镀虐,用來存儲(chǔ)和傳輸圖像數(shù)據(jù)。為了將這樣的圖像繪制到屏幕上沟绪,計(jì)算機(jī)必須解壓圖像數(shù)據(jù)刮便,然后表示成像素?cái)?shù)組。因此绽慈,我們的樣圖會(huì)消耗320 × 240 × 3 = 230,400 bytes的內(nèi)存恨旱。也就是,圖片寬度乘以高度獲得圖片的像素?cái)?shù)坝疼。之后再乘以3搜贤,因?yàn)槊總€(gè)像素由3個(gè)字節(jié)描述(RGB)。如果圖片包含透明通道钝凶,就得乘以4仪芒,因?yàn)楦郊拥囊粋€(gè)字節(jié)用來描述透明度(RGBa):320 × 240 × 4 = 307,200 bytes

瀏覽器總是按照RGBa圖像的形式繪制合成層耕陷。似乎沒有行之有效的方法來確定圖片是否包含了透明通道掂名。

再看一個(gè)更常見的例子:一個(gè)有10張圖的旋轉(zhuǎn)盤,每張圖800×600像素哟沫。我們希望用戶交互饺蔑,比如拖拽時(shí),圖片之間能夠平滑過渡南用,因此膀钠,我們?yōu)槊糠鶊D添加will-change: transform掏湾。這會(huì)提前將圖片提升到合成層,因此肿嘲,用戶一開始交互時(shí)融击,過渡就會(huì)開始。現(xiàn)在計(jì)算下僅僅展示這一旋轉(zhuǎn)盤需要多少額外內(nèi)存:800 × 600 × 4 × 10 ≈ 19 MB雳窟。

僅僅一個(gè)控制點(diǎn)就需要額外19MB內(nèi)存尊浪!如果你是一個(gè)單頁應(yīng)用的WEB開發(fā)者,頁面中有多個(gè)動(dòng)畫控制點(diǎn)封救,視差效果拇涤,高分辨率圖像和其它視覺增強(qiáng)效果,那么每個(gè)頁面多增加100到200MB僅僅是個(gè)開始誉结。再考慮上隱式合成的話(承認(rèn)吧——你之前根本沒想過這個(gè))鹅士,最終頁面會(huì)耗盡設(shè)備的內(nèi)存。

此外惩坑,多數(shù)情況下掉盅,這些內(nèi)存會(huì)被浪費(fèi)掉,用來顯示同樣的結(jié)果:
<iframe src="https://sergeche.github.io/gpu-article-assets/examples/example5.html" height="620" frameborder="no" allowtransparency="true" style="width: 100%;"></iframe>

對(duì)于桌面客戶端來說以舒,這可能不是個(gè)問題趾痘,但會(huì)深深刺痛移動(dòng)用戶的心。首先蔓钟,絕大多數(shù)現(xiàn)代設(shè)備擁有高分辨率的屏幕:這就將合成層圖片的體量乘以4到9永票。其次,移動(dòng)設(shè)備不像桌面設(shè)備那樣有那么大的內(nèi)存滥沫。比如侣集,不是太舊的iPhone6僅搭載1GB共享內(nèi)存(即,內(nèi)存同時(shí)用于RAM和VRAM)佣谐《抢簦考慮到至少三分之一的內(nèi)存用于操作系統(tǒng)和后臺(tái)進(jìn)程,另外的三分之一用于瀏覽器和當(dāng)前頁面(最好的情況是高度優(yōu)化的頁面狭魂,沒有太多的framework)罚攀,我們至多剩下200到300MB供GPU渲染。并且iPhone6是個(gè)相當(dāng)昂貴的高端設(shè)備雌澄,更多平價(jià)的手機(jī)所搭載的內(nèi)存更少斋泄。

你也許會(huì)問:“有可能在GPU上存儲(chǔ)PNG圖片來減少內(nèi)存占用嗎?”技術(shù)上是可行的镐牺。唯一的問題是GPU在屏幕上是逐像素繪制的炫掐,這意味著它必須一次次地解碼整個(gè)PNG圖片來獲取每個(gè)像素?cái)?shù)據(jù)。我懷疑這種情況下的動(dòng)畫比每秒一幀快點(diǎn)睬涧。

GPU特定的圖像壓縮格式確實(shí)存在募胃,但毫無意義旗唁。從壓縮比來看,根本比不上PNG或者JPEG痹束,并且使用上也缺乏硬件支持检疫。

優(yōu)缺點(diǎn)

既然學(xué)了些GPU動(dòng)畫的基本原理,讓我們總結(jié)下它的優(yōu)缺點(diǎn):

優(yōu)點(diǎn)

  • 動(dòng)畫既快又流暢祷嘶,達(dá)到每秒60幀屎媳。
  • 精心制作的動(dòng)畫在獨(dú)立的線程中運(yùn)行,不會(huì)被繁重的JavaScript計(jì)算阻塞
  • 3D變換很“廉價(jià)”

缺點(diǎn)

  • 需要附加的重繪來將元素提升到合成層论巍。有時(shí)這個(gè)過程很慢(比如烛谊,進(jìn)行全層重繪,而不是增量重繪)嘉汰。
  • 繪制的層必須傳到GPU中丹禀。依據(jù)層的大小和數(shù)量,傳輸可能很慢鞋怀。這可能導(dǎo)致中低端設(shè)備上元素閃爍湃崩。
  • 每個(gè)合成層消耗額外的內(nèi)存。在移動(dòng)設(shè)備上接箫,內(nèi)存是寶貴的資源。內(nèi)存超標(biāo)使用會(huì)使瀏覽器崩潰朵诫。
  • 如果你不考慮隱式合成辛友,重繪緩慢、額外內(nèi)存使用和瀏覽器崩潰的可能性會(huì)很高。
  • 我們會(huì)看到視覺假象,比如Safari上文本渲染余舶,在某些情況下适肠,頁面內(nèi)容會(huì)消失或者混亂。

如你所見砸西,盡管有些獨(dú)特的優(yōu)勢(shì),GPU動(dòng)畫仍然有些令人討厭的問題。最重要的是重繪和大量的內(nèi)存消耗掖看;因此,以下所有的優(yōu)化策略都是處理這些問題的面哥。

瀏覽器設(shè)置

在開始優(yōu)化之前哎壳,我們得學(xué)習(xí)一些工具,來幫助我們檢查頁面的合成層尚卫,并且提供合理的優(yōu)化反饋归榕。

Safari

Safari的web 監(jiān)視器有個(gè)很棒的“Layers”邊條,它顯示所有的層以及內(nèi)存消耗吱涉,以及合成的原因刹泄。來看看這個(gè)邊條:

  1. 在Safari中外里,按? + ? + I打開web監(jiān)視器。如果不起作用特石,打開“Preferences” → “Advanced”盅蝗,開啟“Show Develop Menu in menu bar”選項(xiàng),再試一次县匠。
  2. 當(dāng)web監(jiān)視器打開后风科,選擇“Elements”面板,選擇右邊條的“Layers”乞旦。
  3. 現(xiàn)在贼穆,當(dāng)你在主“Elements”上點(diǎn)擊一個(gè)DOM元素時(shí),你會(huì)看到一個(gè)關(guān)于選擇元素以及所有后代合成層的信息層(如果使用了合成的話)兰粉。
  4. 點(diǎn)擊一個(gè)后代層故痊,查看其合成原因。瀏覽器會(huì)告訴你為什么決定把這個(gè)元素遷移至它自己的合成層玖姑。


Chrome

Chrome的開發(fā)者工具欄有個(gè)類似的面板愕秫,但你必須首先激活它:

  1. 在Chrome中,訪問chrome://flags/#enable-devtools-experiments焰络,之后啟用“Developer Tools experiments”項(xiàng)戴甩。
  2. ? + ? + I(Mac)或者Ctrl + Shift + I(PC)打開工具欄,之后點(diǎn)擊右上角的如下圖標(biāo)闪彼,選擇“Setting”菜單項(xiàng):
  3. 回到“Experiments”面板甜孤,啟用“Layers”面板。
  4. 重新打開開發(fā)者工具欄∥吠螅現(xiàn)在缴川,你就能看到“Layers”面板了。


這個(gè)面板以樹的形式展示當(dāng)前頁面所有活動(dòng)的合成層描馅。當(dāng)選擇一個(gè)層的時(shí)候把夸,你會(huì)看到諸如尺寸,內(nèi)存消耗铭污,重繪次數(shù)和合成原因恋日。

優(yōu)化建議

現(xiàn)在我們已經(jīng)設(shè)置好環(huán)境,可以開始優(yōu)化合成層了况凉。我們已經(jīng)確定了合成的兩個(gè)主要問題:額外的重繪谚鄙,也會(huì)造成數(shù)據(jù)數(shù)據(jù)傳送到GPU,還有額外的內(nèi)存消耗刁绒。因此闷营,以下所有的優(yōu)化建議都是針對(duì)這兩個(gè)問題的。

避免隱式合成

這是最簡(jiǎn)單明了的建議,同樣也十分重要傻盟。提醒你一下速蕊,處在一個(gè)顯式合成層(比如position: fixed,視頻娘赴,CSS動(dòng)畫等)之上的所有非合成DOM元素规哲,會(huì)被強(qiáng)制提升到自己的層,僅僅為了最后的GPU圖像合成诽表。在移動(dòng)設(shè)備上唉锌,可能會(huì)導(dǎo)致動(dòng)畫啟動(dòng)緩慢。

舉個(gè)簡(jiǎn)單的例子:

<iframe height="305" scrolling="no" src="https://codepen.io/sergeche/embed/jrZZgL/?height=305&theme-id=light&default-tab=result&embed-version=2" frameborder="no" allowtransparency="true" allowfullscreen="true" style="width: 100%;"></iframe>

A元素是個(gè)需要用戶交互啟動(dòng)的動(dòng)畫竿奏。如果你在“Layers”面板中查看這個(gè)頁面袄简,你會(huì)發(fā)現(xiàn)沒有多余的層。但當(dāng)點(diǎn)擊“Play”按鈕后泛啸,你會(huì)看到更多的層绿语,這些層在動(dòng)畫結(jié)束后立即被移除。如果看下“Timeline”面板候址,會(huì)發(fā)現(xiàn)動(dòng)畫的開始和結(jié)束位置充斥大片區(qū)域的重繪:

以下是瀏覽器所做的吕粹,一步接一步:

  1. 頁面加載完成后,瀏覽器找不到任何合成的理由岗仑,因此選擇了最優(yōu)的策略:在單個(gè)背景層上繪制整個(gè)頁面內(nèi)容匹耕。
  2. 點(diǎn)擊“Play”按鈕,我們給元素A顯式增加了合成層——transfrom屬性的一個(gè)變換荠雕。但是瀏覽器發(fā)現(xiàn)按照棧順序泌神,元素A在元素B下面,因此也將B提升到自己的合成層(隱式合成)舞虱。
  3. 提升到合成層總會(huì)造成一次重繪:瀏覽器必須為元素創(chuàng)建一個(gè)新的紋理,然后從前一個(gè)層中移除掉母市。
  4. 新層圖像必須發(fā)送到GPU中矾兜,用來合成用戶最終看到圖像。依層的數(shù)量患久、紋理尺寸和內(nèi)容復(fù)雜度的不同椅寺,重繪和數(shù)據(jù)傳輸可能花許多時(shí)間。這就是許多動(dòng)畫在開始和結(jié)束時(shí)出現(xiàn)元素閃爍的原因蒋失。
  5. 動(dòng)畫結(jié)束一剎那返帕,我們從元素A上移除了合成的原因。此時(shí)篙挽,瀏覽器發(fā)現(xiàn)不需要浪費(fèi)資源來進(jìn)行合成荆萤,因此很快回到最優(yōu)策略:將頁面的整個(gè)內(nèi)容繪制在一個(gè)背景層當(dāng)中,這意味著必須把AB重繪回背景層當(dāng)中(另一次重繪),之后把更新的紋理發(fā)送給GPU链韭。如上步驟偏竟,可能造成閃爍。

為了免受隱式合成問題的困擾敞峭,減少視覺假象踊谋,有如下建議:

  • 給予動(dòng)畫元素盡可能高的z-index。理想情況下旋讹,這些元素應(yīng)該是body元素的直接子元素殖蚕。當(dāng)然,動(dòng)畫元素在DOM樹中嵌入很深沉迹、并且依賴常規(guī)流時(shí)睦疫,這是不大可能的。在此種情況下胚股,你可以克隆該元素笼痛,將其放置到body中僅作動(dòng)畫之用。
  • 你可以利用will-changeCSS屬性給瀏覽器一個(gè)提示琅拌,表明你要使用合成缨伊。將這個(gè)元素設(shè)置在元素上,瀏覽器會(huì)(并不總是)將其提前提升到一個(gè)合成層中进宝,因此動(dòng)畫能夠流暢地啟動(dòng)和停止刻坊。但別濫用這個(gè)屬性,否則最終會(huì)導(dǎo)致內(nèi)存的急劇消耗党晋!

僅將tranformopacity屬性動(dòng)畫化

tranformopacity屬性能夠確保既不影響也不會(huì)被常規(guī)流或者DOM環(huán)境影響(也就是說谭胚,它們不會(huì)造成回流或者重繪,因此動(dòng)畫完全交由GPU渲染)未玻≡侄基本上,這意味著你可以高效地實(shí)現(xiàn)移動(dòng)扳剿、縮放旁趟、旋轉(zhuǎn)、透明變換動(dòng)畫庇绽,并且只有仿射變換锡搜。有時(shí),你可以用這些屬性模擬其它動(dòng)畫類型瞧掺。

舉個(gè)非常常見的例子:背景色變換耕餐。基本方法是添加一個(gè)transition屬性:

<div id="bg-change"></div>
<style>
#bg-change {
 width: 100px;
 height: 100px;
 background: red;
 transition: background 0.4s;
}

#bg-change:hover {
 background: blue;
}
</style>

在這個(gè)例子中辟狈,動(dòng)畫完全運(yùn)行在CPU中肠缔,動(dòng)畫的每個(gè)階段都會(huì)重繪。但我們可以讓動(dòng)畫運(yùn)行在GPU上。我們可以在上面添加一層桩砰,將其不透明度動(dòng)畫化拓春,而不是background-color屬性:

<div id="bg-change"></div>
<style>
#bg-change {
 width: 100px;
 height: 100px;
 background: red;
}

#bg-change::before {
 background: blue;
 opacity: 0;
 transition: opacity 0.4s;
}

#bg-change:hover::before {
 opacity: 1;
}
</style>

這個(gè)動(dòng)畫會(huì)更快、更流暢亚隅。但記住硼莽,可能會(huì)引起隱式合成和額外的內(nèi)存消耗。然而此種情況下煮纵,可以極大減少內(nèi)存消耗懂鸵。

減少合成層的大小

看下面兩張圖,看到區(qū)別了嗎行疏?
<iframe src="https://sergeche.github.io/gpu-article-assets/examples/layer-size.html" height="130" frameborder="no" allowtransparency="true" style="width: 100%;"></iframe>
這兩個(gè)合成層從視覺上來看是一樣的匆光,但第一個(gè)有40,000字節(jié)(30KB)酿联,第二個(gè)僅僅400字節(jié)——小了100倍终息。為什么?看下代碼:

<div id="a"></div>
<div id="b"></div>

<style>
#a, #b {
 will-change: transform;
}

#a {
 width: 100px;
 height: 100px;
}

#b {
 width: 10px;
 height: 10px;
 transform: scale(10);
}
</style>

差別在于物理尺寸贞让,#a為100×100像素(100×100×4=40000bytes)周崭,而#b僅為10×10像素(10×10×4=400bytes),但通過transform: scale(10)縮放到100×100像素喳张。因?yàn)?code>#b是一個(gè)復(fù)合層续镇,由于will-change屬性,transform在最終的圖像繪制過程中销部,將完全在GPU中進(jìn)行摸航。

手法很簡(jiǎn)單:通過widthheight屬性減少合成層的物理大小,之后通過transform: scale(…)放大紋理舅桩。當(dāng)然酱虎,這種把戲只能減少非常簡(jiǎn)單的、純色層的內(nèi)存消耗擂涛。但是逢净,舉個(gè)例子,如果你想為一個(gè)大的照片創(chuàng)建動(dòng)畫歼指,可以減少5%到10%的大小,之后放大甥雕;用戶可能看不出任何差別踩身,而你可以節(jié)省好幾兆寶貴的內(nèi)存。

盡可能地使用CSS 變換和動(dòng)畫

我們知道社露,通過CSS transform和animation的transformopacity動(dòng)畫會(huì)自動(dòng)創(chuàng)建合成層挟阻,并且運(yùn)行在GPU上。我們也可以通過JavaScript實(shí)現(xiàn)動(dòng)畫,但為了元素獲取自己的合成層附鸽,必選先添加transform: translateZ(0)will-change: transform, opacity脱拼。
JavaScript動(dòng)畫的每一步是在requestAnimationFrame回調(diào)函數(shù)中手動(dòng)計(jì)算的。通過Element.animate()實(shí)現(xiàn)的動(dòng)畫是聲明式CSS動(dòng)畫的變體坷备。

一方面熄浓,通過CSS transition和animation創(chuàng)建簡(jiǎn)單可復(fù)用的動(dòng)畫非常容易;另一方面省撑,創(chuàng)建包含漂亮軌跡的復(fù)雜動(dòng)畫時(shí)赌蔑,JavaScript動(dòng)畫又比CSS動(dòng)畫容易實(shí)現(xiàn)。另外竟秫,JavaScript是和用戶輸入交互的唯一方式娃惯。

哪一個(gè)更好?我們可以只用一個(gè)通用的JavaScript動(dòng)畫庫來實(shí)現(xiàn)所有動(dòng)畫嗎肥败?

基于CSS的動(dòng)畫有個(gè)很重要的特性:完全在GPU上運(yùn)行趾浅。因?yàn)槟?strong>聲明了動(dòng)畫如何開始和結(jié)束,瀏覽器可以趕在動(dòng)畫開始之前馒稍,準(zhǔn)備好所需要的所有指令皿哨,之后發(fā)送給GPU。在必須使用JavaScript的情況下筷黔,瀏覽器所知的只有當(dāng)前幀的狀態(tài)往史。對(duì)一個(gè)流暢動(dòng)畫而言,我們必須以每秒60次的速度在瀏覽器主線程中計(jì)算好新幀佛舱,然后發(fā)送給GPU椎例。這些計(jì)算和數(shù)據(jù)發(fā)送不僅比CSS動(dòng)畫慢,同時(shí)也依賴于主線程的工作負(fù)載:
<iframe src="https://sergeche.github.io/gpu-article-assets/examples/js-vs-css.html" height="180" frameborder="no" allowtransparency="true" style="width: 100%;"></iframe>

在上面的例子當(dāng)中请祖,當(dāng)主線程被繁重的JavaScript任務(wù)阻塞的時(shí)候订歪,你會(huì)看到發(fā)生了什么。CSS動(dòng)畫不受影響肆捕,因?yàn)樾聨窃讵?dú)立的線程上計(jì)算的刷晋,而JavaScript動(dòng)畫必須等到繁重的計(jì)算完成,之后才計(jì)算新幀慎陵。

因此眼虱,試著盡可能使用基于CSS的動(dòng)畫,尤其是加載和進(jìn)度指示條席纽。不僅更快捏悬,而且還不會(huì)被大量的JavaScript計(jì)算阻塞。

現(xiàn)實(shí)世界中優(yōu)化的例子

本篇文章是我在為 Chaos Fighters開發(fā)頁面時(shí)的研究和實(shí)驗(yàn)結(jié)果润梯。這是個(gè)響應(yīng)式的手機(jī)游戲促銷頁面过牙,有大量的動(dòng)畫甥厦。當(dāng)開始開發(fā)的時(shí)候,我唯一所知的就是如何實(shí)現(xiàn)基于GPU的動(dòng)畫寇钉,但我并不知其工作原理刀疙。因此,在最初的里程碑頁扫倡,就造成了iPhone5——當(dāng)時(shí)最新的Apple手機(jī)——在頁面加載完幾秒鐘后崩潰了∏恚現(xiàn)在,這個(gè)頁面運(yùn)行良好镊辕,即使是在性能稍弱的設(shè)備上油够。

按照我的觀點(diǎn),讓我們考慮下這個(gè)網(wǎng)站中最有趣的優(yōu)化部分征懈。

頁面的最頂端是游戲的介紹石咬,有個(gè)像太陽光線東西在背景上旋轉(zhuǎn)。這是個(gè)無線循環(huán)卖哎、非交互的旋轉(zhuǎn)盤——正適合用簡(jiǎn)單的CSS動(dòng)畫實(shí)現(xiàn)鬼悠。首先想到的方案(錯(cuò)誤嘗試)是保存太陽光線的圖片,將它放在img中亏娜,之后使用無限CSS動(dòng)畫:
<iframe width="350" height="402" scrolling="no" src="https://codepen.io/sergeche/embed/gwBjqG/?height=402&theme-id=light&default-tab=result&embed-version=2" frameborder="no" allowtransparency="true" allowfullscreen="true"></iframe>

似乎如預(yù)期的那樣萬事大吉焕窝。但是太陽的圖片相當(dāng)大。移動(dòng)用戶可能不開心了维贺。

再仔細(xì)看下圖片它掂。只是從圖片中心發(fā)出來幾道光線而已。光線是一樣的溯泣,因此我們可以保存單個(gè)光線虐秋,復(fù)用它來實(shí)現(xiàn)最終的圖片。最后垃沦,我們僅用了一個(gè)單光線的圖片客给,相比剛開始的圖片,大小少了一個(gè)數(shù)量級(jí)肢簿。

對(duì)于這種優(yōu)化靶剑,我們的標(biāo)記語言就必須復(fù)雜一點(diǎn)了:.sun是光線圖片元素的容器。每條光線在特定的角度旋轉(zhuǎn)池充。
<iframe width="350" height="402" scrolling="no" src="https://codepen.io/sergeche/embed/qaJraq/?height=402&theme-id=light&default-tab=css&embed-version=2" frameborder="no" allowtransparency="true" allowfullscreen="true"></iframe>

視覺效果是一樣的桩引,但是網(wǎng)絡(luò)傳輸?shù)臄?shù)據(jù)量會(huì)少得多。另外收夸,合成層的大小保持不變:500 × 500 × 4 ≈ 977 KB坑匠。

為了保證簡(jiǎn)單,例子中太陽光線的大小是相當(dāng)小的咱圆,只有500 × 500像素笛辟。在實(shí)際網(wǎng)站中,服務(wù)于不同尺寸的設(shè)備(手機(jī)序苏、平板和桌面電腦)和不同分辨率手幢,最終圖片的大小大約000 × 3000 × 4 = 36 MB!而這僅僅是頁面中的一個(gè)動(dòng)畫元素忱详。

在“Layers”面板中再看下頁面的元素围来。通過旋轉(zhuǎn)整個(gè)太陽容器,使得動(dòng)畫實(shí)現(xiàn)更容易匈睁。因此监透,這個(gè)容器被提升到一個(gè)合成層,被繪制進(jìn)一個(gè)大的紋理圖像中航唆,之后發(fā)送給GPU胀蛮。但是由于我們的簡(jiǎn)化,現(xiàn)在紋理中包含無用的數(shù)據(jù):光線之間的間隔糯钙。

此外粪狼,無用的數(shù)據(jù)比有用的數(shù)據(jù)大很多!這不是利用有限內(nèi)存資源的最佳方式任岸。

解決辦法和我們優(yōu)化網(wǎng)絡(luò)傳輸時(shí)一樣:只發(fā)送有用的數(shù)據(jù)(即光線)給GPU再榄。我們可以計(jì)算下節(jié)約多少內(nèi)存:

  • 整個(gè)太陽容器:500 × 500 × 4 ≈ 977 KB
  • 12個(gè)太陽光線:250 × 40 × 4 × 12 ≈ 469 KB

內(nèi)存消耗可以減少一倍,為實(shí)現(xiàn)這一方案享潜,我們必須為每個(gè)光線單獨(dú)實(shí)現(xiàn)動(dòng)畫困鸥,而不是整個(gè)容器。因此剑按,只有光線圖像會(huì)被發(fā)送到GPU當(dāng)中疾就;它們之間的間隔不會(huì)占用任何資源。

為了實(shí)現(xiàn)獨(dú)立的光線動(dòng)畫吕座,標(biāo)記語言已經(jīng)有點(diǎn)復(fù)雜了虐译,此處的CSS更是一個(gè)障礙。我們已經(jīng)為光線的初始旋轉(zhuǎn)使用了transform吴趴,必須從同樣的角度啟動(dòng)動(dòng)畫漆诽,然后旋轉(zhuǎn)360度÷嘀Γ基本上厢拭,我們得為每個(gè)光線分別實(shí)現(xiàn)一個(gè)@keyframes,這是個(gè)不小的網(wǎng)絡(luò)傳輸撇叁。

光線的初始放置和微調(diào)動(dòng)畫供鸠,光線數(shù)量等,寫個(gè)簡(jiǎn)短的JavaScript來處理這些問題會(huì)更容易陨闹。

<iframe width="350" height="402" scrolling="no" src="https://codepen.io/sergeche/embed/bwmxoz/?height=402&theme-id=light&default-tab=js&embed-version=2" frameborder="no" allowtransparency="true" allowfullscreen="true"></iframe>

新的動(dòng)畫看起來和前一個(gè)一樣楞捂,但是內(nèi)存消耗只有一半薄坏。

還沒結(jié)束。從布局合成的角度來說寨闹,這個(gè)太陽動(dòng)畫不是主元素胶坠,而是一個(gè)背景元素。并且光線沒有鮮明的對(duì)比元素繁堡。這意味著我們可以發(fā)送一個(gè)低分辨率的光線紋理給GPU沈善,之后放大它,這可以節(jié)省一部分內(nèi)存椭蹄。
我們?cè)囍鴾p少10%的紋理大小闻牡。光線的物理尺寸為50 × 0.9 × 40 × 0.9 = 225 × 36 像素。為了使它看起來和250 × 20一樣绳矩,我們需要放大250 ÷ 225 ≈ 1.111倍罩润。

我們會(huì)在代碼中加一行:給.sun-ray加上background-size: cover——這樣背景圖就會(huì)自動(dòng)調(diào)整到元素的大小,并且為光線的動(dòng)畫添加transform: scale(1.111)埋酬。
<iframe width="350" height="402" scrolling="no" src="https://codepen.io/sergeche/embed/YGJOva/?height=402&theme-id=light&default-tab=js&embed-version=2" frameborder="no" allowtransparency="true" allowfullscreen="true"></iframe>

注意哨啃,我們只改變了元素的大小写妥;PNG圖片的大小仍然一樣拳球。由DOM元素創(chuàng)建的矩形被渲染成紋理供GPU使用,而不是PNG圖片珍特。

在GPU中祝峻,太陽光線的新合成大小現(xiàn)在為225 × 36 × 4 × 12 ≈ 380 KB(原來是469KB)。我們已經(jīng)減少了19%的內(nèi)存消耗扎筒,并且實(shí)現(xiàn)了非常靈活的代碼莱找,可以通過縮放來實(shí)現(xiàn)最優(yōu)的質(zhì)量?jī)?nèi)存比。因此嗜桌,通過提高動(dòng)畫(起先看起來很簡(jiǎn)單)的復(fù)雜度奥溺,我們減少了內(nèi)存使用量977 ÷ 380 ≈ 2.5 倍!

我想你已經(jīng)發(fā)現(xiàn)了這個(gè)方法的缺陷:動(dòng)畫現(xiàn)在工作在CPU上骨宠,可能被大量的JavaScript計(jì)算阻塞浮定。如果你想更熟悉優(yōu)化GPU動(dòng)畫,我留個(gè)小小的家庭作業(yè)层亿。ForkCodepen of the sun rays桦卒,然后將太陽光線完全轉(zhuǎn)移到GPU上運(yùn)行,然而還要和初始的例子一樣節(jié)省內(nèi)存和靈活匿又。將你的例子提交到注釋中方灾,我會(huì)回復(fù)你的。

獲得的教訓(xùn)

優(yōu)化Chaos Fighters頁面的研究使我完全重新思考開發(fā)現(xiàn)代web頁面的過程碌更。以下是我的主要原則:

  • 一定要和客戶端和設(shè)計(jì)者溝通網(wǎng)站上所有的動(dòng)畫和效果裕偿。這可能極大地影響頁面的標(biāo)記語言洞慎,并且也有利于更好地合成。
  • 從一開始就要注意合成層的大小和數(shù)量——尤其是隱式合成層嘿棘。瀏覽器開發(fā)者工具中的“Layers”面板是你最好的朋友拢蛋。
  • 現(xiàn)代瀏覽器大量運(yùn)用合成,不僅僅是動(dòng)畫蔫巩,還有優(yōu)化頁面元素的繪制。比如快压,position: fixediframe圆仔、video元素也使用合成。
  • 合成層的大小可能比數(shù)量更重要蔫劣。在某些情況下坪郭,瀏覽器會(huì)試圖減少合成層的數(shù)量(參見“GPU Accelerated Compositing in Chrome”中的“Layer Squashing”一節(jié));這會(huì)阻止所謂的“層爆炸”和減少內(nèi)存消耗脉幢,尤其是當(dāng)層有大量的交集時(shí)歪沃。但有時(shí),這種優(yōu)化有副作用嫌松,比如當(dāng)一個(gè)很大的紋理消耗的內(nèi)存比多個(gè)小層多時(shí)沪曙。為了避免這種優(yōu)化,我給每個(gè)元素加了個(gè)小的萎羔、唯一的translateZ()值液走,比如translateZ(0.0001px)translateZ(0.0002px)等贾陷。瀏覽器會(huì)認(rèn)為處在3D空間的不同層缘眶,從而跳過優(yōu)化。
  • 為了從視覺上提高動(dòng)畫的性能或者避免視覺假象髓废,你不能只是簡(jiǎn)單地給任何元素添加transform: translateZ(0)或者will-change: transform巷懈。GPU合成有許多缺點(diǎn)和權(quán)衡需要考慮。使用不當(dāng)時(shí)慌洪,可能會(huì)降低整體的性能顶燕,甚至導(dǎo)致瀏覽器崩潰。

請(qǐng)?jiān)试S我再提醒下免責(zé)聲明:關(guān)于GPU合成蒋譬,沒有任何官方規(guī)范割岛,每個(gè)瀏覽器廠商解決同一個(gè)問題的方案不盡相同。本篇文章中的某些部分幾個(gè)月后可能就過時(shí)了犯助。比如癣漆,Google Chrome 開發(fā)者正在想方法減少CPU和GPU之間數(shù)據(jù)傳輸?shù)拈_銷,包括使用特殊的共享內(nèi)存剂买,這樣就沒有開銷了惠爽。另外癌蓖,Safari已經(jīng)能夠?qū)⒑?jiǎn)單元素的繪制(比如具有background-color的空DOM元素)代理到GPU,而不是在CPU上為其創(chuàng)建圖像婚肆。

無論如何租副,我希望本篇文章已經(jīng)幫助你更好地理解瀏覽器采用GPU渲染的原理,從而幫你創(chuàng)建在各種設(shè)備上都能快速運(yùn)行的令人難忘的網(wǎng)站较性。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末用僧,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子赞咙,更是在濱河造成了極大的恐慌责循,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,682評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件攀操,死亡現(xiàn)場(chǎng)離奇詭異院仿,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)速和,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門歹垫,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人颠放,你說我怎么就攤上這事排惨。” “怎么了碰凶?”我有些...
    開封第一講書人閱讀 165,083評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵若贮,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我痒留,道長(zhǎng)谴麦,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,763評(píng)論 1 295
  • 正文 為了忘掉前任伸头,我火速辦了婚禮匾效,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘恤磷。我一直安慰自己面哼,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,785評(píng)論 6 392
  • 文/花漫 我一把揭開白布扫步。 她就那樣靜靜地躺著魔策,像睡著了一般。 火紅的嫁衣襯著肌膚如雪河胎。 梳的紋絲不亂的頭發(fā)上闯袒,一...
    開封第一講書人閱讀 51,624評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼政敢。 笑死其徙,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的喷户。 我是一名探鬼主播唾那,決...
    沈念sama閱讀 40,358評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼褪尝!你這毒婦竟也來了闹获?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,261評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤河哑,失蹤者是張志新(化名)和其女友劉穎昌罩,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體灾馒,經(jīng)...
    沈念sama閱讀 45,722評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年遣总,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了睬罗。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,030評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡旭斥,死狀恐怖容达,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情垂券,我是刑警寧澤花盐,帶...
    沈念sama閱讀 35,737評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站菇爪,受9級(jí)特大地震影響算芯,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜凳宙,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,360評(píng)論 3 330
  • 文/蒙蒙 一熙揍、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧氏涩,春花似錦届囚、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至饺汹,卻和暖如春蛔添,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評(píng)論 1 270
  • 我被黑心中介騙來泰國(guó)打工作郭, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留陨囊,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,237評(píng)論 3 371
  • 正文 我出身青樓夹攒,卻偏偏與公主長(zhǎng)得像蜘醋,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子咏尝,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,976評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • 譯者序:原文GPU Animation: Doing It Right压语,發(fā)表于2016年12月6日,本文是對(duì)該篇的...
    smilewalker閱讀 1,607評(píng)論 0 8
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫编检、插件胎食、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 12,105評(píng)論 4 62
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,162評(píng)論 25 707
  • 我的茉莉清茶
    晴天兔子閱讀 216評(píng)論 0 0
  • 有一種神奇的生物叫做“同學(xué)”,只要你們?cè)谕粋€(gè)教室上過一節(jié)課允懂,他就會(huì)是你的同學(xué)厕怜。 他會(huì)出現(xiàn)在你的QQ里,微信里蕾总,微...
    zzz宇閱讀 498評(píng)論 0 0