上篇博客我們寫到了 Java/Android 內(nèi)存的分配以及相關(guān) GC 的詳細(xì)分析,這篇博客我們會繼續(xù)分析 Android 中內(nèi)存泄漏的檢測以及相關(guān)案例,和 Android 的內(nèi)存優(yōu)化相關(guān)內(nèi)容。
上篇:Android 性能優(yōu)化之內(nèi)存泄漏檢測以及內(nèi)存優(yōu)化(上)。
中篇:Android 性能優(yōu)化之內(nèi)存泄漏檢測以及內(nèi)存優(yōu)化(中)。
下篇:Android 性能優(yōu)化之內(nèi)存泄漏檢測以及內(nèi)存優(yōu)化(下)。
轉(zhuǎn)載請注明出處:http://blog.csdn.net/self_study/article/details/66969064
對技術(shù)感興趣的同鞋加群544645972一起交流。
Android 內(nèi)存泄漏檢測
?通過上篇博客我們了解了 Android JVM/ART 內(nèi)存的相關(guān)知識和泄漏的原因,再來歸類一下內(nèi)存泄漏的源頭,這里我們簡單將其歸為一下三類:<ul><li>自身編碼引起</li>由項目開發(fā)人員自身的編碼造成;<li>第三方代碼引起</li>這里的第三方代碼包含兩類,第三方非開源的 SDK 和開源的第三方框架;<li>系統(tǒng)原因</li>由 Android 系統(tǒng)自身造成的泄漏,如像 WebView、InputMethodManager 等引起的問題,還有某些第三方 ROM 存在的問題。</ul>
Android 內(nèi)存泄漏的定位,檢測與修復(fù)
?內(nèi)存泄漏不像閃退的 BUG,排查起來相對要困難一些,比較極端的情況是當(dāng)你的應(yīng)用 OOM 才發(fā)現(xiàn)存在內(nèi)存泄漏問題,到了這種情況才去排查處理問題的話,對用戶的影響就太大了,為此我們應(yīng)該在編碼階段盡早地發(fā)現(xiàn)問題,而不是拖到上線之后去影響用戶體驗,下面總結(jié)一下常用內(nèi)存泄漏的定位和檢測工具:
Lint
Lint 是 Android studio 自帶的靜態(tài)代碼分析工具,使用起來也很方便,選中需要掃描的 module,然后點擊頂部菜單欄 Analyze -> Inspect Code ,選擇需要掃描的地方即可:
?
最后在 Performance 里面有一項是 Handler reference leaks,里面列出來了可能由于內(nèi)部 Handler 對象持有外部 Activity 引用導(dǎo)致內(nèi)存泄漏的地方,這些地方都可以根據(jù)實際的使用場景去排查一下,因為畢竟不是每個內(nèi)部 Handler 對象都會導(dǎo)致內(nèi)存泄漏。Lint 還可以自定義掃描規(guī)則,使用姿勢很多很強大,感興趣的可以去了解一下,除了 Lint 之外,還有像 FindBugs、Checkstyle 等靜態(tài)代碼分析工具也是很不錯的。
StrictMode
StrictMode 是 Android 系統(tǒng)提供的 API,在開發(fā)環(huán)境下引入可以更早的暴露發(fā)現(xiàn)問題給開發(fā)者,于開發(fā)階段解決它,StrictMode 最常被使用來檢測在主線程中進行讀寫磁盤或者網(wǎng)絡(luò)操作等耗時任務(wù),把這些耗時任務(wù)放置于主線程會造成主線程阻塞卡頓甚至可能出現(xiàn) ANR ,官方例子:
public void onCreate() {
if (DEVELOPER_MODE) {
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
.detectDiskReads()
.detectDiskWrites()
.detectNetwork() // or .detectAll() for all detectable problems
.penaltyLog()
.build());
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
.detectLeakedSqlLiteObjects()
.detectLeakedClosableObjects()
.penaltyLog()
.penaltyDeath()
.build());
}
super.onCreate();
}
把上面這段代碼放在早期初始化的 Application、Activity 或者其他應(yīng)用組件的 onCreate 函數(shù)里面來啟用 StrictMode 功能,一般 StrictMode 只是在測試環(huán)境下啟用,到了線上環(huán)境就不要開啟這個功能。啟用 StrictMode 之后,在 logcat 過濾日志的地方加上 StrictMode 的過濾 tag,如果發(fā)現(xiàn)一堆紅色告警的 log,說明可能就出現(xiàn)了內(nèi)存泄漏或者其他的相關(guān)問題了:
比如上面這個就是因為調(diào)用 registerReceiver 之后忘記調(diào)用 unRegisterReceiver 導(dǎo)致的 activity 泄漏,根據(jù)錯誤信息便可以定位和修復(fù)問題。
LeakCanary
? LeakCanary 是一個 Android 內(nèi)存泄漏檢測的神器,正確使用可以大大減少內(nèi)存泄漏和 OOM 問題,地址:
https://github.com/square/leakcanary
集成 LeakCanary 也很簡單,在 build.gradle 文件中加入:
dependencies {
debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5'
releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'
testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'
}
然后在 Application 類中添加下面代碼:
public class ExampleApplication extends Application {
@Override public void onCreate() {
super.onCreate();
if (LeakCanary.isInAnalyzerProcess(this)) {
// This process is dedicated to LeakCanary for heap analysis.
// You should not init your app in this process.
return;
}
LeakCanary.install(this);
// Normal app init code...
}
}
上面兩步做完之后就算是集成了 LeakCanary 了,非常簡單方便,如果程序出現(xiàn)了內(nèi)存泄漏會彈出 notification,點擊這個 notification 就會進入到下面這個界面,或者集成 LeakCanary 之后在桌面會有一個 LeakCanary 的圖標(biāo),點擊進去是所有的內(nèi)存泄漏列表,點擊其中一項同樣是進入到下面界面:
這個界面就會詳細(xì)展示引用持有鏈,一目了然,對于問題的解決方便了很多,堪稱神器,更多實用姿勢可以看看 LeakCanary FAQ。
?還有一點需要提到的是,LeakCanary 在檢測內(nèi)存泄漏的時候會阻塞主界面,這是一點體驗有點不爽的地方,但是這時候阻塞肯定是必要的,因為此時必須要掛起線程來獲取當(dāng)前堆的狀態(tài)。然后也并不是每個 LeakCanary 提示的地方都有內(nèi)存泄漏,這時候可能需要借助 MAT 等工具去具體分析。不過 LeakCanary 有一點非常好的地方是因為 Android 系統(tǒng)也會有一些內(nèi)存泄漏,而 LeakCanary 對此則提供了一個 AndroidExcludedRefs 類來幫助我們排除這些問題。
Android Memory Monitor
?Memory Monitor 是 Android Studio 自帶的一個監(jiān)控內(nèi)存使用狀態(tài)的工具,入口如下所示:
在 Android Monitor 點開之后 logcat 的右側(cè)就是 Monitor 工具,其中可以檢測內(nèi)存、CPU、網(wǎng)絡(luò)等內(nèi)容,我們這里只用到了 Memory Monitor 功能,點擊紅色箭頭所指的區(qū)域,就會 dump 此時此刻的 Memory 信息,并且生成一個 .hprof 文件,dump 完成之后會自動打開這個文件的顯示界面,如果沒有打開,可以通過點擊最左側(cè)的 Capture 界面或者 Tool Window 里面的 Capture 進入 dump 的 .hprof 文件列表:
?接著我們來分析一下這個生成的 .hprof 文件所展示的信息:
首先左上角的下拉框,可以選擇 App Heap、Image Heap 和 Zygote Heap,對應(yīng)的就是上篇博客講到的 Allocation Space,Image Space 和 Zygote Space,我們這里選擇 Allocation Space,然后第二個選擇 PackageTreeView 這一項,展開之后就能看見一個樹形結(jié)構(gòu)了,然后繼續(xù)展開我們應(yīng)用包名的對應(yīng)對象,就可以很清晰的看到有多少個 Activity 對象了,上面那兩欄展示的信息按照從左到右的順序,定義如下所示:
Column | Description |
---|---|
Class Name | 占有這塊內(nèi)存的類名 |
Total Count | 未被處理的數(shù)量 |
Heap Count | 在上面選擇的指定 heap 中的數(shù)量 |
Sizeof | 這個對象的大小,如果在變化中,就顯示 0 |
Shallow Size | 在當(dāng)前這個 heap 中的所有該對象的總數(shù) |
Retained Size | 這個類的所有對象占有的總內(nèi)存大小 |
Instance | 這個類的指定對象 |
Reference Tree | 指向這個選中對象的引用,還有指向這個引用的引用 |
Depth | 從 GC Root 到該對象的引用鏈路的最短步數(shù) |
Shallow Size | 這個引用的大小 |
Dominating Size | 這個引用占有的內(nèi)存大小 |
然后可以點擊展開右側(cè)的 Analyzer Tasks 項,勾選上需要檢測的任務(wù),然后系統(tǒng)就會給你分析出結(jié)果:
從分析的結(jié)果可以看到泄漏的 Activity 有兩個,非常直觀,然后點開其中一個,觀察下面的 ReferenceTree 選項:
可以看到 Thread 對象持有了 SecondActivity 對象的引用,也就是 GC Root 持有了該 Activity 的引用,導(dǎo)致這個 Activity 無法回收,問題的根源我們就發(fā)現(xiàn)了,接下來去處理它就好了。
?關(guān)于更多 Android Memory Monitor 的使用可以去看看這個官方文檔:HPROF Viewer and Analyzer。
MAT
MAT(Memory Analyzer Tools)是一個 Eclipse 插件,它是一個快速、功能豐富的 JAVA heap 分析工具,它可以幫助我們查找內(nèi)存泄漏和減少內(nèi)存消耗,MAT 插件的下載地址:Eclipse Memory Analyzer Open Source Project,上面通過 Android studio 生成的 .hprof 文件因為格式稍有不同,所以需要經(jīng)過一個簡單的轉(zhuǎn)換,然后就可以通過 MAT 去打開了:
通過 MAT 去打開轉(zhuǎn)換之后的這個文件:
用的最多的就是 Histogram 功能,點擊 Actions 下的 Histogram 項就可以得到 Histogram 結(jié)果:
我們可以在左上角寫入一個正則表達(dá)式,然后就可以對所有的 Class Name 進行篩選了,很方便,頂欄展示的信息 "Objects" 代表該類名對象的數(shù)量,剩下的 "Shallow Heap" 和 "Retained Heap" 則和 Android Memory Monitor 類似。咱們接著點擊 SecondActivity,然后右鍵:
在彈出來的菜單中選擇 List objects->with incoming references 將該類的實例全部列出來:
通過這個列表我們可以看到 SecondActivity@0x12faa900 這個對象被一個 this$00x12c65140 的匿名內(nèi)部類對象持有,然后展開這一項,發(fā)現(xiàn)這個對象是一個 handler 對象:
快速定位找到這個對象沒有被釋放的原因,可以右鍵 Path to GC Roots->exclude all phantom/weak/soft etc. references 來顯示出這個對象到 GC Root 的引用鏈,因為強引用才會導(dǎo)致對象無法釋放,所以這里我們要排除其他三種引用:
這么處理之后的結(jié)果就很明顯了:
一個非常明顯的強引用持有鏈,GC Root 我們前面的博客中說到包含了線程,所以這里的 Thread 對象 GC Root 持有了 SecondActivity 的引用,導(dǎo)致該 Activity 無法被釋放。
?MAT 還有一個功能就是能夠?qū)Ρ葍蓚€ .hprof 文件,將兩個文件都添加到 Compare Basket 里面:
添加進去之后點擊右上角的 ! 按鈕,然后就會生成兩個文件的對比:
同樣適用正則表達(dá)式將需要的類篩選出來:
結(jié)果也很明顯,退出 Activity 之后該 Activity 對象未被回收,仍然在內(nèi)存中,或者可以調(diào)整對比選項讓對比結(jié)果更加明顯:
也可以對比兩個對象集合,方法與此類似,都是將兩個 Dump 結(jié)果中的對象集合添加到 Compare Basket 中去對比,找出差異后用 Histogram 查詢的方法找出 GC Root,定位到具體的某個對象上。
adb shell && Memory Usage
可以通過命令 adb shell dumpsys meminfo [package name]
來將指定 package name 的內(nèi)存信息打印出來,這種模式可以非常直觀地看到 Activity 未釋放導(dǎo)致的內(nèi)存泄漏:
或者也可以通過 Android studio 的 Memory Usage 功能進行查看,最后的結(jié)果是一樣的:
Allocation Tracker
Android studio 還自帶一個 Allocation Tracker 工具,功能和 DDMS 中的基本差不多,這個工具可以監(jiān)控一段時間之內(nèi)的內(nèi)存分配:
在內(nèi)存圖中點擊途中標(biāo)紅的部分,啟動追蹤,再次點擊就是停止追蹤,隨后自動生成一個 .alloc 文件,這個文件就記錄了這次追蹤到的所有數(shù)據(jù),然后會在右上角打開一個數(shù)據(jù)面板:
這個工具詳細(xì)的介紹可以看看這個博客:Android性能專項測試之Allocation Tracker(Android Studio)。
常見的內(nèi)存泄漏案例
?我們來看看常見的導(dǎo)致內(nèi)存泄漏的案例:
靜態(tài)變量造成的內(nèi)存泄漏
?由于靜態(tài)變量的生命周期和應(yīng)用一樣長,所以如果靜態(tài)變量持有 Activity 或者 Activity 中 View 對象的應(yīng)用,就會導(dǎo)致該靜態(tài)變量一直直接或者間接持有 Activity 的引用,導(dǎo)致該 Activity 無法釋放,從而引發(fā)內(nèi)存泄漏,不過需要注意的是在大多數(shù)這種情況下由于靜態(tài)變量只是持有了一個 Activity 的引用,所以導(dǎo)致的結(jié)果只是一個 Activity 對象未能在退出之后釋放,這種問題一般不會導(dǎo)致 OOM 問題,只能通過上面介紹過的幾種工具在開發(fā)中去觀察發(fā)現(xiàn)。
這種問題的解決思路很簡單,就是不讓靜態(tài)變量直接或者間接持有 Activity 的強引用,可以將其修改為 soft reference 或者 weak reference 等等之類的,或者如果可以的話將 Activity Context 更換為 Application Context,這樣就能保證生命周期一致不會導(dǎo)致內(nèi)存泄漏的問題了。
內(nèi)部類持有外部類引用
我們上面的 demo 中模擬的就是內(nèi)部類對象持有外部類對象的引用導(dǎo)致外部類對象無法釋放的問題,在 Java 中非靜態(tài)內(nèi)部類和匿名內(nèi)部類會持有他們所屬外部類對象的引用,如果這個非靜態(tài)內(nèi)部類對象或者匿名內(nèi)部類對象被一個耗時的線程(或者其他 GC Root)直接或者間接的引用,甚至這些內(nèi)部類對象本身就在做一些耗時操作,這樣就會導(dǎo)致這個內(nèi)部類對象直接或者間接無法釋放,內(nèi)部類對象無法釋放,外部類的對象也就無法釋放造成內(nèi)存泄漏,而且如果無法釋放的對象積累起來就會造成 OOM,示例代碼如下所示:
public class SecondActivity extends AppCompatActivity{
private Handler handler;
private Bitmap bitmap;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_second);
bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.pic);//decode 一個大圖來模擬內(nèi)存無法釋放導(dǎo)致的崩潰
findViewById(R.id.btn_second).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
finish();
}
});
handler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
}
};
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
handler.sendEmptyMessage(0);
}
}).start();
}
}
這個問題的解決方法可以根據(jù)實際情況進行選擇:<ul><li>將非靜態(tài)內(nèi)部類或者匿名內(nèi)部類修改為靜態(tài)內(nèi)部類,比如 Handler 修改為靜態(tài)內(nèi)部類,然后讓 Handler 持有外部 Activity 的一個 Weak Reference 或者 Soft Reference;</li><li>在 Activity 頁面銷毀的時候?qū)⒑臅r任務(wù)停止,這樣就能保證 GC Root 不會間接持有 Activity 的引用,也就不會導(dǎo)致內(nèi)存泄漏;</li></ul>
錯誤使用 Activity Context
這個很好理解,在一個錯誤的地方使用 Activity Context,造成 Activity Context 被靜態(tài)變量長時間引用導(dǎo)致無法釋放而引發(fā)的內(nèi)存泄漏,這個問題的處理方式也很簡單,如果可以的話修改為 Application Context 或者將強引用變成其他引用。
資源對象沒關(guān)閉造成的內(nèi)存泄漏
資源性對象比如(Cursor,F(xiàn)ile 文件等)往往都用了一些緩沖,我們在不使用的時候應(yīng)該及時關(guān)閉它們,以便它們的緩沖對象被及時回收,這些緩沖不僅存在于 java 虛擬機內(nèi),還存在于 java 虛擬機外,如果我們僅僅是把它的引用設(shè)置為 null 而不關(guān)閉它們,往往會造成內(nèi)存泄漏。但是有些資源性對象,比如 SQLiteCursor(在析構(gòu)函數(shù) finalize(),如果我們沒有關(guān)閉它,它自己會調(diào) close() 關(guān)閉),如果我們沒有關(guān)閉它系統(tǒng)在回收它時也會關(guān)閉它,但是這樣的效率太低了。因此對于資源性對象在不使用的時候,應(yīng)該調(diào)用它的 close() 函數(shù),將其關(guān)閉掉,然后再置為 null,在我們的程序退出時一定要確保我們的資源性對象已經(jīng)關(guān)閉。
程序中經(jīng)常會進行查詢數(shù)據(jù)庫的操作,但是經(jīng)常會有使用完畢 Cursor 后沒有關(guān)閉的情況,如果我們的查詢結(jié)果集比較小,對內(nèi)存的消耗不容易被發(fā)現(xiàn),只有在常時間大量操作的情況下才會出現(xiàn)內(nèi)存問題,這樣就會給以后的測試和問題排查帶來困難和風(fēng)險,示例代碼:
Cursor cursor = getContentResolver().query(uri...);
if (cursor.moveToNext()) {
... ...
}
更正代碼:
Cursor cursor = null;
try {
cursor = getContentResolver().query(uri...);
if (cursor != null && cursor.moveToNext()) {
... ...
}
} finally {
if (cursor != null) {
try {
cursor.close();
} catch (Exception e) {
//ignore this
}
}
}
集合中對象沒清理造成的內(nèi)存泄漏
在實際開發(fā)過程中難免會有把對象添加到集合容器(比如 ArrayList)中的需求,如果在一個對象使用結(jié)束之后未將該對象從該容器中移除掉,就會造成該對象不能被正確回收,從而造成內(nèi)存泄漏,解決辦法當(dāng)然就是在使用完之后將該對象從容器中移除。
WebView造成的內(nèi)存泄露
具體的可以看看我的這篇博客:android WebView詳解,常見漏洞詳解和安全源碼(下)。
未取消注冊導(dǎo)致的內(nèi)存泄漏
一些 Android 程序可能引用我們的 Android 程序的對象(比如注冊機制),即使我們的 Android 程序已經(jīng)結(jié)束了,但是別的應(yīng)用程序仍然還持有對我們 Android 程序某個對象的引用,這樣也會造成內(nèi)存不能被回收,比如調(diào)用 registerReceiver 后未調(diào)用unregisterReceiver。假設(shè)我們希望在鎖屏界面(LockScreen)中,監(jiān)聽系統(tǒng)中的電話服務(wù)以獲取一些信息,則可以在 LockScreen 中定義一個 PhoneStateListener 的對象,同時將它注冊到 TelephonyManager 服務(wù)中,對于 LockScreen 對象,當(dāng)需要顯示鎖屏界面的時候就會創(chuàng)建一個 LockScreen 對象,而當(dāng)鎖屏界面消失的時候 LockScreen 對象就會被釋放掉,但是如果在釋放 LockScreen 對象的時候忘記取消我們之前注冊的 PhoneStateListener 對象,則會間接導(dǎo)致 LockScreen 無法被回收,如果不斷的使鎖屏界面顯示和消失,則最終會由于大量的 LockScreen 對象沒有辦法被回收而引起 OOM,雖然有些系統(tǒng)程序本身好像是可以自動取消注冊的(當(dāng)然不及時),但是我們還是應(yīng)該在程序結(jié)束時明確的取消注冊。
因為內(nèi)存碎片導(dǎo)致分配內(nèi)存不足
還有一種情況是因為頻繁的內(nèi)存分配和釋放,導(dǎo)致內(nèi)存區(qū)域里面存在很多碎片,當(dāng)這些碎片足夠多,new 一個大對象的時候,所有的碎片中沒有一個碎片足夠大以分配給這個對象,但是所有的碎片空間加起來又是足夠的時候,就會出現(xiàn) OOM,而且這種 OOM 從某種意義上講,是完全能夠避免的。
由于產(chǎn)生內(nèi)存碎片的場景很多,從 Memory Monitor 來看,下面場景的內(nèi)存抖動是很容易產(chǎn)生內(nèi)存碎片的:
最常見產(chǎn)生內(nèi)存抖動的例子就是在 ListView 的 getView 方法中未復(fù)用 convertView 導(dǎo)致 View 的頻繁創(chuàng)建和釋放,針對這個問題的處理方式那當(dāng)然就是復(fù)用 convertView;或者是 String 拼接創(chuàng)建大量小的對象(比如在一些頻繁調(diào)用的地方打字符串拼接的 log 的時候);如果是其他的問題,就需要通過 Memory Monitor 去觀察內(nèi)存的實時分配釋放情況,找到內(nèi)存抖動的地方修復(fù)它,或者如果當(dāng)出現(xiàn)下面這種情況下的 OOM 時,也是由于內(nèi)存碎片導(dǎo)致無法分配內(nèi)存:
出現(xiàn)上面這種類型的 Crash 時就要去分析應(yīng)用里面是不是存在大量分配釋放對象的地方了。
Android 內(nèi)存優(yōu)化
內(nèi)存優(yōu)化請看下篇:Android 性能優(yōu)化之內(nèi)存泄漏檢測以及內(nèi)存優(yōu)化(下)。
引用
http://blog.csdn.net/luoshengyang/article/details/42555483
http://blog.csdn.net/luoshengyang/article/details/41688319
http://blog.csdn.net/luoshengyang/article/details/42492621
http://blog.csdn.net/luoshengyang/article/details/41338251
http://blog.csdn.net/luoshengyang/article/details/41581063
https://mp.weixin.qq.com/s?__biz=MzA4MzEwOTkyMQ==&mid=2667377215&idx=1&sn=26e3e9ec5f4cf3e7ed1e90a0790cc071&chksm=84f32371b384aa67166a3ff60e3f8ffdfbeed17b4c8b46b538d5a3eec524c9d0bcac33951a1a&scene=0&key=c2240201df732cf062d22d3cf95164740442d817864520af90bb0e71fa51102f2e91475a4f597ec20653c59d305c8a3e518d3f575d419dfcf8fb63a776e0d9fa6d3a9a6a52e84fedf3f467fe4af1ba8b&ascene=0&uin=Mjg5MDI3NjQ2Mg%3D%3D&devicetype=iMac+MacBookPro11%2C4+OSX+OSX+10.12.3+build(16D32)&version=12010310&nettype=WIFI&fontScale=100&pass_ticket=Upl17Ws6QQsmZSia%2F%2B0xkZs9DYxAJBQicqh8rcaxYUjcu3ztlJUPxYrQKML%2BUtuf
http://geek.csdn.net/news/detail/127226
http://www.lxweimin.com/p/216b03c22bb8
https://zhuanlan.zhihu.com/p/25213586
https://joyrun.github.io/2016/08/08/AndroidMemoryLeak/
http://www.cnblogs.com/larack/p/6071209.html
https://source.android.com/devices/tech/dalvik/gc-debug.html
http://blog.csdn.net/high2011/article/details/53138202
http://gityuan.com/2015/10/03/Android-GC/
http://www.ayqy.net/blog/android-gc-log%E8%A7%A3%E8%AF%BB/
https://developer.android.com/studio/profile/investigate-ram.html
https://zhuanlan.zhihu.com/p/26043999