天天看點

Android記憶體優化總結

1.記憶體洩漏

一個長生命周期的對象持有一個短生命周期對象的引用,通俗講就是該回收的對象,因為引用問題沒有被回收,最終會産生OOM。

1.1 非業務需要不要把activity的上下文做參數傳遞,可以傳遞application的上下文

  • 因為Application的生命周期等于整個應用的生命周期,非必須的情況下用Application的上下文可以避免發生記憶體洩露

1.2 和Activity有關聯的對象不要寫成static

  • static修飾的成員變量的生命周期等于應用程式的生命周期,不要使用static修飾符Context或者ui控件等等

1.3 非靜态内部類和匿名内部類會持有activity引用

  • 非靜态内部類和匿名内部類預設持有外部類的引用,經常會引起記憶體洩漏的情況有三種:
1.3.1 非靜态内部類的執行個體的引用被設定為靜态

非靜态内部類所建立的執行個體為靜态(其生命周期等于應用的生命周期),會因非靜态内部類預設持有外部類的引用而導緻外部類無法釋放,最終造成記憶體洩露,如下

public class TestActivity extends AppCompatActivity {  
    
    // 非靜态内部類的執行個體的引用設定為靜态  
    public static InnerClass innerClass = null; 
   
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {        
        super.onCreate(savedInstanceState);   
        // 保證非靜态内部類的執行個體隻有1個
        if (innerClass == null)
            innerClass = new InnerClass();
    }

    // 非靜态内部類的定義    
    private class InnerClass {        
        //...
    }
}
           

解決方法

  • 将非靜态内部類設定為靜态内部類(靜态内部類預設不持有外部類的引用)
  • 盡量建立一個檔案定義類
  • 避免非靜态内部類所建立的執行個體為靜态
1.3.2 使用非靜态内部類和匿名内部類的方式實作多線程,例如Thread、AsyncTask、Timer

正如使用内部類一樣,隻要不跨越生命周期,内部類是完全沒問題的。但是,這些類是用于産生背景線程的,這些Java線程是全局的,而且持有建立者的引用(即匿名類的引用),而匿名類又持有外部類的引用。線程是可能長時間運作的,是以一直持有Activity的引用導緻當銷毀時無法回收。如下所示

void startAsyncTask() {
    new AsyncTask<Void, Void, Void>() {
        @Override protected Void doInBackground(Void... params) {
            while(true);
        }
    }.execute();
}

void scheduleTimer() {
    new Timer().schedule(new TimerTask() {
        @Override
        public void run() {
            while(true);
        }
    }, Long.MAX_VALUE >> 1);
}
           

解決方法

  • 使用靜态内部類
private static class NimbleTask extends AsyncTask<Void, Void, Void> {
    @Override protected Void doInBackground(Void... params) {
        while(true);
    }
}

void startAsyncTask() {
    new NimbleTask().execute();
}

private static class NimbleTimerTask extends TimerTask {
    @Override public void run() {
        while(true);
    }
}

void scheduleTimer() {
    new Timer().schedule(new NimbleTimerTask(), Long.MAX_VALUE >> 1);
}
           
  • 在生命周期結束時中斷線程
private Thread thread;

@Override
public void onDestroy() {
    super.onDestroy();
    if (thread != null) {
        thread.interrupt();
    }
}

void spawnThread() {
    thread = new Thread() {
        @Override public void run() {
            while (!isInterrupted()) {
            }
        }
    }
    thread.start();
}
           
1.3.3 Handle的使用問題

Handler的兩種用法 内部類和匿名内部類預設持有外部類引用,在Handler消息隊列還有未處理的消息或正在處理消息時,此時若需銷毀外部類Activity,但由于消息隊列中的Message持有Handler執行個體的引用,垃圾回收器(GC)無法回收Activity,進而造成記憶體洩漏

/** 
     * 方式1:建立Handle子類(内部類)
     */  
            class FHandler extends Handler {
                @Override
                public void handleMessage(Message msg) {
                    switch (msg.what) {
                        case 1:
                            break;
                    }
                }
   /** 
     * 方式2:匿名Handle内部類
     */ 
  		Handler handler = new  Handler(){
                @Override
                public void handleMessage(Message msg) {
                        switch (msg.what) {
                            case 1:
                                break;
                        }
                    }
            };
           

解決方法

  • 使用靜态内部類,同時還可以使用WeakReference弱引用持有Activity執行個體,斷開Message消息 -> Handler執行個體 -> 外部類 的引用關系
private static class FHandler extends Handler{

        // 定義 弱引用執行個體
        private WeakReference<Activity> reference;

        // 在構造方法中傳入需持有的Activity執行個體
        public FHandler(Activity activity) {
            // 使用WeakReference弱引用持有Activity執行個體
            reference = new WeakReference<Activity>(activity); 
        }

        // 通過複寫handlerMessage() 進而确定更新UI的操作
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case 1:
                    break;
            }
        }
    }
           
  • 當外部類結束生命周期時,清空Handler内消息隊列
