Android 中內存泄漏的原因分析及解決方案

Android內存管理

??內存管理的目的就是讓我們在開發中怎么避免我們的應用出現內存泄漏的問題。簡單的來說,就是該釋放的對象沒有釋放,一直被某個或某些實例持有卻不再使用導致GC不能回收。我會從Java內存泄漏的基礎知識開始,并通過具體的例子來說明Android引起內存泄漏的各種原因。

Java內存分配策略

??Java程序運行的內存分配策略有3種,分別是靜態分配、棧式分配、和堆式分配。對應的,3種存儲策略使用的內存空間分別是靜態存儲區(方法區)、棧區和堆區。

  • 靜態存儲區(方法區):主要存放靜態數據、全局static數據和常亮。這塊區域內存在編寫程序時已經分配好,并且在程序整個運行期間都存在.
  • 棧區: 當方法執行時,方法體內的局部變量(其中包括基礎數據類型、對象的引用)都在棧上創建,并在方法執行結束后時這些局部變量所持有的內存將會自動釋放。
  • 堆區:又稱動態內存分配,通常是指在程序運行時直接new出來的內存,也就是對象的實例。這部分內存在不使用時由Java垃圾回收器來負責回收。

堆和棧的區別

??在方法體定義的(局部變量)一些基本類型的變量和對象的引用都是在方法的堆內存中分配的,當在一段方法中定義一個變量時,Java就會在棧中分配空間,當超過該變量的作用域后,該變量也就無效了,分配給它的內存空間也將被釋放,該內存空間也可以重新被使用。
??堆內存中用來存放所有由new創建的對象(包括該對象所有的成員變量)和數組。在堆中分配的內存,將有Java垃圾回收器來自動管理。在堆中產生一個數組和對象后,還可以在棧中定義一個特殊的變量,這個變量的取值等于數組和對象在堆內存的首地址,這個特殊的變量就是我們常說的引用變量。我們可以通過這個引用變量來訪問堆中的對象或者數組。
舉個例子:

public class Sample {
    int s1 = 0;
    Sample mSample1 = new Sample();

    public void method() {
        int s2 = 1;
        Sample mSample2 = new Sample();
    }
}

Sample 類的局部變量 s2 和引用變量 mSample2 都是存在于棧中,但 mSample2 指向的對象是存在于堆上的。

Java如何管理內存的

??Java內存管理就是對象的分配和釋放問題。在java中,程序員通過關鍵字new為每個對象申請內存空間,所有對象在堆中分配空間。另外,對象的釋放有GC決定和執行的。
??為了更好理解GC的工作原理,我們可以將對象考慮為有向圖的頂點,將引用關系考慮為圖的有向邊,有向邊從引用者指向被引用對象。例如:大多數程序從main入口開始執行的,那么該圖就是以main進程頂點開始的一顆根樹,跟頂點到達的對象都是有效對象,GC將不回收這些對象。如果某個對象和跟頂點不可達,我們就認為這些對象不再被引用,可以被GC回收。(此時的obj2為可回收對象)


java.png

什么是Java中的內存泄漏

??在java中,內存泄漏的對象有兩個特點:1.這些對象是可達的,在有向圖中,存在通路可以相連,2.這些對象時無用的,程序以后不會再使用這些對象,如果對象滿足這兩個條件,這些對象可以認定Java中的內存泄漏,這些對象沒有被GC回收,但還占用著對象。
區別.png

Android中的內存泄漏

  • 集合類泄漏
    ??集合如果僅僅有添加元素的方法,而沒有相應的刪除機制,導致內存泄漏。如果這個集合類是全局性的變量(比如類中的靜態屬性),那么沒有相應的刪除機制,很可能導致集合所占用的內存只增不減。
  • 單例造成的內存泄漏
public class AppManager {

    private static AppManager mInstance;

    private Context context;

    private AppManager(Context context) {
        this.context = context;
    }

    public static AppManager getInstance(Context context) {
        if (mInstance == null) {
            mInstance = new AppManager(context);
        }
        return mInstance;
    }
}

這是個普通的單例模式,但是在創建單例時,需要傳入一個Context,所以這個Context的生命周期的長短至關重要。
(1)如果此時傳入的是Application的context,因為這個context對象是伴隨著整個應用的生命周期,所以這沒有任何問題。
(2)如果此時傳入的是Activity的context,當這個context對應的Activity被銷毀時,由于該context仍然被單例對象持有,所以這個Activity不會被回收,這就造成內存泄漏。

