1. 程式人生 > >你不必使用弱引用以避免記憶體洩漏

你不必使用弱引用以避免記憶體洩漏

我的一位同事最近提到,他們看到了一個演講,說:“如果你是一個Android開發者,你不使用使用弱引用,將會有問題。”
我個人認為,這不僅是一個錯誤的論點,而且完全是一個誤導。WeakReference應該是修復記憶體洩漏的最後手段。
下面是谷歌開發專家Enrique López Mañas釋出的文章:“
Finally understanding how references work in Android and Java
(英文不好,弱弱的依賴翻譯工具翻譯一下,正文如下)
幾個星期前,我參加了mobiconf,這是一個最好的移動開發者的會議之一,我有幸在波蘭參加。在他不拘一格的演講“The best (good) practices”期間,我的朋友和同事Jorge Barroso提出了讓我說說自己的想法關於:”如果你是一個Android開發者,你不使用使用弱引用,你將會有問題。”
就一個好時機的例子,幾個月前,我確實發表了我的最後一本書,和Diego Grancini一起合著的—–“Android High Performance(安卓高效能)“,其中最有激情的章節之一是一個談論在安卓系統的記憶體管理。在這一章中,我們討論了在移動裝置中的記憶體如何工作,記憶體洩漏是如何發生的,我們可以應用哪些技術來避免它們和為什麼避免記憶體洩漏如此重要。自從我開始開發android,我總是觀察到一種傾向,人們不知不覺的避免或把記憶體洩漏和記憶體管理相關的處理的優先順序低於其它一切。如果功能標準得到滿足,為什麼要自增煩惱呢?我們總是在匆忙開發新的功能,我們寧願在我們的下一個Sprint demo 演示視覺可見的東西,而不是關心沒有人會在第一眼看到的東西。
一個很好的論點,這不可避免地導致收購技術債務。我甚至會補充說,技術債務也有一定的影響,在現實世界中我們不能用單位測試衡量:失望,在同開發商之間的摩擦,低質量的軟體運和損失的動機。這種影響是很難衡量的原因是因為通常他們發生在一個長的週期內。它的發生有點像政治家:如果我只會任期8年,為什麼我要擔心在12後發生了什麼事?不同的是在軟體開發中一切動作速度更快一些。
要完全寫出在軟體開發中採用適當的和正確的思維方式,可能需要寫很多,而且已經有許多你可以探索書籍和文章了。然而,簡要闡述的記憶體引用的不同型別,它們的意義是什麼,他們如何能被應用在Android將是一個簡短的任務,這就是我想做的文章。

First of all: what is a reference in Java?

A reference is the direction of an object that is annotated, so you
can access it.

Java has by default 4 types of references: strong, soft, weak and phantom.

有些人認為,只有兩種型別的引用,強和弱,弱引用可以呈現2個程度的弱引用。我們傾向於把生活中的一切都與一個植物學家的分類的毅力。無論它對你的工作更好,但首先你需要了解他們。然後你可以找到你自己的分類。

What does each type of reference mean?(每種引用型別意味著什麼?)
Strong reference: 強引用是java的普通引用。任何時候我們建立一個新的物件,一個強引用預設建立。例如,當我們這樣做:

MyObject object = new MyObject();

一個新的物件是MyObject被創造,則一個它的強引用被儲存在物件中。此物件是 strongly reachable ,意味著它可以通過一個強引用到達,這將防止垃圾回收器銷燬它。但是現在我們來看一個反對我們觀點的例子:

