Java內(nèi)存問(wèn)題 及 LeakCanary 原理分析

前些天,有人問(wèn)到 “開發(fā)過(guò)程中常見的內(nèi)存泄漏都有哪些?”,一時(shí)脫口而出:靜態(tài)的對(duì)象中(包括單例)持有一個(gè)生命周期較短的引用時(shí),或內(nèi)部類的子代碼塊對(duì)象的生命周期超過(guò)了外面代碼的生命周期(如非靜態(tài)內(nèi)部類,線程),會(huì)導(dǎo)致這個(gè)短生命周期的對(duì)象內(nèi)存泄漏。總之就是一個(gè)對(duì)象的生命周期結(jié)束(不再使用該對(duì)象)后,依然被某些對(duì)象所持有該對(duì)象強(qiáng)引用的場(chǎng)景就是內(nèi)存泄漏。

這樣回答很明顯并不是問(wèn)答人想要的都有哪些場(chǎng)景,所以這里抽時(shí)間整理了下內(nèi)存相關(guān)的知識(shí)點(diǎn),及LeakCanary工具的原理分析。

Java內(nèi)存問(wèn)題 及 LeakCanary 原理分析

在安卓等其他移動(dòng)平臺(tái)上,內(nèi)存問(wèn)題顯得特別重要,想要做到虛擬機(jī)內(nèi)存的高效利用,及內(nèi)存問(wèn)題的快速定位,了解下虛擬機(jī)內(nèi)存模塊及管理相關(guān)知識(shí)是很有必要的,這篇文章將從最基礎(chǔ)的知識(shí)分析,內(nèi)存問(wèn)題的產(chǎn)生地方、原因、解決方案等原理。

一、運(yùn)行時(shí)內(nèi)存區(qū)域

內(nèi)存區(qū)

這里以Java虛擬機(jī)為例,將運(yùn)行時(shí)內(nèi)存區(qū)分為不同的區(qū)域,每個(gè)區(qū)域承擔(dān)著不同的功能。

方法區(qū)
用戶存儲(chǔ)已被虛擬機(jī)加載的類信息,常量,靜態(tài)常量,即時(shí)編譯器編譯后的代碼等數(shù)據(jù)。異常狀態(tài) OutOfMemoryError,其中包含常量池和用戶存放編譯器生成的各種字面量和符號(hào)引用。


是JVM所管理的內(nèi)存中最大的一塊。唯一目的就是存放實(shí)例對(duì)象,幾乎所有的對(duì)象實(shí)例都在這里分配。Java堆是垃圾收集器管理的主要區(qū)域,因此很多時(shí)候也被稱為“GC堆”。異常狀態(tài) OutOfMemoryError。

虛擬機(jī)棧
描述的是java方法執(zhí)行的內(nèi)存模型,每個(gè)方法在執(zhí)行時(shí)都會(huì)創(chuàng)建一個(gè)棧幀,用戶存儲(chǔ)局部變量表,操作數(shù)棧,動(dòng)態(tài)連接,方法出口等信息。每一個(gè)方法從調(diào)用直至完成的過(guò)程,就對(duì)應(yīng)著一個(gè)棧幀在虛擬機(jī)棧中入棧到出棧的過(guò)程。 對(duì)這個(gè)區(qū)域定義了兩種異常狀態(tài) OutOfMemoryError、StackOverflowError。

本地方法棧
虛擬機(jī)棧為虛擬機(jī)執(zhí)行java方法,而本地方法棧為虛擬機(jī)使用到的Native方法服務(wù)。異常狀態(tài)StackOverFlowError、OutOfMemoryError。

程序計(jì)數(shù)器
一塊較小的內(nèi)存,當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)指示器。字節(jié)碼解釋器工作時(shí),就是通過(guò)改變這個(gè)計(jì)數(shù)器的值來(lái)選取下一條需要執(zhí)行的字節(jié)碼指令。

內(nèi)存模型

Java內(nèi)存模型規(guī)定了所有的變量都存儲(chǔ)在主內(nèi)存中。每條線程中還有自己的工作內(nèi)存,線程的工作內(nèi)存中保存了被該線程所使用到的變量,這些變量是從主內(nèi)存中拷貝而來(lái)。線程對(duì)變量的所有操作(讀,寫)都必須在工作內(nèi)存中進(jìn)行。不同線程之間也無(wú)法直接訪問(wèn)對(duì)方工作內(nèi)存中的變量,線程間變量值的傳遞均需要通過(guò)主內(nèi)存來(lái)完成。

