Android內(nèi)存優(yōu)化一:java垃圾回收機制
Android內(nèi)存優(yōu)化二:內(nèi)存泄漏
Android內(nèi)存優(yōu)化三:內(nèi)存泄漏檢測與監(jiān)控
Android內(nèi)存優(yōu)化四:OOM
Android內(nèi)存優(yōu)化五:Bitmap優(yōu)化
內(nèi)存泄漏
內(nèi)存泄漏指的是不需要的對象無法被回收。
其本質(zhì)是對象生命周期的不一致性導(dǎo)致的,即生命周期較長的對象持有生命周期較短的對象的引用。
對象回收時機:
- 根據(jù)Java垃圾回收機制,Java 虛擬機使用可達(dá)性分析算法來判斷對象是否可被回收,以 GC Roots 為起始點進(jìn)行搜索,可達(dá)的對象都是存活的,不可達(dá)的對象可被回收。
- 當(dāng)一個不需要的對象,仍然可達(dá)(從GC Roots到此對象具有完整的引用鏈),則無法被回收,導(dǎo)致此對象仍然占用著內(nèi)存,就發(fā)生了內(nèi)存泄漏
- 根據(jù)Java垃圾回收機制,判定對象是否可被回收與引用有關(guān)。
- 被強引用關(guān)聯(lián)的對象不會被回收。
- 被軟引用關(guān)聯(lián)的對象只有在內(nèi)存不夠的情況下才會被回收。
- 被弱引用關(guān)聯(lián)的對象一定會被回收,也就是說它只能存活到下一次垃圾回收發(fā)生之前。
- 被虛引用關(guān)聯(lián)的對象,不會對其生存時間造成影響,也無法通過虛引用得到一個對象。
注:通過軟引用可以解決大部分內(nèi)存泄漏問題。只有當(dāng)對象只被軟引用關(guān)聯(lián)時,gc發(fā)生時,才會被回收;如果對象同時被強引用和弱引用關(guān)聯(lián),則無法被回收。
類型
內(nèi)部類
內(nèi)部類(包括匿名內(nèi)部類)會隱式持有外部類對象的引用
/**
* 外部類
*/
public class Outer {
private int count;
private Inner inner = new Inner();
public void doAction() {
inner.doSomething();
}
/**
* 內(nèi)部類
*/
private class Inner {
public void doSomething() {
count++;
}
}
}
編譯后的字節(jié)碼
class Outer$Inner
{
private Outer$Inner(Outer paramOuter) {}
public void doSomething()
{
Outer.access$108(this.this$0);
}
}
- 可以看到,編譯器為內(nèi)部類單獨生成了一個.class文件,構(gòu)造函數(shù)被定義為私有的,且傳入了外部類的實例,所以它持有了外部類實例的強引用,也因此,內(nèi)部類可以調(diào)用到外部類的方法和屬性。
- 當(dāng)內(nèi)部類生命周期長于外部類時,比如外部類為Activity,內(nèi)部類為Thread,當(dāng)Activity退出時,Thread仍未執(zhí)行完,導(dǎo)致Activity仍被Thread強引用著無法被回收而出現(xiàn)了內(nèi)存泄漏
解決方法
解決方法是使用靜態(tài)內(nèi)部類 + 弱引用,靜態(tài)內(nèi)部類編譯后的字節(jié)碼文件,編譯器并沒有為它添加額外的構(gòu)造函數(shù),所以它其實和我們的外部類沒有任何關(guān)系,這是寫在同一個.java源文件中而已。
- 由于靜態(tài)內(nèi)部類未持有外部類引用,所以無法調(diào)用外部類的方法及屬性,解決方式是將外部類通過弱引用傳遞給靜態(tài)內(nèi)部類,當(dāng)外部類未被強引用時,則GC的時候,外部類會被回收,此時通過弱引用取出外部類的引用則為null
/**
* 外部類
*/
public class Outer {
private int count;
private Inner inner = new Inner();
public void doAction() {
inner.doSomething();
}
/**
* 靜態(tài)內(nèi)部類
*/
private static class Inner {
WeakReference<Outer> ref;
Inner(Outer outer){
ref = new WeakReference<Outer>(outer);
}
public void doSomething() {
// 由于靜態(tài)內(nèi)部類未持有外部類引用,所以無法調(diào)用外部類的方法及屬性
// 這里通過弱引用獲取到外部類的引用
Outer outer = ref.get();
// 當(dāng)外部類被回收時,取出的引用為null
if (outer != null){
outer.count++;
}
}
}
}
Handler
通過handler發(fā)送一個延遲消息,由于匿名內(nèi)部類Runnable持有了外部類的引用,且其生命周期大于外部類,所以造成了內(nèi)存泄漏。
public void post(){
Handler handler = new Handler();
handler.postDelayed(new Runnable() {
@Override
public void run() {
// todo
}
},500);
}
通過handler發(fā)送非延遲消息,也會造成內(nèi)存泄漏。
這是因為消息會插入到消息隊列MessageQueue中,如果在這個消息之前有其他消息產(chǎn)生阻塞,或者存在同步屏障,會導(dǎo)致這個消息延遲執(zhí)行,所以其生命周期同樣會大于外部類。
public void post(){
Handler handler = new Handler();
handler.post(new Runnable() {
@Override
public void run() {
// todo
}
});
}
同樣,如下這種方式也會產(chǎn)生泄漏,這里的new Handler()創(chuàng)建的也是一個匿名內(nèi)部類
public void post(){
Handler handler = new Handler(){
@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
// todo
}
};
Message msg = Message.obtain();
msg.what = 1;
handler.sendMessage(msg);
}
引用鏈
- 通過post調(diào)用,會將runnable存入創(chuàng)建的Message中,使Message持有runnable
// Handler
public final boolean post(@NonNull Runnable r) {
return sendMessageDelayed(getPostMessage(r), 0);
}
private static Message getPostMessage(Runnable r) {
Message m = Message.obtain();
m.callback = r;
return m;
}
當(dāng)發(fā)送一個Message,最終都會調(diào)用enqueueMessage將Message插入MessageQueue中
同時將當(dāng)前handler對象賦值給Message的target變量,使Message持有handler
// Handler
private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
long uptimeMillis) {
// 這里的this為Handler
msg.target = this;
msg.workSourceUid = ThreadLocalWorkSource.getUid();
if (mAsynchronous) {
msg.setAsynchronous(true);
}
return queue.enqueueMessage(msg, uptimeMillis);
}
- MessageQueue是一個單向鏈表,在Looper初始化使創(chuàng)建
// Looper
private Looper(boolean quitAllowed) {
mQueue = new MessageQueue(quitAllowed);
mThread = Thread.currentThread();
}
- Looper用于為線程運行消息循環(huán)的類,一個線程只有一個Looper,主線程的Loop在ActivityThread中創(chuàng)建,并開啟循環(huán),作用于整個應(yīng)用的生命周期中。
// ActivityThread
public static void main(String[] args) {
....
// 創(chuàng)建主線程的Looper
Looper.prepareMainLooper();
// 開啟循環(huán)
Looper.loop();
....
}
引用鏈為:
Looper(****GC Roots****) -> MessageQueue -> Message -> Runnable(或Handler) -> Activity(外部類)
所以插入到MessageQueue中的Message如果還未得到執(zhí)行,其生命周期相當(dāng)于整個應(yīng)用的生命周期,且其間接持有了外部類的引用,所以造成了外部類的泄漏
解決方法
使用靜態(tài)內(nèi)部類+弱引用的方式 可以解決Handler造成外部類泄漏的問題,但是并沒有從根本上解決泄漏,因為MessageQueue仍然持有Message,仍會造成Message的泄漏。
可以如下方式移除當(dāng)前handler創(chuàng)建的Message
handler.removeCallbacksAndMessages(null);
靜態(tài)變量
靜態(tài)變量導(dǎo)致內(nèi)存泄漏的原因是因為短生命周期對象被聲明成為長生命周期對象,靜態(tài)變量是GC Roots的一種
注
- 靜態(tài)變量是屬于類的不是屬于實例對象的,存儲在方法區(qū)中
- 方法區(qū)主要存放永久代對象,而永久代對象的回收率比新生代低很多,所以在方法區(qū)上進(jìn)行回收性價比不高,主要是對常量池的回收和對類的卸載。
類的卸載條件很多,需要滿足以下三個條件,并且滿足了條件也不一定會被卸載:
該類所有的實例都已經(jīng)被回收,此時堆中不存在該類的任何實例。
加載該類的 ClassLoader 已經(jīng)被回收。
該類對應(yīng)的 Class 對象沒有在任何地方被引用,也就無法在任何地方通過反射訪問該類方法。
所以一般來說,靜態(tài)變量很少會被GC回收的
比如:
private static Context context;
當(dāng)將Activity賦值給靜態(tài)的context變量時,Activity成為了長生命周期對象GC Roots,而Activity 的生命周期可能很短,用戶一打開就關(guān)閉了。導(dǎo)致Activity無法被回收而產(chǎn)生泄漏
解決方法
如果需要靜態(tài) Context, 可以考慮使用 ApplicationContext,ApplicationContext的生命周期為整個應(yīng)用的生命周期,不會產(chǎn)生泄漏問題。
單例模式
單例模式產(chǎn)生泄漏的原因是因為長生命周期對象持有了短生命周期對象的引用
比如:
final class SingleTon{
private static SingleTon instance;
private Context context;
}
單例對象實際是一個靜態(tài)變量,持有了context的引用,當(dāng)context為Activity時,導(dǎo)致Activity無法被回收而產(chǎn)生泄漏
解決方法
如果需要單例需要引用 Context, 可以考慮使用 ApplicationContext,ApplicationContext的生命周期為整個應(yīng)用的生命周期,不會產(chǎn)生泄漏問題。
資源未釋放
忘了注銷 BroadcastReceiver
忘了關(guān)閉數(shù)據(jù)庫游標(biāo)(Cursor)
忘了關(guān)閉流
忘了調(diào)用 recycle() 方法回收創(chuàng)建的 Bitmap 使用的內(nèi)存
忘了在 Activity 退出時取消 RxJava 或協(xié)程所開啟異步任務(wù)
Webview
不同的 Android 版本的 Webview 會有差異,加上不同廠商定制 ROM 的 Webview 的差異,導(dǎo)致 Webview 存在很大的兼容問題,一般情況下,在應(yīng)用中只要使用一次 Webview,它占用的內(nèi)存就不會被釋放,解決方案:WebView內(nèi)存泄漏--解決方法小結(jié) 。