public class MainActivity
extends Activity {
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); new MyAsyncTask().execute(); } private class MyAsyncTask extends AsyncTask { @Override protected Object doInBackground(Object[] params) { return doSomeStuff(); } private Object doSomeStuff() { //do something to get result return new MyObject(); } } }

AsyncTask將在oncreate()方法建立和執行。但在這裡,我們有一個問題:內部類需要訪問外部類在其整個生命週期。當activity被銷燬時會發生什麼?AsyncTask擁有activity的引用,以至於activity不能被GC回收。這就是我們所說的記憶體洩漏。

實際上,記憶體洩漏不僅發生在activity本身被銷燬時,而且發生在由於配置改變或記憶體不足而被系統強制銷燬時,等等。如果AsyncTask是複雜的(即擁有activity中view的引用等)甚至可能導致宕機,因為view引用為null。

那麼如何才能防止這個問題再次發生呢?我們來看一下其它型別的引用:
WeakReference: 弱引用是一個沒有足夠強大保持物件一直存在記憶體中的引用。如果我們不確定是否對一個物件使用強引用,這時就出現了弱引用,該物件將被垃圾回收器收集。為了便於理解,最好是拋開理論用一個實際的例子,用WeakReference去改寫我們以前的程式碼來避免記憶體洩漏:

public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        new MyAsyncTask(this).execute();
    }
    private static class MyAsyncTask extends AsyncTask {
        private WeakReference<MainActivity> mainActivity;    

        public MyAsyncTask(MainActivity mainActivity) {   
            this.mainActivity = new WeakReference<>(mainActivity);            
        }
        @Override
        protected Object doInBackground(Object[] params) {
            return doSomeStuff();
        }
        private Object doSomeStuff() {
            //do something to get result
            return new Object();
        }
        @Override
        protected void onPostExecute(Object object) {
            super.onPostExecute(object);
            if (mainActivity.get() != null){
                //adapt contents
            }
        }
    }
}

請注意一個主要的區別:內部類的activity的引用現在如下:

private WeakReference<MainActivity> mainActivity;

這樣會發生什麼?當activity不再存在,因為它是通過一個WeakReference的方式,它可以被垃圾回收器回收。因此,沒有記憶體洩漏會發生。

現在,我希望你對使用弱引用有更進一步的瞭解了,你會發現一個有用的類WeakHashMap。它如同一個HashMap,除了鑰匙(key,不是values)是指通過使用弱引用。這使他們非常有用,如快取記憶體。

我們之前提到了更多的引用。讓我們看看它們都用在哪裡,有什麼優點:
SoftReference:我們可以把軟引用看作為一個稍強一點的WeakReference。而WeakReference會立即被收集,軟引用會請求GC儘可能不回收它除非沒有其他的選擇。垃圾收集器演算法是個很令人興奮的東西,你可以潛心研究它幾個小時不覺得累。但基本上,垃圾回收器會說,“我將一直收回WeakReference。如果物件是一個軟引用,我會根據系統條件決定去怎麼處理”。這使得軟引用在快取實現時非常有用:只要記憶體足夠,我們不必手動刪除物件。為了瞭解軟引用,下面是另一個blog中的例子
《《《《java中的每個物件都有對其他物件的引用。只要一個物件引用了它,則該物件將不會被回收。當應用程式記憶體溢位而且GC無法刪除那些沒有真正引用的物件來釋放記憶體,只有softrefences將被刪除。所以, 基本 ,只有軟引用它將 專案保持 在記憶體儘可能長的時間,直到沒有其他的方法來釋放記憶體。
建立一個軟引用一個物件,你可以使用下面的方式:

SoftReference<String> ref = new SoftReference<String>("Hello world");

檢索資料:

String value = ref.get();

if (value == null) {
  // The reference is cleaned up by the Garbage Collector.
  // Initialize the object again.
}

// Use the value (from the SoftReference or from re-initializing.
.......

SoftReferenceCache
我們如何建立一個快取使用softreferences?我們將做一個HashMap和java泛型。
首先我們從一個叫 softreferencecache會接收兩個泛型引數的類開始。一個key一個value,在建構函式中我們將建立HashMap。

public class SoftReferenceCache<K, V> {
  private final HashMap<K, SoftReference<V>> mCache;

  public SoftReferenceCache() {
    mCache = new HashMap<K, SoftReference<V>>();
  }
}

向快取中新增新的鍵值對的方法

public void put(K key, V value) {
  mCache.put(key, new SoftReference<V>(value));
}

根據key 取value的方法

public V get(K key) {
  V value = null;

  SoftReference<V> reference = mCache.get(key);

  if (reference != null) {
    value = reference.get();
  }

  return value;
}

使用這個快取類如下

SoftReferenceCache<Integer, Person> mPersonCache = new SoftReferenceCache<Integer, Person>();