為了保證內(nèi)存可見性,常常利用volatile關(guān)鍵子特性來(lái)保證變量的可見性(并不能保證并發(fā)時(shí)原子性)。

二、內(nèi)存如何回收

內(nèi)存的分配

一個(gè)對(duì)象從被創(chuàng)建到回收,主要經(jīng)歷階段有 1:創(chuàng)建階段(Created)、2: 應(yīng)用階段(In Use)、3:不可見階段(Invisible)、4:不可達(dá)階段(Unreachable)、5:收集階段(Collected)、6:終結(jié)階段(、Finalized)、7:對(duì)象空間重分配階段(De-allocated)。

內(nèi)存的分配實(shí)在創(chuàng)建階段,這個(gè)階段要先用類加載器加載目標(biāo)class,當(dāng)通過(guò)加載器檢測(cè)后,就開始為新對(duì)象分配內(nèi)存。對(duì)象分配內(nèi)存大小在類加載完成后便可以確定。
當(dāng)初始化完成后,虛擬機(jī)還要對(duì)對(duì)象進(jìn)行必要的設(shè)置,如那個(gè)類的實(shí)例,如何查找元數(shù)據(jù)、對(duì)象的GC年代等。

內(nèi)存的回收(GC)

那些不可能再被任何途徑使用的對(duì)象,需要被回收,否則內(nèi)存遲早都會(huì)被消耗空。

GC機(jī)制主要是通過(guò)可達(dá)性分析法,通過(guò)一系列稱為“GC Roots”的對(duì)象作為起始點(diǎn),從這些節(jié)點(diǎn)向下搜索,搜索所走過(guò)的路徑稱為引用鏈,當(dāng)一個(gè)對(duì)象到GC Roots沒(méi)有任何引用鏈時(shí),即GC Roots到對(duì)象不可達(dá),則證明此對(duì)象是不可達(dá)的。

根據(jù)《深入理解Java虛擬機(jī)》書中描述,可作為GC Root的地方如下:

  • 虛擬機(jī)棧(棧幀中的局部變量區(qū),也叫做局部變量表)中引用的對(duì)象。
  • 方法區(qū)中的類靜態(tài)屬性引用的對(duì)象。
  • 方法區(qū)中常量引用的對(duì)象。
  • 本地方法棧中JNI(Native方法)引用的對(duì)象。

當(dāng)一個(gè)對(duì)象或幾個(gè)相互引用的對(duì)象組沒(méi)有任何引用鏈時(shí),會(huì)被當(dāng)成垃圾處理,可以進(jìn)行回收。

如何一個(gè)對(duì)象在程序中已經(jīng)不再使用,但是(強(qiáng))引用還是會(huì)被其他對(duì)象持有,則稱為內(nèi)存泄漏。內(nèi)存泄漏并不會(huì)使程序馬上異常,但是多處的未處理的內(nèi)存泄漏則可能導(dǎo)致內(nèi)存溢出,造成不可預(yù)估的后果。

引用的分類

在JDK1.2之后,為了優(yōu)化內(nèi)存的利用及GC的效率,Java對(duì)引用的概念進(jìn)行了擴(kuò)充,將引用分為強(qiáng)引用、軟引用、弱引用、虛引用4種。

1、強(qiáng)引用,只要強(qiáng)引用還存在,垃圾收集器永遠(yuǎn)不會(huì)回收掉被引用的對(duì)象。

2、軟引用,在系統(tǒng)將要發(fā)生內(nèi)存溢出異常之前,將會(huì)把這些對(duì)象列進(jìn)回收范圍進(jìn)行二次回收。如果這次回收還沒(méi)有足夠的內(nèi)存,才會(huì)拋出內(nèi)存溢出異常。SoftReference表示軟引用。

3、弱引用,只要有GC,無(wú)論當(dāng)前內(nèi)存是否足夠,都會(huì)回收掉被弱引用關(guān)聯(lián)的對(duì)象。WeakReference表示弱引用。

4、虛引用,這個(gè)引用存在的唯一目的就是在這個(gè)對(duì)象被收集器回收時(shí)收到一個(gè)系統(tǒng)通知,被虛引用關(guān)聯(lián)的對(duì)象,和其生存時(shí)間完全沒(méi)關(guān)系。PhantomReference表示虛引用,需要搭配ReferenceQueue使用,檢測(cè)對(duì)象回收情況。

關(guān)于JVM內(nèi)存管理的一些建議

