1. 程式人生 > >Android中物件池的使用

Android中物件池的使用

物件池的使用

Android開發中經常會發現當日志中出現大量GC時我們的應用常常出現卡頓。這是因為當虛擬機器進行垃圾回收操作時應用所有執行緒都會暫停,完成後恢復。如果出現大量GC操作時主執行緒頻繁暫停就會影響應用效能了。所以我們在開發中要儘量避免。

瞭解Android 垃圾回收

Android裡面是一個三級Generation的記憶體模型,最近分配的物件會存放在Young Generation區域,當這個物件在這個區域停留的時間達到一定程度,它會被移動到Old Generation,最後到Permanent Generation區域。每一個級別的記憶體區域都有固定的大小,此後不斷有新的物件被分配到此區域,當這些物件總的大小快達到這一級別記憶體區域的閥值時,會觸發GC的操作,以便騰出空間來存放其他新的物件。每次GC發生的時候,所有的執行緒都是暫停狀態的。GC所佔用的時間和它是哪一個Generation也有關係,Young Generation的每次GC操作時間是最短的,Old Generation其次,Permanent Generation最長。

導致GC頻繁執行有兩個原因:

Memory Churn記憶體抖動,記憶體抖動是因為大量的物件被建立又在短時間內馬上被釋放。
瞬間產生大量的物件會嚴重佔用Young Generation的記憶體區域,當達到閥值,剩餘空間不夠的時候,也會觸發GC。即使每次分配的物件佔用了很少的記憶體,但是他們疊加在一起會增加Heap的壓力,從而觸發更多其他型別的GC。這個操作有可能會影響到幀率,並使得使用者感知到效能問題。

如何避免

根據上面GC頻繁原因我們可以得出一個簡單結論,那就是我們的程式碼中在卡頓那個操作中進行了大量的物件建立。當然這個還可以通過 Android studio的 Memory Monitor 記憶體浮動觀察到;也可以通過Allocation Tracker來跟蹤問題出現的位置。但是我認為直接去看卡頓操作部分對應的程式碼,應該很容易發現。

如何解決

回到主題,如果我們發現了大量物件的建立該如何處理呢?
可以優化就優化,比如在onDraw中初始化了一些物件,我們可以考慮是否可以將這些物件初始化到外部(比如構造方法),而不要在檢視繪製需要反覆呼叫的方法中去new
不能優化的採用物件池解決,如果我們這些物件的初始化不可避免,那麼我們要考慮物件的複用,採用物件池來解決

物件池

我們在Android開發中其實可能已經使用過,只是我們沒用關注而已。比如在handler傳送訊息時,Message的初始化經常會用Message.obtain()來例項化Message物件;在View自定義中用到手勢速度控制的VelocityTracker。根據原始碼雖然兩者對實現方式不同(Message使用連結串列、VelocityTracker使用陣列),但是原理是一樣的。即:

1.  初始化一個固定大小池子,我們每次建立物件時候先去池子中找有沒有,
2.  如果有直接取出,沒有new出來使用後還到池子裡。這樣便可達到物件複用的目的
  • 1
  • 2
save_snippets.png
  • 1
  • 2

使用物件池的代價以及注意事項

當然使用物件池也是要有一定代價的:

短時間內生成了大量的物件佔滿了池子,那麼後續的物件是不能複用的
物件池是靜態的,如果池子被佔滿,當我們離開該頁面這些物件可能不再需要,那麼池子不釋放其中的無用物件還是要佔用一定的記憶體空間*

注意事項:

使用時候申請(obtain)和釋放(recycle)成對出現,使用一個物件後一定要釋放還給池子
池子的大小要根據實際情況合理指定。池子太大上面提到的不釋放而佔用的記憶體會很大,池子太小物件過多而且因為操作耗而不能立即釋放還給池子時候,池子滿了,後續物件還是不能複用。所以,根據專案實際場景制定合理的大小是很必要的
物件池的建立方法
有很多方法都可以實現,比如Message的連結串列、或者自己實現都可以,但是為了簡便這裡只說一種最簡便方法。採用Android的SynchronizedPool,以一個User的物件池為例

    public class User {

    public String id;

      public String name;

      private static final SynchronizedPool<User> sPool = new SynchronizedPool<User>(
          10);

      public static User obtain() {
      User instance = sPool.acquire();
      return (instance != null) ? instance : new User();
      }

      public void recycle() {
          sPool.release(this);
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
save_snippets.png
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

我們在申請例項化時呼叫

//從物件池中獲取,第一次物件池沒有,會直接new一個,如果有會複用
User user = User.obtain();