上一篇文章總結了一些常見的記憶體洩露場景及優化方案,這篇文章繼續總結記憶體洩露的一些常用的檢測和分析方法。
Lint程式碼檢查
AndroidStudio自動Lint程式碼檢查工具,一些常見的程式碼警告Lint工具都會給我們提示。使用也比較簡單:
Analyze —> Inspect Code 然後選擇檢查範圍:
比如如果存在非靜態內部類的Handler,可能會導致記憶體洩露,檢查結果就會顯示在AndroidStudio的控制檯。
其實,只要我們在設定裡面勾選了Lint程式碼檢查(AnroidStudio預設是勾選了的),在寫程式碼的時候就會自動提示可能發生記憶體洩露。
通常在寫Handler、靜態欄位、標記物件等可能存在的記憶體洩露時,Lint檢查工具都會有一個警告提示資訊,我們可以根據Lint檢查的提示資訊來避免這些有可能發生的記憶體洩露。
Android Monitor
在AndroidStudio中,可以通過Monitors來監控Memory、CPU、Network、GPU等。在Monitors監控中,我們可以獲取記憶體的各種資訊來分析記憶體洩露。
首先執行工程後,開啟控制檯的Android Monitor:
在執行裝置中使用app(各個頁面的跳轉,使用相應的各種功能),就可以看到記憶體使用的不斷變化:
淡藍色和淺灰色區域就是記憶體分配的變化過程,淺灰色表示空閒記憶體,淡藍色表示使用記憶體。
通常,我們在開啟一個新的頁面後,使用的記憶體就會增加,相應的,關閉一個頁面後,系統執行了GC,使用的記憶體應該下降。如果我們在退出介面並執行GC後,記憶體使用並未下降明顯,或者使用記憶體沒有下降初始的使用大小,那麼有可能就發生了記憶體洩露。
執行工程,在裝置上操作app,觀察Monitor中記憶體的變化,點選 initiate GC 觸發GC,然後點選Dump Java Heap轉出堆資訊,稍等片刻,生成hprof檔案,生成後會在Studio中自動開啟。
點選右側的Analyzer Tasks,再點選Perform Analyzer,展開下面分析結果
中的 Leaked Activities 就可以看到發生記憶體洩露的Activity了。
可以根據左側的引用樹,來查詢持有Activity引用的位置,從而判斷出哪個地方導致了記憶體洩露。
Mat
使用第三方的Mat工具來分析記憶體洩露,需要在官網下載獨立版的Mat。
將Android Monitor生成的hprof檔案匯出為標準的hprof檔案(必須這樣匯出,直接copy出來會報錯的):
使用Mat開啟匯出的hprof檔案:
點選Histogram(直方圖),可以看到類對應的例項數量的統計。
在Class Name下面輸入需要匹配的類名,根據類來檢視它的例項的引用,進而分析是否存在記憶體洩露。
可以看到TestActivity和MyHandler都只有一個例項被引用。
TestActivity點選右鍵—>Merge Shortest Paths to GC Roots —>exclude all phantom/weak/soft etc.references。
Merge Shortest Paths to GC Roots 可以檢視一個物件到RC Roots是否存在引用鏈相連線, 在JAVA中是通過可達性(Reachability Analysis)來判斷物件是否存活,這個演算法的基本思想是通過一系列的稱謂"GC Roots"的物件作為起始點,從這些節點開始向下搜尋,搜尋所走得路徑稱為引用鏈,當一個物件到GC Roots沒有任何引用鏈相連則該物件被判定為可以被回收的物件,反之不能被回收,我們可以選擇 exclude all phantom/weak/soft etc.references(排查虛引用/弱引用/軟引用等)因為被虛引用/弱引用/軟引用的物件可以直接被GC給回收。
參考自Android 效能優化之使用MAT分析記憶體洩露問題。
可以看到TestActivity例項存在GC Roots鏈,TextActivity例項被mMessageQueue.mMessae.target.this$0持有,那麼發生了記憶體洩露,我們可以根據引用鏈來在程式碼中找到記憶體洩露的位置。
LeakCanary
LeakCanary是square開源的檢測記憶體洩露的第三方庫。它最大的有點就是開發者只需要新增簡單程式碼,app在執行時如果發生了記憶體洩露,就會很直觀的將記憶體洩露的詳細資訊展示在通知欄上,這樣避免了Android Monitor或者Mat等工具的繁瑣的分析過程。
在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 App extends Application {
// 模擬記憶體洩露場景
public static ArrayList<Activity> sActivities = new ArrayList<>();
@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...
}
}複製程式碼
在Application中新建一個靜態List,裡面儲存Activity,來模擬記憶體洩露案例:
public class TestActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
App.sActivities.add(this);
}
public void back(View view) {
finish();
}
}複製程式碼
執行app後,在控制檯可以看到LeakCanary的日誌:
04-25 10:20:46.793 D/LeakCanary: In com.xiao.memoryleakexample:1.0:1.
04-25 10:20:46.793 D/LeakCanary: * com.xiao.memoryleakexample.TestActivity has leaked:
04-25 10:20:46.793 D/LeakCanary: * GC ROOT static com.xiao.memoryleakexample.app.App.sActivities
04-25 10:20:46.793 D/LeakCanary: * references java.util.ArrayList.array
04-25 10:20:46.793 D/LeakCanary: * references array java.lang.Object[].[0]
04-25 10:20:46.793 D/LeakCanary: * leaks com.xiao.memoryleakexample.TestActivity instance
04-25 10:20:46.793 D/LeakCanary: * Retaining: 88 KB.
04-25 10:20:46.793 D/LeakCanary: * Reference Key: 915bf11a-db9f-468e-8064-d6fb103710e9
04-25 10:20:46.793 D/LeakCanary: * Device: OPPO OPPO OPPO R9 Plusm A R9PlusmA
04-25 10:20:46.793 D/LeakCanary: * Android Version: 5.1.1 API: 22 LeakCanary: 1.5 00f37f5
04-25 10:20:46.793 D/LeakCanary: * Durations: watch=5049ms, gc=207ms, heap dump=979ms, analysis=122889ms
04-25 10:20:46.793 D/LeakCanary: * Details:
04-25 10:20:46.793 D/LeakCanary: * Class com.xiao.memoryleakexample.app.App
04-25 10:20:46.793 D/LeakCanary: | static $staticOverhead = byte[24]@314667009 (0x12c17001)
04-25 10:20:46.793 D/LeakCanary: | static sActivities = java.util.ArrayList@315492800 (0x12ce09c0)
04-25 10:20:46.793 D/LeakCanary: | static serialVersionUID = -920324649544707127
04-25 10:20:46.793 D/LeakCanary: | static $change = null
04-25 10:20:46.793 D/LeakCanary: * Instance of java.util.ArrayList
04-25 10:20:46.793 D/LeakCanary: | static $staticOverhead = byte[16]@1893824473 (0x70e177d9)
04-25 10:20:46.793 D/LeakCanary: | static MIN_CAPACITY_INCREMENT = 12
04-25 10:20:46.793 D/LeakCanary: | static serialVersionUID = 8683452581122892189
04-25 10:20:46.793 D/LeakCanary: | array = java.lang.Object[12]@318048768 (0x12f50a00)
04-25 10:20:46.793 D/LeakCanary: | size = 1
04-25 10:20:46.793 D/LeakCanary: | modCount = 1
04-25 10:20:46.793 D/LeakCanary: * Array of java.lang.Object[]
04-25 10:20:46.793 D/LeakCanary: | [0] = com.xiao.memoryleakexample.TestActivity@316091520 (0x12d72c80)
04-25 10:20:46.793 D/LeakCanary: | [1] = null
04-25 10:20:46.793 D/LeakCanary: | [2] = null
04-25 10:20:46.793 D/LeakCanary: | [3] = null
04-25 10:20:46.793 D/LeakCanary: | [4] = null
04-25 10:20:46.793 D/LeakCanary: | [5] = null
04-25 10:20:46.793 D/LeakCanary: | [6] = null
04-25 10:20:46.793 D/LeakCanary: | [7] = null
04-25 10:20:46.793 D/LeakCanary: | [8] = null
04-25 10:20:46.793 D/LeakCanary: | [9] = null
04-25 10:20:46.793 D/LeakCanary: | [10] = null
04-25 10:20:46.793 D/LeakCanary: | [11] = null
04-25 10:20:46.793 D/LeakCanary: * Instance of com.xiao.memoryleakexample.TestActivity
04-25 10:20:46.793 D/LeakCanary: | static $staticOverhead = byte[16]@316125185 (0x12d7b001)
04-25 10:20:46.793 D/LeakCanary: | static serialVersionUID = 836998863274086997
04-25 10:20:46.793 D/LeakCanary: | static $change = null
04-25 10:20:46.793 D/LeakCanary: | mHandler = com.xiao.memoryleakexample.TestActivity$MyHandler@318005952 (0x12f462c0)
04-25 10:20:46.793 D/LeakCanary: | mDelegate = android.support.v7.app.AppCompatDelegateImplV14@314816320 (0x12c3b740)
04-25 10:20:46.793 D/LeakCanary: | mEatKeyUpEvent = false
04-25 10:20:46.793 D/LeakCanary: | mResources = null
04-25 10:20:46.793 D/LeakCanary: | mThemeId = 2131230884
04-25 10:20:46.793 D/LeakCanary: | mCreated = true
04-25 10:20:46.793 D/LeakCanary: | mFragments = android.support.v4.app.FragmentController@317876896 (0x12f26aa0)
04-25 10:20:46.793 D/LeakCanary: | mHandler = android.support.v4.app.FragmentActivity$1@318005920 (0x12f462a0)
04-25 10:20:46.793 D/LeakCanary: | mNextCandidateRequestIndex = 0
04-25 10:20:46.793 D/LeakCanary: | mOptionsMenuInvalidated = false
04-25 10:20:46.793 D/LeakCanary: | mPendingFragmentActivityResults = android.support.v4.util.SparseArrayCompat@318008352 (0x12f46c20)
04-25 10:20:46.793 D/LeakCanary: | mReallyStopped = true
04-25 10:20:46.793 D/LeakCanary: | mRequestedPermissionsFromFragment = false
04-25 10:20:46.793 D/LeakCanary: | mResumed = false
04-25 10:20:46.793 D/LeakCanary: | mRetaining = false
04-25 10:20:46.793 D/LeakCanary: | mStopped = true
04-25 10:20:46.793 D/LeakCanary: | mStartedActivityFromFragment = false
04-25 10:20:46.793 D/LeakCanary: | mStartedIntentSenderFromFragment = false
04-25 10:20:46.793 D/LeakCanary: | mExtraDataMap = android.support.v4.util.SimpleArrayMap@318005888 (0x12f46280)
04-25 10:20:46.793 D/LeakCanary: | mActionBar = null
04-25 10:20:46.793 D/LeakCanary: | mActivityInfo = android.content.pm.ActivityInfo@318009472 (0x12f47080)
04-25 10:20:46.793 D/LeakCanary: | mActivityTransitionState = android.app.ActivityTransitionState@317937344 (0x12f356c0)
04-25 10:20:46.793 D/LeakCanary: | mAllLoaderManagers = android.util.ArrayMap@318081312 (0x12f58920)
04-25 10:20:46.793 D/LeakCanary: | mApplication = com.xiao.memoryleakexample.app.App@315492832 (0x12ce09e0)
04-25 10:20:46.793 D/LeakCanary: | mCalled = true
04-25 10:20:46.793 D/LeakCanary: | mChangeCanvasToTranslucent = false
04-25 10:20:46.793 D/LeakCanary: | mChangingConfigurations = false
04-25 10:20:46.793 D/LeakCanary: | mCheckedForLoaderManager = true
04-25 10:20:46.793 D/LeakCanary: | mComponent = android.content.ComponentName@314990768 (0x12c660b0)
04-25 10:20:46.793 D/LeakCanary: | mConfigChangeFlags = 0
04-25 10:20:46.793 D/LeakCanary: | mContainer = android.app.Activity$1@317876848 (0x12f26a70)
04-25 10:20:46.793 D/LeakCanary: | mCurrentConfig = android.content.res.Configuration@317856672 (0x12f21ba0)
04-25 10:20:46.793 D/LeakCanary: | mDecor = null
04-25 10:20:46.793 D/LeakCanary: | mDefaultKeyMode = 0
04-25 10:20:46.793 D/LeakCanary: | mDefaultKeySsb = null
04-25 10:20:46.793 D/LeakCanary: | mDestroyed = true
04-25 10:20:46.793 D/LeakCanary: | mDoReportFullyDrawn = false
04-25 10:20:46.793 D/LeakCanary: | mEmbeddedID = null
04-25 10:20:46.793 D/LeakCanary: | mEnableDefaultActionBarUp = false
04-25 10:20:46.793 D/LeakCanary: | mEnterTransitionListener = android.app.SharedElementCallback$1@1893595344 (0x70ddf8d0)
04-25 10:20:46.793 D/LeakCanary: | mExitTransitionListener = android.app.SharedElementCallback$1@1893595344 (0x70ddf8d0)
04-25 10:20:46.793 D/LeakCanary: | mFinished = true
04-25 10:20:46.793 D/LeakCanary: | mFragments = android.app.FragmentManagerImpl@317856448 (0x12f21ac0)
04-25 10:20:46.793 D/LeakCanary: | mHandler = android.os.Handler@318005856 (0x12f46260)
04-25 10:20:46.793 D/LeakCanary: | mIdent = 578025123
04-25 10:20:46.793 D/LeakCanary: | mInstanceTracker = android.os.StrictMode$InstanceTracker@317876864 (0x12f26a80)
04-25 10:20:46.793 D/LeakCanary: | mInstrumentation = android.app.Instrumentation@315352176 (0x12cbe470)
04-25 10:20:46.793 D/LeakCanary: | mIntent = android.content.Intent@317362304 (0x12ea9080)
04-25 10:20:46.793 D/LeakCanary: | mLastNonConfigurationInstances = null
04-25 10:20:46.793 D/LeakCanary: | mLoaderManager = null
04-25 10:20:46.793 D/LeakCanary: | mLoadersStarted = false
04-25 10:20:46.793 D/LeakCanary: | mMainThread = android.app.ActivityThread@314856000 (0x12c45240)
04-25 10:20:46.803 D/LeakCanary: | mManagedCursors = java.util.ArrayList@318005792 (0x12f46220)
04-25 10:20:46.803 D/LeakCanary: | mManagedDialogs = null
04-25 10:20:46.803 D/LeakCanary: | mMenuInflater = null
04-25 10:20:46.803 D/LeakCanary: | mParent = null
04-25 10:20:46.803 D/LeakCanary: | mReferrer = java.lang.String@314984512 (0x12c64840)
04-25 10:20:46.803 D/LeakCanary: | mResultCode = 0
04-25 10:20:46.803 D/LeakCanary: | mResultData = null
04-25 10:20:46.803 D/LeakCanary: | mResumed = false
04-25 10:20:46.803 D/LeakCanary: | mSearchManager = null
04-25 10:20:46.803 D/LeakCanary: | mStartedActivity = false
04-25 10:20:46.803 D/LeakCanary: | mStopped = true
04-25 10:20:46.803 D/LeakCanary: | mTemporaryPause = false
04-25 10:20:46.803 D/LeakCanary: | mTitle = java.lang.String@316164352 (0x12d84900)
04-25 10:20:46.803 D/LeakCanary: | mTitleColor = 0
04-25 10:20:46.803 D/LeakCanary: | mTitleReady = true
04-25 10:20:46.803 D/LeakCanary: | mToken = android.os.BinderProxy@314983040 (0x12c64280)
04-25 10:20:46.803 D/LeakCanary: | mTranslucentCallback = null
04-25 10:20:46.803 D/LeakCanary: | mUiThread = java.lang.Thread@1967775656 (0x7549dfa8)
04-25 10:20:46.803 D/LeakCanary: | mVisibleBehind = false
04-25 10:20:46.803 D/LeakCanary: | mVisibleFromClient = true
04-25 10:20:46.803 D/LeakCanary: | mVisibleFromServer = true
04-25 10:20:46.803 D/LeakCanary: | mVoiceInteractor = null
04-25 10:20:46.803 D/LeakCanary: | mWindow = com.android.internal.policy.impl.PhoneWindow@315631936 (0x12d02940)
04-25 10:20:46.803 D/LeakCanary: | mWindowAdded = true
04-25 10:20:46.803 D/LeakCanary: | mWindowManager = android.view.WindowManagerImpl@318006848 (0x12f46640)
04-25 10:20:46.803 D/LeakCanary: | mInflater = com.android.internal.policy.impl.PhoneLayoutInflater@317929312 (0x12f33760)
04-25 10:20:46.803 D/LeakCanary: | mOverrideConfiguration = null
04-25 10:20:46.803 D/LeakCanary: | mResources = android.content.res.Resources@314591360 (0x12c04880)
04-25 10:20:46.803 D/LeakCanary: | mTheme = android.content.res.Resources$Theme@318006400 (0x12f46480)
04-25 10:20:46.803 D/LeakCanary: | mThemeResource = 2131230884
04-25 10:20:46.803 D/LeakCanary: | mBase = android.app.ContextImpl@317145792 (0x12e742c0)
04-25 10:20:46.803 D/LeakCanary: * Excluded Refs:
04-25 10:20:46.803 D/LeakCanary: | Field: android.view.inputmethod.InputMethodManager.mNextServedView
04-25 10:20:46.803 D/LeakCanary: | Field: android.view.inputmethod.InputMethodManager.mServedView
04-25 10:20:46.803 D/LeakCanary: | Field: android.view.inputmethod.InputMethodManager.mServedInputConnection
04-25 10:20:46.803 D/LeakCanary: | Field: android.view.inputmethod.InputMethodManager.mCurRootView
04-25 10:20:46.803 D/LeakCanary: | Field: android.animation.LayoutTransition$1.val$parent
04-25 10:20:46.803 D/LeakCanary: | Field: android.view.textservice.SpellCheckerSession$1.this$0
04-25 10:20:46.803 D/LeakCanary: | Field: android.support.v7.internal.widget.ActivityChooserModel.mActivityChoserModelPolicy
04-25 10:20:46.803 D/LeakCanary: | Field: android.widget.ActivityChooserModel.mActivityChoserModelPolicy
04-25 10:20:46.803 D/LeakCanary: | Field: android.accounts.AccountManager$AmsTask$Response.this$1
04-25 10:20:46.803 D/LeakCanary: | Field: android.media.MediaScannerConnection.mContext
04-25 10:20:46.803 D/LeakCanary: | Field: android.os.UserManager.mContext
04-25 10:20:46.803 D/LeakCanary: | Field: android.media.AudioManager$1.this$0
04-25 10:20:46.803 D/LeakCanary: | Field: android.widget.Editor$Blink.this$0
04-25 10:20:46.803 D/LeakCanary: | Field: android.net.ConnectivityManager.sInstance
04-25 10:20:46.803 D/LeakCanary: | Field: android.view.Choreographer$FrameDisplayEventReceiver.mMessageQueue (always)
04-25 10:20:46.803 D/LeakCanary: | Static field: android.text.TextLine.sCached
04-25 10:20:46.803 D/LeakCanary: | Thread:FinalizerWatchdogDaemon (always)
04-25 10:20:46.803 D/LeakCanary: | Thread:main (always)
04-25 10:20:46.803 D/LeakCanary: | Thread:LeakCanary-Heap-Dump (always)
04-25 10:20:46.803 D/LeakCanary: | Class:java.lang.ref.WeakReference (always)
04-25 10:20:46.803 D/LeakCanary: | Class:java.lang.ref.SoftReference (always)
04-25 10:20:46.803 D/LeakCanary: | Class:java.lang.ref.PhantomReference (always)
04-25 10:20:46.803 D/LeakCanary: | Class:java.lang.ref.Finalizer (always)
04-25 10:20:46.803 D/LeakCanary: | Class:java.lang.ref.FinalizerReference (always)複製程式碼
日誌當中展示了詳細的記憶體洩露資訊。同時,在執行裝置上,會以通知的形式展示記憶體洩露:
點選通知欄後會展示訊息的記憶體洩露資訊,包括洩露的具體例項、以及發生在哪個類中的具體引用位置:
最後
Lint、Android Monitor、Mat,以及LeakCanary都能讓我們在平常的開發過程中非常有效的避免記憶體洩露,至於選擇哪個工具,那種方式,就看自己平常的習慣了。個人還是更喜歡使用LeakCanary,只需要簡單的整合,就可以更加快速,直觀展示記憶體洩露的資訊。