1、盡可能的手動(dòng)將無(wú)用對(duì)象置為null,加快內(nèi)存回收。
2、可考慮對(duì)象池技術(shù)生成可重用的對(duì)象,較少對(duì)象的生成。
3、合理利用四種引用。

三、內(nèi)存泄漏

持有一個(gè)生命周期較短的引用時(shí)或內(nèi)部的子模塊對(duì)象的生命周期超過(guò)了外面模塊的生命周期,即本該被回收的對(duì)象不能被回收而停留在堆內(nèi)存中,這就產(chǎn)生了內(nèi)存泄漏。

內(nèi)存泄漏是造成應(yīng)用程序OOM的主要原因之一,尤其在像安卓這樣的移動(dòng)平臺(tái),難免會(huì)導(dǎo)致應(yīng)用所需要的內(nèi)存超過(guò)系統(tǒng)分配的內(nèi)存限額,這就造成了內(nèi)存溢出Error。

安卓平臺(tái)常見的內(nèi)存泄漏

1、靜態(tài)成員變量持有外部(短周期臨時(shí))對(duì)象引用。 如單例類(類內(nèi)部靜態(tài)屬性)持有一個(gè)activity(或其他短周期對(duì)象)引用時(shí),導(dǎo)致被持有的對(duì)象內(nèi)存無(wú)法釋放。

2、內(nèi)部類。當(dāng)內(nèi)部類與外部類生命周期不一致時(shí),就會(huì)造成內(nèi)存泄漏。如非靜態(tài)內(nèi)部類創(chuàng)建靜態(tài)實(shí)例、Activity中的Handler或Thread等。

3、資源沒(méi)有及時(shí)關(guān)閉。如數(shù)據(jù)庫(kù)、IO流、Bitmap、注冊(cè)的相關(guān)服務(wù)、webview、動(dòng)畫等。

4、集合內(nèi)部Item沒(méi)有置空。

5、方法塊內(nèi)不使用的對(duì)象,沒(méi)有及時(shí)置空。

四、如何檢測(cè)內(nèi)存泄漏

Android Studio供了許多對(duì)App性能分析的工具,可以方便分析App性能。我們可以使用Memory Monitor和Heap Dump來(lái)觀察內(nèi)存的使用情況、使用Allocation Tracker來(lái)跟蹤內(nèi)存分配的情況,也可以通過(guò)這些工具來(lái)找到疑似發(fā)生內(nèi)存泄漏的位置。

堆存儲(chǔ)文件(hpof)可以使用DDMS或者M(jìn)emory Monitor來(lái)生成,輸出的文件格式為hpof,而MAT(Memory Analysis Tool)就是來(lái)分析堆存儲(chǔ)文件的。

然而MAT工具分析內(nèi)存問(wèn)題并不是一件容易的事情,需要一定的經(jīng)驗(yàn)區(qū)做引用鏈的分析,需要一定的門檻。
隨著安卓技術(shù)生態(tài)的發(fā)展,LeakCanary 開源項(xiàng)目誕生了,只要幾行代碼引入目標(biāo)項(xiàng)目,就可以自動(dòng)分析hpof文件,把內(nèi)存泄漏的地方展示出來(lái)。

五、LeakCanary原理解析

LeakCanary

A small leak will sink a great ship.

LeakCanary內(nèi)存檢測(cè)工具是由squar公司開源的著名項(xiàng)目,這里主要分析下源碼實(shí)現(xiàn)原理。

基本原理

主要是在Activity的&onDestroy方法中,手動(dòng)調(diào)用 GC,然后利用ReferenceQueue+WeakReference,來(lái)判斷是否有釋放不掉的引用,然后結(jié)合dump memory的hpof文件, 用HaHa分析出泄漏地方。

源碼分析

LeakCanary集成很方便,只要幾行代碼,所以可以從入口跟蹤代碼,分析原理

                if (!LeakCanary.isInAnalyzerProcess(WeiboApplication.this)) {
                    LeakCanary.install(WeiboApplication.this);
                }
                
                public static RefWatcher install(Application application) {
                      return ((AndroidRefWatcherBuilder)refWatcher(application)
                      .listenerServiceClass(DisplayLeakService.class).excludedRefs(AndroidExcludedRefs.createAppDefaults().build()))//配置監(jiān)聽器及分析數(shù)據(jù)格式
                      .buildAndInstall();
               }