mPersonCache.put(0, new Person("Peter");
mPersonCache.put(1, new Person("Jan");
mPersonCahce.put(2, new Person("Kees");

Person p = (Person)mPersonCache.get(1); // This will retrieve the Person object of Jan.
/**
 * SoftRefenceCache
 * @param <K> The type of the key's.
 * @param <V> The type of the value's.
 */
public class SoftReferenceCache<K, V> {
  private final HashMap<K, SoftReference<V>> mCache;

  public SoftReferenceCache() {
    mCache = new HashMap<K, SoftReference<V>>();
  }

  /**
   * Put a new item in the cache. This item can be gone after a GC run.
   * 
   * @param key
   *            The key of the value.
   * @param value
   *            The value to store.
   */
  public void put(K key, V value) {
    mCache.put(key, new SoftReference<V>(value));
  }

  /**
   * Retrieve a value from the cache (if available).
   * 
   * @param key
   *            The key to look for.
   * @return The value if it's found. Return null if the key-value pair is not
   *         stored yet or the GC has removed the value from memory.
   */
  public V get(K key) {
    V value = null;

    SoftReference<V> reference = mCache.get(key);

    if (reference != null) {
      value = reference.get();
    }

    return value;
  }
}

》》》》》》》》》》》》》》》》》》》》》》》》》》》

回到正題,接下來是

PhantomReference:哎,phantomreferences!我想我可以用手指頭就可以數的過來他們在生產環境中使用的次數。一個物件如果只通過phantomreference引用,那麼不管何時垃圾回收器都可以回收它。那我們為什麼還要用它呢?phantomreference可準確檢測一個物件是否被從記憶體中移除。

這是非常好的文章,用例子講解了java中引用是如何工作的。文章沒有說,我們必須使用WeakReference但也沒有給其它選擇。我覺得我必須給出其它選擇來證明不是必須使用WeakReference。

我相信使用WeakReference並不是在每個地方都是最佳選擇。使用一個WeakReference修復記憶體洩漏表明缺乏建模或建築。雖然在文章中給出的例子修復了潛在的記憶體洩漏,但還有其他方式。我將給出幾個示例實現,以避免當您有一個長時間執行的後臺任務時記憶體洩漏。

一個簡單的例子來避免記憶體洩漏的AsyncTask會是這樣的:

以下是我的 Activity:

public class MainActivity extends Activity {
  private MyAsyncTask task;
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    task = new MyAsyncTask();
    task.setListener(createListener());
    task.execute();
  }
  @Override
  protected void onDestroy() {
    task.setListener(null);
    super.onDestroy();
  }
  private MyAsyncTask.Listener createListener() {
    return new MyAsyncTask.Listener() {
      @Override
      public void onSuccess(Object object) {
        // adapt contents
      }
    };
  }
}

以下是我的 AsyncTask:

class MyAsyncTask extends AsyncTask {
  private Listener listener;
  @Override
  protected Object doInBackground(Object[] params) {
    return doSomeStuff();
  }
  private Object doSomeStuff() {
    //do something to get result
    return new Object();
  }
  @Override
  protected void onPostExecute(Object object) {
    if (listener != null) {
      listener.onSuccess(object);
    }
  }
  public void setListener(Listener listener) {
    this.listener = listener;
  }
  interface Listener {
    void onSuccess(Object object);
  }
}

就是以介面的方式來回調,這種實現是非常基本的,但我認為這是不夠的,以展示另一種解決方案。

以下是一個基於rxjava 非常簡單的實現,當我們沒有WeakReference時:

public class MainActivity extends Activity {

  private Subscription subscription;

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

    subscription = Observable
        .fromCallable(new Callable<Object>() {
          @Override
          public Object call() throws Exception {
            return doSomeStuff();
          }
        })
        .subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe(new Action1<Object>() {
          @Override
          public void call(Object o) {
            // adapt contents
          }
        });
  }

  private Object doSomeStuff() {
    //do something to get result
    return new Object();
  }

  @Override
  protected void onDestroy() {
    subscription.unsubscribe();
    super.onDestroy();
  }

}

請注意,如果我們不取消訂閱,我們可能仍然有記憶體洩漏。

最後我想給幾個來自Novoda的工程例項。他們是偉大的學習資源。你能猜到的,他們沒有任何WeakReference :)

第一個例項

第二個例項

我相信經驗法則(對我來說)當使用內部類時要使用靜態內部類。特別是如果他們在後臺做長時間執行的話。可以的話,我們儘量把這些類變成了一個完整的類。