@Override
    protected void onDestroy() {
        super.onDestroy();
        mHandler.removeCallbacksAndMessages(null);
    }
           

1.4 單例模式持有activity引用

單例模式由于其靜态特性,其生命周期的長度等于應用程式的生命周期。若一個對象已不需再使用而單例對象還持有該對象的引用,那麼該對象将不能被正常回收進而導緻記憶體洩漏,例如:

//在使用單例setCallback的地方,callback持有外部類引用
public class Singleton {
    private static Singleton singleton;
    private Callback callback;

    public static Singleton getInstance(){
        if(singleton==null){
            singleton=new Singleton();
        }
        return singleton;
    }
      private Singleton () {     }  
  
    public void setCallback(Callback callback){
        this.callback=callback;
    }
    public Callback getCallback(){
        return callback.get();
    }
    public interface Callback{
        void callback();
    }
}

           
//若傳入的是Activity的Context,此時單例 則持有該Activity的引用
public class Singleton {    
    private static Singleton instance;    
    private Context mContext;    
    private Singleton (Context context) {        
        this.mContext = context; // 傳遞的是Activity的context
    }  
  
    public Singleton getInstance(Context context) {        
        if (instance == null) {
            instance = new Singleton(context);
        }        
        return instance;
    }
}
           

解決方法

  • 單例中成員變量使用弱引用,例如 private WeakReference callback
  • 需要Context時傳遞Application的Context,因Application的生命周期等于整個應用的生命周期

1.5 WebView記憶體洩漏

我們都知道,對應 WebView 來說,其 網絡延時、引擎 Session 管理、Cookies 管理、引擎核心線程、HTML5 調用系統聲音、視訊播放元件等産生的引用鍊條無法及時打斷,造成的記憶體問題基本上可以用”無解“來形容。

解決方案是我們可以 把 WebView 裝入另一個程序,使用AIDL與應用的主程序進行通信。具體為在 AndroidManifes 中對目前的 Activity 設定 android:process 屬性即可,最後,在 Activity 的 onDestory 中退出程序,這樣即可基本上終結 WebView 造成的洩漏。

1.6 資源性對象未關閉

對于資源性對象不再使用時,應該立即調用它的close()函數,将其關閉,然後再置為null。例如Bitmap等資源未關閉會造成記憶體洩漏,此時我們應該在Activity銷毀時及時關閉。

1.7 注冊對象未登出

例如BraodcastReceiver、EventBus未登出造成的記憶體洩漏,我們應該在Activity銷毀時及時登出。

2.記憶體抖動

什麼是記憶體抖動呢?

Android裡記憶體抖動是指記憶體頻繁地配置設定和回收,而頻繁的gc會導緻卡頓,嚴重時還會導緻OOM。一個很經典的案例是string拼接建立大量小的對象(比如在一些頻繁調用的地方打字元串拼接的log的時候)

而記憶體抖動為什麼會引起OOM呢?

主要原因還是有因為大量小的對象頻繁建立,導緻記憶體碎片,進而當需要配置設定記憶體時,雖然總體上還是有剩餘記憶體可配置設定,而由于這些記憶體不連續,導緻無法配置設定,系統直接就傳回OOM了。

2.1 字元串少用加号拼接

  • 使用StringBuilder和StringBuffer代替字元串加号拼接

2.2 記憶體重複申請的問題

  • 不要在頻繁調用的方法中new對象,例如遞歸函數 ,回調函數,流的循環讀取,自定義View的方法等。
  • 自定義View不要在onMeause() onLayout() onDraw() 中去重新整理UI(requestLayout)

2.3 避免GC回收将來要複用的對象

對于能夠複用的對象,可以使用對象池+LRU算法将它們緩存起來。

public abstract class ObjectPool<T> {
    //空閑池,使用者從這個裡面拿對象
    private SparseArray<T> freePool;
    //正在使用池,使用者正在使用的對象放在這個池記錄
    private SparseArray<T> lentPool;

    //池的最大值
    private int maxCapacity;

    public ObjectPool(int initialCapacity, int maxCapacity) {
        //初始化對象池
        initalize(initialCapacity);
        this.maxCapacity=maxCapacity;
    }

    private void initalize(int initialCapacity) {
        lentPool=new SparseArray<>();
        freePool=new SparseArray<>();
        for(int i=0;i<initialCapacity;i++){
            freePool.put(i,create());
        }
    }

