1.Java內(nèi)存概要
在java內(nèi)存模型中,一般分為5個部分,棧(stack),堆(heap),方法區(qū)(method),本地方法區(qū)(native method),程序計(jì)數(shù)器。其實(shí)我們比較熟悉是棧,堆和方法區(qū)。
棧
主要存儲基本數(shù)據(jù)類型和引用類型
堆
主要存儲對象類型,一個虛擬機(jī)只有一個,所有線程共享,由虛擬機(jī)GC管理
方法區(qū)
又稱靜態(tài)區(qū),主要存儲class對象和靜態(tài)變量,一個虛擬機(jī)只有一個,所有線程是共享
2.內(nèi)存泄漏的原因與后果
對象有自己的生命周期,大部分情況都是比較短暫的,當(dāng)對象的生命周期結(jié)束后,由于一些特殊原因?qū)е略搶ο鬅o法被虛擬機(jī)回收,這就是內(nèi)存泄漏。當(dāng)內(nèi)存泄漏到了一定數(shù)值導(dǎo)致應(yīng)用占用的內(nèi)存達(dá)到閥值,這時虛擬機(jī)就會報出OutOfMenoryError的錯誤,從而程序崩潰。在android平臺中,由于移動端的特點(diǎn),這個問題將會更加明顯。
3.android常見的內(nèi)存泄漏情景
單例模式的內(nèi)存泄漏
單例對象的生命周期一般是從第一次創(chuàng)建使用到應(yīng)用結(jié)束,雖然不能完全和應(yīng)用的生命周期保持一致,但是也是和應(yīng)用同步消亡的。而在android中使用單例中,往往會把上下文對象(Context)傳給單例對象,一般會把Activity對象傳進(jìn)去,這時就產(chǎn)生了矛盾,兩者的生命周期是不一致的,這種不一致就會導(dǎo)致Activity對象onDestroy以后不能被虛擬機(jī)回收。處理的方法簡單,只需要調(diào)用Context的getApplicationContext,把對象轉(zhuǎn)稱application的上下文對象,這樣兩者消亡的生命周期就一致了,就不會內(nèi)存泄漏了。(注:這里不用考慮單例的寫法,不管是餓漢,懶漢,以及靜態(tài)單例或者枚舉寫法都是一樣)
修改前代碼:
public class Singleton {
private static Singleton instacne = null;
private Context mContext;
private Singleton(){}
public static Singleton getInstacne(Context context){
if (instacne == null)
instacne = new Singleton();
instacne.mContext = context;
return instacne;
}
}
修改后代碼:
public class Singleton {
private static Singleton instacne = null;
private Context mContext;
private Singleton(){}
public static Singleton getInstacne(Context context){
if (instacne == null)
instacne = new Singleton();
instacne.mContext = context.getApplicationContext();
return instacne;
}
}
非靜態(tài)內(nèi)部類創(chuàng)建靜態(tài)變量
非靜態(tài)內(nèi)部類會隱式持有外部類的實(shí)例,如果用非靜態(tài)內(nèi)部類創(chuàng)建靜態(tài)變量就會導(dǎo)致外部類的實(shí)例不能被及時回收。打個比方,如果這個外部類是Activity,非靜態(tài)內(nèi)部類是Adapter,在Activity中有個Adapter類型的靜態(tài)變量mAdapter,Adapter隱式持有Activity實(shí)例,mAdapter由于是靜態(tài)的,將和應(yīng)用消失的生命周期一致,當(dāng)Actviity銷毀后所占有的內(nèi)存就不會被虛擬機(jī)回收造成類內(nèi)存泄漏。修改的方式也比較簡單:1.把內(nèi)部類修飾成靜態(tài)的,2.或者把Adapter抽出來寫。
修改前
public class MainActivity extends Activity {
private static MyAdapter mAdapter;
......
}
class MyAdapter extends BaseAdapter{
......
}
修改后
public class MainActivity extends Activity {
private static MyAdapter mAdapter;
......
}
static class MyAdapter extends BaseAdapter{
......
}
使用Handler的內(nèi)存泄漏
handler的內(nèi)存泄漏場景本質(zhì)上和第二種是一樣的,所以原因也是大同小異的,先看一段代碼
public class MainActivity extends Activity {
private Handler mHandler = new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
}
};
}
這段代碼是不是非常熟悉,很多時候我們代碼就是這么寫的,一般情況下,這段代碼也是不會產(chǎn)生內(nèi)存泄漏的,但是出現(xiàn)下面的場景時就會內(nèi)存泄漏。
public class MainActivity extends Activity {
private Handler mHandler = new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Message msg = Message.obtain();
mHandler.sendMessageDelayed(msg,1000*60*5);
finish();
}
});
}
}
問題代碼的邏輯是點(diǎn)擊按鈕通過handler發(fā)送一條需要延遲5分鐘的message到消息隊(duì)列,然后退出當(dāng)前activity。那為什么這樣就會引起內(nèi)存泄漏呢?由于mHandler是非靜態(tài)的變量,他持有了Activity的實(shí)例,在handler,messageQueue,Loop機(jī)制中,handler既是發(fā)送者又是消費(fèi)者,所以當(dāng)handler在某些業(yè)務(wù)場景下不能跟activity生命周期產(chǎn)生同步就會引起actviity的內(nèi)存泄漏。修改的方式跟第二種類型,要將mHandler修飾成static。
線程的內(nèi)存泄漏
這種場景和第2,3情況是類似的,就是線程持有當(dāng)前activity的實(shí)例,當(dāng)activity要銷毀時,線程還沒有完成任務(wù),這樣就導(dǎo)致了activity的內(nèi)存泄漏,下面我們用thread來舉個例子。
new Thread(){
@Override
public void run() {
super.run();
......
}
}.start();
這樣的寫法在我們?nèi)粘i_發(fā)中極為常見,為了方便快速直接在邏輯中起個線程區(qū)完成異步任務(wù),這樣的寫法會導(dǎo)致線程管理混亂,更會引起activity的內(nèi)存泄漏。當(dāng)線程中的任務(wù)耗時過長,已經(jīng)大大超出activity的生命周期范圍中效果就更加明顯。原因跟上兩種雷同,thread匿名內(nèi)部類持也有了activity的實(shí)例,本身的任務(wù)又不能及時完成,影響activity內(nèi)存回收。修改的方式原則就是將線程和當(dāng)前activity脫離干系,交與專門的線程管理類操作。
Webview的內(nèi)存泄漏
現(xiàn)在的app的開發(fā)多多少少會涉及混合開發(fā),當(dāng)webview加載的頁面過多過于復(fù)雜,或者web頁面中js代碼不太完善時,webview就會占有大量內(nèi)存,同時也不可避免會出現(xiàn)內(nèi)存泄漏的情況。解決的方案就是給webview一個單獨(dú)的進(jìn)程,當(dāng)webview所在的activity銷毀時,直接殺掉這個單獨(dú)開啟的進(jìn)程。
AndroidManifeast.xml
<activity
android:name=".WebviewActivity"
android:process=":web_h5"
android:screenOrientation="portrait"
android:configChanges="orientation|screenSize|keyboardHidden"
/>
Activity
@Override
protected void onDestroy() {
super.onDestroy();
android.os.Process.killProcess(android.os.Process.myPid());
}
此方案的注意點(diǎn):
1.app內(nèi)進(jìn)程間通信需要通過廣播和aidl交互
2.數(shù)據(jù)持久化需要通過ContentProvider進(jìn)行處理
4.Java引用類型
在講Leakcanary之前,先復(fù)習(xí)下java中4中引用類型和引用隊(duì)列
強(qiáng)引用(StrongReference)
我們一般情況下使用的都是強(qiáng)引用,申請內(nèi)存后,如果不手動設(shè)置成Null,虛擬機(jī)是你會回收此內(nèi)存,就算內(nèi)存不足也不會回收,哪怕報出OOM
軟引用(SoftReference)
在虛擬機(jī)內(nèi)存足夠的情況下,掃描到軟引用是不會被GC回收的,但是內(nèi)存不足的情況下也會被GC回收
弱引用(WeakReference)
不管虛擬機(jī)內(nèi)存是否足夠,只要被GC掃描到,就會被回收
虛引用(PhantomReference)
形同虛設(shè)的引用,不會決定對象的生命周期,很少涉及
引用隊(duì)列(ReferenceQueue)
一般和軟引用弱引用配合使用
5.Leakcanary的介紹及使用
Leakcanary是square公司開源的一款內(nèi)存監(jiān)測框架,Github的地址:https://github.com/square/leakcanary
集成的方式
dependencies {
debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.6.3'
releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.6.3'
// Optional, if you use support library fragments:
debugImplementation 'com.squareup.leakcanary:leakcanary-support-fragment:1.6.3'
}
初始化的方式
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集成工作結(jié)束,然后我們寫一個內(nèi)存泄漏代碼也實(shí)驗(yàn)下。我們選擇上面介紹過的handler內(nèi)存泄漏的代碼去執(zhí)行。執(zhí)行完代碼邏輯,發(fā)現(xiàn)手機(jī)里面不僅有我們實(shí)驗(yàn)的app,還有一個名字叫做Leaks的圖標(biāo),進(jìn)入Leaks后發(fā)現(xiàn)真的發(fā)現(xiàn)檢測到了內(nèi)存泄漏的activity,引起的原因是MessageQueue,果然厲害。然后我們把handler的解決方法試下,運(yùn)行后發(fā)現(xiàn)Leaks里面空空如也,這說明我們修復(fù)了之前的內(nèi)存泄漏。