從這里可看出,LeakCanary會(huì)單獨(dú)開一進(jìn)程,用來(lái)執(zhí)行分析任務(wù),和監(jiān)聽任務(wù)分開處理。

方法install中主要是構(gòu)造來(lái)一個(gè)RefWatcher,

   public RefWatcher buildAndInstall() {
        RefWatcher refWatcher = this.build();
        if(refWatcher != RefWatcher.DISABLED) {
            LeakCanary.enableDisplayLeakActivity(this.context);
            ActivityRefWatcher.install((Application)this.context, refWatcher);
        }

        return refWatcher;
    }
    
    public static void install(Application application, RefWatcher refWatcher) {
        (new ActivityRefWatcher(application, refWatcher)).watchActivities();
    }
    
    private final ActivityLifecycleCallbacks lifecycleCallbacks = new ActivityLifecycleCallbacks() {
        public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
        }
        public void onActivityStarted(Activity activity) {}
        public void onActivityResumed(Activity activity) {}
        public void onActivityPaused(Activity activity) {}
        public void onActivityStopped(Activity activity) { }
        public void onActivitySaveInstanceState(Activity activity, Bundle outState) {}

        public void onActivityDestroyed(Activity activity) {
            ActivityRefWatcher.this.onActivityDestroyed(activity);
        }
    };
    
    void onActivityDestroyed(Activity activity) {
        this.refWatcher.watch(activity);
    }

具體監(jiān)聽的原理在于 Application 的registerActivityLifecycleCallbacks方法,該方法可以對(duì)應(yīng)用內(nèi)所有 Activity 的生命周期做監(jiān)聽, LeakCanary只監(jiān)聽了Destroy方法。

在每個(gè)Activity的OnDestroy()方法中都會(huì)回調(diào)refWatcher.watch()方法,那我們找到的RefWatcher的實(shí)現(xiàn)類,看看具體做什么。

 public void watch(Object watchedReference, String referenceName) {
        if(this != DISABLED) {
            Preconditions.checkNotNull(watchedReference, "watchedReference");
            Preconditions.checkNotNull(referenceName, "referenceName");
            long watchStartNanoTime = System.nanoTime();
            String key = UUID.randomUUID().toString();//保證key的唯一性
            this.retainedKeys.add(key);
            KeyedWeakReference reference = new KeyedWeakReference(watchedReference, key, referenceName, this.queue);
            this.ensureGoneAsync(watchStartNanoTime, reference);
        }
    }
    
    
  final class KeyedWeakReference extends WeakReference<Object> {
    public final String key;
    public final String name;

    KeyedWeakReference(Object referent, String key, String name, ReferenceQueue<Object> referenceQueue) {//ReferenceQueue類監(jiān)聽回收情況
        super(Preconditions.checkNotNull(referent, "referent"), (ReferenceQueue)Preconditions.checkNotNull(referenceQueue, "referenceQueue"));
        this.key = (String)Preconditions.checkNotNull(key, "key");
        this.name = (String)Preconditions.checkNotNull(name, "name");
    }
  }

    private void ensureGoneAsync(final long watchStartNanoTime, final KeyedWeakReference reference) {
        this.watchExecutor.execute(new Retryable() {
            public Result run() {
                return RefWatcher.this.ensureGone(reference, watchStartNanoTime);
            }
        });
    }

KeyedWeakReference是WeakReference類的子類,用了 KeyedWeakReference(referent, key, name, ReferenceQueue<Object> )的構(gòu)造方法,將監(jiān)聽的對(duì)象(activity)引用傳遞進(jìn)來(lái),并且New出一個(gè)ReferenceQueue來(lái)監(jiān)聽GC后 的回收情況。

