前言
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)監控函數執行期花費的時間。講道理不如舉例子,為了更清楚地了解它的功能概況,我們可以編寫一個測試列子來觀察它們的作用。這個列子簡單一些,使得我們分析的數據更清晰一些。
<!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根。用代碼表示是這樣的:
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)就是該對象依賴的其他對象所占用的內存。你要明白這些都是官方的解釋,所以即使你覺得雲里霧里也是正常的,官方解釋肯定是官腔嘛。按照鹵煮自己的理解是這樣的:
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中也存在着引用關系:我們通過代碼來看下這種引用關系:
<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這一列的意義比較好理解,從字面上我們就知道了其意義。就是對象實例化的數量。用代碼表示就是這樣的:
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。敬請關注。