前言
Profiles皮膚功能的作用主要是監控網頁中各種方法執行時間和記憶體的變化,簡單來說它就是Timeline的數字化版本。它的功能選項卡不是很多(只有三個),操作起來比較前面的幾塊功能版本來說簡單,但是裡面的資料確很多,很雜,要弄懂它們需要花費一些時間。尤其是在記憶體快照中的各種龐雜的資料。在這篇部落格中滷煮將繼續給大家分享Chrome開發者工具的使用經驗。如果你遇到不懂的地方或者有不對的地方,可以在評論中回覆滷煮,文章最後滷煮會最後把祕籍交出來。下面要介紹的是Profiles。首先開啟Profiles皮膚。
Profiles介面分為左右兩個區域,左邊區域是放檔案的區域,右邊是展示資料的區域。在開始檢測之前可以看到右邊區域有三個選項,它們分別代表者不同的功能:
1.(Collect JavaScript CPU Profile)監控函式執行期花費的時間
2.(Take Heap Snapshot)為當前介面拍一個記憶體快照
3.(Record Heap Allocations)實時監控記錄記憶體變化(物件分配跟蹤)
一、Collect JavaScript CPU Profile(函式收集器)
首先來關注第一個功能,(Collect JavaScript CPU Profile)監控函式執行期花費的時間。講道理不如舉例子,為了更清楚地瞭解它的功能概況,我們可以編寫一個測試列子來觀察它們的作用。這個列子簡單一些,使得我們分析的資料更清晰一些。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
<!DOCTYPE html> <html> <head> <title></title> </head> <body> <button id="btn"> click me</button> <script type="text/javascript"> function a() { console.log('hello world'); } function b() { a(); } function c() { b(); } document.getElementById('btn').addEventListener('click', c, true); </script> </body> </html> |
在右邊區域中選擇Collect JavaScript CPU Profile 選項,點選下方的Start按鈕(也可以點選左邊的黑色圓圈),這時候Chrome會開始記錄網頁的方法執行,然後我們點選介面的按鈕來執行函式。最後再點選右邊區域的Stop按鈕(或者左邊的紅色圓圈),這時監控就結束了。左邊Profiles會列出一個檔案,單擊可以看到如下介面:
生存了一個資料表格,它們的意義在上圖中已經標記出來了。它記錄的是函式執行的時間以及函式執行的順序。通過右邊區域的型別選項可以切換資料顯示的方式。有正包含關係,逆包含關係,圖表型別三種選項。我們可以選擇其中的圖表型別:
可以看到這個皮膚似曾相識,沒錯,它跟之前的TimeLine皮膚很像,的確,雖然很像,但功能不一樣,不然也就沒必要重複做了。從上圖可以看到點選按鈕執行的各個函式執行的時間,順序,包含關係和CUP變化等。你可以在生成檔案之後在左邊區域中儲存該檔案記錄,下次只需要在區域2這中點選load按鈕便可以載入出來。也就是說你可以本地永久地記錄該段時間內的方法執行時間。第一個功能大概就這麼多,比較其他兩個來說簡單。
二、Take Heap Snapshot(記憶體快照)
下面我們來介紹一下第二個功能的用法。第二個功能是給當前網頁拍一個記憶體快照.選擇第二個拍照功能,按下 Take Snapshot 按鈕,給當前的網頁拍下一個記憶體快照,得到如下圖。
可以看到左邊區域生成個檔案,檔名下方有數字,表示這個張快照記錄到的記憶體大小(此時為3.2M)。右邊區域是個列表,它分為五列,表頭可以按照數值大小手動排序。在這張表格中列出的一些列數字和標識,以及表頭的意義比較複雜,涉及到一些js和記憶體的知識,我們就先從這些表頭開始瞭解他們。從左到右的順序它們分別表示:
Constructor(建構函式)表示所有通過該建構函式生成的物件
Distance 物件到達GC根的最短距離
Objects Count 物件的例項數
Shallow size 對應建構函式生成的物件的shallow sizes(直接佔用記憶體)總數
Retained size 展示了對應物件所佔用的最大記憶體
CG根!是神馬東西?在google的官方文件中的建議是CG根不必用到開發者去關心。但是我們在這裡可以簡單說明一下。大家都知道js物件可以互相引用,在某個物件申請了一塊記憶體後,它很可能會被其他物件應用,而其他物件又被另外的物件應用,一層一層,但它們的指標都是指向同一塊記憶體的,我們把這最初引用的那塊記憶體就可以成為GC根。用程式碼表示是這樣的:
1 2 3 4 5 |
var obj = {a:1}; obj.pro = { a : 100 }; obj.pro.pro = { b : 200 }; var two = obj.pro.pro; //這種情況下 {b:200} 就是被two引用到了,{b:200}物件引用的記憶體就是CG根 |
用一張官方的圖可以如下表示:
構成這張關係網的元素有兩種:
Nodes:節點,對應一個物件,用建立該物件的構造方法來命名
Edges:連線線,對應著物件間的引用關係,用物件屬性名來命名
從上圖你也可以看到了第二列的表頭Dishtance的意義是什麼,沒錯,它指的就是CG根和引用物件之間的距離。根據這條解釋,圖中的物件5到CG根的距離就是2!那麼什麼是直接佔用記憶體(Shallow size)和最大佔用記憶體(Retained size)呢?直接佔用記憶體指的是物件本身佔用的記憶體,因為物件在記憶體中會通過兩種方式存在著,一種是被一個別的物件保留(我們可以說這個物件依賴別的物件)或者被Dom物件這樣的原生物件隱含保留。在這裡直接佔有記憶體指的就是前一種。(通常來講,陣列和字串會保留更多的直接佔有記憶體)。而最大記憶體(Retained size)就是該物件依賴的其他物件所佔用的記憶體。你要明白這些都是官方的解釋,所以即使你覺得雲裡霧裡也是正常的,官方解釋肯定是官腔嘛。按照滷煮自己的理解是這樣的:
1 2 3 4 5 6 7 8 9 10 11 |
function a() { var obj = [1,2,.......n]; return function() { //js作用域的原因,在此閉包執行的上下文中可以訪問到obj這個物件 console.log(obj); } } //正常情況下,a函式執行完畢 obj佔用的記憶體會被回收,但是此處a函式返回了一個函式表示式(見Tom大叔的部落格函式表示式和函式宣告),其中obj因為js的作用域的特殊性一直存在,所以我們可以說b引用了obj。 var b = a(); //每次執行b函式的時候都可以訪問到obj,說明記憶體未被回收 所以對於obj來說直接佔用記憶體[1,2,....n], 而b依賴obj,所obj是b的最大記憶體。 b() |
在dom中也存在著引用關係:我們通過程式碼來看下這種引用關係:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<html> <body> <div id="refA"> <ul> <li><a></a></li> <li><a></a></li> <li><a id="#refB"></a></li> </ul> </div> <div></div> <div></div> </body> </html> <script> var refA = document.getElementById('refA'); var refB = document.getElementById('refB');//refB引用了refA。它們之間是dom樹父節點和子節點的關係。 </script> |
現在,問題來了,如果我現在在dom中移除div#refA會怎麼樣呢?答案是dom記憶體依然存在,因為它被js引用。那麼我把refA變數置為null呢?答案是記憶體依然存在了。因為refB對refA存在引用,所以除非在把refB釋放,否則dom節點記憶體會一直存在瀏覽器中無法被回收掉。上圖:
所以你看到Constructor這一列中物件如果有紅色背景就表示有可能被JavaScript引用到但是沒有被回收。以上只是滷煮個人理解,如果不對頭,請你一定要提醒滷煮好即時更新,免得誤人子弟!接著上文,Objects Count這一列是什麼意思呢?Objects Count這一列的意義比較好理解,從字面上我們就知道了其意義。就是物件例項化的數量。用程式碼表示就是這樣的:
1 2 3 4 5 |
var ConstructorFunction = function() {};//建構函式 var a = new ConstructorFunction();//第一個例項 var b = new ConstructorFunction();//第二個例項 ....... var n = new ConstructorFunction();//第n個例項 |
可以看到建構函式在上面有n個例項,那麼對應在Objects Count這列裡面就會有數字n。在這裡,ConstructorFunction是我們自己定義的建構函式。那麼這些建構函式在哪裡呢,聰明的你一定可以猜到就在第一列Constructor中。實際上你可以看到列表中的Constructor這一列,其中多數都是系統級別的建構函式,有一部分也是我們自己編寫的:
global property – 全域性物件(像 ‘window’)和引用它的物件之間的中間物件。如果一個物件由建構函式Person生成並被全域性物件引用,那麼引用路徑就是這樣的:[global] > (global property > Person。這跟一般的直接引用彼此的物件不一樣。我們用中間物件是有效能方面的原因,全域性物件改變會很頻繁,非全域性變數的屬性訪問優化對全域性變數來說並不適用。
roots – constructor中roots的內容引用它所選中的物件。它們也可以是由引擎自主建立的一些引用。這個引擎有用於引用物件的快取,但是這些引用不會阻止引用物件被回收,所以它們不是真正的強引用(FIXME)。
closure – 一些函式閉包中的一組物件的引用
array, string, number, regexp – 一組屬性引用了Array,String,Number或正規表示式的物件型別
compiled code – 簡單來說,所有東西都與compoled code有關。Script像一個函式,但其實對應了<script>的內容。SharedFunctionInfos (SFI)是函式和compiled code之間的物件。函式通常有內容,而SFIS沒有(FIXME)。
HTMLDivElement, HTMLAnchorElement, DocumentFragment 等 – 你程式碼中對elements或document物件的引用。
點選展開它們檢視詳細項,@符號表示該物件ID。:
一個快照可以有多個試圖,在左邊區域的右上角我們可以看到點選下拉選單可以得到四個個任務檢視選項:
他們分別代表:
Summary(概要) – 通過建構函式名分類顯示物件;
Comparison(對照) – 顯示兩個快照間物件的差異;
Containment(控制) – 探測堆內容;
Statistic(圖形表)-用圖表的方式瀏覽記憶體使用概要
Comparison是指對比快照之間的差異,你可以首先拍一個快照A,操作網頁一段時間後拍下另外一個快照B,然後在B快照的右邊距區域的左上角選擇該選項。然後就可以看到對比圖。上面顯示的是每個列,每一項的變化。在對照檢視下,兩個快照之間的不同就會展現出來了。當展開一個總類目後,增加和刪除了的物件就顯示出來了:
嘗試一下官方示例幫助你瞭解對比的功能。
你也可以嘗試著檢視Statistic選項,它會以圖表的方式描述記憶體概況。
三、Record Heap Allocations.(物件跟蹤器)
好了,第二個功能也介紹完了,最後讓我們來瞧瞧最後一個功能Record Heap Allocations.這個功能是幹啥的呢。它的作用是為為我們拍下一系列的快照(頻率為50ms),為我們檢測在啟用它的時候每個物件的生存情況。形象一點說就是假如拍攝記憶體快照的功能是照相那麼它功能相當於錄影。當我們啟用start按鈕的時候它便開始錄影,直到結束。你會看到左側區域上半部分有一些藍色和灰色的柱條。灰色的表示你監控這段時間內活躍過的物件,但是被回收掉了。藍色的表示依舊沒有沒回收。你依舊可以滑動滾輪縮放時間軸。
物件跟蹤器功能的好處在於你可以連續不斷的跟蹤物件,在結束時,你可以選擇某個時間段內(比如說藍色條沒有變灰)檢視期間活躍的物件。幫助你定位記憶體洩露問題。
四、結束
好了,差不多把Profiles講完了。這東西對我們查詢記憶體洩露來說還是蠻有作用的。對於工具來說,主要是多用,熟能生巧嘛。如果你覺得不過癮,我推薦你去閱讀官方文件,裡面有N多的例子,N多的說明,非常詳細。前提是你能跳到牆外去。當然也有翻譯文件(滷煮的祕籍都給你了,推薦一下吧)。最後真的是要像一片文章裡面寫的一樣“感謝發明計算機的人,讓我們這些剪刀加漿糊的學術土匪變成了複製加貼上版的學術海盜。”下期是Console和Audits。敬請關注。