以下代碼ensureGone()方法就是LeakCanary進(jìn)行檢測(cè)回收的核心代碼:

    Result ensureGone(KeyedWeakReference reference, long watchStartNanoTime) {
        long gcStartNanoTime = System.nanoTime();
        long watchDurationMs = TimeUnit.NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);
        this.removeWeaklyReachableReferences();//先將引用嘗試從隊(duì)列中poll出來(lái)
        if(this.debuggerControl.isDebuggerAttached()) {//規(guī)避調(diào)試模式
            return Result.RETRY;
        } else if(this.gone(reference)) {//檢測(cè)是否已經(jīng)回收
            return Result.DONE;
        } else {
        //如果沒(méi)有被回收,則手動(dòng)GC
            this.gcTrigger.runGc();//手動(dòng)GC方法
            this.removeWeaklyReachableReferences();//再次嘗試poll,檢測(cè)是否被回收
            if(!this.gone(reference)) {
                // 還沒(méi)有被回收,則dump堆信息,調(diào)起分析進(jìn)程進(jìn)行分析
                long startDumpHeap = System.nanoTime();
                long gcDurationMs = TimeUnit.NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);
                File heapDumpFile = this.heapDumper.dumpHeap();
                if(heapDumpFile == HeapDumper.RETRY_LATER) {
                    return Result.RETRY;//需要重試
                }

                long heapDumpDurationMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);
                this.heapdumpListener.analyze(new HeapDump(heapDumpFile, reference.key, reference.name, this.excludedRefs, watchDurationMs, gcDurationMs, heapDumpDurationMs));
            }

            return Result.DONE;
        }
    }

    private boolean gone(KeyedWeakReference reference) {
        return !this.retainedKeys.contains(reference.key);
    }

    private void removeWeaklyReachableReferences() {
        KeyedWeakReference ref;
        while((ref = (KeyedWeakReference)this.queue.poll()) != null) {
            this.retainedKeys.remove(ref.key);
        }
    }

方法ensureGone中通過(guò)檢測(cè)referenceQueue隊(duì)列中的引用情況,來(lái)判斷回收情況,通過(guò)手動(dòng)GC來(lái)進(jìn)一步確認(rèn)回收情況。
整個(gè)過(guò)程肯定是個(gè)耗時(shí)卡UI的,整個(gè)過(guò)程會(huì)在WatchExecutor中執(zhí)行的,那WatchExecutor又是在哪里執(zhí)行的呢?

LeakCanary已經(jīng)利用Looper機(jī)制做了一定優(yōu)化,利用主線程空閑的時(shí)候執(zhí)行檢測(cè)任務(wù),這里找到WatchExecutor的實(shí)現(xiàn)類,研究下原理:

public final class AndroidWatchExecutor implements WatchExecutor {
    static final String LEAK_CANARY_THREAD_NAME = "LeakCanary-Heap-Dump";
    private final Handler mainHandler = new Handler(Looper.getMainLooper());
    private final Handler backgroundHandler;
    private final long initialDelayMillis;
    private final long maxBackoffFactor;

    public AndroidWatchExecutor(long initialDelayMillis) {
        HandlerThread handlerThread = new HandlerThread("LeakCanary-Heap-Dump");
        handlerThread.start();
        this.backgroundHandler = new Handler(handlerThread.getLooper());
        this.initialDelayMillis = initialDelayMillis;
        this.maxBackoffFactor = 9223372036854775807L / initialDelayMillis;
    }

    public void execute(Retryable retryable) {
        if(Looper.getMainLooper().getThread() == Thread.currentThread()) {
            this.waitForIdle(retryable, 0);//需要在主線程中檢測(cè)
        } else {
            this.postWaitForIdle(retryable, 0);//post到主線程
        }

    }

    void postWaitForIdle(final Retryable retryable, final int failedAttempts) {
        this.mainHandler.post(new Runnable() {
            public void run() {
                AndroidWatchExecutor.this.waitForIdle(retryable, failedAttempts);
            }
        });
    }

    void waitForIdle(final Retryable retryable, final int failedAttempts) {
        Looper.myQueue().addIdleHandler(new IdleHandler() {
            public boolean queueIdle() {
                AndroidWatchExecutor.this.postToBackgroundWithDelay(retryable, failedAttempts);//切換到子線程
                return false;
            }
        });
    }

    void postToBackgroundWithDelay(final Retryable retryable, final int failedAttempts) {
        long exponentialBackoffFactor = (long)Math.min(Math.pow(2.0D, (double)failedAttempts), (double)this.maxBackoffFactor);
        long delayMillis = this.initialDelayMillis * exponentialBackoffFactor;
        this.backgroundHandler.postDelayed(new Runnable() {
            public void run() {
                Result result = retryable.run();//RefWatcher.this.ensureGone(reference, watchStartNanoTime)執(zhí)行
                if(result == Result.RETRY) {
                    AndroidWatchExecutor.this.postWaitForIdle(retryable, failedAttempts + 1);
                }

            }
        }, delayMillis);
    }
}