    /**
     * 申請對象
     * @return
     */
    public T acquire() throws Exception {

        T t=null;
        synchronized (freePool){
            int freeSize=freePool.size();
            for(int i=0;i<freeSize;i++){
                int key=freePool.keyAt(i);
                t=freePool.get(key);
                if(t!=null){
                    this.lentPool.put(key,t);
                    this.freePool.remove(key);
                    return t;
                }
            }
            //如果沒對象可取了
            if(t==null && lentPool.size()+freeSize<maxCapacity){
                //這裡可以自己處理,超過大小
                if(lentPool.size()+freeSize==maxCapacity){
                    throw new Exception();
                }
                t=create();
                lentPool.put(lentPool.size()+freeSize,t);
            }
        }
        return t;
    }

    /**
     * 回收對象
     * @return
     */
    public void release(T t){
        if(t==null){
            return;
        }
        int key=lentPool.indexOfValue(t);
        //釋放前可以把這個對象交給使用者處理
        restore(t);
        this.freePool.put(key,t);
        this.lentPool.remove(key);
    }

    protected  void restore(T t){
    };

    protected abstract T create();

    public ObjectPool(int maxCapacity) {
        this(maxCapacity/2,maxCapacity);
    }
}
           

3.優化記憶體的良好習慣

3.1 static和static final

static String strVal = "Hello, world!";
           

編譯器會在類首次被使用到的時候,使用初始化方法來初始化上面的值,之後通路的時候會需要先到它那裡查找,然後才傳回資料。我們可以使用static final來提升性能:

static final String strVal = "Hello, world!";
           

這時再也不需要上面的那個方法來做多餘的查找動作了。是以,請盡可能的為常量聲明為static final類型的。

3.2 資料類型選擇

不要使用比需求更占空間的基本資料類型

  • 條件允許下,盡量避免使用float類型,Android系統中float類型的資料存取速度是int類型的一半,盡量優先采用int類型。

3.3 循環用foreach少用iterator

3.4 使用SparseArray和Arraymap代替HashMap

利用Android Framework裡面優化過的容器類,例如SparseArray, SparseBooleanArray, 與 LongSparseArray。 通常的HashMap的實作方式更加消耗記憶體,因為它需要一個額外的執行個體對象來記錄Mapping操作。另外,SparseArray更加高效在于他們避免了對key與value的autobox自動裝箱,并且避免了裝箱後的解箱。

是以資料在千級以内

  • 如果key的類型已經确定為int類型,那麼使用SparseArray,因為它避免了自動裝箱的過程,如果key為long類型,它還提供了一個LongSparseArray來確定key為long類型時的使用
  • 如果key類型為其它的類型,則使用ArrayMap

3.5 盡量少使用枚舉

每一個枚舉值都是一個單例對象,在使用它時會增加額外的記憶體消耗,是以枚舉相比與 Integer 和 String 會占用更多的記憶體,較多的使用 Enum 會增加 DEX 檔案的大小,會造成運作時更多的IO開銷,使我們的應用需要更多的空間,特别是分dex多的大型APP,枚舉的初始化很容易導緻ANR

可以使用自定義注解實作類似效果,例如

public class SHAPE {
    public static final int RECTANGLE=0;
    public static final int TRIANGLE=1;
    public static final int SQUARE=2;
    public static final int CIRCLE=3;

    @IntDef(flag=true,value={RECTANGLE,TRIANGLE,SQUARE,CIRCLE})
    @Target({ElementType.PARAMETER,ElementType.METHOD,ElementType.FIELD})
    @Retention(RetentionPolicy.SOURCE)
    public @interface Model{
    }
    
    private @Model int value=RECTANGLE;
    public void setShape(@Model int value){
        this.value=value;
    }
    @Model
    public int getShape(){
        return this.value;
    }
}

           

3.6 盡量使用IntentService而不是Service

如果應用程式當中需要使用Service來執行背景任務的話,請一定要注意隻有當任務正在執行的時候才應該讓Service運作起來。另外,當任務執行完之後去停止Service的時候,要小心Service停止失敗導緻記憶體洩漏的情況。

當我們啟動一個Service時,系統會傾向于将這個Service所依賴的程序進行保留,這樣就會導緻這個程序變得非常消耗記憶體。并且,系統可以在LRU cache當中緩存的程序數量也會減少,導緻切換應用程式的時候耗費更多性能。嚴重的話,甚至有可能會導緻崩潰,因為系統在記憶體非常吃緊的時候可能已無法維護所有正在運作的Service所依賴的程序了。

為了能夠控制Service的生命周期,Android官方推薦的最佳解決方案就是使用IntentService,這種Service的最大特點就是當背景任務執行結束後會自動停止,進而極大程度上避免了Service記憶體洩漏的可能性。

3.7 在合适的時候适當采用軟引用和弱引用。

4.Bitmap中優化

可以參考另一篇文章:

Bitmap記憶體優化

繼續閱讀