6.Leakcanary源碼及原理(Activity為例)
Leakcanary的原理就是監(jiān)控應(yīng)用中所有activity生命周期中的onDestroy方法,在onDestroy中把a(bǔ)ctivity轉(zhuǎn)成弱引用保存在引用隊(duì)列中,然后框架會不斷遍歷分析引用隊(duì)列,檢測到內(nèi)存地址是否可達(dá),并輸出內(nèi)存泄漏的最短路徑。
開始看源碼,我們從LeakCanary.install()入口。
/**
* Creates a {@link RefWatcher} that works out of the box, and starts watching activity
* references (on ICS+).
*/
public static @NonNull RefWatcher install(@NonNull Application application) {
return refWatcher(application).listenerServiceClass(DisplayLeakService.class)
.excludedRefs(AndroidExcludedRefs.createAppDefaults().build())
.buildAndInstall();
}
此方法會創(chuàng)建并返回RefWatcher對象,這對象是Leakcanary的核心,buildAndInstall方法是該方法的核心,我們繼續(xù)跟進(jìn)去看
/**
* Creates a {@link RefWatcher} instance and makes it available through {@link
* LeakCanary#installedRefWatcher()}.
*
* Also starts watching activity references if {@link #watchActivities(boolean)} was set to true.
*
* @throws UnsupportedOperationException if called more than once per Android process.
*/
public @NonNull RefWatcher buildAndInstall() {
if (LeakCanaryInternals.installedRefWatcher != null) {
throw new UnsupportedOperationException("buildAndInstall() should only be called once.");
}
RefWatcher refWatcher = build();
if (refWatcher != DISABLED) {
if (enableDisplayLeakActivity) {
LeakCanaryInternals.setEnabledAsync(context, DisplayLeakActivity.class, true);
}
if (watchActivities) {
ActivityRefWatcher.install(context, refWatcher);
}
if (watchFragments) {
FragmentRefWatcher.Helper.install(context, refWatcher);
}
}
LeakCanaryInternals.installedRefWatcher = refWatcher;
return refWatcher;
}
真正創(chuàng)建RefWatcher對象就是這里的build方法,Leakcanary支持監(jiān)控Activity和Fragment的內(nèi)存泄漏情況,我們這里主要是研究Activity,所以這里接著跟ActivityRefWatcher.install方法
public static void install(@NonNull Context context, @NonNull RefWatcher refWatcher) {
Application application = (Application) context.getApplicationContext();
ActivityRefWatcher activityRefWatcher = new ActivityRefWatcher(application, refWatcher);
application.registerActivityLifecycleCallbacks(activityRefWatcher.lifecycleCallbacks);
}
application.registerActivityLifecycleCallbacks此方法是系統(tǒng)方法,用來監(jiān)控app內(nèi)部所有Activity的生命周期。
那我們就來看看activityRefWatcher.lifecycleCallbacks的實(shí)現(xiàn)是什么?
private final Application.ActivityLifecycleCallbacks lifecycleCallbacks =
new ActivityLifecycleCallbacksAdapter() {
@Override public void onActivityDestroyed(Activity activity) {
refWatcher.watch(activity);
}
};
只實(shí)現(xiàn)了onActivityDestroyed回調(diào),接著跟進(jìn)去refWatcher.watch方法
public void watch(Object watchedReference, String referenceName) {
if (this == DISABLED) {
return;
}
checkNotNull(watchedReference, "watchedReference");
checkNotNull(referenceName, "referenceName");
final long watchStartNanoTime = System.nanoTime();
String key = UUID.randomUUID().toString();
retainedKeys.add(key);
final KeyedWeakReference reference =
new KeyedWeakReference(watchedReference, key, referenceName, queue);
ensureGoneAsync(watchStartNanoTime, reference);
}
這個方法的核心是給每個觀察的引用產(chǎn)生一個隨機(jī)的key,保存在集合中,并把引用轉(zhuǎn)成弱引用保存在引用隊(duì)列中,ensureGoneAsync方法是核心的工作方法,作用是啟動工作線程異步的去分析內(nèi)存泄漏的情況。
private void ensureGoneAsync(final long watchStartNanoTime, final KeyedWeakReference reference) {
watchExecutor.execute(new Retryable() {
@Override public Retryable.Result run() {
return ensureGone(reference, watchStartNanoTime);
}
});
}
Retryable.Result ensureGone(final KeyedWeakReference reference, final long watchStartNanoTime) {
long gcStartNanoTime = System.nanoTime();
long watchDurationMs = NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);
removeWeaklyReachableReferences();
if (debuggerControl.isDebuggerAttached()) {
// The debugger can create false leaks.
return RETRY;
}
if (gone(reference)) {
return DONE;
}
gcTrigger.runGc();
removeWeaklyReachableReferences();
if (!gone(reference)) {
long startDumpHeap = System.nanoTime();
long gcDurationMs = NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);
File heapDumpFile = heapDumper.dumpHeap();
if (heapDumpFile == RETRY_LATER) {
// Could not dump the heap.
return RETRY;
}
long heapDumpDurationMs = NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);
HeapDump heapDump = heapDumpBuilder.heapDumpFile(heapDumpFile).referenceKey(reference.key)
.referenceName(reference.name)
.watchDurationMs(watchDurationMs)
.gcDurationMs(gcDurationMs)
.heapDumpDurationMs(heapDumpDurationMs)
.build();
heapdumpListener.analyze(heapDump);
}
return DONE;
}
ensureGone方法的作用就是確保能回收的引用從隊(duì)列中刪除,如果引用還在隊(duì)列中沒有被回收就會生成heap文件,然后分析這個heap文件。真正分析heap文件的工作在HeapAnalyzerService服務(wù)中。
@Override protected void onHandleIntentInForeground(@Nullable Intent intent) {
if (intent == null) {
CanaryLog.d("HeapAnalyzerService received a null intent, ignoring.");
return;
}
String listenerClassName = intent.getStringExtra(LISTENER_CLASS_EXTRA);
HeapDump heapDump = (HeapDump) intent.getSerializableExtra(HEAPDUMP_EXTRA);
HeapAnalyzer heapAnalyzer =
new HeapAnalyzer(heapDump.excludedRefs, this, heapDump.reachabilityInspectorClasses);
AnalysisResult result = heapAnalyzer.checkForLeak(heapDump.heapDumpFile, heapDump.referenceKey,
heapDump.computeRetainedHeapSize);
AbstractAnalysisResultService.sendResultToListener(this, listenerClassName, heapDump, result);
}
整個框架中最核心的方法出現(xiàn)了heapAnalyzer.checkForLeak,通過這個方法就能判斷是否內(nèi)存泄漏,并輸出結(jié)果。
public @NonNull AnalysisResult checkForLeak(@NonNull File heapDumpFile,
@NonNull String referenceKey,
boolean computeRetainedSize) {
long analysisStartNanoTime = System.nanoTime();
if (!heapDumpFile.exists()) {
Exception exception = new IllegalArgumentException("File does not exist: " + heapDumpFile);
return failure(exception, since(analysisStartNanoTime));
}
try {
listener.onProgressUpdate(READING_HEAP_DUMP_FILE);
HprofBuffer buffer = new MemoryMappedFileBuffer(heapDumpFile);
HprofParser parser = new HprofParser(buffer);
listener.onProgressUpdate(PARSING_HEAP_DUMP);
Snapshot snapshot = parser.parse();
listener.onProgressUpdate(DEDUPLICATING_GC_ROOTS);
deduplicateGcRoots(snapshot);
listener.onProgressUpdate(FINDING_LEAKING_REF);
Instance leakingRef = findLeakingReference(referenceKey, snapshot);
// False alarm, weak reference was cleared in between key check and heap dump.
if (leakingRef == null) {
String className = leakingRef.getClassObj().getClassName();
return noLeak(className, since(analysisStartNanoTime));
}
return findLeakTrace(analysisStartNanoTime, snapshot, leakingRef, computeRetainedSize);
} catch (Throwable e) {
return failure(e, since(analysisStartNanoTime));
}
}
先把heap文件讀到內(nèi)存緩存中HprofBuffer,然后通過HprofParser 進(jìn)行解析內(nèi)容,產(chǎn)生Snapshot快照,deduplicateGcRoots去除重復(fù)的GcRoot,findLeakingReference方法是通過之前產(chǎn)生的key,在快照中查找我們真正檢測的引用是否存在,如果不存在說明無內(nèi)存泄漏,如果存在,就找出內(nèi)存泄漏的路徑,輸出結(jié)果。
以上就是Leakcanary檢測內(nèi)存泄漏的完整流程。第一遍看框架源碼主要還是梳理執(zhí)行流程,抓核心,放細(xì)節(jié)。如果對Leakcanary這個框架設(shè)計(jì)感興趣不妨多看幾遍源碼。這邊就不做展開了。