Android 性能優化 ~ 內存篇

一、android官方一些內存方面的內存tips

1、避免創建不必要的對象。

  • 如儘量避免字符串的加號拼接,可以使用StringBuilder來拼接。
  • 如果需要TextView設置多個字符串片段,可以使用textView.append方法,不要直接用加號拼起來。

2、儘量使用for-each循環,對於ArrayList,請使用普通的for,如:

int len = list.size();
for (int i = 0; i < len; ++i) {
    //todo somgthing
}

3、使用系統庫函數。

  • 使用系統庫函數,並且還有彙編級別的優化,他們通常比帶有JIT的Java編譯出來的代碼更高效 如:System.arraycopy(); String.indexOf()等。
  • 如果系統函數能夠解決的,不要加入第三方庫,可能第三方庫使用起來比較簡單(jsoup,htmlparser等)。如解析HTML可以使用系統的XmlPullParser

二、使用 ArrayMap、SparseArray代替HashMap

ArrayMap 和 HashMap的在內存的使用上,更加高效。

ArrayMap實現上有兩個數組,一個數組是保存key hash,另一個數組保存value,ArrayMap通過二分法(binary search)來進行查找的。

HashMap通過一個數組來實現的,key hash作爲數組的索引,這樣就需要更大的內存來減少key hash的衝突,key hash就是數組的索引,所以查找效率很高。

使用建議:

  • 當數據量比較小的時候(小於1000),優先使用ArrayMap,否則使用HashMap。

  • map裏嵌套map。

使用方法:

  • ArrayMap和HashMap的使用方法都是一樣的,ArrayMap也實現了Map接口。

  • 另外,ArrayMap可以通過keyAt(index)方法來獲取第index位置的key,keyValue(int index)同理。但是HashMap是不可以的。

  arrayMap.keyAt(0);
  arrayMap.valueAt(0);

SparseArray和ArrayMap非常像,它們都是通過兩種緊密包裝的數組,而不是一個大的哈希散列,從而減少了整個內存的覆蓋區。但是查詢的速度就慢了。

只不過SparseArray和ArrayMap最大的區別是SparseArray的key是一個基本類型。

SparseArray的key是int類型,而不是Integer。像以前使用HashMap的時候,如果key是整形,必須是Integer。

Integer佔16個字節,int只佔4個字節,如果元素比較多,從而可以很好的減少內存的佔用。

除了SparseArray類還有如下類可供使用:

SparseBooleanMap <boolean,Object>
SparseIntMap <int,Object>
SparseLongMap <long,Object>

SparseArray和ArrayMap的使用建議使用方法都是一樣的。

三、Thread與Thread Pool

在android開發中,一些耗時的操作都會放到後臺線程去執行,比如:網絡、本地文件、數據庫等。

    new Thread(new Runnable() {
        @Override
        public void run() {
            //do something...
        }
    }).start();

每個線程至少消耗64k的內存,如果你在某個時間點,迅速開啓了很多線程(比如加載列表圖片,然後用戶滑動列表),這個時候可能內存使用量就會飆升。

  • 會出現內存抖動(memory churn),因爲短時間開啓了很多線程,完成任務後,這些線程都會被回收。內存表現爲:低-高-低。甚至可能出現OOM。

  • 一個系統所能處理的線程數量是有限的,如果超多了最大承載量,性能會受到很大的影響。而且可能還會影響用戶的後續操作。

這時候Thread Pool線程池的作用就凸顯出來了。

Java爲我們提供了操作線程池的api ThreadPoolExecutor ,ExecutorService是一個接口,相關的線程池的類都實現了該接口,如 ThreadPoolExecutor 。

創建一個線程池可以通過 ThreadPoolExecutor類來實現。

ThreadPoolExecutor pool = new ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue);//新建一個線程池
pool.execute(Runnable);//執行任務

下面是官方對ThreadPoolExecutor的參數說明:

Parameters:
	corePoolSize - the number of threads to keep in the pool, even if they are idle, unless allowCoreThreadTimeOut is set
	maximumPoolSize - the maximum number of threads to allow in the pool
	keepAliveTime - when the number of threads is greater than the core, this is the maximum time that excess idle threads will wait for new tasks before terminating.
	unit - the time unit for the keepAliveTime argument
	workQueue - the queue to use for holding tasks before they are executed. This queue will hold only the Runnable tasks submitted by the execute method.
  • corePoolSize 核心線程數,核心線程會一直存活,即使沒有任務需要處理。當線程數小於核心線程數時,即使現有的線程空閒,線程池也會優先創建新線程來處理任務,而不是直接交給現有的線程處理。核心線程在allowCoreThreadTimeout被設置爲true時會超時退出,默認情況下不會退出。

  • maxPoolSize 線程池允許最大的線程數量。

  • keepAliveTime 當線程空閒時間達到keepAliveTime,該線程會退出,直到線程數量等於corePoolSize。如果allowCoreThreadTimeout設置爲true,則所有線程均會退出直到線程數量爲0。

  • allowCoreThreadTimeout 是否允許核心線程空閒keepAliveTime退出,默認值爲false。

  • workQueue 任務隊列。pool.execute(runnable) 提交的 task 會放到 workQueue。如果當前運行的線程小於 corePoolSize 提交的任務不會放在 workQueue中,而是放在 HashSet workers 中

下面來一個簡單的sample:

public class MyClass {

    private ThreadPoolExecutor pool ;