private AppManager(Context context) {
this.context = context.getApplicationContext();// 使用Application 的context
}

或者不用傳入context

private AppManager() {
this.context = MyApplication.getContext();// 使用Application 的context
}
  • 匿名內部類/非靜態內部類和異步線程
    有時候我們可能會在啟動頻繁的Activity中,為了避免創建相同的數據資源,可能會出現這種寫法:
public class SampleActivity extends AppCompatActivity {

    private static TestResource testResource;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (testResource == null) {
            testResource = new TestResource();
        }
    }

    /**
     * 非靜態內部類
     */
    class TestResource {

    }
}

這樣在Activity中創建了一個非靜態內部類,每次啟動Activity·都會使用該類的數據,雖然這樣避免了資源的重復創建,不過這種寫法會造成內存泄漏,因為非靜態內部類默認持有外部類,而該非靜態內部類又創建了一個靜態實例,該實例的生命周期和應用一樣長,這就導致了一直持有Activity的引用,導致Activity不能回收,正確的做法應該是把內部類設置為靜態或者將該內部類抽取出來封裝成一個單例。但是Application的context不是萬能的
context.png
  • 匿名內部類
 public class MainActivity extends Activity {

      @Override
      protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        exampleOne();
      }

      private void exampleOne() {
        new Thread() {
          @Override
          public void run() {
            while (true) {
              SystemClock.sleep(1000);
            }
          }
        }.start();
      }
    }

解決方法:

   public class MainActivity extends Activity {

      @Override
      protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        exampleTwo();
      }

      private void exampleTwo() {
        new MyThread().start();
      }

      private static class MyThread extends Thread {
        @Override
        public void run() {
          while (true) {
            SystemClock.sleep(1000);
          }
        }
      }
    }

通過上面的代碼,新線程再也不會持有一個外部 Activity 的隱式引用,而且該 Activity 也會在配置改變后被回收。

  • Handler造成的內存泄漏
public class  SampleActivity   extends AppCompatActivity {


    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            //TODO....
        }
    };

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                //TODO...
            }
        }, 1000 * 60 * 10);
        finish();
    }

}

在該SampleActivity 聲明一個延遲10分鐘后執行的消息,Hander的創建屬于非靜態內部類,持有Activity,當執行finish()方法后,Activity不會被回收,從而造成內存泄漏。

public class SampleActivity extends Activity {

  
  private static class MyHandler extends Handler {
    private final WeakReference<SampleActivity> mActivity;

    public MyHandler(SampleActivity activity) {
      mActivity = new WeakReference<SampleActivity>(activity);
    }

    @Override
    public void handleMessage(Message msg) {
      SampleActivity activity = mActivity.get();
      if (activity != null) {
        // ...
      }
    }
  }

  private final MyHandler mHandler = new MyHandler(this);

 
  private static final Runnable sRunnable = new Runnable() {
      @Override
      public void run() { /* ... */ }
  };

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

     
    mHandler.postDelayed(sRunnable, 1000 * 60 * 10);

 
    finish();
  }
}

這里使用靜態內部類+弱引用WeakReference 這種方式。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 前言 之前研究過一段時間關于 Android 內存泄漏的知識,大致了解了導致內存泄漏的一些原因,但是沒有深入去探究...
    Zackratos閱讀 19,290評論 19 49
  • Android 內存泄漏總結 內存管理的目的就是讓我們在開發中怎么有效的避免我們的應用出現內存泄漏的問題。內存泄漏...
    _痞子閱讀 1,656評論 0 8
  • 內存管理的目的就是讓我們在開發中怎么有效的避免我們的應用出現內存泄漏的問題。內存泄漏大家都不陌生了,簡單粗俗的講,...
    宇宙只有巴掌大閱讀 2,416評論 0 12
  • Android 內存泄漏總結 內存管理的目的就是讓我們在開發中怎么有效的避免我們的應用出現內存泄漏的問題。內存泄漏...
    apkcore閱讀 1,239評論 2 7
  • 內存管理的目的就是讓我們在開發中怎么有效的避免我們的應用出現內存泄漏的問題。內存泄漏大家都不陌生了,簡單粗俗的講,...
    DreamFish閱讀 803評論 0 5