這里用到了Handler相關(guān)知識(shí),Looper中的MessageQueue有個(gè)mIdleHandlers隊(duì)列,在獲取下個(gè)要執(zhí)行的Message時(shí),如果沒(méi)有發(fā)現(xiàn)可執(zhí)行的下個(gè)Msg,就會(huì)回調(diào)queueIdle()方法。

    Message next() {
        int pendingIdleHandlerCount = -1; // -1 only during first iteration
        int nextPollTimeoutMillis = 0;
        for (;;) {
                ···
                ···//省略部分消息查找代碼
                
                if (msg != null) {
                    if (now < msg.when) {
                        // Next message is not ready.  Set a timeout to wake up when it is ready.
                        nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                    } else {
                        ···
                        
                        return msg;
                    }
                } else {
                    // No more messages.
                    nextPollTimeoutMillis = -1;
                }

                
                // If first time idle, then get the number of idlers to run.
                // Idle handles only run if the queue is empty or if the first message
                // in the queue (possibly a barrier) is due to be handled in the future.
                if (pendingIdleHandlerCount < 0
                        && (mMessages == null || now < mMessages.when)) {
                    pendingIdleHandlerCount = mIdleHandlers.size();
                }
                if (pendingIdleHandlerCount <= 0) {
                    // No idle handlers to run.  Loop and wait some more.
                    mBlocked = true;
                    continue;
                }

                if (mPendingIdleHandlers == null) {
                    mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
                }
                mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
            }

            // Run the idle handlers.
            // We only ever reach this code block during the first iteration.
            for (int i = 0; i < pendingIdleHandlerCount; i++) {
                final IdleHandler idler = mPendingIdleHandlers[i];
                mPendingIdleHandlers[i] = null; // release the reference to the handler

                boolean keep = false;
                try {
                    keep = idler.queueIdle();
                } catch (Throwable t) {
                    Log.wtf(TAG, "IdleHandler threw exception", t);
                }

                if (!keep) {//返回false,則從隊(duì)列移除,下次空閑不會(huì)調(diào)用。
                    synchronized (this) {
                        mIdleHandlers.remove(idler);
                    }
                }
            }

            // Reset the idle handler count to 0 so we do not run them again.
            pendingIdleHandlerCount = 0;

            // While calling an idle handler, a new message could have been delivered
            // so go back and look again for a pending message without waiting.
            nextPollTimeoutMillis = 0;
        }
    }

其中的MessageQueue中加入一個(gè)IdleHandler,當(dāng)線程空閑時(shí),就會(huì)去調(diào)用queueIdle()函數(shù),如果返回值為True,那么后續(xù)空閑時(shí)會(huì)繼續(xù)的調(diào)用此函數(shù),否則不再調(diào)用;

知識(shí)點(diǎn)

1,用ActivityLifecycleCallbacks接口來(lái)檢測(cè)Activity生命周期
2,WeakReference + ReferenceQueue 來(lái)監(jiān)聽對(duì)象回收情況
3,Apolication中可通過(guò)processName判斷是否是任務(wù)執(zhí)行進(jìn)程
4,MessageQueue中加入一個(gè)IdleHandler來(lái)得到主線程空閑回調(diào)
5,LeakCanary檢測(cè)只針對(duì)Activiy里的相關(guān)對(duì)象。其他類無(wú)法使用,還得用MAT原始方法

六、總結(jié)

內(nèi)存相關(guān)的問(wèn)題基本問(wèn)題回顧了下,發(fā)現(xiàn)技術(shù)細(xì)節(jié)越扒越多。想要得到技術(shù)的提高,對(duì)這些技術(shù)細(xì)節(jié)的掌握是必要的,只有長(zhǎng)時(shí)間的積累扎實(shí)的技術(shù)細(xì)節(jié)基礎(chǔ),才能讓自己的技術(shù)走的更高。

基礎(chǔ)知識(shí)對(duì)每個(gè)工程師發(fā)展的不同階段意義不同,理解的角度和深度也不同。至少自己來(lái)看,基礎(chǔ)知識(shí)是永遠(yuǎn)值得學(xué)習(xí)和鞏固,來(lái)支撐技術(shù)的創(chuàng)新實(shí)踐。


歡迎轉(zhuǎn)載,請(qǐng)標(biāo)明出處:常興E站 canking.win

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,786評(píng)論 6 534
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,656評(píng)論 3 419
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,697評(píng)論 0 379
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,098評(píng)論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,855評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,254評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,322評(píng)論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,473評(píng)論 0 289
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,014評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,833評(píng)論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,016評(píng)論 1 371
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,568評(píng)論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,273評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,680評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,946評(píng)論 1 288
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,730評(píng)論 3 393
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,006評(píng)論 2 374

推薦閱讀更多精彩內(nèi)容