    private MyClass(){
    	//創建線程池
        pool = new ThreadPoolExecutor(4, 7, 60, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
    }

    public static void main(String[] args) {
        MyClass myClass = new MyClass();
        for (int i = 0; i < 10; i++) {
        	//提交任務
            myClass.pool.execute(new MyRunnable(myClass));
        }
        myClass.pool.shutdown();
    }

    private String getCount() {
        return pool.getCorePoolSize()+"-"+pool.getActiveCount() + "-" + pool.getMaximumPoolSize();
    }

    private static class MyRunnable implements Runnable {
        MyClass myClass;

        MyRunnable(MyClass myClass) {
            this.myClass = myClass;
        }

        @Override
        public void run() {
            System.out.println("thread name:" + Thread.currentThread().getName() + " start " + myClass.getCount());
            try {
            	//模擬耗時任務
                Thread.sleep(3000L);
                System.out.println("thread name:" + Thread.currentThread().getName() + " end " + myClass.getCount());

            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

上面的代碼很簡單:創建了一個corePoolSize爲4,maxPoolSize爲7的線程池。
然後往線程池裏提交10個任務,每個任務打印pool.getCorePoolSize()+"-"+pool.getActiveCount() + “-” + pool.getMaximumPoolSize(),即corePoolSize(核心線程數),activeCount(正在活動的線程總數)和maximumPoolSize(線程池允許的最大線程數)值。

測試結果如下:

thread name:pool-1-thread-1 start 4-2-7
thread name:pool-1-thread-2 start 4-4-7
thread name:pool-1-thread-3 start 4-4-7
thread name:pool-1-thread-4 start 4-4-7
thread name:pool-1-thread-1 end 4-4-7
thread name:pool-1-thread-2 end 4-3-7
thread name:pool-1-thread-4 end 4-4-7
thread name:pool-1-thread-1 start 4-4-7
thread name:pool-1-thread-4 start 4-4-7
thread name:pool-1-thread-2 start 4-4-7
thread name:pool-1-thread-3 end 4-4-7
thread name:pool-1-thread-3 start 4-4-7
thread name:pool-1-thread-2 end 4-4-7
thread name:pool-1-thread-4 end 4-4-7
thread name:pool-1-thread-3 end 4-4-7
thread name:pool-1-thread-4 start 4-4-7
thread name:pool-1-thread-1 end 4-3-7
thread name:pool-1-thread-2 start 4-4-7
thread name:pool-1-thread-4 end 4-2-7
thread name:pool-1-thread-2 end 4-2-7

Process finished with exit code 0

從測試結果來看,我們打印pool.getCorePoolSize()+"-"+pool.getActiveCount() + “-” + pool.getMaximumPoolSize()的值是正常的。但是隻創建了4個線程:

pool-1-thread-1
pool-1-thread-2
pool-1-thread-3
pool-1-thread-4

我們設置了線程池的最大數爲7,我們提交了10個任務,但是爲什麼只創建了corePoolSize=4個線程?

查看官方文檔可以找到答案:

  • 當通過execute(Runnable)提交一個新任務,並且小於corePoolSize正在運行的線程數,將會創建一個新的線程來處理這個任務,不管線程池裏有沒有空閒的線程。

  • If there are more than corePoolSize but less than maximumPoolSize threads running, a new thread will be created only if the queue is full.
    大於corePoolSize小於maximumPoolSize,workQueue隊列滿了,纔會創建新的線程。

  • 如果corePoolSize和maximumPoolSize值設置成一樣的,相當於創建了一個固定數量的線程池。

  • 多數情況下,都是通過構造方法來設置corePoolSize和maximumPoolSize,但是也可以通過setCorePoolSize和setMaximumPoolSize來動態設置。

所以上面的例子,只創建了4個線程,因爲雖然我們提交了10個任務,但是構建workQueue時候沒有傳入隊列大小,默認大小是Integer.MAX_VALUE,所以workQueue是不會滿的。所以最多就創建了4個線程。

據此,我把workQueue隊列容量改成4:

pool = new ThreadPoolExecutor(4, 7, 60, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(4));

測試結果:

thread name:pool-1-thread-1 start 4-2-7
thread name:pool-1-thread-2 start 4-2-7
thread name:pool-1-thread-3 start 4-3-7
thread name:pool-1-thread-4 start 4-4-7
thread name:pool-1-thread-5 start 4-6-7
thread name:pool-1-thread-6 start 4-6-7
thread name:pool-1-thread-1 end 4-6-7
thread name:pool-1-thread-2 end 4-6-7
thread name:pool-1-thread-2 start 4-5-7
thread name:pool-1-thread-1 start 4-6-7
thread name:pool-1-thread-3 end 4-6-7
thread name:pool-1-thread-3 start 4-6-7
thread name:pool-1-thread-4 end 4-6-7
thread name:pool-1-thread-5 end 4-6-7
thread name:pool-1-thread-4 start 4-6-7
thread name:pool-1-thread-6 end 4-5-7
thread name:pool-1-thread-1 end 4-4-7
thread name:pool-1-thread-2 end 4-4-7
thread name:pool-1-thread-3 end 4-2-7
thread name:pool-1-thread-4 end 4-1-7

Process finished with exit code 0

發現創建了6個線程,大於上一次的測試結果(上一次是創建了4個線程),可是我們設置的maximumPoolSize爲7,按道理應該是創建7個線程纔對呀,這是爲什麼呢?

這需要了解下workQueue隊列的策略了。我們上面的列子使用的是 LinkedBlockingQueue。

下面來看看官方文檔對 BlockingQueue的描述:

Any link BlockingQueue may be used to transfer and hold
submitted tasks.  The use of this queue interacts with pool sizing:
If fewer than corePoolSize threads are running, the Executor
always prefers adding a new thread
rather than queuing.

If corePoolSize or more threads are running, the Executor
always prefers queuing a request rather than adding a new
thread.

If a request cannot be queued, a new thread is created unless
this would exceed maximumPoolSize, in which case, the task will be
rejected.

主要意思就是:

  • 如果運行的線程少於 corePoolSize,Executor會創建新線程來執行任務,不會把任務放進queue。

  • 如果運行的線程等於或多於 corePoolSize,Executor將請求加入隊列,而不是創建新的線程。

  • 如果隊列已滿,無法將請求加入隊列,則創建新的線程,除非創建此線程超出 maximumPoolSize,在這種情況下,任務將被拒絕。

這樣就能解釋爲什麼只創建6個線程了。

總共有10個task,核心線程corePoolSize=4,所以3個任務是不會放進queue的,直接創建3個新線程來處理task了,然後再執行execute(Runnable)的時候,就會大於等於corePoolSize,所以就會把接下來的4個任務放進queue(容量爲4),然後就剩下3個task了,發現隊列已經滿了,創建3個線程來處理這剩下的3個task,所以總共只創建6個線程了。

maximumPoolSize=7,我就是想讓它創建7個線程,我們知道了上面的原理就很簡單了,可以把隊列的最大容量改成3或者添加11個任務就可以了:

new LinkedBlockingQueue<Runnable>(3)

for (int i = 0; i < 11; i++) {
    myClass.pool.execute(new MyRunnable(myClass));
}

效果如下:

thread name:pool-1-thread-1 start 0-1-7
thread name:pool-1-thread-2 start 0-2-7
thread name:pool-1-thread-3 start 1-4-7
thread name:pool-1-thread-4 start 4-5-7
thread name:pool-1-thread-5 start 4-7-7
thread name:pool-1-thread-6 start 4-7-7
thread name:pool-1-thread-7 start 4-7-7
.....

Java提供了3種策略的Queue

  • SynchronousQueue 直接傳送task(Direct handoffs)

  • LinkedBlockingQueue 無邊界隊列(Unbounded queues)

  • ArrayBlockingQueue 有邊界隊列(Bounded queues)
    更多詳細信息可以查看官方文檔。

Java給我們提供了一些工廠方法來來創建線程池():

  • Executors.newFixedThreadPool(int threads);

  • Executors.newCachedThreadPool();

  • Executors.newSingleThreadExecutor();

  • Executors.newScheduledThreadPool(int threads);

這些方法都是通過構建ThreadPoolExecutor來實現的,具體的細節可以去看看文檔,如果都不滿足你的需求,可以自己構造ThreadPoolExecutor。

四、IntentService與Service

一般我們在app裏的版本更新邏輯在Service裏起一個線程來檢測。

爲了避免Service一直存在,減少內存消耗,檢測版本後,還需要手動stopSelf,略麻煩。

這時候用IntentService就比較合適了,默認就給你啓動了一個線程來執行耗時操作,完成自動關閉service。

Service和IntentService主要區別:

  • IntentService執行完會自動關閉(stopSelf),而Service不會。

  • IntentService會啓動一個線程來執行耗時操作,把耗時操作放到onHandleIntent(Intent intent)方法裏。而Service需要自己new Thread。

  • 如果調用startService(intent)多次,IntentService會執行多次onHandleIntent(Intent intent),且必須等本次的onHandleIntent(Intent intent)執行完,纔會執行下一次onHandleIntent(Intent intent),說白了就是如果正在執行任務,會把後面啓動的命令放到隊列裏。而多次調用startService(intent),Service僅僅會多次調用onStartCommand方法。

五、避免常見的內存泄露

1、CountDownTimer、TimerTask、Handler導致的內存泄露

在項目中,我們常常可能要做活動倒計時的功能,我是用CountDownTimer來做的。如:

public static class TimeCounter extends CountDownTimer {
	public TimeCounter(long millisInFuture, long countDownInterval) {
	    super(millisInFuture, countDownInterval);
	}

	@Override
	public void onFinish() {
	    //倒計時結束
	}

	@Override
	public void onTick(long millisUntilFinished) {
	    //每間隔固定時間執行一次
	    //在此處理倒計時邏輯
	}
}

如下圖所示:

倒計時
因爲要在TimeCounter內部要修改View的顯示,所以要把TextView傳遞進來,使用WeakReference來引用TextView避免內存泄露,如:

public static class TimeCounter extends CountDownTimer {

	private WeakReference<TextView> weakText;

    public TimeCounter(TextView textView, long millisInFuture, long countDownInterval) {
        super(millisInFuture, countDownInterval);
        weakText = new WeakReference<>(textView);
    }

	@Override
	public void onFinish() {
	    //倒計時結束
	}

	@Override
	public void onTick(long millisUntilFinished) {
	    //每間隔固定時間執行一次
	    //再次處理倒計時邏輯
	}

	private void setText(long millisUntilFinished){
		if (weakText.get() != null) {
			weakText.get().setText(xxx);
		}
	}
}

我們知道,使用WeakReference包裝TextView,在發生GC的時候,TextView就會被回收。

需要注意的是,只有WeakReference所引用的對象沒有被其他對象引用,當發生了GC,WeakReference所引用的對象纔會被回收的。

例如,A界面有個倒計時功能,然後把TextView傳給了上面的TimeCounter,然後在A界面的基礎上啓動了其他界面,這時候假如發生了GC,這時候TextView是不會被回收的。

因爲還有A界面(Activity)對TextView還有引用(強引用)。所以只有把A界面關閉了(也就是Activity對TextView的強引用沒有了),且發生GC了 TextView纔會被收回。也就是說 當一個對象僅僅被弱引用引用的時候, 發生GC該對象纔會被回收

在網上也看到一些,加上了WeakReference在GC的時候也沒有釋放。可能的原因是WeakReference所引用的對象被其他對象強引用着,所以發生GC了,該對象還是沒被回收。

類似這樣的內存泄露除了CountDownTimer還有Timer、Handler 等,表現如下:

Timer

//設置爲每秒執行一次(TimerTask的run方法是在後臺線程執行的)
new Timer().scheduleAtFixedRate(
        new TimerTask() {
            @Override
            public void run() {
                Log.e("Timer", "timer==========");
            }
        }, 0, 1000);

Handler

//設置爲每秒執行一次
handler = new android.os.Handler() {
    @Override
    public void handleMessage(Message msg) {
        super.handleMessage(msg);
        Log.e("Handler", "Handler");
        sendEmptyMessageDelayed(1, 1000);
    }
};
handler.sendEmptyMessage(1);

//不可見時 移除消息
@Override
protected void onStop() {
    super.onStop();
    handler.removeMessages(1);
}
//可見的時 發送消息
@Override
protected void onResume() {
    super.onResume();
    if (!handler.hasMessages(1)) {
        sendEmptyMessageDelayed(1, 1000);
    }
}

上面的 TimerHandler 都可以實現諸如每隔1秒執行的功能,都會導致內存泄露的問題。

解決方案:

  • 使用靜態內部類(相當於外部類,訪問域不一樣),如果需要使用外部類的變量 如View,使用WeakReference引用外部的View;
  • 當界面關閉的時候一定要把定時器 Timer或 CountDownTimer關閉掉(cancel),如果是Handler使用removeMessages(int what)方法;

2、內部類(如Thread)導致的內存泄露

在項目常常要做一些耗時操作,可以起一個Thread來做,如:

new Thread(new Runnable() {
    @Override
    public void run() {
        Log.e("Log", "執行耗時操作");
    }
}).start();

如果在Activity直接這樣使用,容易造成activity的內存泄露。

因爲上面的Thread代碼段,實際上是匿名內部類對象,它會對外部類(Activity)有一個引用。

如果Thread一直在執行,就算用戶按了返回鍵,activity對象會一直存在的。

因爲匿名內部類對象一直引用者activity對象。

是否泄露在於Thread執行的耗時任務執行時間,如果Thread執行非常短時間就完畢了,基本上造成不了多大的內存泄露,但是耗時任務執行的時間無法預測的。

下面一個簡單的例子來演示下這個問題:

public class ThreadActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activty_count_down_timer);
        new Thread(new Runnable() {
            @Override
            public void run() {
                Log.e("ThreadActivity", "執行耗時操作------" + ThreadActivity.this);
                try {
                    //模擬耗時操作60秒
                    Thread.sleep(1000 * 60);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                Log.e("ThreadActivity", "耗時操作執行完成------" + ThreadActivity.this);
            }
        }).start();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        Log.e("ThreadActivity", "onDestroy------");
    }
}

運行起來後,接着按返回鍵,測試效果如下:

執行耗時操作------com.chiclaim.twitter.ThreadActivity@2e92d7ee
onDestroy------
耗時操作執行完成------com.chiclaim.twitter.ThreadActivity@2e92d7ee

從上面的代碼可以看出,耗時任務我們定爲60秒,啓動了Activity後,立馬輸出了:

執行耗時操作------com.chiclaim.twitter.ThreadActivity@2e92d7ee

緊接着按了返回鍵,輸出了:

onDestroy------

過了大概60秒,輸出了:

耗時操作執行完成------com.chiclaim.twitter.ThreadActivity@2e92d7ee

從上面的例子中我們得出了2個結論:

  • 匿名內部類對象,它會對外部類有一個引用。
  • Activity執行了onDestroy方法,不見得Activity被銷燬了。上面的例子中,過了60秒依然可以輸出外部類(Activity)對象。

解決方案:

  • 儘量減少非靜態內部類的使用,上面的例子可以使用靜態匿名內部類對象或者使用靜態內部類,這樣就不會對外部類對象由引用了。
  • 在app中的這種耗時任務,一般我們處理邏輯的時候,只需要處理成功失敗的情況,比如說網絡請求,如果一個界面有多個請求,那麼就會有很多內部類(回調)嵌套了,我的做法是使用者實現一個回調接口,該接口定義了成功和失敗的兩個方法,Activity onCreate的時候,把回調註冊到一個容器內,在onDestroy方法裏從容器中移除該回調。這樣也不會對Activity造成內存泄露。
  • 另外,如果某個耗時操作,需要傳入Context,如果沒有特殊的要求,不要傳遞Activity的Context。傳入Application級別的Context,這樣不管耗時操作執行多久,都不會導致Activity內存泄露。

3、由static的Activity、View、List導致的內存泄露

千萬不要是有static來修飾activity、View對象,這種內存泄露更加嚴重,因爲它將貫穿程序的生命週期。
爲了更好的管理Activity常常把Activity放進容器中,如Stack。如果忘記了移除,也會造成內存泄漏。

六、onTrimMemory(int level)與onLowMemory()

onTrimMemory 回調是 Android 4.0 之後提供的一個API。

它的主要用來提示開發者在系統內存不足的時候,根據當前內存情況(level),釋放相關資源以減小系統內存壓力,這樣可以減少app進程被系統殺死的可能性。

儘可能的保存app進程,等到用戶在下一次使用的時候,啓動速度就會比較快。

在Android 4.0之前都是onLowMemory來處理這類邏輯的,onLowMemory和onTrimMemory中的TRIM_MEMORY_COMPLETE級別相同。如果想兼容Android4.0之前的系統可以實現該方法,否則只需要處理onTrimMemory方法。

下面來聊一聊onTrimMemory(int level)回調level的常量:

TRIM_MEMORY_UI_HIDDEN = 20 表示應用程序的所有UI界面被隱藏了,即用戶點擊了Home鍵或者剩下最後一個界面,按返回鍵後,Application的onTrimMemory回調也會調用。

下面三個等級是當我們的應用程序正在前臺運行時的回調:

  • TRIM_MEMORY_RUNNING_MODERATE = 5 表示應用程序正常運行,並且不會被殺掉。但是目前手機的內存已經有點低了,你的正在運行的進程需要釋放一些不需要的內存資源。

  • TRIM_MEMORY_RUNNING_LOW = 10 表示應用程序正常運行,並且不會被殺掉。但是目前手機的內存已經非常低了,你的正在運行的進程需要釋放一些不需要的內存資源。

  • TRIM_MEMORY_RUNNING_CRITICAL = 15 表示應用程序仍然正常運行,但是系統內存已經極度低了,即將不能保留任何後臺進程 了。這個時候我們應當儘可能地去釋放任何不必要的資源,下一步onLowMemory將會被調用,這樣的話,後臺將不會保留任何進程。

當app進程不可見處於LRU list中,則會收到以下常量的回調:

  • TRIM_MEMORY_BACKGROUND = 40 app進程不可見,處於LRU列表中,這時候是個釋放資源的好時機。

  • TRIM_MEMORY_MODERATE = 60 系統目前內存已經很低了,並且我們的程序處於LRU緩存列表的中間位置。騰出一些內存讓系統運行其他的進程。

  • TRIM_MEMORY_COMPLETE = 80 系統目前內存已經很低了,並且我們的程序處於LRU緩存列表的最邊緣位置,如果系統找不到更多可能的內存,我們的app進程很快將被殺死。

從上面可以看出,這些常量大致可以分爲兩類,一類是大於TRIM_MEMORY_UI_HIDDEN = 20,這類表示進程不可見。一類是小於TRIM_MEMORY_UI_HIDDEN = 20,這類表示app進程正在前臺運行。並且常量值越大,說明系統內存越緊張。

下面是官方推薦實現方案

/**
* Release memory when the UI becomes hidden or when system resources become low.
* @param level the memory-related event that was raised.
*/
public void onTrimMemory(int level) {

// Determine which lifecycle or system event was raised.
switch (level) {

    case ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN:
        /*
           Release any UI objects that currently hold memory.

           The user interface has moved to the background.
        */
        break;

    case ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE:
    case ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW:
    case ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL:
        /*
           Release any memory that your app doesn't need to run.

           The device is running low on memory while the app is running.
           The event raised indicates the severity of the memory-related event.
           If the event is TRIM_MEMORY_RUNNING_CRITICAL, then the system will
           begin killing background processes.
        */
        break;

    case ComponentCallbacks2.TRIM_MEMORY_BACKGROUND:
    case ComponentCallbacks2.TRIM_MEMORY_MODERATE:
    case ComponentCallbacks2.TRIM_MEMORY_COMPLETE:
        /*
           Release as much memory as the process can.

           The app is on the LRU list and the system is running low on memory.
           The event raised indicates where the app sits within the LRU list.
           If the event is TRIM_MEMORY_COMPLETE, the process will be one of
           the first to be terminated.
        */
        break;

    default:
        /*
          Release any non-critical data structures.

          The app received an unrecognized memory level value
          from the system. Treat this as a generic low-memory message.
        */
        break;
}

如何針對上面的這些常量,分別釋放app裏哪些資源呢?(目前我只處理app在後臺的情況)

當回調的參數level=TRIM_MEMORY_BACKGROUND(40)或TRIM_MEMORY_MODERATE(60)或TRIM_MEMORY_COMPLETE(80)時,

  • 把圖片佔用的內存釋放掉。

  • 清空緩存數據,例如列表中List的數據; 還可以把動態創建的View或fragment釋放掉, 甚至activity的相關資源都釋放掉變成空Activity,從新回到這個Activity的時候,重新初始化數據。

  • 我會把所有的activity移除掉,僅留下主Activity。當然如果主Activity比較複雜,佔用的資源比較多,可以把資源都釋放掉,留下一個空主Activity。當用戶回來的時候可以迅速回來,如果內容清空了,重新加載即可。

相關注意事項:

  • 在回調中釋放內存,要注意該要釋放的界面是否可見,界面如果正在顯示,你卻把當前的界面的List數據清空了,這樣顯然是不妥的,系統會通知所有實現了onTrimMemory方法的相關組件(Application、Activity、Fragment、Service、ContentProvider)

  • 還要注意,做好相關數據恢復的邏輯,例如,把相關的數據清空了,該界面重新顯示的時候,要把相關釋放的數據,重新加載,如果可以的話,儘可能回到用戶上一次操作的狀態,例如滑動的位置,所填寫的數據等。


下面是我的公衆號,乾貨文章不錯過,有需要的可以關注下,有任何問題可以聯繫我:
公衆號:  chiclaim

參考鏈接

http://blog.csdn.net/zhouhl_cn/article/details/7392607

https://www.youtube.com/watch?v=uCmHoEY1iTM&list=PLWz5rJ2EKKc9CBxr3BVjPTPoDPLdPIFCE&index=6

https://www.youtube.com/watch?annotation_id=annotation_2743589091&feature=iv&src_vid=uCmHoEY1iTM&v=NwFXVsM15Co

http://stackoverflow.com/questions/15524280/service-vs-intentservice

http://www.codeceo.com/article/android-ontrimmemory-mem.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章